diff --git a/docs/ref/animated_formats.rst b/docs/ref/animated_formats.rst index c8436455..d5271612 100644 --- a/docs/ref/animated_formats.rst +++ b/docs/ref/animated_formats.rst @@ -21,9 +21,25 @@ PNGs. There have been issues with conversion from GIF to WEBP, so it's currently not recommended to enable this specific conversion for animated images. +Animated GIF +============ + +Thumbnailing animated GIFs requires extra processing. To avoid this, you can enable the +`RGB_ALWAYS` loading strategy for the GifImagePlugin by adding this to your project: + +.. code-block:: python + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS + +This setting is an optional optimization because it changes how all GIFs are loaded by +Pillow, not just animated GIFs. The `Pillow GIF docs +`_ +explain how this settings works and you can decide if it's the right choice for +your project. Remark ====== In the future, Easy Thumbnails might preserve animated images by default, and/or provide the -option to enable/disable animations for each generated thumbnail. \ No newline at end of file +option to enable/disable animations for each generated thumbnail. diff --git a/easy_thumbnails/processors.py b/easy_thumbnails/processors.py index 7f7ba20c..213c4a7f 100644 --- a/easy_thumbnails/processors.py +++ b/easy_thumbnails/processors.py @@ -3,7 +3,7 @@ from functools import partial from io import BytesIO -from PIL import Image, ImageChops, ImageFilter +from PIL import Image, ImageChops, ImageFilter, GifImagePlugin from easy_thumbnails import utils @@ -55,7 +55,20 @@ def apply_to_frames(self, method, *args, **kwargs): new_frames[0].save( write_to, format=self.im.format, save_all=True, append_images=new_frames[1:] ) - return Image.open(write_to) + + to_return = Image.open(write_to) + # Animated GIFs are always opened in palette mode (P). We need seek through the + # frames to ensure that to_return has the correct mode. + # Background information: + # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#gif + if ( + to_return.format == "GIF" + and to_return.is_animated + and GifImagePlugin.LOADING_STRATEGY != GifImagePlugin.LoadingStrategy.RGB_ALWAYS + ): + for i in range(to_return.n_frames): + to_return.seek(i) + return to_return def __getattr__(self, key): method = getattr(self.im, key) diff --git a/easy_thumbnails/tests/files/animated_mode_p.gif b/easy_thumbnails/tests/files/animated_mode_p.gif new file mode 100644 index 00000000..b09eed01 Binary files /dev/null and b/easy_thumbnails/tests/files/animated_mode_p.gif differ diff --git a/easy_thumbnails/tests/test_animated_formats.py b/easy_thumbnails/tests/test_animated_formats.py index f3cf81b9..7f50cc7c 100644 --- a/easy_thumbnails/tests/test_animated_formats.py +++ b/easy_thumbnails/tests/test_animated_formats.py @@ -1,7 +1,9 @@ from io import BytesIO -from PIL import Image, ImageChops, ImageDraw +from PIL import Image, ImageDraw, GifImagePlugin from easy_thumbnails import processors -from unittest import TestCase +from unittest import TestCase, mock + +from easy_thumbnails.files import get_thumbnailer def create_animated_image(mode='RGB', format="gif", size=(1000, 1000), no_frames=6): @@ -81,3 +83,23 @@ def test_background(self): # indeed processed? self.assertEqual(frames_count, processed_frames_count) self.assertEqual(processed.size, (1000, 1800)) + + def test_gif_with_mode_p(self): + image_path = "easy_thumbnails/tests/files/animated_mode_p.gif" + with open(image_path, "rb") as im: + t = get_thumbnailer(im, image_path) + # Should not fail because of wrong mode and should still be animated. + # https://github.com/SmileyChris/easy-thumbnails/issues/653 + thumbnail = t.get_thumbnail({'size': (500, 50), 'crop': True}) + self.assertTrue(thumbnail.image.is_animated) + + @mock.patch("PIL.GifImagePlugin.LOADING_STRATEGY", GifImagePlugin.LoadingStrategy.RGB_ALWAYS) + def test_gif_with_mode_p__gif_plug_loading_strategy_rgb_always(self): + print("m",GifImagePlugin.LOADING_STRATEGY) + image_path = "easy_thumbnails/tests/files/animated_mode_p.gif" + with open(image_path, "rb") as im: + t = get_thumbnailer(im, image_path) + # Should not fail because of wrong mode and should still be animated. + # https://github.com/SmileyChris/easy-thumbnails/issues/653 + thumbnail = t.get_thumbnail({'size': (500, 50), 'crop': True}) + self.assertTrue(thumbnail.image.is_animated)