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

170 statements  

1from __future__ import annotations 

2 

3import os 

4from io import BytesIO 

5from typing import IO 

6 

7from . import ExifTags, Image, ImageFile, ImageSequence 

8 

9try: 

10 from . import _avif 

11 

12 SUPPORTED = True 

13except ImportError: 

14 SUPPORTED = False 

15 

16# Decoder options as module globals, until there is a way to pass parameters 

17# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) 

18DECODE_CODEC_CHOICE = "auto" 

19DEFAULT_MAX_THREADS = 0 

20 

21 

22def get_codec_version(codec_name: str) -> str | None: 

23 versions = _avif.codec_versions() 

24 for version in versions.split(", "): 

25 if version.split(" [")[0] == codec_name: 

26 return version.split(":")[-1].split(" ")[0] 

27 return None 

28 

29 

30def _accept(prefix: bytes) -> bool | str: 

31 if prefix[4:8] != b"ftyp": 

32 return False 

33 major_brand = prefix[8:12] 

34 if major_brand in ( 

35 # coding brands 

36 b"avif", 

37 b"avis", 

38 # We accept files with AVIF container brands; we can't yet know if 

39 # the ftyp box has the correct compatible brands, but if it doesn't 

40 # then the plugin will raise a SyntaxError which Pillow will catch 

41 # before moving on to the next plugin that accepts the file. 

42 # 

43 # Also, because this file might not actually be an AVIF file, we 

44 # don't raise an error if AVIF support isn't properly compiled. 

45 b"mif1", 

46 b"msf1", 

47 ): 

48 if not SUPPORTED: 

49 return ( 

50 "image file could not be identified because AVIF support not installed" 

51 ) 

52 return True 

53 return False 

54 

55 

56def _get_default_max_threads() -> int: 

57 if DEFAULT_MAX_THREADS: 

58 return DEFAULT_MAX_THREADS 

59 if hasattr(os, "sched_getaffinity"): 

60 return len(os.sched_getaffinity(0)) 

61 else: 

62 return os.cpu_count() or 1 

63 

64 

65class AvifImageFile(ImageFile.ImageFile): 

66 format = "AVIF" 

67 format_description = "AVIF image" 

68 __frame = -1 

69 

70 def _open(self) -> None: 

71 if not SUPPORTED: 

72 msg = "image file could not be opened because AVIF support not installed" 

73 raise SyntaxError(msg) 

74 

75 if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( 

76 DECODE_CODEC_CHOICE 

77 ): 

78 msg = "Invalid opening codec" 

79 raise ValueError(msg) 

80 

81 assert self.fp is not None 

82 self._decoder = _avif.AvifDecoder( 

83 self.fp.read(), 

84 DECODE_CODEC_CHOICE, 

85 _get_default_max_threads(), 

86 ) 

87 

88 # Get info from decoder 

89 self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( 

90 self._decoder.get_info() 

91 ) 

92 self.is_animated = self.n_frames > 1 

93 

94 if icc: 

95 self.info["icc_profile"] = icc 

96 if xmp: 

97 self.info["xmp"] = xmp 

98 

99 if exif_orientation != 1 or exif: 

100 exif_data = Image.Exif() 

101 if exif: 

102 exif_data.load(exif) 

103 original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) 

104 else: 

105 original_orientation = 1 

106 if exif_orientation != original_orientation: 

107 exif_data[ExifTags.Base.Orientation] = exif_orientation 

108 exif = exif_data.tobytes() 

109 if exif: 

110 self.info["exif"] = exif 

111 self.seek(0) 

112 

113 def seek(self, frame: int) -> None: 

114 if not self._seek_check(frame): 

115 return 

116 

117 # Set tile 

118 self.__frame = frame 

119 self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] 

120 

121 def load(self) -> Image.core.PixelAccess | None: 

122 if self.tile: 

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

124 data, timescale, pts_in_timescales, duration_in_timescales = ( 

125 self._decoder.get_frame(self.__frame) 

126 ) 

127 self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) 

128 self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) 

129 

130 if self.fp and self._exclusive_fp: 

131 self.fp.close() 

132 self.fp = BytesIO(data) 

133 

134 return super().load() 

135 

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

137 pass 

138 

139 def tell(self) -> int: 

140 return self.__frame 

141 

142 

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

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

145 

146 

147def _save( 

148 im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False 

149) -> None: 

150 info = im.encoderinfo.copy() 

151 if save_all: 

152 append_images = list(info.get("append_images", [])) 

153 else: 

154 append_images = [] 

155 

156 grayscale_modes = {"1", "L", "I", "I;16", "I;16L", "I;16B", "I;16N", "F"} 

157 grayscale = all( 

158 frame.mode in grayscale_modes 

159 for ims in [im] + append_images 

160 for frame in ImageSequence.Iterator(ims) 

161 ) 

162 

163 quality = info.get("quality", 75) 

164 if not isinstance(quality, int) or quality < 0 or quality > 100: 

165 msg = "Invalid quality setting" 

166 raise ValueError(msg) 

167 

168 duration = info.get("duration", 0) 

169 subsampling = info.get("subsampling", "4:0:0" if grayscale else "4:2:0") 

170 speed = info.get("speed", 6) 

171 max_threads = info.get("max_threads", _get_default_max_threads()) 

172 codec = info.get("codec", "auto") 

173 if codec != "auto" and not _avif.encoder_codec_available(codec): 

174 msg = "Invalid saving codec" 

175 raise ValueError(msg) 

176 range_ = info.get("range", "full") 

177 tile_rows_log2 = info.get("tile_rows", 0) 

178 tile_cols_log2 = info.get("tile_cols", 0) 

179 alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) 

180 autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) 

181 

182 icc_profile = info.get("icc_profile", im.info.get("icc_profile")) 

183 exif_orientation = 1 

184 if exif := info.get("exif"): 

185 if isinstance(exif, Image.Exif): 

186 exif_data = exif 

187 else: 

188 exif_data = Image.Exif() 

189 exif_data.load(exif) 

190 if ExifTags.Base.Orientation in exif_data: 

191 exif_orientation = exif_data.pop(ExifTags.Base.Orientation) 

192 exif = exif_data.tobytes() if exif_data else b"" 

193 elif isinstance(exif, Image.Exif): 

194 exif = exif_data.tobytes() 

195 

196 xmp = info.get("xmp") 

197 

198 if isinstance(xmp, str): 

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

200 

201 advanced = info.get("advanced") 

202 if advanced is not None: 

203 if isinstance(advanced, dict): 

204 advanced = advanced.items() 

205 try: 

206 advanced = tuple(advanced) 

207 except TypeError: 

208 invalid = True 

209 else: 

210 invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) 

211 if invalid: 

212 msg = ( 

213 "advanced codec options must be a dict of key-value string " 

214 "pairs or a series of key-value two-tuples" 

215 ) 

216 raise ValueError(msg) 

217 

218 # Setup the AVIF encoder 

219 enc = _avif.AvifEncoder( 

220 im.size, 

221 subsampling, 

222 quality, 

223 speed, 

224 max_threads, 

225 codec, 

226 range_, 

227 tile_rows_log2, 

228 tile_cols_log2, 

229 alpha_premultiplied, 

230 autotiling, 

231 icc_profile or b"", 

232 exif or b"", 

233 exif_orientation, 

234 xmp or b"", 

235 advanced, 

236 ) 

237 

238 # Add each frame 

239 frame_idx = 0 

240 frame_duration = 0 

241 cur_idx = im.tell() 

242 is_single_frame = not append_images and not getattr(im, "is_animated", False) 

243 try: 

244 for ims in [im] + append_images: 

245 for frame in ImageSequence.Iterator(ims): 

246 # Make sure image mode is supported 

247 rawmode = frame.mode 

248 if ims.mode not in {"L", "RGB", "RGBA"}: 

249 if ims.has_transparency_data: 

250 rawmode = "RGBA" 

251 elif ims.mode in grayscale_modes: 

252 rawmode = "L" 

253 else: 

254 rawmode = "RGB" 

255 frame = frame.convert(rawmode) 

256 

257 # Update frame duration 

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

259 frame_duration = duration[frame_idx] 

260 else: 

261 frame_duration = duration 

262 

263 # Append the frame to the animation encoder 

264 enc.add( 

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

266 frame_duration, 

267 frame.size, 

268 rawmode, 

269 is_single_frame, 

270 ) 

271 

272 # Update frame index 

273 frame_idx += 1 

274 

275 if not save_all: 

276 break 

277 

278 finally: 

279 im.seek(cur_idx) 

280 

281 # Get the final output from the encoder 

282 data = enc.finish() 

283 if data is None: 

284 msg = "cannot write file as AVIF (encoder returned None)" 

285 raise OSError(msg) 

286 

287 fp.write(data) 

288 

289 

290Image.register_open(AvifImageFile.format, AvifImageFile, _accept) 

291if SUPPORTED: 

292 Image.register_save(AvifImageFile.format, _save) 

293 Image.register_save_all(AvifImageFile.format, _save_all) 

294 Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) 

295 Image.register_mime(AvifImageFile.format, "image/avif")