16
16
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
17
17
DECODE_CODEC_CHOICE = "auto"
18
18
CHROMA_UPSAMPLING = "auto"
19
+ # Decoding is only affected by this for libavif **0.8.4** or greater.
19
20
DEFAULT_MAX_THREADS = 0
20
21
21
- _VALID_AVIF_MODES = {"RGB" , "RGBA" }
22
-
23
-
24
22
if sys .version_info [0 ] == 2 :
25
23
text_type = unicode # noqa
26
24
else :
29
27
30
28
def _accept (prefix ):
31
29
if prefix [4 :8 ] != b"ftyp" :
32
- return
33
- coding_brands = (b"avif" , b"avis" )
34
- container_brands = (b"mif1" , b"msf1" )
30
+ return False
35
31
major_brand = prefix [8 :12 ]
36
- if major_brand in coding_brands :
37
- if not SUPPORTED :
38
- return (
39
- "image file could not be identified because AVIF "
40
- "support not installed"
41
- )
42
- return True
43
- if major_brand in container_brands :
32
+ if major_brand in (
33
+ # coding brands
34
+ b"avif" ,
35
+ b"avis" ,
44
36
# We accept files with AVIF container brands; we can't yet know if
45
37
# the ftyp box has the correct compatible brands, but if it doesn't
46
38
# then the plugin will raise a SyntaxError which Pillow will catch
47
39
# before moving on to the next plugin that accepts the file.
48
40
#
49
41
# Also, because this file might not actually be an AVIF file, we
50
42
# don't raise an error if AVIF support isn't properly compiled.
43
+ b"mif1" ,
44
+ b"msf1" ,
45
+ ):
46
+ if not SUPPORTED :
47
+ return (
48
+ "image file could not be identified because AVIF support not installed"
49
+ )
51
50
return True
51
+ return False
52
52
53
53
54
54
class AvifImageFile (ImageFile .ImageFile ):
55
55
format = "AVIF"
56
56
format_description = "AVIF image"
57
- __loaded = - 1
58
- __frame = 0
59
-
60
- def load_seek (self , pos ):
61
- pass
57
+ __frame = - 1
62
58
63
59
def _open (self ):
60
+ if not SUPPORTED :
61
+ msg = "image file could not be opened because AVIF support not installed"
62
+ raise SyntaxError (msg )
63
+
64
+ if DECODE_CODEC_CHOICE != "auto" and not _avif .decoder_codec_available (
65
+ DECODE_CODEC_CHOICE
66
+ ):
67
+ msg = "Invalid opening codec"
68
+ raise ValueError (msg )
64
69
self ._decoder = _avif .AvifDecoder (
65
70
self .fp .read (), DECODE_CODEC_CHOICE , CHROMA_UPSAMPLING , DEFAULT_MAX_THREADS
66
71
)
67
72
68
73
# Get info from decoder
69
- width , height , n_frames , mode , icc , exif , xmp = self ._decoder .get_info ()
70
- self ._size = width , height
71
- self .n_frames = n_frames
74
+ (
75
+ width ,
76
+ height ,
77
+ self .n_frames ,
78
+ mode ,
79
+ icc ,
80
+ exif ,
81
+ exif_orientation ,
82
+ xmp ,
83
+ ) = self ._decoder .get_info ()
84
+ self ._size = (width , height )
72
85
self .is_animated = self .n_frames > 1
73
86
try :
74
87
self .mode = self .rawmode = mode
75
88
except AttributeError :
76
89
self ._mode = self .rawmode = mode
77
- self .tile = []
78
90
79
91
if icc :
80
92
self .info ["icc_profile" ] = icc
81
- if exif :
82
- self .info ["exif" ] = exif
83
93
if xmp :
84
94
self .info ["xmp" ] = xmp
85
95
96
+ if exif_orientation != 1 or exif :
97
+ exif_data = Image .Exif ()
98
+ orientation_tag = next (
99
+ k for k , v in ExifTags .TAGS .items () if v == "Orientation"
100
+ )
101
+ if exif :
102
+ exif_data .load (exif )
103
+ original_orientation = exif_data .get (orientation_tag , 1 )
104
+ else :
105
+ original_orientation = 1
106
+ if exif_orientation != original_orientation :
107
+ exif_data [orientation_tag ] = exif_orientation
108
+ exif = exif_data .tobytes ()
109
+ if exif :
110
+ self .info ["exif" ] = exif
111
+ self .seek (0 )
112
+
86
113
def seek (self , frame ):
87
114
if not self ._seek_check (frame ):
88
115
return
89
116
117
+ # Set tile
90
118
self .__frame = frame
119
+ if hasattr (ImageFile , "_Tile" ):
120
+ self .tile = [ImageFile ._Tile ("raw" , (0 , 0 ) + self .size , 0 , self .mode )]
121
+ else :
122
+ self .tile = [("raw" , (0 , 0 ) + self .size , 0 , self .mode )]
91
123
92
124
def load (self ):
93
- if self .__loaded != self . __frame :
125
+ if self .tile :
94
126
# We need to load the image data for this frame
95
- data , timescale , tsp_in_ts , dur_in_ts = self . _decoder . get_frame (
96
- self . __frame
97
- )
98
- timestamp = round ( 1000 * ( tsp_in_ts / timescale ))
99
- duration = round ( 1000 * ( dur_in_ts / timescale ))
100
- self . info [ "timestamp" ] = timestamp
101
- self .info ["duration " ] = duration
102
- self .__loaded = self . __frame
127
+ (
128
+ data ,
129
+ timescale ,
130
+ pts_in_timescales ,
131
+ duration_in_timescales ,
132
+ ) = self . _decoder . get_frame ( self . __frame )
133
+ self .info ["timestamp " ] = round ( 1000 * ( pts_in_timescales / timescale ))
134
+ self .info [ "duration" ] = round ( 1000 * ( duration_in_timescales / timescale ))
103
135
104
- # Set tile
105
136
if self .fp and self ._exclusive_fp :
106
137
self .fp .close ()
107
138
self .fp = BytesIO (data )
108
- self .tile = [("raw" , (0 , 0 ) + self .size , 0 , self .rawmode )]
109
139
110
140
return super (AvifImageFile , self ).load ()
111
141
142
+ def load_seek (self , pos ):
143
+ pass
144
+
112
145
def tell (self ):
113
146
return self .__frame
114
147
@@ -128,19 +161,21 @@ def _save(im, fp, filename, save_all=False):
128
161
for ims in [im ] + append_images :
129
162
total += getattr (ims , "n_frames" , 1 )
130
163
131
- is_single_frame = total == 1
132
-
133
164
qmin = info .get ("qmin" , - 1 )
134
165
qmax = info .get ("qmax" , - 1 )
135
166
quality = info .get ("quality" , 75 )
136
167
if not isinstance (quality , int ) or quality < 0 or quality > 100 :
137
- raise ValueError ("Invalid quality setting" )
168
+ msg = "Invalid quality setting"
169
+ raise ValueError (msg )
138
170
139
171
duration = info .get ("duration" , 0 )
140
172
subsampling = info .get ("subsampling" , "4:2:0" )
141
173
speed = info .get ("speed" , 6 )
142
174
max_threads = info .get ("max_threads" , DEFAULT_MAX_THREADS )
143
175
codec = info .get ("codec" , "auto" )
176
+ if codec != "auto" and not _avif .encoder_codec_available (codec ):
177
+ msg = "Invalid saving codec"
178
+ raise ValueError (msg )
144
179
range_ = info .get ("range" , "full" )
145
180
tile_rows_log2 = info .get ("tile_rows" , 0 )
146
181
tile_cols_log2 = info .get ("tile_cols" , 0 )
@@ -171,20 +206,21 @@ def _save(im, fp, filename, save_all=False):
171
206
xmp = xmp .encode ("utf-8" )
172
207
173
208
advanced = info .get ("advanced" )
174
- if isinstance (advanced , dict ):
175
- advanced = tuple ([k , v ] for (k , v ) in advanced .items ())
176
209
if advanced is not None :
210
+ if isinstance (advanced , dict ):
211
+ advanced = tuple (advanced .items ())
177
212
try :
178
213
advanced = tuple (advanced )
179
214
except TypeError :
180
215
invalid = True
181
216
else :
182
- invalid = all ( isinstance (v , tuple ) and len (v ) = = 2 for v in advanced )
217
+ invalid = any ( not isinstance (v , tuple ) or len (v ) ! = 2 for v in advanced )
183
218
if invalid :
184
- raise ValueError (
219
+ msg = (
185
220
"advanced codec options must be a dict of key-value string "
186
221
"pairs or a series of key-value two-tuples"
187
222
)
223
+ raise ValueError (msg )
188
224
advanced = tuple (
189
225
[(str (k ).encode ("utf-8" ), str (v ).encode ("utf-8" )) for k , v in advanced ]
190
226
)
@@ -214,8 +250,9 @@ def _save(im, fp, filename, save_all=False):
214
250
215
251
# Add each frame
216
252
frame_idx = 0
217
- frame_dur = 0
253
+ frame_duration = 0
218
254
cur_idx = im .tell ()
255
+ is_single_frame = total == 1
219
256
try :
220
257
for ims in [im ] + append_images :
221
258
# Get # of frames in this image
@@ -228,7 +265,7 @@ def _save(im, fp, filename, save_all=False):
228
265
# Make sure image mode is supported
229
266
frame = ims
230
267
rawmode = ims .mode
231
- if ims .mode not in _VALID_AVIF_MODES :
268
+ if ims .mode not in { "RGB" , "RGBA" } :
232
269
alpha = (
233
270
"A" in ims .mode
234
271
or "a" in ims .mode
@@ -243,14 +280,14 @@ def _save(im, fp, filename, save_all=False):
243
280
244
281
# Update frame duration
245
282
if isinstance (duration , (list , tuple )):
246
- frame_dur = duration [frame_idx ]
283
+ frame_duration = duration [frame_idx ]
247
284
else :
248
- frame_dur = duration
285
+ frame_duration = duration
249
286
250
287
# Append the frame to the animation encoder
251
288
enc .add (
252
289
frame .tobytes ("raw" , rawmode ),
253
- frame_dur ,
290
+ frame_duration ,
254
291
frame .size [0 ],
255
292
frame .size [1 ],
256
293
rawmode ,
@@ -269,7 +306,8 @@ def _save(im, fp, filename, save_all=False):
269
306
# Get the final output from the encoder
270
307
data = enc .finish ()
271
308
if data is None :
272
- raise OSError ("cannot write file as AVIF (encoder returned None)" )
309
+ msg = "cannot write file as AVIF (encoder returned None)"
310
+ raise OSError (msg )
273
311
274
312
fp .write (data )
275
313
0 commit comments