From 86d869e2470a4a62532ce65e1a4aa0f6546cea05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 5 Feb 2024 14:45:22 +0100 Subject: [PATCH 01/10] anigif WIP --- easy_thumbnails/processors.py | 53 ++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/easy_thumbnails/processors.py b/easy_thumbnails/processors.py index f01ee08b..98890bf8 100644 --- a/easy_thumbnails/processors.py +++ b/easy_thumbnails/processors.py @@ -35,6 +35,27 @@ def _points_table(): yield j +def _call_pil_method(im, method, *args, **kwargs): + """ + call a method on the provided PIL image + if im.n_frames > 1 (image with multiple images, like GIF or WEBP) + call the method on all frames + """ + n_frames = getattr(im, "n_frames", 1) + method = getattr(im, method, None) + if not method: + return None + if n_frames <= 1: + return method(*args, **kwargs) + index = 0 + while index < im.n_frames: + im.seek(index) + temp = method(*args, **kwargs) + im.paste(temp) + index += 1 + return im + + def colorspace(im, bw=False, replace_alpha=False, **kwargs): """ Convert images to the correct color space. @@ -57,7 +78,8 @@ def colorspace(im, bw=False, replace_alpha=False, **kwargs): if im.mode == 'I': # PIL (and pillow) have can't convert 16 bit grayscale images to lower # modes, so manually convert them to an 8 bit grayscale. - im = im.point(list(_points_table()), 'L') + # im = im.point(list(_points_table()), "L") + im = _call_pil_method(im, "point", list(_points_table()), "L") is_transparent = utils.is_transparent(im) is_grayscale = im.mode in ('L', 'LA') @@ -78,8 +100,8 @@ def colorspace(im, bw=False, replace_alpha=False, **kwargs): new_mode = new_mode + 'A' if im.mode != new_mode: - im = im.convert(new_mode) - + # im = im.convert(new_mode) + im = _call_pil_method(im, "convert", new_mode) return im @@ -108,7 +130,8 @@ def autocrop(im, autocrop=False, **kwargs): bg = Image.new('L', im.size, 255) bbox = ImageChops.difference(bw, bg).getbbox() if bbox: - im = im.crop(bbox) + # im = im.crop(bbox) + im = _call_pil_method(im, "crop", bbox) return im @@ -202,9 +225,15 @@ def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, if scale < 1.0 or (scale > 1.0 and upscale): # Resize the image to the target size boundary. Round the scaled # boundary sizes to avoid floating point errors. - im = im.resize((int(round(source_x * scale)), - int(round(source_y * scale))), - resample=Image__Resampling__LANCZOS) + # im = im.resize((int(round(source_x * scale)), + # int(round(source_y * scale))), + # resample=Image__Resampling__LANCZOS) + im = _call_pil_method( + im, + "resize", + (int(round(source_x * scale)), int(round(source_y * scale))), + resample=Image__Resampling__LANCZOS, + ) if crop: # Use integer values now. @@ -274,7 +303,8 @@ def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, diff_y = diff_y - add - remove box = (left, top, right, bottom) # Finally, crop the image! - im = im.crop(box) + # im = im.crop(box) + im = _call_pil_method(im, "crop", box) return im @@ -291,9 +321,11 @@ def filters(im, detail=False, sharpen=False, **kwargs): """ if detail: - im = im.filter(ImageFilter.DETAIL) + # im = im.filter(ImageFilter.DETAIL) + im = _call_pil_method(im, "filter", ImageFilter.DETAIL) if sharpen: - im = im.filter(ImageFilter.SHARPEN) + # im = im.filter(ImageFilter.SHARPEN) + im = _call_pil_method(im, "filter", ImageFilter.SHARPEN) return im @@ -321,5 +353,6 @@ def background(im, size, background=None, **kwargs): if new_im.mode != im.mode: new_im = new_im.convert(im.mode) offset = (size[0]-x)//2, (size[1]-y)//2 + # animated GIF support must manually be added, here. new_im.paste(im, offset) return new_im From 6801c75fba2b560ab1116b589f88d5dafd944009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Sat, 10 Feb 2024 15:59:17 +0100 Subject: [PATCH 02/10] WIP tests --- easy_thumbnails/tests/files/demo.gif | Bin 0 -> 32236 bytes .../tests/test_animated_formats.py | 19 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100755 easy_thumbnails/tests/files/demo.gif create mode 100644 easy_thumbnails/tests/test_animated_formats.py diff --git a/easy_thumbnails/tests/files/demo.gif b/easy_thumbnails/tests/files/demo.gif new file mode 100755 index 0000000000000000000000000000000000000000..8856872a18896dbe83e5cabfa47a26fceb6c98f2 GIT binary patch literal 32236 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

lipcy0UOfd3gg=kWvGgOP; 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 Ol>>Vl5CH;$@BaX~g$6+Y literal 0 HcmV?d00001 diff --git a/easy_thumbnails/tests/test_animated_formats.py b/easy_thumbnails/tests/test_animated_formats.py new file mode 100644 index 00000000..e5086127 --- /dev/null +++ b/easy_thumbnails/tests/test_animated_formats.py @@ -0,0 +1,19 @@ +from PIL import Image, ImageChops, ImageDraw +from PIL.GifImagePlugin import GifImageFile +from easy_thumbnails import processors +from unittest import TestCase + + +class AnimatedGIFProcessorsTests(TestCase): + + demo_gif = 'easy_thumbnails/tests/files/demo.gif' + + def test_scale(self): + with Image.open(self.demo_gif) as im: + frames = im.n_frames + print(frames) + processed = processors.scale_and_crop(im, (100, 100)) + processed_frames = processed.n_frames + self.assertEqual(frames, processed_frames) + print(processed.size) + self.assertEqual(processed.size, (100, 75)) From fdb100b838d54edc655f6b0719896a4ca597c46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 19 Feb 2024 12:35:24 +0100 Subject: [PATCH 03/10] wip, trying things --- easy_thumbnails/processors.py | 99 +++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 29 deletions(-) diff --git a/easy_thumbnails/processors.py b/easy_thumbnails/processors.py index 98890bf8..86748431 100644 --- a/easy_thumbnails/processors.py +++ b/easy_thumbnails/processors.py @@ -1,5 +1,7 @@ import itertools import re +from functools import partial +from io import BytesIO from PIL import Image, ImageChops, ImageFilter from easy_thumbnails import utils @@ -35,25 +37,57 @@ def _points_table(): yield j -def _call_pil_method(im, method, *args, **kwargs): +def _call_pil_method(im, method_name, *args, **kwargs): """ call a method on the provided PIL image if im.n_frames > 1 (image with multiple images, like GIF or WEBP) call the method on all frames """ n_frames = getattr(im, "n_frames", 1) - method = getattr(im, method, None) + method = getattr(im, method_name, None) if not method: return None if n_frames <= 1: return method(*args, **kwargs) index = 0 + print(method) + new_frames = [] while index < im.n_frames: im.seek(index) temp = method(*args, **kwargs) - im.paste(temp) + new_frames.append(temp) index += 1 - return im + write_to = BytesIO() + new_frames[0].save( + write_to, format=im.format, save_all=True, append_images=new_frames[1:] + ) + return Image.open(write_to) + # return im + + +class FrameAware: + def __new__(cls, im): + if getattr(im, "n_frames", 1) > 1: + return super().__new__(cls) + return im + + def __init__(self, im): + self.im = im + + def apply_to_frames(self, method, *args, **kwargs): + new_frames = [] + for i in range(self.im.n_frames): + self.im.seek(i) + new_frames.append(method(*args, **kwargs)) + write_to = BytesIO() + new_frames[0].save( + write_to, format=self.im.format, save_all=True, append_images=new_frames[1:] + ) + return Image.open(write_to) + + def __getattr__(self, key): + method = getattr(self.im, key) + return partial(self.apply_to_frames, method) def colorspace(im, bw=False, replace_alpha=False, **kwargs): @@ -75,29 +109,29 @@ def colorspace(im, bw=False, replace_alpha=False, **kwargs): white. """ - if im.mode == 'I': + if im.mode == "I": # PIL (and pillow) have can't convert 16 bit grayscale images to lower # modes, so manually convert them to an 8 bit grayscale. # im = im.point(list(_points_table()), "L") im = _call_pil_method(im, "point", list(_points_table()), "L") is_transparent = utils.is_transparent(im) - is_grayscale = im.mode in ('L', 'LA') + is_grayscale = im.mode in ("L", "LA") new_mode = im.mode if is_grayscale or bw: - new_mode = 'L' + new_mode = "L" else: - new_mode = 'RGB' + new_mode = "RGB" if is_transparent: if replace_alpha: - if im.mode != 'RGBA': - im = im.convert('RGBA') - base = Image.new('RGBA', im.size, replace_alpha) + if im.mode != "RGBA": + im = im.convert("RGBA") + base = Image.new("RGBA", im.size, replace_alpha) base.paste(im, mask=im) im = base else: - new_mode = new_mode + 'A' + new_mode = new_mode + "A" if im.mode != new_mode: # im = im.convert(new_mode) @@ -119,15 +153,15 @@ def autocrop(im, autocrop=False, **kwargs): if autocrop: # If transparent, flatten. if utils.is_transparent(im): - no_alpha = Image.new('L', im.size, (255)) + no_alpha = Image.new("L", im.size, (255)) no_alpha.paste(im, mask=im.split()[-1]) else: - no_alpha = im.convert('L') + no_alpha = im.convert("L") # Convert to black and white image. - bw = no_alpha.convert('L') + bw = no_alpha.convert("L") # bw = bw.filter(ImageFilter.MedianFilter) # White background. - bg = Image.new('L', im.size, 255) + bg = Image.new("L", im.size, 255) bbox = ImageChops.difference(bw, bg).getbbox() if bbox: # im = im.crop(bbox) @@ -135,8 +169,9 @@ def autocrop(im, autocrop=False, **kwargs): return im -def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, - **kwargs): +def scale_and_crop( + im, size, crop=False, upscale=False, zoom=None, target=None, **kwargs +): """ Handle scaling and cropping the source image. @@ -228,9 +263,13 @@ def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, # im = im.resize((int(round(source_x * scale)), # int(round(source_y * scale))), # resample=Image__Resampling__LANCZOS) - im = _call_pil_method( - im, - "resize", + # im = _call_pil_method( + # im, + # "resize", + # (int(round(source_x * scale)), int(round(source_y * scale))), + # resample=Image__Resampling__LANCZOS, + # ) + im = FrameAware(im).resize( (int(round(source_x * scale)), int(round(source_y * scale))), resample=Image__Resampling__LANCZOS, ) @@ -241,9 +280,9 @@ def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, # Difference between new image size and requested size. diff_x = int(source_x - min(source_x, target_x)) diff_y = int(source_y - min(source_y, target_y)) - if crop != 'scale' and (diff_x or diff_y): + if crop != "scale" and (diff_x or diff_y): if isinstance(target, str): - target = re.match(r'(\d+)?,(\d+)?$', target) + target = re.match(r"(\d+)?,(\d+)?$", target) if target: target = target.groups() if target: @@ -261,8 +300,9 @@ def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, box.append(int(min(source_x, box[0] + target_x))) box.append(int(min(source_y, box[1] + target_y))) # See if an edge cropping argument was provided. - edge_crop = (isinstance(crop, str) and - re.match(r'(?:(-?)(\d+))?,(?:(-?)(\d+))?$', crop)) + edge_crop = isinstance(crop, str) and re.match( + r"(?:(-?)(\d+))?,(?:(-?)(\d+))?$", crop + ) if edge_crop and filter(None, edge_crop.groups()): x_right, x_crop, y_bottom, y_crop = edge_crop.groups() if x_crop: @@ -282,7 +322,7 @@ def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, box[1] = offset box[3] = source_y - (diff_y - offset) # See if the image should be "smart cropped". - elif crop == 'smart': + elif crop == "smart": left = top = 0 right, bottom = source_x, source_y while diff_x: @@ -304,7 +344,8 @@ def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, box = (left, top, right, bottom) # Finally, crop the image! # im = im.crop(box) - im = _call_pil_method(im, "crop", box) + # im = _call_pil_method(im, "crop", box) + im = FrameAware(im).crop(box) return im @@ -349,10 +390,10 @@ def background(im, size, background=None, **kwargs): # there's nothing to do. return im im = colorspace(im, replace_alpha=background, **kwargs) - new_im = Image.new('RGB', size, background) + new_im = Image.new("RGB", size, background) if new_im.mode != im.mode: new_im = new_im.convert(im.mode) - offset = (size[0]-x)//2, (size[1]-y)//2 + offset = (size[0] - x) // 2, (size[1] - y) // 2 # animated GIF support must manually be added, here. new_im.paste(im, offset) return new_im From 53ab06429ab0d391a03d23ad03c69f097c920930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 19 Feb 2024 15:19:16 +0100 Subject: [PATCH 04/10] added tests --- easy_thumbnails/processors.py | 90 ++++++++---------- easy_thumbnails/tests/files/demo.gif | Bin 32236 -> 0 bytes .../tests/test_animated_formats.py | 73 ++++++++++++-- 3 files changed, 104 insertions(+), 59 deletions(-) delete mode 100755 easy_thumbnails/tests/files/demo.gif diff --git a/easy_thumbnails/processors.py b/easy_thumbnails/processors.py index 86748431..bb2094b1 100644 --- a/easy_thumbnails/processors.py +++ b/easy_thumbnails/processors.py @@ -37,34 +37,6 @@ def _points_table(): yield j -def _call_pil_method(im, method_name, *args, **kwargs): - """ - call a method on the provided PIL image - if im.n_frames > 1 (image with multiple images, like GIF or WEBP) - call the method on all frames - """ - n_frames = getattr(im, "n_frames", 1) - method = getattr(im, method_name, None) - if not method: - return None - if n_frames <= 1: - return method(*args, **kwargs) - index = 0 - print(method) - new_frames = [] - while index < im.n_frames: - im.seek(index) - temp = method(*args, **kwargs) - new_frames.append(temp) - index += 1 - write_to = BytesIO() - new_frames[0].save( - write_to, format=im.format, save_all=True, append_images=new_frames[1:] - ) - return Image.open(write_to) - # return im - - class FrameAware: def __new__(cls, im): if getattr(im, "n_frames", 1) > 1: @@ -112,8 +84,7 @@ def colorspace(im, bw=False, replace_alpha=False, **kwargs): if im.mode == "I": # PIL (and pillow) have can't convert 16 bit grayscale images to lower # modes, so manually convert them to an 8 bit grayscale. - # im = im.point(list(_points_table()), "L") - im = _call_pil_method(im, "point", list(_points_table()), "L") + im = FrameAware(im).point(list(_points_table()), "L") is_transparent = utils.is_transparent(im) is_grayscale = im.mode in ("L", "LA") @@ -125,17 +96,31 @@ def colorspace(im, bw=False, replace_alpha=False, **kwargs): if is_transparent: if replace_alpha: - if im.mode != "RGBA": - im = im.convert("RGBA") - base = Image.new("RGBA", im.size, replace_alpha) - base.paste(im, mask=im) - im = base + if not getattr(im, "is_animated", False): + if im.mode != "RGBA": + im = FrameAware(im).convert("RGBA") + base = Image.new("RGBA", im.size, replace_alpha) + base.paste(im, mask=im) + im = base + else: + frames = [] + for i in range(im.n_frames): + im.seek(i) + if im.mode != "RGBA": + im = FrameAware(im).convert("RGBA") + base = Image.new("RGBA", im.size, replace_alpha) + base.paste(im, mask=im) + frames.append(base) + write_to = BytesIO() + frames[0].save( + write_to, format=im.format, save_all=True, append_images=frames[1:] + ) + return Image.open(write_to) else: new_mode = new_mode + "A" if im.mode != new_mode: - # im = im.convert(new_mode) - im = _call_pil_method(im, "convert", new_mode) + im = FrameAware(im).convert(new_mode) return im @@ -165,7 +150,7 @@ def autocrop(im, autocrop=False, **kwargs): bbox = ImageChops.difference(bw, bg).getbbox() if bbox: # im = im.crop(bbox) - im = _call_pil_method(im, "crop", bbox) + im = FrameAware(im).crop(bbox) return im @@ -263,12 +248,6 @@ def scale_and_crop( # im = im.resize((int(round(source_x * scale)), # int(round(source_y * scale))), # resample=Image__Resampling__LANCZOS) - # im = _call_pil_method( - # im, - # "resize", - # (int(round(source_x * scale)), int(round(source_y * scale))), - # resample=Image__Resampling__LANCZOS, - # ) im = FrameAware(im).resize( (int(round(source_x * scale)), int(round(source_y * scale))), resample=Image__Resampling__LANCZOS, @@ -363,10 +342,10 @@ def filters(im, detail=False, sharpen=False, **kwargs): """ if detail: # im = im.filter(ImageFilter.DETAIL) - im = _call_pil_method(im, "filter", ImageFilter.DETAIL) + im = FrameAware(im).filter(ImageFilter.DETAIL) if sharpen: # im = im.filter(ImageFilter.SHARPEN) - im = _call_pil_method(im, "filter", ImageFilter.SHARPEN) + im = FrameAware(im).filter(ImageFilter.SHARPEN) return im @@ -394,6 +373,19 @@ def background(im, size, background=None, **kwargs): if new_im.mode != im.mode: new_im = new_im.convert(im.mode) offset = (size[0] - x) // 2, (size[1] - y) // 2 - # animated GIF support must manually be added, here. - new_im.paste(im, offset) - return new_im + # animated format (gif/webp/...) support manually added. + if not getattr(im, "is_animated", False): + new_im.paste(im, offset) + return new_im + else: + frames = [] + for i in range(im.n_frames): + im.seek(i) + copied_new_im = new_im.copy() + copied_new_im.paste(im, offset) + frames.append(copied_new_im) + write_to = BytesIO() + frames[0].save( + write_to, format=im.format, save_all=True, append_images=frames[1:] + ) + return Image.open(write_to) diff --git a/easy_thumbnails/tests/files/demo.gif b/easy_thumbnails/tests/files/demo.gif deleted file mode 100755 index 8856872a18896dbe83e5cabfa47a26fceb6c98f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32236 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

lipcy0UOfd3gg=kWvGgOP; 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 Ol>>Vl5CH;$@BaX~g$6+Y diff --git a/easy_thumbnails/tests/test_animated_formats.py b/easy_thumbnails/tests/test_animated_formats.py index e5086127..7baa5385 100644 --- a/easy_thumbnails/tests/test_animated_formats.py +++ b/easy_thumbnails/tests/test_animated_formats.py @@ -1,19 +1,72 @@ +from io import BytesIO from PIL import Image, ImageChops, ImageDraw from PIL.GifImagePlugin import GifImageFile from easy_thumbnails import processors from unittest import TestCase -class AnimatedGIFProcessorsTests(TestCase): +def create_animated_image(mode='RGB', format="gif", size=(500, 500)): + frames = [] + for i in range(10): + image = Image.new(mode, size, (255, 255, 255)) + draw = ImageDraw.Draw(image) + x_bit, y_bit = size[0] // 10 * i, size[1] // 10 * i + draw.rectangle((x_bit, y_bit * 2, x_bit * 7, y_bit * 3), 'red') + draw.rectangle((x_bit * 2, y_bit, x_bit * 3, y_bit * 8), 'red') + frames.append(image) + write_to = BytesIO() + frames[0].save( + write_to, format=format, save_all=True, append_images=frames[1:] + ) + return Image.open(write_to) - demo_gif = 'easy_thumbnails/tests/files/demo.gif' + +class AnimatedFormatProcessorsTests(TestCase): def test_scale(self): - with Image.open(self.demo_gif) as im: - frames = im.n_frames - print(frames) - processed = processors.scale_and_crop(im, (100, 100)) - processed_frames = processed.n_frames - self.assertEqual(frames, processed_frames) - print(processed.size) - self.assertEqual(processed.size, (100, 75)) + im = create_animated_image() + frames_count = im.n_frames + self.assertGreater(frames_count, 1) + processed = processors.scale_and_crop(im, (100, 100)) + processed_frames_count = processed.n_frames + self.assertEqual(frames_count, processed_frames_count) + self.assertEqual(processed.size, (100, 100)) + + def test_scale_crop(self): + im = create_animated_image() + frames_count = im.n_frames + processed = processors.scale_and_crop(im, (100, 50), crop=True) + processed_frames_count = processed.n_frames + self.assertEqual(frames_count, processed_frames_count) + self.assertEqual(processed.size, (100, 50)) + + def test_colorspace(self): + # to have a color conversion + im = create_animated_image(format="png") + frames_count = im.n_frames + processed = processors.colorspace(im, bw=True) + processed_frames_count = processed.n_frames + # indeed processed? + self.assertEqual(processed.mode, "L") + self.assertEqual(frames_count, processed_frames_count) + self.assertEqual(processed.size, (500, 500)) + + def test_filter(self): + # to have a color conversion + im = create_animated_image(format="webp") + frames_count = im.n_frames + processed = processors.filters(im, detail=True, sharpen=True) + processed_frames_count = processed.n_frames + # indeed processed? + self.assertEqual(frames_count, processed_frames_count) + self.assertEqual(processed.size, (500, 500)) + + def test_background(self): + # to have a color conversion + im = create_animated_image(format="webp") + frames_count = im.n_frames + processed = processors.background(im, background="#ff00ff", size=(500, 800)) + processed_frames_count = processed.n_frames + # indeed processed? + self.assertEqual(frames_count, processed_frames_count) + self.assertEqual(processed.size, (500, 800)) From dbd8fd3d141d13514ca357815f2acce985cca879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 19 Feb 2024 15:27:40 +0100 Subject: [PATCH 05/10] trigger actions --- easy_thumbnails/processors.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/easy_thumbnails/processors.py b/easy_thumbnails/processors.py index bb2094b1..81c95fe7 100644 --- a/easy_thumbnails/processors.py +++ b/easy_thumbnails/processors.py @@ -245,9 +245,6 @@ def scale_and_crop( if scale < 1.0 or (scale > 1.0 and upscale): # Resize the image to the target size boundary. Round the scaled # boundary sizes to avoid floating point errors. - # im = im.resize((int(round(source_x * scale)), - # int(round(source_y * scale))), - # resample=Image__Resampling__LANCZOS) im = FrameAware(im).resize( (int(round(source_x * scale)), int(round(source_y * scale))), resample=Image__Resampling__LANCZOS, From c53bce13ad674693f0e70f0c779324ec70f3f9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 9 Sep 2024 11:33:53 +0200 Subject: [PATCH 06/10] revert code style changes --- easy_thumbnails/processors.py | 46 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/easy_thumbnails/processors.py b/easy_thumbnails/processors.py index 81c95fe7..e7c1220f 100644 --- a/easy_thumbnails/processors.py +++ b/easy_thumbnails/processors.py @@ -81,34 +81,34 @@ def colorspace(im, bw=False, replace_alpha=False, **kwargs): white. """ - if im.mode == "I": + if im.mode == 'I': # PIL (and pillow) have can't convert 16 bit grayscale images to lower # modes, so manually convert them to an 8 bit grayscale. im = FrameAware(im).point(list(_points_table()), "L") is_transparent = utils.is_transparent(im) - is_grayscale = im.mode in ("L", "LA") + is_grayscale = im.mode in ('L', 'LA') new_mode = im.mode if is_grayscale or bw: - new_mode = "L" + new_mode = 'L' else: - new_mode = "RGB" + new_mode = 'RGB' if is_transparent: if replace_alpha: - if not getattr(im, "is_animated", False): - if im.mode != "RGBA": - im = FrameAware(im).convert("RGBA") - base = Image.new("RGBA", im.size, replace_alpha) + if not getattr(im, 'is_animated', False): + if im.mode != 'RGBA': + im = FrameAware(im).convert('RGBA') + base = Image.new('RGBA', im.size, replace_alpha) base.paste(im, mask=im) im = base else: frames = [] for i in range(im.n_frames): im.seek(i) - if im.mode != "RGBA": - im = FrameAware(im).convert("RGBA") - base = Image.new("RGBA", im.size, replace_alpha) + if im.mode != 'RGBA': + im = FrameAware(im).convert('RGBA') + base = Image.new('RGBA', im.size, replace_alpha) base.paste(im, mask=im) frames.append(base) write_to = BytesIO() @@ -117,7 +117,7 @@ def colorspace(im, bw=False, replace_alpha=False, **kwargs): ) return Image.open(write_to) else: - new_mode = new_mode + "A" + new_mode = new_mode + 'A' if im.mode != new_mode: im = FrameAware(im).convert(new_mode) @@ -138,15 +138,15 @@ def autocrop(im, autocrop=False, **kwargs): if autocrop: # If transparent, flatten. if utils.is_transparent(im): - no_alpha = Image.new("L", im.size, (255)) + no_alpha = Image.new('L', im.size, (255)) no_alpha.paste(im, mask=im.split()[-1]) else: - no_alpha = im.convert("L") + no_alpha = im.convert('L') # Convert to black and white image. - bw = no_alpha.convert("L") + bw = no_alpha.convert('L') # bw = bw.filter(ImageFilter.MedianFilter) # White background. - bg = Image.new("L", im.size, 255) + bg = Image.new('L', im.size, 255) bbox = ImageChops.difference(bw, bg).getbbox() if bbox: # im = im.crop(bbox) @@ -256,9 +256,9 @@ def scale_and_crop( # Difference between new image size and requested size. diff_x = int(source_x - min(source_x, target_x)) diff_y = int(source_y - min(source_y, target_y)) - if crop != "scale" and (diff_x or diff_y): + if crop != 'scale' and (diff_x or diff_y): if isinstance(target, str): - target = re.match(r"(\d+)?,(\d+)?$", target) + target = re.match(r'(\d+)?,(\d+)?$', target) if target: target = target.groups() if target: @@ -277,7 +277,7 @@ def scale_and_crop( box.append(int(min(source_y, box[1] + target_y))) # See if an edge cropping argument was provided. edge_crop = isinstance(crop, str) and re.match( - r"(?:(-?)(\d+))?,(?:(-?)(\d+))?$", crop + r'(?:(-?)(\d+))?,(?:(-?)(\d+))?$', crop ) if edge_crop and filter(None, edge_crop.groups()): x_right, x_crop, y_bottom, y_crop = edge_crop.groups() @@ -297,8 +297,8 @@ def scale_and_crop( else: box[1] = offset box[3] = source_y - (diff_y - offset) - # See if the image should be "smart cropped". - elif crop == "smart": + # See if the image should be 'smart cropped". + elif crop == 'smart': left = top = 0 right, bottom = source_x, source_y while diff_x: @@ -366,12 +366,12 @@ def background(im, size, background=None, **kwargs): # there's nothing to do. return im im = colorspace(im, replace_alpha=background, **kwargs) - new_im = Image.new("RGB", size, background) + new_im = Image.new('RGB', size, background) if new_im.mode != im.mode: new_im = new_im.convert(im.mode) offset = (size[0] - x) // 2, (size[1] - y) // 2 # animated format (gif/webp/...) support manually added. - if not getattr(im, "is_animated", False): + if not getattr(im, 'is_animated', False): new_im.paste(im, offset) return new_im else: From 3a38f7e86e1588a45d5414a5f901b0cdd191364d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 9 Sep 2024 11:36:54 +0200 Subject: [PATCH 07/10] revert code style changes --- easy_thumbnails/processors.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/easy_thumbnails/processors.py b/easy_thumbnails/processors.py index e7c1220f..2b3e1681 100644 --- a/easy_thumbnails/processors.py +++ b/easy_thumbnails/processors.py @@ -154,9 +154,8 @@ def autocrop(im, autocrop=False, **kwargs): return im -def scale_and_crop( - im, size, crop=False, upscale=False, zoom=None, target=None, **kwargs -): +def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, + **kwargs): """ Handle scaling and cropping the source image. From 0af745e3c8e4ef85cdf3b4306ae07ac53e56c239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 9 Sep 2024 15:03:54 +0200 Subject: [PATCH 08/10] some words --- CHANGES.rst | 5 +++++ docs/ref/animated_formats.rst | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 docs/ref/animated_formats.rst diff --git a/CHANGES.rst b/CHANGES.rst index f9a367c0..ece7c0c0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Changes ======= +2.9.0 (2024-09-??) +------------------ +* Experimental support for animated image formats. See documentation for more infos. + + 2.8.5 (2023-01-09) ------------------ * Fix regression introduced in version 2.8.4. Argument ``quality`` is not removed for images diff --git a/docs/ref/animated_formats.rst b/docs/ref/animated_formats.rst new file mode 100644 index 00000000..c8436455 --- /dev/null +++ b/docs/ref/animated_formats.rst @@ -0,0 +1,29 @@ +======================= +Animated images support +======================= + +Support for animated image formats in easy-thumbnails is experimental and must be activated +manually, via `SETTINGS`. + +Example settings, that will preserve GIF, WEBP and PNG formats, but wont allow animations on +PNGs. + +.. code-block:: python + + THUMBNAIL_IMAGE_SAVE_OPTIONS = { + "GIF": {"save_all": True}, # to save all frames available + "WEBP": {"save_all": True}, + "PNG": {"save_all": False}, # dont allow animated PNGs + } + THUMBNAIL_PRESERVE_EXTENSIONS = ("webp", "gif", "png") + + +There have been issues with conversion from GIF to WEBP, so it's currently not recommended to +enable this specific conversion for animated images. + + +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 From e5243bd34e11e7b43437e788fad1cf895a7f1516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 9 Sep 2024 15:08:18 +0200 Subject: [PATCH 09/10] remove commented code --- easy_thumbnails/processors.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/easy_thumbnails/processors.py b/easy_thumbnails/processors.py index 2b3e1681..8bb81bbd 100644 --- a/easy_thumbnails/processors.py +++ b/easy_thumbnails/processors.py @@ -149,7 +149,6 @@ def autocrop(im, autocrop=False, **kwargs): bg = Image.new('L', im.size, 255) bbox = ImageChops.difference(bw, bg).getbbox() if bbox: - # im = im.crop(bbox) im = FrameAware(im).crop(bbox) return im @@ -318,8 +317,6 @@ def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, diff_y = diff_y - add - remove box = (left, top, right, bottom) # Finally, crop the image! - # im = im.crop(box) - # im = _call_pil_method(im, "crop", box) im = FrameAware(im).crop(box) return im @@ -337,10 +334,8 @@ def filters(im, detail=False, sharpen=False, **kwargs): """ if detail: - # im = im.filter(ImageFilter.DETAIL) im = FrameAware(im).filter(ImageFilter.DETAIL) if sharpen: - # im = im.filter(ImageFilter.SHARPEN) im = FrameAware(im).filter(ImageFilter.SHARPEN) return im From 0e25a0285e9b13f476b9671b5d6fe5c7ed19da2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ben=20St=C3=A4hli?= Date: Mon, 9 Sep 2024 17:47:42 +0200 Subject: [PATCH 10/10] tests pass, but --- .../tests/test_animated_formats.py | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/easy_thumbnails/tests/test_animated_formats.py b/easy_thumbnails/tests/test_animated_formats.py index 7baa5385..f3cf81b9 100644 --- a/easy_thumbnails/tests/test_animated_formats.py +++ b/easy_thumbnails/tests/test_animated_formats.py @@ -1,72 +1,83 @@ from io import BytesIO from PIL import Image, ImageChops, ImageDraw -from PIL.GifImagePlugin import GifImageFile from easy_thumbnails import processors from unittest import TestCase -def create_animated_image(mode='RGB', format="gif", size=(500, 500)): +def create_animated_image(mode='RGB', format="gif", size=(1000, 1000), no_frames=6): frames = [] - for i in range(10): + for i in range(no_frames): image = Image.new(mode, size, (255, 255, 255)) draw = ImageDraw.Draw(image) - x_bit, y_bit = size[0] // 10 * i, size[1] // 10 * i + x_bit, y_bit = size[0] // 40 * i, size[1] // 40 * i draw.rectangle((x_bit, y_bit * 2, x_bit * 7, y_bit * 3), 'red') - draw.rectangle((x_bit * 2, y_bit, x_bit * 3, y_bit * 8), 'red') + draw.rectangle((x_bit * 2, y_bit, x_bit * 3, y_bit * 8), 'yellow') frames.append(image) write_to = BytesIO() frames[0].save( write_to, format=format, save_all=True, append_images=frames[1:] ) - return Image.open(write_to) + im = Image.open(write_to) + # for debugging + # with open(f"animated{no_frames}.{format}", "wb") as f: + # write_to.seek(0) + # f.write(write_to.read()) + return im class AnimatedFormatProcessorsTests(TestCase): def test_scale(self): - im = create_animated_image() + no_frames = 20 + im = create_animated_image(no_frames=no_frames) frames_count = im.n_frames - self.assertGreater(frames_count, 1) + self.assertEqual(frames_count, no_frames) processed = processors.scale_and_crop(im, (100, 100)) processed_frames_count = processed.n_frames self.assertEqual(frames_count, processed_frames_count) self.assertEqual(processed.size, (100, 100)) def test_scale_crop(self): - im = create_animated_image() + frames = 9 + im = create_animated_image(no_frames=frames) frames_count = im.n_frames - processed = processors.scale_and_crop(im, (100, 50), crop=True) + self.assertEqual(frames_count, frames) + processed = processors.scale_and_crop(im, (900, 950), crop=True) processed_frames_count = processed.n_frames self.assertEqual(frames_count, processed_frames_count) - self.assertEqual(processed.size, (100, 50)) + self.assertEqual(processed.size, (900, 950)) def test_colorspace(self): # to have a color conversion + no_frames = 6 im = create_animated_image(format="png") frames_count = im.n_frames + self.assertEqual(frames_count, no_frames) processed = processors.colorspace(im, bw=True) processed_frames_count = processed.n_frames # indeed processed? - self.assertEqual(processed.mode, "L") self.assertEqual(frames_count, processed_frames_count) - self.assertEqual(processed.size, (500, 500)) + self.assertEqual(processed.mode, "L") + self.assertEqual(processed.size, (1000, 1000)) def test_filter(self): - # to have a color conversion - im = create_animated_image(format="webp") + no_frames = 12 + im = create_animated_image(format="webp", no_frames=no_frames) frames_count = im.n_frames + self.assertEqual(frames_count, no_frames) processed = processors.filters(im, detail=True, sharpen=True) processed_frames_count = processed.n_frames # indeed processed? self.assertEqual(frames_count, processed_frames_count) - self.assertEqual(processed.size, (500, 500)) + self.assertEqual(processed.size, (1000, 1000)) def test_background(self): - # to have a color conversion - im = create_animated_image(format="webp") + no_frames = 9 + im = create_animated_image(format="webp", no_frames=no_frames) frames_count = im.n_frames - processed = processors.background(im, background="#ff00ff", size=(500, 800)) + self.assertEqual(frames_count, no_frames) + processed = processors.background(im, background="#ff00ff", size=(1000, 1800)) processed_frames_count = processed.n_frames # indeed processed? self.assertEqual(frames_count, processed_frames_count) - self.assertEqual(processed.size, (500, 800)) + self.assertEqual(processed.size, (1000, 1800))