Use rgb.rowBytes in overflow check (#18)

* Removed skip_unless_feature on methods when class is already skipped

* Test speed less than slowest and greater than fastest

* Updated type hints

* Only access angle when AVIF_TRANSFORM_IROT flag is present

* Added AVIF_ROOT

* Only define normalize_quantize_value if it will be used

* Build libavif after libjpeg

* Use rgb.rowBytes in overflow check

* Group EXIF info

* Removed __loaded

* If brew is not installed, use /usr prefix

* Sort AVIF codecs alphabetically

* Updated rav1e license

* Fixed catching warning, as per #8505

* Simplified code

* Fixed typos

* Test further scenarios

* Use y* to parse bytes

---------

Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
This commit is contained in:
Andrew Murray 2025-02-03 12:03:13 +11:00 committed by GitHub
parent 6cbad27c27
commit 19ba2dd6d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 137 additions and 176 deletions

View File

@ -15,8 +15,8 @@ brew install \
little-cms2 \
openjpeg \
webp \
dav1d \
aom \
dav1d \
rav1e \
svt-av1
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"

View File

@ -125,9 +125,9 @@ function build_libavif {
-DBUILD_SHARED_LIBS=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_RAV1E=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \
-DAVIF_CODEC_DAV1D=LOCAL \
-DAVIF_CODEC_RAV1E=LOCAL \
-DAVIF_CODEC_SVT=LOCAL \
-DENABLE_NASM=ON \
-DCMAKE_MODULE_PATH=/tmp/cmake/Modules \
@ -143,8 +143,6 @@ function build {
fi
build_zlib_ng
build_libavif
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
@ -168,6 +166,7 @@ function build {
build_tiff
fi
build_libavif
build_libpng
build_lcms2
build_openjpeg

View File

@ -4,7 +4,7 @@ import gc
import os
import re
import warnings
from collections.abc import Generator
from collections.abc import Generator, Sequence
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
@ -70,8 +70,7 @@ def is_docker_qemu() -> bool:
init_proc_exe = os.readlink("/proc/1/exe")
except (FileNotFoundError, PermissionError):
return False
else:
return "qemu" in init_proc_exe
return "qemu" in init_proc_exe
class TestUnsupportedAvif:
@ -116,36 +115,32 @@ class TestFileAvif:
image, "Tests/images/avif/hopper_avif_write.png", 11.5
)
def _roundtrip(self, tmp_path: Path, mode: str, epsilon: float) -> None:
temp_file = str(tmp_path / "temp.avif")
hopper(mode).save(temp_file)
with Image.open(temp_file) as image:
assert image.mode == "RGB"
assert image.size == (128, 128)
assert image.format == "AVIF"
image.getdata()
if mode == "RGB":
# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
image, "Tests/images/avif/hopper_avif_write.png", 6.02
)
# This test asserts that the images are similar. If the average pixel
# difference between the two images is less than the epsilon value,
# then we're going to accept that it's a reasonable lossy version of
# the image.
expected = hopper()
assert_image_similar(image, expected, epsilon)
def test_write_rgb(self, tmp_path: Path) -> None:
"""
Can we write a RGB mode file to avif without error?
Does it have the bits we expect?
"""
self._roundtrip(tmp_path, "RGB", 8.62)
temp_file = str(tmp_path / "temp.avif")
im = hopper()
im.save(temp_file)
with Image.open(temp_file) as reloaded:
assert reloaded.mode == "RGB"
assert reloaded.size == (128, 128)
assert reloaded.format == "AVIF"
reloaded.getdata()
# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02
)
# This test asserts that the images are similar. If the average pixel
# difference between the two images is less than the epsilon value,
# then we're going to accept that it's a reasonable lossy version of
# the image.
assert_image_similar(reloaded, im, 8.62)
def test_AvifEncoder_with_invalid_args(self) -> None:
"""
@ -186,9 +181,10 @@ class TestFileAvif:
def test_no_resource_warning(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
temp_file = str(tmp_path / "temp.avif")
with warnings.catch_warnings():
im.save(temp_file)
warnings.simplefilter("error")
im.save(tmp_path / "temp.avif")
@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
def test_accept_ftyp_brands(self, major_brand: bytes) -> None:
@ -293,7 +289,8 @@ class TestFileAvif:
exif = im.getexif()
assert exif[274] == 3
@pytest.mark.parametrize("use_bytes, orientation", [(True, 1), (False, 2)])
@pytest.mark.parametrize("use_bytes", [True, False])
@pytest.mark.parametrize("orientation", [1, 2])
def test_exif_save(
self,
tmp_path: Path,
@ -313,7 +310,7 @@ class TestFileAvif:
else:
assert reloaded.info["exif"] == exif_data
def test_exif_without_orientation(self, tmp_path: Path):
def test_exif_without_orientation(self, tmp_path: Path) -> None:
exif = Image.Exif()
exif[272] = b"test"
exif_data = exif.tobytes()
@ -347,17 +344,13 @@ class TestFileAvif:
self, rot: int, mir: int, exif_orientation: int, tmp_path: Path
) -> None:
with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im:
exif = im.info["exif"]
exif = im.getexif()
assert exif[274] == exif_orientation
test_file = str(tmp_path / "temp.avif")
im.save(test_file, exif=exif)
exif_data = Image.Exif()
exif_data.load(exif)
assert exif_data[274] == exif_orientation
with Image.open(test_file) as reloaded:
exif_data = Image.Exif()
exif_data.load(reloaded.info["exif"])
assert exif_data[274] == exif_orientation
assert reloaded.getexif()[274] == exif_orientation
def test_xmp(self) -> None:
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
@ -397,7 +390,7 @@ class TestFileAvif:
with pytest.raises(EOFError):
im.seek(1)
@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:0:0"])
@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"])
def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
@ -409,10 +402,11 @@ class TestFileAvif:
with pytest.raises(ValueError):
im.save(test_file, subsampling="foo")
def test_encoder_range(self, tmp_path: Path) -> None:
@pytest.mark.parametrize("value", ["full", "limited"])
def test_encoder_range(self, tmp_path: Path, value: str) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, range="limited")
im.save(test_file, range=value)
def test_encoder_range_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
@ -421,7 +415,6 @@ class TestFileAvif:
im.save(test_file, range="foo")
@skip_unless_avif_encoder("aom")
@skip_unless_feature("avif")
def test_encoder_codec_param(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
@ -434,15 +427,13 @@ class TestFileAvif:
im.save(test_file, codec="foo")
@skip_unless_avif_decoder("dav1d")
@skip_unless_feature("avif")
def test_encoder_codec_cannot_encode(self, tmp_path: Path) -> None:
def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, codec="dav1d")
@skip_unless_avif_encoder("aom")
@skip_unless_feature("avif")
@pytest.mark.parametrize(
"advanced",
[
@ -451,10 +442,11 @@ class TestFileAvif:
"enable-chroma-deltaq": "1",
},
(("aq-mode", "1"), ("enable-chroma-deltaq", "1")),
[("aq-mode", "1"), ("enable-chroma-deltaq", "1")],
],
)
def test_encoder_advanced_codec_options(
self, advanced: dict[str, str] | tuple[tuple[str, str], ...]
self, advanced: dict[str, str] | Sequence[tuple[str, str]]
) -> None:
with Image.open(TEST_AVIF_FILE) as im:
ctrl_buf = BytesIO()
@ -469,7 +461,6 @@ class TestFileAvif:
assert ctrl_buf.getvalue() != test_buf.getvalue()
@skip_unless_avif_encoder("aom")
@skip_unless_feature("avif")
@pytest.mark.parametrize("advanced", [{"foo": "bar"}, 1234])
def test_encoder_advanced_codec_options_invalid(
self, tmp_path: Path, advanced: dict[str, str] | int
@ -480,7 +471,6 @@ class TestFileAvif:
im.save(test_file, codec="aom", advanced=advanced)
@skip_unless_avif_decoder("aom")
@skip_unless_feature("avif")
def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom")
@ -488,8 +478,7 @@ class TestFileAvif:
assert im.size == (128, 128)
@skip_unless_avif_encoder("rav1e")
@skip_unless_feature("avif")
def test_decoder_codec_cannot_decode(
def test_encoder_codec_cannot_decode(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e")
@ -506,7 +495,6 @@ class TestFileAvif:
pass
@skip_unless_avif_encoder("aom")
@skip_unless_feature("avif")
def test_encoder_codec_available(self) -> None:
assert _avif.encoder_codec_available("aom") is True
@ -515,7 +503,6 @@ class TestFileAvif:
_avif.encoder_codec_available()
@skip_unless_avif_decoder("dav1d")
@skip_unless_feature("avif")
def test_encoder_codec_available_cannot_decode(self) -> None:
assert _avif.encoder_codec_available("dav1d") is False
@ -529,7 +516,6 @@ class TestFileAvif:
im.save(test_file, quality="invalid")
@skip_unless_avif_decoder("aom")
@skip_unless_feature("avif")
def test_decoder_codec_available(self) -> None:
assert _avif.decoder_codec_available("aom") is True
@ -538,7 +524,6 @@ class TestFileAvif:
_avif.decoder_codec_available()
@skip_unless_avif_encoder("rav1e")
@skip_unless_feature("avif")
def test_decoder_codec_available_cannot_decode(self) -> None:
assert _avif.decoder_codec_available("rav1e") is False
@ -566,9 +551,10 @@ class TestFileAvif:
assert im.size == (480, 270)
@skip_unless_avif_encoder("aom")
def test_aom_optimizations(self, tmp_path: Path) -> None:
@pytest.mark.parametrize("speed", [-1, 1, 11])
def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None:
test_file = str(tmp_path / "temp.avif")
hopper().save(test_file, codec="aom", speed=1)
hopper().save(test_file, codec="aom", speed=speed)
@skip_unless_avif_encoder("svt")
def test_svt_optimizations(self, tmp_path: Path) -> None:
@ -579,7 +565,7 @@ class TestFileAvif:
@skip_unless_feature("avif")
class TestAvifAnimation:
@contextmanager
def star_frames(self) -> Generator[list[ImageFile.ImageFile], None, None]:
def star_frames(self) -> Generator[list[Image.Image], None, None]:
with Image.open("Tests/images/avif/star.png") as f:
yield [f, f.rotate(90), f.rotate(180), f.rotate(270)]
@ -600,24 +586,25 @@ class TestAvifAnimation:
def test_write_animation_P(self, tmp_path: Path) -> None:
"""
Convert an animated GIF to animated AVIF, then compare the frame
count, and first and second-to-last frames to ensure they're visually similar.
count, and ensure the frames are visually similar to the originals.
"""
with Image.open("Tests/images/avif/star.gif") as orig:
assert orig.n_frames > 1
with Image.open("Tests/images/avif/star.gif") as original:
assert original.n_frames > 1
temp_file = str(tmp_path / "temp.avif")
orig.save(temp_file, save_all=True)
original.save(temp_file, save_all=True)
with Image.open(temp_file) as im:
assert im.n_frames == orig.n_frames
assert im.n_frames == original.n_frames
# Compare first frame in P mode to frame from original GIF
assert_image_similar(im, orig.convert("RGBA"), 2)
assert_image_similar(im, original.convert("RGBA"), 2)
# Compare second-to-last frame in RGBA mode to frame from original GIF
orig.seek(orig.n_frames - 2)
im.seek(im.n_frames - 2)
assert_image_similar(im, orig, 2.54)
# Compare later frames in RGBA mode to frames from original GIF
for frame in range(1, original.n_frames):
original.seek(frame)
im.seek(frame)
assert_image_similar(im, original, 2.54)
def test_write_animation_RGBA(self, tmp_path: Path) -> None:
"""
@ -645,8 +632,8 @@ class TestAvifAnimation:
# Test appending using a generator
def imGenerator(
ims: list[ImageFile.ImageFile],
) -> Generator[ImageFile.ImageFile, None, None]:
ims: list[Image.Image],
) -> Generator[Image.Image, None, None]:
yield from ims
temp_file2 = str(tmp_path / "temp_generator.avif")
@ -703,13 +690,13 @@ class TestAvifAnimation:
assert im.is_animated
# Check that timestamps and durations match original values specified
ts = 0
timestamp = 0
for frame in range(im.n_frames):
im.seek(frame)
im.load()
assert im.info["duration"] == durations[frame]
assert im.info["timestamp"] == ts
ts += durations[frame]
assert im.info["timestamp"] == timestamp
timestamp += durations[frame]
def test_seeking(self, tmp_path: Path) -> None:
"""
@ -717,14 +704,14 @@ class TestAvifAnimation:
reverse-order, verifying the timestamps and durations are correct.
"""
dur = 33
duration = 33
temp_file = str(tmp_path / "temp.avif")
with self.star_frames() as frames:
frames[0].save(
temp_file,
save_all=True,
append_images=(frames[1:] + [frames[0]]),
duration=dur,
duration=duration,
)
with Image.open(temp_file) as im:
@ -732,13 +719,13 @@ class TestAvifAnimation:
assert im.is_animated
# Traverse frames in reverse, checking timestamps and durations
ts = dur * (im.n_frames - 1)
timestamp = duration * (im.n_frames - 1)
for frame in reversed(range(im.n_frames)):
im.seek(frame)
im.load()
assert im.info["duration"] == dur
assert im.info["timestamp"] == ts
ts -= dur
assert im.info["duration"] == duration
assert im.info["timestamp"] == timestamp
timestamp -= duration
def test_seek_errors(self) -> None:
with Image.open("Tests/images/avif/star.avifs") as im:

View File

@ -7,7 +7,7 @@ version=1.1.1
pushd libavif-$version
if [ $(uname) == "Darwin" ]; then
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
PREFIX=$(brew --prefix)
else
PREFIX=/usr
@ -19,11 +19,22 @@ LIBAVIF_CMAKE_FLAGS=()
HAS_DECODER=0
HAS_ENCODER=0
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
@ -34,17 +45,6 @@ if $PKGCONFIG --exists SvtAv1Enc; then
HAS_ENCODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi

View File

@ -89,7 +89,6 @@ Many of Pillow's features require external libraries:
* **libxcb** provides X11 screengrab support.
* **libavif** provides support for the AVIF format.
* Pillow requires libavif version **0.8.0** or greater, which is when

View File

@ -32,6 +32,7 @@ configuration: dict[str, list[str]] = {}
PILLOW_VERSION = get_version()
AVIF_ROOT = None
FREETYPE_ROOT = None
HARFBUZZ_ROOT = None
FRIBIDI_ROOT = None
@ -481,6 +482,7 @@ class pil_build_ext(build_ext):
#
# add configured kits
for root_name, lib_name in {
"AVIF_ROOT": "avif",
"JPEG_ROOT": "libjpeg",
"JPEG2K_ROOT": "libopenjp2",
"TIFF_ROOT": ("libtiff-5", "libtiff-4"),

View File

@ -45,7 +45,7 @@ def _accept(prefix: bytes) -> bool | str:
return False
def _get_default_max_threads():
def _get_default_max_threads() -> int:
if DEFAULT_MAX_THREADS:
return DEFAULT_MAX_THREADS
if hasattr(os, "sched_getaffinity"):
@ -57,8 +57,7 @@ def _get_default_max_threads():
class AvifImageFile(ImageFile.ImageFile):
format = "AVIF"
format_description = "AVIF image"
__loaded = -1
__frame = 0
__frame = -1
def _open(self) -> None:
if not SUPPORTED:
@ -80,7 +79,7 @@ class AvifImageFile(ImageFile.ImageFile):
)
# Get info from decoder
width, height, n_frames, mode, icc, exif, xmp, exif_orientation = (
width, height, n_frames, mode, icc, exif, exif_orientation, xmp = (
self._decoder.get_info()
)
self._size = width, height
@ -105,28 +104,28 @@ class AvifImageFile(ImageFile.ImageFile):
exif = exif_data.tobytes()
if exif:
self.info["exif"] = exif
self.seek(0)
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
self.__frame = frame
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
def load(self) -> Image.core.PixelAccess | None:
if self.__loaded != self.__frame:
if self.tile:
# We need to load the image data for this frame
data, timescale, tsp_in_ts, dur_in_ts = self._decoder.get_frame(
self.__frame
)
self.info["timestamp"] = round(1000 * (tsp_in_ts / timescale))
self.info["duration"] = round(1000 * (dur_in_ts / timescale))
self.__loaded = self.__frame
# Set tile
if self.fp and self._exclusive_fp:
self.fp.close()
self.fp = BytesIO(data)
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
return super().load()

View File

@ -7,9 +7,6 @@
typedef struct {
PyObject_HEAD avifEncoder *encoder;
avifImage *image;
PyObject *icc_bytes;
PyObject *exif_bytes;
PyObject *xmp_bytes;
int frame_index;
} AvifEncoderObject;
@ -18,12 +15,13 @@ static PyTypeObject AvifEncoder_Type;
// Decoder type
typedef struct {
PyObject_HEAD avifDecoder *decoder;
PyObject *data;
Py_buffer buffer;
char *mode;
} AvifDecoderObject;
static PyTypeObject AvifDecoder_Type;
#if AVIF_VERSION < 1000000
static int
normalize_quantize_value(int qvalue) {
if (qvalue < AVIF_QUANTIZER_BEST_QUALITY) {
@ -34,6 +32,7 @@ normalize_quantize_value(int qvalue) {
return qvalue;
}
}
#endif
static int
normalize_tiles_log2(int value) {
@ -70,10 +69,10 @@ irot_imir_to_exif_orientation(const avifImage *image) {
#else
axis = image->imir.mode;
#endif
uint8_t angle = image->irot.angle;
int imir = image->transformFlags & AVIF_TRANSFORM_IMIR;
int irot = image->transformFlags & AVIF_TRANSFORM_IROT;
if (irot) {
uint8_t angle = image->irot.angle;
if (angle == 1) {
if (imir) {
return axis ? 7 // 90 degrees anti-clockwise then swap left and right.
@ -238,9 +237,9 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
int speed;
int exif_orientation;
int max_threads;
PyObject *icc_bytes;
PyObject *exif_bytes;
PyObject *xmp_bytes;
Py_buffer icc_buffer;
Py_buffer exif_buffer;
Py_buffer xmp_buffer;
PyObject *alpha_premultiplied;
PyObject *autotiling;
int tile_rows_log2;
@ -253,7 +252,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
if (!PyArg_ParseTuple(
args,
"IIsiiissiiOOSSiSO",
"IIsiiissiiOOy*y*iy*O",
&width,
&height,
&subsampling,
@ -266,10 +265,10 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
&tile_cols_log2,
&alpha_premultiplied,
&autotiling,
&icc_bytes,
&exif_bytes,
&icc_buffer,
&exif_buffer,
&exif_orientation,
&xmp_bytes,
&xmp_buffer,
&advanced
)) {
return NULL;
@ -374,19 +373,10 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
return NULL;
}
self->frame_index = -1;
self->icc_bytes = NULL;
self->exif_bytes = NULL;
self->xmp_bytes = NULL;
avifResult result;
Py_ssize_t size = PyBytes_GET_SIZE(icc_bytes);
if (size) {
self->icc_bytes = icc_bytes;
Py_INCREF(icc_bytes);
result = avifImageSetProfileICC(
image, (uint8_t *)PyBytes_AS_STRING(icc_bytes), size
);
if (icc_buffer.len) {
result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),
@ -395,7 +385,9 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
);
avifImageDestroy(image);
avifEncoderDestroy(encoder);
Py_XDECREF(self->icc_bytes);
PyBuffer_Release(&icc_buffer);
PyBuffer_Release(&exif_buffer);
PyBuffer_Release(&xmp_buffer);
PyObject_Del(self);
return NULL;
}
@ -408,15 +400,10 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
}
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
PyBuffer_Release(&icc_buffer);
size = PyBytes_GET_SIZE(exif_bytes);
if (size) {
self->exif_bytes = exif_bytes;
Py_INCREF(exif_bytes);
result = avifImageSetMetadataExif(
image, (uint8_t *)PyBytes_AS_STRING(exif_bytes), size
);
if (exif_buffer.len) {
result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),
@ -425,21 +412,16 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
);
avifImageDestroy(image);
avifEncoderDestroy(encoder);
Py_XDECREF(self->icc_bytes);
Py_XDECREF(self->exif_bytes);
PyBuffer_Release(&exif_buffer);
PyBuffer_Release(&xmp_buffer);
PyObject_Del(self);
return NULL;
}
}
PyBuffer_Release(&exif_buffer);
size = PyBytes_GET_SIZE(xmp_bytes);
if (size) {
self->xmp_bytes = xmp_bytes;
Py_INCREF(xmp_bytes);
result = avifImageSetMetadataXMP(
image, (uint8_t *)PyBytes_AS_STRING(xmp_bytes), size
);
if (xmp_buffer.len) {
result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),
@ -448,13 +430,13 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
);
avifImageDestroy(image);
avifEncoderDestroy(encoder);
Py_XDECREF(self->icc_bytes);
Py_XDECREF(self->exif_bytes);
Py_XDECREF(self->xmp_bytes);
PyBuffer_Release(&xmp_buffer);
PyObject_Del(self);
return NULL;
}
}
PyBuffer_Release(&xmp_buffer);
if (exif_orientation > 1) {
exif_orientation_to_irot_imir(image, exif_orientation);
}
@ -473,9 +455,6 @@ _encoder_dealloc(AvifEncoderObject *self) {
if (self->image) {
avifImageDestroy(self->image);
}
Py_XDECREF(self->icc_bytes);
Py_XDECREF(self->exif_bytes);
Py_XDECREF(self->xmp_bytes);
Py_RETURN_NONE;
}
@ -662,7 +641,7 @@ _encoder_finish(AvifEncoderObject *self) {
// Decoder functions
PyObject *
AvifDecoderNew(PyObject *self_, PyObject *args) {
PyObject *avif_bytes;
Py_buffer buffer;
AvifDecoderObject *self = NULL;
avifDecoder *decoder;
@ -672,7 +651,7 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
avifResult result;
if (!PyArg_ParseTuple(args, "Ssi", &avif_bytes, &codec_str, &max_threads)) {
if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) {
return NULL;
}
@ -685,12 +664,10 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type);
if (!self) {
PyErr_SetString(PyExc_RuntimeError, "could not create decoder object");
PyBuffer_Release(&buffer);
return NULL;
}
Py_INCREF(avif_bytes);
self->data = avif_bytes;
decoder = avifDecoderCreate();
#if AVIF_VERSION >= 80400
decoder->maxThreads = max_threads;
@ -705,9 +682,7 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
#endif
decoder->codecChoice = codec;
result = avifDecoderSetIOMemory(
decoder, (uint8_t *)PyBytes_AS_STRING(self->data), PyBytes_GET_SIZE(self->data)
);
result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),
@ -715,6 +690,7 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
avifResultToString(result)
);
avifDecoderDestroy(decoder);
PyBuffer_Release(&buffer);
PyObject_Del(self);
return NULL;
}
@ -727,6 +703,7 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
avifResultToString(result)
);
avifDecoderDestroy(decoder);
PyBuffer_Release(&buffer);
PyObject_Del(self);
return NULL;
}
@ -738,6 +715,7 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
}
self->decoder = decoder;
self->buffer = buffer;
return (PyObject *)self;
}
@ -747,7 +725,7 @@ _decoder_dealloc(AvifDecoderObject *self) {
if (self->decoder) {
avifDecoderDestroy(self->decoder);
}
Py_XDECREF(self->data);
PyBuffer_Release(&self->buffer);
Py_RETURN_NONE;
}
@ -775,15 +753,15 @@ _decoder_get_info(AvifDecoderObject *self) {
}
ret = Py_BuildValue(
"IIIsSSSI",
"IIIsSSIS",
image->width,
image->height,
decoder->imageCount,
self->mode,
NULL == icc ? Py_None : icc,
NULL == exif ? Py_None : exif,
NULL == xmp ? Py_None : xmp,
irot_imir_to_exif_orientation(image)
irot_imir_to_exif_orientation(image),
NULL == xmp ? Py_None : xmp
);
Py_XDECREF(xmp);
@ -803,7 +781,6 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
avifDecoder *decoder;
avifImage *image;
uint32_t frame_index;
uint32_t row_bytes;
decoder = self->decoder;
@ -836,13 +813,6 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
rgb.ignoreAlpha = AVIF_TRUE;
}
row_bytes = rgb.width * avifRGBImagePixelSize(&rgb);
if (rgb.height > PY_SSIZE_T_MAX / row_bytes) {
PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
return NULL;
}
result = avifRGBImageAllocatePixels(&rgb);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
@ -867,6 +837,11 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
return NULL;
}
if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) {
PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
return NULL;
}
size = rgb.rowBytes * rgb.height;
bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size);

View File

@ -1,6 +1,6 @@
BSD 2-Clause License
Copyright (c) 2017-2021, the rav1e contributors
Copyright (c) 2017-2023, the rav1e contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@ -59,9 +59,9 @@ Run ``build_prepare.py`` to configure the build::
build architecture (default: same as host Python)
--nmake build dependencies using NMake instead of Ninja
--no-imagequant skip GPL-licensed optional dependency libimagequant
--no-avif skip optional dependency libavif
--no-fribidi, --no-raqm
skip LGPL-licensed optional dependency FriBiDi
--no-avif skip optional dependency libavif
Arguments can also be supplied using the environment variables PILLOW_BUILD,
PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information.

View File

@ -396,11 +396,11 @@ DEPS: dict[str, dict[str, Any]] = {
*cmds_cmake(
"avif_static",
"-DBUILD_SHARED_LIBS=OFF",
"-DAVIF_CODEC_AOM=LOCAL",
"-DAVIF_LIBYUV=LOCAL",
"-DAVIF_LIBSHARPYUV=LOCAL",
"-DAVIF_CODEC_RAV1E=LOCAL",
"-DAVIF_LIBYUV=LOCAL",
"-DAVIF_CODEC_AOM=LOCAL",
"-DAVIF_CODEC_DAV1D=LOCAL",
"-DAVIF_CODEC_RAV1E=LOCAL",
"-DAVIF_CODEC_SVT=LOCAL",
),
cmd_xcopy("include", "{inc_dir}"),