From be7a191b6e861054bbc25b5d258c30440d8b4cd3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 11 Sep 2016 11:57:45 +1000 Subject: [PATCH 1/2] Added local color table for subsequent GIF frames --- PIL/GifImagePlugin.py | 102 ++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 6bca4dd03..6a75a6678 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -372,6 +372,7 @@ def _save(im, fp, filename, save_all=False): if bbox: # compress difference + encoderinfo['include_color_table'] = True for s in getdata(im_frame.crop(bbox), bbox[:2], **im.encoderinfo): fp.write(s) @@ -455,7 +456,7 @@ def _get_local_header(fp, im, offset, flags): fp.write(b"!" + o8(249) + # extension intro o8(4) + # length - o8(transparency_flag) + # transparency info present + o8(transparency_flag) + # packed fields o16(duration) + # duration o8(transparency) + # transparency index o8(0)) @@ -476,13 +477,27 @@ def _get_local_header(fp, im, offset, flags): o8(1) + o16(number_of_loops) + # number of loops o8(0)) + include_color_table = im.encoderinfo.get('include_color_table') + if include_color_table: + try: + palette = im.encoderinfo["palette"] + except KeyError: + palette = None + palette_bytes = _get_palette_bytes(im, palette, im.encoderinfo)[0] + color_table_size = _get_color_table_size(palette_bytes) + if color_table_size: + flags = flags | 128 # local color table flag + flags = flags | color_table_size + fp.write(b"," + o16(offset[0]) + # offset o16(offset[1]) + o16(im.size[0]) + # size o16(im.size[1]) + - o8(flags) + # flags - o8(8)) # bits + o8(flags)) # flags + if include_color_table and color_table_size: + fp.write(_get_header_palette(palette_bytes)) + fp.write(o8(8)) # bits def _save_netpbm(im, fp, filename): @@ -550,31 +565,25 @@ def _get_used_palette_colors(im): return used_palette_colors +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 -def getheader(im, palette=None, info=None): - """Return a list of strings representing a GIF header""" +def _get_header_palette(palette_bytes): + color_table_size = _get_color_table_size(palette_bytes) - # Header Block - # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp - - version = b"87a" - for extensionKey in ["transparency", "duration", "loop", "comment"]: - if info and extensionKey in info: - if ((extensionKey == "duration" and info[extensionKey] == 0) or - (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))): - continue - version = b"89a" - break - else: - if im.info.get("version") == "89a": - version = b"89a" - - header = [ - b"GIF"+version + # signature + version - o16(im.size[0]) + # canvas width - o16(im.size[1]) # canvas height - ] + # add the missing amount of bytes + # the palette has to be 2< 0: + palette_bytes += o8(0) * 3 * actual_target_size_diff + return palette_bytes +def _get_palette_bytes(im, palette, info): if im.mode == "P": if palette and isinstance(palette, bytes): source_palette = palette[:768] @@ -617,15 +626,38 @@ def getheader(im, palette=None, info=None): if not palette_bytes: palette_bytes = source_palette + return palette_bytes, used_palette_colors + +def getheader(im, palette=None, info=None): + """Return a list of strings representing a GIF header""" + + # Header Block + # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + + version = b"87a" + for extensionKey in ["transparency", "duration", "loop", "comment"]: + if info and extensionKey in info: + if ((extensionKey == "duration" and info[extensionKey] == 0) or + (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))): + continue + version = b"89a" + break + else: + if im.info.get("version") == "89a": + version = b"89a" + + header = [ + b"GIF"+version + # signature + version + o16(im.size[0]) + # canvas width + o16(im.size[1]) # canvas height + ] + + palette_bytes, used_palette_colors = _get_palette_bytes(im, palette, info) # Logical Screen Descriptor - # 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 + color_table_size = _get_color_table_size(palette_bytes) # size of global color table + global color table flag - header.append(o8(color_table_size + 128)) + header.append(o8(color_table_size + 128)) # packed fields # background + reserved/aspect if info and "background" in info: background = info["background"] @@ -640,14 +672,8 @@ def getheader(im, palette=None, info=None): header.append(o8(background) + o8(0)) # end of Logical Screen Descriptor - # add the missing amount of bytes - # the palette has to be 2< 0: - palette_bytes += o8(0) * 3 * actual_target_size_diff - # Header + Logical Screen Descriptor + Global Color Table - header.append(palette_bytes) + header.append(_get_header_palette(palette_bytes)) return header, used_palette_colors From b346ed36f17d25ff6f83cee9e773ac98ff1c0633 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 11 Sep 2016 12:04:01 +1000 Subject: [PATCH 2/2] Added append_images parameter to GIF saving --- PIL/GifImagePlugin.py | 57 +++++++++++++++------------- Tests/test_file_gif.py | 18 +++++++++ docs/handbook/image-file-formats.rst | 4 +- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 6a75a6678..c72d04708 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -351,35 +351,38 @@ def _save(im, fp, filename, save_all=False): previous = None first_frame = None - for im_frame in ImageSequence.Iterator(im): - im_frame = _convert_mode(im_frame) + append_images = im.encoderinfo.get("append_images", []) + for imSequence in [im]+append_images: + for im_frame in ImageSequence.Iterator(imSequence): + encoderinfo = im.encoderinfo.copy() + im_frame = _convert_mode(im_frame) - # 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, im.encoderinfo)[0] - first_frame += getdata(im_frame, (0, 0), **im.encoderinfo) - else: - if first_frame: - for s in first_frame: - fp.write(s) - first_frame = None - - # delta frame - delta = ImageChops.subtract_modulo(im_frame, previous.copy()) - bbox = delta.getbbox() - - if bbox: - # compress difference - encoderinfo['include_color_table'] = True - for s in getdata(im_frame.crop(bbox), - bbox[:2], **im.encoderinfo): - fp.write(s) + # 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: - # FIXME: what should we do in this case? - pass - previous = im_frame + if first_frame: + for s in first_frame: + fp.write(s) + first_frame = None + + # delta frame + delta = ImageChops.subtract_modulo(im_frame, previous.copy()) + 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: save_all = False if not save_all: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index b7f180fc4..2a3cfd1ad 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -306,6 +306,24 @@ class TestFileGif(PillowTestCase): reread = Image.open(out) self.assertEqual(reread.info["version"], b"GIF87a") + def test_append_images(self): + out = self.tempfile('temp.gif') + + # Test appending single frame images + im = Image.new('RGB', (100, 100), '#f00') + ims = [Image.new('RGB', (100, 100), color) for color in ['#0f0', '#00f']] + im.save(out, save_all=True, append_images=ims) + + reread = Image.open(out) + self.assertEqual(reread.n_frames, 3) + + # Tests appending single and multiple frame images + im = Image.open("Tests/images/dispose_none.gif") + ims = [Image.open("Tests/images/dispose_prev.gif")] + im.save(out, save_all=True, append_images=ims) + + reread = Image.open(out) + self.assertEqual(reread.n_frames, 10) if __name__ == '__main__': unittest.main() diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 991681631..d71983540 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -103,7 +103,9 @@ Saving sequences When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used, by default only the first frame will be saved. To save all frames, the -``save_all`` parameter must be present and set to ``True``. +``save_all`` parameter must be present and set to ``True``. To append +additional frames when saving, the ``append_images`` parameter can be set to a +list of images containing the extra frames. If present, the ``loop`` parameter can be used to set the number of times the GIF should loop, and the ``duration`` parameter can set the number of