diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index fc2b95e5a..5ecdf9f15 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -252,8 +252,9 @@ def _save(im, fp, filename): palette = im.encoderinfo["palette"] 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"," + @@ -293,7 +308,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 @@ -304,6 +318,7 @@ def _save(im, fp, filename): fp.flush() except: pass + def _save_netpbm(im, fp, filename): # @@ -329,42 +344,81 @@ def getheader(im, palette=None, info=None): optimize = info and info.get("optimize", 0) - s = [ + # start of header + header = [ 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 + usedPaletteColors = None + + if palette is not None and isinstance(palette, bytes): + paletteBytes = palette[:768] else: - maxcolor = 256 + usedPaletteColors = [] - # global palette - if im.mode == "P": - # colour palette - if palette is not None and isinstance(palette, bytes): - paletteBytes = palette + 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 and countUsedPaletteColors < 256: + 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) + # greyscale + if countUsedPaletteColors > 0 and countUsedPaletteColors < 256: + paletteBytes = b""; + # add only the used grayscales to the palette + for i in usedPaletteColors: + paletteBytes += o8(i)*3 + else : + paletteBytes = bytearray([i//3 for i in range(768)]) - else: - # greyscale - for i in range(maxcolor): - s.append(o8(i) * 3) + # 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 + break + + im.frombytes(bytes(imageBytes)) + + # calculate the palette size for the header + import math + colorTableSize = int(math.ceil(math.log(len(paletteBytes)//3, 2)))-1 + if colorTableSize < 0: colorTableSize = 0 + 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 + # the palette has to be 2< 0: + paletteBytes += o8(0) * 3 * actualTargetSizeDiff + + # global color palette + header.append(paletteBytes) + return header, usedPaletteColors - return s def getdata(im, offset = (0, 0), **params): """Return a list of strings representing this image. diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 336aaba63..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) @@ -25,4 +26,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) diff --git a/setup.py b/setup.py index c3de24e6a..3686c5adb 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" #