Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/AvifImagePlugin.py: 43%
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
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
1from __future__ import annotations
3__lazy_modules__ = {"io", "typing"}
5import os
6from typing import IO
8from . import ExifTags, Image, ImageFile, ImageSequence
10try:
11 from . import _avif
13 SUPPORTED = True
14except ImportError:
15 SUPPORTED = False
17# Decoder options as module globals, until there is a way to pass parameters
18# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
19DECODE_CODEC_CHOICE = "auto"
20DEFAULT_MAX_THREADS = 0
23def get_codec_version(codec_name: str) -> str | None:
24 versions = _avif.codec_versions()
25 for version in versions.split(", "):
26 if version.split(" [")[0] == codec_name:
27 return version.split(":")[-1].split(" ")[0]
28 return None
31def _accept(prefix: bytes) -> bool | str:
32 if prefix[4:8] != b"ftyp":
33 return False
34 major_brand = prefix[8:12]
35 if major_brand in (
36 # coding brands
37 b"avif",
38 b"avis",
39 # We accept files with AVIF container brands; we can't yet know if
40 # the ftyp box has the correct compatible brands, but if it doesn't
41 # then the plugin will raise a SyntaxError which Pillow will catch
42 # before moving on to the next plugin that accepts the file.
43 #
44 # Also, because this file might not actually be an AVIF file, we
45 # don't raise an error if AVIF support isn't properly compiled.
46 b"mif1",
47 b"msf1",
48 ):
49 if not SUPPORTED:
50 return (
51 "image file could not be identified because AVIF support not installed"
52 )
53 return True
54 return False
57def _get_default_max_threads() -> int:
58 if DEFAULT_MAX_THREADS:
59 return DEFAULT_MAX_THREADS
60 if hasattr(os, "sched_getaffinity"):
61 return len(os.sched_getaffinity(0))
62 else:
63 return os.cpu_count() or 1
66class AvifImageFile(ImageFile.ImageFile):
67 format = "AVIF"
68 format_description = "AVIF image"
69 __frame = -1
71 def _open(self) -> None:
72 if not SUPPORTED:
73 msg = "image file could not be opened because AVIF support not installed"
74 raise SyntaxError(msg)
76 if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available(
77 DECODE_CODEC_CHOICE
78 ):
79 msg = "Invalid opening codec"
80 raise ValueError(msg)
82 assert self.fp is not None
83 self._decoder = _avif.AvifDecoder(
84 self.fp.read(),
85 DECODE_CODEC_CHOICE,
86 _get_default_max_threads(),
87 )
89 # Get info from decoder
90 self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = (
91 self._decoder.get_info()
92 )
93 self.is_animated = self.n_frames > 1
95 if icc:
96 self.info["icc_profile"] = icc
97 if xmp:
98 self.info["xmp"] = xmp
100 if exif_orientation != 1 or exif:
101 exif_data = Image.Exif()
102 if exif:
103 exif_data.load(exif)
104 original_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
105 else:
106 original_orientation = 1
107 if exif_orientation != original_orientation:
108 exif_data[ExifTags.Base.Orientation] = exif_orientation
109 exif = exif_data.tobytes()
110 if exif:
111 self.info["exif"] = exif
112 self.seek(0)
114 def seek(self, frame: int) -> None:
115 if not self._seek_check(frame):
116 return
117 self.info.pop("timestamp", None)
118 self.info.pop("duration", None)
120 # Set tile
121 self.__frame = frame
122 self._im = None
124 def load(self) -> Image.core.PixelAccess | None:
125 if self._im is None:
126 # We need to load the image data for this frame
127 data, timescale, pts_in_timescales, duration_in_timescales = (
128 self._decoder.get_frame(self.__frame)
129 )
130 self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
131 self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
133 if self.fp and self._exclusive_fp:
134 self.fp.close()
135 self.fp = None
136 self.frombytes(data)
138 return super().load()
140 def load_seek(self, pos: int) -> None:
141 pass
143 def tell(self) -> int:
144 return self.__frame
147def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
148 _save(im, fp, filename, save_all=True)
151def _save(
152 im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
153) -> None:
154 info = im.encoderinfo.copy()
155 if save_all:
156 append_images = list(info.get("append_images", []))
157 else:
158 append_images = []
160 grayscale_modes = {"1", "L", "I", "I;16", "I;16L", "I;16B", "I;16N", "F"}
161 grayscale = all(
162 frame.mode in grayscale_modes
163 for ims in [im] + append_images
164 for frame in ImageSequence.Iterator(ims)
165 )
167 quality = info.get("quality", 75)
168 if not isinstance(quality, int) or quality < 0 or quality > 100:
169 msg = "Invalid quality setting"
170 raise ValueError(msg)
172 duration = info.get("duration", 0)
173 subsampling = info.get("subsampling", "4:0:0" if grayscale else "4:2:0")
174 speed = info.get("speed", 6)
175 max_threads = info.get("max_threads", _get_default_max_threads())
176 codec = info.get("codec", "auto")
177 if codec != "auto" and not _avif.encoder_codec_available(codec):
178 msg = "Invalid saving codec"
179 raise ValueError(msg)
180 range_ = info.get("range", "full")
181 tile_rows_log2 = info.get("tile_rows", 0)
182 tile_cols_log2 = info.get("tile_cols", 0)
183 alpha_premultiplied = bool(info.get("alpha_premultiplied", False))
184 autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0))
186 icc_profile = info.get("icc_profile", im.info.get("icc_profile"))
187 exif_orientation = 1
188 if exif := info.get("exif"):
189 if isinstance(exif, Image.Exif):
190 exif_data = exif
191 else:
192 exif_data = Image.Exif()
193 exif_data.load(exif)
194 if ExifTags.Base.Orientation in exif_data:
195 exif_orientation = exif_data.pop(ExifTags.Base.Orientation)
196 exif = exif_data.tobytes() if exif_data else b""
197 elif isinstance(exif, Image.Exif):
198 exif = exif_data.tobytes()
200 xmp = info.get("xmp")
202 if isinstance(xmp, str):
203 xmp = xmp.encode("utf-8")
205 advanced = info.get("advanced")
206 if advanced is not None:
207 if isinstance(advanced, dict):
208 advanced = advanced.items()
209 try:
210 advanced = tuple(advanced)
211 except TypeError:
212 invalid = True
213 else:
214 invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
215 if invalid:
216 msg = (
217 "advanced codec options must be a dict of key-value string "
218 "pairs or a series of key-value two-tuples"
219 )
220 raise ValueError(msg)
222 # Setup the AVIF encoder
223 enc = _avif.AvifEncoder(
224 im.size,
225 subsampling,
226 quality,
227 speed,
228 max_threads,
229 codec,
230 range_,
231 tile_rows_log2,
232 tile_cols_log2,
233 alpha_premultiplied,
234 autotiling,
235 icc_profile or b"",
236 exif or b"",
237 exif_orientation,
238 xmp or b"",
239 advanced,
240 )
242 supported_modes = {"RGB", "RGBA"}
243 if tuple(int(v) for v in _avif.libavif_version.split(".")) >= (1, 3):
244 # AVIF_RGB_FORMAT_GRAY was only added in libavif 1.3.0
245 supported_modes.add("L")
247 # Add each frame
248 frame_idx = 0
249 frame_duration = 0
250 cur_idx = im.tell()
251 is_single_frame = not append_images and not getattr(im, "is_animated", False)
252 try:
253 for ims in [im] + append_images:
254 for frame in ImageSequence.Iterator(ims):
255 # Make sure image mode is supported
256 rawmode = frame.mode
257 if ims.mode not in supported_modes:
258 if ims.has_transparency_data:
259 rawmode = "RGBA"
260 elif ims.mode in grayscale_modes and "L" in supported_modes:
261 rawmode = "L"
262 else:
263 rawmode = "RGB"
264 frame = frame.convert(rawmode)
266 # Update frame duration
267 if isinstance(duration, (list, tuple)):
268 frame_duration = duration[frame_idx]
269 else:
270 frame_duration = duration
272 # Append the frame to the animation encoder
273 enc.add(
274 frame.tobytes("raw", rawmode),
275 frame_duration,
276 frame.size,
277 rawmode,
278 is_single_frame,
279 )
281 # Update frame index
282 frame_idx += 1
284 if not save_all:
285 break
287 finally:
288 im.seek(cur_idx)
290 # Get the final output from the encoder
291 data = enc.finish()
292 if data is None:
293 msg = "cannot write file as AVIF (encoder returned None)"
294 raise OSError(msg)
296 fp.write(data)
299Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
300if SUPPORTED:
301 Image.register_save(AvifImageFile.format, _save)
302 Image.register_save_all(AvifImageFile.format, _save_all)
303 Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"])
304 Image.register_mime(AvifImageFile.format, "image/avif")