Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for Pillow Image Object in toga #2154

Closed
wants to merge 11 commits into from

Conversation

dr-gavitron
Copy link
Contributor

@dr-gavitron dr-gavitron commented Oct 14, 2023

Added a new parameter called src in toga.Image that takes any supported image sources

supported image sources: str, pathlib.Path, bytes, BytesIO, BufferedReader, PIL.Image.Image
type of the src is detected using isinstance. Then it is converted to either path or data. Rest of the code is untouched.
Only an overhead is added for the sake of users.

Added support for Pillow Image Object in toga.Image.

toga.Image can take PIL.Image.Image as its source. Internally PIL.Image.Image is saved to a BytesIO using its save method.
then from the BytesIO, bytes are extracted and passed to data parameter.

Added support from PIL.Image in toga.ImageView

a type checking occurs in init which on detect of PIL.Image.Image, converts it to toga.Image by passing it to the same.

implemented toga.Image.as_format

has a parameter format which when not set(i.e. None) returns the same toga.Image object from which it is being called. But when format is set to PIL.Image.Image, it will convert the image by following mean:

  • Saving it to a tempfile in disk using save method of self.
  • Loading the tempfile as Pillow Object by calling PIL_Image.open
  • Deleting the tempfile
  • returning the pillow object

implemented toga.ImageView.as_image

has a parameter called format. which if None returns toga.Image else if set to PIL_Image.Image, will return Pillow object. Internally calls toga.Image.as_format

modified Canvas.as_image to support PIL.Image return

has parameter format which if none returns toga.Image. if set to PIL_Image.Image return an object of same but this time uses PIL_Image.open(BytesIO) unlike toga.Image.as_format which uses saving temp file to disk. The BytesIO for PIL_Image.open is constructed from self._impl.get_image_data which returns raw bytes.

toga.Image can take any supported image sources as its first parameter #2156
Allow toga to work with Pillow image files #2142

Fixes #2142, Fixes #2156

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

@dr-gavitron dr-gavitron changed the title Fixed #2142 Fixed #2142: Allow toga to work with Pillow image files Oct 14, 2023
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments inline, in addition to the ones on the main ticket about API design.

Also - this PR will need tests, plus a changenote.

core/src/toga/images.py Outdated Show resolved Hide resolved
core/src/toga/images.py Outdated Show resolved Hide resolved
core/src/toga/images.py Outdated Show resolved Hide resolved
core/src/toga/images.py Outdated Show resolved Hide resolved
core/src/toga/images.py Outdated Show resolved Hide resolved
core/src/toga/widgets/imageview.py Outdated Show resolved Hide resolved
@dr-gavitron
Copy link
Contributor Author

I will resolve them as you have said. Sorry for the silly mistakes.. Indeed this is my first GitHub contribution.

@dr-gavitron
Copy link
Contributor Author

How to test this features? And how to add a changenote?

@freakboy3742
Copy link
Member

See the contribution guide for details on writing a change note.

As for tests - as the changes are all in the core API, you only need to add tests for the core API. The tests are all Pytest test cases; you need to add tests that validate the new code paths you've added - that is, instantiating Image instances using PIL objects. You'll be looking in the following files:

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good first pass at tests; but a few notes inline.

changes/2142.feature.rst Outdated Show resolved Hide resolved
core/tests/test_images.py Outdated Show resolved Hide resolved
core/tests/test_images.py Outdated Show resolved Hide resolved
core/tests/test_images.py Outdated Show resolved Hide resolved
core/tests/widgets/canvas/test_canvas.py Outdated Show resolved Hide resolved
core/tests/widgets/test_imageview.py Outdated Show resolved Hide resolved
def test_as_image_format_is_none():
img = toga.Image("resources/toga.png")
imageview = toga.ImageView(image=img)
img2 = imageview.as_image()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with Image, this should be an argument error.

wrote tests for unified src
Minor changes to code for supporting pillow
Fixes beeware#2156
@dr-gavitron dr-gavitron changed the title Fixed #2142: Allow toga to work with Pillow image files Allow toga to work with Pillow image files Oct 16, 2023
@dr-gavitron dr-gavitron changed the title Allow toga to work with Pillow image files Adding support for Pillow Image Object in toga Oct 16, 2023
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more suggestions inline.

I also strongly suggest you read the rest of the contribution guide I've linked previously - it contains details like how to run the test suite locally, and how to set up pre-commit so that code formatting issues will be corrected before pushing to CI.

core/src/toga/images.py Outdated Show resolved Hide resolved
core/src/toga/images.py Outdated Show resolved Hide resolved
if path is not None:
if isinstance(path, Path):
self.path = path
else:
self.path = Path(path)
self.data = None
else:
self.path = None
if data is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fairly certain this leaves open a path toga.Image() won't raise an error. The test suite should catch this... but the test suite isn't currently running, because of other problems with the test suite.

self.path = toga.App.app.paths.app / self.path
if not self.path.is_file():
raise FileNotFoundError(f"Image file {self.path} does not exist")
self._impl = self.factory.Image(interface=self, path=self.path)




Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be 2 lines between classes. This is picked up automatically when you install pre-commit.

if format==None:
return self
elif isinstance(format, type(self)):
return self
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me why as_format() shouldn't require a single argument - the format you want the image in.

It's also not clear to me why as_format(toga.Image) is something anyone would ever invoke... on toga.Image.

if format == None:
return Image(data=self._impl.get_image_data())
elif isinstance(format, toga.Image):
return Image(data=self._impl.get_image_data())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effectively means that format has a default value of toga.Image. This simplifies the type description, and simplifies the implementation because there's no duplicated code path.

@@ -63,7 +70,7 @@ def rehint_imageview(image, style, scale=1):
class ImageView(Widget):
def __init__(
self,
image: Image | Path | str | None = None,
image: Image | Path | str | Any | None = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're using Any, there's no need to specify anything else, because any type is a subset of Any.

@@ -79,6 +86,8 @@ def __init__(
# Prime the image attribute
self._image = None
self._impl = self.factory.ImageView(interface=self)
if PIL_Image!=None and PIL_Image.isImageType(image):
image = Image(image) #PIL.Image.Image -> toga.Image
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said in my previous review - this isn't needed. imageview.image = image calls the setter, and if the argument isn't None or a toga.Image(), a toga.Image is constructed. We can completely defer all type checks to the toga.Image() constructor.

if format==None:
return self.image
elif isinstance(format, Image):
return self.image
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If two branches have the same implementation, perhaps consider whether an or is needed.

core/tests/test_images.py Show resolved Hide resolved
@freakboy3742
Copy link
Member

Closing; superseded by #2231

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants