pillow-avif-plugin 1.4.3__tar.gz → 1.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pillow-avif-plugin might be problematic. Click here for more details.
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/PKG-INFO +2 -5
- pillow-avif-plugin-1.4.4/src/pillow_avif/AvifImagePlugin.py +282 -0
- pillow-avif-plugin-1.4.3/src/pillow_avif/AvifImagePlugin.py → pillow-avif-plugin-1.4.4/src/pillow_avif/AvifImagePlugin.py.orig +8 -1
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif/__init__.py +1 -1
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif/_avif.c +23 -6
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif_plugin.egg-info/PKG-INFO +2 -5
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif_plugin.egg-info/SOURCES.txt +4 -2
- pillow-avif-plugin-1.4.4/tests/test_file_avif.py +783 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/LICENSE +0 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/MANIFEST.in +0 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/README.md +0 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/pyproject.toml +0 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/setup.cfg +0 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/setup.py +0 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif_plugin.egg-info/dependency_links.txt +0 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif_plugin.egg-info/top_level.txt +0 -0
- /pillow-avif-plugin-1.4.3/src/pillow_avif_plugin.egg-info/not-zip-safe → /pillow-avif-plugin-1.4.4/src/pillow_avif_plugin.egg-info/zip-safe +0 -0
- {pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/tox.ini +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pillow-avif-plugin
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: A pillow plugin that adds avif support via libavif
|
|
5
5
|
Home-page: https://github.com/fdintino/pillow-avif-plugin/
|
|
6
|
+
Download-URL: https://github.com/fdintino/pillow-avif-plugin/releases
|
|
6
7
|
Author: Frankie Dintino
|
|
7
8
|
Author-email: fdintino@theatlantic.com
|
|
8
9
|
License: MIT License
|
|
9
|
-
Download-URL: https://github.com/fdintino/pillow-avif-plugin/releases
|
|
10
|
-
Platform: UNKNOWN
|
|
11
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
11
|
Classifier: Environment :: Web Environment
|
|
13
12
|
Classifier: Intended Audience :: Developers
|
|
@@ -35,5 +34,3 @@ License-File: LICENSE
|
|
|
35
34
|
This is a plugin that adds support for AVIF files until official support has been added (see [this pull request](https://github.com/python-pillow/Pillow/pull/5201)).
|
|
36
35
|
|
|
37
36
|
To register this plugin with pillow you will need to add `import pillow_avif` somewhere in your application.
|
|
38
|
-
|
|
39
|
-
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from __future__ import division
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from PIL import ExifTags, Image, ImageFile
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from pillow_avif import _avif
|
|
10
|
+
|
|
11
|
+
SUPPORTED = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
SUPPORTED = False
|
|
14
|
+
|
|
15
|
+
# Decoder options as module globals, until there is a way to pass parameters
|
|
16
|
+
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
|
|
17
|
+
DECODE_CODEC_CHOICE = "auto"
|
|
18
|
+
CHROMA_UPSAMPLING = "auto"
|
|
19
|
+
DEFAULT_MAX_THREADS = 0
|
|
20
|
+
|
|
21
|
+
_VALID_AVIF_MODES = {"RGB", "RGBA"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if sys.version_info[0] == 2:
|
|
25
|
+
text_type = unicode # noqa
|
|
26
|
+
else:
|
|
27
|
+
text_type = str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _accept(prefix):
|
|
31
|
+
if prefix[4:8] != b"ftyp":
|
|
32
|
+
return
|
|
33
|
+
coding_brands = (b"avif", b"avis")
|
|
34
|
+
container_brands = (b"mif1", b"msf1")
|
|
35
|
+
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:
|
|
44
|
+
# We accept files with AVIF container brands; we can't yet know if
|
|
45
|
+
# the ftyp box has the correct compatible brands, but if it doesn't
|
|
46
|
+
# then the plugin will raise a SyntaxError which Pillow will catch
|
|
47
|
+
# before moving on to the next plugin that accepts the file.
|
|
48
|
+
#
|
|
49
|
+
# Also, because this file might not actually be an AVIF file, we
|
|
50
|
+
# don't raise an error if AVIF support isn't properly compiled.
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AvifImageFile(ImageFile.ImageFile):
|
|
55
|
+
format = "AVIF"
|
|
56
|
+
format_description = "AVIF image"
|
|
57
|
+
__loaded = -1
|
|
58
|
+
__frame = 0
|
|
59
|
+
|
|
60
|
+
def load_seek(self, pos):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def _open(self):
|
|
64
|
+
self._decoder = _avif.AvifDecoder(
|
|
65
|
+
self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING, DEFAULT_MAX_THREADS
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 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
|
|
72
|
+
self.is_animated = self.n_frames > 1
|
|
73
|
+
try:
|
|
74
|
+
self.mode = self.rawmode = mode
|
|
75
|
+
except AttributeError:
|
|
76
|
+
self._mode = self.rawmode = mode
|
|
77
|
+
self.tile = []
|
|
78
|
+
|
|
79
|
+
if icc:
|
|
80
|
+
self.info["icc_profile"] = icc
|
|
81
|
+
if exif:
|
|
82
|
+
self.info["exif"] = exif
|
|
83
|
+
if xmp:
|
|
84
|
+
self.info["xmp"] = xmp
|
|
85
|
+
|
|
86
|
+
def seek(self, frame):
|
|
87
|
+
if not self._seek_check(frame):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self.__frame = frame
|
|
91
|
+
|
|
92
|
+
def load(self):
|
|
93
|
+
if self.__loaded != self.__frame:
|
|
94
|
+
# 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
|
|
103
|
+
|
|
104
|
+
# Set tile
|
|
105
|
+
if self.fp and self._exclusive_fp:
|
|
106
|
+
self.fp.close()
|
|
107
|
+
self.fp = BytesIO(data)
|
|
108
|
+
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
|
|
109
|
+
|
|
110
|
+
return super(AvifImageFile, self).load()
|
|
111
|
+
|
|
112
|
+
def tell(self):
|
|
113
|
+
return self.__frame
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _save_all(im, fp, filename):
|
|
117
|
+
_save(im, fp, filename, save_all=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _save(im, fp, filename, save_all=False):
|
|
121
|
+
info = im.encoderinfo.copy()
|
|
122
|
+
if save_all:
|
|
123
|
+
append_images = list(info.get("append_images", []))
|
|
124
|
+
else:
|
|
125
|
+
append_images = []
|
|
126
|
+
|
|
127
|
+
total = 0
|
|
128
|
+
for ims in [im] + append_images:
|
|
129
|
+
total += getattr(ims, "n_frames", 1)
|
|
130
|
+
|
|
131
|
+
is_single_frame = total == 1
|
|
132
|
+
|
|
133
|
+
qmin = info.get("qmin", -1)
|
|
134
|
+
qmax = info.get("qmax", -1)
|
|
135
|
+
quality = info.get("quality", 75)
|
|
136
|
+
if not isinstance(quality, int) or quality < 0 or quality > 100:
|
|
137
|
+
raise ValueError("Invalid quality setting")
|
|
138
|
+
|
|
139
|
+
duration = info.get("duration", 0)
|
|
140
|
+
subsampling = info.get("subsampling", "4:2:0")
|
|
141
|
+
speed = info.get("speed", 6)
|
|
142
|
+
max_threads = info.get("max_threads", DEFAULT_MAX_THREADS)
|
|
143
|
+
codec = info.get("codec", "auto")
|
|
144
|
+
range_ = info.get("range", "full")
|
|
145
|
+
tile_rows_log2 = info.get("tile_rows", 0)
|
|
146
|
+
tile_cols_log2 = info.get("tile_cols", 0)
|
|
147
|
+
alpha_premultiplied = bool(info.get("alpha_premultiplied", False))
|
|
148
|
+
autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0))
|
|
149
|
+
|
|
150
|
+
icc_profile = info.get("icc_profile", im.info.get("icc_profile"))
|
|
151
|
+
exif = info.get("exif", im.info.get("exif"))
|
|
152
|
+
if isinstance(exif, Image.Exif):
|
|
153
|
+
exif = exif.tobytes()
|
|
154
|
+
|
|
155
|
+
exif_orientation = 0
|
|
156
|
+
if exif:
|
|
157
|
+
exif_data = Image.Exif()
|
|
158
|
+
try:
|
|
159
|
+
exif_data.load(exif)
|
|
160
|
+
except SyntaxError:
|
|
161
|
+
pass
|
|
162
|
+
else:
|
|
163
|
+
orientation_tag = next(
|
|
164
|
+
k for k, v in ExifTags.TAGS.items() if v == "Orientation"
|
|
165
|
+
)
|
|
166
|
+
exif_orientation = exif_data.get(orientation_tag) or 0
|
|
167
|
+
|
|
168
|
+
xmp = info.get("xmp", im.info.get("xmp") or im.info.get("XML:com.adobe.xmp"))
|
|
169
|
+
|
|
170
|
+
if isinstance(xmp, text_type):
|
|
171
|
+
xmp = xmp.encode("utf-8")
|
|
172
|
+
|
|
173
|
+
advanced = info.get("advanced")
|
|
174
|
+
if isinstance(advanced, dict):
|
|
175
|
+
advanced = tuple([k, v] for (k, v) in advanced.items())
|
|
176
|
+
if advanced is not None:
|
|
177
|
+
try:
|
|
178
|
+
advanced = tuple(advanced)
|
|
179
|
+
except TypeError:
|
|
180
|
+
invalid = True
|
|
181
|
+
else:
|
|
182
|
+
invalid = all(isinstance(v, tuple) and len(v) == 2 for v in advanced)
|
|
183
|
+
if invalid:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"advanced codec options must be a dict of key-value string "
|
|
186
|
+
"pairs or a series of key-value two-tuples"
|
|
187
|
+
)
|
|
188
|
+
advanced = tuple(
|
|
189
|
+
[(str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Setup the AVIF encoder
|
|
193
|
+
enc = _avif.AvifEncoder(
|
|
194
|
+
im.size[0],
|
|
195
|
+
im.size[1],
|
|
196
|
+
subsampling,
|
|
197
|
+
qmin,
|
|
198
|
+
qmax,
|
|
199
|
+
quality,
|
|
200
|
+
speed,
|
|
201
|
+
max_threads,
|
|
202
|
+
codec,
|
|
203
|
+
range_,
|
|
204
|
+
tile_rows_log2,
|
|
205
|
+
tile_cols_log2,
|
|
206
|
+
alpha_premultiplied,
|
|
207
|
+
autotiling,
|
|
208
|
+
icc_profile or b"",
|
|
209
|
+
exif or b"",
|
|
210
|
+
exif_orientation,
|
|
211
|
+
xmp or b"",
|
|
212
|
+
advanced,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Add each frame
|
|
216
|
+
frame_idx = 0
|
|
217
|
+
frame_dur = 0
|
|
218
|
+
cur_idx = im.tell()
|
|
219
|
+
try:
|
|
220
|
+
for ims in [im] + append_images:
|
|
221
|
+
# Get # of frames in this image
|
|
222
|
+
nfr = getattr(ims, "n_frames", 1)
|
|
223
|
+
|
|
224
|
+
for idx in range(nfr):
|
|
225
|
+
ims.seek(idx)
|
|
226
|
+
ims.load()
|
|
227
|
+
|
|
228
|
+
# Make sure image mode is supported
|
|
229
|
+
frame = ims
|
|
230
|
+
rawmode = ims.mode
|
|
231
|
+
if ims.mode not in _VALID_AVIF_MODES:
|
|
232
|
+
alpha = (
|
|
233
|
+
"A" in ims.mode
|
|
234
|
+
or "a" in ims.mode
|
|
235
|
+
or (ims.mode == "P" and "A" in ims.im.getpalettemode())
|
|
236
|
+
or (
|
|
237
|
+
ims.mode == "P"
|
|
238
|
+
and ims.info.get("transparency", None) is not None
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
rawmode = "RGBA" if alpha else "RGB"
|
|
242
|
+
frame = ims.convert(rawmode)
|
|
243
|
+
|
|
244
|
+
# Update frame duration
|
|
245
|
+
if isinstance(duration, (list, tuple)):
|
|
246
|
+
frame_dur = duration[frame_idx]
|
|
247
|
+
else:
|
|
248
|
+
frame_dur = duration
|
|
249
|
+
|
|
250
|
+
# Append the frame to the animation encoder
|
|
251
|
+
enc.add(
|
|
252
|
+
frame.tobytes("raw", rawmode),
|
|
253
|
+
frame_dur,
|
|
254
|
+
frame.size[0],
|
|
255
|
+
frame.size[1],
|
|
256
|
+
rawmode,
|
|
257
|
+
is_single_frame,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Update frame index
|
|
261
|
+
frame_idx += 1
|
|
262
|
+
|
|
263
|
+
if not save_all:
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
finally:
|
|
267
|
+
im.seek(cur_idx)
|
|
268
|
+
|
|
269
|
+
# Get the final output from the encoder
|
|
270
|
+
data = enc.finish()
|
|
271
|
+
if data is None:
|
|
272
|
+
raise OSError("cannot write file as AVIF (encoder returned None)")
|
|
273
|
+
|
|
274
|
+
fp.write(data)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
|
|
278
|
+
if SUPPORTED:
|
|
279
|
+
Image.register_save(AvifImageFile.format, _save)
|
|
280
|
+
Image.register_save_all(AvifImageFile.format, _save_all)
|
|
281
|
+
Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"])
|
|
282
|
+
Image.register_mime(AvifImageFile.format, "image/avif")
|
|
@@ -16,6 +16,11 @@ except ImportError:
|
|
|
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
|
+
<<<<<<< HEAD
|
|
20
|
+
DEFAULT_MAX_THREADS = 0
|
|
21
|
+
=======
|
|
22
|
+
DECODE_MAX_THREADS = 0
|
|
23
|
+
>>>>>>> 1e2fa65 (Let users pass max_threads manually as an argument)
|
|
19
24
|
|
|
20
25
|
_VALID_AVIF_MODES = {"RGB", "RGBA"}
|
|
21
26
|
|
|
@@ -61,7 +66,7 @@ class AvifImageFile(ImageFile.ImageFile):
|
|
|
61
66
|
|
|
62
67
|
def _open(self):
|
|
63
68
|
self._decoder = _avif.AvifDecoder(
|
|
64
|
-
self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING
|
|
69
|
+
self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING, DECODE_MAX_THREADS
|
|
65
70
|
)
|
|
66
71
|
|
|
67
72
|
# Get info from decoder
|
|
@@ -138,6 +143,7 @@ def _save(im, fp, filename, save_all=False):
|
|
|
138
143
|
duration = info.get("duration", 0)
|
|
139
144
|
subsampling = info.get("subsampling", "4:2:0")
|
|
140
145
|
speed = info.get("speed", 6)
|
|
146
|
+
max_threads = info.get("max_threads", DEFAULT_MAX_THREADS)
|
|
141
147
|
codec = info.get("codec", "auto")
|
|
142
148
|
range_ = info.get("range", "full")
|
|
143
149
|
tile_rows_log2 = info.get("tile_rows", 0)
|
|
@@ -196,6 +202,7 @@ def _save(im, fp, filename, save_all=False):
|
|
|
196
202
|
qmax,
|
|
197
203
|
quality,
|
|
198
204
|
speed,
|
|
205
|
+
max_threads,
|
|
199
206
|
codec,
|
|
200
207
|
range_,
|
|
201
208
|
tile_rows_log2,
|
|
@@ -46,7 +46,7 @@ typedef struct {
|
|
|
46
46
|
|
|
47
47
|
static PyTypeObject AvifDecoder_Type;
|
|
48
48
|
|
|
49
|
-
static int
|
|
49
|
+
static int default_max_threads = 0;
|
|
50
50
|
|
|
51
51
|
static void
|
|
52
52
|
init_max_threads(void) {
|
|
@@ -85,7 +85,7 @@ init_max_threads(void) {
|
|
|
85
85
|
goto error;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
default_max_threads = (int)num_cpus;
|
|
89
89
|
|
|
90
90
|
done:
|
|
91
91
|
Py_XDECREF(os);
|
|
@@ -321,6 +321,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
|
|
|
321
321
|
int quality = 75;
|
|
322
322
|
int speed = 8;
|
|
323
323
|
int exif_orientation = 0;
|
|
324
|
+
int max_threads = default_max_threads;
|
|
324
325
|
PyObject *icc_bytes;
|
|
325
326
|
PyObject *exif_bytes;
|
|
326
327
|
PyObject *xmp_bytes;
|
|
@@ -336,7 +337,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
|
|
|
336
337
|
|
|
337
338
|
if (!PyArg_ParseTuple(
|
|
338
339
|
args,
|
|
339
|
-
"
|
|
340
|
+
"IIsiiiiissiiOOSSiSO",
|
|
340
341
|
&width,
|
|
341
342
|
&height,
|
|
342
343
|
&subsampling,
|
|
@@ -344,6 +345,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
|
|
|
344
345
|
&qmax,
|
|
345
346
|
&quality,
|
|
346
347
|
&speed,
|
|
348
|
+
&max_threads,
|
|
347
349
|
&codec,
|
|
348
350
|
&range,
|
|
349
351
|
&tile_rows_log2,
|
|
@@ -445,7 +447,10 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
|
|
|
445
447
|
encoder = avifEncoderCreate();
|
|
446
448
|
|
|
447
449
|
if (max_threads == 0) {
|
|
448
|
-
|
|
450
|
+
if (default_max_threads == 0) {
|
|
451
|
+
init_max_threads();
|
|
452
|
+
}
|
|
453
|
+
max_threads = default_max_threads;
|
|
449
454
|
}
|
|
450
455
|
|
|
451
456
|
int is_aom_encode = strcmp(codec, "aom") == 0 ||
|
|
@@ -726,10 +731,11 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
|
|
|
726
731
|
char *codec_str;
|
|
727
732
|
avifCodecChoice codec;
|
|
728
733
|
avifChromaUpsampling upsampling;
|
|
734
|
+
int max_threads = 0;
|
|
729
735
|
|
|
730
736
|
avifResult result;
|
|
731
737
|
|
|
732
|
-
if (!PyArg_ParseTuple(args, "
|
|
738
|
+
if (!PyArg_ParseTuple(args, "Sssi", &avif_bytes, &codec_str, &upsampling_str, &max_threads)) {
|
|
733
739
|
return NULL;
|
|
734
740
|
}
|
|
735
741
|
|
|
@@ -778,9 +784,20 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
|
|
|
778
784
|
self->decoder = avifDecoderCreate();
|
|
779
785
|
#if AVIF_VERSION >= 80400
|
|
780
786
|
if (max_threads == 0) {
|
|
781
|
-
|
|
787
|
+
if (default_max_threads == 0) {
|
|
788
|
+
init_max_threads();
|
|
789
|
+
}
|
|
790
|
+
max_threads = default_max_threads;
|
|
782
791
|
}
|
|
783
792
|
self->decoder->maxThreads = max_threads;
|
|
793
|
+
#endif
|
|
794
|
+
#if AVIF_VERSION >= 90200
|
|
795
|
+
// Turn off libavif's 'clap' (clean aperture) property validation.
|
|
796
|
+
self->decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID;
|
|
797
|
+
// Allow the PixelInformationProperty ('pixi') to be missing in AV1 image
|
|
798
|
+
// items. libheif v1.11.0 and older does not add the 'pixi' item property to
|
|
799
|
+
// AV1 image items.
|
|
800
|
+
self->decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED;
|
|
784
801
|
#endif
|
|
785
802
|
self->decoder->codecChoice = codec;
|
|
786
803
|
|
{pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif_plugin.egg-info/PKG-INFO
RENAMED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pillow-avif-plugin
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: A pillow plugin that adds avif support via libavif
|
|
5
5
|
Home-page: https://github.com/fdintino/pillow-avif-plugin/
|
|
6
|
+
Download-URL: https://github.com/fdintino/pillow-avif-plugin/releases
|
|
6
7
|
Author: Frankie Dintino
|
|
7
8
|
Author-email: fdintino@theatlantic.com
|
|
8
9
|
License: MIT License
|
|
9
|
-
Download-URL: https://github.com/fdintino/pillow-avif-plugin/releases
|
|
10
|
-
Platform: UNKNOWN
|
|
11
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
11
|
Classifier: Environment :: Web Environment
|
|
13
12
|
Classifier: Intended Audience :: Developers
|
|
@@ -35,5 +34,3 @@ License-File: LICENSE
|
|
|
35
34
|
This is a plugin that adds support for AVIF files until official support has been added (see [this pull request](https://github.com/python-pillow/Pillow/pull/5201)).
|
|
36
35
|
|
|
37
36
|
To register this plugin with pillow you will need to add `import pillow_avif` somewhere in your application.
|
|
38
|
-
|
|
39
|
-
|
{pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif_plugin.egg-info/SOURCES.txt
RENAMED
|
@@ -6,10 +6,12 @@ setup.cfg
|
|
|
6
6
|
setup.py
|
|
7
7
|
tox.ini
|
|
8
8
|
src/pillow_avif/AvifImagePlugin.py
|
|
9
|
+
src/pillow_avif/AvifImagePlugin.py.orig
|
|
9
10
|
src/pillow_avif/__init__.py
|
|
10
11
|
src/pillow_avif/_avif.c
|
|
11
12
|
src/pillow_avif_plugin.egg-info/PKG-INFO
|
|
12
13
|
src/pillow_avif_plugin.egg-info/SOURCES.txt
|
|
13
14
|
src/pillow_avif_plugin.egg-info/dependency_links.txt
|
|
14
|
-
src/pillow_avif_plugin.egg-info/
|
|
15
|
-
src/pillow_avif_plugin.egg-info/
|
|
15
|
+
src/pillow_avif_plugin.egg-info/top_level.txt
|
|
16
|
+
src/pillow_avif_plugin.egg-info/zip-safe
|
|
17
|
+
tests/test_file_avif.py
|
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import xml.etree.ElementTree
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from os import cpu_count
|
|
9
|
+
except ImportError:
|
|
10
|
+
from multiprocessing import cpu_count
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from PIL import Image, ImageDraw
|
|
15
|
+
from pillow_avif import AvifImagePlugin
|
|
16
|
+
|
|
17
|
+
from .helper import (
|
|
18
|
+
PillowLeakTestCase,
|
|
19
|
+
assert_image,
|
|
20
|
+
assert_image_similar,
|
|
21
|
+
assert_image_similar_tofile,
|
|
22
|
+
hopper,
|
|
23
|
+
has_alpha_premultiplied,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from pillow_avif import _avif
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from PIL import UnidentifiedImageError
|
|
30
|
+
except ImportError:
|
|
31
|
+
UnidentifiedImageError = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
CURR_DIR = os.path.dirname(os.path.dirname(__file__))
|
|
35
|
+
TEST_AVIF_FILE = "%s/tests/images/hopper.avif" % CURR_DIR
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def assert_xmp_orientation(xmp, expected):
|
|
39
|
+
assert isinstance(xmp, bytes)
|
|
40
|
+
root = xml.etree.ElementTree.fromstring(xmp)
|
|
41
|
+
orientation = None
|
|
42
|
+
for elem in root.iter():
|
|
43
|
+
if elem.tag.endswith("}Description"):
|
|
44
|
+
orientation = elem.attrib.get("{http://ns.adobe.com/tiff/1.0/}Orientation")
|
|
45
|
+
if orientation:
|
|
46
|
+
orientation = int(orientation)
|
|
47
|
+
break
|
|
48
|
+
assert orientation == expected
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def roundtrip(im, **options):
|
|
52
|
+
out = BytesIO()
|
|
53
|
+
im.save(out, "AVIF", **options)
|
|
54
|
+
out.seek(0)
|
|
55
|
+
return Image.open(out)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def skip_unless_avif_decoder(codec_name):
|
|
59
|
+
reason = "%s decode not available" % codec_name
|
|
60
|
+
return pytest.mark.skipif(
|
|
61
|
+
not _avif or not _avif.decoder_codec_available(codec_name), reason=reason
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def skip_unless_avif_encoder(codec_name):
|
|
66
|
+
reason = "%s encode not available" % codec_name
|
|
67
|
+
return pytest.mark.skipif(
|
|
68
|
+
not _avif or not _avif.encoder_codec_available(codec_name), reason=reason
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_docker_qemu():
|
|
73
|
+
try:
|
|
74
|
+
init_proc_exe = os.readlink("/proc/1/exe")
|
|
75
|
+
except: # noqa: E722
|
|
76
|
+
return False
|
|
77
|
+
else:
|
|
78
|
+
return "qemu" in init_proc_exe
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def skip_unless_avif_version_gte(version):
|
|
82
|
+
if not _avif:
|
|
83
|
+
reason = "AVIF unavailable"
|
|
84
|
+
should_skip = True
|
|
85
|
+
else:
|
|
86
|
+
version_str = ".".join([str(v) for v in version])
|
|
87
|
+
reason = "%s < %s" % (_avif.libavif_version, version_str)
|
|
88
|
+
should_skip = _avif.VERSION < version
|
|
89
|
+
return pytest.mark.skipif(should_skip, reason=reason)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def skip_unless_avif_version_lt(version):
|
|
93
|
+
if not _avif:
|
|
94
|
+
reason = "AVIF unavailable"
|
|
95
|
+
should_skip = True
|
|
96
|
+
else:
|
|
97
|
+
version_str = ".".join([str(v) for v in version])
|
|
98
|
+
reason = "%s > %s" % (_avif.libavif_version, version_str)
|
|
99
|
+
should_skip = _avif.VERSION >= version
|
|
100
|
+
return pytest.mark.skipif(should_skip, reason=reason)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestUnsupportedAvif:
|
|
104
|
+
def test_unsupported(self):
|
|
105
|
+
AvifImagePlugin.SUPPORTED = False
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
file_path = "%s/tests/images/hopper.avif" % CURR_DIR
|
|
109
|
+
if UnidentifiedImageError:
|
|
110
|
+
pytest.warns(
|
|
111
|
+
UserWarning,
|
|
112
|
+
lambda: pytest.raises(
|
|
113
|
+
UnidentifiedImageError, Image.open, file_path
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
with pytest.raises(IOError):
|
|
118
|
+
Image.open(file_path)
|
|
119
|
+
finally:
|
|
120
|
+
AvifImagePlugin.SUPPORTED = True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestFileAvif:
|
|
124
|
+
def test_version(self):
|
|
125
|
+
_avif.AvifCodecVersions()
|
|
126
|
+
|
|
127
|
+
def test_read(self):
|
|
128
|
+
"""
|
|
129
|
+
Can we read an AVIF file without error?
|
|
130
|
+
Does it have the bits we expect?
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
with Image.open("%s/tests/images/hopper.avif" % CURR_DIR) as image:
|
|
134
|
+
assert image.mode == "RGB"
|
|
135
|
+
assert image.size == (128, 128)
|
|
136
|
+
assert image.format == "AVIF"
|
|
137
|
+
assert image.get_format_mimetype() == "image/avif"
|
|
138
|
+
image.load()
|
|
139
|
+
image.getdata()
|
|
140
|
+
|
|
141
|
+
# generated with:
|
|
142
|
+
# avifdec hopper.avif hopper_avif_write.png
|
|
143
|
+
assert_image_similar_tofile(
|
|
144
|
+
image, "%s/tests/images/hopper_avif_write.png" % CURR_DIR, 12.0
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _roundtrip(self, tmp_path, mode, epsilon, args={}):
|
|
148
|
+
temp_file = str(tmp_path / "temp.avif")
|
|
149
|
+
|
|
150
|
+
hopper(mode).save(temp_file, **args)
|
|
151
|
+
with Image.open(temp_file) as image:
|
|
152
|
+
assert image.mode == "RGB"
|
|
153
|
+
assert image.size == (128, 128)
|
|
154
|
+
assert image.format == "AVIF"
|
|
155
|
+
image.load()
|
|
156
|
+
image.getdata()
|
|
157
|
+
|
|
158
|
+
if mode == "RGB":
|
|
159
|
+
# avifdec hopper.avif avif/hopper_avif_write.png
|
|
160
|
+
assert_image_similar_tofile(
|
|
161
|
+
image, "%s/tests/images/hopper_avif_write.png" % CURR_DIR, 12.0
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# This test asserts that the images are similar. If the average pixel
|
|
165
|
+
# difference between the two images is less than the epsilon value,
|
|
166
|
+
# then we're going to accept that it's a reasonable lossy version of
|
|
167
|
+
# the image.
|
|
168
|
+
target = hopper(mode)
|
|
169
|
+
if mode != "RGB":
|
|
170
|
+
target = target.convert("RGB")
|
|
171
|
+
assert_image_similar(image, target, epsilon)
|
|
172
|
+
|
|
173
|
+
def test_write_rgb(self, tmp_path):
|
|
174
|
+
"""
|
|
175
|
+
Can we write a RGB mode file to avif without error?
|
|
176
|
+
Does it have the bits we expect?
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
self._roundtrip(tmp_path, "RGB", 12.5)
|
|
180
|
+
|
|
181
|
+
def test_AvifEncoder_with_invalid_args(self):
|
|
182
|
+
"""
|
|
183
|
+
Calling encoder functions with no arguments should result in an error.
|
|
184
|
+
"""
|
|
185
|
+
with pytest.raises(TypeError):
|
|
186
|
+
_avif.AvifEncoder()
|
|
187
|
+
|
|
188
|
+
def test_AvifDecoder_with_invalid_args(self):
|
|
189
|
+
"""
|
|
190
|
+
Calling decoder functions with no arguments should result in an error.
|
|
191
|
+
"""
|
|
192
|
+
with pytest.raises(TypeError):
|
|
193
|
+
_avif.AvifDecoder()
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
|
|
196
|
+
def test_accept_ftyp_brands(self, major_brand):
|
|
197
|
+
data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand
|
|
198
|
+
assert AvifImagePlugin._accept(data) is True
|
|
199
|
+
|
|
200
|
+
def test_no_resource_warning(self, tmp_path):
|
|
201
|
+
with Image.open(TEST_AVIF_FILE) as image:
|
|
202
|
+
temp_file = str(tmp_path / "temp.avif")
|
|
203
|
+
with warnings.catch_warnings():
|
|
204
|
+
warnings.simplefilter("error")
|
|
205
|
+
image.save(temp_file)
|
|
206
|
+
|
|
207
|
+
def test_file_pointer_could_be_reused(self):
|
|
208
|
+
with open(TEST_AVIF_FILE, "rb") as blob:
|
|
209
|
+
Image.open(blob).load()
|
|
210
|
+
Image.open(blob).load()
|
|
211
|
+
|
|
212
|
+
def test_background_from_gif(self, tmp_path):
|
|
213
|
+
with Image.open("%s/tests/images/chi.gif" % CURR_DIR) as im:
|
|
214
|
+
original_value = im.convert("RGB").getpixel((1, 1))
|
|
215
|
+
|
|
216
|
+
# Save as AVIF
|
|
217
|
+
out_avif = str(tmp_path / "temp.avif")
|
|
218
|
+
im.save(out_avif, save_all=True)
|
|
219
|
+
|
|
220
|
+
# Save as GIF
|
|
221
|
+
out_gif = str(tmp_path / "temp.gif")
|
|
222
|
+
Image.open(out_avif).save(out_gif)
|
|
223
|
+
|
|
224
|
+
with Image.open(out_gif) as reread:
|
|
225
|
+
reread_value = reread.convert("RGB").getpixel((1, 1))
|
|
226
|
+
difference = sum(
|
|
227
|
+
[abs(original_value[i] - reread_value[i]) for i in range(0, 3)]
|
|
228
|
+
)
|
|
229
|
+
assert difference < 5
|
|
230
|
+
|
|
231
|
+
def test_save_single_frame(self, tmp_path):
|
|
232
|
+
temp_file = str(tmp_path / "temp.avif")
|
|
233
|
+
with Image.open("%s/tests/images/chi.gif" % CURR_DIR) as im:
|
|
234
|
+
# Save as AVIF
|
|
235
|
+
im.save(temp_file)
|
|
236
|
+
with Image.open(temp_file) as im:
|
|
237
|
+
assert im.n_frames == 1
|
|
238
|
+
|
|
239
|
+
def test_invalid_file(self):
|
|
240
|
+
invalid_file = "tests/images/flower.jpg"
|
|
241
|
+
|
|
242
|
+
with pytest.raises(SyntaxError):
|
|
243
|
+
AvifImagePlugin.AvifImageFile(invalid_file)
|
|
244
|
+
|
|
245
|
+
def test_load_transparent_rgb(self):
|
|
246
|
+
test_file = "tests/images/transparency.avif"
|
|
247
|
+
with Image.open(test_file) as im:
|
|
248
|
+
assert_image(im, "RGBA", (64, 64))
|
|
249
|
+
|
|
250
|
+
# image has 876 transparent pixels
|
|
251
|
+
assert im.getchannel("A").getcolors()[0][0] == 876
|
|
252
|
+
|
|
253
|
+
def test_save_transparent(self, tmp_path):
|
|
254
|
+
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
|
|
255
|
+
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
|
256
|
+
|
|
257
|
+
test_file = str(tmp_path / "temp.avif")
|
|
258
|
+
im.save(test_file)
|
|
259
|
+
|
|
260
|
+
# check if saved image contains same transparency
|
|
261
|
+
with Image.open(test_file) as im:
|
|
262
|
+
assert_image(im, "RGBA", (10, 10))
|
|
263
|
+
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
|
264
|
+
|
|
265
|
+
def test_save_icc_profile(self):
|
|
266
|
+
with Image.open("tests/images/icc_profile_none.avif") as im:
|
|
267
|
+
assert im.info.get("icc_profile") is None
|
|
268
|
+
|
|
269
|
+
with Image.open("tests/images/icc_profile.avif") as with_icc:
|
|
270
|
+
expected_icc = with_icc.info.get("icc_profile")
|
|
271
|
+
assert expected_icc is not None
|
|
272
|
+
|
|
273
|
+
im = roundtrip(im, icc_profile=expected_icc)
|
|
274
|
+
assert im.info["icc_profile"] == expected_icc
|
|
275
|
+
|
|
276
|
+
def test_discard_icc_profile(self):
|
|
277
|
+
with Image.open("tests/images/icc_profile.avif") as im:
|
|
278
|
+
im = roundtrip(im, icc_profile=None)
|
|
279
|
+
assert "icc_profile" not in im.info
|
|
280
|
+
|
|
281
|
+
def test_roundtrip_icc_profile(self):
|
|
282
|
+
with Image.open("tests/images/icc_profile.avif") as im:
|
|
283
|
+
expected_icc = im.info["icc_profile"]
|
|
284
|
+
|
|
285
|
+
im = roundtrip(im)
|
|
286
|
+
assert im.info["icc_profile"] == expected_icc
|
|
287
|
+
|
|
288
|
+
def test_roundtrip_no_icc_profile(self):
|
|
289
|
+
with Image.open("tests/images/icc_profile_none.avif") as im:
|
|
290
|
+
assert im.info.get("icc_profile") is None
|
|
291
|
+
|
|
292
|
+
im = roundtrip(im)
|
|
293
|
+
assert "icc_profile" not in im.info
|
|
294
|
+
|
|
295
|
+
def test_exif(self):
|
|
296
|
+
# With an EXIF chunk
|
|
297
|
+
with Image.open("tests/images/exif.avif") as im:
|
|
298
|
+
exif = im.getexif()
|
|
299
|
+
assert exif[274] == 1
|
|
300
|
+
|
|
301
|
+
def test_exif_save(self, tmp_path):
|
|
302
|
+
with Image.open("tests/images/exif.avif") as im:
|
|
303
|
+
test_file = str(tmp_path / "temp.avif")
|
|
304
|
+
im.save(test_file)
|
|
305
|
+
|
|
306
|
+
with Image.open(test_file) as reloaded:
|
|
307
|
+
exif = reloaded.getexif()
|
|
308
|
+
assert exif[274] == 1
|
|
309
|
+
|
|
310
|
+
def test_exif_obj_argument(self, tmp_path):
|
|
311
|
+
exif = Image.Exif()
|
|
312
|
+
exif[274] = 1
|
|
313
|
+
exif_data = exif.tobytes()
|
|
314
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
315
|
+
test_file = str(tmp_path / "temp.avif")
|
|
316
|
+
im.save(test_file, exif=exif)
|
|
317
|
+
|
|
318
|
+
with Image.open(test_file) as reloaded:
|
|
319
|
+
assert reloaded.info["exif"] == exif_data
|
|
320
|
+
|
|
321
|
+
def test_exif_bytes_argument(self, tmp_path):
|
|
322
|
+
exif = Image.Exif()
|
|
323
|
+
exif[274] = 1
|
|
324
|
+
exif_data = exif.tobytes()
|
|
325
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
326
|
+
test_file = str(tmp_path / "temp.avif")
|
|
327
|
+
im.save(test_file, exif=exif_data)
|
|
328
|
+
|
|
329
|
+
with Image.open(test_file) as reloaded:
|
|
330
|
+
assert reloaded.info["exif"] == exif_data
|
|
331
|
+
|
|
332
|
+
def test_exif_invalid(self, tmp_path):
|
|
333
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
334
|
+
test_file = str(tmp_path / "temp.avif")
|
|
335
|
+
with pytest.raises(ValueError):
|
|
336
|
+
im.save(test_file, exif=b"invalid")
|
|
337
|
+
|
|
338
|
+
def test_xmp(self):
|
|
339
|
+
with Image.open("tests/images/xmp_tags_orientation.avif") as im:
|
|
340
|
+
xmp = im.info.get("xmp")
|
|
341
|
+
assert_xmp_orientation(xmp, 3)
|
|
342
|
+
|
|
343
|
+
def test_xmp_save(self, tmp_path):
|
|
344
|
+
with Image.open("tests/images/xmp_tags_orientation.avif") as im:
|
|
345
|
+
test_file = str(tmp_path / "temp.avif")
|
|
346
|
+
im.save(test_file)
|
|
347
|
+
|
|
348
|
+
with Image.open(test_file) as reloaded:
|
|
349
|
+
xmp = reloaded.info.get("xmp")
|
|
350
|
+
assert_xmp_orientation(xmp, 3)
|
|
351
|
+
|
|
352
|
+
def test_xmp_save_from_png(self, tmp_path):
|
|
353
|
+
with Image.open("tests/images/xmp_tags_orientation.png") as im:
|
|
354
|
+
test_file = str(tmp_path / "temp.avif")
|
|
355
|
+
im.save(test_file)
|
|
356
|
+
|
|
357
|
+
with Image.open(test_file) as reloaded:
|
|
358
|
+
xmp = reloaded.info.get("xmp")
|
|
359
|
+
assert_xmp_orientation(xmp, 3)
|
|
360
|
+
|
|
361
|
+
def test_xmp_save_argument(self, tmp_path):
|
|
362
|
+
xmp_arg = "\n".join(
|
|
363
|
+
[
|
|
364
|
+
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>',
|
|
365
|
+
'<x:xmpmeta xmlns:x="adobe:ns:meta/">',
|
|
366
|
+
' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">',
|
|
367
|
+
' <rdf:Description rdf:about=""',
|
|
368
|
+
' xmlns:tiff="http://ns.adobe.com/tiff/1.0/"',
|
|
369
|
+
' tiff:Orientation="1"/>',
|
|
370
|
+
" </rdf:RDF>",
|
|
371
|
+
"</x:xmpmeta>",
|
|
372
|
+
'<?xpacket end="r"?>',
|
|
373
|
+
]
|
|
374
|
+
)
|
|
375
|
+
with Image.open("tests/images/hopper.avif") as im:
|
|
376
|
+
test_file = str(tmp_path / "temp.avif")
|
|
377
|
+
im.save(test_file, xmp=xmp_arg)
|
|
378
|
+
|
|
379
|
+
with Image.open(test_file) as reloaded:
|
|
380
|
+
xmp = reloaded.info.get("xmp")
|
|
381
|
+
assert_xmp_orientation(xmp, 1)
|
|
382
|
+
|
|
383
|
+
def test_tell(self):
|
|
384
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
385
|
+
assert im.tell() == 0
|
|
386
|
+
|
|
387
|
+
def test_seek(self):
|
|
388
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
389
|
+
im.seek(0)
|
|
390
|
+
|
|
391
|
+
with pytest.raises(EOFError):
|
|
392
|
+
im.seek(1)
|
|
393
|
+
|
|
394
|
+
@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:0:0"])
|
|
395
|
+
def test_encoder_subsampling(self, tmp_path, subsampling):
|
|
396
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
397
|
+
test_file = str(tmp_path / "temp.avif")
|
|
398
|
+
im.save(test_file, subsampling=subsampling)
|
|
399
|
+
|
|
400
|
+
def test_encoder_subsampling_invalid(self, tmp_path):
|
|
401
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
402
|
+
test_file = str(tmp_path / "temp.avif")
|
|
403
|
+
with pytest.raises(ValueError):
|
|
404
|
+
im.save(test_file, subsampling="foo")
|
|
405
|
+
|
|
406
|
+
def test_encoder_range(self, tmp_path):
|
|
407
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
408
|
+
test_file = str(tmp_path / "temp.avif")
|
|
409
|
+
im.save(test_file, range="limited")
|
|
410
|
+
|
|
411
|
+
def test_encoder_range_invalid(self, tmp_path):
|
|
412
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
413
|
+
test_file = str(tmp_path / "temp.avif")
|
|
414
|
+
with pytest.raises(ValueError):
|
|
415
|
+
im.save(test_file, range="foo")
|
|
416
|
+
|
|
417
|
+
@skip_unless_avif_encoder("aom")
|
|
418
|
+
def test_encoder_codec_param(self, tmp_path):
|
|
419
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
420
|
+
test_file = str(tmp_path / "temp.avif")
|
|
421
|
+
im.save(test_file, codec="aom")
|
|
422
|
+
|
|
423
|
+
def test_encoder_codec_invalid(self, tmp_path):
|
|
424
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
425
|
+
test_file = str(tmp_path / "temp.avif")
|
|
426
|
+
with pytest.raises(ValueError):
|
|
427
|
+
im.save(test_file, codec="foo")
|
|
428
|
+
|
|
429
|
+
@skip_unless_avif_decoder("dav1d")
|
|
430
|
+
def test_encoder_codec_cannot_encode(self, tmp_path):
|
|
431
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
432
|
+
test_file = str(tmp_path / "temp.avif")
|
|
433
|
+
with pytest.raises(ValueError):
|
|
434
|
+
im.save(test_file, codec="dav1d")
|
|
435
|
+
|
|
436
|
+
@skip_unless_avif_encoder("aom")
|
|
437
|
+
@skip_unless_avif_version_gte((0, 8, 2))
|
|
438
|
+
def test_encoder_advanced_codec_options(self):
|
|
439
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
440
|
+
ctrl_buf = BytesIO()
|
|
441
|
+
im.save(ctrl_buf, "AVIF", codec="aom")
|
|
442
|
+
test_buf = BytesIO()
|
|
443
|
+
im.save(
|
|
444
|
+
test_buf,
|
|
445
|
+
"AVIF",
|
|
446
|
+
codec="aom",
|
|
447
|
+
advanced={
|
|
448
|
+
"aq-mode": "1",
|
|
449
|
+
"enable-chroma-deltaq": "1",
|
|
450
|
+
},
|
|
451
|
+
)
|
|
452
|
+
assert ctrl_buf.getvalue() != test_buf.getvalue()
|
|
453
|
+
|
|
454
|
+
@skip_unless_avif_encoder("aom")
|
|
455
|
+
@skip_unless_avif_version_gte((0, 8, 2))
|
|
456
|
+
@pytest.mark.parametrize("val", [{"foo": "bar"}, 1234])
|
|
457
|
+
def test_encoder_advanced_codec_options_invalid(self, tmp_path, val):
|
|
458
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
459
|
+
test_file = str(tmp_path / "temp.avif")
|
|
460
|
+
with pytest.raises(ValueError):
|
|
461
|
+
im.save(test_file, codec="aom", advanced=val)
|
|
462
|
+
|
|
463
|
+
@skip_unless_avif_decoder("aom")
|
|
464
|
+
def test_decoder_codec_param(self):
|
|
465
|
+
AvifImagePlugin.DECODE_CODEC_CHOICE = "aom"
|
|
466
|
+
try:
|
|
467
|
+
with Image.open(TEST_AVIF_FILE) as im:
|
|
468
|
+
assert im.size == (128, 128)
|
|
469
|
+
finally:
|
|
470
|
+
AvifImagePlugin.DECODE_CODEC_CHOICE = "auto"
|
|
471
|
+
|
|
472
|
+
@skip_unless_avif_encoder("rav1e")
|
|
473
|
+
def test_decoder_codec_cannot_decode(self, tmp_path):
|
|
474
|
+
AvifImagePlugin.DECODE_CODEC_CHOICE = "rav1e"
|
|
475
|
+
try:
|
|
476
|
+
with pytest.raises(ValueError):
|
|
477
|
+
with Image.open(TEST_AVIF_FILE):
|
|
478
|
+
pass
|
|
479
|
+
finally:
|
|
480
|
+
AvifImagePlugin.DECODE_CODEC_CHOICE = "auto"
|
|
481
|
+
|
|
482
|
+
def test_decoder_codec_invalid(self):
|
|
483
|
+
AvifImagePlugin.DECODE_CODEC_CHOICE = "foo"
|
|
484
|
+
try:
|
|
485
|
+
with pytest.raises(ValueError):
|
|
486
|
+
with Image.open(TEST_AVIF_FILE):
|
|
487
|
+
pass
|
|
488
|
+
finally:
|
|
489
|
+
AvifImagePlugin.DECODE_CODEC_CHOICE = "auto"
|
|
490
|
+
|
|
491
|
+
@skip_unless_avif_encoder("aom")
|
|
492
|
+
def test_encoder_codec_available(self):
|
|
493
|
+
assert _avif.encoder_codec_available("aom") is True
|
|
494
|
+
|
|
495
|
+
def test_encoder_codec_available_bad_params(self):
|
|
496
|
+
with pytest.raises(TypeError):
|
|
497
|
+
_avif.encoder_codec_available()
|
|
498
|
+
|
|
499
|
+
@skip_unless_avif_encoder("dav1d")
|
|
500
|
+
def test_encoder_codec_available_cannot_decode(self):
|
|
501
|
+
assert _avif.encoder_codec_available("dav1d") is False
|
|
502
|
+
|
|
503
|
+
def test_encoder_codec_available_invalid(self):
|
|
504
|
+
assert _avif.encoder_codec_available("foo") is False
|
|
505
|
+
|
|
506
|
+
@skip_unless_avif_version_lt((1, 0, 0))
|
|
507
|
+
@pytest.mark.parametrize(
|
|
508
|
+
"quality,expected_qminmax",
|
|
509
|
+
[
|
|
510
|
+
[0, (63, 63)],
|
|
511
|
+
[100, (0, 0)],
|
|
512
|
+
[90, (0, 10)],
|
|
513
|
+
[None, (0, 25)], # default
|
|
514
|
+
[50, (14, 50)],
|
|
515
|
+
],
|
|
516
|
+
)
|
|
517
|
+
def test_encoder_quality_qmin_qmax_map(self, tmp_path, quality, expected_qminmax):
|
|
518
|
+
qmin, qmax = expected_qminmax
|
|
519
|
+
with Image.open("tests/images/hopper.avif") as im:
|
|
520
|
+
out_quality = BytesIO()
|
|
521
|
+
out_qminmax = BytesIO()
|
|
522
|
+
im.save(out_qminmax, "AVIF", qmin=qmin, qmax=qmax)
|
|
523
|
+
if quality is None:
|
|
524
|
+
im.save(out_quality, "AVIF")
|
|
525
|
+
else:
|
|
526
|
+
im.save(out_quality, "AVIF", quality=quality)
|
|
527
|
+
assert len(out_quality.getvalue()) == len(out_qminmax.getvalue())
|
|
528
|
+
|
|
529
|
+
def test_encoder_quality_valueerror(self, tmp_path):
|
|
530
|
+
with Image.open("tests/images/hopper.avif") as im:
|
|
531
|
+
test_file = str(tmp_path / "temp.avif")
|
|
532
|
+
with pytest.raises(ValueError):
|
|
533
|
+
im.save(test_file, quality="invalid")
|
|
534
|
+
|
|
535
|
+
@skip_unless_avif_decoder("aom")
|
|
536
|
+
def test_decoder_codec_available(self):
|
|
537
|
+
assert _avif.decoder_codec_available("aom") is True
|
|
538
|
+
|
|
539
|
+
def test_decoder_codec_available_bad_params(self):
|
|
540
|
+
with pytest.raises(TypeError):
|
|
541
|
+
_avif.decoder_codec_available()
|
|
542
|
+
|
|
543
|
+
@skip_unless_avif_encoder("rav1e")
|
|
544
|
+
def test_decoder_codec_available_cannot_decode(self):
|
|
545
|
+
assert _avif.decoder_codec_available("rav1e") is False
|
|
546
|
+
|
|
547
|
+
def test_decoder_codec_available_invalid(self):
|
|
548
|
+
assert _avif.decoder_codec_available("foo") is False
|
|
549
|
+
|
|
550
|
+
@pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"])
|
|
551
|
+
def test_decoder_upsampling(self, upsampling):
|
|
552
|
+
AvifImagePlugin.CHROMA_UPSAMPLING = upsampling
|
|
553
|
+
try:
|
|
554
|
+
with Image.open(TEST_AVIF_FILE):
|
|
555
|
+
pass
|
|
556
|
+
finally:
|
|
557
|
+
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"
|
|
558
|
+
|
|
559
|
+
def test_decoder_upsampling_invalid(self):
|
|
560
|
+
AvifImagePlugin.CHROMA_UPSAMPLING = "foo"
|
|
561
|
+
try:
|
|
562
|
+
with pytest.raises(ValueError):
|
|
563
|
+
with Image.open(TEST_AVIF_FILE):
|
|
564
|
+
pass
|
|
565
|
+
finally:
|
|
566
|
+
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"
|
|
567
|
+
|
|
568
|
+
def test_p_mode_transparency(self):
|
|
569
|
+
im = Image.new("P", size=(64, 64))
|
|
570
|
+
draw = ImageDraw.Draw(im)
|
|
571
|
+
draw.rectangle(xy=[(0, 0), (32, 32)], fill=255)
|
|
572
|
+
draw.rectangle(xy=[(32, 32), (64, 64)], fill=255)
|
|
573
|
+
|
|
574
|
+
buf_png = BytesIO()
|
|
575
|
+
im.save(buf_png, format="PNG", transparency=0)
|
|
576
|
+
im_png = Image.open(buf_png)
|
|
577
|
+
buf_out = BytesIO()
|
|
578
|
+
im_png.save(buf_out, format="AVIF", quality=100)
|
|
579
|
+
|
|
580
|
+
assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1)
|
|
581
|
+
|
|
582
|
+
def test_decoder_strict_flags(self):
|
|
583
|
+
# This would fail if full avif strictFlags were enabled
|
|
584
|
+
with Image.open("%s/tests/images/chimera-missing-pixi.avif" % CURR_DIR) as im:
|
|
585
|
+
assert im.size == (480, 270)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class TestAvifAnimation:
|
|
589
|
+
@contextmanager
|
|
590
|
+
def star_frames(self):
|
|
591
|
+
with Image.open("%s/tests/images/star.png" % CURR_DIR) as f1:
|
|
592
|
+
with Image.open("%s/tests/images/star90.png" % CURR_DIR) as f2:
|
|
593
|
+
with Image.open("%s/tests/images/star180.png" % CURR_DIR) as f3:
|
|
594
|
+
with Image.open("%s/tests/images/star270.png" % CURR_DIR) as f4:
|
|
595
|
+
yield [f1, f2, f3, f4]
|
|
596
|
+
|
|
597
|
+
def test_n_frames(self):
|
|
598
|
+
"""
|
|
599
|
+
Ensure that AVIF format sets n_frames and is_animated attributes
|
|
600
|
+
correctly.
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
with Image.open("tests/images/hopper.avif") as im:
|
|
604
|
+
assert im.n_frames == 1
|
|
605
|
+
assert not im.is_animated
|
|
606
|
+
|
|
607
|
+
with Image.open("tests/images/star.avifs") as im:
|
|
608
|
+
assert im.n_frames == 5
|
|
609
|
+
assert im.is_animated
|
|
610
|
+
|
|
611
|
+
def test_write_animation_L(self, tmp_path):
|
|
612
|
+
"""
|
|
613
|
+
Convert an animated GIF to animated AVIF, then compare the frame
|
|
614
|
+
count, and first and last frames to ensure they're visually similar.
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
with Image.open("tests/images/star.gif") as orig:
|
|
618
|
+
assert orig.n_frames > 1
|
|
619
|
+
|
|
620
|
+
temp_file = str(tmp_path / "temp.avif")
|
|
621
|
+
orig.save(temp_file, save_all=True)
|
|
622
|
+
with Image.open(temp_file) as im:
|
|
623
|
+
assert im.n_frames == orig.n_frames
|
|
624
|
+
|
|
625
|
+
# Compare first and second-to-last frames to the original animated GIF
|
|
626
|
+
orig.load()
|
|
627
|
+
im.load()
|
|
628
|
+
assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0)
|
|
629
|
+
orig.seek(orig.n_frames - 2)
|
|
630
|
+
im.seek(im.n_frames - 2)
|
|
631
|
+
orig.load()
|
|
632
|
+
im.load()
|
|
633
|
+
assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0)
|
|
634
|
+
|
|
635
|
+
def test_write_animation_RGB(self, tmp_path):
|
|
636
|
+
"""
|
|
637
|
+
Write an animated AVIF from RGB frames, and ensure the frames
|
|
638
|
+
are visually similar to the originals.
|
|
639
|
+
"""
|
|
640
|
+
|
|
641
|
+
def check(temp_file):
|
|
642
|
+
with Image.open(temp_file) as im:
|
|
643
|
+
assert im.n_frames == 4
|
|
644
|
+
|
|
645
|
+
# Compare first frame to original
|
|
646
|
+
im.load()
|
|
647
|
+
assert_image_similar(im, frame1.convert("RGBA"), 25.0)
|
|
648
|
+
|
|
649
|
+
# Compare second frame to original
|
|
650
|
+
im.seek(1)
|
|
651
|
+
im.load()
|
|
652
|
+
assert_image_similar(im, frame2.convert("RGBA"), 25.0)
|
|
653
|
+
|
|
654
|
+
with self.star_frames() as frames:
|
|
655
|
+
frame1 = frames[0]
|
|
656
|
+
frame2 = frames[1]
|
|
657
|
+
temp_file1 = str(tmp_path / "temp.avif")
|
|
658
|
+
frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:])
|
|
659
|
+
check(temp_file1)
|
|
660
|
+
|
|
661
|
+
# Tests appending using a generator
|
|
662
|
+
def imGenerator(ims):
|
|
663
|
+
for im in ims:
|
|
664
|
+
yield im
|
|
665
|
+
|
|
666
|
+
temp_file2 = str(tmp_path / "temp_generator.avif")
|
|
667
|
+
frames[0].copy().save(
|
|
668
|
+
temp_file2,
|
|
669
|
+
save_all=True,
|
|
670
|
+
append_images=imGenerator(frames[1:]),
|
|
671
|
+
)
|
|
672
|
+
check(temp_file2)
|
|
673
|
+
|
|
674
|
+
def test_sequence_dimension_mismatch_check(self, tmp_path):
|
|
675
|
+
temp_file = str(tmp_path / "temp.avif")
|
|
676
|
+
frame1 = Image.new("RGB", (100, 100))
|
|
677
|
+
frame2 = Image.new("RGB", (150, 150))
|
|
678
|
+
with pytest.raises(ValueError):
|
|
679
|
+
frame1.save(temp_file, save_all=True, append_images=[frame2], duration=100)
|
|
680
|
+
|
|
681
|
+
def test_heif_raises_unidentified_image_error(self):
|
|
682
|
+
with pytest.raises(UnidentifiedImageError or IOError):
|
|
683
|
+
with Image.open("tests/images/rgba10.heif"):
|
|
684
|
+
pass
|
|
685
|
+
|
|
686
|
+
@skip_unless_avif_version_gte((0, 9, 0))
|
|
687
|
+
@pytest.mark.parametrize("alpha_premultipled", [False, True])
|
|
688
|
+
def test_alpha_premultiplied_true(self, alpha_premultipled):
|
|
689
|
+
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
|
|
690
|
+
im_buf = BytesIO()
|
|
691
|
+
im.save(im_buf, "AVIF", alpha_premultiplied=alpha_premultipled)
|
|
692
|
+
im_bytes = im_buf.getvalue()
|
|
693
|
+
assert has_alpha_premultiplied(im_bytes) is alpha_premultipled
|
|
694
|
+
|
|
695
|
+
def test_timestamp_and_duration(self, tmp_path):
|
|
696
|
+
"""
|
|
697
|
+
Try passing a list of durations, and make sure the encoded
|
|
698
|
+
timestamps and durations are correct.
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
durations = [1, 10, 20, 30, 40]
|
|
702
|
+
temp_file = str(tmp_path / "temp.avif")
|
|
703
|
+
with self.star_frames() as frames:
|
|
704
|
+
frames[0].save(
|
|
705
|
+
temp_file,
|
|
706
|
+
save_all=True,
|
|
707
|
+
append_images=(frames[1:] + [frames[0]]),
|
|
708
|
+
duration=durations,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
with Image.open(temp_file) as im:
|
|
712
|
+
assert im.n_frames == 5
|
|
713
|
+
assert im.is_animated
|
|
714
|
+
|
|
715
|
+
# Check that timestamps and durations match original values specified
|
|
716
|
+
ts = 0
|
|
717
|
+
for frame in range(im.n_frames):
|
|
718
|
+
im.seek(frame)
|
|
719
|
+
im.load()
|
|
720
|
+
assert im.info["duration"] == durations[frame]
|
|
721
|
+
assert im.info["timestamp"] == ts
|
|
722
|
+
ts += durations[frame]
|
|
723
|
+
|
|
724
|
+
def test_seeking(self, tmp_path):
|
|
725
|
+
"""
|
|
726
|
+
Create an animated AVIF file, and then try seeking through frames in
|
|
727
|
+
reverse-order, verifying the timestamps and durations are correct.
|
|
728
|
+
"""
|
|
729
|
+
|
|
730
|
+
dur = 33
|
|
731
|
+
temp_file = str(tmp_path / "temp.avif")
|
|
732
|
+
with self.star_frames() as frames:
|
|
733
|
+
frames[0].save(
|
|
734
|
+
temp_file,
|
|
735
|
+
save_all=True,
|
|
736
|
+
append_images=(frames[1:] + [frames[0]]),
|
|
737
|
+
duration=dur,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
with Image.open(temp_file) as im:
|
|
741
|
+
assert im.n_frames == 5
|
|
742
|
+
assert im.is_animated
|
|
743
|
+
|
|
744
|
+
# Traverse frames in reverse, checking timestamps and durations
|
|
745
|
+
ts = dur * (im.n_frames - 1)
|
|
746
|
+
for frame in reversed(range(im.n_frames)):
|
|
747
|
+
im.seek(frame)
|
|
748
|
+
im.load()
|
|
749
|
+
assert im.info["duration"] == dur
|
|
750
|
+
assert im.info["timestamp"] == ts
|
|
751
|
+
ts -= dur
|
|
752
|
+
|
|
753
|
+
def test_seek_errors(self):
|
|
754
|
+
with Image.open("tests/images/star.avifs") as im:
|
|
755
|
+
with pytest.raises(EOFError):
|
|
756
|
+
im.seek(-1)
|
|
757
|
+
|
|
758
|
+
with pytest.raises(EOFError):
|
|
759
|
+
im.seek(42)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
if hasattr(os, "sched_getaffinity"):
|
|
763
|
+
MAX_THREADS = len(os.sched_getaffinity(0))
|
|
764
|
+
else:
|
|
765
|
+
MAX_THREADS = cpu_count()
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class TestAvifLeaks(PillowLeakTestCase):
|
|
769
|
+
mem_limit = MAX_THREADS * 3 * 1024
|
|
770
|
+
iterations = 100
|
|
771
|
+
|
|
772
|
+
@pytest.mark.skipif(
|
|
773
|
+
is_docker_qemu(), reason="Skipping on cross-architecture containers"
|
|
774
|
+
)
|
|
775
|
+
def test_leak_load(self):
|
|
776
|
+
with open(TEST_AVIF_FILE, "rb") as f:
|
|
777
|
+
im_data = f.read()
|
|
778
|
+
|
|
779
|
+
def core():
|
|
780
|
+
with Image.open(BytesIO(im_data)) as im:
|
|
781
|
+
im.load()
|
|
782
|
+
|
|
783
|
+
self._test_leak(core)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pillow-avif-plugin-1.4.3 → pillow-avif-plugin-1.4.4}/src/pillow_avif_plugin.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|