diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 2775a00f1..9b6222405 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -352,10 +352,21 @@ def _save(im, fp, filename, save_all=False): first_frame = None append_images = im.encoderinfo.get("append_images", []) + if "duration" in im.encoderinfo: + duration = im.encoderinfo["duration"] + else: + duration = None + 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) + if duration is not None: + if not isinstance(duration, list): + encoderinfo["duration"] = duration + else: + 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) @@ -587,7 +598,7 @@ def _get_header_palette(palette_bytes): return palette_bytes # Force optimization so that we can test performance against -# cases where it took lots of memory and time previously. +# cases where it took lots of memory and time previously. _FORCE_OPTIMIZE = False def _get_palette_bytes(im, palette, info): @@ -613,12 +624,12 @@ def _get_palette_bytes(im, palette, info): # lengths are restricted to 3*(2**N) bytes. Max saving would # be 768 -> 6 bytes if we went all the way down to 2 colors. # * If we're over 128 colors, we can't save any space. - # * If there aren't any holes, it's not worth collapsing. + # * If there aren't any holes, it's not worth collapsing. # * If we have a 'large' image, the palette is in the noise. # create the new palette if not every color is used if _FORCE_OPTIMIZE or im.mode == 'L' or \ - (len(used_palette_colors) <= 128 and + (len(used_palette_colors) <= 128 and max(used_palette_colors) > len(used_palette_colors) and im.width * im.height < 512 * 512): palette_bytes = b"" @@ -655,15 +666,15 @@ def _get_palette_bytes(im, palette, info): m_im.palette = ImagePalette.ImagePalette("RGB", palette=mapping_palette*3, size=768) - #possibly set palette dirty, then + #possibly set palette dirty, then #m_im.putpalette(mapping_palette, 'L') # converts to 'P' # or just force it. # UNDONE -- this is part of the general issue with palettes m_im.im.putpalette(*m_im.palette.getdata()) - + m_im = m_im.convert('L') - - # Internally, we require 768 bytes for a palette. + + # Internally, we require 768 bytes for a palette. new_palette_bytes = (palette_bytes + (768 - len(palette_bytes)) * b'\x00') m_im.putpalette(new_palette_bytes) @@ -672,9 +683,9 @@ def _get_palette_bytes(im, palette, info): size=len(palette_bytes)) # oh gawd, this is modifying the image in place so I can pass by ref. - # REFACTOR SOONEST + # REFACTOR SOONEST im.frombytes(m_im.tobytes()) - + if not palette_bytes: palette_bytes = source_palette diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 1672a14f0..043ec74a8 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')) @@ -281,6 +281,25 @@ class TestFileGif(PillowTestCase): self.assertEqual(reread.info['duration'], duration) + def test_multiple_duration(self): + duration_list = [1000, 2000, 3000] + + out = self.tempfile('temp.gif') + im_list = [ + Image.new('L', (100, 100), '#000'), + Image.new('L', (100, 100), '#111'), + Image.new('L', (100, 100), '#222'), + ] + im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration_list) + reread = Image.open(out) + + for duration in duration_list: + self.assertEqual(reread.info['duration'], duration) + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass + def test_number_of_loops(self): number_of_loops = 2 diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 11486c76b..36ff379f9 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -111,7 +111,9 @@ additional frames when saving, the ``append_images`` parameter works with 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. +milliseconds between each frame. The ``duration`` parameter can be either +an integer or a list of integer. Passing a list to ``duration`` parameter +will set the ``duration`` of each frame respectively. Reading local images ~~~~~~~~~~~~~~~~~~~~