From ae194096d064d6dd02d2a0cfa147fa4730d81f57 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 8 Mar 2019 12:48:22 -0700 Subject: [PATCH 01/18] Allow correct delta generation for GIFs with disposal 2 (Fixes #3665) --- src/PIL/GifImagePlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2ebd8b248..44c32faeb 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,7 +443,10 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if _get_palette_bytes(im_frame) == \ + if encoderinfo["disposal"] == 2: + # If diposing whole frame, treat full new frame as delta + delta = im_frame + elif _get_palette_bytes(im_frame) == \ _get_palette_bytes(previous['im']): delta = ImageChops.subtract_modulo(im_frame, previous['im']) From 1f6d1be7fc609ae436b8ab9e8674ba05163ed8e9 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 8 Mar 2019 12:57:53 -0700 Subject: [PATCH 02/18] Ensure disposal key exists before checking --- src/PIL/GifImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 44c32faeb..7ba178d39 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,7 +443,7 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if encoderinfo["disposal"] == 2: + if "disposal" in encoderinfo and encoderinfo["disposal"] == 2: # If diposing whole frame, treat full new frame as delta delta = im_frame elif _get_palette_bytes(im_frame) == \ From 4a2be2af2d74e049cf53aa894688cb6aecf22522 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 8 Mar 2019 15:56:31 -0700 Subject: [PATCH 03/18] Create gif frame delta by subtracting 0x0 image if disposal is mode 2 --- src/PIL/GifImagePlugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 7ba178d39..d6fd27440 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,9 +443,11 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if "disposal" in encoderinfo and encoderinfo["disposal"] == 2: - # If diposing whole frame, treat full new frame as delta - delta = im_frame + if encoderinfo["disposal"] == 2: + # Entire frame should be delta + # Create delta by subtracting empty image from frame (This is required) + delta = ImageChops.subtract_modulo( + im_frame, Image.new('P', (0,0))) elif _get_palette_bytes(im_frame) == \ _get_palette_bytes(previous['im']): delta = ImageChops.subtract_modulo(im_frame, From c73da62ce949547b1ec6ac32027de6f1708c422b Mon Sep 17 00:00:00 2001 From: Sir Cinnamon Date: Fri, 8 Mar 2019 16:34:44 -0700 Subject: [PATCH 04/18] Ensure disposal key exists before checking --- src/PIL/GifImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d6fd27440..776875138 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,7 +443,7 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if encoderinfo["disposal"] == 2: + if "disposal" in encoderinfo and encoderinfo["disposal"] == 2: # Entire frame should be delta # Create delta by subtracting empty image from frame (This is required) delta = ImageChops.subtract_modulo( From 3b1a1fbfd2863be6141fc024ff69e23bd70ffdc1 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Mon, 11 Mar 2019 07:41:14 -0600 Subject: [PATCH 05/18] Create background image for calculating gif deltas --- src/PIL/GifImagePlugin.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d6fd27440..f301a09eb 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,20 +443,20 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if encoderinfo["disposal"] == 2: - # Entire frame should be delta - # Create delta by subtracting empty image from frame (This is required) - delta = ImageChops.subtract_modulo( - im_frame, Image.new('P', (0,0))) - elif _get_palette_bytes(im_frame) == \ - _get_palette_bytes(previous['im']): + if ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2): + base_image = background + else: + base_image = previous["im"] + + if _get_palette_bytes(im_frame) == \ + _get_palette_bytes(base_image): delta = ImageChops.subtract_modulo(im_frame, - previous['im']) + base_image) else: delta = ImageChops.subtract_modulo( - im_frame.convert('RGB'), previous['im'].convert('RGB')) + im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox: + if not bbox and not ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2): # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ @@ -464,6 +464,7 @@ def _write_multiple_frames(im, fp, palette): continue else: bbox = None + background = Image.new("P", im_frame.size, 0) im_frames.append({ 'im': im_frame, 'bbox': bbox, From 3b74281a2ff91d807fbdcea864374cef58b34124 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Mon, 11 Mar 2019 07:55:37 -0600 Subject: [PATCH 06/18] Fix line lengths and init background out of loop --- src/PIL/GifImagePlugin.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f301a09eb..9d0ce5404 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -423,6 +423,7 @@ def _write_multiple_frames(im, fp, palette): im_frames = [] frame_count = 0 + background = None for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for im_frame in ImageSequence.Iterator(imSequence): @@ -443,20 +444,21 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2): + if ("disposal" in im.encoderinfo \ + and im.encoderinfo["disposal"] == 2): base_image = background else: base_image = previous["im"] if _get_palette_bytes(im_frame) == \ _get_palette_bytes(base_image): - delta = ImageChops.subtract_modulo(im_frame, - base_image) + delta = ImageChops.subtract_modulo(im_frame, base_image) else: delta = ImageChops.subtract_modulo( im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox and not ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2): + if not bbox and not ("disposal" in im.encoderinfo \ + and im.encoderinfo["disposal"] == 2): # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ From 583d731a967c2e1d9b0bfe31bc1e36696115a866 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Mon, 11 Mar 2019 08:02:04 -0600 Subject: [PATCH 07/18] Fix line indents for linting --- src/PIL/GifImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9d0ce5404..dafe6a3f9 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -444,8 +444,8 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if ("disposal" in im.encoderinfo \ - and im.encoderinfo["disposal"] == 2): + if "disposal" in im.encoderinfo \ + and im.encoderinfo["disposal"] == 2: base_image = background else: base_image = previous["im"] @@ -457,8 +457,8 @@ def _write_multiple_frames(im, fp, palette): delta = ImageChops.subtract_modulo( im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox and not ("disposal" in im.encoderinfo \ - and im.encoderinfo["disposal"] == 2): + if not bbox and not ("disposal" in im.encoderinfo + and im.encoderinfo["disposal"] == 2): # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ From 96c5a4c5438c9f95acb47c530935e9abc8d1c31a Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Thu, 14 Mar 2019 14:40:31 -0600 Subject: [PATCH 08/18] Add test for disposal mode 2 gifs --- Tests/test_file_gif.py | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6a4b14d40..9b0d61541 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,6 +1,6 @@ from .helper import unittest, PillowTestCase, hopper, netpbm_available -from PIL import Image, ImagePalette, GifImagePlugin +from PIL import Image, ImagePalette, GifImagePlugin, ImageDraw from io import BytesIO @@ -315,6 +315,55 @@ class TestFileGif(PillowTestCase): img.seek(img.tell() + 1) self.assertEqual(img.disposal_method, i+1) + def test_dispose2_diff(self): + out = self.tempfile('temp.gif') + # 4 backgrounds: White, Grey, Black, Red + im_list = [ + Image.new('RGB', (100, 100), '#fff'), + Image.new('RGB', (100, 100), '#999'), + Image.new('RGB', (100, 100), '#000'), + Image.new('RGB', (100, 100), '#f00'), + ] + # Red circle in center of each frame + for img in im_list: + d = ImageDraw.Draw(img) + d.ellipse([(40,40),(60,60)], fill='#f00') + + # check per frame disposal + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + disposal=2 + ) + + img = Image.open(out) + top_left_pixels = [] + center_pixels = [] + + # # Get pixel in top left + # rgb_img = img.convert('RGB') + # r, g, b = rgb_img.getpixel((1,1)) + # top_left_pixels += [(r,g,b)] + # r, g, b = rgb_img.getpixel((50,50)) + # center_pixels += [(r,g,b)] + + for i in range(3): + rgb_img = img.convert('RGB') + # Get pixel in top left + r, g, b = rgb_img.getpixel((1,1)) + top_left_pixels += [(r,g,b)] + # Get pixel in center + r, g, b = rgb_img.getpixel((50,50)) + center_pixels += [(r,g,b)] + for prev in top_left_pixels[:i]: + # Change background every frame + self.assertNotEqual((r,g,b), prev) + for prev in center_pixels[:i]: + # Center remains red every frame + self.assertEqual((r,g,b), (255,0,0)) + img.seek(img.tell() + 1) + def test_iss634(self): img = Image.open("Tests/images/iss634.gif") # seek to the second frame From 8a36a15ebd8635e4d2be0ea4629d99c50628cb3b Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Thu, 14 Mar 2019 14:41:10 -0600 Subject: [PATCH 09/18] Force include colour table for disposal=2 gifs and pad colour table to be valid when flag is set --- src/PIL/GifImagePlugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dafe6a3f9..13036b1b4 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -476,6 +476,9 @@ def _write_multiple_frames(im, fp, palette): if len(im_frames) > 1: for frame_data in im_frames: im_frame = frame_data['im'] + if("disposal" in frame_data["encoderinfo"] \ + and frame_data["encoderinfo"]["disposal"]==2): + frame_data['encoderinfo']['include_color_table'] = True if not frame_data['bbox']: # global header for s in _get_global_header(im_frame, @@ -585,6 +588,8 @@ def _write_local_header(fp, im, offset, flags): include_color_table = im.encoderinfo.get('include_color_table') if include_color_table: palette_bytes = _get_palette_bytes(im) + # If needed, expand palette to minimum size + while(len(palette_bytes)<9): palette_bytes = palette_bytes*2 color_table_size = _get_color_table_size(palette_bytes) if color_table_size: flags = flags | 128 # local color table flag From 53cfd19a44b49da1603608b747facc4d374678b5 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Thu, 14 Mar 2019 14:44:15 -0600 Subject: [PATCH 10/18] Check encoder info for disposal mode --- src/PIL/GifImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 13036b1b4..1171c1fe1 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -444,8 +444,8 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if "disposal" in im.encoderinfo \ - and im.encoderinfo["disposal"] == 2: + if "disposal" in encoderinfo \ + and encoderinfo["disposal"] == 2: base_image = background else: base_image = previous["im"] @@ -457,8 +457,8 @@ def _write_multiple_frames(im, fp, palette): delta = ImageChops.subtract_modulo( im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox and not ("disposal" in im.encoderinfo - and im.encoderinfo["disposal"] == 2): + if not bbox and not ("disposal" in encoderinfo + and encoderinfo["disposal"] == 2): # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ From 85a07bb3852b76a5f205de27945ed3ee5a41b442 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Thu, 14 Mar 2019 14:51:13 -0600 Subject: [PATCH 11/18] Linting changes --- Tests/test_file_gif.py | 22 +++++++--------------- src/PIL/GifImagePlugin.py | 7 ++++--- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 9b0d61541..7110e34e4 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -327,9 +327,8 @@ class TestFileGif(PillowTestCase): # Red circle in center of each frame for img in im_list: d = ImageDraw.Draw(img) - d.ellipse([(40,40),(60,60)], fill='#f00') + d.ellipse([(40, 40), (60, 60)], fill='#f00') - # check per frame disposal im_list[0].save( out, save_all=True, @@ -341,27 +340,20 @@ class TestFileGif(PillowTestCase): top_left_pixels = [] center_pixels = [] - # # Get pixel in top left - # rgb_img = img.convert('RGB') - # r, g, b = rgb_img.getpixel((1,1)) - # top_left_pixels += [(r,g,b)] - # r, g, b = rgb_img.getpixel((50,50)) - # center_pixels += [(r,g,b)] - for i in range(3): rgb_img = img.convert('RGB') # Get pixel in top left - r, g, b = rgb_img.getpixel((1,1)) - top_left_pixels += [(r,g,b)] + r, g, b = rgb_img.getpixel((1, 1)) + top_left_pixels += [(r, g, b)] # Get pixel in center - r, g, b = rgb_img.getpixel((50,50)) - center_pixels += [(r,g,b)] + r, g, b = rgb_img.getpixel((50, 50)) + center_pixels += [(r, g, b)] for prev in top_left_pixels[:i]: # Change background every frame - self.assertNotEqual((r,g,b), prev) + self.assertNotEqual((r, g, b), prev) for prev in center_pixels[:i]: # Center remains red every frame - self.assertEqual((r,g,b), (255,0,0)) + self.assertEqual((r, g, b), (255, 0, 0)) img.seek(img.tell() + 1) def test_iss634(self): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 1171c1fe1..49417c303 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -476,8 +476,8 @@ def _write_multiple_frames(im, fp, palette): if len(im_frames) > 1: for frame_data in im_frames: im_frame = frame_data['im'] - if("disposal" in frame_data["encoderinfo"] \ - and frame_data["encoderinfo"]["disposal"]==2): + if("disposal" in frame_data["encoderinfo"] + and frame_data["encoderinfo"]["disposal"] == 2): frame_data['encoderinfo']['include_color_table'] = True if not frame_data['bbox']: # global header @@ -589,7 +589,8 @@ def _write_local_header(fp, im, offset, flags): if include_color_table: palette_bytes = _get_palette_bytes(im) # If needed, expand palette to minimum size - while(len(palette_bytes)<9): palette_bytes = palette_bytes*2 + while(len(palette_bytes) < 9): + palette_bytes = palette_bytes*2 color_table_size = _get_color_table_size(palette_bytes) if color_table_size: flags = flags | 128 # local color table flag From 0b630e06dc38b6b4ad301571979a35b1eee8c391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Mar 2019 10:29:33 +1100 Subject: [PATCH 12/18] Test that background colours read are equal to saved colours --- Tests/test_file_gif.py | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 7110e34e4..21a22630e 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -317,18 +317,20 @@ class TestFileGif(PillowTestCase): def test_dispose2_diff(self): out = self.tempfile('temp.gif') + # 4 backgrounds: White, Grey, Black, Red - im_list = [ - Image.new('RGB', (100, 100), '#fff'), - Image.new('RGB', (100, 100), '#999'), - Image.new('RGB', (100, 100), '#000'), - Image.new('RGB', (100, 100), '#f00'), - ] - # Red circle in center of each frame - for img in im_list: + backgrounds = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] + + im_list = [] + for background in backgrounds: + img = Image.new('RGB', (100, 100), background) + + # Red circle in center of each frame d = ImageDraw.Draw(img) d.ellipse([(40, 40), (60, 60)], fill='#f00') + im_list.append(img) + im_list[0].save( out, save_all=True, @@ -337,24 +339,16 @@ class TestFileGif(PillowTestCase): ) img = Image.open(out) - top_left_pixels = [] - center_pixels = [] - for i in range(3): + for i, background in enumerate(backgrounds): + img.seek(i) rgb_img = img.convert('RGB') - # Get pixel in top left - r, g, b = rgb_img.getpixel((1, 1)) - top_left_pixels += [(r, g, b)] - # Get pixel in center - r, g, b = rgb_img.getpixel((50, 50)) - center_pixels += [(r, g, b)] - for prev in top_left_pixels[:i]: - # Change background every frame - self.assertNotEqual((r, g, b), prev) - for prev in center_pixels[:i]: - # Center remains red every frame - self.assertEqual((r, g, b), (255, 0, 0)) - img.seek(img.tell() + 1) + + # Check top left pixel matches background + self.assertEqual(rgb_img.getpixel((0, 0)), background) + + # Center remains red every frame + self.assertEqual(rgb_img.getpixel((50, 50)), (255, 0, 0)) def test_iss634(self): img = Image.open("Tests/images/iss634.gif") From 4b2746fc39afe17abcaf2096250e61bf784d631c Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 22 Mar 2019 08:19:01 -0600 Subject: [PATCH 13/18] Remove disposal 2 duplicate frame exemption and add true delta test --- Tests/test_file_gif.py | 63 ++++++++++++++++++++++++++++++++++----- src/PIL/GifImagePlugin.py | 3 +- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 21a22630e..c754d6ed5 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -315,19 +315,19 @@ class TestFileGif(PillowTestCase): img.seek(img.tell() + 1) self.assertEqual(img.disposal_method, i+1) - def test_dispose2_diff(self): + def test_dispose2_palette(self): out = self.tempfile('temp.gif') # 4 backgrounds: White, Grey, Black, Red - backgrounds = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] + circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] im_list = [] - for background in backgrounds: - img = Image.new('RGB', (100, 100), background) + for circle in circles: + img = Image.new('RGB', (100, 100), (255,0,0)) # Red circle in center of each frame d = ImageDraw.Draw(img) - d.ellipse([(40, 40), (60, 60)], fill='#f00') + d.ellipse([(40, 40), (60, 60)], fill=circle) im_list.append(img) @@ -340,15 +340,62 @@ class TestFileGif(PillowTestCase): img = Image.open(out) - for i, background in enumerate(backgrounds): + for i, circle in enumerate(circles): img.seek(i) rgb_img = img.convert('RGB') # Check top left pixel matches background - self.assertEqual(rgb_img.getpixel((0, 0)), background) + self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0)) # Center remains red every frame - self.assertEqual(rgb_img.getpixel((50, 50)), (255, 0, 0)) + self.assertEqual(rgb_img.getpixel((50, 50)), circle) + + def test_dispose2_diff(self): + out = self.tempfile('temp.gif') + + # 4 frames: red/blue, red/red, blue/blue, red/blue + circles = [ + ((255, 0, 0, 255),(0, 0, 255, 255)), + ((255, 0, 0, 255),(255, 0, 0, 255)), + ((0, 0, 255, 255),(0, 0, 255, 255)), + ((255, 0, 0, 255),(0, 0, 255, 255)) + ] + + im_list = [] + for i in range(len(circles)): + # Transparent BG + img = Image.new('RGBA', (100, 100), (255,255,255,0)) + + # Two circles per frame + d = ImageDraw.Draw(img) + d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) + d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) + + im_list.append(img) + + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + disposal=2, + transparency=0 + ) + + img = Image.open(out) + + for i, colours in enumerate(circles): + img.seek(i) + rgb_img = img.convert('RGBA') + + # Check left circle is correct colour + self.assertEqual(rgb_img.getpixel((20, 50)), colours[0]) + + # Check right circle is correct colour + self.assertEqual(rgb_img.getpixel((80, 50)), colours[1]) + + # Check BG is correct colour + self.assertEqual(rgb_img.getpixel((1, 1)), (255,255,255,0)) + def test_iss634(self): img = Image.open("Tests/images/iss634.gif") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 49417c303..64ef63b2e 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -457,8 +457,7 @@ def _write_multiple_frames(im, fp, palette): delta = ImageChops.subtract_modulo( im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox and not ("disposal" in encoderinfo - and encoderinfo["disposal"] == 2): + if not bbox: # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ From ad70fc73cbd17da4fb28cb00303ec2a52f82ba38 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 22 Mar 2019 08:26:16 -0600 Subject: [PATCH 14/18] Linting changes --- Tests/test_file_gif.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index c754d6ed5..36a2b2648 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -323,7 +323,7 @@ class TestFileGif(PillowTestCase): im_list = [] for circle in circles: - img = Image.new('RGB', (100, 100), (255,0,0)) + img = Image.new('RGB', (100, 100), (255, 0, 0)) # Red circle in center of each frame d = ImageDraw.Draw(img) @@ -355,16 +355,16 @@ class TestFileGif(PillowTestCase): # 4 frames: red/blue, red/red, blue/blue, red/blue circles = [ - ((255, 0, 0, 255),(0, 0, 255, 255)), - ((255, 0, 0, 255),(255, 0, 0, 255)), - ((0, 0, 255, 255),(0, 0, 255, 255)), - ((255, 0, 0, 255),(0, 0, 255, 255)) + ((255, 0, 0, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (255, 0, 0, 255)), + ((0, 0, 255, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (0, 0, 255, 255)) ] im_list = [] for i in range(len(circles)): # Transparent BG - img = Image.new('RGBA', (100, 100), (255,255,255,0)) + img = Image.new('RGBA', (100, 100), (255, 255, 255, 0)) # Two circles per frame d = ImageDraw.Draw(img) @@ -394,8 +394,7 @@ class TestFileGif(PillowTestCase): self.assertEqual(rgb_img.getpixel((80, 50)), colours[1]) # Check BG is correct colour - self.assertEqual(rgb_img.getpixel((1, 1)), (255,255,255,0)) - + self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0)) def test_iss634(self): img = Image.open("Tests/images/iss634.gif") From 7443e6d36beec01674e36e6ee11424cbb0e8786e Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Tue, 9 Apr 2019 08:23:59 -0600 Subject: [PATCH 15/18] Clean up disposal flag check --- src/PIL/GifImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 64ef63b2e..4f4e62d6d 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -444,8 +444,7 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if "disposal" in encoderinfo \ - and encoderinfo["disposal"] == 2: + if disposal == 2: base_image = background else: base_image = previous["im"] From 3e4db05249dd4d409243668bcd377034bf40abc8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 23:02:17 +1000 Subject: [PATCH 16/18] Removed code not required by tests --- src/PIL/GifImagePlugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d6d646c59..d541483b8 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -471,9 +471,6 @@ def _write_multiple_frames(im, fp, palette): if len(im_frames) > 1: for frame_data in im_frames: im_frame = frame_data["im"] - if("disposal" in frame_data["encoderinfo"] - and frame_data["encoderinfo"]["disposal"] == 2): - frame_data['encoderinfo']['include_color_table'] = True if not frame_data["bbox"]: # global header for s in _get_global_header(im_frame, frame_data["encoderinfo"]): From 97c15a245c603f615417f28a9e4b31b06fe9060a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 19:24:12 +1000 Subject: [PATCH 17/18] Corrected color table size calculation --- Tests/test_file_gif.py | 2 +- src/PIL/GifImagePlugin.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 1f0358bec..0c4a13a5b 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -59,7 +59,7 @@ class TestFileGif(PillowTestCase): return len(test_file.getvalue()) self.assertEqual(test_grayscale(0), 800) - self.assertEqual(test_grayscale(1), 38) + self.assertEqual(test_grayscale(1), 44) self.assertEqual(test_bilevel(0), 800) self.assertEqual(test_bilevel(1), 800) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d541483b8..f55ca4471 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -580,9 +580,6 @@ def _write_local_header(fp, im, offset, flags): include_color_table = im.encoderinfo.get("include_color_table") if include_color_table: palette_bytes = _get_palette_bytes(im) - # If needed, expand palette to minimum size - while(len(palette_bytes) < 9): - palette_bytes = palette_bytes*2 color_table_size = _get_color_table_size(palette_bytes) if color_table_size: flags = flags | 128 # local color table flag @@ -693,10 +690,12 @@ def _get_color_table_size(palette_bytes): # calculate the palette size for the header import math - color_table_size = int(math.ceil(math.log(len(palette_bytes) // 3, 2))) - 1 - if color_table_size < 0: - color_table_size = 0 - return color_table_size + if not palette_bytes: + return 0 + elif len(palette_bytes) < 9: + return 1 + else: + return int(math.ceil(math.log(len(palette_bytes) // 3, 2))) - 1 def _get_header_palette(palette_bytes): From 90d3d3716457c1bf69fd4814759350fc46383c0d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 23:06:45 +1000 Subject: [PATCH 18/18] Do not presume that the background color index is 0 --- Tests/test_file_gif.py | 52 +++++++++++++++++++++++++-------------- src/PIL/GifImagePlugin.py | 48 ++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0c4a13a5b..45409cbc6 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -319,14 +319,14 @@ class TestFileGif(PillowTestCase): self.assertEqual(img.disposal_method, i + 1) def test_dispose2_palette(self): - out = self.tempfile('temp.gif') + out = self.tempfile("temp.gif") # 4 backgrounds: White, Grey, Black, Red circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] im_list = [] for circle in circles: - img = Image.new('RGB', (100, 100), (255, 0, 0)) + img = Image.new("RGB", (100, 100), (255, 0, 0)) # Red circle in center of each frame d = ImageDraw.Draw(img) @@ -334,18 +334,13 @@ class TestFileGif(PillowTestCase): im_list.append(img) - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=2 - ) + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) img = Image.open(out) for i, circle in enumerate(circles): img.seek(i) - rgb_img = img.convert('RGB') + rgb_img = img.convert("RGB") # Check top left pixel matches background self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0)) @@ -354,20 +349,20 @@ class TestFileGif(PillowTestCase): self.assertEqual(rgb_img.getpixel((50, 50)), circle) def test_dispose2_diff(self): - out = self.tempfile('temp.gif') + out = self.tempfile("temp.gif") # 4 frames: red/blue, red/red, blue/blue, red/blue circles = [ ((255, 0, 0, 255), (0, 0, 255, 255)), ((255, 0, 0, 255), (255, 0, 0, 255)), ((0, 0, 255, 255), (0, 0, 255, 255)), - ((255, 0, 0, 255), (0, 0, 255, 255)) + ((255, 0, 0, 255), (0, 0, 255, 255)), ] im_list = [] for i in range(len(circles)): # Transparent BG - img = Image.new('RGBA', (100, 100), (255, 255, 255, 0)) + img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) # Two circles per frame d = ImageDraw.Draw(img) @@ -377,18 +372,14 @@ class TestFileGif(PillowTestCase): im_list.append(img) im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=2, - transparency=0 + out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 ) img = Image.open(out) for i, colours in enumerate(circles): img.seek(i) - rgb_img = img.convert('RGBA') + rgb_img = img.convert("RGBA") # Check left circle is correct colour self.assertEqual(rgb_img.getpixel((20, 50)), colours[0]) @@ -399,6 +390,31 @@ class TestFileGif(PillowTestCase): # Check BG is correct colour self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0)) + def test_dispose2_background(self): + out = self.tempfile("temp.gif") + + im_list = [] + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(50, 0), (100, 100)], fill="#f00") + d.rectangle([(0, 0), (50, 100)], fill="#0f0") + im_list.append(im) + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], fill="#f00") + d.rectangle([(0, 50), (100, 100)], fill="#0f0") + im_list.append(im) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 + ) + + im = Image.open(out) + im.seek(1) + self.assertEqual(im.getpixel((0, 0)), 0) + def test_iss634(self): img = Image.open("Tests/images/iss634.gif") # seek to the second frame diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f55ca4471..bbf1c603f 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -426,9 +426,8 @@ def _write_multiple_frames(im, fp, palette): im_frames = [] frame_count = 0 - background = None - for imSequence in itertools.chain([im], - im.encoderinfo.get("append_images", [])): + background_im = None + for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for im_frame in ImageSequence.Iterator(imSequence): # a copy is required here since seek can still mutate the image im_frame = _normalize_mode(im_frame.copy()) @@ -447,16 +446,23 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if disposal == 2: - base_image = background + if encoderinfo.get("disposal") == 2: + if background_im is None: + background = _get_background( + im, + im.encoderinfo.get("background", im.info.get("background")), + ) + background_im = Image.new("P", im_frame.size, background) + background_im.putpalette(im_frames[0]["im"].palette) + base_im = background_im else: - base_image = previous["im"] - - if _get_palette_bytes(im_frame) == _get_palette_bytes(base_frame): - delta = ImageChops.subtract_modulo(im_frame, base_image) + base_im = previous["im"] + if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): + delta = ImageChops.subtract_modulo(im_frame, base_im) else: delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_image.convert("RGB")) + im_frame.convert("RGB"), base_im.convert("RGB") + ) bbox = delta.getbbox() if not bbox: # This frame is identical to the previous frame @@ -465,7 +471,6 @@ def _write_multiple_frames(im, fp, palette): continue else: bbox = None - background = Image.new("P", im_frame.size, 0) im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) if len(im_frames) > 1: @@ -726,6 +731,18 @@ def _get_palette_bytes(im): return im.palette.palette +def _get_background(im, infoBackground): + background = 0 + if infoBackground: + background = infoBackground + if isinstance(background, tuple): + # WebPImagePlugin stores an RGBA value in info["background"] + # So it must be converted to the same format as GifImagePlugin's + # info["background"] - a global color table index + background = im.palette.getcolor(background) + return background + + def _get_global_header(im, info): """Return a list of strings representing a GIF header""" @@ -745,14 +762,7 @@ def _get_global_header(im, info): if im.info.get("version") == b"89a": version = b"89a" - background = 0 - if "background" in info: - background = info["background"] - if isinstance(background, tuple): - # WebPImagePlugin stores an RGBA value in info["background"] - # So it must be converted to the same format as GifImagePlugin's - # info["background"] - a global color table index - background = im.palette.getcolor(background) + background = _get_background(im, info.get("background")) palette_bytes = _get_palette_bytes(im) color_table_size = _get_color_table_size(palette_bytes)