Merge pull request #1384 from radarhere/gifparams

GIF 89a and animation parameters
This commit is contained in:
wiredfool 2015-09-18 14:41:45 +01:00
commit a38fb2d0c5
3 changed files with 119 additions and 18 deletions

View File

@ -24,7 +24,8 @@
# See the README file for information on usage and redistribution.
#
from PIL import Image, ImageFile, ImagePalette, ImageChops, ImageSequence, _binary
from PIL import Image, ImageFile, ImagePalette, \
ImageChops, ImageSequence, _binary
__version__ = "0.9"
@ -300,14 +301,17 @@ RAWMODE = {
}
def _convert_mode(im):
def _convert_mode(im, initial_call=False):
# convert on the fly (EXPERIMENTAL -- I'm not sure PIL
# should automatically convert images on save...)
if Image.getmodebase(im.mode) == "RGB":
palette_size = 256
if im.palette:
palette_size = len(im.palette.getdata()[1]) // 3
return im.convert("P", palette=1, colors=palette_size)
if initial_call:
palette_size = 256
if im.palette:
palette_size = len(im.palette.getdata()[1]) // 3
return im.convert("P", palette=1, colors=palette_size)
else:
return im.convert("P")
return im.convert("L")
@ -317,6 +321,7 @@ def _save_all(im, fp, filename):
def _save(im, fp, filename, save_all=False):
im.encoderinfo.update(im.info)
if _imaging_gif:
# call external driver
try:
@ -328,7 +333,7 @@ def _save(im, fp, filename, save_all=False):
if im.mode in RAWMODE:
im_out = im.copy()
else:
im_out = _convert_mode(im)
im_out = _convert_mode(im, True)
# header
try:
@ -340,6 +345,7 @@ def _save(im, fp, filename, save_all=False):
if save_all:
previous = None
first_frame = None
for im_frame in ImageSequence.Iterator(im):
im_frame = _convert_mode(im_frame)
@ -347,22 +353,30 @@ def _save(im, fp, filename, save_all=False):
# 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)
first_frame = getheader(im_frame, palette, im.encoderinfo)[0]
first_frame += getdata(im_frame, (0, 0), **im.encoderinfo)
else:
if first_frame:
for s in first_frame:
fp.write(s)
first_frame = None
# delta frame
delta = ImageChops.subtract_modulo(im_frame, previous)
delta = ImageChops.subtract_modulo(im_frame, previous.copy())
bbox = delta.getbbox()
if bbox:
# compress difference
for s in getdata(im_frame.crop(bbox), offset=bbox[:2]):
for s in getdata(im_frame.crop(bbox),
bbox[:2], **im.encoderinfo):
fp.write(s)
else:
# FIXME: what should we do in this case?
pass
previous = im_frame.copy()
else:
previous = im_frame
if first_frame:
save_all = False
if not save_all:
header = getheader(im_out, palette, im.encoderinfo)[0]
for s in header:
fp.write(s)
@ -533,8 +547,19 @@ def getheader(im, palette=None, info=None):
# Header Block
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
version = b"87a"
for extensionKey in ["transparency", "duration", "loop"]:
if info and extensionKey in info and \
not (extensionKey == "duration" and info[extensionKey] == 0):
version = b"89a"
break
else:
if im.info.get("version") == "89a":
version = b"89a"
header = [
b"GIF87a" + # signature + version
b"GIF"+version + # signature + version
o16(im.size[0]) + # canvas width
o16(im.size[1]) # canvas height
]
@ -591,7 +616,16 @@ def getheader(im, palette=None, info=None):
# size of global color table + global color table flag
header.append(o8(color_table_size + 128))
# background + reserved/aspect
background = im.info["background"] if "background" in im.info else 0
if info and "background" in info:
background = info["background"]
elif "background" in im.info:
# This elif is redundant within GifImagePlugin
# since im.info parameters are bundled into the info dictionary
# However, external scripts may call getheader directly
# So this maintains earlier behaviour
background = im.info["background"]
else:
background = 0
header.append(o8(background) + o8(0))
# end of Logical Screen Descriptor

View File

@ -24,6 +24,7 @@ class TestFileGif(PillowTestCase):
self.assertEqual(im.mode, "P")
self.assertEqual(im.size, (128, 128))
self.assertEqual(im.format, "GIF")
self.assertEqual(im.info["version"], b"GIF89a")
def test_invalid_file(self):
invalid_file = "Tests/images/flower.jpg"
@ -95,6 +96,21 @@ class TestFileGif(PillowTestCase):
self.assertEqual(reread.n_frames, 5)
def test_headers_saving_for_animated_gifs(self):
important_headers = ['background', 'version', 'duration', 'loop']
# Multiframe image
im = Image.open("Tests/images/dispose_bgnd.gif")
out = self.tempfile('temp.gif')
im.save(out, save_all=True)
reread = Image.open(out)
for header in important_headers:
self.assertEqual(
im.info[header],
reread.info[header]
)
def test_palette_handling(self):
# see https://github.com/python-pillow/Pillow/issues/513
@ -251,6 +267,34 @@ class TestFileGif(PillowTestCase):
self.assertEqual(reread.info['background'], im.info['background'])
def test_version(self):
out = self.tempfile('temp.gif')
# Test that GIF87a is used by default
im = Image.new('L', (100, 100), '#000')
im.save(out)
reread = Image.open(out)
self.assertEqual(reread.info["version"], b"GIF87a")
# Test that adding a GIF89a feature changes the version
im.info["transparency"] = 1
im.save(out)
reread = Image.open(out)
self.assertEqual(reread.info["version"], b"GIF89a")
# Test that a GIF87a image is also saved in that format
im = Image.open(TEST_GIF)
im.save(out)
reread = Image.open(out)
self.assertEqual(reread.info["version"], b"GIF87a")
# Test that a GIF89a image is also saved in that format
im.info["version"] = "GIF89a"
im.save(out)
reread = Image.open(out)
self.assertEqual(reread.info["version"], b"GIF87a")
if __name__ == '__main__':
unittest.main()

View File

@ -54,8 +54,11 @@ GIF
^^^
PIL reads GIF87a and GIF89a versions of the GIF file format. The library writes
run-length encoded GIF87a files. Note that GIF files are always read as
grayscale (``L``) or palette mode (``P``) images.
run-length encoded files in GIF87a by default, unless GIF89a features
are used or GIF89a is already in use.
Note that GIF files are always read as grayscale (``L``)
or palette mode (``P``) images.
The :py:meth:`~PIL.Image.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -73,12 +76,32 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
**version**
Version (either ``GIF87a`` or ``GIF89a``).
**duration**
May not be present. The time to display each frame of the GIF, in
milliseconds.
**loop**
May not be present. The number of times the GIF should loop.
Reading sequences
~~~~~~~~~~~~~~~~~
The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell`
methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind
the file by seeking to the first frame. Random access is not supported. ``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame.
the file by seeking to the first frame. Random access is not supported.
``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame.
Saving sequences
~~~~~~~~~~~~~~~~
When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used,
by default only the first frame will be saved. To save all frames, the
``save_all`` parameter must be present and set to ``True``.
If present, the ``loop`` parameter can be used to set the number of times
the GIF should loop, and the ``duration`` parameter can set the number of
milliseconds between each frame.
Reading local images
~~~~~~~~~~~~~~~~~~~~