diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 2775a00f1..889f71caa 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -434,18 +434,16 @@ def _get_local_header(fp, im, offset, flags): # optimize the block away if transparent color is not used transparent_color_exists = True - if _get_optimize(im, im.encoderinfo): - used_palette_colors = _get_used_palette_colors(im) - + used_palette_colors = _get_optimize(im, im.encoderinfo) + if used_palette_colors is not None: # adjust the transparency index after optimize - if len(used_palette_colors) < 256: - for i in range(len(used_palette_colors)): - if used_palette_colors[i] == transparency: - transparency = i - transparent_color_exists = True - break - else: - transparent_color_exists = False + for i, palette_color in enumerate(used_palette_colors): + if palette_color == transparency: + transparency = i + transparent_color_exists = True + break + else: + transparent_color_exists = False if "duration" in im.encoderinfo: duration = int(im.encoderinfo["duration"] / 10) @@ -552,9 +550,28 @@ def _save_netpbm(im, fp, filename): # -------------------------------------------------------------------- # GIF utilities -def _get_optimize(im, info): - return im.mode in ("P", "L") and info and info.get("optimize", 0) +# Force optimization so that we can test performance against +# cases where it took lots of memory and time previously. +_FORCE_OPTIMIZE = False +def _get_optimize(im, info): + if im.mode in ("P", "L") and info and info.get("optimize", 0): + # Potentially expensive operation. + + # The palette saves 3 bytes per color not used, but palette + # 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 we have a 'large' image, the palette is in the noise. + + # create the new palette if not every color is used + used_palette_colors = _get_used_palette_colors(im) + if _FORCE_OPTIMIZE or im.mode == 'L' or \ + (len(used_palette_colors) <= 128 and + max(used_palette_colors) > len(used_palette_colors) and + im.width * im.height < 512 * 512): + return used_palette_colors def _get_used_palette_colors(im): used_palette_colors = [] @@ -586,10 +603,6 @@ def _get_header_palette(palette_bytes): palette_bytes += o8(0) * 3 * actual_target_size_diff return palette_bytes -# Force optimization so that we can test performance against -# cases where it took lots of memory and time previously. -_FORCE_OPTIMIZE = False - def _get_palette_bytes(im, palette, info): if im.mode == "P": if palette and isinstance(palette, bytes): @@ -602,79 +615,64 @@ def _get_palette_bytes(im, palette, info): else: source_palette = bytearray(i//3 for i in range(768)) - used_palette_colors = palette_bytes = None + palette_bytes = None - if _get_optimize(im, info): - used_palette_colors = _get_used_palette_colors(im) + used_palette_colors = _get_optimize(im, info) + if used_palette_colors is not None: + palette_bytes = b"" + new_positions = [0]*256 - # Potentially expensive operation. + # pick only the used colors from the palette + for i, oldPosition in enumerate(used_palette_colors): + palette_bytes += source_palette[oldPosition*3:oldPosition*3+3] + new_positions[oldPosition] = i - # The palette saves 3 bytes per color not used, but palette - # 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 we have a 'large' image, the palette is in the noise. + # replace the palette color id of all pixel with the new id - # create the new palette if not every color is used - if _FORCE_OPTIMIZE or im.mode == 'L' or \ - (len(used_palette_colors) <= 128 and - max(used_palette_colors) > len(used_palette_colors) and - im.width * im.height < 512 * 512): - palette_bytes = b"" - new_positions = [0]*256 + # Palette images are [0..255], mapped through a 1 or 3 + # byte/color map. We need to remap the whole image + # from palette 1 to palette 2. New_positions is + # an array of indexes into palette 1. Palette 2 is + # palette 1 with any holes removed. - # pick only the used colors from the palette - for i, oldPosition in enumerate(used_palette_colors): - palette_bytes += source_palette[oldPosition*3:oldPosition*3+3] - new_positions[oldPosition] = i + # We're going to leverage the convert mechanism to use the + # C code to remap the image from palette 1 to palette 2, + # by forcing the source image into 'L' mode and adding a + # mapping 'L' mode palette, then converting back to 'L' + # sans palette thus converting the image bytes, then + # assigning the optimized RGB palette. - # replace the palette color id of all pixel with the new id + # perf reference, 9500x4000 gif, w/~135 colors + # 14 sec prepatch, 1 sec postpatch with optimization forced. - # Palette images are [0..255], mapped through a 1 or 3 - # byte/color map. We need to remap the whole image - # from palette 1 to palette 2. New_positions is - # an array of indexes into palette 1. Palette 2 is - # palette 1 with any holes removed. + mapping_palette = bytearray(new_positions) - # We're going to leverage the convert mechanism to use the - # C code to remap the image from palette 1 to palette 2, - # by forcing the source image into 'L' mode and adding a - # mapping 'L' mode palette, then converting back to 'L' - # sans palette thus converting the image bytes, then - # assigning the optimized RGB palette. + m_im = im.copy() + m_im.mode = 'P' - # perf reference, 9500x4000 gif, w/~135 colors - # 14 sec prepatch, 1 sec postpatch with optimization forced. + m_im.palette = ImagePalette.ImagePalette("RGB", + palette=mapping_palette*3, + size=768) + #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()) - mapping_palette = bytearray(new_positions) + m_im = m_im.convert('L') - m_im = im.copy() - m_im.mode = 'P' + # 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) + m_im.palette = ImagePalette.ImagePalette("RGB", + palette=palette_bytes, + size=len(palette_bytes)) - m_im.palette = ImagePalette.ImagePalette("RGB", - palette=mapping_palette*3, - size=768) - #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. - new_palette_bytes = (palette_bytes + - (768 - len(palette_bytes)) * b'\x00') - m_im.putpalette(new_palette_bytes) - m_im.palette = ImagePalette.ImagePalette("RGB", - palette=palette_bytes, - size=len(palette_bytes)) + # oh gawd, this is modifying the image in place so I can pass by ref. + # REFACTOR SOONEST + im.frombytes(m_im.tobytes()) - # oh gawd, this is modifying the image in place so I can pass by ref. - # 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..7812409ac 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -362,5 +362,28 @@ class TestFileGif(PillowTestCase): reread = Image.open(out) self.assertEqual(reread.n_frames, 10) + def test_transparent_optimize(self): + # from issue #2195, if the transparent color is incorrectly + # optimized out, gif loses transparency Need a palette that + # isn't using the 0 color, and one that's > 128 items where + # the transparent color is actually the top palette entry to + # trigger the bug. + + from PIL import ImagePalette + + data = bytes(bytearray(range(1,254))) + palette = ImagePalette.ImagePalette("RGB", list(range(256))*3) + + im = Image.new('L', (253,1)) + im.frombytes(data) + im.putpalette(palette) + + out = self.tempfile('temp.gif') + im.save(out, transparency=253) + reloaded = Image.open(out) + + self.assertEqual(reloaded.info['transparency'], 253) + + if __name__ == '__main__': unittest.main()