From a466b3e09982ebcf5aee2cbe957ce90e6ddfd0ae Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Thu, 23 May 2013 14:06:09 +0200 Subject: [PATCH 1/9] fixes #211 replace the gif optimization with a working version --- PIL/GifImagePlugin.py | 95 +++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index fc2b95e5a..2f420dae3 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -293,7 +293,6 @@ def _save(im, fp, filename): o8(8)) # bits imOut.encoderconfig = (8, interlace) - ImageFile._save(imOut, fp, [("gif", (0,0)+im.size, 0, rawmode)]) fp.write(b"\0") # end of image data @@ -303,6 +302,7 @@ def _save(im, fp, filename): try: fp.flush() except: pass + def _save_netpbm(im, fp, filename): @@ -329,42 +329,77 @@ def getheader(im, palette=None, info=None): optimize = info and info.get("optimize", 0) + # start of header s = [ b"GIF87a" + # magic o16(im.size[0]) + # size - o16(im.size[1]) + - o8(7 + 128) + # flags: bits + palette - o8(0) + # background - o8(0) # reserved/aspect + o16(im.size[1]) ] - - if optimize: - # minimize color palette - i = 0 - maxcolor = 0 - for count in im.histogram(): - if count: - maxcolor = i - i = i + 1 + + # if the user adds a palette, use it + if palette is not None and isinstance(palette, bytes): + paletteBytes = palette else: - maxcolor = 256 - - # global palette - if im.mode == "P": - # colour palette - if palette is not None and isinstance(palette, bytes): - paletteBytes = palette + usedPaletteColors = [] + + if optimize: + # minimize color palette if wanted + i = 0 + for count in im.histogram(): + if count: + usedPaletteColors.append(i) + i += 1 + + countUsedPaletteColors = len(usedPaletteColors) + + # create the global palette + if im.mode == "P": + # colour palette + if countUsedPaletteColors > 0: + paletteBytes = b""; + # pick only the used colors from the palette + for i in usedPaletteColors: + paletteBytes += im.im.getpalette("RGB")[i*3:i*3+3] + else : + paletteBytes = im.im.getpalette("RGB")[:768] else: - paletteBytes =im.im.getpalette("RGB")[:maxcolor*3] - - s.append(paletteBytes) - - else: - # greyscale - for i in range(maxcolor): - s.append(o8(i) * 3) - + # greyscale + if countUsedPaletteColors > 0: + paletteBytes = b""; + # add only the used grayscales to the palette + for i in usedPaletteColors: + paletteBytes += o8(i)*3 + else : + paletteBytes = bytes([i//3 for i in range(768)]) + + # TODO improve this, maybe add numpy support + # replace the palette color id of all pixel with the new id + if countUsedPaletteColors > 0 and countUsedPaletteColors < 256: + imageBytes = bytearray(im.tobytes()) + for i in range(len(imageBytes)): + for newI in range(countUsedPaletteColors): + if imageBytes[i] == usedPaletteColors[newI]: + imageBytes[i] = newI + + im.frombytes(bytes(imageBytes)) + + # calculate the palette size for the header + import math + colorTableSize = math.ceil(math.log(len(paletteBytes)//3, 2))-1 + s.append(o8(colorTableSize + 128)) # size of global color table + global color table flag + s.append(o8(0) + o8(0)) # background + reserved/aspect + # end of screen descriptor header + + # add the missing amount of bytes + # the palette can only be 2< 0: + paletteBytes += o8(0) * 3 * actualTargetSizeDiff + + # global color palette + s.append(paletteBytes) return s + def getdata(im, offset = (0, 0), **params): """Return a list of strings representing this image. From 5cd1b9f01e2dae4105b7e357055d03d0bd1c671b Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Thu, 23 May 2013 14:31:48 +0200 Subject: [PATCH 2/9] minor addition --- PIL/GifImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 2f420dae3..7c9322754 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -355,7 +355,7 @@ def getheader(im, palette=None, info=None): # create the global palette if im.mode == "P": # colour palette - if countUsedPaletteColors > 0: + if countUsedPaletteColors > 0 and countUsedPaletteColors < 256: paletteBytes = b""; # pick only the used colors from the palette for i in usedPaletteColors: @@ -364,7 +364,7 @@ def getheader(im, palette=None, info=None): paletteBytes = im.im.getpalette("RGB")[:768] else: # greyscale - if countUsedPaletteColors > 0: + if countUsedPaletteColors > 0 and countUsedPaletteColors < 256: paletteBytes = b""; # add only the used grayscales to the palette for i in usedPaletteColors: @@ -391,7 +391,7 @@ def getheader(im, palette=None, info=None): # end of screen descriptor header # add the missing amount of bytes - # the palette can only be 2< 0: paletteBytes += o8(0) * 3 * actualTargetSizeDiff From dad5e8622611261ee002b2cac969a305195d1b4b Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Thu, 23 May 2013 15:24:06 +0200 Subject: [PATCH 3/9] fix gif test, fix gif optimization for palette length < 3 --- PIL/GifImagePlugin.py | 1 + Tests/test_file_gif.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 7c9322754..9202a6d1b 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -386,6 +386,7 @@ def getheader(im, palette=None, info=None): # calculate the palette size for the header import math colorTableSize = math.ceil(math.log(len(paletteBytes)//3, 2))-1 + if colorTableSize < 0: colorTableSize = 0 s.append(o8(colorTableSize + 128)) # size of global color table + global color table flag s.append(o8(0) + o8(0)) # background + reserved/aspect # end of screen descriptor header diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 336aaba63..3d3d43a7e 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -25,4 +25,4 @@ def test_optimize(): im.save(file, "GIF", optimize=optimize) return len(file.getvalue()) assert_equal(test(0), 800) - assert_equal(test(1), 32) + assert_equal(test(1), 38) From 12cea1928055e6dc24082bb4f5ee760ee5e040a3 Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Thu, 23 May 2013 15:45:11 +0200 Subject: [PATCH 4/9] a break --- PIL/GifImagePlugin.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 9202a6d1b..1254a396f 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -252,7 +252,7 @@ def _save(im, fp, filename): palette = im.encoderinfo["palette"] except KeyError: palette = None - + for s in getheader(imOut, palette, im.encoderinfo): fp.write(s) @@ -302,7 +302,7 @@ def _save(im, fp, filename): try: fp.flush() except: pass - + def _save_netpbm(im, fp, filename): @@ -335,13 +335,13 @@ def getheader(im, palette=None, info=None): o16(im.size[0]) + # size o16(im.size[1]) ] - + # if the user adds a palette, use it if palette is not None and isinstance(palette, bytes): paletteBytes = palette else: usedPaletteColors = [] - + if optimize: # minimize color palette if wanted i = 0 @@ -349,9 +349,9 @@ def getheader(im, palette=None, info=None): if count: usedPaletteColors.append(i) i += 1 - + countUsedPaletteColors = len(usedPaletteColors) - + # create the global palette if im.mode == "P": # colour palette @@ -371,18 +371,19 @@ def getheader(im, palette=None, info=None): paletteBytes += o8(i)*3 else : paletteBytes = bytes([i//3 for i in range(768)]) - + # TODO improve this, maybe add numpy support # replace the palette color id of all pixel with the new id if countUsedPaletteColors > 0 and countUsedPaletteColors < 256: imageBytes = bytearray(im.tobytes()) for i in range(len(imageBytes)): for newI in range(countUsedPaletteColors): - if imageBytes[i] == usedPaletteColors[newI]: + if imageBytes[i] == usedPaletteColors[newI]: imageBytes[i] = newI - + break + im.frombytes(bytes(imageBytes)) - + # calculate the palette size for the header import math colorTableSize = math.ceil(math.log(len(paletteBytes)//3, 2))-1 @@ -390,17 +391,17 @@ def getheader(im, palette=None, info=None): s.append(o8(colorTableSize + 128)) # size of global color table + global color table flag s.append(o8(0) + o8(0)) # background + reserved/aspect # end of screen descriptor header - + # add the missing amount of bytes # the palette has to be 2< 0: paletteBytes += o8(0) * 3 * actualTargetSizeDiff - + # global color palette s.append(paletteBytes) return s - + def getdata(im, offset = (0, 0), **params): """Return a list of strings representing this image. From d78371cccd94cb8d0c0462e3a176644865a5f1a1 Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Thu, 23 May 2013 19:45:39 +0300 Subject: [PATCH 5/9] remove a float/int problem --- PIL/GifImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 1254a396f..0a6bd9c44 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -386,7 +386,7 @@ def getheader(im, palette=None, info=None): # calculate the palette size for the header import math - colorTableSize = math.ceil(math.log(len(paletteBytes)//3, 2))-1 + colorTableSize = int(math.ceil(math.log(len(paletteBytes)//3, 2)))-1 if colorTableSize < 0: colorTableSize = 0 s.append(o8(colorTableSize + 128)) # size of global color table + global color table flag s.append(o8(0) + o8(0)) # background + reserved/aspect From 65266a74193edc59fcae91d513efd771bc8413a8 Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Thu, 23 May 2013 20:02:19 +0300 Subject: [PATCH 6/9] Update test_file_gif.py remove warning --- Tests/test_file_gif.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3d3d43a7e..0c27865cd 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -9,7 +9,8 @@ if "gif_encoder" not in codecs or "gif_decoder" not in codecs: # sample gif stream file = "Images/lena.gif" -data = open(file, "rb").read() +with open(file, "rb") as f: + data = f.read() def test_sanity(): im = Image.open(file) From a9cb1281f46a98a473274971d0ab3185286f4c73 Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Fri, 24 May 2013 11:55:31 +0200 Subject: [PATCH 7/9] fix Python 2 compatibility --- PIL/GifImagePlugin.py | 4 ++-- setup.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 1254a396f..5c3e580a4 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -370,7 +370,7 @@ def getheader(im, palette=None, info=None): for i in usedPaletteColors: paletteBytes += o8(i)*3 else : - paletteBytes = bytes([i//3 for i in range(768)]) + paletteBytes = bytearray([i//3 for i in range(768)]) # TODO improve this, maybe add numpy support # replace the palette color id of all pixel with the new id @@ -386,7 +386,7 @@ def getheader(im, palette=None, info=None): # calculate the palette size for the header import math - colorTableSize = math.ceil(math.log(len(paletteBytes)//3, 2))-1 + colorTableSize = int(math.ceil(math.log(len(paletteBytes)//3, 2)))-1 if colorTableSize < 0: colorTableSize = 0 s.append(o8(colorTableSize + 128)) # size of global color table + global color table flag s.append(o8(0) + o8(0)) # background + reserved/aspect diff --git a/setup.py b/setup.py index dbb83fd48..d0cf95aff 100644 --- a/setup.py +++ b/setup.py @@ -285,10 +285,9 @@ class pil_build_ext(build_ext): elif _find_library_file(self, "tk" + TCL_VERSION): feature.tk = "tk" + TCL_VERSION - if ( - _find_include_file(self, "webp/encode.h") and + if (_find_include_file(self, "webp/encode.h") and _find_include_file(self, "webp/decode.h")): - if _find_library_file(self, "webp"): + if _find_library_file(self, "webp"): # in googles precompiled zip it is call "libwebp" feature.webp = "webp" # From 71b30352d9dabfc7e8c07f94b3f32cfe78267d49 Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Fri, 24 May 2013 13:16:16 +0300 Subject: [PATCH 8/9] limit custom palette size --- PIL/GifImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 5c3e580a4..91bdec7c7 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -338,7 +338,7 @@ def getheader(im, palette=None, info=None): # if the user adds a palette, use it if palette is not None and isinstance(palette, bytes): - paletteBytes = palette + paletteBytes = palette[:768] else: usedPaletteColors = [] From b66d888b0e894722850eb90e55278aa96c025d75 Mon Sep 17 00:00:00 2001 From: David Schmidt Date: Fri, 24 May 2013 13:12:40 +0200 Subject: [PATCH 9/9] adjust the transparency index after successful optimize skip transparency block if transparent color is not used after optimize --- PIL/GifImagePlugin.py | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 91bdec7c7..5ecdf9f15 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -253,7 +253,8 @@ def _save(im, fp, filename): except KeyError: palette = None - for s in getheader(imOut, palette, im.encoderinfo): + header, usedPaletteColors = getheader(imOut, palette, im.encoderinfo) + for s in header: fp.write(s) flags = 0 @@ -275,14 +276,28 @@ def _save(im, fp, filename): except KeyError: pass else: + transparency = int(transparency) + # optimize the block away if transparent color is not used + transparentColorExists = True + # adjust the transparency index after optimize + if usedPaletteColors is not None and len(usedPaletteColors) < 256: + for i in range(len(usedPaletteColors)): + if usedPaletteColors[i] == transparency: + transparency = i + transparentColorExists = True + break + else: + transparentColorExists = False + # transparency extension block - fp.write(b"!" + - o8(249) + # extension intro - o8(4) + # length - o8(1) + # transparency info present - o16(0) + # duration - o8(int(transparency)) # transparency index - + o8(0)) + if transparentColorExists: + fp.write(b"!" + + o8(249) + # extension intro + o8(4) + # length + o8(1) + # transparency info present + o16(0) + # duration + o8(transparency) # transparency index + + o8(0)) # local image header fp.write(b"," + @@ -330,13 +345,15 @@ def getheader(im, palette=None, info=None): optimize = info and info.get("optimize", 0) # start of header - s = [ + header = [ b"GIF87a" + # magic o16(im.size[0]) + # size o16(im.size[1]) ] # if the user adds a palette, use it + usedPaletteColors = None + if palette is not None and isinstance(palette, bytes): paletteBytes = palette[:768] else: @@ -388,8 +405,8 @@ def getheader(im, palette=None, info=None): import math colorTableSize = int(math.ceil(math.log(len(paletteBytes)//3, 2)))-1 if colorTableSize < 0: colorTableSize = 0 - s.append(o8(colorTableSize + 128)) # size of global color table + global color table flag - s.append(o8(0) + o8(0)) # background + reserved/aspect + header.append(o8(colorTableSize + 128)) # size of global color table + global color table flag + header.append(o8(0) + o8(0)) # background + reserved/aspect # end of screen descriptor header # add the missing amount of bytes @@ -399,8 +416,8 @@ def getheader(im, palette=None, info=None): paletteBytes += o8(0) * 3 * actualTargetSizeDiff # global color palette - s.append(paletteBytes) - return s + header.append(paletteBytes) + return header, usedPaletteColors def getdata(im, offset = (0, 0), **params):