diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 4e8ba9f58..51fd11999 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -24,7 +24,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, ImagePalette, _binary +from PIL import Image, ImageFile, ImagePalette, ImageChops, ImageSequence, _binary __version__ = "0.9" @@ -299,8 +299,10 @@ RAWMODE = { "P": "P", } +def _save_all(im, fp, filename): + _save(im, fp, filename, save_all=True) -def _save(im, fp, filename): +def _save(im, fp, filename, save_all=False): if _imaging_gif: # call external driver @@ -330,23 +332,47 @@ def _save(im, fp, filename): palette = None im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) - header, used_palette_colors = getheader(im_out, palette, im.encoderinfo) - for s in header: - fp.write(s) + if save_all: + previous = None - flags = 0 + for im_frame in ImageSequence.Iterator(im_out): + # To specify duration, add the time in milliseconds to getdata(), + # 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): + fp.write(s) + else: + # delta frame + delta = ImageChops.subtract_modulo(im_frame, previous) + bbox = delta.getbbox() - if get_interlace(im): - flags = flags | 64 + if bbox: + # compress difference + for s in getdata(im_frame.crop(bbox), offset=bbox[:2]): + fp.write(s) + else: + # FIXME: what should we do in this case? + pass + previous = im_frame.copy() + else: + header = getheader(im_out, palette, im.encoderinfo)[0] + for s in header: + fp.write(s) - # local image header - get_local_header(fp, im, (0, 0), flags) + flags = 0 - im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, - RAWMODE[im_out.mode])]) + if get_interlace(im): + flags = flags | 64 - fp.write(b"\0") # end of image data + # local image header + _get_local_header(fp, im, (0, 0), flags) + + im_out.encoderconfig = (8, get_interlace(im)) + ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, + RAWMODE[im_out.mode])]) + + fp.write(b"\0") # end of image data fp.write(b";") # end of file @@ -369,7 +395,7 @@ def get_interlace(im): return interlace -def get_local_header(fp, im, offset, flags): +def _get_local_header(fp, im, offset, flags): transparent_color_exists = False try: transparency = im.encoderinfo["transparency"] @@ -592,7 +618,7 @@ def getdata(im, offset=(0, 0), **params): im.encoderinfo = params # local image header - get_local_header(fp, im, offset, 0) + _get_local_header(fp, im, offset, 0) ImageFile._save(im, fp, [("gif", (0, 0)+im.size, 0, RAWMODE[im.mode])]) @@ -609,6 +635,7 @@ def getdata(im, offset=(0, 0), **params): Image.register_open(GifImageFile.format, GifImageFile, _accept) Image.register_save(GifImageFile.format, _save) +Image.register_save_all(GifImageFile.format, _save_all) Image.register_extension(GifImageFile.format, ".gif") Image.register_mime(GifImageFile.format, "image/gif") diff --git a/PIL/Image.py b/PIL/Image.py index 3740b51c6..141d1b264 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -204,6 +204,7 @@ ID = [] OPEN = {} MIME = {} SAVE = {} +SAVE_ALL = {} EXTENSION = {} # -------------------------------------------------------------------- @@ -1669,6 +1670,10 @@ class Image(object): # may mutate self! self.load() + save_all = False + if 'save_all' in params: + save_all = params['save_all'] + del params['save_all'] self.encoderinfo = params self.encoderconfig = () @@ -1686,11 +1691,12 @@ class Image(object): except KeyError: raise KeyError(ext) # unknown extension - try: - save_handler = SAVE[format.upper()] - except KeyError: + if format.upper() not in SAVE: init() - save_handler = SAVE[format.upper()] # unknown format + if save_all: + save_handler = SAVE_ALL[format.upper()] + else: + save_handler = SAVE[format.upper()] if isPath(fp): fp = builtins.open(fp, "wb") @@ -2469,6 +2475,18 @@ def register_save(id, driver): SAVE[id.upper()] = driver +def register_save_all(id, driver): + """ + Registers an image function to save all the frames + of a multiframe format. This function should not be + used in application code. + + :param id: An image format identifier. + :param driver: A function to save images in this format. + """ + SAVE_ALL[id.upper()] = driver + + def register_extension(id, extension): """ Registers an image extension. This function should not be diff --git a/Scripts/gifmaker.py b/Scripts/gifmaker.py index bf162eb2f..c0679ca79 100644 --- a/Scripts/gifmaker.py +++ b/Scripts/gifmaker.py @@ -14,104 +14,9 @@ # See the README file for information on usage and redistribution. # -# -# For special purposes, you can import this module and call -# the makedelta or compress functions yourself. For example, -# if you have an application that generates a sequence of -# images, you can convert it to a GIF animation using some- -# thing like the following code: -# -# import Image -# import gifmaker -# -# sequence = [] -# -# # generate sequence -# for i in range(100): -# im = -# sequence.append(im) -# -# # write GIF animation -# fp = open("out.gif", "wb") -# gifmaker.makedelta(fp, sequence) -# fp.close() -# -# Alternatively, use an iterator to generate the sequence, and -# write data directly to a socket. Or something... -# - from __future__ import print_function -from PIL import Image, ImageChops, ImageSequence - -from PIL.GifImagePlugin import getheader, getdata - -# -------------------------------------------------------------------- -# straightforward delta encoding - - -def makedelta(fp, sequence): - """Convert list of image frames to a GIF animation file""" - - frames = 0 - - previous = None - - for im in sequence: - - # To specify duration, add the time in milliseconds to getdata(), - # e.g. getdata(im, duration=1000) - - if not previous: - - # global header - for s in getheader(im)[0] + getdata(im): - fp.write(s) - - else: - - # delta frame - delta = ImageChops.subtract_modulo(im, previous) - - bbox = delta.getbbox() - - if bbox: - - # compress difference - for s in getdata(im.crop(bbox), offset=bbox[:2]): - fp.write(s) - - else: - # FIXME: what should we do in this case? - pass - - previous = im.copy() - - frames += 1 - - fp.write(b";") - - return frames - -# -------------------------------------------------------------------- -# main hack - - -def compress(infile, outfile): - - # open input image, and force loading of first frame - im = Image.open(infile) - im.load() - - # open output file - fp = open(outfile, "wb") - - seq = ImageSequence.Iterator(im) - - makedelta(fp, seq) - - fp.close() - +from PIL import Image if __name__ == "__main__": @@ -122,4 +27,5 @@ if __name__ == "__main__": print("Usage: gifmaker infile outfile") sys.exit(1) - compress(sys.argv[1], sys.argv[2]) + im = Image.open(sys.argv[1]) + im.save(sys.argv[2], save_all=True) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index abf7d547b..1141e0372 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -71,6 +71,14 @@ class TestFileGif(PillowTestCase): self.assert_image_similar(reread.convert('RGB'), hopper(), 50) + def test_roundtrip_save_all(self): + out = self.tempfile('temp.gif') + im = hopper() + im.save(out, save_all=True) + reread = Image.open(out) + + self.assert_image_similar(reread.convert('RGB'), im, 50) + def test_palette_handling(self): # see https://github.com/python-pillow/Pillow/issues/513