1"""
2PIL formats for multiple images.
3"""
4
5import logging
6
7import numpy as np
8
9from .pillow_legacy import PillowFormat, image_as_uint, ndarray_to_pil
10
11logger = logging.getLogger(__name__)
12
13NeuQuant = None # we can implement this when we need it
14
15
16class TIFFFormat(PillowFormat):
17 _modes = "i" # arg, why bother; people should use the tiffile version
18 _description = "TIFF format (Pillow)"
19
20
21class GIFFormat(PillowFormat):
22 """See :mod:`imageio.plugins.pillow_legacy`"""
23
24 _modes = "iI"
25 _description = "Static and animated gif (Pillow)"
26
27 # GIF reader needs no modifications compared to base pillow reader
28
29 class Writer(PillowFormat.Writer): # pragma: no cover
30 def _open(
31 self,
32 loop=0,
33 duration=None,
34 fps=10,
35 palettesize=256,
36 quantizer=0,
37 subrectangles=False,
38 ):
39 from PIL import __version__ as pillow_version
40
41 major, minor, patch = tuple(int(x) for x in pillow_version.split("."))
42 if major == 10 and minor >= 1:
43 raise ImportError(
44 f"Pillow v{pillow_version} is not supported by ImageIO's legacy "
45 "pillow plugin when writing GIFs. Consider switching to the new "
46 "plugin or downgrading to `pillow<10.1.0`."
47 )
48
49 # Check palettesize
50 palettesize = int(palettesize)
51 if palettesize < 2 or palettesize > 256:
52 raise ValueError("GIF quantize param must be 2..256")
53 if palettesize not in [2, 4, 8, 16, 32, 64, 128, 256]:
54 palettesize = 2 ** int(np.log2(128) + 0.999)
55 logger.warning(
56 "Warning: palettesize (%r) modified to a factor of "
57 "two between 2-256." % palettesize
58 )
59 # Duratrion / fps
60 if duration is None:
61 self._duration = 1.0 / float(fps)
62 elif isinstance(duration, (list, tuple)):
63 self._duration = [float(d) for d in duration]
64 else:
65 self._duration = float(duration)
66 # loop
67 loop = float(loop)
68 if loop <= 0 or loop == float("inf"):
69 loop = 0
70 loop = int(loop)
71 # Subrectangles / dispose
72 subrectangles = bool(subrectangles)
73 self._dispose = 1 if subrectangles else 2
74 # The "0" (median cut) quantizer is by far the best
75
76 fp = self.request.get_file()
77 self._writer = GifWriter(
78 fp, subrectangles, loop, quantizer, int(palettesize)
79 )
80
81 def _close(self):
82 self._writer.close()
83
84 def _append_data(self, im, meta):
85 im = image_as_uint(im, bitdepth=8)
86 if im.ndim == 3 and im.shape[-1] == 1:
87 im = im[:, :, 0]
88 duration = self._duration
89 if isinstance(duration, list):
90 duration = duration[min(len(duration) - 1, self._writer._count)]
91 dispose = self._dispose
92 self._writer.add_image(im, duration, dispose)
93
94 return
95
96
97def intToBin(i):
98 return i.to_bytes(2, byteorder="little")
99
100
101class GifWriter: # pragma: no cover
102 """Class that for helping write the animated GIF file. This is based on
103 code from images2gif.py (part of visvis). The version here is modified
104 to allow streamed writing.
105 """
106
107 def __init__(
108 self,
109 file,
110 opt_subrectangle=True,
111 opt_loop=0,
112 opt_quantizer=0,
113 opt_palette_size=256,
114 ):
115 self.fp = file
116
117 self.opt_subrectangle = opt_subrectangle
118 self.opt_loop = opt_loop
119 self.opt_quantizer = opt_quantizer
120 self.opt_palette_size = opt_palette_size
121
122 self._previous_image = None # as np array
123 self._global_palette = None # as bytes
124 self._count = 0
125
126 from PIL.GifImagePlugin import getdata
127
128 self.getdata = getdata
129
130 def add_image(self, im, duration, dispose):
131 # Prepare image
132 im_rect, rect = im, (0, 0)
133 if self.opt_subrectangle:
134 im_rect, rect = self.getSubRectangle(im)
135 im_pil = self.converToPIL(im_rect, self.opt_quantizer, self.opt_palette_size)
136
137 # Get pallette - apparently, this is the 3d element of the header
138 # (but it has not always been). Best we've got. Its not the same
139 # as im_pil.palette.tobytes().
140 from PIL.GifImagePlugin import getheader
141
142 palette = getheader(im_pil)[0][3]
143
144 # Write image
145 if self._count == 0:
146 self.write_header(im_pil, palette, self.opt_loop)
147 self._global_palette = palette
148 self.write_image(im_pil, palette, rect, duration, dispose)
149 # assert len(palette) == len(self._global_palette)
150
151 # Bookkeeping
152 self._previous_image = im
153 self._count += 1
154
155 def write_header(self, im, globalPalette, loop):
156 # Gather info
157 header = self.getheaderAnim(im)
158 appext = self.getAppExt(loop)
159 # Write
160 self.fp.write(header)
161 self.fp.write(globalPalette)
162 self.fp.write(appext)
163
164 def close(self):
165 self.fp.write(";".encode("utf-8")) # end gif
166
167 def write_image(self, im, palette, rect, duration, dispose):
168 fp = self.fp
169
170 # Gather local image header and data, using PIL's getdata. That
171 # function returns a list of bytes objects, but which parts are
172 # what has changed multiple times, so we put together the first
173 # parts until we have enough to form the image header.
174 data = self.getdata(im)
175 imdes = b""
176 while data and len(imdes) < 11:
177 imdes += data.pop(0)
178 assert len(imdes) == 11
179
180 # Make image descriptor suitable for using 256 local color palette
181 lid = self.getImageDescriptor(im, rect)
182 graphext = self.getGraphicsControlExt(duration, dispose)
183
184 # Write local header
185 if (palette != self._global_palette) or (dispose != 2):
186 # Use local color palette
187 fp.write(graphext)
188 fp.write(lid) # write suitable image descriptor
189 fp.write(palette) # write local color table
190 fp.write(b"\x08") # LZW minimum size code
191 else:
192 # Use global color palette
193 fp.write(graphext)
194 fp.write(imdes) # write suitable image descriptor
195
196 # Write image data
197 for d in data:
198 fp.write(d)
199
200 def getheaderAnim(self, im):
201 """Get animation header. To replace PILs getheader()[0]"""
202 bb = b"GIF89a"
203 bb += intToBin(im.size[0])
204 bb += intToBin(im.size[1])
205 bb += b"\x87\x00\x00"
206 return bb
207
208 def getImageDescriptor(self, im, xy=None):
209 """Used for the local color table properties per image.
210 Otherwise global color table applies to all frames irrespective of
211 whether additional colors comes in play that require a redefined
212 palette. Still a maximum of 256 color per frame, obviously.
213
214 Written by Ant1 on 2010-08-22
215 Modified by Alex Robinson in Janurari 2011 to implement subrectangles.
216 """
217
218 # Defaule use full image and place at upper left
219 if xy is None:
220 xy = (0, 0)
221
222 # Image separator,
223 bb = b"\x2C"
224
225 # Image position and size
226 bb += intToBin(xy[0]) # Left position
227 bb += intToBin(xy[1]) # Top position
228 bb += intToBin(im.size[0]) # image width
229 bb += intToBin(im.size[1]) # image height
230
231 # packed field: local color table flag1, interlace0, sorted table0,
232 # reserved00, lct size111=7=2^(7 + 1)=256.
233 bb += b"\x87"
234
235 # LZW minimum size code now comes later, begining of [imagedata] blocks
236 return bb
237
238 def getAppExt(self, loop):
239 """Application extension. This part specifies the amount of loops.
240 If loop is 0 or inf, it goes on infinitely.
241 """
242 if loop == 1:
243 return b""
244 if loop == 0:
245 loop = 2**16 - 1
246 bb = b""
247 if loop != 0: # omit the extension if we would like a nonlooping gif
248 bb = b"\x21\xFF\x0B" # application extension
249 bb += b"NETSCAPE2.0"
250 bb += b"\x03\x01"
251 bb += intToBin(loop)
252 bb += b"\x00" # end
253 return bb
254
255 def getGraphicsControlExt(self, duration=0.1, dispose=2):
256 """Graphics Control Extension. A sort of header at the start of
257 each image. Specifies duration and transparancy.
258
259 Dispose
260 -------
261 * 0 - No disposal specified.
262 * 1 - Do not dispose. The graphic is to be left in place.
263 * 2 - Restore to background color. The area used by the graphic
264 must be restored to the background color.
265 * 3 - Restore to previous. The decoder is required to restore the
266 area overwritten by the graphic with what was there prior to
267 rendering the graphic.
268 * 4-7 -To be defined.
269 """
270
271 bb = b"\x21\xF9\x04"
272 bb += chr((dispose & 3) << 2).encode("utf-8")
273 # low bit 1 == transparency,
274 # 2nd bit 1 == user input , next 3 bits, the low two of which are used,
275 # are dispose.
276 bb += intToBin(int(duration * 100 + 0.5)) # in 100th of seconds
277 bb += b"\x00" # no transparant color
278 bb += b"\x00" # end
279 return bb
280
281 def getSubRectangle(self, im):
282 """Calculate the minimal rectangle that need updating. Returns
283 a two-element tuple containing the cropped image and an x-y tuple.
284
285 Calculating the subrectangles takes extra time, obviously. However,
286 if the image sizes were reduced, the actual writing of the GIF
287 goes faster. In some cases applying this method produces a GIF faster.
288 """
289
290 # Cannot do subrectangle for first image
291 if self._count == 0:
292 return im, (0, 0)
293
294 prev = self._previous_image
295
296 # Get difference, sum over colors
297 diff = np.abs(im - prev)
298 if diff.ndim == 3:
299 diff = diff.sum(2)
300 # Get begin and end for both dimensions
301 X = np.argwhere(diff.sum(0))
302 Y = np.argwhere(diff.sum(1))
303 # Get rect coordinates
304 if X.size and Y.size:
305 x0, x1 = int(X[0]), int(X[-1] + 1)
306 y0, y1 = int(Y[0]), int(Y[-1] + 1)
307 else: # No change ... make it minimal
308 x0, x1 = 0, 2
309 y0, y1 = 0, 2
310
311 return im[y0:y1, x0:x1], (x0, y0)
312
313 def converToPIL(self, im, quantizer, palette_size=256):
314 """Convert image to Paletted PIL image.
315
316 PIL used to not do a very good job at quantization, but I guess
317 this has improved a lot (at least in Pillow). I don't think we need
318 neuqant (and we can add it later if we really want).
319 """
320
321 im_pil = ndarray_to_pil(im, "gif")
322
323 if quantizer in ("nq", "neuquant"):
324 # NeuQuant algorithm
325 nq_samplefac = 10 # 10 seems good in general
326 im_pil = im_pil.convert("RGBA") # NQ assumes RGBA
327 nqInstance = NeuQuant(im_pil, nq_samplefac) # Learn colors
328 im_pil = nqInstance.quantize(im_pil, colors=palette_size)
329 elif quantizer in (0, 1, 2):
330 # Adaptive PIL algorithm
331 if quantizer == 2:
332 im_pil = im_pil.convert("RGBA")
333 else:
334 im_pil = im_pil.convert("RGB")
335 im_pil = im_pil.quantize(colors=palette_size, method=quantizer)
336 else:
337 raise ValueError("Invalid value for quantizer: %r" % quantizer)
338 return im_pil