mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-28 19:06:18 +03:00
cecdb2af61
Avoids unnecessary creation of temporary files as well as unnecessarily storing process output in memory. The temporary files would sometimes remain and not be cleaned up.
777 lines
25 KiB
Python
777 lines
25 KiB
Python
#
|
|
# The Python Imaging Library.
|
|
# $Id$
|
|
#
|
|
# GIF file handling
|
|
#
|
|
# History:
|
|
# 1995-09-01 fl Created
|
|
# 1996-12-14 fl Added interlace support
|
|
# 1996-12-30 fl Added animation support
|
|
# 1997-01-05 fl Added write support, fixed local colour map bug
|
|
# 1997-02-23 fl Make sure to load raster data in getdata()
|
|
# 1997-07-05 fl Support external decoder (0.4)
|
|
# 1998-07-09 fl Handle all modes when saving (0.5)
|
|
# 1998-07-15 fl Renamed offset attribute to avoid name clash
|
|
# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6)
|
|
# 2001-04-17 fl Added palette optimization (0.7)
|
|
# 2002-06-06 fl Added transparency support for save (0.8)
|
|
# 2004-02-24 fl Disable interlacing for small images
|
|
#
|
|
# Copyright (c) 1997-2004 by Secret Labs AB
|
|
# Copyright (c) 1995-2004 by Fredrik Lundh
|
|
#
|
|
# See the README file for information on usage and redistribution.
|
|
#
|
|
|
|
from . import Image, ImageFile, ImagePalette, \
|
|
ImageChops, ImageSequence
|
|
from ._binary import i8, i16le as i16, o8, o16le as o16
|
|
|
|
__version__ = "0.9"
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
# Helpers
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
# Identify/read GIF files
|
|
|
|
def _accept(prefix):
|
|
return prefix[:6] in [b"GIF87a", b"GIF89a"]
|
|
|
|
|
|
##
|
|
# Image plugin for GIF images. This plugin supports both GIF87 and
|
|
# GIF89 images.
|
|
|
|
class GifImageFile(ImageFile.ImageFile):
|
|
|
|
format = "GIF"
|
|
format_description = "Compuserve GIF"
|
|
global_palette = None
|
|
|
|
def data(self):
|
|
s = self.fp.read(1)
|
|
if s and i8(s):
|
|
return self.fp.read(i8(s))
|
|
return None
|
|
|
|
def _open(self):
|
|
|
|
# Screen
|
|
s = self.fp.read(13)
|
|
if s[:6] not in [b"GIF87a", b"GIF89a"]:
|
|
raise SyntaxError("not a GIF file")
|
|
|
|
self.info["version"] = s[:6]
|
|
self.size = i16(s[6:]), i16(s[8:])
|
|
self.tile = []
|
|
flags = i8(s[10])
|
|
bits = (flags & 7) + 1
|
|
|
|
if flags & 128:
|
|
# get global palette
|
|
self.info["background"] = i8(s[11])
|
|
# check if palette contains colour indices
|
|
p = self.fp.read(3 << bits)
|
|
for i in range(0, len(p), 3):
|
|
if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])):
|
|
p = ImagePalette.raw("RGB", p)
|
|
self.global_palette = self.palette = p
|
|
break
|
|
|
|
self.__fp = self.fp # FIXME: hack
|
|
self.__rewind = self.fp.tell()
|
|
self._n_frames = None
|
|
self._is_animated = None
|
|
self._seek(0) # get ready to read first frame
|
|
|
|
@property
|
|
def n_frames(self):
|
|
if self._n_frames is None:
|
|
current = self.tell()
|
|
try:
|
|
while True:
|
|
self.seek(self.tell() + 1)
|
|
except EOFError:
|
|
self._n_frames = self.tell() + 1
|
|
self.seek(current)
|
|
return self._n_frames
|
|
|
|
@property
|
|
def is_animated(self):
|
|
if self._is_animated is None:
|
|
current = self.tell()
|
|
|
|
try:
|
|
self.seek(1)
|
|
self._is_animated = True
|
|
except EOFError:
|
|
self._is_animated = False
|
|
|
|
self.seek(current)
|
|
return self._is_animated
|
|
|
|
def seek(self, frame):
|
|
if frame == self.__frame:
|
|
return
|
|
if frame < self.__frame:
|
|
self._seek(0)
|
|
|
|
last_frame = self.__frame
|
|
for f in range(self.__frame + 1, frame + 1):
|
|
try:
|
|
self._seek(f)
|
|
except EOFError:
|
|
self.seek(last_frame)
|
|
raise EOFError("no more images in GIF file")
|
|
|
|
def _seek(self, frame):
|
|
|
|
if frame == 0:
|
|
# rewind
|
|
self.__offset = 0
|
|
self.dispose = None
|
|
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
|
|
self.__frame = -1
|
|
self.__fp.seek(self.__rewind)
|
|
self._prev_im = None
|
|
self.disposal_method = 0
|
|
else:
|
|
# ensure that the previous frame was loaded
|
|
if not self.im:
|
|
self.load()
|
|
|
|
if frame != self.__frame + 1:
|
|
raise ValueError("cannot seek to frame %d" % frame)
|
|
self.__frame = frame
|
|
|
|
self.tile = []
|
|
|
|
self.fp = self.__fp
|
|
if self.__offset:
|
|
# backup to last frame
|
|
self.fp.seek(self.__offset)
|
|
while self.data():
|
|
pass
|
|
self.__offset = 0
|
|
|
|
if self.dispose:
|
|
self.im.paste(self.dispose, self.dispose_extent)
|
|
|
|
from copy import copy
|
|
self.palette = copy(self.global_palette)
|
|
|
|
while True:
|
|
|
|
s = self.fp.read(1)
|
|
if not s or s == b";":
|
|
break
|
|
|
|
elif s == b"!":
|
|
#
|
|
# extensions
|
|
#
|
|
s = self.fp.read(1)
|
|
block = self.data()
|
|
if i8(s) == 249:
|
|
#
|
|
# graphic control extension
|
|
#
|
|
flags = i8(block[0])
|
|
if flags & 1:
|
|
self.info["transparency"] = i8(block[3])
|
|
self.info["duration"] = i16(block[1:3]) * 10
|
|
|
|
# disposal method - find the value of bits 4 - 6
|
|
dispose_bits = 0b00011100 & flags
|
|
dispose_bits = dispose_bits >> 2
|
|
if dispose_bits:
|
|
# only set the dispose if it is not
|
|
# unspecified. I'm not sure if this is
|
|
# correct, but it seems to prevent the last
|
|
# frame from looking odd for some animations
|
|
self.disposal_method = dispose_bits
|
|
elif i8(s) == 254:
|
|
#
|
|
# comment extension
|
|
#
|
|
self.info["comment"] = block
|
|
elif i8(s) == 255:
|
|
#
|
|
# application extension
|
|
#
|
|
self.info["extension"] = block, self.fp.tell()
|
|
if block[:11] == b"NETSCAPE2.0":
|
|
block = self.data()
|
|
if len(block) >= 3 and i8(block[0]) == 1:
|
|
self.info["loop"] = i16(block[1:3])
|
|
while self.data():
|
|
pass
|
|
|
|
elif s == b",":
|
|
#
|
|
# local image
|
|
#
|
|
s = self.fp.read(9)
|
|
|
|
# extent
|
|
x0, y0 = i16(s[0:]), i16(s[2:])
|
|
x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])
|
|
self.dispose_extent = x0, y0, x1, y1
|
|
flags = i8(s[8])
|
|
|
|
interlace = (flags & 64) != 0
|
|
|
|
if flags & 128:
|
|
bits = (flags & 7) + 1
|
|
self.palette =\
|
|
ImagePalette.raw("RGB", self.fp.read(3 << bits))
|
|
|
|
# image data
|
|
bits = i8(self.fp.read(1))
|
|
self.__offset = self.fp.tell()
|
|
self.tile = [("gif",
|
|
(x0, y0, x1, y1),
|
|
self.__offset,
|
|
(bits, interlace))]
|
|
break
|
|
|
|
else:
|
|
pass
|
|
# raise IOError, "illegal GIF tag `%x`" % i8(s)
|
|
|
|
try:
|
|
if self.disposal_method < 2:
|
|
# do not dispose or none specified
|
|
self.dispose = None
|
|
elif self.disposal_method == 2:
|
|
# replace with background colour
|
|
self.dispose = Image.core.fill("P", self.size,
|
|
self.info["background"])
|
|
else:
|
|
# replace with previous contents
|
|
if self.im:
|
|
self.dispose = self.im.copy()
|
|
|
|
# only dispose the extent in this frame
|
|
if self.dispose:
|
|
self.dispose = self.dispose.crop(self.dispose_extent)
|
|
except (AttributeError, KeyError):
|
|
pass
|
|
|
|
if not self.tile:
|
|
# self.__fp = None
|
|
raise EOFError
|
|
|
|
self.mode = "L"
|
|
if self.palette:
|
|
self.mode = "P"
|
|
|
|
def tell(self):
|
|
return self.__frame
|
|
|
|
def load_end(self):
|
|
ImageFile.ImageFile.load_end(self)
|
|
|
|
# if the disposal method is 'do not dispose', transparent
|
|
# pixels should show the content of the previous frame
|
|
if self._prev_im and self.disposal_method == 1:
|
|
# we do this by pasting the updated area onto the previous
|
|
# frame which we then use as the current image content
|
|
updated = self.im.crop(self.dispose_extent)
|
|
self._prev_im.paste(updated, self.dispose_extent,
|
|
updated.convert('RGBA'))
|
|
self.im = self._prev_im
|
|
self._prev_im = self.im.copy()
|
|
|
|
# --------------------------------------------------------------------
|
|
# Write GIF files
|
|
|
|
try:
|
|
import _imaging_gif
|
|
except ImportError:
|
|
_imaging_gif = None
|
|
|
|
RAWMODE = {
|
|
"1": "L",
|
|
"L": "L",
|
|
"P": "P",
|
|
}
|
|
|
|
|
|
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":
|
|
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")
|
|
|
|
|
|
def _save_all(im, fp, filename):
|
|
_save(im, fp, filename, save_all=True)
|
|
|
|
|
|
def _save(im, fp, filename, save_all=False):
|
|
|
|
im.encoderinfo.update(im.info)
|
|
if _imaging_gif:
|
|
# call external driver
|
|
try:
|
|
_imaging_gif.save(im, fp, filename)
|
|
return
|
|
except IOError:
|
|
pass # write uncompressed file
|
|
|
|
if im.mode in RAWMODE:
|
|
im_out = im.copy()
|
|
else:
|
|
im_out = _convert_mode(im, True)
|
|
|
|
# header
|
|
try:
|
|
palette = im.encoderinfo["palette"]
|
|
except KeyError:
|
|
palette = None
|
|
im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
|
|
|
|
if save_all:
|
|
previous = None
|
|
|
|
first_frame = None
|
|
append_images = im.encoderinfo.get("append_images", [])
|
|
if "duration" in im.encoderinfo:
|
|
duration = im.encoderinfo["duration"]
|
|
else:
|
|
duration = None
|
|
frame_count = 0
|
|
for imSequence in [im]+append_images:
|
|
for im_frame in ImageSequence.Iterator(imSequence):
|
|
encoderinfo = im.encoderinfo.copy()
|
|
im_frame = _convert_mode(im_frame)
|
|
if isinstance(duration, (list, tuple)):
|
|
encoderinfo["duration"] = duration[frame_count]
|
|
frame_count += 1
|
|
|
|
# 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:
|
|
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:
|
|
header = getheader(im_out, palette, im.encoderinfo)[0]
|
|
for s in header:
|
|
fp.write(s)
|
|
|
|
flags = 0
|
|
|
|
if get_interlace(im):
|
|
flags = flags | 64
|
|
|
|
# local image header
|
|
_get_local_header(fp, im, (0, 0), flags)
|
|
|
|
im_out.encoderconfig = (8, get_interlace(im))
|
|
ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0,
|
|
RAWMODE[im_out.mode])])
|
|
|
|
fp.write(b"\0") # end of image data
|
|
|
|
fp.write(b";") # end of file
|
|
|
|
if hasattr(fp, "flush"):
|
|
fp.flush()
|
|
|
|
|
|
def get_interlace(im):
|
|
try:
|
|
interlace = im.encoderinfo["interlace"]
|
|
except KeyError:
|
|
interlace = 1
|
|
|
|
# workaround for @PIL153
|
|
if min(im.size) < 16:
|
|
interlace = 0
|
|
|
|
return interlace
|
|
|
|
|
|
def _get_local_header(fp, im, offset, flags):
|
|
transparent_color_exists = False
|
|
try:
|
|
transparency = im.encoderinfo["transparency"]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
transparency = int(transparency)
|
|
# optimize the block away if transparent color is not used
|
|
transparent_color_exists = True
|
|
|
|
used_palette_colors = _get_optimize(im, im.encoderinfo)
|
|
if used_palette_colors is not None:
|
|
# adjust the transparency index after optimize
|
|
for i, palette_color in enumerate(used_palette_colors):
|
|
if palette_color == transparency:
|
|
transparency = i
|
|
transparent_color_exists = True
|
|
break
|
|
else:
|
|
transparent_color_exists = False
|
|
|
|
if "duration" in im.encoderinfo:
|
|
duration = int(im.encoderinfo["duration"] / 10)
|
|
else:
|
|
duration = 0
|
|
if transparent_color_exists or duration != 0:
|
|
transparency_flag = 1 if transparent_color_exists else 0
|
|
if not transparent_color_exists:
|
|
transparency = 0
|
|
|
|
fp.write(b"!" +
|
|
o8(249) + # extension intro
|
|
o8(4) + # length
|
|
o8(transparency_flag) + # packed fields
|
|
o16(duration) + # duration
|
|
o8(transparency) + # transparency index
|
|
o8(0))
|
|
|
|
if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]) <= 255:
|
|
fp.write(b"!" +
|
|
o8(254) + # extension intro
|
|
o8(len(im.encoderinfo["comment"])) +
|
|
im.encoderinfo["comment"] +
|
|
o8(0))
|
|
if "loop" in im.encoderinfo:
|
|
number_of_loops = im.encoderinfo["loop"]
|
|
fp.write(b"!" +
|
|
o8(255) + # extension intro
|
|
o8(11) +
|
|
b"NETSCAPE2.0" +
|
|
o8(3) +
|
|
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
|
|
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):
|
|
|
|
#
|
|
# If you need real GIF compression and/or RGB quantization, you
|
|
# can use the external NETPBM/PBMPLUS utilities. See comments
|
|
# below for information on how to enable this.
|
|
|
|
import os
|
|
from subprocess import Popen, check_call, PIPE, CalledProcessError
|
|
import tempfile
|
|
file = im._dump()
|
|
|
|
with open(filename, 'wb') as f:
|
|
if im.mode != "RGB":
|
|
with open(os.devnull, 'wb') as devnull:
|
|
check_call(["ppmtogif", file], stdout=f, stderr=devnull)
|
|
else:
|
|
# Pipe ppmquant output into ppmtogif
|
|
# "ppmquant 256 %s | ppmtogif > %s" % (file, filename)
|
|
quant_cmd = ["ppmquant", "256", file]
|
|
togif_cmd = ["ppmtogif"]
|
|
with open(os.devnull, 'wb') as devnull:
|
|
quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull)
|
|
togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout,
|
|
stdout=f, stderr=devnull)
|
|
|
|
# Allow ppmquant to receive SIGPIPE if ppmtogif exits
|
|
quant_proc.stdout.close()
|
|
|
|
retcode = quant_proc.wait()
|
|
if retcode:
|
|
raise CalledProcessError(retcode, quant_cmd)
|
|
|
|
retcode = togif_proc.wait()
|
|
if retcode:
|
|
raise CalledProcessError(retcode, togif_cmd)
|
|
|
|
try:
|
|
os.unlink(file)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
# GIF utilities
|
|
|
|
# Force optimization so that we can test performance against
|
|
# cases where it took lots of memory and time previously.
|
|
_FORCE_OPTIMIZE = False
|
|
|
|
def _get_optimize(im, info):
|
|
if im.mode in ("P", "L") and info and info.get("optimize", 0):
|
|
# Potentially expensive operation.
|
|
|
|
# The palette saves 3 bytes per color not used, but palette
|
|
# lengths are restricted to 3*(2**N) bytes. Max saving would
|
|
# be 768 -> 6 bytes if we went all the way down to 2 colors.
|
|
# * If we're over 128 colors, we can't save any space.
|
|
# * If there aren't any holes, it's not worth collapsing.
|
|
# * If we have a 'large' image, the palette is in the noise.
|
|
|
|
# create the new palette if not every color is used
|
|
used_palette_colors = _get_used_palette_colors(im)
|
|
if _FORCE_OPTIMIZE or im.mode == 'L' or \
|
|
(len(used_palette_colors) <= 128 and
|
|
max(used_palette_colors) > len(used_palette_colors) and
|
|
im.width * im.height < 512 * 512):
|
|
return used_palette_colors
|
|
|
|
def _get_used_palette_colors(im):
|
|
used_palette_colors = []
|
|
|
|
# check which colors are used
|
|
i = 0
|
|
for count in im.histogram():
|
|
if count:
|
|
used_palette_colors.append(i)
|
|
i += 1
|
|
|
|
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 _get_header_palette(palette_bytes):
|
|
color_table_size = _get_color_table_size(palette_bytes)
|
|
|
|
# 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]
|
|
else:
|
|
source_palette = im.im.getpalette("RGB")[:768]
|
|
else: # L-mode
|
|
if palette and isinstance(palette, bytes):
|
|
source_palette = palette[:768]
|
|
else:
|
|
source_palette = bytearray(i//3 for i in range(768))
|
|
|
|
palette_bytes = None
|
|
|
|
used_palette_colors = _get_optimize(im, info)
|
|
if used_palette_colors is not None:
|
|
palette_bytes = b""
|
|
new_positions = [0]*256
|
|
|
|
# pick only the used colors from the palette
|
|
for i, oldPosition in enumerate(used_palette_colors):
|
|
palette_bytes += source_palette[oldPosition*3:oldPosition*3+3]
|
|
new_positions[oldPosition] = i
|
|
|
|
# replace the palette color id of all pixel with the new id
|
|
|
|
# Palette images are [0..255], mapped through a 1 or 3
|
|
# byte/color map. We need to remap the whole image
|
|
# from palette 1 to palette 2. New_positions is
|
|
# an array of indexes into palette 1. Palette 2 is
|
|
# palette 1 with any holes removed.
|
|
|
|
# We're going to leverage the convert mechanism to use the
|
|
# C code to remap the image from palette 1 to palette 2,
|
|
# by forcing the source image into 'L' mode and adding a
|
|
# mapping 'L' mode palette, then converting back to 'L'
|
|
# sans palette thus converting the image bytes, then
|
|
# assigning the optimized RGB palette.
|
|
|
|
# perf reference, 9500x4000 gif, w/~135 colors
|
|
# 14 sec prepatch, 1 sec postpatch with optimization forced.
|
|
|
|
mapping_palette = bytearray(new_positions)
|
|
|
|
m_im = im.copy()
|
|
m_im.mode = 'P'
|
|
|
|
m_im.palette = ImagePalette.ImagePalette("RGB",
|
|
palette=mapping_palette*3,
|
|
size=768)
|
|
#possibly set palette dirty, then
|
|
#m_im.putpalette(mapping_palette, 'L') # converts to 'P'
|
|
# or just force it.
|
|
# UNDONE -- this is part of the general issue with palettes
|
|
m_im.im.putpalette(*m_im.palette.getdata())
|
|
|
|
m_im = m_im.convert('L')
|
|
|
|
# Internally, we require 768 bytes for a palette.
|
|
new_palette_bytes = (palette_bytes +
|
|
(768 - len(palette_bytes)) * b'\x00')
|
|
m_im.putpalette(new_palette_bytes)
|
|
m_im.palette = ImagePalette.ImagePalette("RGB",
|
|
palette=palette_bytes,
|
|
size=len(palette_bytes))
|
|
|
|
# oh gawd, this is modifying the image in place so I can pass by ref.
|
|
# REFACTOR SOONEST
|
|
im.frombytes(m_im.tobytes())
|
|
|
|
if not palette_bytes:
|
|
palette_bytes = source_palette
|
|
|
|
# returning palette, _not_ padded to 768 bytes like our internal ones.
|
|
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") == b"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
|
|
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)) # packed fields
|
|
# background + reserved/aspect
|
|
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
|
|
|
|
# Header + Logical Screen Descriptor + Global Color Table
|
|
header.append(_get_header_palette(palette_bytes))
|
|
return header, used_palette_colors
|
|
|
|
|
|
def getdata(im, offset=(0, 0), **params):
|
|
"""Return a list of strings representing this image.
|
|
The first string is a local image header, the rest contains
|
|
encoded image data."""
|
|
|
|
class Collector(object):
|
|
data = []
|
|
|
|
def write(self, data):
|
|
self.data.append(data)
|
|
|
|
im.load() # make sure raster data is available
|
|
|
|
fp = Collector()
|
|
|
|
try:
|
|
im.encoderinfo = params
|
|
|
|
# local image header
|
|
_get_local_header(fp, im, offset, 0)
|
|
|
|
ImageFile._save(im, fp, [("gif", (0, 0)+im.size, 0, RAWMODE[im.mode])])
|
|
|
|
fp.write(b"\0") # end of image data
|
|
|
|
finally:
|
|
del im.encoderinfo
|
|
|
|
return fp.data
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
# Registry
|
|
|
|
Image.register_open(GifImageFile.format, GifImageFile, _accept)
|
|
Image.register_save(GifImageFile.format, _save)
|
|
Image.register_save_all(GifImageFile.format, _save_all)
|
|
Image.register_extension(GifImageFile.format, ".gif")
|
|
Image.register_mime(GifImageFile.format, "image/gif")
|
|
|
|
#
|
|
# Uncomment the following line if you wish to use NETPBM/PBMPLUS
|
|
# instead of the built-in "uncompressed" GIF encoder
|
|
|
|
# Image.register_save(GifImageFile.format, _save_netpbm)
|