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

api: proper GIF resize #42

Merged
merged 1 commit into from
Apr 20, 2017
Merged
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
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