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

176 statements  

1from __future__ import annotations 

2 

3__lazy_modules__ = {"io", "typing"} 

4 

5import os 

6from typing import IO 

7 

8from . import ExifTags, Image, ImageFile, ImageSequence 

9 

10try: 

11 from . import _avif 

12 

13 SUPPORTED = True 

14except ImportError: 

15 SUPPORTED = False 

16 

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 

21 

22 

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 

29 

30 

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 

55 

56 

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 

64 

65 

66class AvifImageFile(ImageFile.ImageFile): 

67 format = "AVIF" 

68 format_description = "AVIF image" 

69 __frame = -1 

70 

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) 

75 

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) 

81 

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 ) 

88 

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 

94 

95 if icc: 

96 self.info["icc_profile"] = icc 

97 if xmp: 

98 self.info["xmp"] = xmp 

99 

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) 

113 

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) 

119 

120 # Set tile 

121 self.__frame = frame 

122 self._im = None 

123 

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)) 

132 

133 if self.fp and self._exclusive_fp: 

134 self.fp.close() 

135 self.fp = None 

136 self.frombytes(data) 

137 

138 return super().load() 

139 

140 def load_seek(self, pos: int) -> None: 

141 pass 

142 

143 def tell(self) -> int: 

144 return self.__frame 

145 

146 

147def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

148 _save(im, fp, filename, save_all=True) 

149 

150 

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 = [] 

159 

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 ) 

166 

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) 

171 

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)) 

185 

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() 

199 

200 xmp = info.get("xmp") 

201 

202 if isinstance(xmp, str): 

203 xmp = xmp.encode("utf-8") 

204 

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) 

221 

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 ) 

241 

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") 

246 

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) 

265 

266 # Update frame duration 

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

268 frame_duration = duration[frame_idx] 

269 else: 

270 frame_duration = duration 

271 

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 ) 

280 

281 # Update frame index 

282 frame_idx += 1 

283 

284 if not save_all: 

285 break 

286 

287 finally: 

288 im.seek(cur_idx) 

289 

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) 

295 

296 fp.write(data) 

297 

298 

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")