| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- """
- PIL formats for multiple images.
- """
- import logging
- import numpy as np
- from .pillow_legacy import PillowFormat, image_as_uint, ndarray_to_pil
- logger = logging.getLogger(__name__)
- NeuQuant = None # we can implement this when we need it
- class TIFFFormat(PillowFormat):
- _modes = "i" # arg, why bother; people should use the tiffile version
- _description = "TIFF format (Pillow)"
- class GIFFormat(PillowFormat):
- """See :mod:`imageio.plugins.pillow_legacy`"""
- _modes = "iI"
- _description = "Static and animated gif (Pillow)"
- # GIF reader needs no modifications compared to base pillow reader
- class Writer(PillowFormat.Writer): # pragma: no cover
- def _open(
- self,
- loop=0,
- duration=None,
- fps=10,
- palettesize=256,
- quantizer=0,
- subrectangles=False,
- ):
- from PIL import __version__ as pillow_version
- major, minor, patch = tuple(int(x) for x in pillow_version.split("."))
- if major == 10 and minor >= 1:
- raise ImportError(
- f"Pillow v{pillow_version} is not supported by ImageIO's legacy "
- "pillow plugin when writing GIFs. Consider switching to the new "
- "plugin or downgrading to `pillow<10.1.0`."
- )
- # Check palettesize
- palettesize = int(palettesize)
- if palettesize < 2 or palettesize > 256:
- raise ValueError("GIF quantize param must be 2..256")
- if palettesize not in [2, 4, 8, 16, 32, 64, 128, 256]:
- palettesize = 2 ** int(np.log2(128) + 0.999)
- logger.warning(
- "Warning: palettesize (%r) modified to a factor of "
- "two between 2-256." % palettesize
- )
- # Duratrion / fps
- if duration is None:
- self._duration = 1.0 / float(fps)
- elif isinstance(duration, (list, tuple)):
- self._duration = [float(d) for d in duration]
- else:
- self._duration = float(duration)
- # loop
- loop = float(loop)
- if loop <= 0 or loop == float("inf"):
- loop = 0
- loop = int(loop)
- # Subrectangles / dispose
- subrectangles = bool(subrectangles)
- self._dispose = 1 if subrectangles else 2
- # The "0" (median cut) quantizer is by far the best
- fp = self.request.get_file()
- self._writer = GifWriter(
- fp, subrectangles, loop, quantizer, int(palettesize)
- )
- def _close(self):
- self._writer.close()
- def _append_data(self, im, meta):
- im = image_as_uint(im, bitdepth=8)
- if im.ndim == 3 and im.shape[-1] == 1:
- im = im[:, :, 0]
- duration = self._duration
- if isinstance(duration, list):
- duration = duration[min(len(duration) - 1, self._writer._count)]
- dispose = self._dispose
- self._writer.add_image(im, duration, dispose)
- return
- def intToBin(i):
- return i.to_bytes(2, byteorder="little")
- class GifWriter: # pragma: no cover
- """Class that for helping write the animated GIF file. This is based on
- code from images2gif.py (part of visvis). The version here is modified
- to allow streamed writing.
- """
- def __init__(
- self,
- file,
- opt_subrectangle=True,
- opt_loop=0,
- opt_quantizer=0,
- opt_palette_size=256,
- ):
- self.fp = file
- self.opt_subrectangle = opt_subrectangle
- self.opt_loop = opt_loop
- self.opt_quantizer = opt_quantizer
- self.opt_palette_size = opt_palette_size
- self._previous_image = None # as np array
- self._global_palette = None # as bytes
- self._count = 0
- from PIL.GifImagePlugin import getdata
- self.getdata = getdata
- def add_image(self, im, duration, dispose):
- # Prepare image
- im_rect, rect = im, (0, 0)
- if self.opt_subrectangle:
- im_rect, rect = self.getSubRectangle(im)
- im_pil = self.converToPIL(im_rect, self.opt_quantizer, self.opt_palette_size)
- # Get pallette - apparently, this is the 3d element of the header
- # (but it has not always been). Best we've got. Its not the same
- # as im_pil.palette.tobytes().
- from PIL.GifImagePlugin import getheader
- palette = getheader(im_pil)[0][3]
- # Write image
- if self._count == 0:
- self.write_header(im_pil, palette, self.opt_loop)
- self._global_palette = palette
- self.write_image(im_pil, palette, rect, duration, dispose)
- # assert len(palette) == len(self._global_palette)
- # Bookkeeping
- self._previous_image = im
- self._count += 1
- def write_header(self, im, globalPalette, loop):
- # Gather info
- header = self.getheaderAnim(im)
- appext = self.getAppExt(loop)
- # Write
- self.fp.write(header)
- self.fp.write(globalPalette)
- self.fp.write(appext)
- def close(self):
- self.fp.write(";".encode("utf-8")) # end gif
- def write_image(self, im, palette, rect, duration, dispose):
- fp = self.fp
- # Gather local image header and data, using PIL's getdata. That
- # function returns a list of bytes objects, but which parts are
- # what has changed multiple times, so we put together the first
- # parts until we have enough to form the image header.
- data = self.getdata(im)
- imdes = b""
- while data and len(imdes) < 11:
- imdes += data.pop(0)
- assert len(imdes) == 11
- # Make image descriptor suitable for using 256 local color palette
- lid = self.getImageDescriptor(im, rect)
- graphext = self.getGraphicsControlExt(duration, dispose)
- # Write local header
- if (palette != self._global_palette) or (dispose != 2):
- # Use local color palette
- fp.write(graphext)
- fp.write(lid) # write suitable image descriptor
- fp.write(palette) # write local color table
- fp.write(b"\x08") # LZW minimum size code
- else:
- # Use global color palette
- fp.write(graphext)
- fp.write(imdes) # write suitable image descriptor
- # Write image data
- for d in data:
- fp.write(d)
- def getheaderAnim(self, im):
- """Get animation header. To replace PILs getheader()[0]"""
- bb = b"GIF89a"
- bb += intToBin(im.size[0])
- bb += intToBin(im.size[1])
- bb += b"\x87\x00\x00"
- return bb
- def getImageDescriptor(self, im, xy=None):
- """Used for the local color table properties per image.
- Otherwise global color table applies to all frames irrespective of
- whether additional colors comes in play that require a redefined
- palette. Still a maximum of 256 color per frame, obviously.
- Written by Ant1 on 2010-08-22
- Modified by Alex Robinson in Janurari 2011 to implement subrectangles.
- """
- # Defaule use full image and place at upper left
- if xy is None:
- xy = (0, 0)
- # Image separator,
- bb = b"\x2c"
- # Image position and size
- bb += intToBin(xy[0]) # Left position
- bb += intToBin(xy[1]) # Top position
- bb += intToBin(im.size[0]) # image width
- bb += intToBin(im.size[1]) # image height
- # packed field: local color table flag1, interlace0, sorted table0,
- # reserved00, lct size111=7=2^(7 + 1)=256.
- bb += b"\x87"
- # LZW minimum size code now comes later, begining of [imagedata] blocks
- return bb
- def getAppExt(self, loop):
- """Application extension. This part specifies the amount of loops.
- If loop is 0 or inf, it goes on infinitely.
- """
- if loop == 1:
- return b""
- if loop == 0:
- loop = 2**16 - 1
- bb = b""
- if loop != 0: # omit the extension if we would like a nonlooping gif
- bb = b"\x21\xff\x0b" # application extension
- bb += b"NETSCAPE2.0"
- bb += b"\x03\x01"
- bb += intToBin(loop)
- bb += b"\x00" # end
- return bb
- def getGraphicsControlExt(self, duration=0.1, dispose=2):
- """Graphics Control Extension. A sort of header at the start of
- each image. Specifies duration and transparancy.
- Dispose
- -------
- * 0 - No disposal specified.
- * 1 - Do not dispose. The graphic is to be left in place.
- * 2 - Restore to background color. The area used by the graphic
- must be restored to the background color.
- * 3 - Restore to previous. The decoder is required to restore the
- area overwritten by the graphic with what was there prior to
- rendering the graphic.
- * 4-7 -To be defined.
- """
- bb = b"\x21\xf9\x04"
- bb += chr((dispose & 3) << 2).encode("utf-8")
- # low bit 1 == transparency,
- # 2nd bit 1 == user input , next 3 bits, the low two of which are used,
- # are dispose.
- bb += intToBin(int(duration * 100 + 0.5)) # in 100th of seconds
- bb += b"\x00" # no transparant color
- bb += b"\x00" # end
- return bb
- def getSubRectangle(self, im):
- """Calculate the minimal rectangle that need updating. Returns
- a two-element tuple containing the cropped image and an x-y tuple.
- Calculating the subrectangles takes extra time, obviously. However,
- if the image sizes were reduced, the actual writing of the GIF
- goes faster. In some cases applying this method produces a GIF faster.
- """
- # Cannot do subrectangle for first image
- if self._count == 0:
- return im, (0, 0)
- prev = self._previous_image
- # Get difference, sum over colors
- diff = np.abs(im - prev)
- if diff.ndim == 3:
- diff = diff.sum(2)
- # Get begin and end for both dimensions
- X = np.argwhere(diff.sum(0))
- Y = np.argwhere(diff.sum(1))
- # Get rect coordinates
- if X.size and Y.size:
- x0, x1 = int(X[0]), int(X[-1] + 1)
- y0, y1 = int(Y[0]), int(Y[-1] + 1)
- else: # No change ... make it minimal
- x0, x1 = 0, 2
- y0, y1 = 0, 2
- return im[y0:y1, x0:x1], (x0, y0)
- def converToPIL(self, im, quantizer, palette_size=256):
- """Convert image to Paletted PIL image.
- PIL used to not do a very good job at quantization, but I guess
- this has improved a lot (at least in Pillow). I don't think we need
- neuqant (and we can add it later if we really want).
- """
- im_pil = ndarray_to_pil(im, "gif")
- if quantizer in ("nq", "neuquant"):
- # NeuQuant algorithm
- nq_samplefac = 10 # 10 seems good in general
- im_pil = im_pil.convert("RGBA") # NQ assumes RGBA
- nqInstance = NeuQuant(im_pil, nq_samplefac) # Learn colors
- im_pil = nqInstance.quantize(im_pil, colors=palette_size)
- elif quantizer in (0, 1, 2):
- # Adaptive PIL algorithm
- if quantizer == 2:
- im_pil = im_pil.convert("RGBA")
- else:
- im_pil = im_pil.convert("RGB")
- im_pil = im_pil.quantize(colors=palette_size, method=quantizer)
- else:
- raise ValueError("Invalid value for quantizer: %r" % quantizer)
- return im_pil
|