From 1f8a37841c91e45f48587319e5e14aee9f575fe8 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko Date: Fri, 24 Jul 2015 11:14:20 +0200 Subject: [PATCH 1/7] Testing that animated gif preserves all important headers --- Tests/test_file_gif.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 70438eb03..5c83ba8e4 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -95,6 +95,21 @@ class TestFileGif(PillowTestCase): self.assertEqual(reread.n_frames, 5) + def test_headers_saving_for_animated_gifs(self): + important_headers = ['background', 'version', 'transparency', 'duration', 'loop'] + # Multiframe image + im = Image.open("Tests/images/dispose_bgnd.gif") + + out = self.tempfile('temp.gif') + im.save(out, save_all=True) + reread = Image.open(out) + + for header in important_headers: + self.assertEqual( + im.info[header], + reread.info[header] + ) + def test_palette_handling(self): # see https://github.com/python-pillow/Pillow/issues/513 From f64bc891d470ee4a9c0e2b3840f4dabc41aa86a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Aug 2015 22:09:05 +1000 Subject: [PATCH 2/7] Changed GifImagePlugin to include use image info params --- PIL/GifImagePlugin.py | 21 +++++++++++++++++---- Tests/test_file_gif.py | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 08567fcd0..45fc34021 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -24,7 +24,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, ImagePalette, ImageChops, ImageSequence, _binary +from PIL import Image, ImageFile, ImagePalette, \ + ImageChops, ImageSequence, _binary __version__ = "0.9" @@ -317,6 +318,7 @@ 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 try: @@ -347,7 +349,8 @@ def _save(im, fp, filename, save_all=False): # e.g. getdata(im_frame, duration=1000) if not previous: # global header - for s in getheader(im_frame, palette, im.encoderinfo)[0] + getdata(im_frame): + for s in getheader(im_frame, palette, im.encoderinfo)[0] + \ + getdata(im_frame, (0, 0), **im.encoderinfo): fp.write(s) else: # delta frame @@ -356,7 +359,8 @@ def _save(im, fp, filename, save_all=False): if bbox: # compress difference - for s in getdata(im_frame.crop(bbox), offset=bbox[:2]): + for s in getdata(im_frame.crop(bbox), + bbox[:2], **im.encoderinfo): fp.write(s) else: # FIXME: what should we do in this case? @@ -591,7 +595,16 @@ def getheader(im, palette=None, info=None): # size of global color table + global color table flag header.append(o8(color_table_size + 128)) # background + reserved/aspect - background = im.info["background"] if "background" in im.info else 0 + if info and "background" in info: + background = info["background"] + elif "background" in im.info: + # This elif is redundant within GifImagePlugin + # since im.info parameters are bundled into the info dictionary + # However, external scripts may call getheader directly + # So this maintains earlier behaviour + background = im.info["background"] + else: + background = 0 header.append(o8(background) + o8(0)) # end of Logical Screen Descriptor diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 5c83ba8e4..dd91325fc 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -96,7 +96,7 @@ class TestFileGif(PillowTestCase): self.assertEqual(reread.n_frames, 5) def test_headers_saving_for_animated_gifs(self): - important_headers = ['background', 'version', 'transparency', 'duration', 'loop'] + important_headers = ['background', 'version', 'duration', 'loop'] # Multiframe image im = Image.open("Tests/images/dispose_bgnd.gif") From b8ff91ab3bcc788c612f5d879e0483d295c395d9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Aug 2015 22:10:13 +1000 Subject: [PATCH 3/7] Corrected version number when saving GIFs --- PIL/GifImagePlugin.py | 13 ++++++++++++- Tests/test_file_gif.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 45fc34021..1287f2a52 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -537,8 +537,19 @@ def getheader(im, palette=None, info=None): # Header Block # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + + version = b"87a" + for extensionKey in ["transparency", "duration", "loop"]: + if info and extensionKey in info and \ + not (extensionKey == "duration" and info[extensionKey] == 0): + version = b"89a" + break + else: + if im.info.get("version") == "89a": + version = b"89a" + header = [ - b"GIF87a" + # signature + version + b"GIF"+version + # signature + version o16(im.size[0]) + # canvas width o16(im.size[1]) # canvas height ] diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index dd91325fc..0ac67cd63 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -24,6 +24,7 @@ class TestFileGif(PillowTestCase): self.assertEqual(im.mode, "P") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "GIF") + self.assertEqual(im.info["version"], b"GIF89a") def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -266,6 +267,34 @@ class TestFileGif(PillowTestCase): self.assertEqual(reread.info['background'], im.info['background']) + def test_version(self): + out = self.tempfile('temp.gif') + + # Test that GIF87a is used by default + im = Image.new('L', (100, 100), '#000') + im.save(out) + reread = Image.open(out) + self.assertEqual(reread.info["version"], b"GIF87a") + + # Test that adding a GIF89a feature changes the version + im.info["transparency"] = 1 + im.save(out) + reread = Image.open(out) + self.assertEqual(reread.info["version"], b"GIF89a") + + # Test that a GIF87a image is also saved in that format + im = Image.open(TEST_GIF) + im.save(out) + reread = Image.open(out) + self.assertEqual(reread.info["version"], b"GIF87a") + + # Test that a GIF89a image is also saved in that format + im.info["version"] = "GIF89a" + im.save(out) + reread = Image.open(out) + self.assertEqual(reread.info["version"], b"GIF87a") + + if __name__ == '__main__': unittest.main() From 88fca0f55531777d578831dd42d39819beea90dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Aug 2015 23:23:07 +1000 Subject: [PATCH 4/7] Fixed palette issue when saving --- PIL/GifImagePlugin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 1287f2a52..bd45e2dbc 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -301,14 +301,17 @@ RAWMODE = { } -def _convert_mode(im): +def _convert_mode(im, initial_call=False): # convert on the fly (EXPERIMENTAL -- I'm not sure PIL # should automatically convert images on save...) if Image.getmodebase(im.mode) == "RGB": - palette_size = 256 - if im.palette: - palette_size = len(im.palette.getdata()[1]) // 3 - return im.convert("P", palette=1, colors=palette_size) + if initial_call: + palette_size = 256 + if im.palette: + palette_size = len(im.palette.getdata()[1]) // 3 + return im.convert("P", palette=1, colors=palette_size) + else: + return im.convert("P") return im.convert("L") @@ -330,7 +333,7 @@ def _save(im, fp, filename, save_all=False): if im.mode in RAWMODE: im_out = im.copy() else: - im_out = _convert_mode(im) + im_out = _convert_mode(im, True) # header try: From e88e90b8ede189221199c429340d3242fc9dc318 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Aug 2015 21:56:23 +1000 Subject: [PATCH 5/7] Minor improvement, one less copy --- PIL/GifImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index bd45e2dbc..d78e55f1f 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -357,7 +357,7 @@ def _save(im, fp, filename, save_all=False): fp.write(s) else: # delta frame - delta = ImageChops.subtract_modulo(im_frame, previous) + delta = ImageChops.subtract_modulo(im_frame, previous.copy()) bbox = delta.getbbox() if bbox: @@ -368,7 +368,7 @@ def _save(im, fp, filename, save_all=False): else: # FIXME: what should we do in this case? pass - previous = im_frame.copy() + previous = im_frame else: header = getheader(im_out, palette, im.encoderinfo)[0] for s in header: From 1b8d12b048dd1d4c7da834ae3c44c9661e24e47a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Aug 2015 23:51:02 +1000 Subject: [PATCH 6/7] If only one frame, do not use image from sequence --- PIL/GifImagePlugin.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index d78e55f1f..83169bf21 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -345,6 +345,7 @@ def _save(im, fp, filename, save_all=False): if save_all: previous = None + first_frame = None for im_frame in ImageSequence.Iterator(im): im_frame = _convert_mode(im_frame) @@ -352,10 +353,14 @@ def _save(im, fp, filename, save_all=False): # e.g. getdata(im_frame, duration=1000) if not previous: # global header - for s in getheader(im_frame, palette, im.encoderinfo)[0] + \ - getdata(im_frame, (0, 0), **im.encoderinfo): - fp.write(s) + 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() @@ -369,7 +374,9 @@ def _save(im, fp, filename, save_all=False): # FIXME: what should we do in this case? pass previous = im_frame - else: + if first_frame: + save_all = False + if not save_all: header = getheader(im_out, palette, im.encoderinfo)[0] for s in header: fp.write(s) From 01356a9ad8c713577c719d4ac077745ec10a2b88 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Sep 2015 21:41:36 +1000 Subject: [PATCH 7/7] Improved documentation [ci skip] --- docs/handbook/image-file-formats.rst | 29 +++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 11ec60401..ec3c49ecc 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -54,8 +54,11 @@ GIF ^^^ PIL reads GIF87a and GIF89a versions of the GIF file format. The library writes -run-length encoded GIF87a files. Note that GIF files are always read as -grayscale (``L``) or palette mode (``P``) images. +run-length encoded files in GIF87a by default, unless GIF89a features +are used or GIF89a is already in use. + +Note that GIF files are always read as grayscale (``L``) +or palette mode (``P``) images. The :py:meth:`~PIL.Image.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -73,12 +76,32 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following **version** Version (either ``GIF87a`` or ``GIF89a``). +**duration** + May not be present. The time to display each frame of the GIF, in + milliseconds. + +**loop** + May not be present. The number of times the GIF should loop. + Reading sequences ~~~~~~~~~~~~~~~~~ The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell` methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind -the file by seeking to the first frame. Random access is not supported. ``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame. +the file by seeking to the first frame. Random access is not supported. + +``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame. + +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``. + +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 +milliseconds between each frame. Reading local images ~~~~~~~~~~~~~~~~~~~~