mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 09:26: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.
|
# 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"
|
__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
|
# convert on the fly (EXPERIMENTAL -- I'm not sure PIL
|
||||||
# should automatically convert images on save...)
|
# should automatically convert images on save...)
|
||||||
if Image.getmodebase(im.mode) == "RGB":
|
if Image.getmodebase(im.mode) == "RGB":
|
||||||
|
if initial_call:
|
||||||
palette_size = 256
|
palette_size = 256
|
||||||
if im.palette:
|
if im.palette:
|
||||||
palette_size = len(im.palette.getdata()[1]) // 3
|
palette_size = len(im.palette.getdata()[1]) // 3
|
||||||
return im.convert("P", palette=1, colors=palette_size)
|
return im.convert("P", palette=1, colors=palette_size)
|
||||||
|
else:
|
||||||
|
return im.convert("P")
|
||||||
return im.convert("L")
|
return im.convert("L")
|
||||||
|
|
||||||
|
|
||||||
|
@ -317,6 +321,7 @@ def _save_all(im, fp, filename):
|
||||||
|
|
||||||
def _save(im, fp, filename, save_all=False):
|
def _save(im, fp, filename, save_all=False):
|
||||||
|
|
||||||
|
im.encoderinfo.update(im.info)
|
||||||
if _imaging_gif:
|
if _imaging_gif:
|
||||||
# call external driver
|
# call external driver
|
||||||
try:
|
try:
|
||||||
|
@ -328,7 +333,7 @@ def _save(im, fp, filename, save_all=False):
|
||||||
if im.mode in RAWMODE:
|
if im.mode in RAWMODE:
|
||||||
im_out = im.copy()
|
im_out = im.copy()
|
||||||
else:
|
else:
|
||||||
im_out = _convert_mode(im)
|
im_out = _convert_mode(im, True)
|
||||||
|
|
||||||
# header
|
# header
|
||||||
try:
|
try:
|
||||||
|
@ -340,6 +345,7 @@ def _save(im, fp, filename, save_all=False):
|
||||||
if save_all:
|
if save_all:
|
||||||
previous = None
|
previous = None
|
||||||
|
|
||||||
|
first_frame = None
|
||||||
for im_frame in ImageSequence.Iterator(im):
|
for im_frame in ImageSequence.Iterator(im):
|
||||||
im_frame = _convert_mode(im_frame)
|
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)
|
# e.g. getdata(im_frame, duration=1000)
|
||||||
if not previous:
|
if not previous:
|
||||||
# global header
|
# global header
|
||||||
for s in getheader(im_frame, palette, im.encoderinfo)[0] + getdata(im_frame):
|
first_frame = getheader(im_frame, palette, im.encoderinfo)[0]
|
||||||
fp.write(s)
|
first_frame += getdata(im_frame, (0, 0), **im.encoderinfo)
|
||||||
else:
|
else:
|
||||||
|
if first_frame:
|
||||||
|
for s in first_frame:
|
||||||
|
fp.write(s)
|
||||||
|
first_frame = None
|
||||||
|
|
||||||
# delta frame
|
# delta frame
|
||||||
delta = ImageChops.subtract_modulo(im_frame, previous)
|
delta = ImageChops.subtract_modulo(im_frame, previous.copy())
|
||||||
bbox = delta.getbbox()
|
bbox = delta.getbbox()
|
||||||
|
|
||||||
if bbox:
|
if bbox:
|
||||||
# compress difference
|
# 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)
|
fp.write(s)
|
||||||
else:
|
else:
|
||||||
# FIXME: what should we do in this case?
|
# FIXME: what should we do in this case?
|
||||||
pass
|
pass
|
||||||
previous = im_frame.copy()
|
previous = im_frame
|
||||||
else:
|
if first_frame:
|
||||||
|
save_all = False
|
||||||
|
if not save_all:
|
||||||
header = getheader(im_out, palette, im.encoderinfo)[0]
|
header = getheader(im_out, palette, im.encoderinfo)[0]
|
||||||
for s in header:
|
for s in header:
|
||||||
fp.write(s)
|
fp.write(s)
|
||||||
|
@ -533,8 +547,19 @@ def getheader(im, palette=None, info=None):
|
||||||
|
|
||||||
# Header Block
|
# Header Block
|
||||||
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
# 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 = [
|
header = [
|
||||||
b"GIF87a" + # signature + version
|
b"GIF"+version + # signature + version
|
||||||
o16(im.size[0]) + # canvas width
|
o16(im.size[0]) + # canvas width
|
||||||
o16(im.size[1]) # canvas height
|
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
|
# size of global color table + global color table flag
|
||||||
header.append(o8(color_table_size + 128))
|
header.append(o8(color_table_size + 128))
|
||||||
# background + reserved/aspect
|
# 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))
|
header.append(o8(background) + o8(0))
|
||||||
# end of Logical Screen Descriptor
|
# end of Logical Screen Descriptor
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ class TestFileGif(PillowTestCase):
|
||||||
self.assertEqual(im.mode, "P")
|
self.assertEqual(im.mode, "P")
|
||||||
self.assertEqual(im.size, (128, 128))
|
self.assertEqual(im.size, (128, 128))
|
||||||
self.assertEqual(im.format, "GIF")
|
self.assertEqual(im.format, "GIF")
|
||||||
|
self.assertEqual(im.info["version"], b"GIF89a")
|
||||||
|
|
||||||
def test_invalid_file(self):
|
def test_invalid_file(self):
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
@ -95,6 +96,21 @@ class TestFileGif(PillowTestCase):
|
||||||
|
|
||||||
self.assertEqual(reread.n_frames, 5)
|
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):
|
def test_palette_handling(self):
|
||||||
# see https://github.com/python-pillow/Pillow/issues/513
|
# see https://github.com/python-pillow/Pillow/issues/513
|
||||||
|
|
||||||
|
@ -251,6 +267,34 @@ class TestFileGif(PillowTestCase):
|
||||||
|
|
||||||
self.assertEqual(reread.info['background'], im.info['background'])
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
|
@ -54,8 +54,11 @@ GIF
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
PIL reads GIF87a and GIF89a versions of the GIF file format. The library writes
|
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
|
run-length encoded files in GIF87a by default, unless GIF89a features
|
||||||
grayscale (``L``) or palette mode (``P``) images.
|
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
|
The :py:meth:`~PIL.Image.Image.open` method sets the following
|
||||||
:py:attr:`~PIL.Image.Image.info` properties:
|
:py:attr:`~PIL.Image.Image.info` properties:
|
||||||
|
@ -73,12 +76,32 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
|
||||||
**version**
|
**version**
|
||||||
Version (either ``GIF87a`` or ``GIF89a``).
|
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
|
Reading sequences
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell`
|
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
|
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
|
Reading local images
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
Loading…
Reference in New Issue
Block a user