emoji: Add support for animated GIF images.

This commit adds 'resize_gif()' function which extracts each frame,
resize it and coalesces them again to form the resized GIF while
preserving the duration of the GIF. I read some stackoverflow
answers all of which were referring to BiggleZX's script
(https://gist.github.com/BigglesZX/4016539) for working with animated
GIF. I modified the script to fit to our usecase and did some manual
testing but the function was failing for some specific GIFs and was not
preserving the duration of animation. So I went ahead and read about
GIF format itself as well as PIL's `GifImagePlugin` code and came up
with this simple function which gets the worked done in a much cleaner
way. I tested this function on a number of GIF images from giphy.com
and it resized all of them correctly.

Fixes: #9945.
This commit is contained in:
Harshit Bansal
2018-07-17 18:27:09 +00:00
committed by Tim Abbott
parent 1d2f8bed92
commit 25fa9a25ff
3 changed files with 37 additions and 28 deletions

View File

@@ -27,6 +27,7 @@ import base64
import os
import re
from PIL import Image, ImageOps, ExifTags
from PIL.GifImagePlugin import GifImageFile
import io
import random
import logging
@@ -117,26 +118,39 @@ def resize_avatar(image_data: bytes, size: int=DEFAULT_AVATAR_SIZE) -> bytes:
return out.getvalue()
def resize_gif(im: GifImageFile, size: int=DEFAULT_EMOJI_SIZE) -> bytes:
frames = []
duration_info = []
# If 'loop' info is not set then loop for infinite number of times.
loop = im.info.get("loop", 0)
for frame_num in range(0, im.n_frames):
im.seek(frame_num)
new_frame = Image.new("RGBA", im.size)
new_frame.paste(im, (0, 0), im.convert("RGBA"))
new_frame = ImageOps.fit(new_frame, (size, size), Image.ANTIALIAS)
frames.append(new_frame)
duration_info.append(im.info['duration'])
out = io.BytesIO()
frames[0].save(out, save_all=True, optimize=True,
format="GIF", append_images=frames[1:],
duration=duration_info,
loop=loop)
return out.getvalue()
def resize_emoji(image_data: bytes, size: int=DEFAULT_EMOJI_SIZE) -> bytes:
try:
im = Image.open(io.BytesIO(image_data))
image_format = im.format
im = exif_rotate(im)
if image_format == 'GIF' and im.is_animated:
if im.size[0] != im.size[1]:
raise JsonableError(
_("Animated emoji must have the same width and height."))
elif im.size[0] > size:
raise JsonableError(
_("Animated emoji can't be larger than 64px in width or height."))
else:
return image_data
im = ImageOps.fit(im, (size, size), Image.ANTIALIAS)
if image_format == "GIF":
return resize_gif(im, size)
else:
im = exif_rotate(im)
im = ImageOps.fit(im, (size, size), Image.ANTIALIAS)
out = io.BytesIO()
im.save(out, format=image_format)
return out.getvalue()
except IOError:
raise BadImageError("Could not decode image; did you upload an image file?")
out = io.BytesIO()
im.save(out, format=image_format)
return out.getvalue()
### Common