Skip to content

Commit 3fbc201

Browse files
committed
chore: sync with changes from python-pillow/Pillow#5201
Updates the pillow-avif-plugin code to more closely match the current state of the open Pillow PR, python-pillow/Pillow#5201. The differences that remain have to do with python 2.7 compatibility. Most of the code changes from the Pillow PR are stylistic, not functional, but there are two bug fixes included: - AvifImagePlugin.CHROMA_UPSAMPLING is now actually used by the decoder. Previously, although it was passed into the decoder, it did not have any effect. Note that this is different from the Pillow PR, where this functionality was removed instead. - AVIF images with irot and imir now have those values converted to an EXIF orientation when decoded. EXIF orientation has been preserved by the encoder since 1.4.2, which is when we started setting irot and imir. But if such an image was converted to another format, the orientation would have been lost.
1 parent 791c6dd commit 3fbc201

File tree

3 files changed

+469
-393
lines changed

3 files changed

+469
-393
lines changed

CHANGELOG.rst

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Changelog
44
Changes since 1.4.6 (unreleased)
55
--------------------------------
66

7+
* **Fixed**: Convert AVIF irot and imir into EXIF orientation when decoding
8+
an image, in `#70`_. EXIF orientation has been preserved by the encoder
9+
since 1.4.2, which is when we started setting irot and imir. But if an AVIF
10+
image with non-default irot or imir values was converted to another format,
11+
its orientation would be lost.
12+
* **Fixed**: ``pillow_avif.AvifImagePlugin.CHROMA_UPSAMPLING`` is now actually
13+
used when decoding an image, in `#70`_.
14+
* **Added**: Python 3.13 free-thread mode support (experimental).
715
* **CI**: Update libavif to 1.2.0 (`4eb0a40`_, 2025-03-05); publish wheels
816
for python 3.13. See the table below for the current AVIF codec versions.
917
Libraries whose versions have changed since the last pillow-avif-plugin
@@ -19,6 +27,7 @@ Changes since 1.4.6 (unreleased)
1927
rav1e 0.7.1
2028
=========== ==========
2129

30+
.. _#70: https://github.com/fdintino/pillow-avif-plugin/pull/70
2231
.. _4eb0a40: https://github.com/AOMediaCodec/libavif/commit/4eb0a40fb06612adf53650a14c692eaf62c068e6
2332

2433
1.4.6 (Jul 14, 2024)

src/pillow_avif/AvifImagePlugin.py

+84-49
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@
1616
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
1717
DECODE_CODEC_CHOICE = "auto"
1818
CHROMA_UPSAMPLING = "auto"
19+
# Decoding is only affected by this for libavif **0.8.4** or greater.
1920
DEFAULT_MAX_THREADS = 0
2021

21-
_VALID_AVIF_MODES = {"RGB", "RGBA"}
22-
23-
2422
if sys.version_info[0] == 2:
2523
text_type = unicode # noqa
2624
else:
@@ -29,86 +27,118 @@
2927

3028
def _accept(prefix):
3129
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
3531
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",
4436
# We accept files with AVIF container brands; we can't yet know if
4537
# the ftyp box has the correct compatible brands, but if it doesn't
4638
# then the plugin will raise a SyntaxError which Pillow will catch
4739
# before moving on to the next plugin that accepts the file.
4840
#
4941
# Also, because this file might not actually be an AVIF file, we
5042
# 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+
)
5150
return True
51+
return False
5252

5353

5454
class AvifImageFile(ImageFile.ImageFile):
5555
format = "AVIF"
5656
format_description = "AVIF image"
57-
__loaded = -1
58-
__frame = 0
59-
60-
def load_seek(self, pos):
61-
pass
57+
__frame = -1
6258

6359
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)
6469
self._decoder = _avif.AvifDecoder(
6570
self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING, DEFAULT_MAX_THREADS
6671
)
6772

6873
# 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)
7285
self.is_animated = self.n_frames > 1
7386
try:
7487
self.mode = self.rawmode = mode
7588
except AttributeError:
7689
self._mode = self.rawmode = mode
77-
self.tile = []
7890

7991
if icc:
8092
self.info["icc_profile"] = icc
81-
if exif:
82-
self.info["exif"] = exif
8393
if xmp:
8494
self.info["xmp"] = xmp
8595

96+
if exif_orientation != 1 or exif:
97+
exif_data = Image.Exif()
98+
if exif:
99+
exif_data.load(exif)
100+
original_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
101+
else:
102+
original_orientation = 1
103+
if exif_orientation != original_orientation:
104+
exif_data[ExifTags.Base.Orientation] = exif_orientation
105+
exif = exif_data.tobytes()
106+
if exif:
107+
self.info["exif"] = exif
108+
self.seek(0)
109+
86110
def seek(self, frame):
87111
if not self._seek_check(frame):
88112
return
89113

114+
# Set tile
90115
self.__frame = frame
116+
if hasattr(ImageFile, "_Tile"):
117+
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
118+
else:
119+
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
91120

92121
def load(self):
93-
if self.__loaded != self.__frame:
122+
if self.tile:
94123
# 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
124+
(
125+
data,
126+
timescale,
127+
pts_in_timescales,
128+
duration_in_timescales,
129+
) = self._decoder.get_frame(self.__frame)
130+
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
131+
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
103132

104-
# Set tile
105133
if self.fp and self._exclusive_fp:
106134
self.fp.close()
107135
self.fp = BytesIO(data)
108-
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
109136

110137
return super(AvifImageFile, self).load()
111138

139+
def load_seek(self, pos):
140+
pass
141+
112142
def tell(self):
113143
return self.__frame
114144

@@ -128,19 +158,21 @@ def _save(im, fp, filename, save_all=False):
128158
for ims in [im] + append_images:
129159
total += getattr(ims, "n_frames", 1)
130160

131-
is_single_frame = total == 1
132-
133161
qmin = info.get("qmin", -1)
134162
qmax = info.get("qmax", -1)
135163
quality = info.get("quality", 75)
136164
if not isinstance(quality, int) or quality < 0 or quality > 100:
137-
raise ValueError("Invalid quality setting")
165+
msg = "Invalid quality setting"
166+
raise ValueError(msg)
138167

139168
duration = info.get("duration", 0)
140169
subsampling = info.get("subsampling", "4:2:0")
141170
speed = info.get("speed", 6)
142171
max_threads = info.get("max_threads", DEFAULT_MAX_THREADS)
143172
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)
144176
range_ = info.get("range", "full")
145177
tile_rows_log2 = info.get("tile_rows", 0)
146178
tile_cols_log2 = info.get("tile_cols", 0)
@@ -171,20 +203,21 @@ def _save(im, fp, filename, save_all=False):
171203
xmp = xmp.encode("utf-8")
172204

173205
advanced = info.get("advanced")
174-
if isinstance(advanced, dict):
175-
advanced = tuple([k, v] for (k, v) in advanced.items())
176206
if advanced is not None:
207+
if isinstance(advanced, dict):
208+
advanced = tuple(advanced.items())
177209
try:
178210
advanced = tuple(advanced)
179211
except TypeError:
180212
invalid = True
181213
else:
182-
invalid = all(isinstance(v, tuple) and len(v) == 2 for v in advanced)
214+
invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
183215
if invalid:
184-
raise ValueError(
216+
msg = (
185217
"advanced codec options must be a dict of key-value string "
186218
"pairs or a series of key-value two-tuples"
187219
)
220+
raise ValueError(msg)
188221
advanced = tuple(
189222
[(str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced]
190223
)
@@ -214,8 +247,9 @@ def _save(im, fp, filename, save_all=False):
214247

215248
# Add each frame
216249
frame_idx = 0
217-
frame_dur = 0
250+
frame_duration = 0
218251
cur_idx = im.tell()
252+
is_single_frame = total == 1
219253
try:
220254
for ims in [im] + append_images:
221255
# Get # of frames in this image
@@ -228,7 +262,7 @@ def _save(im, fp, filename, save_all=False):
228262
# Make sure image mode is supported
229263
frame = ims
230264
rawmode = ims.mode
231-
if ims.mode not in _VALID_AVIF_MODES:
265+
if ims.mode not in {"RGB", "RGBA"}:
232266
alpha = (
233267
"A" in ims.mode
234268
or "a" in ims.mode
@@ -243,14 +277,14 @@ def _save(im, fp, filename, save_all=False):
243277

244278
# Update frame duration
245279
if isinstance(duration, (list, tuple)):
246-
frame_dur = duration[frame_idx]
280+
frame_duration = duration[frame_idx]
247281
else:
248-
frame_dur = duration
282+
frame_duration = duration
249283

250284
# Append the frame to the animation encoder
251285
enc.add(
252286
frame.tobytes("raw", rawmode),
253-
frame_dur,
287+
frame_duration,
254288
frame.size[0],
255289
frame.size[1],
256290
rawmode,
@@ -269,7 +303,8 @@ def _save(im, fp, filename, save_all=False):
269303
# Get the final output from the encoder
270304
data = enc.finish()
271305
if data is None:
272-
raise OSError("cannot write file as AVIF (encoder returned None)")
306+
msg = "cannot write file as AVIF (encoder returned None)"
307+
raise OSError(msg)
273308

274309
fp.write(data)
275310

0 commit comments

Comments
 (0)