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 \ little-cms2 \
openjpeg \ openjpeg \
webp \ webp \
dav1d \
aom \ aom \
dav1d \
rav1e \ rav1e \
svt-av1 svt-av1
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"

View File

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

View File

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

View File

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

View File

@ -89,7 +89,6 @@ Many of Pillow's features require external libraries:
* **libxcb** provides X11 screengrab support. * **libxcb** provides X11 screengrab support.
* **libavif** provides support for the AVIF format. * **libavif** provides support for the AVIF format.
* Pillow requires libavif version **0.8.0** or greater, which is when * 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() PILLOW_VERSION = get_version()
AVIF_ROOT = None
FREETYPE_ROOT = None FREETYPE_ROOT = None
HARFBUZZ_ROOT = None HARFBUZZ_ROOT = None
FRIBIDI_ROOT = None FRIBIDI_ROOT = None
@ -481,6 +482,7 @@ class pil_build_ext(build_ext):
# #
# add configured kits # add configured kits
for root_name, lib_name in { for root_name, lib_name in {
"AVIF_ROOT": "avif",
"JPEG_ROOT": "libjpeg", "JPEG_ROOT": "libjpeg",
"JPEG2K_ROOT": "libopenjp2", "JPEG2K_ROOT": "libopenjp2",
"TIFF_ROOT": ("libtiff-5", "libtiff-4"), "TIFF_ROOT": ("libtiff-5", "libtiff-4"),

View File

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

View File

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

View File

@ -1,6 +1,6 @@
BSD 2-Clause License BSD 2-Clause License
Copyright (c) 2017-2021, the rav1e contributors Copyright (c) 2017-2023, the rav1e contributors
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without 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) build architecture (default: same as host Python)
--nmake build dependencies using NMake instead of Ninja --nmake build dependencies using NMake instead of Ninja
--no-imagequant skip GPL-licensed optional dependency libimagequant --no-imagequant skip GPL-licensed optional dependency libimagequant
--no-avif skip optional dependency libavif
--no-fribidi, --no-raqm --no-fribidi, --no-raqm
skip LGPL-licensed optional dependency FriBiDi skip LGPL-licensed optional dependency FriBiDi
--no-avif skip optional dependency libavif
Arguments can also be supplied using the environment variables PILLOW_BUILD, Arguments can also be supplied using the environment variables PILLOW_BUILD,
PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information.

View File

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