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

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

139 statements  

1# -*- coding: utf-8 -*- 

2# imageio is distributed under the terms of the (new) BSD License. 

3 

4"""Plugin for multi-image freeimafe formats, like animated GIF and ico. 

5""" 

6 

7import logging 

8import numpy as np 

9 

10from ..core import Format, image_as_uint 

11from ._freeimage import fi, IO_FLAGS 

12from .freeimage import FreeimageFormat 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17class FreeimageMulti(FreeimageFormat): 

18 """Base class for freeimage formats that support multiple images.""" 

19 

20 _modes = "iI" 

21 _fif = -1 

22 

23 class Reader(Format.Reader): 

24 def _open(self, flags=0): 

25 flags = int(flags) 

26 # Create bitmap 

27 self._bm = fi.create_multipage_bitmap( 

28 self.request.filename, self.format.fif, flags 

29 ) 

30 self._bm.load_from_filename(self.request.get_local_filename()) 

31 

32 def _close(self): 

33 self._bm.close() 

34 

35 def _get_length(self): 

36 return len(self._bm) 

37 

38 def _get_data(self, index): 

39 sub = self._bm.get_page(index) 

40 try: 

41 return sub.get_image_data(), sub.get_meta_data() 

42 finally: 

43 sub.close() 

44 

45 def _get_meta_data(self, index): 

46 index = index or 0 

47 if index < 0 or index >= len(self._bm): 

48 raise IndexError() 

49 sub = self._bm.get_page(index) 

50 try: 

51 return sub.get_meta_data() 

52 finally: 

53 sub.close() 

54 

55 # -- 

56 

57 class Writer(FreeimageFormat.Writer): 

58 def _open(self, flags=0): 

59 # Set flags 

60 self._flags = flags = int(flags) 

61 # Instantiate multi-page bitmap 

62 self._bm = fi.create_multipage_bitmap( 

63 self.request.filename, self.format.fif, flags 

64 ) 

65 self._bm.save_to_filename(self.request.get_local_filename()) 

66 

67 def _close(self): 

68 # Close bitmap 

69 self._bm.close() 

70 

71 def _append_data(self, im, meta): 

72 # Prepare data 

73 if im.ndim == 3 and im.shape[-1] == 1: 

74 im = im[:, :, 0] 

75 im = image_as_uint(im, bitdepth=8) 

76 # Create sub bitmap 

77 sub1 = fi.create_bitmap(self._bm._filename, self.format.fif) 

78 # Let subclass add data to bitmap, optionally return new 

79 sub2 = self._append_bitmap(im, meta, sub1) 

80 # Add 

81 self._bm.append_bitmap(sub2) 

82 sub2.close() 

83 if sub1 is not sub2: 

84 sub1.close() 

85 

86 def _append_bitmap(self, im, meta, bitmap): 

87 # Set data 

88 bitmap.allocate(im) 

89 bitmap.set_image_data(im) 

90 bitmap.set_meta_data(meta) 

91 # Return that same bitmap 

92 return bitmap 

93 

94 def _set_meta_data(self, meta): 

95 pass # ignore global meta data 

96 

97 

98class MngFormat(FreeimageMulti): 

99 """An Mng format based on the Freeimage library. 

100 

101 Read only. Seems broken. 

102 """ 

103 

104 _fif = 6 

105 

106 def _can_write(self, request): # pragma: no cover 

107 return False 

108 

109 

110class IcoFormat(FreeimageMulti): 

111 """An ICO format based on the Freeimage library. 

112 

113 This format supports grayscale, RGB and RGBA images. 

114 

115 The freeimage plugin requires a `freeimage` binary. If this binary 

116 is not available on the system, it can be downloaded by either 

117 

118 - the command line script ``imageio_download_bin freeimage`` 

119 - the Python method ``imageio.plugins.freeimage.download()`` 

120 

121 Parameters for reading 

122 ---------------------- 

123 makealpha : bool 

124 Convert to 32-bit and create an alpha channel from the AND- 

125 mask when loading. Default False. Note that this returns wrong 

126 results if the image was already RGBA. 

127 

128 """ 

129 

130 _fif = 1 

131 

132 class Reader(FreeimageMulti.Reader): 

133 def _open(self, flags=0, makealpha=False): 

134 # Build flags from kwargs 

135 flags = int(flags) 

136 if makealpha: 

137 flags |= IO_FLAGS.ICO_MAKEALPHA 

138 return FreeimageMulti.Reader._open(self, flags) 

139 

140 

141class GifFormat(FreeimageMulti): 

142 """A format for reading and writing static and animated GIF, based 

143 on the Freeimage library. 

144 

145 Images read with this format are always RGBA. Currently, 

146 the alpha channel is ignored when saving RGB images with this 

147 format. 

148 

149 The freeimage plugin requires a `freeimage` binary. If this binary 

150 is not available on the system, it can be downloaded by either 

151 

152 - the command line script ``imageio_download_bin freeimage`` 

153 - the Python method ``imageio.plugins.freeimage.download()`` 

154 

155 Parameters for reading 

156 ---------------------- 

157 playback : bool 

158 'Play' the GIF to generate each frame (as 32bpp) instead of 

159 returning raw frame data when loading. Default True. 

160 

161 Parameters for saving 

162 --------------------- 

163 loop : int 

164 The number of iterations. Default 0 (meaning loop indefinitely) 

165 duration : {float, list} 

166 The duration (in seconds) of each frame. Either specify one value 

167 that is used for all frames, or one value for each frame. 

168 Note that in the GIF format the duration/delay is expressed in 

169 hundredths of a second, which limits the precision of the duration. 

170 fps : float 

171 The number of frames per second. If duration is not given, the 

172 duration for each frame is set to 1/fps. Default 10. 

173 palettesize : int 

174 The number of colors to quantize the image to. Is rounded to 

175 the nearest power of two. Default 256. 

176 quantizer : {'wu', 'nq'} 

177 The quantization algorithm: 

178 * wu - Wu, Xiaolin, Efficient Statistical Computations for 

179 Optimal Color Quantization 

180 * nq (neuqant) - Dekker A. H., Kohonen neural networks for 

181 optimal color quantization 

182 subrectangles : bool 

183 If True, will try and optimize the GIF by storing only the 

184 rectangular parts of each frame that change with respect to the 

185 previous. Unfortunately, this option seems currently broken 

186 because FreeImage does not handle DisposalMethod correctly. 

187 Default False. 

188 """ 

189 

190 _fif = 25 

191 

192 class Reader(FreeimageMulti.Reader): 

193 def _open(self, flags=0, playback=True): 

194 # Build flags from kwargs 

195 flags = int(flags) 

196 if playback: 

197 flags |= IO_FLAGS.GIF_PLAYBACK 

198 FreeimageMulti.Reader._open(self, flags) 

199 

200 def _get_data(self, index): 

201 im, meta = FreeimageMulti.Reader._get_data(self, index) 

202 # im = im[:, :, :3] # Drop alpha channel 

203 return im, meta 

204 

205 # -- writer 

206 

207 class Writer(FreeimageMulti.Writer): 

208 # todo: subrectangles 

209 # todo: global palette 

210 

211 def _open( 

212 self, 

213 flags=0, 

214 loop=0, 

215 duration=None, 

216 fps=10, 

217 palettesize=256, 

218 quantizer="Wu", 

219 subrectangles=False, 

220 ): 

221 # Check palettesize 

222 if palettesize < 2 or palettesize > 256: 

223 raise ValueError("GIF quantize param must be 2..256") 

224 if palettesize not in [2, 4, 8, 16, 32, 64, 128, 256]: 

225 palettesize = 2 ** int(np.log2(128) + 0.999) 

226 logger.warning( 

227 "Warning: palettesize (%r) modified to a factor of " 

228 "two between 2-256." % palettesize 

229 ) 

230 self._palettesize = palettesize 

231 # Check quantizer 

232 self._quantizer = {"wu": 0, "nq": 1}.get(quantizer.lower(), None) 

233 if self._quantizer is None: 

234 raise ValueError('Invalid quantizer, must be "wu" or "nq".') 

235 # Check frametime 

236 if duration is None: 

237 self._frametime = [int(1000 / float(fps) + 0.5)] 

238 elif isinstance(duration, list): 

239 self._frametime = [int(1000 * d) for d in duration] 

240 elif isinstance(duration, (float, int)): 

241 self._frametime = [int(1000 * duration)] 

242 else: 

243 raise ValueError("Invalid value for duration: %r" % duration) 

244 # Check subrectangles 

245 self._subrectangles = bool(subrectangles) 

246 self._prev_im = None 

247 # Init 

248 FreeimageMulti.Writer._open(self, flags) 

249 # Set global meta data 

250 self._meta = {} 

251 self._meta["ANIMATION"] = { 

252 # 'GlobalPalette': np.array([0]).astype(np.uint8), 

253 "Loop": np.array([loop]).astype(np.uint32), 

254 # 'LogicalWidth': np.array([x]).astype(np.uint16), 

255 # 'LogicalHeight': np.array([x]).astype(np.uint16), 

256 } 

257 

258 def _append_bitmap(self, im, meta, bitmap): 

259 # Prepare meta data 

260 meta = meta.copy() 

261 meta_a = meta["ANIMATION"] = {} 

262 # If this is the first frame, assign it our "global" meta data 

263 if len(self._bm) == 0: 

264 meta.update(self._meta) 

265 meta_a = meta["ANIMATION"] 

266 # Set frame time 

267 index = len(self._bm) 

268 if index < len(self._frametime): 

269 ft = self._frametime[index] 

270 else: 

271 ft = self._frametime[-1] 

272 meta_a["FrameTime"] = np.array([ft]).astype(np.uint32) 

273 # Check array 

274 if im.ndim == 3 and im.shape[-1] == 4: 

275 im = im[:, :, :3] 

276 # Process subrectangles 

277 im_uncropped = im 

278 if self._subrectangles and self._prev_im is not None: 

279 im, xy = self._get_sub_rectangles(self._prev_im, im) 

280 meta_a["DisposalMethod"] = np.array([1]).astype(np.uint8) 

281 meta_a["FrameLeft"] = np.array([xy[0]]).astype(np.uint16) 

282 meta_a["FrameTop"] = np.array([xy[1]]).astype(np.uint16) 

283 self._prev_im = im_uncropped 

284 # Set image data 

285 sub2 = sub1 = bitmap 

286 sub1.allocate(im) 

287 sub1.set_image_data(im) 

288 # Quantize it if its RGB 

289 if im.ndim == 3 and im.shape[-1] == 3: 

290 sub2 = sub1.quantize(self._quantizer, self._palettesize) 

291 # Set meta data and return 

292 sub2.set_meta_data(meta) 

293 return sub2 

294 

295 def _get_sub_rectangles(self, prev, im): 

296 """ 

297 Calculate the minimal rectangles that need updating each frame. 

298 Returns a two-element tuple containing the cropped images and a 

299 list of x-y positions. 

300 """ 

301 # Get difference, sum over colors 

302 diff = np.abs(im - prev) 

303 if diff.ndim == 3: 

304 diff = diff.sum(2) 

305 # Get begin and end for both dimensions 

306 X = np.argwhere(diff.sum(0)) 

307 Y = np.argwhere(diff.sum(1)) 

308 # Get rect coordinates 

309 if X.size and Y.size: 

310 x0, x1 = int(X[0]), int(X[-1]) + 1 

311 y0, y1 = int(Y[0]), int(Y[-1]) + 1 

312 else: # No change ... make it minimal 

313 x0, x1 = 0, 2 

314 y0, y1 = 0, 2 

315 # Cut out and return 

316 return im[y0:y1, x0:x1], (x0, y0)