From 82924c999ae8417e1392321e83674959231ada04 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 6 Sep 2020 18:54:21 +0200 Subject: [PATCH] Add animated profile picture support --- apps/member/forms.py | 34 +++++++++++++++++++++++++--------- apps/member/views.py | 14 +++++++++++--- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/apps/member/forms.py b/apps/member/forms.py index abefdf2c..04495159 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -3,7 +3,7 @@ import io -from PIL import Image +from PIL import Image, ImageSequence from django import forms from django.conf import settings from django.contrib.auth.forms import AuthenticationForm @@ -82,13 +82,19 @@ class ImageForm(forms.Form): height = forms.FloatField(widget=forms.HiddenInput()) def clean(self): - """Load image and crop""" + """ + Load image and crop + + In the future, when Pillow will support APNG we will be able to + simplify this code to save only PNG/APNG. + """ cleaned_data = super().clean() # Image size is limited by Django DATA_UPLOAD_MAX_MEMORY_SIZE image = cleaned_data.get('image') if image: # Let Pillow detect and load image + # If it is an animation, then there will be multiple frames try: im = Image.open(image) except OSError: @@ -96,20 +102,30 @@ class ImageForm(forms.Form): # but Pil is unable to load it raise forms.ValidationError(_('This image cannot be loaded.')) - # Crop image + # Crop each frame x = cleaned_data.get('x', 0) y = cleaned_data.get('y', 0) w = cleaned_data.get('width', 200) h = cleaned_data.get('height', 200) - im = im.crop((x, y, x + w, y + h)) - im = im.resize( - (settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH), - Image.ANTIALIAS, - ) + frames = [] + for frame in ImageSequence.Iterator(im): + frame = frame.crop((x, y, x + w, y + h)) + frame = frame.resize( + (settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH), + Image.ANTIALIAS, + ) + frames.append(frame) # Save + om = frames.pop(0) # Get first frame + om.info = im.info # Copy metadata image.file = io.BytesIO() - im.save(image.file, "PNG") + if len(frames) > 1: + # Save as GIF + om.save(image.file, "GIF", save_all=True, append_images=list(frames), loop=0) + else: + # Save as PNG + om.save(image.file, "PNG") return cleaned_data diff --git a/apps/member/views.py b/apps/member/views.py index 746f5c94..d0ae106a 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -271,9 +271,17 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det def form_valid(self, form): """Save image to note""" - image_field = form.cleaned_data['image'] - image_field.name = "{}_pic.png".format(self.object.note.pk) - self.object.note.display_image = image_field + image = form.cleaned_data['image'] + + # Rename as a PNG or GIF + extension = image.name.split(".")[-1] + if extension == "gif": + image.name = "{}_pic.gif".format(self.object.note.pk) + else: + image.name = "{}_pic.png".format(self.object.note.pk) + + # Save + self.object.note.display_image = image self.object.note.save() return super().form_valid(form)