Merge pull request #1320 from radarhere/gifmaker

Merged gifmaker into GifImagePlugin
This commit is contained in:
Alex Clark 2015-06-30 08:00:39 -04:00
commit be1df0f33c
4 changed files with 76 additions and 117 deletions

View File

@ -24,7 +24,7 @@
# See the README file for information on usage and redistribution. # 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" __version__ = "0.9"
@ -299,8 +299,10 @@ RAWMODE = {
"P": "P", "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: if _imaging_gif:
# call external driver # call external driver
@ -330,23 +332,47 @@ def _save(im, fp, filename):
palette = None palette = None
im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
header, used_palette_colors = getheader(im_out, palette, im.encoderinfo) if save_all:
for s in header: previous = None
fp.write(s)
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): if bbox:
flags = flags | 64 # 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 flags = 0
get_local_header(fp, im, (0, 0), flags)
im_out.encoderconfig = (8, get_interlace(im)) if get_interlace(im):
ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, flags = flags | 64
RAWMODE[im_out.mode])])
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 fp.write(b";") # end of file
@ -369,7 +395,7 @@ def get_interlace(im):
return interlace return interlace
def get_local_header(fp, im, offset, flags): def _get_local_header(fp, im, offset, flags):
transparent_color_exists = False transparent_color_exists = False
try: try:
transparency = im.encoderinfo["transparency"] transparency = im.encoderinfo["transparency"]
@ -592,7 +618,7 @@ def getdata(im, offset=(0, 0), **params):
im.encoderinfo = params im.encoderinfo = params
# local image header # 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])]) 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_open(GifImageFile.format, GifImageFile, _accept)
Image.register_save(GifImageFile.format, _save) Image.register_save(GifImageFile.format, _save)
Image.register_save_all(GifImageFile.format, _save_all)
Image.register_extension(GifImageFile.format, ".gif") Image.register_extension(GifImageFile.format, ".gif")
Image.register_mime(GifImageFile.format, "image/gif") Image.register_mime(GifImageFile.format, "image/gif")

View File

@ -204,6 +204,7 @@ ID = []
OPEN = {} OPEN = {}
MIME = {} MIME = {}
SAVE = {} SAVE = {}
SAVE_ALL = {}
EXTENSION = {} EXTENSION = {}
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -1669,6 +1670,10 @@ class Image(object):
# may mutate self! # may mutate self!
self.load() self.load()
save_all = False
if 'save_all' in params:
save_all = params['save_all']
del params['save_all']
self.encoderinfo = params self.encoderinfo = params
self.encoderconfig = () self.encoderconfig = ()
@ -1686,11 +1691,12 @@ class Image(object):
except KeyError: except KeyError:
raise KeyError(ext) # unknown extension raise KeyError(ext) # unknown extension
try: if format.upper() not in SAVE:
save_handler = SAVE[format.upper()]
except KeyError:
init() 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): if isPath(fp):
fp = builtins.open(fp, "wb") fp = builtins.open(fp, "wb")
@ -2469,6 +2475,18 @@ def register_save(id, driver):
SAVE[id.upper()] = 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): def register_extension(id, extension):
""" """
Registers an image extension. This function should not be Registers an image extension. This function should not be

View File

@ -14,104 +14,9 @@
# See the README file for information on usage and redistribution. # 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 = <generate image i>
# 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 __future__ import print_function
from PIL import Image, ImageChops, ImageSequence from PIL import Image
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()
if __name__ == "__main__": if __name__ == "__main__":
@ -122,4 +27,5 @@ if __name__ == "__main__":
print("Usage: gifmaker infile outfile") print("Usage: gifmaker infile outfile")
sys.exit(1) sys.exit(1)
compress(sys.argv[1], sys.argv[2]) im = Image.open(sys.argv[1])
im.save(sys.argv[2], save_all=True)

View File

@ -71,6 +71,14 @@ class TestFileGif(PillowTestCase):
self.assert_image_similar(reread.convert('RGB'), hopper(), 50) 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): def test_palette_handling(self):
# see https://github.com/python-pillow/Pillow/issues/513 # see https://github.com/python-pillow/Pillow/issues/513