diff -Nru willow-1.1/debian/changelog willow-1.4/debian/changelog --- willow-1.1/debian/changelog 2019-08-04 17:46:42.000000000 +0000 +++ willow-1.4/debian/changelog 2020-07-29 19:51:25.000000000 +0000 @@ -1,3 +1,15 @@ +willow (1.4-1) unstable; urgency=medium + + * Team Upload. + * New upstream version 1.4 (Closes: #951990) + * Run tests with runtests.py + * Add upstream/metadata + * standards version: 4.5.0, compat version:13 + * Add "Rules-Requires-Root:no" + * Run wrap-and-sort + + -- Nilesh Patra Thu, 30 Jul 2020 01:21:25 +0530 + willow (1.1-4) unstable; urgency=medium * Team upload. diff -Nru willow-1.1/debian/control willow-1.4/debian/control --- willow-1.1/debian/control 2019-08-04 17:36:13.000000000 +0000 +++ willow-1.4/debian/control 2020-07-29 19:51:25.000000000 +0000 @@ -3,20 +3,29 @@ Uploaders: Christopher Hoskin Section: python Priority: optional -Build-Depends: dh-python, debhelper-compat (= 11), - python3-setuptools, python3-all, - python3-sphinx, python3-sphinx-rtd-theme, python3-sphinxcontrib.spelling, - python3-mock, python3-pil, python3-opencv, python3-wand -Standards-Version: 4.1.3 +Testsuite: autopkgtest-pkg-python +Build-Depends: debhelper-compat (= 13), + dh-python, + python3-all, + python3-mock, + python3-opencv, + python3-pil, + python3-setuptools, + python3-sphinx, + python3-sphinx-rtd-theme, + python3-sphinxcontrib.spelling, + python3-wand +Standards-Version: 4.5.0 Homepage: https://github.com/torchbox/Willow Vcs-Browser: https://salsa.debian.org/python-team/modules/willow Vcs-Git: https://salsa.debian.org/python-team/modules/willow.git +Rules-Requires-Root: no Package: python3-willow Architecture: all Depends: ${misc:Depends}, ${python3:Depends} Recommends: python3-pil | python3-wand -Suggests: python3-opencv, python-willow-doc +Suggests: python-willow-doc, python3-opencv Description: Python image library combining Pillow, Wand and OpenCV (Python 3) Willow is a simple image library that combines the APIs of Pillow, Wand and OpenCV. It converts the image between the libraries when necessary. @@ -30,7 +39,7 @@ Package: python-willow-doc Architecture: all Section: doc -Depends: ${sphinxdoc:Depends}, ${misc:Depends} +Depends: ${misc:Depends}, ${sphinxdoc:Depends} Description: Python image library (documentation) Willow is a simple image library that combines the APIs of Pillow, Wand and OpenCV. It converts the image between the libraries when necessary. diff -Nru willow-1.1/debian/copyright willow-1.4/debian/copyright --- willow-1.1/debian/copyright 2019-08-04 17:29:59.000000000 +0000 +++ willow-1.4/debian/copyright 2020-07-29 19:50:06.000000000 +0000 @@ -3,7 +3,7 @@ Source: https://github.com/torchbox/Willow Files: * -Copyright: 2014-17 Torchbox Ltd and individual contributors +Copyright: 2014-17 Torchbox Ltd and individual contributors License: BSD-3-clause Files: debian/* @@ -16,28 +16,28 @@ License: BSD-3-clause - All rights reserved. + All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: . 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. + list of conditions and the following disclaimer. . 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/ - or other materials provided with the distribution. + or other materials provided with the distribution. . 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. . THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff -Nru willow-1.1/debian/python-willow-doc.lintian-overrides willow-1.4/debian/python-willow-doc.lintian-overrides --- willow-1.1/debian/python-willow-doc.lintian-overrides 1970-01-01 00:00:00.000000000 +0000 +++ willow-1.4/debian/python-willow-doc.lintian-overrides 2020-07-29 19:46:55.000000000 +0000 @@ -0,0 +1,2 @@ +# This is not a part of any package, but is specific to this package +python-willow-doc: embedded-javascript-library usr/share/doc/python-willow-doc/html/_static/language_data.js * diff -Nru willow-1.1/debian/rules willow-1.4/debian/rules --- willow-1.1/debian/rules 2019-08-04 17:29:59.000000000 +0000 +++ willow-1.4/debian/rules 2020-07-29 19:43:41.000000000 +0000 @@ -9,3 +9,7 @@ dh_auto_build PYTHONPATH=. http_proxy='127.0.0.1:9' python3 -m sphinx -N -bhtml docs/ build/html # HTML generator PYTHONPATH=. http_proxy='127.0.0.1:9' python3 -m sphinx -N -bman docs/ build/man # Manpage generator + + +override_dh_auto_test: + dh_auto_test -- --system=custom --test-args="{interpreter} runtests.py" diff -Nru willow-1.1/debian/tests/control willow-1.4/debian/tests/control --- willow-1.1/debian/tests/control 2019-08-04 17:35:54.000000000 +0000 +++ willow-1.4/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -Test-Command: set -e ; for py in $(py3versions -r 2>/dev/null) ; do cd "$AUTOPKGTEST_TMP" ; echo "Testing with $py:" ; $py -c "import willow; print(willow)" ; done -Depends: python3-all, python3-willow diff -Nru willow-1.1/debian/upstream/metadata willow-1.4/debian/upstream/metadata --- willow-1.1/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ willow-1.4/debian/upstream/metadata 2020-07-29 19:47:14.000000000 +0000 @@ -0,0 +1,7 @@ +--- +Archive: GitHub +Bug-Database: https://github.com/torchbox/Willow/issues +Bug-Submit: https://github.com/torchbox/Willow/issues/new +Changelog: https://github.com/torchbox/Willow/tags +Repository: https://github.com/torchbox/Willow.git +Repository-Browse: https://github.com/torchbox/Willow diff -Nru willow-1.1/docs/changelog.rst willow-1.4/docs/changelog.rst --- willow-1.1/docs/changelog.rst 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/docs/changelog.rst 2020-05-26 09:51:25.000000000 +0000 @@ -1,6 +1,23 @@ Changelog ========= +1.4 (26/05/2020) +---------------- + + - Implemented save quality/lossless options for WebP (@mozgsml) + - Added missing docs for WebP support (@mozgsml) + +1.3 (16/10/2019) +---------------- + + - Added ``.get_frame_count()`` operaton (@kaedroho) + +1.2 (11/10/2019) +---------------- + + - Added WebP support (@frmdstryr) + - Added ``.rotate()`` operaton (@mrchrisadams & @simo97) + 1.1 (04/12/2017) ---------------- diff -Nru willow-1.1/docs/guide/extend.rst willow-1.4/docs/guide/extend.rst --- willow-1.1/docs/guide/extend.rst 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/docs/guide/extend.rst 2020-05-26 09:51:25.000000000 +0000 @@ -148,7 +148,7 @@ .. code-block:: python - willow_image_operations = [NewPillowImage] + willow_image_classes = [NewPillowImage] It can now be registered using the :meth:`Registry.register_plugin` method: diff -Nru willow-1.1/docs/guide/operations.rst willow-1.4/docs/guide/operations.rst --- willow-1.1/docs/guide/operations.rst 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/docs/guide/operations.rst 2020-05-26 09:51:25.000000000 +0000 @@ -23,6 +23,12 @@ # For example, 'i' is a 200x200 pixel image i.get_size() == (200, 200) +For animated GIFs, you can get the number of frames by calling the :meth:`Image.get_frame_count` method: + +.. code-block:: python + + i.get_frame_count() == 34 + Resizing images --------------- @@ -42,6 +48,21 @@ isinstance(i, Image) i.get_size() == (100, 100) +Rotating images +--------------- + +To rotate an image, call the :meth:`~Image.rotate` method. This rotates the image clockwise, by a multiple of 90 degrees (i.e 90, 180, 270). + +It returns a new :class:`~Image` object containing the rotated image. The +original image is not modified. + +.. code-block:: python + # in this case, assume 'i' is a 300x150 pixel image + i = i.rotate(90) + isinstance(i, Image) + i.get_size() == (150, 300) + + Cropping images --------------- diff -Nru willow-1.1/docs/guide/save.rst willow-1.4/docs/guide/save.rst --- willow-1.1/docs/guide/save.rst 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/docs/guide/save.rst 2020-05-26 09:51:25.000000000 +0000 @@ -6,6 +6,7 @@ - :meth:`~Image.save_as_jpeg` - :meth:`~Image.save_as_png` - :meth:`~Image.save_as_gif` + - :meth:`~Image.save_as_webp` All three take one positional argument, the file-like object to write the image data to. @@ -17,12 +18,14 @@ with open('out.png', 'wb') as f: i.save_as_png(f) -Changing the JPEG quality setting +Changing the quality setting --------------------------------- -:meth:`~Image.save_as_jpeg` takes a ``quality`` keyword argument, which is a -number between 1 and 100 which defaults to 85. Decreasing this number will -decrease the output file size at the cost of losing image quality. +:meth:`~Image.save_as_jpeg` and :meth:`~Image.save_as_webp` takes a ``quality`` +keyword argument, which is a number between 1 and 100. It defaults to 85 +for :meth:`~Image.save_as_jpeg` and 80 for :meth:`~Image.save_as_webp`. +Decreasing this number will decrease the output file size at the cost +of losing image quality. For example, to save an image with low quality: @@ -43,6 +46,17 @@ with open('progressive.jpg', 'wb') as f: i.save_as_jpeg(f, progressive=True) +Lossless WebP +----------------- + +You can encode the image to WebP without any loss by setting the +``lossless`` keyword argument to ``True``: + +.. code-block:: python + + with open('lossless.webp', 'wb') as f: + i.save_as_webp(f, lossless=True) + Image optimisation ------------------ diff -Nru willow-1.1/docs/installation.rst willow-1.4/docs/installation.rst --- willow-1.1/docs/installation.rst 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/docs/installation.rst 2020-05-26 09:51:25.000000000 +0000 @@ -1,7 +1,7 @@ Installation ============ -Willow supports Python 2.7+ and 3.3+. It's a pure-python library with no hard +Willow supports Python 2.7+ and 3.4+. It's a pure-python library with no hard dependencies so doesn't require a C compiler for a basic installation. Installation using ``pip`` diff -Nru willow-1.1/docs/reference.rst willow-1.4/docs/reference.rst --- willow-1.1/docs/reference.rst 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/docs/reference.rst 2020-05-26 09:51:25.000000000 +0000 @@ -98,6 +98,14 @@ width, height = image.get_size() +.. method:: get_frame_count() + + Returns the number of frames in an animated image: + + .. code-block:: python + + number_of_frames = image.get_frame_count() + .. method:: has_alpha Returns ``True`` if the image has an alpha channel. @@ -249,6 +257,19 @@ with open('out.gif', 'wb') as f: image.save_as_gif(f) +.. method:: save_as_webp(file, quality=80, lossless=False) + + (Pillow/Wand only) + + Saves the image to the specified file-like object in WEBP format. + + returns a ``WebPImageFile`` wrapping the file. + + .. code-block:: python + + with open('out.webp', 'wb') as f: + image.save_as_webp(f) + .. method:: get_pillow_image() (Pillow only) diff -Nru willow-1.1/.gitignore willow-1.4/.gitignore --- willow-1.1/.gitignore 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/.gitignore 2020-05-26 09:51:25.000000000 +0000 @@ -4,3 +4,4 @@ /Willow.egg-info /dist /venv +.vscode diff -Nru willow-1.1/README.rst willow-1.4/README.rst --- willow-1.1/README.rst 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/README.rst 2020-05-26 09:51:25.000000000 +0000 @@ -7,14 +7,14 @@ Willow image library ==================== -A Python image library that sits on top of Pillow, Wand and OpenCV +A wrapper that combines the functionality of multiple Python image libraries into one API. `Documentation `_ Overview -------- -Willow is a simple image library that combines the APIs of Pillow, Wand and OpenCV. It converts the image between the libraries when necessary. +Willow is a simple image library that combines the APIs of `Pillow `_, `Wand `_ and `OpenCV `_. It converts the image between the libraries when necessary. Willow currently has basic resize and crop operations, face and feature detection and animated GIF support. New operations and library integrations can also be `easily implemented `_. @@ -73,13 +73,16 @@ Operation Pillow Wand OpenCV =================================== ==================== ==================== ==================== ``get_size()`` ✓ ✓ ✓ +``get_frame_count()`` ✓** ✓ ✓** ``resize(size)`` ✓ ✓ ``crop(rect)`` ✓ ✓ +``rotate(angle)`` ✓ ✓ ``set_background_color_rgb(color)`` ✓ ✓ ``auto_orient()`` ✓ ✓ ``save_as_jpeg(file, quality)`` ✓ ✓ ``save_as_png(file)`` ✓ ✓ ``save_as_gif(file)`` ✓ ✓ +``save_as_webp(file, quality)`` ✓ ✓ ``has_alpha()`` ✓ ✓ ✓* ``has_animation()`` ✓* ✓ ✓* ``get_pillow_image()`` ✓ @@ -89,3 +92,4 @@ =================================== ==================== ==================== ==================== \* Always returns ``False`` +\** Always returns ``1`` diff -Nru willow-1.1/setup.py willow-1.4/setup.py --- willow-1.1/setup.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/setup.py 2020-05-26 09:51:25.000000000 +0000 @@ -20,7 +20,7 @@ setup( name='Willow', - version='1.1', + version='1.4', description='A Python image library that sits on top of Pillow, Wand and OpenCV', author='Karl Hobley', author_email='karl@kaed.uk', @@ -29,7 +29,7 @@ include_package_data=True, license='BSD', classifiers=[ - 'Development Status :: 2 - Pre-Alpha', + 'Development Status :: 5 - Production/Stable', 'Topic :: Multimedia :: Graphics', 'Topic :: Multimedia :: Graphics :: Graphics Conversion', 'Intended Audience :: Developers', @@ -39,9 +39,10 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ], install_requires=[], zip_safe=False, Binary files /tmp/tmpOrlbfl/PAhC1XHFm7/willow-1.1/tests/images/tree.webp and /tmp/tmpOrlbfl/H5fpg14SoP/willow-1.4/tests/images/tree.webp differ Binary files /tmp/tmpOrlbfl/PAhC1XHFm7/willow-1.1/tests/images/tux_w_alpha.webp and /tmp/tmpOrlbfl/H5fpg14SoP/willow-1.4/tests/images/tux_w_alpha.webp differ diff -Nru willow-1.1/tests/test_opencv.py willow-1.4/tests/test_opencv.py --- willow-1.1/tests/test_opencv.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/tests/test_opencv.py 2020-05-26 09:51:25.000000000 +0000 @@ -28,6 +28,10 @@ self.assertEqual(width, 600) self.assertEqual(height, 400) + def test_get_frame_count(self): + frames = self.image.get_frame_count() + self.assertEqual(frames, 1) + def test_has_alpha(self): has_alpha = self.image.has_alpha() self.assertFalse(has_alpha) diff -Nru willow-1.1/tests/test_pillow.py willow-1.4/tests/test_pillow.py --- willow-1.1/tests/test_pillow.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/tests/test_pillow.py 2020-05-26 09:51:25.000000000 +0000 @@ -4,8 +4,11 @@ from PIL import Image as PILImage -from willow.image import JPEGImageFile, PNGImageFile, GIFImageFile -from willow.plugins.pillow import _PIL_Image, PillowImage +from willow.image import JPEGImageFile, PNGImageFile, GIFImageFile, WebPImageFile +from willow.plugins.pillow import _PIL_Image, PillowImage, UnsupportedRotation + + +no_webp_support = not PillowImage.is_format_supported("WEBP") class TestPillowOperations(unittest.TestCase): @@ -18,6 +21,10 @@ self.assertEqual(width, 200) self.assertEqual(height, 150) + def test_get_frame_count(self): + frames = self.image.get_frame_count() + self.assertEqual(frames, 1) + def test_resize(self): resized_image = self.image.resize((100, 75)) self.assertEqual(resized_image.get_size(), (100, 75)) @@ -26,6 +33,26 @@ cropped_image = self.image.crop((10, 10, 100, 100)) self.assertEqual(cropped_image.get_size(), (90, 90)) + def test_rotate(self): + rotated_image = self.image.rotate(90) + width, height = rotated_image.get_size() + self.assertEqual((width, height), (150, 200)) + + def test_rotate_without_multiple_of_90(self): + with self.assertRaises(UnsupportedRotation) as e: + rotated_image = self.image.rotate(45) + + def test_rotate_greater_than_360(self): + # 450 should end up the same as a 90 rotation + rotated_image = self.image.rotate(450) + width, height = rotated_image.get_size() + self.assertEqual((width, height), (150, 200)) + + def test_rotate_multiple_of_360(self): + rotated_image = self.image.rotate(720) + width, height = rotated_image.get_size() + self.assertEqual((width, height), (200, 150)) + def test_set_background_color_rgb(self): red_background_image = self.image.set_background_color_rgb((255, 0, 0)) self.assertFalse(red_background_image.has_alpha()) @@ -176,6 +203,55 @@ self.assertIsInstance(pillow_image, _PIL_Image().Image) + @unittest.skipIf(no_webp_support, "Pillow does not have WebP support") + def test_save_as_webp(self): + output = io.BytesIO() + return_value = self.image.save_as_webp(output) + output.seek(0) + + self.assertEqual(imghdr.what(output), 'webp') + self.assertIsInstance(return_value, WebPImageFile) + self.assertEqual(return_value.f, output) + + @unittest.skipIf(no_webp_support, "Pillow does not have WebP support") + def test_open_webp(self): + with open('tests/images/tree.webp', 'rb') as f: + image = PillowImage.open(WebPImageFile(f)) + + self.assertFalse(image.has_alpha()) + self.assertFalse(image.has_animation()) + + @unittest.skipIf(no_webp_support, "Pillow does not have WebP support") + def test_open_webp_w_alpha(self): + with open('tests/images/tux_w_alpha.webp', 'rb') as f: + image = PillowImage.open(WebPImageFile(f)) + + self.assertTrue(image.has_alpha()) + self.assertFalse(image.has_animation()) + + @unittest.skipIf(no_webp_support, "Pillow does not have WebP support") + def test_open_webp_quality(self): + high_quality = self.image.save_as_webp(io.BytesIO(), quality=90) + low_quality = self.image.save_as_webp(io.BytesIO(), quality=30) + self.assertTrue(low_quality.f.tell() < high_quality.f.tell()) + + @unittest.skipIf(no_webp_support, "Pillow does not have WebP support") + def test_open_webp_lossless(self): + original_image = self.image.image + lossless_file = self.image.save_as_webp(io.BytesIO(), lossless=True) + lossless_image = PillowImage.open(lossless_file).image + identically = True + for x in range(original_image.width): + for y in range(original_image.height): + original_pixel = original_image.getpixel((x, y)) + # don't compare fully transparent pixels + if original_pixel[3] == 0: + continue + if original_pixel != lossless_image.getpixel((x, y)): + identically = False + break + self.assertTrue(identically) + class TestPillowImageOrientation(unittest.TestCase): def assert_orientation_landscape_image_is_correct(self, image): diff -Nru willow-1.1/tests/test_wand.py willow-1.4/tests/test_wand.py --- willow-1.1/tests/test_wand.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/tests/test_wand.py 2020-05-26 09:51:25.000000000 +0000 @@ -6,8 +6,11 @@ from PIL import Image as PILImage -from willow.image import JPEGImageFile, PNGImageFile, GIFImageFile -from willow.plugins.wand import _wand_image, WandImage +from willow.image import JPEGImageFile, PNGImageFile, GIFImageFile, WebPImageFile +from willow.plugins.wand import _wand_image, WandImage, UnsupportedRotation + + +no_webp_support = not WandImage.is_format_supported("WEBP") class TestWandOperations(unittest.TestCase): @@ -20,6 +23,10 @@ self.assertEqual(width, 200) self.assertEqual(height, 150) + def test_get_frame_count(self): + frames = self.image.get_frame_count() + self.assertEqual(frames, 1) + def test_resize(self): resized_image = self.image.resize((100, 75)) self.assertEqual(resized_image.get_size(), (100, 75)) @@ -28,6 +35,26 @@ cropped_image = self.image.crop((10, 10, 100, 100)) self.assertEqual(cropped_image.get_size(), (90, 90)) + def test_rotate(self): + rotated_image = self.image.rotate(90) + width, height = rotated_image.get_size() + self.assertEqual((width, height), (150, 200)) + + def test_rotate_without_multiple_of_90(self): + with self.assertRaises(UnsupportedRotation) as e: + rotated_image = self.image.rotate(45) + + def test_rotate_greater_than_360(self): + # 450 should end up the same as a 90 rotation + rotated_image = self.image.rotate(450) + width, height = rotated_image.get_size() + self.assertEqual((width, height), (150, 200)) + + def test_rotate_multiple_of_360(self): + rotated_image = self.image.rotate(720) + width, height = rotated_image.get_size() + self.assertEqual((width, height), (200, 150)) + def test_set_background_color_rgb(self): red_background_image = self.image.set_background_color_rgb((255, 0, 0)) self.assertFalse(red_background_image.has_alpha()) @@ -135,6 +162,8 @@ self.assertTrue(image.has_animation()) + self.assertEqual(image.get_frame_count(), 34) + def test_resize_animated_gif(self): with open('tests/images/newtons_cradle.gif', 'rb') as f: image = WandImage.open(GIFImageFile(f)) @@ -148,6 +177,60 @@ self.assertIsInstance(wand_image, _wand_image().Image) + @unittest.skipIf(no_webp_support, + "imagemagic was not built with WebP support") + def test_save_as_webp(self): + output = io.BytesIO() + return_value = self.image.save_as_webp(output) + output.seek(0) + + self.assertEqual(imghdr.what(output), 'webp') + self.assertIsInstance(return_value, WebPImageFile) + self.assertEqual(return_value.f, output) + + @unittest.skipIf(no_webp_support, + "imagemagic was not built with WebP support") + def test_open_webp(self): + with open('tests/images/tree.webp', 'rb') as f: + image = WandImage.open(WebPImageFile(f)) + + self.assertFalse(image.has_alpha()) + self.assertFalse(image.has_animation()) + + @unittest.skipIf(no_webp_support, + "imagemagic was not built with WebP support") + def test_open_webp_w_alpha(self): + with open('tests/images/tux_w_alpha.webp', 'rb') as f: + image = WandImage.open(WebPImageFile(f)) + + self.assertTrue(image.has_alpha()) + self.assertFalse(image.has_animation()) + + @unittest.skipIf(no_webp_support, + "imagemagic does not have WebP support") + def test_open_webp_quality(self): + high_quality = self.image.save_as_webp(io.BytesIO(), quality=90) + low_quality = self.image.save_as_webp(io.BytesIO(), quality=30) + self.assertTrue(low_quality.f.tell() < high_quality.f.tell()) + + @unittest.skipIf(no_webp_support, + "imagemagic does not have WebP support") + def test_open_webp_lossless(self): + original_image = self.image.image + lossless_file = self.image.save_as_webp(io.BytesIO(), lossless=True) + lossless_image = WandImage.open(lossless_file).image + identically = True + for x in range(original_image.width): + for y in range(original_image.height): + original_pixel = original_image[x, y] + # don't compare fully transparent pixels + if original_pixel.alpha == 0.0: + continue + if original_pixel != lossless_image[x, y]: + identically = False + break + self.assertTrue(identically) + class TestWandImageOrientation(unittest.TestCase): def assert_orientation_landscape_image_is_correct(self, image): diff -Nru willow-1.1/.travis.yml willow-1.4/.travis.yml --- willow-1.1/.travis.yml 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/.travis.yml 2020-05-26 09:51:25.000000000 +0000 @@ -3,11 +3,16 @@ # Test matrix python: - 2.7 - - 3.3 - 3.4 - 3.5 - 3.6 +matrix: + include: + - python: 3.7 + dist: xenial + sudo: true + # Package installation install: - pip install Pillow Wand diff -Nru willow-1.1/willow/image.py willow-1.4/willow/image.py --- willow-1.1/willow/image.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/willow/image.py 2020-05-26 09:51:25.000000000 +0000 @@ -5,6 +5,16 @@ from .utils.deprecation import RemovedInWillow05Warning +try: + imghdr.test_webp +except AttributeError: + # Add in webp test for 2.7 and 3.5, see http://bugs.python.org/issue20197 + def test_webp(h, f): + if h.startswith(b'RIFF') and h[8:12] == b'WEBP': + return 'webp' + imghdr.tests.append(test_webp) + + class UnrecognisedImageFormatError(IOError): pass @@ -82,7 +92,7 @@ def save(self, image_format, output): # Get operation name - if image_format not in ['jpeg', 'png', 'gif', 'bmp', 'tiff']: + if image_format not in ['jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']: raise ValueError("Unknown image format: %s" % image_format) operation_name = 'save_as_' + image_format @@ -158,6 +168,10 @@ format_name = 'tiff' +class WebPImageFile(ImageFile): + format_name = 'webp' + + INITIAL_IMAGE_CLASSES = { # A mapping of image formats to their initial class 'jpeg': JPEGImageFile, @@ -165,6 +179,7 @@ 'gif': GIFImageFile, 'bmp': BMPImageFile, 'tiff': TIFFImageFile, + 'webp': WebPImageFile, } diff -Nru willow-1.1/willow/__init__.py willow-1.4/willow/__init__.py --- willow-1.1/willow/__init__.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/willow/__init__.py 2020-05-26 09:51:25.000000000 +0000 @@ -11,6 +11,7 @@ RGBImageBuffer, RGBAImageBuffer, TIFFImageFile, + WebPImageFile, ) from willow.plugins import pillow, wand, opencv @@ -19,6 +20,7 @@ registry.register_image_class(GIFImageFile) registry.register_image_class(BMPImageFile) registry.register_image_class(TIFFImageFile) + registry.register_image_class(WebPImageFile) registry.register_image_class(RGBImageBuffer) registry.register_image_class(RGBAImageBuffer) @@ -29,4 +31,4 @@ setup() -__version__ = '1.1' +__version__ = '1.4' diff -Nru willow-1.1/willow/plugins/opencv.py willow-1.4/willow/plugins/opencv.py --- willow-1.1/willow/plugins/opencv.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/willow/plugins/opencv.py 2020-05-26 09:51:25.000000000 +0000 @@ -32,6 +32,11 @@ return self.size @Image.operation + def get_frame_count(self): + # Animation is not supported by OpenCV + return 1 + + @Image.operation def has_alpha(self): # Alpha is not supported by OpenCV return False diff -Nru willow-1.1/willow/plugins/pillow.py willow-1.4/willow/plugins/pillow.py --- willow-1.1/willow/plugins/pillow.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/willow/plugins/pillow.py 2020-05-26 09:51:25.000000000 +0000 @@ -7,10 +7,12 @@ GIFImageFile, BMPImageFile, TIFFImageFile, + WebPImageFile, RGBImageBuffer, RGBAImageBuffer, ) +class UnsupportedRotation(Exception): pass def _PIL_Image(): import PIL.Image @@ -25,11 +27,21 @@ def check(cls): _PIL_Image() + @classmethod + def is_format_supported(cls, image_format): + formats = _PIL_Image().registered_extensions() + return image_format in formats.values() + @Image.operation def get_size(self): return self.image.size @Image.operation + def get_frame_count(self): + # Animation is not supported by PIL + return 1 + + @Image.operation def has_alpha(self): img = self.image return img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info) @@ -58,6 +70,40 @@ return PillowImage(self.image.crop(rect)) @Image.operation + def rotate(self, angle): + """ + Accept a multiple of 90 to pass to the underlying Pillow function + to rotate the image. + """ + + Image = _PIL_Image() + ORIENTATION_TO_TRANSPOSE = { + 90: Image.ROTATE_90, + 180: Image.ROTATE_180, + 270: Image.ROTATE_270, + } + + modulo_angle = angle % 360 + + # is we're rotating a multiple of 360, it's the same as a no-op + if not modulo_angle: + return self + + transpose_code = ORIENTATION_TO_TRANSPOSE.get(modulo_angle) + + if not transpose_code: + raise UnsupportedRotation( + "Sorry - we only support right angle rotations - i.e. multiples of 90 degrees" + ) + + # We call "transpose", as it rotates the image, + # updating the height and width, whereas using 'rotate' + # only changes the contents of the image. + rotated = self.image.transpose(transpose_code) + + return PillowImage(rotated) + + @Image.operation def set_background_color_rgb(self, color): if not self.has_alpha(): # Don't change image that doesn't have an alpha channel @@ -130,6 +176,11 @@ return GIFImageFile(f) @Image.operation + def save_as_webp(self, f, quality=80, lossless=False): + self.image.save(f, 'WEBP', quality=quality, lossless=lossless) + return WebPImageFile(f) + + @Image.operation def auto_orient(self): # JPEG files can be orientated using an EXIF tag. # Make sure this orientation is applied to the data @@ -173,6 +224,7 @@ @Image.converter_from(GIFImageFile, cost=200) @Image.converter_from(BMPImageFile) @Image.converter_from(TIFFImageFile) + @Image.converter_from(WebPImageFile) def open(cls, image_file): image_file.f.seek(0) image = _PIL_Image().open(image_file.f) diff -Nru willow-1.1/willow/plugins/wand.py willow-1.4/willow/plugins/wand.py --- willow-1.1/willow/plugins/wand.py 2017-12-04 10:50:23.000000000 +0000 +++ willow-1.4/willow/plugins/wand.py 2020-05-26 09:51:25.000000000 +0000 @@ -1,5 +1,7 @@ from __future__ import absolute_import +from ctypes import c_void_p, c_char_p + import functools from willow.image import ( @@ -10,10 +12,15 @@ BMPImageFile, RGBImageBuffer, RGBAImageBuffer, - TIFFImageFile + TIFFImageFile, + WebPImageFile, ) +class UnsupportedRotation(Exception): + pass + + def _wand_image(): import wand.image return wand.image @@ -29,6 +36,11 @@ return wand.api +def _wand_version(): + import wand.version + return wand.version + + class WandImage(Image): def __init__(self, image): self.image = image @@ -38,15 +50,24 @@ _wand_image() _wand_color() _wand_api() + _wand_version() def _clone(self): return WandImage(self.image.clone()) + @classmethod + def is_format_supported(cls, image_format): + return bool(_wand_version().formats(image_format)) + @Image.operation def get_size(self): return self.image.size @Image.operation + def get_frame_count(self): + return len(self.image.sequence) + + @Image.operation def has_alpha(self): return self.image.alpha_channel @@ -67,6 +88,19 @@ return clone @Image.operation + def rotate(self, angle): + not_a_multiple_of_90 = angle % 90 + + if not_a_multiple_of_90: + raise UnsupportedRotation( + "Sorry - we only support right angle rotations - i.e. multiples of 90 degrees" + ) + + clone = self.image.clone() + clone.rotate(angle) + return WandImage(clone) + + @Image.operation def set_background_color_rgb(self, color): if not self.has_alpha(): # Don't change image that doesn't have an alpha channel @@ -110,6 +144,22 @@ return GIFImageFile(f) @Image.operation + def save_as_webp(self, f, quality=80, lossless=False): + with self.image.convert('webp') as converted: + converted.compression_quality = quality + if lossless: + library = _wand_api().library + library.MagickSetOption.argtypes = [c_void_p, + c_char_p, + c_char_p] + library.MagickSetOption(converted.wand, + "webp:lossless".encode('utf-8'), + "true".encode('utf-8')) + converted.save(file=f) + + return WebPImageFile(f) + + @Image.operation def auto_orient(self): image = self.image @@ -148,6 +198,7 @@ @Image.converter_from(GIFImageFile, cost=150) @Image.converter_from(BMPImageFile, cost=150) @Image.converter_from(TIFFImageFile, cost=150) + @Image.converter_from(WebPImageFile, cost=150) def open(cls, image_file): image_file.f.seek(0) image = _wand_image().Image(file=image_file.f)