From 5f3fcf105f7e7eeb1e34b4ad67cce62d73b5b82d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2016 10:23:43 +1100 Subject: [PATCH 1/4] Removed unnecessary copy operation --- PIL/GifImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 41d6dcc1d..8ad78df19 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -378,7 +378,7 @@ def _save(im, fp, filename, save_all=False): first_frame = None # delta frame - delta = ImageChops.subtract_modulo(im_frame, previous.copy()) + delta = ImageChops.subtract_modulo(im_frame, previous) bbox = delta.getbbox() if bbox: From 7c0631b4c6fa14f128459bc4c66e4a8f6228a4c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2016 11:04:41 +1100 Subject: [PATCH 2/4] Resolved GifImagePlugin FIXME --- PIL/GifImagePlugin.py | 69 ++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 8ad78df19..9580c6728 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -348,51 +348,58 @@ def _save(im, fp, filename, save_all=False): im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) if save_all: - previous = None - - first_frame = None - append_images = im.encoderinfo.get("append_images", []) + # To specify duration, add the time in milliseconds to getdata(), + # e.g. getdata(im_frame, duration=1000) if "duration" in im.encoderinfo: duration = im.encoderinfo["duration"] else: duration = None + im_frames = [] + append_images = im.encoderinfo.get("append_images", []) frame_count = 0 for imSequence in [im]+append_images: for im_frame in ImageSequence.Iterator(imSequence): - encoderinfo = im.encoderinfo.copy() im_frame = _convert_mode(im_frame) + + encoderinfo = im.encoderinfo.copy() if isinstance(duration, (list, tuple)): - encoderinfo["duration"] = duration[frame_count] + encoderinfo['duration'] = duration[frame_count] frame_count += 1 - # To specify duration, add the time in milliseconds to getdata(), - # e.g. getdata(im_frame, duration=1000) - if not previous: - # global header - first_frame = getheader(im_frame, palette, encoderinfo)[0] - first_frame += getdata(im_frame, (0, 0), **encoderinfo) - else: - if first_frame: - for s in first_frame: - fp.write(s) - first_frame = None - + if im_frames: # delta frame - delta = ImageChops.subtract_modulo(im_frame, previous) + previous = im_frames[-1] + delta = ImageChops.subtract_modulo(im_frame, + previous['im_frame']) bbox = delta.getbbox() - - if bbox: - # compress difference - encoderinfo['include_color_table'] = True - for s in getdata(im_frame.crop(bbox), - bbox[:2], **encoderinfo): - fp.write(s) - else: - # FIXME: what should we do in this case? - pass - previous = im_frame - if first_frame: + if not bbox: + # This frame is identical to the previous frame + if duration: + previous['encoderinfo']['duration'] += encoderinfo['duration'] + continue + else: + bbox = None + im_frames.append({ + "im_frame":im_frame, + "bbox":bbox, + "encoderinfo":encoderinfo + }) + if len(im_frames) < 2: save_all = False + else: + for data in im_frames: + if data['bbox'] is None: + # global header + header = getheader(data['im_frame'], palette, data['encoderinfo'])[0] + for s in header + getdata(data['im_frame'], + (0, 0), **data['encoderinfo']): + fp.write(s) + else: + # compress difference + data['encoderinfo']['include_color_table'] = True + for s in getdata(data['im_frame'].crop(data['bbox']), + data['bbox'][:2], **data['encoderinfo']): + fp.write(s) if not save_all: header = getheader(im_out, palette, im.encoderinfo)[0] for s in header: From 9ff5cab8b1fce5bf860ac9780402adedea0e8b44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2016 11:28:58 +1100 Subject: [PATCH 3/4] Added test --- Tests/test_file_gif.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 94a8ea92c..75aafbc72 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -56,7 +56,7 @@ class TestFileGif(PillowTestCase): # 256 color Palette image, posterize to > 128 and < 128 levels # Size bigger and smaller than 512x512 # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB + # Check for correctness after conversion back to RGB def check(colors, size, expected_palette_length): # make an image with empty colors in the start of the palette range im = Image.frombytes('P', (colors,colors), @@ -70,7 +70,7 @@ class TestFileGif(PillowTestCase): # check palette length palette_length = max(i+1 for i,v in enumerate(reloaded.histogram()) if v) self.assertEqual(expected_palette_length, palette_length) - + self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB')) @@ -288,7 +288,7 @@ class TestFileGif(PillowTestCase): im_list = [ Image.new('L', (100, 100), '#000'), Image.new('L', (100, 100), '#111'), - Image.new('L', (100, 100), '#222'), + Image.new('L', (100, 100), '#222') ] #duration as list @@ -323,7 +323,31 @@ class TestFileGif(PillowTestCase): except EOFError: pass + def test_identical_frames(self): + duration_list = [1000, 1500, 2000, 4000] + out = self.tempfile('temp.gif') + im_list = [ + Image.new('L', (100, 100), '#000'), + Image.new('L', (100, 100), '#000'), + Image.new('L', (100, 100), '#000'), + Image.new('L', (100, 100), '#111') + ] + + #duration as list + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + duration=duration_list + ) + reread = Image.open(out) + + # Assert that the first three frames were combined + self.assertEqual(reread.n_frames, 2) + + # Assert that the new duration is the total of the identical frames + self.assertEqual(reread.info['duration'], 4500) def test_number_of_loops(self): number_of_loops = 2 @@ -427,7 +451,7 @@ class TestFileGif(PillowTestCase): reloaded = Image.open(out) self.assertEqual(reloaded.info['transparency'], 253) - + if __name__ == '__main__': unittest.main() From c02b25720c94d74359e49043c61794e02832bc6a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2017 11:49:13 +1100 Subject: [PATCH 4/4] Simplified code --- PIL/GifImagePlugin.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 9580c6728..f085e4c8c 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -302,7 +302,7 @@ except ImportError: RAWMODE = { "1": "L", "L": "L", - "P": "P", + "P": "P" } @@ -325,7 +325,6 @@ def _save_all(im, fp, filename): def _save(im, fp, filename, save_all=False): - im.encoderinfo.update(im.info) if _imaging_gif: # call external driver @@ -370,7 +369,7 @@ def _save(im, fp, filename, save_all=False): # delta frame previous = im_frames[-1] delta = ImageChops.subtract_modulo(im_frame, - previous['im_frame']) + previous['im']) bbox = delta.getbbox() if not bbox: # This frame is identical to the previous frame @@ -380,37 +379,36 @@ def _save(im, fp, filename, save_all=False): else: bbox = None im_frames.append({ - "im_frame":im_frame, - "bbox":bbox, - "encoderinfo":encoderinfo + 'im':im_frame, + 'bbox':bbox, + 'encoderinfo':encoderinfo }) if len(im_frames) < 2: save_all = False else: - for data in im_frames: - if data['bbox'] is None: + for frame_data in im_frames: + im_frame = frame_data['im'] + if not frame_data['bbox']: # global header - header = getheader(data['im_frame'], palette, data['encoderinfo'])[0] - for s in header + getdata(data['im_frame'], - (0, 0), **data['encoderinfo']): + for s in getheader(im_frame, palette, frame_data['encoderinfo'])[0]: fp.write(s) + offset = (0, 0) else: # compress difference - data['encoderinfo']['include_color_table'] = True - for s in getdata(data['im_frame'].crop(data['bbox']), - data['bbox'][:2], **data['encoderinfo']): - fp.write(s) + frame_data['encoderinfo']['include_color_table'] = True + + im_frame = im_frame.crop(frame_data['bbox']) + offset = frame_data['bbox'][:2] + for s in getdata(im_frame, offset, **frame_data['encoderinfo']): + fp.write(s) if not save_all: - header = getheader(im_out, palette, im.encoderinfo)[0] - for s in header: + for s in getheader(im_out, palette, im.encoderinfo)[0]: fp.write(s) + # local image header flags = 0 - if get_interlace(im): flags = flags | 64 - - # local image header _get_local_header(fp, im, (0, 0), flags) im_out.encoderconfig = (8, get_interlace(im))