mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 09:26:16 +03:00
Merge pull request #2103 from radarhere/append_images
Added append_images parameter to GIF saving
This commit is contained in:
commit
25797b2baa
|
@ -351,34 +351,38 @@ def _save(im, fp, filename, save_all=False):
|
|||
previous = None
|
||||
|
||||
first_frame = None
|
||||
for im_frame in ImageSequence.Iterator(im):
|
||||
im_frame = _convert_mode(im_frame)
|
||||
append_images = im.encoderinfo.get("append_images", [])
|
||||
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(),
|
||||
# e.g. getdata(im_frame, duration=1000)
|
||||
if not previous:
|
||||
# global header
|
||||
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.copy())
|
||||
bbox = delta.getbbox()
|
||||
|
||||
if bbox:
|
||||
# compress difference
|
||||
for s in getdata(im_frame.crop(bbox),
|
||||
bbox[:2], **im.encoderinfo):
|
||||
fp.write(s)
|
||||
# To specify duration, add the time in milliseconds to getdata(),
|
||||
# e.g. getdata(im_frame, duration=1000)
|
||||
if not previous:
|
||||
# global header
|
||||
first_frame = getheader(im_frame, palette, encoderinfo)[0]
|
||||
first_frame += getdata(im_frame, (0, 0), **encoderinfo)
|
||||
else:
|
||||
# FIXME: what should we do in this case?
|
||||
pass
|
||||
previous = im_frame
|
||||
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
|
||||
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:
|
||||
save_all = False
|
||||
if not save_all:
|
||||
|
@ -455,7 +459,7 @@ def _get_local_header(fp, im, offset, flags):
|
|||
fp.write(b"!" +
|
||||
o8(249) + # extension intro
|
||||
o8(4) + # length
|
||||
o8(transparency_flag) + # transparency info present
|
||||
o8(transparency_flag) + # packed fields
|
||||
o16(duration) + # duration
|
||||
o8(transparency) + # transparency index
|
||||
o8(0))
|
||||
|
@ -476,13 +480,27 @@ def _get_local_header(fp, im, offset, flags):
|
|||
o8(1) +
|
||||
o16(number_of_loops) + # number of loops
|
||||
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"," +
|
||||
o16(offset[0]) + # offset
|
||||
o16(offset[1]) +
|
||||
o16(im.size[0]) + # size
|
||||
o16(im.size[1]) +
|
||||
o8(flags) + # flags
|
||||
o8(8)) # bits
|
||||
o8(flags)) # flags
|
||||
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):
|
||||
|
@ -550,31 +568,25 @@ def _get_used_palette_colors(im):
|
|||
|
||||
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):
|
||||
"""Return a list of strings representing a GIF header"""
|
||||
def _get_header_palette(palette_bytes):
|
||||
color_table_size = _get_color_table_size(palette_bytes)
|
||||
|
||||
# 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
|
||||
]
|
||||
# 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
|
||||
return palette_bytes
|
||||
|
||||
def _get_palette_bytes(im, palette, info):
|
||||
if im.mode == "P":
|
||||
if palette and isinstance(palette, bytes):
|
||||
source_palette = palette[:768]
|
||||
|
@ -617,15 +629,38 @@ def getheader(im, palette=None, info=None):
|
|||
|
||||
if not palette_bytes:
|
||||
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
|
||||
# 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
|
||||
color_table_size = _get_color_table_size(palette_bytes)
|
||||
# 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
|
||||
if info and "background" in info:
|
||||
background = info["background"]
|
||||
|
@ -640,14 +675,8 @@ def getheader(im, palette=None, info=None):
|
|||
header.append(o8(background) + o8(0))
|
||||
# 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.append(palette_bytes)
|
||||
header.append(_get_header_palette(palette_bytes))
|
||||
return header, used_palette_colors
|
||||
|
||||
|
||||
|
|
|
@ -306,6 +306,24 @@ class TestFileGif(PillowTestCase):
|
|||
reread = Image.open(out)
|
||||
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__':
|
||||
unittest.main()
|
||||
|
|
|
@ -103,7 +103,9 @@ 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``.
|
||||
``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
|
||||
the GIF should loop, and the ``duration`` parameter can set the number of
|
||||
|
|
Loading…
Reference in New Issue
Block a user