Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Pillow animated gifs #76

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ Available operations
Operation Pillow Wand OpenCV
=================================== ==================== ==================== ====================
``get_size()`` ✓ ✓ ✓
``get_frame_count()`` ✓ ✓ ✓**
``get_pixel_count()`` ✓ ✓ ✓
``resize(size)`` ✓ ✓
``crop(rect)`` ✓ ✓
``rotate(angle)`` ✓ ✓
Expand All @@ -82,11 +84,12 @@ Operation Pillow Wand Op
``save_as_png(file)`` ✓ ✓
``save_as_gif(file)`` ✓ ✓
``has_alpha()`` ✓ ✓ ✓*
``has_animation()`` ✓* ✓ ✓*
``has_animation()`` ✓ ✓ ✓*
``get_pillow_image()`` ✓
``get_wand_image()`` ✓
``detect_features()`` ✓
``detect_faces(cascade_filename)`` ✓
=================================== ==================== ==================== ====================

\* Always returns ``False``
\** Always returns ``1``
6 changes: 6 additions & 0 deletions docs/guide/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ height as a tuple of two integers:
# 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
---------------

Expand Down
3 changes: 0 additions & 3 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,3 @@ or Wand.

- `Pillow installation <http://pillow.readthedocs.org/en/3.0.x/installation.html#basic-installation>`_
- `Wand installation <http://docs.wand-py.org/en/0.4.2/guide/install.html>`_

Note that Pillow doesn't support animated GIFs and Wand isn't as fast.
Installing both will give best results.
18 changes: 18 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ Here's a full list of operations provided by Willow out of the box:

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:: get_pixel_count()

Returns the number of pixels in an image. This is just the width, height and number
of frames multiplied together and can be used to work out the amount of memory an
image will used when decompressed:

.. code-block:: python

number_of_pixels = image.get_pixel_count()

.. method:: has_alpha

Returns ``True`` if the image has an alpha channel.
Expand Down
Binary file added tests/images/animatedgifwithtransparency.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions tests/test_opencv.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ def test_get_size(self):
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_get_pixel_count(self):
pixels = self.image.get_pixel_count()
self.assertEqual(pixels, 200 * 150)

def test_has_alpha(self):
has_alpha = self.image.has_alpha()
self.assertFalse(has_alpha)
Expand Down
58 changes: 46 additions & 12 deletions tests/test_pillow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def test_get_size(self):
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_get_pixel_count(self):
pixels = self.image.get_pixel_count()
self.assertEqual(pixels, 200 * 150)

def test_resize(self):
resized_image = self.image.resize((100, 75))
self.assertEqual(resized_image.get_size(), (100, 75))
Expand Down Expand Up @@ -119,15 +127,28 @@ def test_save_as_gif_converts_back_to_supported_mode(self):
output = io.BytesIO()

with open('tests/images/transparent.gif', 'rb') as f:
image = PillowImage.open(GIFImageFile(f))
image.image = image.image.convert('RGB')
image = PillowImage.open_animated(GIFImageFile(f))
image.frames[0] = image.frames[0].convert('RGB')

image.save_as_gif(output)
output.seek(0)

image = _PIL_Image().open(output)
self.assertEqual(image.mode, 'P')

def test_save_as_gif_animated(self):
with open('tests/images/newtons_cradle.gif', 'rb') as f:
image = PillowImage.open_animated(GIFImageFile(f))

output = io.BytesIO()
return_value = image.save_as_gif(output)
output.seek(0)

loaded_image = PillowImage.open_animated(GIFImageFile(output))

self.assertTrue(loaded_image.has_animation())
self.assertEqual(loaded_image.get_frame_count(), 34)

def test_has_alpha(self):
has_alpha = self.image.has_alpha()
self.assertTrue(has_alpha)
Expand All @@ -138,7 +159,7 @@ def test_has_animation(self):

def test_transparent_gif(self):
with open('tests/images/transparent.gif', 'rb') as f:
image = PillowImage.open(GIFImageFile(f))
image = PillowImage.open_animated(GIFImageFile(f))

self.assertTrue(image.has_alpha())
self.assertFalse(image.has_animation())
Expand All @@ -148,7 +169,7 @@ def test_transparent_gif(self):

def test_resize_transparent_gif(self):
with open('tests/images/transparent.gif', 'rb') as f:
image = PillowImage.open(GIFImageFile(f))
image = PillowImage.open_animated(GIFImageFile(f))

resized_image = image.resize((100, 75))

Expand All @@ -160,40 +181,53 @@ def test_resize_transparent_gif(self):

def test_save_transparent_gif(self):
with open('tests/images/transparent.gif', 'rb') as f:
image = PillowImage.open(GIFImageFile(f))
image = PillowImage.open_animated(GIFImageFile(f))

# Save it into memory
f = io.BytesIO()
image.save_as_gif(f)

# Reload it
f.seek(0)
image = PillowImage.open(GIFImageFile(f))
image = PillowImage.open_animated(GIFImageFile(f))

self.assertTrue(image.has_alpha())
self.assertFalse(image.has_animation())

# Check that the alpha of pixel 1,1 is 0
self.assertEqual(image.image.convert('RGBA').getpixel((1, 1))[3], 0)

@unittest.expectedFailure # Pillow doesn't support animation
def test_animated_gif(self):
with open('tests/images/newtons_cradle.gif', 'rb') as f:
image = PillowImage.open(GIFImageFile(f))
image = PillowImage.open_animated(GIFImageFile(f))

self.assertFalse(image.has_alpha())
self.assertTrue(image.has_alpha())
self.assertTrue(image.has_animation())

@unittest.expectedFailure # Pillow doesn't support animation
def test_resize_animated_gif(self):
with open('tests/images/newtons_cradle.gif', 'rb') as f:
image = PillowImage.open(GIFImageFile(f))
image = PillowImage.open_animated(GIFImageFile(f))

resized_image = image.resize((100, 75))

self.assertFalse(resized_image.has_alpha())
self.assertTrue(resized_image.has_alpha())
self.assertTrue(resized_image.has_animation())

def test_save_transparent_animated_gif(self):
with open('tests/images/animatedgifwithtransparency.gif', 'rb') as f:
image = PillowImage.open_animated(GIFImageFile(f))

# Save it into memory
f = io.BytesIO()
image.save_as_gif(f)

# Reload it
f.seek(0)
image = PillowImage.open_animated(GIFImageFile(f))

self.assertTrue(image.has_alpha())
self.assertTrue(image.has_animation())

def test_get_pillow_image(self):
pillow_image = self.image.get_pillow_image()

Expand Down
32 changes: 27 additions & 5 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,19 +309,27 @@ def setUp(self):
self.registry._registered_image_classes.add(self.ImageF)

# Add some operations
# foo - reachable on both ImageB and ImageE
# bar - reachable on Image E but unreachable on ImageF
# baz - only unreachable on ImageF
self.b_foo = 'b_foo'
self.e_foo = 'e_foo'
self.f_unreachable = 'f_unreachable'
self.e_bar = 'e_bar'
self.f_bar = 'f_bar'
self.f_baz = 'f_baz'

self.registry._registered_operations = {
self.ImageB: {
'foo': self.b_foo,
},
self.ImageE: {
'foo': self.e_foo,
'bar': self.e_bar,
},
# An unreachable class
self.ImageF: {
'unreachable': self.f_unreachable,
'bar': self.f_bar,
'baz': self.f_baz,
}
}

Expand Down Expand Up @@ -374,8 +382,22 @@ def test_find_operation_foo_from_a_all_unavailable(self):
self.assertIn("ImageB: missing image library", str(e.exception))
self.assertIn("ImageE: another missing image library", str(e.exception))

def test_find_operation_unreachable_from_a(self):
def test_find_operation_bar_from_a(self):
# This operation has both a reachable and an unreachable class it could go to
func, image_class, path, cost = self.registry.find_operation(self.ImageA, 'bar')

self.assertEqual(func, self.e_bar)
self.assertEqual(image_class, self.ImageE)
self.assertEqual(path, [
(self.conv_a_to_b, self.ImageB),
(self.conv_b_to_d, self.ImageD),
(self.conv_d_to_e, self.ImageE),
])
self.assertEqual(cost, 300)

def test_find_operation_baz_from_a(self):
# This operation is only on the unreachable class so should give exception
with self.assertRaises(UnroutableOperationError) as e:
func, image_class, path, cost = self.registry.find_operation(self.ImageA, 'unreachable')
func, image_class, path, cost = self.registry.find_operation(self.ImageA, 'baz')

self.assertEqual(e.exception.args, ("The operation 'unreachable' is available in the image class 'ImageF' but it can't be converted to from 'ImageA'", ))
self.assertEqual(e.exception.args, ("The operation 'baz' is available in the image class 'ImageF' but it can't be converted to from 'ImageA'", ))
25 changes: 25 additions & 0 deletions tests/test_wand.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def test_get_size(self):
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_get_pixel_count(self):
pixels = self.image.get_pixel_count()
self.assertEqual(pixels, 200 * 150)

def test_resize(self):
resized_image = self.image.resize((100, 75))
self.assertEqual(resized_image.get_size(), (100, 75))
Expand Down Expand Up @@ -122,6 +130,19 @@ def test_save_as_gif(self):
self.assertIsInstance(return_value, GIFImageFile)
self.assertEqual(return_value.f, output)

def test_save_as_gif_animated(self):
with open('tests/images/newtons_cradle.gif', 'rb') as f:
image = WandImage.open(GIFImageFile(f))

output = io.BytesIO()
return_value = image.save_as_gif(output)
output.seek(0)

loaded_image = WandImage.open(GIFImageFile(output))

self.assertTrue(loaded_image.has_animation())
self.assertEqual(loaded_image.get_frame_count(), 34)

def test_has_alpha(self):
has_alpha = self.image.has_alpha()
self.assertTrue(has_alpha)
Expand Down Expand Up @@ -158,6 +179,10 @@ def test_animated_gif(self):

self.assertTrue(image.has_animation())

self.assertEqual(image.get_size(), (480, 360))
self.assertEqual(image.get_frame_count(), 34)
self.assertEqual(image.get_pixel_count(), 480 * 360 * 34)

def test_resize_animated_gif(self):
with open('tests/images/newtons_cradle.gif', 'rb') as f:
image = WandImage.open(GIFImageFile(f))
Expand Down
3 changes: 3 additions & 0 deletions willow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def setup():
WebPImageFile,
)
from willow.plugins import pillow, wand, opencv
from willow.generic_operations import get_pixel_count

registry.register_image_class(JPEGImageFile)
registry.register_image_class(PNGImageFile)
Expand All @@ -28,6 +29,8 @@ def setup():
registry.register_plugin(wand)
registry.register_plugin(opencv)

registry.register_generic_operation(['get_size', 'get_frame_count'], 'get_pixel_count', get_pixel_count)

setup()


Expand Down
8 changes: 8 additions & 0 deletions willow/generic_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Generic operations are automatically registered on all image models that implement their dependencies

# Dependencies: get_size, get_frame_count
def get_pixel_count(image):
width, height = image.get_size()
frames = image.get_frame_count()

return width * height * frames
5 changes: 5 additions & 0 deletions willow/plugins/opencv.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def check(cls):
def get_size(self):
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
Expand Down
Loading