Skip to content

Commit 62f7c94

Browse files
authored
chore: sync with changes from python-pillow/Pillow#5201 (#70)
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 62f7c94

File tree

3 files changed

+502
-390
lines changed

3 files changed

+502
-390
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

+87-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,121 @@
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+
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+
86113
def seek(self, frame):
87114
if not self._seek_check(frame):
88115
return
89116

117+
# Set tile
90118
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)]
91123

92124
def load(self):
93-
if self.__loaded != self.__frame:
125+
if self.tile:
94126
# 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))
103135

104-
# Set tile
105136
if self.fp and self._exclusive_fp:
106137
self.fp.close()
107138
self.fp = BytesIO(data)
108-
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
109139

110140
return super(AvifImageFile, self).load()
111141

142+
def load_seek(self, pos):
143+
pass
144+
112145
def tell(self):
113146
return self.__frame
114147

@@ -128,19 +161,21 @@ def _save(im, fp, filename, save_all=False):
128161
for ims in [im] + append_images:
129162
total += getattr(ims, "n_frames", 1)
130163

131-
is_single_frame = total == 1
132-
133164
qmin = info.get("qmin", -1)
134165
qmax = info.get("qmax", -1)
135166
quality = info.get("quality", 75)
136167
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)
138170

139171
duration = info.get("duration", 0)
140172
subsampling = info.get("subsampling", "4:2:0")
141173
speed = info.get("speed", 6)
142174
max_threads = info.get("max_threads", DEFAULT_MAX_THREADS)
143175
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)
144179
range_ = info.get("range", "full")
145180
tile_rows_log2 = info.get("tile_rows", 0)
146181
tile_cols_log2 = info.get("tile_cols", 0)
@@ -171,20 +206,21 @@ def _save(im, fp, filename, save_all=False):
171206
xmp = xmp.encode("utf-8")
172207

173208
advanced = info.get("advanced")
174-
if isinstance(advanced, dict):
175-
advanced = tuple([k, v] for (k, v) in advanced.items())
176209
if advanced is not None:
210+
if isinstance(advanced, dict):
211+
advanced = tuple(advanced.items())
177212
try:
178213
advanced = tuple(advanced)
179214
except TypeError:
180215
invalid = True
181216
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)
183218
if invalid:
184-
raise ValueError(
219+
msg = (
185220
"advanced codec options must be a dict of key-value string "
186221
"pairs or a series of key-value two-tuples"
187222
)
223+
raise ValueError(msg)
188224
advanced = tuple(
189225
[(str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced]
190226
)
@@ -214,8 +250,9 @@ def _save(im, fp, filename, save_all=False):
214250

215251
# Add each frame
216252
frame_idx = 0
217-
frame_dur = 0
253+
frame_duration = 0
218254
cur_idx = im.tell()
255+
is_single_frame = total == 1
219256
try:
220257
for ims in [im] + append_images:
221258
# Get # of frames in this image
@@ -228,7 +265,7 @@ def _save(im, fp, filename, save_all=False):
228265
# Make sure image mode is supported
229266
frame = ims
230267
rawmode = ims.mode
231-
if ims.mode not in _VALID_AVIF_MODES:
268+
if ims.mode not in {"RGB", "RGBA"}:
232269
alpha = (
233270
"A" in ims.mode
234271
or "a" in ims.mode
@@ -243,14 +280,14 @@ def _save(im, fp, filename, save_all=False):
243280

244281
# Update frame duration
245282
if isinstance(duration, (list, tuple)):
246-
frame_dur = duration[frame_idx]
283+
frame_duration = duration[frame_idx]
247284
else:
248-
frame_dur = duration
285+
frame_duration = duration
249286

250287
# Append the frame to the animation encoder
251288
enc.add(
252289
frame.tobytes("raw", rawmode),
253-
frame_dur,
290+
frame_duration,
254291
frame.size[0],
255292
frame.size[1],
256293
rawmode,
@@ -269,7 +306,8 @@ def _save(im, fp, filename, save_all=False):
269306
# Get the final output from the encoder
270307
data = enc.finish()
271308
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)
273311

274312
fp.write(data)
275313

0 commit comments

Comments
 (0)