mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-03-13 09:15:46 +03:00
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:
parent
6cbad27c27
commit
19ba2dd6d7
2
.github/workflows/macos-install.sh
vendored
2
.github/workflows/macos-install.sh
vendored
|
@ -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"
|
||||||
|
|
5
.github/workflows/wheels-dependencies.sh
vendored
5
.github/workflows/wheels-dependencies.sh
vendored
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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"),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
111
src/_avif.c
111
src/_avif.c
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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}"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user