From 134de64ba93c6e07d6e7369da745c4f514f7b79e Mon Sep 17 00:00:00 2001 From: Sandipan Chowdhury Date: Sat, 14 Oct 2023 21:53:53 +0530 Subject: [PATCH 01/11] Added PIL.Image support in toga.Image --- core/src/toga/images.py | 80 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index b072dd2124..b9b33fa96f 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -12,23 +12,98 @@ def __init__( path: str | None | Path = None, *, data: bytes | None = None, + pil_image: PIL.Image | None = None, ): """Create a new image. - An image must be provided either a ``path`` or ``data``, but not both. + An image must be one of ``path``, ``data`` or ``pil_image`` :param path: Path to the image to load. This can be specified as a string, or as a :any:`pathlib.Path` object. The path can be an absolute file system path, or a path relative to the module that defines your Toga application class. :param data: A bytes object with the contents of an image in a supported format. + :param pil_image: PIL.Image object created from an image of a supported format. :raises FileNotFoundError: If a path is provided, but that path does not exist. :raises ValueError: If the path or data cannot be loaded as an image. + :raises TypeError: If pil_image is provided but the type of the object is not PIL.Image """ + #--patch-start:@github/dr-gavitron + # At first we will create a list with these three variable, then count how many None is in that list. + # If the number of None is 1 -> raises ValueError. One and only One of the three variables must be set but here two of them are set + # If the number of None is 3 -> raises ValueError. One and only One of the three variables must be set but here none of them are set + # If the number of None is 2 -> Ok. Check the validity of the value of that non-None variable + none_count = [path, data, pil_image].count(None) + if none_count != 2: + raise ValueError("One and Only one of the three args (path, data, pil_image) must be set.") + # checking validity of the arg(one of the three) + if path is not None: + if isinstance(path, Path): + self.path = path + else: + self.path = Path(path) + self.data = None + self.pil_image = None + if data is not None: + self.data = data + self.path = None + self.pil_image = None + if pil_image is not None: + try: + from PIL import Image as _PIL_Image_Module_ + except ImportError as e: + print("Pillow is required as pil_image is set to a PIL.Image type") + raise ImportError(e) + #type checking + if not _PIL_Image_Module_.isImageType(pil_image): + raise TypeError("pil_image is not a PIL.Image type.") + self.pil_image = pil_image + self.data = None + self.path = None + + + self.factory = get_platform_factory() + if self.data is not None: + self._impl = self.factory.Image(interface=self, data=self.data) + if self.path is not None: + 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) + if self.pil_image is not None: + #What we will do: save the pil_image into a temporary file then load raw byte data + #from that file, then use the bytes to create self._impl, finally delete the temp file + + # creating an unique name for the temporary file + from time import time as _time_time + import os as _os + unique_time = str(_time_time()) + temp_file = f"_PIL_temp_img_file_{unique_time}_" + temp_file_path = toga.App.app.paths.app + while temp_file in _os.listdir(temp_file_path): + temp_file+="1_" + complete_temp_path = _os.path.join(temp_file_path, temp_file) + + # saving the pil_image in the temporary file + self.pil_image.save(complete_temp_path) + + #loading the raw bytes of the image into the memory from the temporary file + with open(complete_temp_path, "rb") as _file: + raw_image_data = _file.read() + + #passing the bytes of the image to create _impl + self._impl = self.factory.Image(interface=self, data=raw_image_data) + + #finally delete the temporary file from the storage + _os.remove(temp_file_path) + + + + #--patch-end + ''' if path is None and data is None: raise ValueError("Either path or data must be set.") if path is not None and data is not None: raise ValueError("Only either path or data can be set.") - if path is not None: if isinstance(path, Path): self.path = path @@ -47,6 +122,7 @@ def __init__( 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) + ''' @property def width(self) -> int: From 811919a5a15e11f5820192368fb8413da2296861 Mon Sep 17 00:00:00 2001 From: Sandipan Chowdhury Date: Sat, 14 Oct 2023 23:21:23 +0530 Subject: [PATCH 02/11] modified imageview to support PIL.Image --- core/src/toga/images.py | 39 +++++++++++++++++++++++++----- core/src/toga/widgets/imageview.py | 21 +++++++++++++++- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index b9b33fa96f..cf3d82aabe 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -5,6 +5,8 @@ import toga from toga.platform import get_platform_factory +import time +import os class Image: def __init__( @@ -74,14 +76,12 @@ def __init__( #from that file, then use the bytes to create self._impl, finally delete the temp file # creating an unique name for the temporary file - from time import time as _time_time - import os as _os - unique_time = str(_time_time()) + unique_time = str(time.time()) temp_file = f"_PIL_temp_img_file_{unique_time}_" temp_file_path = toga.App.app.paths.app - while temp_file in _os.listdir(temp_file_path): + while temp_file in os.listdir(temp_file_path): temp_file+="1_" - complete_temp_path = _os.path.join(temp_file_path, temp_file) + complete_temp_path = os.path.join(temp_file_path, temp_file) # saving the pil_image in the temporary file self.pil_image.save(complete_temp_path) @@ -94,7 +94,7 @@ def __init__( self._impl = self.factory.Image(interface=self, data=raw_image_data) #finally delete the temporary file from the storage - _os.remove(temp_file_path) + os.remove(temp_file_path) @@ -143,3 +143,30 @@ def save(self, path: str | Path): :param path: Path where to save the image. """ self._impl.save(path) + + def as_format(self, format: PIL.Image): + if format == None: + return self + elif format.__name__ == "PIL.Image": + # save the image in storage in a temporary file + # then load it as PIL.Image then delete the temporary file + + # creating an unique name for the temporary file + unique_time = str(time.time()) + temp_file = f"_TOGA_temp_img_file_{unique_time}_" + temp_file_path = toga.App.app.paths.app + while temp_file in os.listdir(temp_file_path): + temp_file+="1_" + complete_temp_path = os.path.join(temp_file_path, temp_file) + + # loading PIL Image + from PIL import Image as _PIL_Image_ + pil_image = _PIL_Image_.open(complete_temp_path) + + # deleting the temporary file + os.remove(complete_temp_path) + + return pil_image + else: + raise TypeError("Unknown Conversion Format") + diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 43be629195..b5de6f7ecc 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -63,7 +63,7 @@ def rehint_imageview(image, style, scale=1): class ImageView(Widget): def __init__( self, - image: Image | Path | str | None = None, + image: Image | Path | str | PIL.Image | None = None, id=None, style=None, ): @@ -79,6 +79,16 @@ def __init__( # Prime the image attribute self._image = None self._impl = self.factory.ImageView(interface=self) + # checking if the image if PIL.Image + try: + from PIL import Image as _PIL_Image_Module_ + if _PIL_Image_Module_.isImageType(image): + image = Image(pil_image=image) #TODO: A change may be necessary if behaviour of Image is changed + except ImportError as e: + # cant import... assume that PIL.Image will not be passed + pass + + self.image = image @property @@ -124,3 +134,12 @@ def image(self, image): self._impl.set_image(self._image) self.refresh() + + def as_image(self, format: PIL.Image=None): + if format == None: + return self.image + elif format.__name__ == "PIL.Image": + import PIL as _PIL_ + return self.image.as_format(_PIL_.Image) + else: + return TypeError("Unknown Conversion Format") From 681118ecccf143b0c1b2f67ac7f60056e503a13e Mon Sep 17 00:00:00 2001 From: Sandipan Chowdhury Date: Sat, 14 Oct 2023 23:53:27 +0530 Subject: [PATCH 03/11] updated canvas --- core/src/toga/images.py | 2 +- core/src/toga/widgets/canvas.py | 13 ++++++++++--- core/src/toga/widgets/imageview.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index cf3d82aabe..c89acb8b32 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -144,7 +144,7 @@ def save(self, path: str | Path): """ self._impl.save(path) - def as_format(self, format: PIL.Image): + def as_format(self, format: PIL.Image|None=None): if format == None: return self elif format.__name__ == "PIL.Image": diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 6592862416..d5c307cab4 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1445,11 +1445,18 @@ def measure_text( # As image ########################################################################### - def as_image(self) -> toga.Image: + def as_image(self, format: PIL.Image | None = None) -> toga.Image: """Render the canvas as an Image. - :returns: A :class:`toga.Image` containing the canvas content.""" - return Image(data=self._impl.get_image_data()) + :returns: A :class:`toga.Image` containing the canvas content if format is None else `PIL.Image` containing the canvas content if format is specified as PIL.Image.""" + if format == None: + return Image(data=self._impl.get_image_data()) + elif format.__name__ == "PIL.Image": + from io import BytesIO as _BytesIO_ + from PIL import Image as _PIL_Image_ + return _PIL_Image_.open(_BytesIO_(self._impl.get_image_data())) + else: + raise TypeError(f"Unsupported Format") ########################################################################### # 2023-07 Backwards compatibility diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index b5de6f7ecc..68464a4c1a 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -135,7 +135,7 @@ def image(self, image): self._impl.set_image(self._image) self.refresh() - def as_image(self, format: PIL.Image=None): + def as_image(self, format: PIL.Image | None=None): if format == None: return self.image elif format.__name__ == "PIL.Image": From 1bf96da0419f28146f3f6f07b7e7fe478315c018 Mon Sep 17 00:00:00 2001 From: Sandipan Chowdhury Date: Sun, 15 Oct 2023 00:04:11 +0530 Subject: [PATCH 04/11] Changes to images.py --- core/src/toga/images.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index c89acb8b32..e6a7a0665d 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -99,6 +99,8 @@ def __init__( #--patch-end + + #--original ''' if path is None and data is None: raise ValueError("Either path or data must be set.") From 1ac2e967dc5888f34a2ed3afb30c14384540f92c Mon Sep 17 00:00:00 2001 From: Sandipan Chowdhury Date: Sun, 15 Oct 2023 00:44:24 +0530 Subject: [PATCH 05/11] Used Any insted of PIL.Image for type annotations --- core/src/toga/images.py | 8 +++++--- core/src/toga/widgets/canvas.py | 5 +++-- core/src/toga/widgets/imageview.py | 6 ++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index e6a7a0665d..e99ffeafdd 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from pathlib import Path import toga @@ -14,7 +16,7 @@ def __init__( path: str | None | Path = None, *, data: bytes | None = None, - pil_image: PIL.Image | None = None, + pil_image: Any | None = None, ): """Create a new image. @@ -99,7 +101,7 @@ def __init__( #--patch-end - + #--original ''' if path is None and data is None: @@ -146,7 +148,7 @@ def save(self, path: str | Path): """ self._impl.save(path) - def as_format(self, format: PIL.Image|None=None): + def as_format(self, format: Any|None=None): if format == None: return self elif format.__name__ == "PIL.Image": diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index d5c307cab4..9c1c335c45 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from contextlib import contextmanager from math import pi -from typing import Protocol +from typing import Protocol, Any from travertino.colors import Color @@ -1445,7 +1445,8 @@ def measure_text( # As image ########################################################################### - def as_image(self, format: PIL.Image | None = None) -> toga.Image: + def as_image(self, format: Any | None = None) -> toga.Image: + """Render the canvas as an Image. :returns: A :class:`toga.Image` containing the canvas content if format is None else `PIL.Image` containing the canvas content if format is specified as PIL.Image.""" diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 68464a4c1a..ff7eff3609 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -2,6 +2,8 @@ from pathlib import Path +from typing import Any + from travertino.size import at_least from toga.images import Image @@ -63,7 +65,7 @@ def rehint_imageview(image, style, scale=1): class ImageView(Widget): def __init__( self, - image: Image | Path | str | PIL.Image | None = None, + image: Image | Path | str | Any | None = None, id=None, style=None, ): @@ -135,7 +137,7 @@ def image(self, image): self._impl.set_image(self._image) self.refresh() - def as_image(self, format: PIL.Image | None=None): + def as_image(self, format: Any | None=None): if format == None: return self.image elif format.__name__ == "PIL.Image": From 7209f5fae7528504663afdef16e7ecdc3c02aded Mon Sep 17 00:00:00 2001 From: dr-gavitron Date: Sun, 15 Oct 2023 01:18:56 +0530 Subject: [PATCH 06/11] removed unnecessary codes --- core/src/toga/images.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index e99ffeafdd..885f591f19 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -31,7 +31,6 @@ def __init__( :raises ValueError: If the path or data cannot be loaded as an image. :raises TypeError: If pil_image is provided but the type of the object is not PIL.Image """ - #--patch-start:@github/dr-gavitron # At first we will create a list with these three variable, then count how many None is in that list. # If the number of None is 1 -> raises ValueError. One and only One of the three variables must be set but here two of them are set # If the number of None is 3 -> raises ValueError. One and only One of the three variables must be set but here none of them are set @@ -100,33 +99,6 @@ def __init__( - #--patch-end - - #--original - ''' - if path is None and data is None: - raise ValueError("Either path or data must be set.") - if path is not None and data is not None: - raise ValueError("Only either path or data can be set.") - if path is not None: - if isinstance(path, Path): - self.path = path - else: - self.path = Path(path) - self.data = None - else: - self.path = None - self.data = data - - self.factory = get_platform_factory() - if self.data is not None: - self._impl = self.factory.Image(interface=self, data=self.data) - else: - 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) - ''' @property def width(self) -> int: From a7e99b69e8dc478d0ec813b43be9b60072b8d93c Mon Sep 17 00:00:00 2001 From: dr-gavitron Date: Sun, 15 Oct 2023 20:35:28 +0530 Subject: [PATCH 07/11] Resolved all the issues --- core/src/toga/images.py | 85 ++++++++++++------------------ core/src/toga/widgets/canvas.py | 19 ++++--- core/src/toga/widgets/imageview.py | 34 +++++++----- 3 files changed, 69 insertions(+), 69 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 885f591f19..48df665c21 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -7,9 +7,15 @@ import toga from toga.platform import get_platform_factory -import time -import os - +from io import BytesIO +import time, os + +try: + from PIL import Image as PIL_Image + PIL_ImportError_Message = None +except ImportError as e: + PIL_Image = None + PIL_ImportError_Message = e class Image: def __init__( self, @@ -51,13 +57,9 @@ def __init__( self.path = None self.pil_image = None if pil_image is not None: - try: - from PIL import Image as _PIL_Image_Module_ - except ImportError as e: - print("Pillow is required as pil_image is set to a PIL.Image type") - raise ImportError(e) - #type checking - if not _PIL_Image_Module_.isImageType(pil_image): + if PIL_Image == None: + raise ImportError(PIL_ImportError_Message) + if not PIL_Image.isImageType(pil_image): raise TypeError("pil_image is not a PIL.Image type.") self.pil_image = pil_image self.data = None @@ -73,29 +75,10 @@ def __init__( raise FileNotFoundError(f"Image file {self.path} does not exist") self._impl = self.factory.Image(interface=self, path=self.path) if self.pil_image is not None: - #What we will do: save the pil_image into a temporary file then load raw byte data - #from that file, then use the bytes to create self._impl, finally delete the temp file - - # creating an unique name for the temporary file - unique_time = str(time.time()) - temp_file = f"_PIL_temp_img_file_{unique_time}_" - temp_file_path = toga.App.app.paths.app - while temp_file in os.listdir(temp_file_path): - temp_file+="1_" - complete_temp_path = os.path.join(temp_file_path, temp_file) - - # saving the pil_image in the temporary file - self.pil_image.save(complete_temp_path) - - #loading the raw bytes of the image into the memory from the temporary file - with open(complete_temp_path, "rb") as _file: - raw_image_data = _file.read() - - #passing the bytes of the image to create _impl - self._impl = self.factory.Image(interface=self, data=raw_image_data) - - #finally delete the temporary file from the storage - os.remove(temp_file_path) + img_buffer = BytesIO() + self.pil_image.save(img_buffer, format=self.pil_image.format) + img_buffer.seek(0) + self._impl = self.factory.Image(interface=self, data=img_buffer.read()) @@ -121,28 +104,30 @@ def save(self, path: str | Path): self._impl.save(path) def as_format(self, format: Any|None=None): + """ + get the image as specified format if supported + :param format: None or A supported type of Image + Supported types are `PIL.Image.Image`, + :returns: toga.Image if format is None, or the specified format if the format is supported + ``` + from PIL import Image + pil_image = toga_image.as_format(Image.Image) + ``` + """ if format == None: return self - elif format.__name__ == "PIL.Image": - # save the image in storage in a temporary file - # then load it as PIL.Image then delete the temporary file - - # creating an unique name for the temporary file + elif PIL_Image != None and format == PIL_Image.Image: + # saving into temporary file unique_time = str(time.time()) - temp_file = f"_TOGA_temp_img_file_{unique_time}_" - temp_file_path = toga.App.app.paths.app - while temp_file in os.listdir(temp_file_path): - temp_file+="1_" - complete_temp_path = os.path.join(temp_file_path, temp_file) - - # loading PIL Image - from PIL import Image as _PIL_Image_ - pil_image = _PIL_Image_.open(complete_temp_path) - + temp_file = f"._toga_Image_as_format_PIL_Image_{unique_time}_" + temp_file_path = os.path.join(toga.App.app.paths.app, temp_file) + self.save(temp_file_path) + # creating PIL.Image.Image from temporary file + pil_image = PIL_Image.open(temp_file_path) # deleting the temporary file - os.remove(complete_temp_path) + os.remove(temp_file_path) return pil_image else: - raise TypeError("Unknown Conversion Format") + raise TypeError(f"Unknown conversion format: {format}") diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 9c1c335c45..7830867bec 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -17,6 +17,12 @@ from .. import Image from .base import Widget +from io import BytesIO +try: + from PIL import Image as PIL_Image +except: + PIL_Image = None + ####################################################################################### # Simple drawing objects ####################################################################################### @@ -1448,16 +1454,15 @@ def measure_text( def as_image(self, format: Any | None = None) -> toga.Image: """Render the canvas as an Image. - - :returns: A :class:`toga.Image` containing the canvas content if format is None else `PIL.Image` containing the canvas content if format is specified as PIL.Image.""" + :param format: None or type of the image that will be returned + Supported Types: PIL.Image.Image, toga.Image + :returns: Object of the specified Image type. If unspecified returns toga.Image""" if format == None: return Image(data=self._impl.get_image_data()) - elif format.__name__ == "PIL.Image": - from io import BytesIO as _BytesIO_ - from PIL import Image as _PIL_Image_ - return _PIL_Image_.open(_BytesIO_(self._impl.get_image_data())) + elif PIL_Image != None and format==PIL_Image.Image: + return PIL_Image.open(BytesIO(self._impl.get_image_data())) else: - raise TypeError(f"Unsupported Format") + raise TypeError(f"Unsupported image format: {format}") ########################################################################### # 2023-07 Backwards compatibility diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index ff7eff3609..6d6a1cf87f 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -10,6 +10,11 @@ from toga.style.pack import NONE from toga.widgets.base import Widget +try: + from PIL import Image as PIL_Image +except ImportError as e: + PIL_Image = None + def rehint_imageview(image, style, scale=1): """Compute the size hints for an ImageView based on the image. @@ -81,14 +86,10 @@ def __init__( # Prime the image attribute self._image = None self._impl = self.factory.ImageView(interface=self) - # checking if the image if PIL.Image - try: - from PIL import Image as _PIL_Image_Module_ - if _PIL_Image_Module_.isImageType(image): - image = Image(pil_image=image) #TODO: A change may be necessary if behaviour of Image is changed - except ImportError as e: - # cant import... assume that PIL.Image will not be passed - pass + # checking if the image is PIL.Image.Image, if it is, then convert it to toga.Image + if PIL_Image!=None and PIL_Image.isImageType(image): + image = Image(pil_image = image) + self.image = image @@ -138,10 +139,19 @@ def image(self, image): self.refresh() def as_image(self, format: Any | None=None): + ''' + get the image from ImageView as specified format if supported + :param format: None or A supported type of Image + Supported types are `PIL.Image.Image`, + :returns: toga.Image if format is None, or the specified format if the format is supported + ``` + from PIL import Image + pil_image = imageview.as_image(Image.Image) + ``` + ''' if format == None: return self.image - elif format.__name__ == "PIL.Image": - import PIL as _PIL_ - return self.image.as_format(_PIL_.Image) + elif PIL_Image!=None and format==PIL_Image.Image: + return self.image.as_format(PIL_Image.Image) else: - return TypeError("Unknown Conversion Format") + return TypeError(f"Unknown conversion format: {format}") From f809bb62695a5041f29dbf59eb6b397aa7df846b Mon Sep 17 00:00:00 2001 From: dr-gavitron Date: Mon, 16 Oct 2023 13:22:28 +0530 Subject: [PATCH 08/11] wrote tests and added a change note --- changes/2142.feature.rst | 45 ++++++++++++++++++++++++ core/tests/test_images.py | 20 +++++++++++ core/tests/widgets/canvas/test_canvas.py | 5 +++ core/tests/widgets/test_imageview.py | 18 ++++++++++ 4 files changed, 88 insertions(+) create mode 100644 changes/2142.feature.rst diff --git a/changes/2142.feature.rst b/changes/2142.feature.rst new file mode 100644 index 0000000000..5808729033 --- /dev/null +++ b/changes/2142.feature.rst @@ -0,0 +1,45 @@ +Added Pillow support in toga +============================ +toga.Image +---------- +`toga.Image` can now take Pillow Image Object. simply set pil_image parameter to the Pillow Object +code-block::python + from PIL import Image as PIL_Image + import toga + myimg = PIL_Image.open("path/to/image.png") + image = toga.Image(pil_image=myimg) + +Also to convert a `toga.Image` object as Pillow Object +code-block::python + pil_image = image.as_format(PIL_Image.Image) + #or + pil_image = image.as_format(format=PIL_Image.Image) + +toga.ImageView +-------------- +pass a Pillow Object in `toga.ImageView` and it will show the image +code-block::python + from PIL import Image as PIL_Image + import toga + myimg = PIL_Image.open("path/to/image.png") + imageview = toga.ImageView(myimg) + +Aslo to extract image as Pillow Object from the `imageview` +code-block::python + pil_img = imageview.as_image(PIL_Image.Image) + #or + pil_img = imageview.as_image(format=PIL_Image.Image) + +toga.Canvas +----------- +`Canvas.as_image` now can return Pillow object if PIL.Image.Image set in the format parameter +code-block::python + from PIL import Image as PIL_Image + pil_img = canvas.as_image(PIL_Image.Image) + #or + pil_img = canvas.as_image(format=PIL_Image.Image) + +When conversion format is `None` +------------------------------ +Also when `format` is `None` in `Image.as_format` or `ImageView.as_image` or `Canvas.as_image` +it will return a `toga.Image` object by default diff --git a/core/tests/test_images.py b/core/tests/test_images.py index 787cc2ed59..506325b0d1 100644 --- a/core/tests/test_images.py +++ b/core/tests/test_images.py @@ -120,3 +120,23 @@ def test_image_save(): image.save(save_path) assert_action_performed_with(image, "save", path=save_path) + +def test_pil_support(): + from PIL import Image as PIL_Image + + pil_img = PIL_Image.open("resources/toga.png") + toga_img = toga.Image("resources/toga.png") + toga_img_from_pil_img = toga.Image(pil_image = pil_img) + + assert toga_img.width == toga_img_from_pil_img.width, "PIL support is faulty" + assert toga_img.height == toga_img_from_pil_img.height, "PIL support is faulty" + + pil_img2 = toga_img_from_pil_img.as_format(PIL_Image.Image) + + assert type(pil_img2) == type(pil_img), "Image.as_format(PIL_Image.Image) is faulty" + assert pil_img2.size == pil_img.size, "Image.as_format(PIL_Image.Image) is faulty" + +def test_as_format_none(): + img = toga.Image("resources/toga.png") + img2 = img.as_format() + assert img == img2, "Image.as_format should return self when nothing is provided as arg, but failed" diff --git a/core/tests/widgets/canvas/test_canvas.py b/core/tests/widgets/canvas/test_canvas.py index 9eb330a707..fdf6fb0a79 100644 --- a/core/tests/widgets/canvas/test_canvas.py +++ b/core/tests/widgets/canvas/test_canvas.py @@ -159,6 +159,11 @@ def test_as_image(widget): assert image is not None assert_action_performed(widget, "get image data") + from PIL import Image as PIL_Image + image = widget.as_image(format=PIL_Image.Image) + assert type(image) == PIL_Image.Image + assert_action_performed(widget, "get image data as Pillow Image Object") + def test_deprecated_drawing_operations(widget): """Deprecated simple drawing operations raise a warning""" diff --git a/core/tests/widgets/test_imageview.py b/core/tests/widgets/test_imageview.py index 447d4542e2..a97ef63889 100644 --- a/core/tests/widgets/test_imageview.py +++ b/core/tests/widgets/test_imageview.py @@ -176,3 +176,21 @@ def test_rehint_empty_image(params): assert width == 0 assert height == 0 assert aspect_ratio is None + +def test_pil_support(): + from PIL import Image as PIL_Image + pil_img = PIL_Image.open("resources/toga.png") + toga_img = PIL_Image.open("resources/toga.png") + + imageview = toga.ImageView(pil_img) + assert type(imageview.image) == toga.Image, "Internal conversion from PIL_Image.Image to toga.Image is faulty" + assert (imageview.image.width, imageview.image.height) == (toga_img.width,toga_img.height) == pil_img.size, "PIL support for imageview is faulty" + + pil_img2 = imageview.as_image(PIL_Image.Image) + assert pil_img2.size == pil_img2.size, "ImageView.as_image(PIL_Image.Image) is faulty" + +def test_as_image_format_is_none(): + img = toga.Image("resources/toga.png") + imageview = toga.ImageView(image=img) + img2 = imageview.as_image() + assert (img2.width, img2.height) == (img.width, img.height), "ImageView.as_image should return toga.Image when nothing is provided as parameter, but failed" \ No newline at end of file From 280c812c22ccb92f8cfaa901bb5ecebad5693720 Mon Sep 17 00:00:00 2001 From: dr-gavitron Date: Mon, 16 Oct 2023 16:08:59 +0530 Subject: [PATCH 09/11] Created param src that takes any supported sources wrote tests for unified src Minor changes to code for supporting pillow Fixes #2156 --- changes/2142.feature.rst | 4 +-- core/src/toga/images.py | 56 ++++++++++++++---------------- core/src/toga/widgets/imageview.py | 6 +--- core/tests/test_images.py | 32 ++++++++++++----- 4 files changed, 53 insertions(+), 45 deletions(-) diff --git a/changes/2142.feature.rst b/changes/2142.feature.rst index 5808729033..416e3a830c 100644 --- a/changes/2142.feature.rst +++ b/changes/2142.feature.rst @@ -2,12 +2,12 @@ Added Pillow support in toga ============================ toga.Image ---------- -`toga.Image` can now take Pillow Image Object. simply set pil_image parameter to the Pillow Object +`toga.Image` can now take Pillow Image Object. code-block::python from PIL import Image as PIL_Image import toga myimg = PIL_Image.open("path/to/image.png") - image = toga.Image(pil_image=myimg) + image = toga.Image(myimg) Also to convert a `toga.Image` object as Pillow Object code-block::python diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 48df665c21..c70356018a 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -7,7 +7,7 @@ import toga from toga.platform import get_platform_factory -from io import BytesIO +from io import BytesIO, BufferedReader import time, os try: @@ -19,51 +19,54 @@ class Image: def __init__( self, - path: str | None | Path = None, + src:str|Path|BytesIO|BufferedReader|bytes|PIL_Image.Image|None=None, *, + path: str | None | Path = None, data: bytes | None = None, - pil_image: Any | None = None, ): """Create a new image. - An image must be one of ``path``, ``data`` or ``pil_image`` - + :param src: string path, pathlib.Path, BufferedReader, BytesIO, bytes, Pillow Object of the image to load :param path: Path to the image to load. This can be specified as a string, or as a :any:`pathlib.Path` object. The path can be an absolute file system path, or a path relative to the module that defines your Toga application class. :param data: A bytes object with the contents of an image in a supported format. - :param pil_image: PIL.Image object created from an image of a supported format. :raises FileNotFoundError: If a path is provided, but that path does not exist. :raises ValueError: If the path or data cannot be loaded as an image. - :raises TypeError: If pil_image is provided but the type of the object is not PIL.Image """ - # At first we will create a list with these three variable, then count how many None is in that list. - # If the number of None is 1 -> raises ValueError. One and only One of the three variables must be set but here two of them are set - # If the number of None is 3 -> raises ValueError. One and only One of the three variables must be set but here none of them are set - # If the number of None is 2 -> Ok. Check the validity of the value of that non-None variable - none_count = [path, data, pil_image].count(None) + none_count = [src, path, data].count(None) if none_count != 2: - raise ValueError("One and Only one of the three args (path, data, pil_image) must be set.") - # checking validity of the arg(one of the three) + raise ValueError("One and Only one of the three parameters (src, path, data) have to be set.") + + if src is not None: + if isinstance(src, str) or isinstance(src, Path): + path = src + src = None + elif isinstance(src, BytesIO) or isinstance(src, BufferedReader): + src.seek(0) + data = src.read() + src = None + elif isinstance(src, bytes): + data = src + src = None + elif PIL_Image is not None and isinstance(src, PIL_Image.Image): + img_buffer = BytesIO() + src.save(img_buffer, format=src.format) + img_buffer.seek(0) + data = img_buffer.read() + src = None + else: + raise TypeError("Unsupported source type for image.") + if path is not None: if isinstance(path, Path): self.path = path else: self.path = Path(path) self.data = None - self.pil_image = None if data is not None: self.data = data self.path = None - self.pil_image = None - if pil_image is not None: - if PIL_Image == None: - raise ImportError(PIL_ImportError_Message) - if not PIL_Image.isImageType(pil_image): - raise TypeError("pil_image is not a PIL.Image type.") - self.pil_image = pil_image - self.data = None - self.path = None self.factory = get_platform_factory() @@ -74,11 +77,6 @@ def __init__( 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) - if self.pil_image is not None: - img_buffer = BytesIO() - self.pil_image.save(img_buffer, format=self.pil_image.format) - img_buffer.seek(0) - self._impl = self.factory.Image(interface=self, data=img_buffer.read()) diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 6d6a1cf87f..fa70214bbc 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -86,12 +86,8 @@ def __init__( # Prime the image attribute self._image = None self._impl = self.factory.ImageView(interface=self) - # checking if the image is PIL.Image.Image, if it is, then convert it to toga.Image if PIL_Image!=None and PIL_Image.isImageType(image): - image = Image(pil_image = image) - - - + image = Image(image) #PIL.Image.Image -> toga.Image self.image = image @property diff --git a/core/tests/test_images.py b/core/tests/test_images.py index 506325b0d1..6cb8800ef0 100644 --- a/core/tests/test_images.py +++ b/core/tests/test_images.py @@ -1,4 +1,6 @@ from pathlib import Path +from io import BytesIO +from PIL import Image as PIL_Image import pytest @@ -122,21 +124,33 @@ def test_image_save(): assert_action_performed_with(image, "save", path=save_path) def test_pil_support(): - from PIL import Image as PIL_Image - pil_img = PIL_Image.open("resources/toga.png") toga_img = toga.Image("resources/toga.png") - toga_img_from_pil_img = toga.Image(pil_image = pil_img) + toga_img_from_pil_img = toga.Image(pil_img) assert toga_img.width == toga_img_from_pil_img.width, "PIL support is faulty" assert toga_img.height == toga_img_from_pil_img.height, "PIL support is faulty" - pil_img2 = toga_img_from_pil_img.as_format(PIL_Image.Image) - - assert type(pil_img2) == type(pil_img), "Image.as_format(PIL_Image.Image) is faulty" - assert pil_img2.size == pil_img.size, "Image.as_format(PIL_Image.Image) is faulty" - -def test_as_format_none(): +def test_as_format(): img = toga.Image("resources/toga.png") img2 = img.as_format() assert img == img2, "Image.as_format should return self when nothing is provided as arg, but failed" + + pil_img = img.as_format(PIL_Image.Image) + assert isinstance(pil_img, PIL_Image.Image) + assert pil_img.size == (img.width, img.height) + +test_image_path = "resources/toga.png" +@pytest.mark.parametrize("unified_image_source",[ + test_image_path, #normal string paths + Path(test_image_path), #pathlib.Path + open(test_image_path, "rb"), #BufferedReader + open(test_image_path, "rb").read(),#bytes + BytesIO(open(test_image_path, "rb").read()), #BytesIO + PIL_Image.open(test_image_path), #PIL_Image.Image +]) + +def test_unified_source(unified_image_source): + image = toga.Image(unified_image_source) + assert image is not None and image.width is not None and image.height is not None + From 3e4c00c976f30860fde9c6912a576816c7ddbf69 Mon Sep 17 00:00:00 2001 From: dr-gavitron Date: Mon, 16 Oct 2023 17:28:00 +0530 Subject: [PATCH 10/11] Resolved some issues --- changes/2142.feature.rst | 46 +----------------------- core/src/toga/images.py | 15 ++++---- core/src/toga/widgets/canvas.py | 4 ++- core/src/toga/widgets/imageview.py | 4 ++- core/tests/test_images.py | 9 +++-- core/tests/widgets/canvas/test_canvas.py | 3 +- core/tests/widgets/test_imageview.py | 16 ++++----- 7 files changed, 29 insertions(+), 68 deletions(-) diff --git a/changes/2142.feature.rst b/changes/2142.feature.rst index 416e3a830c..e6f53a6349 100644 --- a/changes/2142.feature.rst +++ b/changes/2142.feature.rst @@ -1,45 +1 @@ -Added Pillow support in toga -============================ -toga.Image ----------- -`toga.Image` can now take Pillow Image Object. -code-block::python - from PIL import Image as PIL_Image - import toga - myimg = PIL_Image.open("path/to/image.png") - image = toga.Image(myimg) - -Also to convert a `toga.Image` object as Pillow Object -code-block::python - pil_image = image.as_format(PIL_Image.Image) - #or - pil_image = image.as_format(format=PIL_Image.Image) - -toga.ImageView --------------- -pass a Pillow Object in `toga.ImageView` and it will show the image -code-block::python - from PIL import Image as PIL_Image - import toga - myimg = PIL_Image.open("path/to/image.png") - imageview = toga.ImageView(myimg) - -Aslo to extract image as Pillow Object from the `imageview` -code-block::python - pil_img = imageview.as_image(PIL_Image.Image) - #or - pil_img = imageview.as_image(format=PIL_Image.Image) - -toga.Canvas ------------ -`Canvas.as_image` now can return Pillow object if PIL.Image.Image set in the format parameter -code-block::python - from PIL import Image as PIL_Image - pil_img = canvas.as_image(PIL_Image.Image) - #or - pil_img = canvas.as_image(format=PIL_Image.Image) - -When conversion format is `None` ------------------------------- -Also when `format` is `None` in `Image.as_format` or `ImageView.as_image` or `Canvas.as_image` -it will return a `toga.Image` object by default +Added Pillow Object Support in toga.Image, toga.ImageView and toga.Canvas \ No newline at end of file diff --git a/core/src/toga/images.py b/core/src/toga/images.py index c70356018a..6f0769e939 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -104,15 +104,18 @@ def save(self, path: str | Path): def as_format(self, format: Any|None=None): """ get the image as specified format if supported - :param format: None or A supported type of Image - Supported types are `PIL.Image.Image`, - :returns: toga.Image if format is None, or the specified format if the format is supported + :param format: A supported type of Image + Supported types are `PIL.Image.Image`, `toga.Image` + :returns: Object of the specified type ``` - from PIL import Image - pil_image = toga_image.as_format(Image.Image) + from PIL import Image as PIL_Image + pil_image = toga_image.as_format(PIL_Image.Image) + toga_img = toga_image.as_format(toga.Image) ``` """ - if format == None: + if format==None: + return self + elif isinstance(format, type(self)): return self elif PIL_Image != None and format == PIL_Image.Image: # saving into temporary file diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 7830867bec..03843b27e5 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1451,7 +1451,7 @@ def measure_text( # As image ########################################################################### - def as_image(self, format: Any | None = None) -> toga.Image: + def as_image(self, format: Any|None=None) -> toga.Image: """Render the canvas as an Image. :param format: None or type of the image that will be returned @@ -1459,6 +1459,8 @@ def as_image(self, format: Any | None = None) -> toga.Image: :returns: Object of the specified Image type. If unspecified returns 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()) elif PIL_Image != None and format==PIL_Image.Image: return PIL_Image.open(BytesIO(self._impl.get_image_data())) else: diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index fa70214bbc..56e18c55c4 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -145,7 +145,9 @@ def as_image(self, format: Any | None=None): pil_image = imageview.as_image(Image.Image) ``` ''' - if format == None: + if format==None: + return self.image + elif isinstance(format, Image): return self.image elif PIL_Image!=None and format==PIL_Image.Image: return self.image.as_format(PIL_Image.Image) diff --git a/core/tests/test_images.py b/core/tests/test_images.py index 6cb8800ef0..4554798ca9 100644 --- a/core/tests/test_images.py +++ b/core/tests/test_images.py @@ -125,20 +125,19 @@ def test_image_save(): def test_pil_support(): pil_img = PIL_Image.open("resources/toga.png") - toga_img = toga.Image("resources/toga.png") toga_img_from_pil_img = toga.Image(pil_img) - assert toga_img.width == toga_img_from_pil_img.width, "PIL support is faulty" - assert toga_img.height == toga_img_from_pil_img.height, "PIL support is faulty" + assert toga_img_from_pil_img.width == 60, "PIL support is faulty" + assert toga_img_from_pil_img.height == 40, "PIL support is faulty" def test_as_format(): img = toga.Image("resources/toga.png") img2 = img.as_format() - assert img == img2, "Image.as_format should return self when nothing is provided as arg, but failed" + assert img == img2, "Image.as_format should return toga.Image when nothing is provided as arg, but failed" pil_img = img.as_format(PIL_Image.Image) assert isinstance(pil_img, PIL_Image.Image) - assert pil_img.size == (img.width, img.height) + assert pil_img.size == (60, 40) test_image_path = "resources/toga.png" @pytest.mark.parametrize("unified_image_source",[ diff --git a/core/tests/widgets/canvas/test_canvas.py b/core/tests/widgets/canvas/test_canvas.py index fdf6fb0a79..6affceb593 100644 --- a/core/tests/widgets/canvas/test_canvas.py +++ b/core/tests/widgets/canvas/test_canvas.py @@ -157,12 +157,11 @@ def test_as_image(widget): """A rendered canvas can be retrieved as an image""" image = widget.as_image() assert image is not None - assert_action_performed(widget, "get image data") from PIL import Image as PIL_Image image = widget.as_image(format=PIL_Image.Image) assert type(image) == PIL_Image.Image - assert_action_performed(widget, "get image data as Pillow Image Object") + assert_action_performed(widget, "get image data") def test_deprecated_drawing_operations(widget): diff --git a/core/tests/widgets/test_imageview.py b/core/tests/widgets/test_imageview.py index a97ef63889..4e3899a9f0 100644 --- a/core/tests/widgets/test_imageview.py +++ b/core/tests/widgets/test_imageview.py @@ -13,6 +13,8 @@ assert_action_performed_with, ) +from PIL import Image as PIL_Image + @pytest.fixture def app(): @@ -178,19 +180,17 @@ def test_rehint_empty_image(params): assert aspect_ratio is None def test_pil_support(): - from PIL import Image as PIL_Image pil_img = PIL_Image.open("resources/toga.png") - toga_img = PIL_Image.open("resources/toga.png") imageview = toga.ImageView(pil_img) assert type(imageview.image) == toga.Image, "Internal conversion from PIL_Image.Image to toga.Image is faulty" - assert (imageview.image.width, imageview.image.height) == (toga_img.width,toga_img.height) == pil_img.size, "PIL support for imageview is faulty" - - pil_img2 = imageview.as_image(PIL_Image.Image) - assert pil_img2.size == pil_img2.size, "ImageView.as_image(PIL_Image.Image) is faulty" + assert (imageview.image.width, imageview.image.height) == (60, 40), "PIL support for imageview is faulty" -def test_as_image_format_is_none(): +def test_as_image_format(): img = toga.Image("resources/toga.png") imageview = toga.ImageView(image=img) img2 = imageview.as_image() - assert (img2.width, img2.height) == (img.width, img.height), "ImageView.as_image should return toga.Image when nothing is provided as parameter, but failed" \ No newline at end of file + assert (img2.width, img2.height) == (60, 40), "ImageView.as_image should return toga.Image when nothing is provided as parameter, but failed" + + pil_img = imageview.as_image(PIL_Image.Image) + assert pil_img.size == (60, 40), "Faulty conversion to pil image" \ No newline at end of file From f371576e4453c9c34c1b1c17d7bf7e20210772d4 Mon Sep 17 00:00:00 2001 From: dr-gavitron Date: Thu, 19 Oct 2023 23:33:18 +0530 Subject: [PATCH 11/11] Made some changes --- core/src/toga/images.py | 27 ++++++++++----------------- core/src/toga/widgets/canvas.py | 21 ++++++++++++++------- core/src/toga/widgets/imageview.py | 24 ++++++++++++------------ core/tests/test_images.py | 4 +--- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 6f0769e939..66fc22500a 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -7,7 +7,8 @@ import toga from toga.platform import get_platform_factory -from io import BytesIO, BufferedReader +from io import BytesIO + import time, os try: @@ -19,14 +20,14 @@ class Image: def __init__( self, - src:str|Path|BytesIO|BufferedReader|bytes|PIL_Image.Image|None=None, + src:str|Path|bytes|PIL_Image.Image|None=None, *, path: str | None | Path = None, data: bytes | None = None, ): """Create a new image. - :param src: string path, pathlib.Path, BufferedReader, BytesIO, bytes, Pillow Object of the image to load + :param src: path, data or pillow object of the image to load :param path: Path to the image to load. This can be specified as a string, or as a :any:`pathlib.Path` object. The path can be an absolute file system path, or a path relative to the module that defines your Toga application class. @@ -42,10 +43,6 @@ def __init__( if isinstance(src, str) or isinstance(src, Path): path = src src = None - elif isinstance(src, BytesIO) or isinstance(src, BufferedReader): - src.seek(0) - data = src.read() - src = None elif isinstance(src, bytes): data = src src = None @@ -101,23 +98,19 @@ def save(self, path: str | Path): """ self._impl.save(path) - def as_format(self, format: Any|None=None): + def as_format(self, format: Any): """ - get the image as specified format if supported - :param format: A supported type of Image - Supported types are `PIL.Image.Image`, `toga.Image` - :returns: Object of the specified type + Convert the image to format specified. + Supported formats are: Pillow Object, + + To convert the image to Pillow object ``` from PIL import Image as PIL_Image pil_image = toga_image.as_format(PIL_Image.Image) toga_img = toga_image.as_format(toga.Image) ``` """ - if format==None: - return self - elif isinstance(format, type(self)): - return self - elif PIL_Image != None and format == PIL_Image.Image: + if PIL_Image != None and format == PIL_Image.Image: # saving into temporary file unique_time = str(time.time()) temp_file = f"._toga_Image_as_format_PIL_Image_{unique_time}_" diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 03843b27e5..af61ece527 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1451,15 +1451,22 @@ def measure_text( # As image ########################################################################### - def as_image(self, format: Any|None=None) -> toga.Image: + def as_image(self, format: Any=toga.Image): """Render the canvas as an Image. - :param format: None or type of the image that will be returned - Supported Types: PIL.Image.Image, toga.Image - :returns: Object of the specified Image type. If unspecified returns toga.Image""" - if format == None: - return Image(data=self._impl.get_image_data()) - elif isinstance(format, toga.Image): + + To get toga.Image Object + ``` + toga_img = canvas.as_image() + ``` + + To get Pillow Object + ``` + from PIL import Image as PIL_Image + pil_img = canvas.as_image(PIL_Image.Image) + ``` + """ + if isinstance(format, toga.Image): return Image(data=self._impl.get_image_data()) elif PIL_Image != None and format==PIL_Image.Image: return PIL_Image.open(BytesIO(self._impl.get_image_data())) diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 56e18c55c4..971e5dce72 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -86,8 +86,6 @@ 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 self.image = image @property @@ -134,20 +132,22 @@ def image(self, image): self._impl.set_image(self._image) self.refresh() - def as_image(self, format: Any | None=None): + def as_image(self, format: Any=Image): ''' - get the image from ImageView as specified format if supported - :param format: None or A supported type of Image - Supported types are `PIL.Image.Image`, - :returns: toga.Image if format is None, or the specified format if the format is supported + Get image content of the imageview. + + To get the image as toga.Image Object + ``` + toga_img = imageview.as_image() ``` - from PIL import Image - pil_image = imageview.as_image(Image.Image) + + To get the image as Pillow Object + ``` + from PIL import Image as PIL_Image + pil_img = imageview.as_image(PIL_Image.Image) ``` ''' - if format==None: - return self.image - elif isinstance(format, Image): + if isinstance(format, Image): return self.image elif PIL_Image!=None and format==PIL_Image.Image: return self.image.as_format(PIL_Image.Image) diff --git a/core/tests/test_images.py b/core/tests/test_images.py index 4554798ca9..1aeab73da0 100644 --- a/core/tests/test_images.py +++ b/core/tests/test_images.py @@ -143,13 +143,11 @@ def test_as_format(): @pytest.mark.parametrize("unified_image_source",[ test_image_path, #normal string paths Path(test_image_path), #pathlib.Path - open(test_image_path, "rb"), #BufferedReader open(test_image_path, "rb").read(),#bytes - BytesIO(open(test_image_path, "rb").read()), #BytesIO PIL_Image.open(test_image_path), #PIL_Image.Image ]) def test_unified_source(unified_image_source): image = toga.Image(unified_image_source) - assert image is not None and image.width is not None and image.height is not None + assert image is not None and image.width == 60 and image.height == 40