Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/WebPImagePlugin.py: 12%

204 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from io import BytesIO 

2 

3from . import Image, ImageFile 

4 

5try: 

6 from . import _webp 

7 

8 SUPPORTED = True 

9except ImportError: 

10 SUPPORTED = False 

11 

12 

13_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True} 

14 

15_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True} 

16 

17_VP8_MODES_BY_IDENTIFIER = { 

18 b"VP8 ": "RGB", 

19 b"VP8X": "RGBA", 

20 b"VP8L": "RGBA", # lossless 

21} 

22 

23 

24def _accept(prefix): 

25 is_riff_file_format = prefix[:4] == b"RIFF" 

26 is_webp_file = prefix[8:12] == b"WEBP" 

27 is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER 

28 

29 if is_riff_file_format and is_webp_file and is_valid_vp8_mode: 

30 if not SUPPORTED: 

31 return ( 

32 "image file could not be identified because WEBP support not installed" 

33 ) 

34 return True 

35 

36 

37class WebPImageFile(ImageFile.ImageFile): 

38 

39 format = "WEBP" 

40 format_description = "WebP image" 

41 __loaded = 0 

42 __logical_frame = 0 

43 

44 def _open(self): 

45 if not _webp.HAVE_WEBPANIM: 

46 # Legacy mode 

47 data, width, height, self.mode, icc_profile, exif = _webp.WebPDecode( 

48 self.fp.read() 

49 ) 

50 if icc_profile: 

51 self.info["icc_profile"] = icc_profile 

52 if exif: 

53 self.info["exif"] = exif 

54 self._size = width, height 

55 self.fp = BytesIO(data) 

56 self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] 

57 self.n_frames = 1 

58 self.is_animated = False 

59 return 

60 

61 # Use the newer AnimDecoder API to parse the (possibly) animated file, 

62 # and access muxed chunks like ICC/EXIF/XMP. 

63 self._decoder = _webp.WebPAnimDecoder(self.fp.read()) 

64 

65 # Get info from decoder 

66 width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() 

67 self._size = width, height 

68 self.info["loop"] = loop_count 

69 bg_a, bg_r, bg_g, bg_b = ( 

70 (bgcolor >> 24) & 0xFF, 

71 (bgcolor >> 16) & 0xFF, 

72 (bgcolor >> 8) & 0xFF, 

73 bgcolor & 0xFF, 

74 ) 

75 self.info["background"] = (bg_r, bg_g, bg_b, bg_a) 

76 self.n_frames = frame_count 

77 self.is_animated = self.n_frames > 1 

78 self.mode = "RGB" if mode == "RGBX" else mode 

79 self.rawmode = mode 

80 self.tile = [] 

81 

82 # Attempt to read ICC / EXIF / XMP chunks from file 

83 icc_profile = self._decoder.get_chunk("ICCP") 

84 exif = self._decoder.get_chunk("EXIF") 

85 xmp = self._decoder.get_chunk("XMP ") 

86 if icc_profile: 

87 self.info["icc_profile"] = icc_profile 

88 if exif: 

89 self.info["exif"] = exif 

90 if xmp: 

91 self.info["xmp"] = xmp 

92 

93 # Initialize seek state 

94 self._reset(reset=False) 

95 

96 def _getexif(self): 

97 if "exif" not in self.info: 

98 return None 

99 return self.getexif()._get_merged_dict() 

100 

101 def seek(self, frame): 

102 if not self._seek_check(frame): 

103 return 

104 

105 # Set logical frame to requested position 

106 self.__logical_frame = frame 

107 

108 def _reset(self, reset=True): 

109 if reset: 

110 self._decoder.reset() 

111 self.__physical_frame = 0 

112 self.__loaded = -1 

113 self.__timestamp = 0 

114 

115 def _get_next(self): 

116 # Get next frame 

117 ret = self._decoder.get_next() 

118 self.__physical_frame += 1 

119 

120 # Check if an error occurred 

121 if ret is None: 

122 self._reset() # Reset just to be safe 

123 self.seek(0) 

124 raise EOFError("failed to decode next frame in WebP file") 

125 

126 # Compute duration 

127 data, timestamp = ret 

128 duration = timestamp - self.__timestamp 

129 self.__timestamp = timestamp 

130 

131 # libwebp gives frame end, adjust to start of frame 

132 timestamp -= duration 

133 return data, timestamp, duration 

134 

135 def _seek(self, frame): 

136 if self.__physical_frame == frame: 

137 return # Nothing to do 

138 if frame < self.__physical_frame: 

139 self._reset() # Rewind to beginning 

140 while self.__physical_frame < frame: 

141 self._get_next() # Advance to the requested frame 

142 

143 def load(self): 

144 if _webp.HAVE_WEBPANIM: 

145 if self.__loaded != self.__logical_frame: 

146 self._seek(self.__logical_frame) 

147 

148 # We need to load the image data for this frame 

149 data, timestamp, duration = self._get_next() 

150 self.info["timestamp"] = timestamp 

151 self.info["duration"] = duration 

152 self.__loaded = self.__logical_frame 

153 

154 # Set tile 

155 if self.fp and self._exclusive_fp: 

156 self.fp.close() 

157 self.fp = BytesIO(data) 

158 self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] 

159 

160 return super().load() 

161 

162 def tell(self): 

163 if not _webp.HAVE_WEBPANIM: 

164 return super().tell() 

165 

166 return self.__logical_frame 

167 

168 

169def _save_all(im, fp, filename): 

170 encoderinfo = im.encoderinfo.copy() 

171 append_images = list(encoderinfo.get("append_images", [])) 

172 

173 # If total frame count is 1, then save using the legacy API, which 

174 # will preserve non-alpha modes 

175 total = 0 

176 for ims in [im] + append_images: 

177 total += getattr(ims, "n_frames", 1) 

178 if total == 1: 

179 _save(im, fp, filename) 

180 return 

181 

182 background = (0, 0, 0, 0) 

183 if "background" in encoderinfo: 

184 background = encoderinfo["background"] 

185 elif "background" in im.info: 

186 background = im.info["background"] 

187 if isinstance(background, int): 

188 # GifImagePlugin stores a global color table index in 

189 # info["background"]. So it must be converted to an RGBA value 

190 palette = im.getpalette() 

191 if palette: 

192 r, g, b = palette[background * 3 : (background + 1) * 3] 

193 background = (r, g, b, 255) 

194 else: 

195 background = (background, background, background, 255) 

196 

197 duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) 

198 loop = im.encoderinfo.get("loop", 0) 

199 minimize_size = im.encoderinfo.get("minimize_size", False) 

200 kmin = im.encoderinfo.get("kmin", None) 

201 kmax = im.encoderinfo.get("kmax", None) 

202 allow_mixed = im.encoderinfo.get("allow_mixed", False) 

203 verbose = False 

204 lossless = im.encoderinfo.get("lossless", False) 

205 quality = im.encoderinfo.get("quality", 80) 

206 method = im.encoderinfo.get("method", 0) 

207 icc_profile = im.encoderinfo.get("icc_profile") or "" 

208 exif = im.encoderinfo.get("exif", "") 

209 if isinstance(exif, Image.Exif): 

210 exif = exif.tobytes() 

211 xmp = im.encoderinfo.get("xmp", "") 

212 if allow_mixed: 

213 lossless = False 

214 

215 # Sensible keyframe defaults are from gif2webp.c script 

216 if kmin is None: 

217 kmin = 9 if lossless else 3 

218 if kmax is None: 

219 kmax = 17 if lossless else 5 

220 

221 # Validate background color 

222 if ( 

223 not isinstance(background, (list, tuple)) 

224 or len(background) != 4 

225 or not all(0 <= v < 256 for v in background) 

226 ): 

227 raise OSError( 

228 f"Background color is not an RGBA tuple clamped to (0-255): {background}" 

229 ) 

230 

231 # Convert to packed uint 

232 bg_r, bg_g, bg_b, bg_a = background 

233 background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0) 

234 

235 # Setup the WebP animation encoder 

236 enc = _webp.WebPAnimEncoder( 

237 im.size[0], 

238 im.size[1], 

239 background, 

240 loop, 

241 minimize_size, 

242 kmin, 

243 kmax, 

244 allow_mixed, 

245 verbose, 

246 ) 

247 

248 # Add each frame 

249 frame_idx = 0 

250 timestamp = 0 

251 cur_idx = im.tell() 

252 try: 

253 for ims in [im] + append_images: 

254 # Get # of frames in this image 

255 nfr = getattr(ims, "n_frames", 1) 

256 

257 for idx in range(nfr): 

258 ims.seek(idx) 

259 ims.load() 

260 

261 # Make sure image mode is supported 

262 frame = ims 

263 rawmode = ims.mode 

264 if ims.mode not in _VALID_WEBP_MODES: 

265 alpha = ( 

266 "A" in ims.mode 

267 or "a" in ims.mode 

268 or (ims.mode == "P" and "A" in ims.im.getpalettemode()) 

269 ) 

270 rawmode = "RGBA" if alpha else "RGB" 

271 frame = ims.convert(rawmode) 

272 

273 if rawmode == "RGB": 

274 # For faster conversion, use RGBX 

275 rawmode = "RGBX" 

276 

277 # Append the frame to the animation encoder 

278 enc.add( 

279 frame.tobytes("raw", rawmode), 

280 timestamp, 

281 frame.size[0], 

282 frame.size[1], 

283 rawmode, 

284 lossless, 

285 quality, 

286 method, 

287 ) 

288 

289 # Update timestamp and frame index 

290 if isinstance(duration, (list, tuple)): 

291 timestamp += duration[frame_idx] 

292 else: 

293 timestamp += duration 

294 frame_idx += 1 

295 

296 finally: 

297 im.seek(cur_idx) 

298 

299 # Force encoder to flush frames 

300 enc.add(None, timestamp, 0, 0, "", lossless, quality, 0) 

301 

302 # Get the final output from the encoder 

303 data = enc.assemble(icc_profile, exif, xmp) 

304 if data is None: 

305 raise OSError("cannot write file as WebP (encoder returned None)") 

306 

307 fp.write(data) 

308 

309 

310def _save(im, fp, filename): 

311 lossless = im.encoderinfo.get("lossless", False) 

312 quality = im.encoderinfo.get("quality", 80) 

313 icc_profile = im.encoderinfo.get("icc_profile") or "" 

314 exif = im.encoderinfo.get("exif", "") 

315 if isinstance(exif, Image.Exif): 

316 exif = exif.tobytes() 

317 xmp = im.encoderinfo.get("xmp", "") 

318 method = im.encoderinfo.get("method", 4) 

319 

320 if im.mode not in _VALID_WEBP_LEGACY_MODES: 

321 alpha = ( 

322 "A" in im.mode 

323 or "a" in im.mode 

324 or (im.mode == "P" and "transparency" in im.info) 

325 ) 

326 im = im.convert("RGBA" if alpha else "RGB") 

327 

328 data = _webp.WebPEncode( 

329 im.tobytes(), 

330 im.size[0], 

331 im.size[1], 

332 lossless, 

333 float(quality), 

334 im.mode, 

335 icc_profile, 

336 method, 

337 exif, 

338 xmp, 

339 ) 

340 if data is None: 

341 raise OSError("cannot write file as WebP (encoder returned None)") 

342 

343 fp.write(data) 

344 

345 

346Image.register_open(WebPImageFile.format, WebPImageFile, _accept) 

347if SUPPORTED: 347 ↛ exitline 347 didn't exit the module, because the condition on line 347 was never false

348 Image.register_save(WebPImageFile.format, _save) 

349 if _webp.HAVE_WEBPANIM: 349 ↛ 351line 349 didn't jump to line 351, because the condition on line 349 was never false

350 Image.register_save_all(WebPImageFile.format, _save_all) 

351 Image.register_extension(WebPImageFile.format, ".webp") 

352 Image.register_mime(WebPImageFile.format, "image/webp")