Skip to content

Commit

Permalink
PIL/Pillow image class (#960)
Browse files Browse the repository at this point in the history
* Add new base IImage interface and allow it in Image traits.

* Remove methods from IImageResource.

* Update comments.

* Improve docstrings.

* Add image helper methods and tests.

* Add ArrayImage class (no tests yet, but very thin).

* Add PIL/Pillow-based IImage implementation.

* Improvements to PILImage based on improvements in underlying image code.

* Fix bad import.

* Fix bad toolkit syntax.

* Test imports of image helpers work.

* Fix toolkit imports.

* Fix copyrights and remove __future__ imports.

* Add tests for array image class.

* Improvements to tests.

* Fix missing not in size check.

* Fix conflicting imports.

* Missed a marge conflict.

* Make pillow an optional dependency; fix a bug.

* Apply suggestions from code review

Co-authored-by: Poruri Sai Rahul <[email protected]>

* Restore spaces.

* Remove excess whitespace.

* Missed one suggested change

Co-authored-by: Poruri Sai Rahul <[email protected]>

Co-authored-by: Poruri Sai Rahul <[email protected]>
  • Loading branch information
corranwebster and Poruri Sai Rahul authored Jul 21, 2021
1 parent 997c78f commit c6616a3
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 2 deletions.
1 change: 1 addition & 0 deletions etstool.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"numpy",
"pygments",
"coverage",
"pillow",
}

source_dependencies = {
Expand Down
3 changes: 2 additions & 1 deletion pyface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@
__requires__ = [
'importlib-metadata>=3.6.0; python_version<"3.8"',
'importlib-resources>=1.1.0; python_version<"3.9"',
"traits>=6.2"
"traits>=6.2",
]
__extras_require__ = {
"wx": ["wxpython>=4", "numpy"],
"pyqt": ["pyqt>=4.10", "pygments"],
"pyqt5": ["pyqt5", "pygments"],
"pyside2": ["pyside2", "shiboken2", "pygments"],
"pillow": ["pillow"],
"test": ["packaging"],
}

Expand Down
73 changes: 73 additions & 0 deletions pyface/i_pil_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" The interface for a PIL Image. """

from traits.api import HasStrictTraits, Instance

from pyface.i_image import IImage


class IPILImage(IImage):
""" The interface for a image that wraps a PIL Image.
"""

# 'IPILImage' interface --------------------------------------------

#: The PIL Image instance.
image = Instance("PIL.Image.Image")


class MPILImage(HasStrictTraits):
""" The base implementation mixin for a image that wraps a PIL Image.
"""

# 'IPILImage' interface --------------------------------------------

#: The PIL Image instance.
image = Instance("PIL.Image.Image")

def __init__(self, image, **traits):
super().__init__(image=image, **traits)

def create_bitmap(self, size=None):
""" Creates a bitmap image for this image.
Parameters
----------
size : (int, int) or None
The desired size as a (width, height) tuple, or None if wanting
default image size.
Returns
-------
image : bitmap
The toolkit bitmap corresponding to the image and the specified
size.
"""
from pyface.util.image_helpers import image_to_bitmap
return image_to_bitmap(self.create_image(size))

def create_icon(self, size=None):
""" Creates an icon for this image.
Parameters
----------
size : (int, int) or None
The desired size as a (width, height) tuple, or None if wanting
default icon size.
Returns
-------
image : icon
The toolkit image corresponding to the image and the specified
size as an icon.
"""
from pyface.util.image_helpers import bitmap_to_icon
return bitmap_to_icon(self.create_bitmap(size))
19 changes: 19 additions & 0 deletions pyface/pil_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" The implementation of a IPILImage. """


# Import the toolkit specific version.


from .toolkit import toolkit_object

PILImage = toolkit_object("pil_image:PILImage")
49 changes: 49 additions & 0 deletions pyface/tests/test_pil_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!


import os
import unittest

# importlib.resources is new in Python 3.7, and importlib.resources.files is
# new in Python 3.9, so for Python < 3.9 we must rely on the 3rd party
# importlib_resources package.
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files

from PIL import Image

from ..pil_image import PILImage

IMAGE_PATH = os.fspath(files("pyface.tests") / "images" / "core.png")


class TestPILImage(unittest.TestCase):

def setUp(self):
self.pil_image = Image.open(IMAGE_PATH)

def test_create_image(self):
image = PILImage(self.pil_image)
toolkit_image = image.create_image()
self.assertIsNotNone(toolkit_image)
self.assertEqual(image.image, self.pil_image)

def test_create_bitmap(self):
image = PILImage(self.pil_image)
bitmap = image.create_bitmap()
self.assertIsNotNone(bitmap)

def test_create_icon(self):
image = PILImage(self.pil_image)
icon = image.create_icon()
self.assertIsNotNone(icon)
25 changes: 24 additions & 1 deletion pyface/tests/test_ui_traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@
import os
import unittest

# importlib.resources is new in Python 3.7, and importlib.resources.files is
# new in Python 3.9, so for Python < 3.9 we must rely on the 3rd party
# importlib_resources package.
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files

try:
import PIL.Image
except ImportError:
PIL = None

from traits.api import DefaultValue, HasTraits, TraitError
from traits.testing.optional_dependencies import numpy as np, requires_numpy
from traits.testing.api import UnittestTools
Expand All @@ -30,7 +43,7 @@
)


IMAGE_PATH = os.path.join(os.path.dirname(__file__), "images", "core.png")
IMAGE_PATH = os.fspath(files("pyface.tests") / "images" / "core.png")


class ImageClass(HasTraits):
Expand Down Expand Up @@ -112,6 +125,16 @@ def test_init_array_image(self):
self.assertIsInstance(image_class.image, ArrayImage)
self.assertTrue((image_class.image.data == data).all())

@unittest.skipIf(PIL is None, "PIL/Pillow is not available")
def test_init_pil_image(self):
from pyface.pil_image import PILImage

pil_image = PIL.Image.open(IMAGE_PATH)
image = PILImage(pil_image)
image_class = ImageClass(image=image)

self.assertIsInstance(image_class.image, PILImage)


class TestMargin(unittest.TestCase):
def test_defaults(self):
Expand Down
48 changes: 48 additions & 0 deletions pyface/ui/qt4/pil_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

from pyface.qt.QtGui import QIcon, QPixmap

from traits.api import provides

from pyface.i_pil_image import IPILImage, MPILImage
from pyface.ui.qt4.util.image_helpers import resize_image


@provides(IPILImage)
class PILImage(MPILImage):
""" The toolkit specific implementation of a PILImage.
"""

# ------------------------------------------------------------------------
# 'IImage' interface.
# ------------------------------------------------------------------------

def create_image(self, size=None):
""" Creates a Qt image for this image.
Parameters
----------
size : (int, int) or None
The desired size as a (width, height) tuple, or None if wanting
default image size.
Returns
-------
image : QImage
The toolkit image corresponding to the image and the specified
size.
"""
from PIL.ImageQt import ImageQt
image = ImageQt(self.image)
if size is not None:
return resize_image(image, size)
else:
return image
52 changes: 52 additions & 0 deletions pyface/ui/wx/pil_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

import wx

from traits.api import provides

from pyface.i_pil_image import IPILImage, MPILImage
from pyface.ui.wx.util.image_helpers import resize_image


@provides(IPILImage)
class PILImage(MPILImage):
""" The toolkit specific implementation of a PILImage.
"""

# ------------------------------------------------------------------------
# 'IImage' interface.
# ------------------------------------------------------------------------

def create_image(self, size=None):
""" Creates a Wx image for this image.
Parameters
----------
size : (int, int) or None
The desired size as a (width, height) tuple, or None if wanting
default image size.
Returns
-------
image : wx.Image
The toolkit image corresponding to the image and the specified
size.
"""
image = self.image
wx_image = wx.EmptyImage(self.image.size[0], self.image.size[1])
wx_image.SetData(image.convert("RGB").tobytes())
if image.mode == "RGBA":
wx_image.InitAlpha()
wx_image.SetAlpha(image.getchannel("A").tobytes())
if size is not None:
return resize_image(wx_image, size)
else:
return wx_image

0 comments on commit c6616a3

Please sign in to comment.