From 973220804f99474f5edec5edfff01f54261c7c67 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 7 Dec 2023 13:17:48 +0800 Subject: [PATCH 01/10] Add the ability to create a toga Image from the internal platform image representation. --- core/src/toga/images.py | 3 ++ core/tests/conftest.py | 6 +++- core/tests/test_images.py | 42 ++++++++++++++++++++++------ core/tests/test_window.py | 5 ++-- dummy/src/toga_dummy/images.py | 51 ++++++++++++++++++++++++---------- 5 files changed, 80 insertions(+), 27 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 7db9e15859..816cec9767 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -99,6 +99,9 @@ def __init__( src.save(buffer, format="png", compress_level=0) self._impl = self.factory.Image(interface=self, data=buffer.getvalue()) + elif isinstance(src, self.factory.Image.RAW_TYPE): + self._impl = self.factory.Image(interface=self, raw=src) + else: raise TypeError("Unsupported source type for Image") diff --git a/core/tests/conftest.py b/core/tests/conftest.py index e8f8aabe89..a67c369291 100644 --- a/core/tests/conftest.py +++ b/core/tests/conftest.py @@ -22,6 +22,10 @@ def clear_sys_modules(monkeypatch): pass +class TestApp(toga.App): + pass + + @pytest.fixture def app(event_loop): - return toga.App(formal_name="Test App", app_id="org.beeware.toga.test-app") + return TestApp(formal_name="Test App", app_id="org.beeware.toga.test-app") diff --git a/core/tests/test_images.py b/core/tests/test_images.py index ed07fc01a3..4c745c648f 100644 --- a/core/tests/test_images.py +++ b/core/tests/test_images.py @@ -6,8 +6,8 @@ import toga from toga_dummy.utils import assert_action_performed_with -RELATIVE_FILE_PATH = Path("resources/toga.png") -ABSOLUTE_FILE_PATH = Path(toga.__file__).parent / "resources/toga.png" +RELATIVE_FILE_PATH = Path("resources/sample.png") +ABSOLUTE_FILE_PATH = Path(__file__).parent / "resources/sample.png" @pytest.mark.filterwarnings("ignore::DeprecationWarning") @@ -108,6 +108,21 @@ def test_create_from_bytes(args, kwargs): assert_action_performed_with(image, "load image data", data=BYTES) +def test_create_from_raw(): + """An image can be created from a raw data source""" + orig = toga.Image(BYTES) + + print("NATIVE", orig._impl.native) + copy = toga.Image(orig._impl.native) + # Image is bound + assert copy._impl is not None + # impl/interface round trips + assert copy._impl.interface == copy + + # Image was constructed from raw data + assert_action_performed_with(copy, "load image from raw") + + def test_not_enough_arguments(): with pytest.raises( TypeError, @@ -132,7 +147,7 @@ def test_create_from_pil(app): toga_image = toga.Image(pil_image) assert isinstance(toga_image, toga.Image) - assert toga_image.size == (32, 32) + assert toga_image.size == (144, 72) def test_create_from_toga_image(app): @@ -141,7 +156,7 @@ def test_create_from_toga_image(app): toga_image_2 = toga.Image(toga_image) assert isinstance(toga_image_2, toga.Image) - assert toga_image_2.size == (32, 32) + assert toga_image_2.size == (144, 72) @pytest.mark.parametrize("kwargs", [{"data": BYTES}, {"path": ABSOLUTE_FILE_PATH}]) @@ -176,14 +191,23 @@ def test_dimensions(app): """The dimensions of the image can be retrieved""" image = toga.Image(RELATIVE_FILE_PATH) - assert image.size == (32, 32) - assert image.width == image.height == 32 + assert image.size == (144, 72) + assert image.width == 144 + assert image.height == 72 def test_data(app): """The raw data of the image can be retrieved.""" image = toga.Image(ABSOLUTE_FILE_PATH) - assert image.data == ABSOLUTE_FILE_PATH.read_bytes() + + # We can't guarantee the round-trip of image data, + # but the data starts with a PNG header + assert image.data.startswith(b"\x89PNG\r\n\x1a\n") + + # If we build a new image from the data, it has the same properties. + from_data = toga.Image(image.data) + assert from_data.width == image.width + assert from_data.height == image.height def test_image_save(tmp_path): @@ -214,7 +238,7 @@ def test_as_format_toga(app, Class_1, Class_2): image_2 = image_1.as_format(Class_2) assert isinstance(image_2, Class_2) - assert image_2.size == (32, 32) + assert image_2.size == (144, 72) def test_as_format_pil(app): @@ -222,7 +246,7 @@ def test_as_format_pil(app): toga_image = toga.Image(ABSOLUTE_FILE_PATH) pil_image = toga_image.as_format(PIL.Image.Image) assert isinstance(pil_image, PIL.Image.Image) - assert pil_image.size == (32, 32) + assert pil_image.size == (144, 72) # None is same as supplying nothing; also test a random unrecognized class diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 092abb3f31..3aaa11ec01 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -4,7 +4,6 @@ import pytest import toga -import toga_dummy from toga_dummy.utils import ( assert_action_not_performed, assert_action_performed, @@ -351,8 +350,8 @@ def test_as_image(window): """A window can be captured as an image""" image = window.as_image() assert_action_performed(window, "get image data") - path = Path(toga_dummy.__file__).parent / "resources/screenshot.png" - assert image.data == path.read_bytes() + # Don't need to check the raw data; just check it's the right size. + assert image.size == (318, 346) def test_info_dialog(window, app): diff --git a/dummy/src/toga_dummy/images.py b/dummy/src/toga_dummy/images.py index 1b29749414..6b33526de4 100644 --- a/dummy/src/toga_dummy/images.py +++ b/dummy/src/toga_dummy/images.py @@ -12,34 +12,57 @@ import toga +# We need a dummy "internal image format" for the dummy backend It's a wrapper +# around a PIL image. We can't just use a PIL image because that will be +# interpreted *as* a PIL image. +class DummyImage: + def __init__(self, image=None): + self.raw = image + if image: + buffer = BytesIO() + self.raw.save(buffer, format="png", compress_level=0) + self.data = buffer.getvalue() + else: + self.data = b"pretend this is PNG image data" + + class Image(LoggedObject): - def __init__(self, interface: toga.Image, path: Path = None, data: bytes = None): + RAW_TYPE = DummyImage + + def __init__( + self, + interface: toga.Image, + path: Path = None, + data: bytes = None, + raw: BytesIO = None, + ): super().__init__() self.interface = interface if path: self._action("load image file", path=path) if path.is_file(): - self._data = path.read_bytes() - with PIL.Image.open(path) as image: - self._width, self._height = image.size + self.native = DummyImage(PIL.Image.open(path)) else: - self._data = b"pretend this is PNG image data" - self._width, self._height = 60, 40 - else: + self.native = DummyImage() + elif data: self._action("load image data", data=data) - self._data = data - buffer = BytesIO(data) - with PIL.Image.open(buffer) as image: - self._width, self._height = image.size + self.native = DummyImage(PIL.Image.open(BytesIO(data))) + else: + self._action("load image from raw") + self.native = raw def get_width(self): - return self._width + if self.native.raw is None: + return 60 + return self.native.raw.size[0] def get_height(self): - return self._height + if self.native.raw is None: + return 40 + return self.native.raw.size[1] def get_data(self): - return self._data + return self.native.data def save(self, path): self._action("save", path=path) From ef6185a0586eb04113bd4df295d18f70001117bd Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 7 Dec 2023 13:33:48 +0800 Subject: [PATCH 02/10] Add a testbed test of creating images from raw. --- android/src/toga_android/images.py | 8 ++++++-- cocoa/src/toga_cocoa/images.py | 8 ++++++-- gtk/src/toga_gtk/images.py | 8 ++++++-- iOS/src/toga_iOS/images.py | 9 +++++++-- testbed/tests/test_images.py | 10 ++++++++++ winforms/src/toga_winforms/images.py | 8 ++++++-- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/android/src/toga_android/images.py b/android/src/toga_android/images.py index 8ed5ededda..a4ad4938cf 100644 --- a/android/src/toga_android/images.py +++ b/android/src/toga_android/images.py @@ -5,17 +5,21 @@ class Image: - def __init__(self, interface, path=None, data=None): + RAW_TYPE = Bitmap + + def __init__(self, interface, path=None, data=None, raw=None): self.interface = interface if path: self.native = BitmapFactory.decodeFile(str(path)) if self.native is None: raise ValueError(f"Unable to load image from {path}") - else: + elif data: self.native = BitmapFactory.decodeByteArray(data, 0, len(data)) if self.native is None: raise ValueError("Unable to load image from data") + else: + self.native = raw def get_width(self): return self.native.getWidth() diff --git a/cocoa/src/toga_cocoa/images.py b/cocoa/src/toga_cocoa/images.py index 012ab55779..034f5ba581 100644 --- a/cocoa/src/toga_cocoa/images.py +++ b/cocoa/src/toga_cocoa/images.py @@ -19,7 +19,9 @@ def nsdata_to_bytes(data: NSData) -> bytes: class Image: - def __init__(self, interface, path=None, data=None): + RAW_TYPE = NSImage + + def __init__(self, interface, path=None, data=None, raw=None): self.interface = interface try: @@ -36,11 +38,13 @@ def __init__(self, interface, path=None, data=None): self.native = image.initWithContentsOfFile(str(path)) if self.native is None: raise ValueError(f"Unable to load image from {path}") - else: + elif data: nsdata = NSData.dataWithBytes(data, length=len(data)) self.native = image.initWithData(nsdata) if self.native is None: raise ValueError("Unable to load image from data") + else: + self.native = raw finally: image.release() diff --git a/gtk/src/toga_gtk/images.py b/gtk/src/toga_gtk/images.py index cf8534d1d5..6808581646 100644 --- a/gtk/src/toga_gtk/images.py +++ b/gtk/src/toga_gtk/images.py @@ -4,7 +4,9 @@ class Image: - def __init__(self, interface, path=None, data=None): + RAW_TYPE = GdkPixbuf + + def __init__(self, interface, path=None, data=None, raw=None): self.interface = interface if path: @@ -12,12 +14,14 @@ def __init__(self, interface, path=None, data=None): self.native = GdkPixbuf.Pixbuf.new_from_file(str(path)) except GLib.GError: raise ValueError(f"Unable to load image from {path}") - else: + elif data: try: input_stream = Gio.MemoryInputStream.new_from_data(data, None) self.native = GdkPixbuf.Pixbuf.new_from_stream(input_stream, None) except GLib.GError: raise ValueError("Unable to load image from data") + else: + self.native = raw def get_width(self): return self.native.get_width() diff --git a/iOS/src/toga_iOS/images.py b/iOS/src/toga_iOS/images.py index 2d0c0cbf3e..5ab906ecf0 100644 --- a/iOS/src/toga_iOS/images.py +++ b/iOS/src/toga_iOS/images.py @@ -18,19 +18,24 @@ def nsdata_to_bytes(data: NSData) -> bytes: class Image: - def __init__(self, interface, path=None, data=None): + RAW_TYPE = UIImage + + def __init__(self, interface, path=None, data=None, raw=None): self.interface = interface if path: self.native = UIImage.imageWithContentsOfFile(str(path)) if self.native is None: raise ValueError(f"Unable to load image from {path}") - else: + elif data: self.native = UIImage.imageWithData( NSData.dataWithBytes(data, length=len(data)) ) if self.native is None: raise ValueError("Unable to load image from data") + else: + self.native = raw + self.native.retain() def __del__(self): diff --git a/testbed/tests/test_images.py b/testbed/tests/test_images.py index 58e3a4ba43..a72ccf69ae 100644 --- a/testbed/tests/test_images.py +++ b/testbed/tests/test_images.py @@ -22,6 +22,16 @@ async def test_local_image(app): assert image.height == 72 +async def test_raw_image(app): + "An image can be created from the platform's raw representation" + original = toga.Image("resources/sample.png") + + image = toga.Image(original._impl.native) + + assert image.width == original.width + assert image.height == original.height + + async def test_bad_image_file(app): "If a file isn't a loadable image, an error is raised" with pytest.raises( diff --git a/winforms/src/toga_winforms/images.py b/winforms/src/toga_winforms/images.py index fd641a04e5..bccbe177bc 100644 --- a/winforms/src/toga_winforms/images.py +++ b/winforms/src/toga_winforms/images.py @@ -10,7 +10,9 @@ class Image: - def __init__(self, interface, path=None, data=None): + RAW_TYPE = WinImage + + def __init__(self, interface, path=None, data=None, raw=None): self.interface = interface if path: @@ -20,12 +22,14 @@ def __init__(self, interface, path=None, data=None): # OutOfMemoryException is what Winforms raises when a file # isn't a valid image file. raise ValueError(f"Unable to load image from {path}") - else: + elif data: try: stream = MemoryStream(data) self.native = WinImage.FromStream(stream) except ArgumentException: raise ValueError("Unable to load image from data") + else: + self.native = raw def get_width(self): return self.native.Width From 7ffe38f50baf7eb5b1a439b69051894cc0a9494b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 7 Dec 2023 13:40:50 +0800 Subject: [PATCH 03/10] Add changenote. --- changes/2263.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2263.feature.rst diff --git a/changes/2263.feature.rst b/changes/2263.feature.rst new file mode 100644 index 0000000000..6ee2179ac5 --- /dev/null +++ b/changes/2263.feature.rst @@ -0,0 +1 @@ +Images can now be created from the native platform representation of an image, without needing to be transformed to bytes. From 218fadb54cb4df04a82a1c11c9761f0bedcba1ab Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 7 Dec 2023 13:51:03 +0800 Subject: [PATCH 04/10] Document the raw image API. --- core/tests/test_images.py | 1 - docs/reference/api/resources/images.rst | 26 +++++++++++++++++---- docs/reference/data/widgets_by_platform.csv | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/core/tests/test_images.py b/core/tests/test_images.py index 4c745c648f..2fe5d1ad29 100644 --- a/core/tests/test_images.py +++ b/core/tests/test_images.py @@ -112,7 +112,6 @@ def test_create_from_raw(): """An image can be created from a raw data source""" orig = toga.Image(BYTES) - print("NATIVE", orig._impl.native) copy = toga.Image(orig._impl.native) # Image is bound assert copy._impl is not None diff --git a/docs/reference/api/resources/images.rst b/docs/reference/api/resources/images.rst index 3bd15dbac4..ae4a06940e 100644 --- a/docs/reference/api/resources/images.rst +++ b/docs/reference/api/resources/images.rst @@ -1,6 +1,8 @@ Image ===== +Graphical content of arbitrary size. + .. rst-class:: widget-support .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 @@ -8,12 +10,18 @@ Image :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Image|Component)$)'} - -An image is graphical content of arbitrary size. - Usage ----- +An image can be constructed from: + +* A path to a file on disk; +* A blob of bytes containing image data in a known image format; +* A :any:`PIL.Image` object; +* Another :any:`toga.Image`; or +* The native platform representation of an image (see the :ref:`Notes + ` section below for details). + .. code-block:: python from pathlib import Path @@ -40,10 +48,20 @@ Notes * PNG and JPEG formats are guaranteed to be supported. Other formats are available on some platforms: - - macOS: GIF, BMP, TIFF - GTK: BMP + - macOS: GIF, BMP, TIFF - Windows: GIF, BMP, TIFF +.. _native-image-rep: + +* The native platform representations for images are: + + - Android: ``android.graphics.Bitmap`` + - GTK: ``GdkPixbuf`` + - iOS: ``UIImage`` + - macOS: ``NSImage`` + - Windows: ``System.Drawing.Image`` + Reference --------- diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index 0040fe654e..78eb5a0496 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -32,4 +32,4 @@ App Paths,Resource,:class:`~toga.paths.Paths`,A mechanism for obtaining platform Font,Resource,:class:`~toga.Font`,A text font,|y|,|y|,|y|,|y|,|y|,, Command,Resource,:class:`~toga.Command`,Command,|y|,|y|,|y|,,|y|,, Icon,Resource,:class:`~toga.Icon`,"A small, square image, used to provide easily identifiable visual context to a widget.",|y|,|y|,|y|,|y|,|y|,,|b| -Image,Resource,:class:`~toga.Image`,An image,|y|,|y|,|y|,|y|,|y|,, +Image,Resource,:class:`~toga.Image`,Graphical content of arbitrary size.,|y|,|y|,|y|,|y|,|y|,, From cf439365d769f9e552b34050503652c3ec06d529 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 7 Dec 2023 14:10:44 +0800 Subject: [PATCH 05/10] Correct the raw type declaration for GTK. --- docs/reference/api/resources/images.rst | 2 +- gtk/src/toga_gtk/images.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/api/resources/images.rst b/docs/reference/api/resources/images.rst index ae4a06940e..c9124e89c9 100644 --- a/docs/reference/api/resources/images.rst +++ b/docs/reference/api/resources/images.rst @@ -57,7 +57,7 @@ Notes * The native platform representations for images are: - Android: ``android.graphics.Bitmap`` - - GTK: ``GdkPixbuf`` + - GTK: ``GdkPixbuf.Pixbuf`` - iOS: ``UIImage`` - macOS: ``NSImage`` - Windows: ``System.Drawing.Image`` diff --git a/gtk/src/toga_gtk/images.py b/gtk/src/toga_gtk/images.py index 6808581646..2cf2b7498d 100644 --- a/gtk/src/toga_gtk/images.py +++ b/gtk/src/toga_gtk/images.py @@ -4,7 +4,7 @@ class Image: - RAW_TYPE = GdkPixbuf + RAW_TYPE = GdkPixbuf.Pixbuf def __init__(self, interface, path=None, data=None, raw=None): self.interface = interface From b0600a6e55f9cb92885101e7b55636b3a4766251 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 8 Dec 2023 11:30:51 +0800 Subject: [PATCH 06/10] Rework type declarations for Image to allow for native types. --- core/src/toga/images.py | 24 ++++++++++-------------- core/src/toga/widgets/imageview.py | 25 +++++++++---------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 816cec9767..aedffbbfc2 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -3,7 +3,7 @@ import warnings from io import BytesIO from pathlib import Path -from typing import TypeVar +from typing import Any, TypeVar from warnings import warn try: @@ -19,22 +19,16 @@ # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) +# Define a bucket type for any accepted image type. +ImageType = Any +# Define a type variable for generics where an Image type is required. ImageT = TypeVar("ImageT") -# Note: remove PIL type annotation when plugin system is implemented for image format -# registration; replace with ImageT? class Image: def __init__( self, - src: str - | Path - | bytes - | bytearray - | memoryview - | Image - | PIL.Image.Image - | None = None, + src: str | Path | bytes | bytearray | memoryview | ImageType | None = None, *, path=None, # DEPRECATED data=None, # DEPRECATED @@ -43,11 +37,13 @@ def __init__( :param src: The source from which to load the image. Can be a file path (relative or absolute, as a string or :any:`pathlib.Path`), raw - binary data in any supported image format, or another Toga image. Can also - accept a :any:`PIL.Image.Image` if Pillow is installed. + binary data in any supported image format, or another Toga image. + Can also accept the platform's native Image format; if Pillow is + installed, :any:`PIL.Image.Image` can be used. :param path: **DEPRECATED** - Use ``src``. :param data: **DEPRECATED** - Use ``src``. - :raises FileNotFoundError: If a path is provided, but that path does not exist. + :raises FileNotFoundError: If a path is provided, but that path does not + exist. :raises ValueError: If the source cannot be loaded as an image. """ ###################################################################### diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index d64e4e78f0..13a43314a3 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -11,9 +11,7 @@ if TYPE_CHECKING: from pathlib import Path - import PIL.Image - - from toga.images import ImageT + from toga.images import ImageT, ImageType def rehint_imageview(image, style, scale=1): @@ -72,26 +70,21 @@ def rehint_imageview(image, style, scale=1): class ImageView(Widget): def __init__( self, - image: str - | Path - | bytes - | bytearray - | memoryview - | PIL.Image.Image - | None = None, + image: str | Path | bytes | bytearray | memoryview | ImageType | None = None, id=None, style=None, ): """ Create a new image view. - :param image: The image to display. This can take all the same formats as the - `src` parameter to :class:`toga.Image` -- namely, a file path (as string - or :any:`pathlib.Path`), bytes data in a supported image format, - or :any:`PIL.Image.Image`. + :param image: The image to display. This can take all the same formats + as the `src` parameter to :class:`toga.Image` -- namely, a file path + (as string or :any:`pathlib.Path`), bytes data in a supported image + format, an instance of the platform's native Image type, or + :any:`PIL.Image.Image`. :param id: The ID for the widget. - :param style: A style object. If no style is provided, a default style will be - applied to the widget. + :param style: A style object. If no style is provided, a default style + will be applied to the widget. """ super().__init__(id=id, style=style) # Prime the image attribute From 1d2cea7bd5c566cafd60bab752edbb5c0642a506 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 9 Dec 2023 09:26:24 +0800 Subject: [PATCH 07/10] Revert to a simple Any type annotation. --- core/src/toga/images.py | 4 +--- core/src/toga/widgets/imageview.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index aedffbbfc2..305e8b4ff5 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -19,8 +19,6 @@ # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) -# Define a bucket type for any accepted image type. -ImageType = Any # Define a type variable for generics where an Image type is required. ImageT = TypeVar("ImageT") @@ -28,7 +26,7 @@ class Image: def __init__( self, - src: str | Path | bytes | bytearray | memoryview | ImageType | None = None, + src: str | Path | bytes | bytearray | memoryview | Any | None = None, *, path=None, # DEPRECATED data=None, # DEPRECATED diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 13a43314a3..370d77cd51 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from travertino.size import at_least @@ -11,7 +11,7 @@ if TYPE_CHECKING: from pathlib import Path - from toga.images import ImageT, ImageType + from toga.images import ImageT def rehint_imageview(image, style, scale=1): @@ -70,7 +70,7 @@ def rehint_imageview(image, style, scale=1): class ImageView(Widget): def __init__( self, - image: str | Path | bytes | bytearray | memoryview | ImageType | None = None, + image: str | Path | bytes | bytearray | memoryview | Any | None = None, id=None, style=None, ): From a1b0f4720f29f48c99bde5390bdc22f5faf1f829 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 11 Dec 2023 08:40:44 +0800 Subject: [PATCH 08/10] Restore 88 char word wrap. --- core/src/toga/images.py | 11 +++++------ core/src/toga/widgets/imageview.py | 19 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 305e8b4ff5..893e21927e 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -34,14 +34,13 @@ def __init__( """Create a new image. :param src: The source from which to load the image. Can be a file path - (relative or absolute, as a string or :any:`pathlib.Path`), raw - binary data in any supported image format, or another Toga image. - Can also accept the platform's native Image format; if Pillow is - installed, :any:`PIL.Image.Image` can be used. + (relative or absolute, as a string or :any:`pathlib.Path`), raw binary data + in any supported image format, or another Toga image. Can also accept the + platform's native Image format; if Pillow is installed, + :any:`PIL.Image.Image` can be used. :param path: **DEPRECATED** - Use ``src``. :param data: **DEPRECATED** - Use ``src``. - :raises FileNotFoundError: If a path is provided, but that path does not - exist. + :raises FileNotFoundError: If a path is provided, but that path does not exist. :raises ValueError: If the source cannot be loaded as an image. """ ###################################################################### diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 370d77cd51..53c29091b5 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -22,9 +22,9 @@ def rehint_imageview(image, style, scale=1): :param image: The image being displayed. :param style: The style object for the imageview. :param scale: The scale factor (if any) to apply to native pixel sizes. - :returns: A triple containing the intrinsic width hint, intrinsic height - hint, and the aspect ratio to preserve (or None if the aspect ratio - should not be preserved). + :returns: A triple containing the intrinsic width hint, intrinsic height hint, and + the aspect ratio to preserve (or None if the aspect ratio should not be + preserved). """ if image: if style.width != NONE and style.height != NONE: @@ -77,14 +77,13 @@ def __init__( """ Create a new image view. - :param image: The image to display. This can take all the same formats - as the `src` parameter to :class:`toga.Image` -- namely, a file path - (as string or :any:`pathlib.Path`), bytes data in a supported image - format, an instance of the platform's native Image type, or - :any:`PIL.Image.Image`. + :param image: The image to display. This can take all the same formats as the + `src` parameter to :class:`toga.Image` -- namely, a file path (as string or + :any:`pathlib.Path`), bytes data in a supported image format, an instance of + the platform's native Image type, or :any:`PIL.Image.Image`. :param id: The ID for the widget. - :param style: A style object. If no style is provided, a default style - will be applied to the widget. + :param style: A style object. If no style is provided, a default style will be + applied to the widget. """ super().__init__(id=id, style=style) # Prime the image attribute From 472b1c68eb4027b65aee9026960b3fe964ddab41 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 11 Dec 2023 08:42:00 +0800 Subject: [PATCH 09/10] Corrected capitalization of image. --- core/src/toga/images.py | 2 +- core/src/toga/widgets/imageview.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 893e21927e..7b6cb06545 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -36,7 +36,7 @@ def __init__( :param src: The source from which to load the image. Can be a file path (relative or absolute, as a string or :any:`pathlib.Path`), raw binary data in any supported image format, or another Toga image. Can also accept the - platform's native Image format; if Pillow is installed, + platform's native image format; if Pillow is installed, :any:`PIL.Image.Image` can be used. :param path: **DEPRECATED** - Use ``src``. :param data: **DEPRECATED** - Use ``src``. diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 53c29091b5..94c4a77c2e 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -80,7 +80,7 @@ def __init__( :param image: The image to display. This can take all the same formats as the `src` parameter to :class:`toga.Image` -- namely, a file path (as string or :any:`pathlib.Path`), bytes data in a supported image format, an instance of - the platform's native Image type, or :any:`PIL.Image.Image`. + the platform's native image type, or :any:`PIL.Image.Image`. :param id: The ID for the widget. :param style: A style object. If no style is provided, a default style will be applied to the widget. From d606b9532afff846b3897599ada25c9a809bae16 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 13 Dec 2023 09:42:36 +0800 Subject: [PATCH 10/10] Use a documented TypeAlias for image source data. --- core/pyproject.toml | 2 ++ core/src/toga/images.py | 28 ++++++++++++++++-------- core/src/toga/widgets/imageview.py | 24 ++++++-------------- docs/conf.py | 2 +- docs/reference/api/resources/images.rst | 29 ++++++++++++++++++------- docs/spelling_wordlist | 1 + 6 files changed, 51 insertions(+), 35 deletions(-) diff --git a/core/pyproject.toml b/core/pyproject.toml index 8b23cd8c35..1c3f6dff31 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -73,6 +73,8 @@ dev = [ "pytest-freezer == 0.4.8", "setuptools-scm == 8.0.4", "tox == 4.11.4", + # typing-extensions needed for TypeAlias added in Py 3.10 + "typing-extensions == 4.9.0 ; python_version < '3.10'" ] docs = [ "furo == 2023.9.10", diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 7b6cb06545..bc32bf8980 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -1,9 +1,10 @@ from __future__ import annotations +import sys import warnings from io import BytesIO from pathlib import Path -from typing import Any, TypeVar +from typing import TYPE_CHECKING, Any from warnings import warn try: @@ -19,25 +20,34 @@ # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) -# Define a type variable for generics where an Image type is required. -ImageT = TypeVar("ImageT") +if TYPE_CHECKING: + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias, TypeVar + else: + from typing import TypeAlias, TypeVar + + # Define a type variable for generics where an Image type is required. + ImageT = TypeVar("ImageT") + + # Define the types that can be used as Image content + PathLike: TypeAlias = str | Path + BytesLike: TypeAlias = bytes | bytearray | memoryview + ImageLike: TypeAlias = Any + ImageContent: TypeAlias = PathLike | BytesLike | ImageLike class Image: def __init__( self, - src: str | Path | bytes | bytearray | memoryview | Any | None = None, + src: ImageContent | None = None, *, path=None, # DEPRECATED data=None, # DEPRECATED ): """Create a new image. - :param src: The source from which to load the image. Can be a file path - (relative or absolute, as a string or :any:`pathlib.Path`), raw binary data - in any supported image format, or another Toga image. Can also accept the - platform's native image format; if Pillow is installed, - :any:`PIL.Image.Image` can be used. + :param src: The source from which to load the image. Can be any valid + :any:`image content ` type. :param path: **DEPRECATED** - Use ``src``. :param data: **DEPRECATED** - Use ``src``. :raises FileNotFoundError: If a path is provided, but that path does not exist. diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 94c4a77c2e..2bac3c02eb 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from travertino.size import at_least @@ -9,9 +9,7 @@ from toga.widgets.base import Widget if TYPE_CHECKING: - from pathlib import Path - - from toga.images import ImageT + from toga.images import ImageContent, ImageT def rehint_imageview(image, style, scale=1): @@ -70,17 +68,15 @@ def rehint_imageview(image, style, scale=1): class ImageView(Widget): def __init__( self, - image: str | Path | bytes | bytearray | memoryview | Any | None = None, + image: ImageContent | None = None, id=None, style=None, ): """ Create a new image view. - :param image: The image to display. This can take all the same formats as the - `src` parameter to :class:`toga.Image` -- namely, a file path (as string or - :any:`pathlib.Path`), bytes data in a supported image format, an instance of - the platform's native image type, or :any:`PIL.Image.Image`. + :param image: The image to display. Can be any valid :any:`image content + ` type; or :any:`None` to display no image. :param id: The ID for the widget. :param style: A style object. If no style is provided, a default style will be applied to the widget. @@ -112,14 +108,8 @@ def focus(self): def image(self) -> toga.Image | None: """The image to display. - When setting an image, you can provide: - - * An :class:`~toga.images.Image` instance; or - - * Any value that would be a valid path specifier when creating a new - :class:`~toga.images.Image` instance; or - - * :any:`None` to clear the image view. + When setting an image, you can provide any valid :any:`image content + ` type; or :any:`None` to clear the image view. """ return self._image diff --git a/docs/conf.py b/docs/conf.py index fcdb050fb9..a81c65c660 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,6 @@ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.todo", - "sphinx_autodoc_typehints", "sphinx_tabs.tabs", "crate.sphinx.csv", "sphinx_copybutton", @@ -72,6 +71,7 @@ "members": True, "undoc-members": True, } +autodoc_typehints = "description" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/reference/api/resources/images.rst b/docs/reference/api/resources/images.rst index c9124e89c9..f4f749db9a 100644 --- a/docs/reference/api/resources/images.rst +++ b/docs/reference/api/resources/images.rst @@ -13,14 +13,7 @@ Graphical content of arbitrary size. Usage ----- -An image can be constructed from: - -* A path to a file on disk; -* A blob of bytes containing image data in a known image format; -* A :any:`PIL.Image` object; -* Another :any:`toga.Image`; or -* The native platform representation of an image (see the :ref:`Notes - ` section below for details). +An image can be constructed from a :any:`wide range of sources `: .. code-block:: python @@ -45,6 +38,8 @@ An image can be constructed from: Notes ----- +.. _known-image-formats: + * PNG and JPEG formats are guaranteed to be supported. Other formats are available on some platforms: @@ -65,4 +60,22 @@ Notes Reference --------- +.. c:type:: ImageContent + + When specifying content for an :any:`Image`, you can provide: + + * a string specifying an absolute or relative path to a file in a :ref:`known image + format `; + * an absolute or relative :any:`pathlib.Path` object describing a file in a + :ref:`known image format `; + * a "blob of bytes" data type (:any:`bytes`, :any:`bytearray`, or :any:`memoryview`) + containing raw image data in a :ref:`known image format `; + * an instance of :any:`toga.Image`; or + * if `Pillow `__ is installed, an instance of + :any:`PIL.Image.Image`; or + * an instance of the :ref:`native platform image representation `. + + If a relative path is provided, it will be anchored relative to the module that + defines your Toga application class. + .. autoclass:: toga.Image diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 0007cd88af..8f4fd1f365 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -28,6 +28,7 @@ Helvetica initializer instantiation iOS +ImageT iterable KDE linters