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
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1from io import BytesIO
3from . import Image, ImageFile
5try:
6 from . import _webp
8 SUPPORTED = True
9except ImportError:
10 SUPPORTED = False
13_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True}
15_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True}
17_VP8_MODES_BY_IDENTIFIER = {
18 b"VP8 ": "RGB",
19 b"VP8X": "RGBA",
20 b"VP8L": "RGBA", # lossless
21}
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
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
37class WebPImageFile(ImageFile.ImageFile):
39 format = "WEBP"
40 format_description = "WebP image"
41 __loaded = 0
42 __logical_frame = 0
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
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())
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 = []
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
93 # Initialize seek state
94 self._reset(reset=False)
96 def _getexif(self):
97 if "exif" not in self.info:
98 return None
99 return self.getexif()._get_merged_dict()
101 def seek(self, frame):
102 if not self._seek_check(frame):
103 return
105 # Set logical frame to requested position
106 self.__logical_frame = frame
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
115 def _get_next(self):
116 # Get next frame
117 ret = self._decoder.get_next()
118 self.__physical_frame += 1
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")
126 # Compute duration
127 data, timestamp = ret
128 duration = timestamp - self.__timestamp
129 self.__timestamp = timestamp
131 # libwebp gives frame end, adjust to start of frame
132 timestamp -= duration
133 return data, timestamp, duration
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
143 def load(self):
144 if _webp.HAVE_WEBPANIM:
145 if self.__loaded != self.__logical_frame:
146 self._seek(self.__logical_frame)
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
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)]
160 return super().load()
162 def tell(self):
163 if not _webp.HAVE_WEBPANIM:
164 return super().tell()
166 return self.__logical_frame
169def _save_all(im, fp, filename):
170 encoderinfo = im.encoderinfo.copy()
171 append_images = list(encoderinfo.get("append_images", []))
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
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)
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
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
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 )
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)
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 )
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)
257 for idx in range(nfr):
258 ims.seek(idx)
259 ims.load()
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)
273 if rawmode == "RGB":
274 # For faster conversion, use RGBX
275 rawmode = "RGBX"
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 )
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
296 finally:
297 im.seek(cur_idx)
299 # Force encoder to flush frames
300 enc.add(None, timestamp, 0, 0, "", lossless, quality, 0)
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)")
307 fp.write(data)
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)
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")
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)")
343 fp.write(data)
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")