mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-11-04 09:57:43 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			803 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			803 lines
		
	
	
		
			26 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
 | 
						|
 | 
						|
import itertools
 | 
						|
 | 
						|
__version__ = "0.9"
 | 
						|
 | 
						|
 | 
						|
# --------------------------------------------------------------------
 | 
						|
# 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"
 | 
						|
    _close_exclusive_fp_after_loading = False
 | 
						|
    
 | 
						|
    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._crop(self.dispose, 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._crop(self.im, 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
 | 
						|
 | 
						|
RAWMODE = {
 | 
						|
    "1": "L",
 | 
						|
    "L": "L",
 | 
						|
    "P": "P"
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
def _normalize_mode(im, initial_call=False):
 | 
						|
    """
 | 
						|
    Takes an image (or frame), returns an image in a mode that is appropriate
 | 
						|
    for saving in a Gif.
 | 
						|
 | 
						|
    It may return the original image, or it may return an image converted to
 | 
						|
    palette or 'L' mode.
 | 
						|
 | 
						|
    UNDONE: What is the point of mucking with the initial call palette, for
 | 
						|
    an image that shouldn't have a palette, or it would be a mode 'P' and
 | 
						|
    get returned in the RAWMODE clause.
 | 
						|
 | 
						|
    :param im: Image object
 | 
						|
    :param initial_call: Default false, set to true for a single frame.
 | 
						|
    :returns: Image object
 | 
						|
    """
 | 
						|
    if im.mode in RAWMODE:
 | 
						|
        im.load()
 | 
						|
        return im
 | 
						|
    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=Image.ADAPTIVE, colors=palette_size)
 | 
						|
        else:
 | 
						|
            return im.convert("P")
 | 
						|
    return im.convert("L")
 | 
						|
 | 
						|
 | 
						|
def _normalize_palette(im, palette, info):
 | 
						|
    """
 | 
						|
    Normalizes the palette for image.
 | 
						|
      - Sets the palette to the incoming palette, if provided.
 | 
						|
      - Ensures that there's a palette for L mode images
 | 
						|
      - Optimizes the palette if necessary/desired.
 | 
						|
 | 
						|
    :param im: Image object
 | 
						|
    :param palette: bytes object containing the source palette, or ....
 | 
						|
    :param info: encoderinfo
 | 
						|
    :returns: Image object
 | 
						|
    """
 | 
						|
    source_palette = None
 | 
						|
    if palette:
 | 
						|
        # a bytes palette
 | 
						|
        if isinstance(palette, (bytes, bytearray, list)):
 | 
						|
            source_palette = bytearray(palette[:768])
 | 
						|
        if isinstance(palette, ImagePalette.ImagePalette):
 | 
						|
            source_palette = bytearray(itertools.chain.from_iterable(
 | 
						|
                                zip(palette.palette[:256],
 | 
						|
                                    palette.palette[256:512],
 | 
						|
                                    palette.palette[512:768])))
 | 
						|
 | 
						|
    if im.mode == "P":
 | 
						|
        if not source_palette:
 | 
						|
            source_palette = im.im.getpalette("RGB")[:768]
 | 
						|
    else:  # L-mode
 | 
						|
        if not source_palette:
 | 
						|
            source_palette = bytearray(i//3 for i in range(768))
 | 
						|
        im.palette = ImagePalette.ImagePalette("RGB",
 | 
						|
                                               palette=source_palette)
 | 
						|
 | 
						|
    used_palette_colors = _get_optimize(im, info)
 | 
						|
    if used_palette_colors is not None:
 | 
						|
        return im.remap_palette(used_palette_colors, source_palette)
 | 
						|
 | 
						|
    im.palette.palette = source_palette
 | 
						|
    return im
 | 
						|
 | 
						|
 | 
						|
def _write_single_frame(im, fp, palette):
 | 
						|
    im_out = _normalize_mode(im, True)
 | 
						|
    im_out = _normalize_palette(im_out, palette, im.encoderinfo)
 | 
						|
 | 
						|
    for s in _get_global_header(im_out, im.encoderinfo):
 | 
						|
        fp.write(s)
 | 
						|
 | 
						|
    # local image header
 | 
						|
    flags = 0
 | 
						|
    if get_interlace(im):
 | 
						|
        flags = flags | 64
 | 
						|
    _write_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
 | 
						|
 | 
						|
 | 
						|
def _write_multiple_frames(im, fp, palette):
 | 
						|
 | 
						|
    duration = im.encoderinfo.get("duration", None)
 | 
						|
 | 
						|
    im_frames = []
 | 
						|
    frame_count = 0
 | 
						|
    for imSequence in [im]+im.encoderinfo.get("append_images", []):
 | 
						|
        for im_frame in ImageSequence.Iterator(imSequence):
 | 
						|
            # a copy is required here since seek can still mutate the image
 | 
						|
            im_frame = _normalize_mode(im_frame.copy())
 | 
						|
            im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)
 | 
						|
 | 
						|
            encoderinfo = im.encoderinfo.copy()
 | 
						|
            if isinstance(duration, (list, tuple)):
 | 
						|
                encoderinfo['duration'] = duration[frame_count]
 | 
						|
            frame_count += 1
 | 
						|
 | 
						|
            if im_frames:
 | 
						|
                # delta frame
 | 
						|
                previous = im_frames[-1]
 | 
						|
                if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']):
 | 
						|
                    delta = ImageChops.subtract_modulo(im_frame,
 | 
						|
                                                       previous['im'])
 | 
						|
                else:
 | 
						|
                    delta = ImageChops.subtract_modulo(im_frame.convert('RGB'),
 | 
						|
                                                       previous['im'].convert('RGB'))
 | 
						|
                bbox = delta.getbbox()
 | 
						|
                if not bbox:
 | 
						|
                    # This frame is identical to the previous frame
 | 
						|
                    if duration:
 | 
						|
                        previous['encoderinfo']['duration'] += encoderinfo['duration']
 | 
						|
                    continue
 | 
						|
            else:
 | 
						|
                bbox = None
 | 
						|
            im_frames.append({
 | 
						|
                'im': im_frame,
 | 
						|
                'bbox': bbox,
 | 
						|
                'encoderinfo': encoderinfo
 | 
						|
            })
 | 
						|
 | 
						|
    if len(im_frames) > 1:
 | 
						|
        for frame_data in im_frames:
 | 
						|
            im_frame = frame_data['im']
 | 
						|
            if not frame_data['bbox']:
 | 
						|
                # global header
 | 
						|
                for s in _get_global_header(im_frame,
 | 
						|
                                            frame_data['encoderinfo']):
 | 
						|
                    fp.write(s)
 | 
						|
                offset = (0, 0)
 | 
						|
            else:
 | 
						|
                # compress difference
 | 
						|
                frame_data['encoderinfo']['include_color_table'] = True
 | 
						|
 | 
						|
                im_frame = im_frame.crop(frame_data['bbox'])
 | 
						|
                offset = frame_data['bbox'][:2]
 | 
						|
            _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo'])
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
    # header
 | 
						|
    try:
 | 
						|
        palette = im.encoderinfo["palette"]
 | 
						|
    except KeyError:
 | 
						|
        palette = None
 | 
						|
        im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
 | 
						|
 | 
						|
    if not save_all or not _write_multiple_frames(im, fp, palette):
 | 
						|
        _write_single_frame(im, fp, palette)
 | 
						|
 | 
						|
    fp.write(b";")  # end of file
 | 
						|
 | 
						|
    if hasattr(fp, "flush"):
 | 
						|
        fp.flush()
 | 
						|
 | 
						|
 | 
						|
def get_interlace(im):
 | 
						|
    interlace = im.encoderinfo.get("interlace", 1)
 | 
						|
 | 
						|
    # workaround for @PIL153
 | 
						|
    if min(im.size) < 16:
 | 
						|
        interlace = 0
 | 
						|
 | 
						|
    return interlace
 | 
						|
 | 
						|
 | 
						|
def _write_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
 | 
						|
            try:
 | 
						|
                transparency = used_palette_colors.index(transparency)
 | 
						|
            except ValueError:
 | 
						|
                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:
 | 
						|
        palette = im.encoderinfo.get("palette", None)
 | 
						|
        palette_bytes = _get_palette_bytes(im)
 | 
						|
        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):
 | 
						|
 | 
						|
    # Unused by default.
 | 
						|
    # To use, uncomment the register_save call at the end of the file.
 | 
						|
    #
 | 
						|
    # 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
 | 
						|
    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
 | 
						|
 | 
						|
 | 
						|
# 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):
 | 
						|
    """
 | 
						|
    Palette optimization is a potentially expensive operation.
 | 
						|
 | 
						|
    This function determines if the palette should be optimized using
 | 
						|
    some heuristics, then returns the list of palette entries in use.
 | 
						|
 | 
						|
    :param im: Image object
 | 
						|
    :param info: encoderinfo
 | 
						|
    :returns: list of indexes of palette entries in use, or None
 | 
						|
    """
 | 
						|
    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
 | 
						|
        optimise = _FORCE_OPTIMIZE or im.mode == 'L'
 | 
						|
        if optimise or im.width * im.height < 512 * 512:
 | 
						|
            # check which colors are used
 | 
						|
            used_palette_colors = []
 | 
						|
            for i, count in enumerate(im.histogram()):
 | 
						|
                if count:
 | 
						|
                    used_palette_colors.append(i)
 | 
						|
 | 
						|
            if optimise or (len(used_palette_colors) <= 128 and
 | 
						|
               max(used_palette_colors) > len(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 _get_header_palette(palette_bytes):
 | 
						|
    """
 | 
						|
    Returns the palette, null padded to the next power of 2 (*3) bytes
 | 
						|
    suitable for direct inclusion in the GIF header
 | 
						|
 | 
						|
    :param palette_bytes: Unpadded palette bytes, in RGBRGB form
 | 
						|
    :returns: Null padded palette
 | 
						|
    """
 | 
						|
    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):
 | 
						|
    """
 | 
						|
    Gets the palette for inclusion in the gif header
 | 
						|
 | 
						|
    :param im: Image object
 | 
						|
    :returns: Bytes, len<=768 suitable for inclusion in gif header
 | 
						|
    """
 | 
						|
    return im.palette.palette
 | 
						|
 | 
						|
 | 
						|
def _get_global_header(im, info):
 | 
						|
    """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"
 | 
						|
 | 
						|
    palette_bytes = _get_palette_bytes(im)
 | 
						|
    color_table_size = _get_color_table_size(palette_bytes)
 | 
						|
 | 
						|
    background = info["background"] if "background" in info else 0
 | 
						|
 | 
						|
    return [
 | 
						|
        b"GIF"+version +               # signature + version
 | 
						|
        o16(im.size[0]) +              # canvas width
 | 
						|
        o16(im.size[1]),               # canvas height
 | 
						|
 | 
						|
        # Logical Screen Descriptor
 | 
						|
        # size of global color table + global color table flag
 | 
						|
        o8(color_table_size + 128),   # packed fields
 | 
						|
        # background + reserved/aspect
 | 
						|
        o8(background) + o8(0),
 | 
						|
 | 
						|
        # Global Color Table
 | 
						|
        _get_header_palette(palette_bytes)
 | 
						|
    ]
 | 
						|
 | 
						|
 | 
						|
def _write_frame_data(fp, im_frame, offset, params):
 | 
						|
    try:
 | 
						|
        im_frame.encoderinfo = params
 | 
						|
 | 
						|
        # local image header
 | 
						|
        _write_local_header(fp, im_frame, offset, 0)
 | 
						|
 | 
						|
        ImageFile._save(im_frame, fp, [("gif", (0, 0)+im_frame.size, 0,
 | 
						|
                                        RAWMODE[im_frame.mode])])
 | 
						|
 | 
						|
        fp.write(b"\0")  # end of image data
 | 
						|
    finally:
 | 
						|
        del im_frame.encoderinfo
 | 
						|
 | 
						|
# --------------------------------------------------------------------
 | 
						|
# Legacy GIF utilities
 | 
						|
 | 
						|
 | 
						|
def getheader(im, palette=None, info=None):
 | 
						|
    """
 | 
						|
    Legacy Method to get Gif data from image.
 | 
						|
 | 
						|
    Warning:: May modify image data.
 | 
						|
 | 
						|
    :param im: Image object
 | 
						|
    :param palette: bytes object containing the source palette, or ....
 | 
						|
    :param info: encoderinfo
 | 
						|
    :returns: tuple of(list of header items, optimized palette)
 | 
						|
 | 
						|
    """
 | 
						|
    used_palette_colors = _get_optimize(im, info)
 | 
						|
 | 
						|
    if info is None:
 | 
						|
        info = {}
 | 
						|
 | 
						|
    if "background" not in info and "background" in im.info:
 | 
						|
        info["background"] = im.info["background"]
 | 
						|
 | 
						|
    im_mod = _normalize_palette(im, palette, info)
 | 
						|
    im.palette = im_mod.palette
 | 
						|
    im.im = im_mod.im
 | 
						|
    header = _get_global_header(im, info)
 | 
						|
 | 
						|
    return header, used_palette_colors
 | 
						|
 | 
						|
 | 
						|
# To specify duration, add the time in milliseconds to getdata(),
 | 
						|
# e.g. getdata(im_frame, duration=1000)
 | 
						|
def getdata(im, offset=(0, 0), **params):
 | 
						|
    """
 | 
						|
    Legacy Method
 | 
						|
 | 
						|
    Return a list of strings representing this image.
 | 
						|
    The first string is a local image header, the rest contains
 | 
						|
    encoded image data.
 | 
						|
 | 
						|
    :param im: Image object
 | 
						|
    :param offset: Tuple of (x, y) pixels. Defaults to (0,0)
 | 
						|
    :param **params: E.g. duration or other encoder info parameters
 | 
						|
    :returns: List of Bytes containing gif encoded frame data
 | 
						|
 | 
						|
    """
 | 
						|
    class Collector(object):
 | 
						|
        data = []
 | 
						|
 | 
						|
        def write(self, data):
 | 
						|
            self.data.append(data)
 | 
						|
 | 
						|
    im.load()  # make sure raster data is available
 | 
						|
 | 
						|
    fp = Collector()
 | 
						|
 | 
						|
    _write_frame_data(fp, im, offset, params)
 | 
						|
 | 
						|
    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)
 |