Merge pull request #2103 from radarhere/append_images

Added append_images parameter to GIF saving
This commit is contained in:
wiredfool 2016-09-22 10:21:31 +01:00 committed by GitHub
commit 25797b2baa
3 changed files with 114 additions and 65 deletions

View File

@ -351,34 +351,38 @@ def _save(im, fp, filename, save_all=False):
previous = None previous = None
first_frame = None first_frame = None
for im_frame in ImageSequence.Iterator(im): append_images = im.encoderinfo.get("append_images", [])
im_frame = _convert_mode(im_frame) for imSequence in [im]+append_images:
for im_frame in ImageSequence.Iterator(imSequence):
encoderinfo = im.encoderinfo.copy()
im_frame = _convert_mode(im_frame)
# To specify duration, add the time in milliseconds to getdata(), # To specify duration, add the time in milliseconds to getdata(),
# e.g. getdata(im_frame, duration=1000) # e.g. getdata(im_frame, duration=1000)
if not previous: if not previous:
# global header # global header
first_frame = getheader(im_frame, palette, im.encoderinfo)[0] first_frame = getheader(im_frame, palette, encoderinfo)[0]
first_frame += getdata(im_frame, (0, 0), **im.encoderinfo) first_frame += getdata(im_frame, (0, 0), **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.copy())
bbox = delta.getbbox()
if bbox:
# compress difference
for s in getdata(im_frame.crop(bbox),
bbox[:2], **im.encoderinfo):
fp.write(s)
else: else:
# FIXME: what should we do in this case? if first_frame:
pass for s in first_frame:
previous = im_frame fp.write(s)
first_frame = None
# delta frame
delta = ImageChops.subtract_modulo(im_frame, previous.copy())
bbox = delta.getbbox()
if bbox:
# compress difference
encoderinfo['include_color_table'] = True
for s in getdata(im_frame.crop(bbox),
bbox[:2], **encoderinfo):
fp.write(s)
else:
# FIXME: what should we do in this case?
pass
previous = im_frame
if first_frame: if first_frame:
save_all = False save_all = False
if not save_all: if not save_all:
@ -455,7 +459,7 @@ def _get_local_header(fp, im, offset, flags):
fp.write(b"!" + fp.write(b"!" +
o8(249) + # extension intro o8(249) + # extension intro
o8(4) + # length o8(4) + # length
o8(transparency_flag) + # transparency info present o8(transparency_flag) + # packed fields
o16(duration) + # duration o16(duration) + # duration
o8(transparency) + # transparency index o8(transparency) + # transparency index
o8(0)) o8(0))
@ -476,13 +480,27 @@ def _get_local_header(fp, im, offset, flags):
o8(1) + o8(1) +
o16(number_of_loops) + # number of loops o16(number_of_loops) + # number of loops
o8(0)) o8(0))
include_color_table = im.encoderinfo.get('include_color_table')
if include_color_table:
try:
palette = im.encoderinfo["palette"]
except KeyError:
palette = None
palette_bytes = _get_palette_bytes(im, palette, im.encoderinfo)[0]
color_table_size = _get_color_table_size(palette_bytes)
if color_table_size:
flags = flags | 128 # local color table flag
flags = flags | color_table_size
fp.write(b"," + fp.write(b"," +
o16(offset[0]) + # offset o16(offset[0]) + # offset
o16(offset[1]) + o16(offset[1]) +
o16(im.size[0]) + # size o16(im.size[0]) + # size
o16(im.size[1]) + o16(im.size[1]) +
o8(flags) + # flags o8(flags)) # flags
o8(8)) # bits if include_color_table and color_table_size:
fp.write(_get_header_palette(palette_bytes))
fp.write(o8(8)) # bits
def _save_netpbm(im, fp, filename): def _save_netpbm(im, fp, filename):
@ -550,31 +568,25 @@ def _get_used_palette_colors(im):
return used_palette_colors return used_palette_colors
def _get_color_table_size(palette_bytes):
# calculate the palette size for the header
import math
color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1
if color_table_size < 0:
color_table_size = 0
return color_table_size
def getheader(im, palette=None, info=None): def _get_header_palette(palette_bytes):
"""Return a list of strings representing a GIF header""" color_table_size = _get_color_table_size(palette_bytes)
# Header Block # add the missing amount of bytes
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp # the palette has to be 2<<n in size
actual_target_size_diff = (2 << color_table_size) - len(palette_bytes)//3
version = b"87a" if actual_target_size_diff > 0:
for extensionKey in ["transparency", "duration", "loop", "comment"]: palette_bytes += o8(0) * 3 * actual_target_size_diff
if info and extensionKey in info: return palette_bytes
if ((extensionKey == "duration" and info[extensionKey] == 0) or
(extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))):
continue
version = b"89a"
break
else:
if im.info.get("version") == "89a":
version = b"89a"
header = [
b"GIF"+version + # signature + version
o16(im.size[0]) + # canvas width
o16(im.size[1]) # canvas height
]
def _get_palette_bytes(im, palette, info):
if im.mode == "P": if im.mode == "P":
if palette and isinstance(palette, bytes): if palette and isinstance(palette, bytes):
source_palette = palette[:768] source_palette = palette[:768]
@ -617,15 +629,38 @@ def getheader(im, palette=None, info=None):
if not palette_bytes: if not palette_bytes:
palette_bytes = source_palette palette_bytes = source_palette
return palette_bytes, used_palette_colors
def getheader(im, palette=None, info=None):
"""Return a list of strings representing a GIF header"""
# Header Block
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
version = b"87a"
for extensionKey in ["transparency", "duration", "loop", "comment"]:
if info and extensionKey in info:
if ((extensionKey == "duration" and info[extensionKey] == 0) or
(extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))):
continue
version = b"89a"
break
else:
if im.info.get("version") == "89a":
version = b"89a"
header = [
b"GIF"+version + # signature + version
o16(im.size[0]) + # canvas width
o16(im.size[1]) # canvas height
]
palette_bytes, used_palette_colors = _get_palette_bytes(im, palette, info)
# Logical Screen Descriptor # Logical Screen Descriptor
# calculate the palette size for the header color_table_size = _get_color_table_size(palette_bytes)
import math
color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1
if color_table_size < 0:
color_table_size = 0
# 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)) # packed fields
# background + reserved/aspect # background + reserved/aspect
if info and "background" in info: if info and "background" in info:
background = info["background"] background = info["background"]
@ -640,14 +675,8 @@ def getheader(im, palette=None, info=None):
header.append(o8(background) + o8(0)) header.append(o8(background) + o8(0))
# end of Logical Screen Descriptor # end of Logical Screen Descriptor
# add the missing amount of bytes
# the palette has to be 2<<n in size
actual_target_size_diff = (2 << color_table_size) - len(palette_bytes)//3
if actual_target_size_diff > 0:
palette_bytes += o8(0) * 3 * actual_target_size_diff
# Header + Logical Screen Descriptor + Global Color Table # Header + Logical Screen Descriptor + Global Color Table
header.append(palette_bytes) header.append(_get_header_palette(palette_bytes))
return header, used_palette_colors return header, used_palette_colors

View File

@ -306,6 +306,24 @@ class TestFileGif(PillowTestCase):
reread = Image.open(out) reread = Image.open(out)
self.assertEqual(reread.info["version"], b"GIF87a") self.assertEqual(reread.info["version"], b"GIF87a")
def test_append_images(self):
out = self.tempfile('temp.gif')
# Test appending single frame images
im = Image.new('RGB', (100, 100), '#f00')
ims = [Image.new('RGB', (100, 100), color) for color in ['#0f0', '#00f']]
im.save(out, save_all=True, append_images=ims)
reread = Image.open(out)
self.assertEqual(reread.n_frames, 3)
# Tests appending single and multiple frame images
im = Image.open("Tests/images/dispose_none.gif")
ims = [Image.open("Tests/images/dispose_prev.gif")]
im.save(out, save_all=True, append_images=ims)
reread = Image.open(out)
self.assertEqual(reread.n_frames, 10)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -103,7 +103,9 @@ Saving sequences
When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used, 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 by default only the first frame will be saved. To save all frames, the
``save_all`` parameter must be present and set to ``True``. ``save_all`` parameter must be present and set to ``True``. To append
additional frames when saving, the ``append_images`` parameter can be set to a
list of images containing the extra frames.
If present, the ``loop`` parameter can be used to set the number of times 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 the GIF should loop, and the ``duration`` parameter can set the number of