From a438644124028e27df2b1d70244c7673d40ed9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Mon, 20 Nov 2017 22:19:24 +0100 Subject: [PATCH] [preview] Add Animated GIF support --- url_image_preview/resize_gif.py | 86 ++++++++++++++++++++++++++ url_image_preview/url_image_preview.py | 39 +++++++++--- 2 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 url_image_preview/resize_gif.py diff --git a/url_image_preview/resize_gif.py b/url_image_preview/resize_gif.py new file mode 100644 index 0000000..0f04535 --- /dev/null +++ b/url_image_preview/resize_gif.py @@ -0,0 +1,86 @@ +from io import BytesIO +from PIL import Image + + +def resize_gif(mem, path, resize_to): + frames = extract_and_resize_frames(mem, resize_to) + + if len(frames) == 1: + frames[0].save(path, optimize=True) + else: + frames[0].save(path, + optimize=True, + save_all=True, + append_images=frames[1:], + loop=1000) + + +def analyse_image(mem): + """ + Pre-process pass over the image to determine the mode (full or additive). + Necessary as assessing single frames isn't reliable. Need to know the mode + before processing all frames. + """ + image = Image.open(BytesIO(mem)) + results = { + 'size': image.size, + 'mode': 'full', + } + try: + while True: + if image.tile: + tile = image.tile[0] + update_region = tile[1] + update_region_dimensions = update_region[2:] + if update_region_dimensions != image.size: + results['mode'] = 'partial' + break + image.seek(image.tell() + 1) + except EOFError: + pass + return results + + +def extract_and_resize_frames(mem, resize_to): + mode = analyse_image(mem)['mode'] + image = Image.open(BytesIO(mem)) + + i = 0 + palette = image.getpalette() + last_frame = image.convert('RGBA') + + frames = [] + + try: + while True: + ''' + If the GIF uses local colour tables, + each frame will have its own palette. + If not, we need to apply the global palette to the new frame. + ''' + if not image.getpalette(): + image.putpalette(palette) + + new_frame = Image.new('RGBA', image.size) + + ''' + Is this file a "partial"-mode GIF where frames update a region + of a different size to the entire image? + If so, we need to construct the new frame by + pasting it on top of the preceding frames. + ''' + if mode == 'partial': + new_frame.paste(last_frame) + + new_frame.paste(image, (0, 0), image.convert('RGBA')) + + new_frame.thumbnail(resize_to, Image.ANTIALIAS) + frames.append(new_frame) + + i += 1 + last_frame = new_frame + image.seek(image.tell() + 1) + except EOFError: + pass + + return frames diff --git a/url_image_preview/url_image_preview.py b/url_image_preview/url_image_preview.py index cb6ea48..4434d6c 100644 --- a/url_image_preview/url_image_preview.py +++ b/url_image_preview/url_image_preview.py @@ -34,6 +34,7 @@ from gajim.plugins import GajimPlugin from gajim.plugins.helpers import log_calls from url_image_preview.http_functions import get_http_head, get_http_file from url_image_preview.config_dialog import UrlImagePreviewConfigDialog +from url_image_preview.resize_gif import resize_gif log = logging.getLogger('gajim.plugin_system.preview') @@ -257,7 +258,10 @@ class Base(object): loader = GdkPixbuf.PixbufLoader() loader.write(mem) loader.close() - pixbuf = loader.get_pixbuf() + if loader.get_format().get_name() == 'gif': + pixbuf = loader.get_animation() + else: + pixbuf = loader.get_pixbuf() except GLib.GError as error: log.info('Failed to load image using Gdk.Pixbuf') log.debug(error) @@ -273,12 +277,21 @@ class Base(object): array, GdkPixbuf.Colorspace.RGB, True, 8, width, height, width * 4) - thumbnail = pixbuf.scale_simple( - size, size, GdkPixbuf.InterpType.BILINEAR) - try: self._create_path(os.path.dirname(thumbpath)) - thumbnail.savev(thumbpath, 'png', [], []) + height, width = pixbuf.get_height(), pixbuf.get_width() + thumbnail = pixbuf + if isinstance(pixbuf, GdkPixbuf.PixbufAnimation): + if size <= height and size <= width: + resize_gif(mem, thumbpath, (size, size)) + thumbnail = self._load_thumbnail(thumbpath) + else: + self._write_file(thumbpath, mem) + else: + if size <= height and size <= width: + thumbnail = pixbuf.scale_simple( + size, size, GdkPixbuf.InterpType.BILINEAR) + thumbnail.savev(thumbpath, 'png', [], []) except Exception as error: dialogs.ErrorDialog( _('Could not save file'), @@ -286,14 +299,19 @@ class Base(object): 'for image file (see error log for more ' 'information)'), transient_for=app.app.get_active_window()) - log.error(error) + log.exception(error) return return thumbnail - def _load_thumbnail(self, thumbpath): + @staticmethod + def _load_thumbnail(thumbpath): + ext = os.path.splitext(thumbpath)[1] + if ext == '.gif': + return GdkPixbuf.PixbufAnimation.new_from_file(thumbpath) return GdkPixbuf.Pixbuf.new_from_file(thumbpath) - def _write_file(self, path, data): + @staticmethod + def _write_file(path, data): log.info("Writing '%s' of size %d...", path, len(data)) try: with open(path, "wb") as output_file: @@ -327,7 +345,10 @@ class Base(object): anchor = buffer_.create_child_anchor(iter_) - image = Gtk.Image.new_from_pixbuf(pixbuf) + if isinstance(pixbuf, GdkPixbuf.PixbufAnimation): + image = Gtk.Image.new_from_animation(pixbuf) + else: + image = Gtk.Image.new_from_pixbuf(pixbuf) event_box.add(image) event_box.show_all() self.textview.tv.add_child_at_anchor(event_box, anchor)