Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/imageio-2.35.1-py3.8.egg/imageio/plugins/pillowmulti.py: 92%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

13 statements  

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