diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 60ec46c41..ada335770 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -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" diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 83339e41d..0715a2d50 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -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 diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 85028b7ec..f34fed466 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -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: diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index ef4d30cbb..581a1c5aa 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -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 diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index c817b4c08..14c6f59f3 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -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 diff --git a/setup.py b/setup.py index c9f4340a8..21da1c714 100644 --- a/setup.py +++ b/setup.py @@ -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"), diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 8125b548a..09f0cd95a 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -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() diff --git a/src/_avif.c b/src/_avif.c index c50516710..c43fddd6a 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -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); diff --git a/wheels/dependency_licenses/RAV1E.txt b/wheels/dependency_licenses/RAV1E.txt index 4c6c3029a..3d6c825c4 100644 --- a/wheels/dependency_licenses/RAV1E.txt +++ b/wheels/dependency_licenses/RAV1E.txt @@ -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 diff --git a/winbuild/build.rst b/winbuild/build.rst index 96a19dc06..3c20c7d17 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -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. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4fe0e3c91..1d72edd32 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -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}"),