Skip to content

Commit

Permalink
api: proper GIF resize
Browse files Browse the repository at this point in the history
* Adds custom behaviour for resizing GIF images, as `PIL.Image.resize`
  does not work out-of-the-box (addresses #41).

* Additionally passes `save_all=True` as a keyword arguement on
  `PIL.Image.save` when serving the file over HTTP (addresses #41).

* Bumps `Pillow`'s version to 3.4, where they introduce functionality
  for creating GIF images out of individual frame images.

* Updates tests to cover GIF resizing.

Signed-off-by: Orestis Melkonian <[email protected]>
  • Loading branch information
omelkonian authored and drjova committed Apr 20, 2017
1 parent e4ff135 commit a7420be
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 8 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Contact us at `[email protected] <mailto:[email protected]>`_
Contributors
^^^^^^^^^^^^

* Alexander Ioannidis <[email protected]>
* Harris Tzovanakis <[email protected]>
* Jiri Kuncar <[email protected]>
* Orestis Melkonian <[email protected]>
* Tibor Simko <[email protected]>
* Alexander Ioannidis <[email protected]>
2 changes: 1 addition & 1 deletion flask_iiif/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def init_app(self, app):
app.context_processor(lambda: ctx)

def init_restful(self, api, prefix='/api/multimedia/image/'):
"""Setup the urls.
"""Set up the urls.
:param str prefix: the url perfix
Expand Down
18 changes: 14 additions & 4 deletions flask_iiif/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
MultimediaImageFormatError, MultimediaImageNotFound, \
MultimediaImageQualityError, MultimediaImageResizeError, \
MultimediaImageRotateError
from .utils import resize_gif


class MultimediaObject(object):
Expand Down Expand Up @@ -66,11 +67,11 @@ class MultimediaImage(MultimediaObject):
@blueprint.route('/serve/<string:uuid>/<string:size>')
def serve_thumbnail(uuid, size):
\"""Serve the image thumbnail.
\"\"\"Serve the image thumbnail.
:param uuid: The document uuid.
:param size: The desired image size.
\"""
\"\"\"
# Initialize the image with the uuid
path = current_app.extensions['iiif'].uuid_to_path(uuid)
image = IIIFImageAPIWrapper.from_file(path)
Expand Down Expand Up @@ -195,7 +196,11 @@ def resize(self, dimensions, resample=None):
" been given").format(width, height)
)

self.image = self.image.resize((width, height), resample=resample)
arguments = dict(size=(width, height), resample=resample)
if self.image.format == 'GIF':
self.image = resize_gif(self.image, **arguments)
else:
self.image = self.image.resize(**arguments)

def crop(self, coordinates):
"""Crop the image.
Expand Down Expand Up @@ -379,7 +384,12 @@ def serve(self, image_format="png", quality=90):
image_buffer = BytesIO()
# transform `image_format` is lower case and not equals to jpg
cleaned_image_format = self._prepare_for_output(image_format)
self.image.save(image_buffer, cleaned_image_format, quality=quality)
save_kwargs = dict(quality=quality)

if self.image.format == 'GIF':
save_kwargs.update(save_all=True)

self.image.save(image_buffer, cleaned_image_format, **save_kwargs)
image_buffer.seek(0)

return image_buffer
Expand Down
2 changes: 1 addition & 1 deletion flask_iiif/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def inner(*args, **kwargs):


def api_decorator(f):
"""API decorator."""
"""Decorate API method."""
@wraps(f)
def inner(*args, **kwargs):
if current_iiif.api_decorator_callback:
Expand Down
48 changes: 48 additions & 0 deletions flask_iiif/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
# more details.

"""Flask-IIIF utilities."""
import shutil
import tempfile
from os.path import dirname, join

from flask import abort, url_for
from PIL import Image, ImageSequence

__all__ = ('iiif_image_url', )

Expand Down Expand Up @@ -52,3 +56,47 @@ def iiif_image_url(**kwargs):
uuid=kwargs.get('uuid'),
version=kwargs.get('version', 'v2'),
)


def create_gif_from_frames(frames, duration=500, loop=0):
"""Create a GIF image.
:param frames: the sequence of frames that resulting GIF should contain
:param duration: the duration of each frame (in milliseconds)
:param loop: the number of iterations of the frames (0 for infinity)
:returns: GIF image
:rtype: PIL.Image
.. note:: Uses ``tempfile``, as PIL allows GIF creation only on ``save``.
"""
# Save GIF to temporary file
tmp = tempfile.mkdtemp(dir=dirname(__file__))
tmp_file = join(tmp, 'temp.gif')

head, tail = frames[0], frames[1:]
head.save(tmp_file, 'GIF',
save_all=True,
append_images=tail,
duration=duration,
loop=loop)

gif_image = Image.open(tmp_file)
assert gif_image.is_animated

# Cleanup temporary file
shutil.rmtree(tmp)

return gif_image


def resize_gif(image, size, resample):
"""Resize a GIF image.
:param image: the original GIF image
:param size: the dimensions to resize to
:param resample: the method of resampling
:returns: resized GIF image
:rtype: PIL.Image
"""
return create_gif_from_frames([frame.resize(size, resample=resample)
for frame in ImageSequence.Iterator(image)])
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def run_tests(self):
'check-manifest>=0.25',
'coverage>=3.7,<4.0',
'isort>=4.2.2',
'pydocstyle>=1.0.0',
'pydocstyle>=2.0.0',
'pytest-cache>=1.0',
'pytest-cov>=1.8.0',
'pytest-pep8>=1.0.6',
Expand Down
21 changes: 21 additions & 0 deletions tests/test_multimedia_image_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

from io import BytesIO

from flask_iiif.utils import create_gif_from_frames

from .helpers import IIIFTestCase


Expand All @@ -28,6 +30,12 @@ def setUp(self):
image = Image.new("RGBA", (1280, 1024), (255, 0, 0, 0))
image.save(tmp_file, 'png')

# create a new gif image
self.image_gif = MultimediaImage(create_gif_from_frames([
Image.new("RGB", (1280, 1024), color)
for color in ['blue', 'yellow', 'red', 'black', 'white']
]))

# Initialize it for our object and create and instance for
# each test
tmp_file.seek(0)
Expand Down Expand Up @@ -65,6 +73,19 @@ def setUp(self):
tmp_file.seek(0)
self.image_tiff = MultimediaImage.from_string(tmp_file)

def test_gif_resize(self):
"""Test image resize function on GIF images."""
# Check original size and GIF properties
self.assertEqual(self.image_gif.image.is_animated, True)
self.assertEqual(self.image_gif.image.n_frames, 5)
self.assertEqual(str(self.image_gif.size()), str((1280, 1024)))

# Assert proper resize and preservation of GIF properties
self.image_gif.resize('720,680')
self.assertEqual(self.image_gif.image.is_animated, True)
self.assertEqual(self.image_gif.image.n_frames, 5)
self.assertEqual(str(self.image_gif.size()), str((720, 680)))

def test_image_resize(self):
"""Test image resize function."""
# Test image size before
Expand Down

0 comments on commit a7420be

Please sign in to comment.