mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-24 00:46:16 +03:00
Merge pull request #1384 from radarhere/gifparams
GIF 89a and animation parameters
This commit is contained in:
commit
a38fb2d0c5
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
Loading…
Reference in New Issue
Block a user