diff --git a/.ci/install.sh b/.ci/install.sh index e85e6bdc575..f24768d788e 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -23,7 +23,8 @@ if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ - sway wl-clipboard libopenblas-dev + sway wl-clipboard libopenblas-dev\ + ninja-build build-essential nasm fi python3 -m pip install --upgrade pip @@ -62,6 +63,9 @@ if [[ $(uname) != CYGWIN* ]]; then # raqm pushd depends && ./install_raqm.sh && popd + # libavif + pushd depends && ./install_libavif.sh && popd + # extra test images pushd depends && ./install_extra_test_images.sh && popd else diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index ddb4212301d..4e54f49922f 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -13,7 +13,11 @@ brew install \ libtiff \ little-cms2 \ openjpeg \ - webp + webp \ + dav1d \ + aom \ + rav1e \ + ninja if [[ "$ImageOS" == "macos13" ]]; then brew install --ignore-dependencies libraqm else @@ -31,5 +35,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install numpy +# libavif +pushd depends && ./install_libavif.sh && popd + # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a1d6ba61c9b..5db9140675d 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -63,6 +63,7 @@ jobs: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libavif \ mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d905a392585..1430e91e137 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -139,6 +139,18 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" + - name: Build dependencies / rav1e + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_rav1e.cmd" + + - name: Build dependencies / meson + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\install_meson.cmd" + + - name: Build dependencies / libavif + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libavif.cmd" + # for FreeType WOFF2 font support - name: Build dependencies / brotli if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af16e..b1154f23bb1 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -25,7 +25,7 @@ else MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} fi -PLAT=$CIBW_ARCHS +PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" # Define custom utilities source wheels/multibuild/common_utils.sh @@ -59,6 +59,8 @@ LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +LIBAVIF_VERSION=1.1.1 +RAV1E_VERSION=0.7.1 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi @@ -96,6 +98,75 @@ function build_harfbuzz { touch harfbuzz-stamp } +function install_rav1e { + if [ -n "$IS_MACOS" ]; then + suffix="macos" + if [[ "$PLAT" == "arm64" ]]; then + suffix+="-aarch64" + fi + else + suffix="linux" + if [[ "$PLAT" == "aarch64" ]]; then + suffix+="-aarch64" + else + suffix+="-generic" + fi + fi + + curl -sLo - \ + https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/librav1e-$RAV1E_VERSION-$suffix.tar.gz \ + | tar -C $BUILD_PREFIX --exclude LICENSE --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf - + + if [ -z "$IS_MACOS" ]; then + sed -i 's/-lgcc_s/-lgcc_eh/g' "${BUILD_PREFIX}/lib/pkgconfig/rav1e.pc" + fi + + # Force libavif to treat system rav1e as if it were local + mkdir -p /tmp/cmake/Modules + cat < /tmp/cmake/Modules/Findrav1e.cmake + add_library(rav1e::rav1e STATIC IMPORTED GLOBAL) + set_target_properties(rav1e::rav1e PROPERTIES + IMPORTED_LOCATION "$BUILD_PREFIX/lib/librav1e.a" + AVIF_LOCAL ON + INTERFACE_INCLUDE_DIRECTORIES "$BUILD_PREFIX/include/rav1e" + ) +EOF +} + +function build_libavif { + install_rav1e + python3 -m pip install meson ninja + + if [[ "$PLAT" == "x86_64" ]]; then + build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/ + fi + + local cmake=$(get_modern_cmake) + local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) + + (cd $out_dir \ + && $cmake \ + -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + -DAVIF_CODEC_RAV1E=SYSTEM \ + -DAVIF_CODEC_AOM=LOCAL \ + -DAVIF_CODEC_DAV1D=LOCAL \ + -DAVIF_CODEC_SVT=LOCAL \ + -DENABLE_NASM=ON \ + -DCMAKE_MODULE_PATH=/tmp/cmake/Modules \ + . \ + && make install) + + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libavif.a /usr/local/lib + cp /usr/local/lib64/pkgconfig/libavif.pc /usr/local/lib/pkgconfig + fi +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then @@ -103,6 +174,13 @@ function build { fi build_new_zlib + ORIGINAL_LDFLAGS=$LDFLAGS + if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then + LDFLAGS="${LDFLAGS} -ld64" + fi + build_libavif + LDFLAGS=$ORIGINAL_LDFLAGS + 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 diff --git a/Tests/check_avif_leaks.py b/Tests/check_avif_leaks.py new file mode 100644 index 00000000000..343349dd6a1 --- /dev/null +++ b/Tests/check_avif_leaks.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from io import BytesIO + +import pytest + +from PIL import Image + +from .helper import is_win32, skip_unless_feature + +# Limits for testing the leak +mem_limit = 1024 * 1048576 +stack_size = 8 * 1048576 +iterations = int((mem_limit / stack_size) * 2) +test_file = "Tests/images/avif/hopper.avif" + +pytestmark = [ + pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), + skip_unless_feature("avif"), +] + + +def test_leak_load() -> None: + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + + +def test_leak_save() -> None: + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + test_output = BytesIO() + with Image.open(test_file) as im: + im.save(test_output, "AVIF") + test_output.seek(0) + test_output.read() diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f58a..002dccde62a 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,12 +1,14 @@ from __future__ import annotations +import platform +import struct import sys from PIL import features def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} # tkinter is not available in cibuildwheel installed CPython on Windows try: @@ -16,6 +18,11 @@ def test_wheel_modules() -> None: except ImportError: expected_modules.remove("tkinter") + # libavif is not available on windows for x86 and ARM64 architectures + if sys.platform == "win32": + if platform.machine() == "ARM64" or struct.calcsize("P") == 4: + expected_modules.remove("avif") + assert set(features.get_supported_modules()) == expected_modules diff --git a/Tests/images/avif/chimera-missing-pixi.avif b/Tests/images/avif/chimera-missing-pixi.avif new file mode 100644 index 00000000000..4039547d574 Binary files /dev/null and b/Tests/images/avif/chimera-missing-pixi.avif differ diff --git a/Tests/images/avif/exif.avif b/Tests/images/avif/exif.avif new file mode 100644 index 00000000000..07964487f3c Binary files /dev/null and b/Tests/images/avif/exif.avif differ diff --git a/Tests/images/avif/hopper.avif b/Tests/images/avif/hopper.avif new file mode 100644 index 00000000000..87e4394f059 Binary files /dev/null and b/Tests/images/avif/hopper.avif differ diff --git a/Tests/images/avif/hopper_avif_write.png b/Tests/images/avif/hopper_avif_write.png new file mode 100644 index 00000000000..a47a0562bbc Binary files /dev/null and b/Tests/images/avif/hopper_avif_write.png differ diff --git a/Tests/images/avif/icc_profile.avif b/Tests/images/avif/icc_profile.avif new file mode 100644 index 00000000000..658cfec176e Binary files /dev/null and b/Tests/images/avif/icc_profile.avif differ diff --git a/Tests/images/avif/icc_profile_none.avif b/Tests/images/avif/icc_profile_none.avif new file mode 100644 index 00000000000..c73e70a3a52 Binary files /dev/null and b/Tests/images/avif/icc_profile_none.avif differ diff --git a/Tests/images/avif/rgba10.heif b/Tests/images/avif/rgba10.heif new file mode 100644 index 00000000000..8429a8b01ea Binary files /dev/null and b/Tests/images/avif/rgba10.heif differ diff --git a/Tests/images/avif/star.avifs b/Tests/images/avif/star.avifs new file mode 100644 index 00000000000..f2753395f8c Binary files /dev/null and b/Tests/images/avif/star.avifs differ diff --git a/Tests/images/avif/star.gif b/Tests/images/avif/star.gif new file mode 100644 index 00000000000..52076cafdd8 Binary files /dev/null and b/Tests/images/avif/star.gif differ diff --git a/Tests/images/avif/star.png b/Tests/images/avif/star.png new file mode 100644 index 00000000000..468dcde005d Binary files /dev/null and b/Tests/images/avif/star.png differ diff --git a/Tests/images/avif/star180.png b/Tests/images/avif/star180.png new file mode 100644 index 00000000000..2c5f5222115 Binary files /dev/null and b/Tests/images/avif/star180.png differ diff --git a/Tests/images/avif/star270.png b/Tests/images/avif/star270.png new file mode 100644 index 00000000000..8812b9bdeb8 Binary files /dev/null and b/Tests/images/avif/star270.png differ diff --git a/Tests/images/avif/star90.png b/Tests/images/avif/star90.png new file mode 100644 index 00000000000..93526260bab Binary files /dev/null and b/Tests/images/avif/star90.png differ diff --git a/Tests/images/avif/transparency.avif b/Tests/images/avif/transparency.avif new file mode 100644 index 00000000000..f808357fc7c Binary files /dev/null and b/Tests/images/avif/transparency.avif differ diff --git a/Tests/images/avif/xmp_tags_orientation.avif b/Tests/images/avif/xmp_tags_orientation.avif new file mode 100644 index 00000000000..41faa60501b Binary files /dev/null and b/Tests/images/avif/xmp_tags_orientation.avif differ diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py new file mode 100644 index 00000000000..32dd694f580 --- /dev/null +++ b/Tests/test_file_avif.py @@ -0,0 +1,778 @@ +from __future__ import annotations + +import gc +import os +import re +import warnings +from collections.abc import Generator +from contextlib import contextmanager +from io import BytesIO +from pathlib import Path +from struct import unpack +from typing import Any + +import pytest + +from PIL import ( + AvifImagePlugin, + Image, + ImageDraw, + ImageFile, + UnidentifiedImageError, + features, +) + +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +try: + from PIL import _avif + + HAVE_AVIF = True +except ImportError: + HAVE_AVIF = False + + +TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" + + +def assert_xmp_orientation(xmp: bytes, expected: int) -> None: + assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected + + +def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile: + out = BytesIO() + im.save(out, "AVIF", **options) + return Image.open(out) + + +def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} decode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason + ) + + +def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} encode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason + ) + + +def is_docker_qemu() -> bool: + try: + init_proc_exe = os.readlink("/proc/1/exe") + except (FileNotFoundError, PermissionError): + return False + else: + return "qemu" in init_proc_exe + + +def has_alpha_premultiplied(im_bytes: bytes) -> bool: + stream = BytesIO(im_bytes) + length = len(im_bytes) + while stream.tell() < length: + start = stream.tell() + size, boxtype = unpack(">L4s", stream.read(8)) + if not all(0x20 <= c <= 0x7E for c in boxtype): + # Not ascii + return False + if size == 1: # 64bit size + (size,) = unpack(">Q", stream.read(8)) + end = start + size + version, _ = unpack(">B3s", stream.read(4)) + if boxtype in (b"ftyp", b"hdlr", b"pitm", b"iloc", b"iinf"): + # Skip these boxes + stream.seek(end) + continue + elif boxtype == b"meta": + # Container box possibly including iref prem, continue to parse boxes + # inside it + continue + elif boxtype == b"iref": + while stream.tell() < end: + _, iref_type = unpack(">L4s", stream.read(8)) + version, _ = unpack(">B3s", stream.read(4)) + if iref_type == b"prem": + return True + stream.read(2 if version == 0 else 4) + else: + return False + return False + + +class TestUnsupportedAvif: + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + with pytest.warns(UserWarning): + with pytest.raises(UnidentifiedImageError): + with Image.open(TEST_AVIF_FILE): + pass + + def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) + + +@skip_unless_feature("avif") +class TestFileAvif: + def test_version(self) -> None: + _avif.AvifCodecVersions() + + version = features.version_module("avif") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) + + def test_read(self) -> None: + """ + Can we read an AVIF file without error? + Does it have the bits we expect? + """ + + with Image.open(TEST_AVIF_FILE) as image: + assert image.mode == "RGB" + assert image.size == (128, 128) + assert image.format == "AVIF" + assert image.get_format_mimetype() == "image/avif" + image.getdata() + + # generated with: + # avifdec hopper.avif hopper_avif_write.png + assert_image_similar_tofile( + image, "Tests/images/avif/hopper_avif_write.png", 12.0 + ) + + 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", 12.0 + ) + + # 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. + target = hopper(mode) + if mode != "RGB": + target = target.convert("RGB") + assert_image_similar(image, target, 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", 12.5) + + def test_AvifEncoder_with_invalid_args(self) -> None: + """ + Calling encoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifEncoder() + + def test_AvifDecoder_with_invalid_args(self) -> None: + """ + Calling decoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifDecoder() + + def test_encoder_finish_none_error( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Save should raise an OSError if AvifEncoder.finish returns None""" + + class _mock_avif: + class AvifEncoder: + def __init__(self, *args: Any) -> None: + pass + + def add(self, *args: Any) -> None: + pass + + def finish(self) -> None: + return None + + monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) + + im = Image.new("RGB", (150, 150)) + test_file = str(tmp_path / "temp.avif") + with pytest.raises(OSError): + im.save(test_file) + + 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) + + @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) + def test_accept_ftyp_brands(self, major_brand: bytes) -> None: + data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand + assert AvifImagePlugin._accept(data) is True + + def test_file_pointer_could_be_reused(self) -> None: + with open(TEST_AVIF_FILE, "rb") as blob: + with Image.open(blob) as im: + im.load() + with Image.open(blob) as im: + im.load() + + def test_background_from_gif(self, tmp_path: Path) -> None: + with Image.open("Tests/images/chi.gif") as im: + original_value = im.convert("RGB").getpixel((1, 1)) + + # Save as AVIF + out_avif = str(tmp_path / "temp.avif") + im.save(out_avif, save_all=True) + + # Save as GIF + out_gif = str(tmp_path / "temp.gif") + with Image.open(out_avif) as im: + im.save(out_gif) + + with Image.open(out_gif) as reread: + reread_value = reread.convert("RGB").getpixel((1, 1)) + difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) + assert difference < 5 + + def test_save_single_frame(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.avif") + with Image.open("Tests/images/chi.gif") as im: + im.save(temp_file) + with Image.open(temp_file) as im: + assert im.n_frames == 1 + + def test_invalid_file(self) -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(invalid_file) + + def test_load_transparent_rgb(self) -> None: + test_file = "Tests/images/avif/transparency.avif" + with Image.open(test_file) as im: + assert_image(im, "RGBA", (64, 64)) + + # image has 876 transparent pixels + assert im.getchannel("A").getcolors()[0] == (876, 0) + + def test_save_transparent(self, tmp_path: Path) -> None: + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + test_file = str(tmp_path / "temp.avif") + im.save(test_file) + + # check if saved image contains the same transparency + with Image.open(test_file) as im: + assert_image(im, "RGBA", (10, 10)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + def test_save_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert im.info.get("icc_profile") is None + + with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: + expected_icc = with_icc.info.get("icc_profile") + assert expected_icc is not None + + im = roundtrip(im, icc_profile=expected_icc) + assert im.info["icc_profile"] == expected_icc + + def test_discard_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + im = roundtrip(im, icc_profile=None) + assert "icc_profile" not in im.info + + def test_roundtrip_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + expected_icc = im.info["icc_profile"] + + im = roundtrip(im) + assert im.info["icc_profile"] == expected_icc + + def test_roundtrip_no_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert im.info.get("icc_profile") is None + + im = roundtrip(im) + assert "icc_profile" not in im.info + + def test_exif(self) -> None: + # With an EXIF chunk + with Image.open("Tests/images/avif/exif.avif") as im: + exif = im.getexif() + assert exif[274] == 1 + + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + exif = im.getexif() + assert exif[274] == 3 + + def test_exif_save_default(self, tmp_path: Path) -> None: + with Image.open("Tests/images/avif/exif.avif") as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file) + + with Image.open(test_file) as reloaded: + exif = reloaded.getexif() + assert exif[274] == 1 + + @pytest.mark.parametrize("bytes", [True, False]) + def test_exif_save_argument(self, tmp_path: Path, bytes: bool) -> None: + exif = Image.Exif() + exif[274] = 1 + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file, exif=exif_data if bytes else exif) + + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == exif_data + + def test_exif_invalid(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, exif=b"invalid") + + def test_xmp(self) -> None: + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + xmp = im.info["xmp"] + assert_xmp_orientation(xmp, 3) + + def test_xmp_save(self, tmp_path: Path) -> None: + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info["xmp"] + assert_xmp_orientation(xmp, 3) + + def test_xmp_save_from_png(self, tmp_path: Path) -> None: + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info["xmp"] + assert_xmp_orientation(xmp, 3) + + def test_xmp_save_argument(self, tmp_path: Path) -> None: + xmp_arg = "\n".join( + [ + '', + '', + ' ', + ' ', + " ", + "", + '', + ] + ) + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file, xmp=xmp_arg) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info["xmp"] + assert_xmp_orientation(xmp, 1) + + def test_tell(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + assert im.tell() == 0 + + def test_seek(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + im.seek(0) + + with pytest.raises(EOFError): + im.seek(1) + + @pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "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") + im.save(test_file, subsampling=subsampling) + + def test_encoder_subsampling_invalid(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, subsampling="foo") + + def test_encoder_range(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file, range="limited") + + def test_encoder_range_invalid(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, 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") + im.save(test_file, codec="aom") + + def test_encoder_codec_invalid(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="foo") + + @skip_unless_avif_decoder("dav1d") + @skip_unless_feature("avif") + def test_encoder_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") + def test_encoder_advanced_codec_options(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + ctrl_buf = BytesIO() + im.save(ctrl_buf, "AVIF", codec="aom") + test_buf = BytesIO() + im.save( + test_buf, + "AVIF", + codec="aom", + advanced={ + "aq-mode": "1", + "enable-chroma-deltaq": "1", + }, + ) + assert ctrl_buf.getvalue() != test_buf.getvalue() + + @skip_unless_avif_encoder("aom") + @skip_unless_feature("avif") + @pytest.mark.parametrize("val", [{"foo": "bar"}, 1234]) + def test_encoder_advanced_codec_options_invalid( + self, tmp_path: Path, val: dict[str, str] | int + ) -> 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="aom", advanced=val) + + @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") + + with Image.open(TEST_AVIF_FILE) as im: + assert im.size == (128, 128) + + @skip_unless_avif_encoder("rav1e") + @skip_unless_feature("avif") + def test_decoder_codec_cannot_decode( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + + def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + 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 + + def test_encoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _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 + + def test_encoder_codec_available_invalid(self) -> None: + assert _avif.encoder_codec_available("foo") is False + + def test_encoder_quality_valueerror(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, 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 + + def test_decoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _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 + + def test_decoder_codec_available_invalid(self) -> None: + assert _avif.decoder_codec_available("foo") is False + + def test_p_mode_transparency(self) -> None: + im = Image.new("P", size=(64, 64)) + draw = ImageDraw.Draw(im) + draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) + draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) + + buf_png = BytesIO() + im.save(buf_png, format="PNG", transparency=0) + im_png = Image.open(buf_png) + buf_out = BytesIO() + im_png.save(buf_out, format="AVIF", quality=100) + + with Image.open(buf_out) as expected: + assert_image_similar(im_png.convert("RGBA"), expected, 1) + + def test_decoder_strict_flags(self) -> None: + # This would fail if full avif strictFlags were enabled + with Image.open("Tests/images/avif/chimera-missing-pixi.avif") as im: + assert im.size == (480, 270) + + @skip_unless_avif_encoder("aom") + def test_aom_optimizations(self) -> None: + im = hopper("RGB") + buf = BytesIO() + im.save(buf, format="AVIF", codec="aom", speed=1) + + @skip_unless_avif_encoder("svt") + def test_svt_optimizations(self) -> None: + im = hopper("RGB") + buf = BytesIO() + im.save(buf, format="AVIF", codec="svt", speed=1) + + +@skip_unless_feature("avif") +class TestAvifAnimation: + @contextmanager + def star_frames(self) -> Generator[list[ImageFile.ImageFile], None, None]: + with Image.open("Tests/images/avif/star.png") as f1: + with Image.open("Tests/images/avif/star90.png") as f2: + with Image.open("Tests/images/avif/star180.png") as f3: + with Image.open("Tests/images/avif/star270.png") as f4: + yield [f1, f2, f3, f4] + + def test_n_frames(self) -> None: + """ + Ensure that AVIF format sets n_frames and is_animated attributes + correctly. + """ + + with Image.open(TEST_AVIF_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated + + with Image.open("Tests/images/avif/star.avifs") as im: + assert im.n_frames == 5 + assert im.is_animated + + def test_write_animation_L(self, tmp_path: Path) -> None: + """ + Convert an animated GIF to animated AVIF, then compare the frame + count, and first and last frames to ensure they're visually similar. + """ + + with Image.open("Tests/images/avif/star.gif") as orig: + assert orig.n_frames > 1 + + temp_file = str(tmp_path / "temp.avif") + orig.save(temp_file, save_all=True) + with Image.open(temp_file) as im: + assert im.n_frames == orig.n_frames + + # Compare first and second-to-last frames to the original animated GIF + assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) + orig.seek(orig.n_frames - 2) + im.seek(im.n_frames - 2) + assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) + + def test_write_animation_RGB(self, tmp_path: Path) -> None: + """ + Write an animated AVIF from RGB frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file: str) -> None: + with Image.open(temp_file) as im: + assert im.n_frames == 4 + + # Compare first frame to original + assert_image_similar(im, frame1.convert("RGBA"), 25.0) + + # Compare second frame to original + im.seek(1) + assert_image_similar(im, frame2.convert("RGBA"), 25.0) + + with self.star_frames() as frames: + frame1 = frames[0] + frame2 = frames[1] + temp_file1 = str(tmp_path / "temp.avif") + frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) + check(temp_file1) + + # Tests appending using a generator + def imGenerator( + ims: list[ImageFile.ImageFile], + ) -> Generator[ImageFile.ImageFile, None, None]: + yield from ims + + temp_file2 = str(tmp_path / "temp_generator.avif") + frames[0].copy().save( + temp_file2, + save_all=True, + append_images=imGenerator(frames[1:]), + ) + check(temp_file2) + + def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.avif") + frame1 = Image.new("RGB", (100, 100)) + frame2 = Image.new("RGB", (150, 150)) + with pytest.raises(ValueError): + frame1.save(temp_file, save_all=True, append_images=[frame2]) + + def test_heif_raises_unidentified_image_error(self) -> None: + with pytest.raises(UnidentifiedImageError): + with Image.open("Tests/images/avif/rgba10.heif"): + pass + + @pytest.mark.parametrize("alpha_premultipled", [False, True]) + def test_alpha_premultiplied_true(self, alpha_premultipled: bool) -> None: + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + im_buf = BytesIO() + im.save(im_buf, "AVIF", alpha_premultiplied=alpha_premultipled) + im_bytes = im_buf.getvalue() + assert has_alpha_premultiplied(im_bytes) is alpha_premultipled + + def test_timestamp_and_duration(self, tmp_path: Path) -> None: + """ + Try passing a list of durations, and make sure the encoded + timestamps and durations are correct. + """ + + durations = [1, 10, 20, 30, 40] + 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=durations, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Check that timestamps and durations match original values specified + ts = 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] + + def test_seeking(self, tmp_path: Path) -> None: + """ + Create an animated AVIF file, and then try seeking through frames in + reverse-order, verifying the timestamps and durations are correct. + """ + + dur = 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, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Traverse frames in reverse, checking timestamps and durations + ts = dur * (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 + + def test_seek_errors(self) -> None: + with Image.open("Tests/images/avif/star.avifs") as im: + with pytest.raises(EOFError): + im.seek(-1) + + with pytest.raises(EOFError): + im.seek(42) + + +MAX_THREADS = os.cpu_count() or 1 + + +@skip_unless_feature("avif") +class TestAvifLeaks(PillowLeakTestCase): + mem_limit = MAX_THREADS * 3 * 1024 + iterations = 100 + + @pytest.mark.skipif( + is_docker_qemu(), reason="Skipping on cross-architecture containers" + ) + def test_leak_load(self) -> None: + with open(TEST_AVIF_FILE, "rb") as f: + im_data = f.read() + + def core() -> None: + with Image.open(BytesIO(im_data)) as im: + im.load() + gc.collect() + + self._test_leak(core) diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh new file mode 100755 index 00000000000..09646c4ad78 --- /dev/null +++ b/depends/install_libavif.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -eo pipefail + +version=1.1.1 + +./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + +pushd libavif-$version + +if uname -s | grep -q Darwin; then + PREFIX=$(brew --prefix) +else + PREFIX=/usr +fi + +PKGCONFIG=${PKGCONFIG:-pkg-config} + +LIBAVIF_CMAKE_FLAGS=() +HAS_DECODER=0 +HAS_ENCODER=0 + +if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) + HAS_ENCODER=1 +fi + +if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) + 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 + +cmake -G Ninja -S . -B build \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DAVIF_LIBYUV=LOCAL \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_MACOSX_RPATH=OFF \ + "${LIBAVIF_CMAKE_FLAGS[@]}" + +sudo ninja -C build install + +popd diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bf3087f6f68..0660c8e829b 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -235,7 +235,7 @@ following options are available:: **append_images** A list of images to append as additional frames. Each of the images in the list can be single or multiframe images. - This is currently supported for GIF, PDF, PNG, TIFF, and WebP. + This is currently supported for AVIF, GIF, PDF, PNG, TIFF and WebP. It is also supported for ICO and ICNS. If images are passed in of relevant sizes, they will be used instead of scaling down the main image. @@ -1335,6 +1335,79 @@ XBM Pillow reads and writes X bitmap files (mode ``1``). +AVIF +^^^^ + +Pillow reads and writes AVIF files, including AVIF sequence images. Currently, +it is only possible to save 8-bit AVIF images, and all AVIF images are decoded +as 8-bit RGB(A). + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**quality** + Integer, 1-100, defaults to 90. 0 gives the smallest size and poorest + quality, 100 the largest and best quality. The value of this setting + controls the ``qmin`` and ``qmax`` encoder options. + +**qmin** / **qmax** + Integer, 0-63. The quality of images created by an AVIF encoder are + controlled by minimum and maximum quantizer values. The higher these + values are, the worse the quality. + +**subsampling** + If present, sets the subsampling for the encoder. Defaults to ``4:2:0``. + Options include: + + * ``4:0:0`` + * ``4:2:0`` + * ``4:2:2`` + * ``4:4:4`` + +**speed** + Quality/speed trade-off (0=slower-better, 10=fastest). Defaults to 8. + +**range** + YUV range, either "full" or "limited". Defaults to "full" + +**codec** + AV1 codec to use for encoding. Possible values are "aom", "rav1e", and + "svt", depending on what codecs were compiled with libavif. Defaults to + "auto", which will choose the first available codec in the order of the + preceding list. + +**tile_rows** / **tile_cols** + For tile encoding, the (log 2) number of tile rows and columns to use. + Valid values are 0-6, default 0. + +**alpha_premultiplied** + Encode the image with premultiplied alpha, defaults ``False`` + +**icc_profile** + The ICC Profile to include in the saved file. + +**exif** + The exif data to include in the saved file. + +**xmp** + The XMP data to include in the saved file. + +Saving sequences +~~~~~~~~~~~~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +options will also be available. + +**append_images** + A list of images to append as additional frames. Each of the + images in the list can be single or multiframe images. + +**duration** + The display duration of each frame, in milliseconds. Pass a single + integer for a constant duration, or a list or tuple to set the + duration for each frame separately. + Read-only formats ----------------- diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 19294f72c1c..2603fa9c6f1 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -89,6 +89,16 @@ 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 + AVIF image sequence support was added. + * libavif is merely an API that wraps AVIF codecs. If you are compiling + libavif from source, you will also need to install both an AVIF encoder + and decoder, such as rav1e and dav1d, or libaom, which both encodes and + decodes AVIF images. + .. tab:: Linux If you didn't build Python from source, make sure you have Python's @@ -117,6 +127,12 @@ Many of Pillow's features require external libraries: To install libraqm, ``sudo apt-get install meson`` and then see ``depends/install_raqm.sh``. + Build prerequisites for libavif on Ubuntu are installed with:: + + sudo apt-get install cmake ninja-build nasm + + Then see ``depends/install_libavif.sh`` to build and install libavif. + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ @@ -156,6 +172,12 @@ Many of Pillow's features require external libraries: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + To install libavif on macOS use Homebrew to install its build dependencies:: + + brew install aom dav1d rav1e + + Then see ``depends/install_libavif.sh`` to install libavif. + .. tab:: Windows We recommend you use prebuilt wheels from PyPI. @@ -193,7 +215,8 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libavif .. tab:: FreeBSD @@ -205,9 +228,10 @@ Many of Pillow's features require external libraries: Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + See ``depends/install_raqm_cmake.sh`` to install libraqm and + ``depends/install_libavif.sh`` to install libavif. .. tab:: Android diff --git a/docs/reference/features.rst b/docs/reference/features.rst index fcff9673567..d1e4f7470eb 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -21,6 +21,7 @@ Support for the following modules can be checked: * ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. * ``webp``: WebP image support. +* ``avif``: AVIF image support. .. autofunction:: PIL.features.check_module .. autofunction:: PIL.features.version_module diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 454b94d8ce7..c789f575700 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,6 +1,14 @@ Plugin reference ================ +:mod:`~PIL.AvifImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.AvifImagePlugin + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL.BmpImagePlugin` Module --------------------------------- diff --git a/setup.py b/setup.py index fbd23a5685a..b6e4076c2a3 100644 --- a/setup.py +++ b/setup.py @@ -305,6 +305,7 @@ class ext_feature: "jpeg2000", "imagequant", "xcb", + "avif", ] required = {"jpeg", "zlib"} @@ -842,6 +843,12 @@ def build_extensions(self) -> None: if _find_library_file(self, "xcb"): feature.set("xcb", "xcb") + if feature.want("avif"): + _dbg("Looking for avif") + if _find_include_file(self, "avif/avif.h"): + if _find_library_file(self, "avif"): + feature.set("avif", "avif") + for f in feature: if not feature.get(f) and feature.require(f): if f in ("jpeg", "zlib"): @@ -930,6 +937,14 @@ def build_extensions(self) -> None: else: self._remove_extension("PIL._webp") + if feature.get("avif"): + libs = [feature.get("avif")] + if sys.platform == "win32": + libs.extend(["ntdll", "userenv", "ws2_32", "bcrypt"]) + self._update_extension("PIL._avif", libs) + else: + self._remove_extension("PIL._avif") + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] self._update_extension("PIL._imagingtk", tk_libs) @@ -972,6 +987,7 @@ def summary_report(self, feature: ext_feature) -> None: (feature.get("lcms"), "LITTLECMS2"), (feature.get("webp"), "WEBP"), (feature.get("xcb"), "XCB (X protocol)"), + (feature.get("avif"), "LIBAVIF"), ] all = 1 @@ -1014,6 +1030,7 @@ def debug_build() -> bool: Extension("PIL._imagingft", ["src/_imagingft.c"]), Extension("PIL._imagingcms", ["src/_imagingcms.c"]), Extension("PIL._webp", ["src/_webp.c"]), + Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), Extension("PIL._imagingmath", ["src/_imagingmath.c"]), Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py new file mode 100644 index 00000000000..1c90f428a09 --- /dev/null +++ b/src/PIL/AvifImagePlugin.py @@ -0,0 +1,286 @@ +from __future__ import annotations + +import os +from io import BytesIO +from typing import IO + +from . import ExifTags, Image, ImageFile + +try: + from . import _avif + + SUPPORTED = True +except ImportError: + SUPPORTED = False + +# Decoder options as module globals, until there is a way to pass parameters +# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) +DECODE_CODEC_CHOICE = "auto" +DEFAULT_MAX_THREADS = 0 + +_VALID_AVIF_MODES = {"RGB", "RGBA"} + + +def _accept(prefix: bytes) -> bool | str: + if prefix[4:8] != b"ftyp": + return False + coding_brands = (b"avif", b"avis") + container_brands = (b"mif1", b"msf1") + major_brand = prefix[8:12] + if major_brand in coding_brands: + if not SUPPORTED: + return ( + "image file could not be identified because AVIF " + "support not installed" + ) + return True + if major_brand in container_brands: + # We accept files with AVIF container brands; we can't yet know if + # the ftyp box has the correct compatible brands, but if it doesn't + # then the plugin will raise a SyntaxError which Pillow will catch + # before moving on to the next plugin that accepts the file. + # + # Also, because this file might not actually be an AVIF file, we + # don't raise an error if AVIF support isn't properly compiled. + return True + return False + + +def _get_default_max_threads(): + if DEFAULT_MAX_THREADS: + return DEFAULT_MAX_THREADS + if hasattr(os, "sched_getaffinity"): + return len(os.sched_getaffinity(0)) + else: + return os.cpu_count() or 1 + + +class AvifImageFile(ImageFile.ImageFile): + format = "AVIF" + format_description = "AVIF image" + __loaded = -1 + __frame = 0 + + def load_seek(self, pos: int) -> None: + pass + + def _open(self) -> None: + if not SUPPORTED: + msg = ( + "image file could not be identified because AVIF " + "support not installed" + ) + raise SyntaxError(msg) + + self._decoder = _avif.AvifDecoder( + self.fp.read(), + DECODE_CODEC_CHOICE, + _get_default_max_threads(), + ) + + # Get info from decoder + width, height, n_frames, mode, icc, exif, xmp = self._decoder.get_info() + self._size = width, height + self.n_frames = n_frames + self.is_animated = self.n_frames > 1 + self._mode = mode + + if icc: + self.info["icc_profile"] = icc + if exif: + self.info["exif"] = exif + if xmp: + self.info["xmp"] = xmp + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + self.__frame = frame + + def load(self) -> Image.core.PixelAccess | None: + if self.__loaded != self.__frame: + # We need to load the image data for this frame + data, timescale, tsp_in_ts, dur_in_ts = self._decoder.get_frame( + self.__frame + ) + timestamp = round(1000 * (tsp_in_ts / timescale)) + duration = round(1000 * (dur_in_ts / timescale)) + self.info["timestamp"] = timestamp + self.info["duration"] = duration + 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() + + def tell(self) -> int: + return self.__frame + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: + info = im.encoderinfo.copy() + if save_all: + append_images = list(info.get("append_images", [])) + else: + append_images = [] + + total = 0 + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) + + is_single_frame = total == 1 + + qmin = info.get("qmin", -1) + qmax = info.get("qmax", -1) + quality = info.get("quality", 75) + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + + duration = info.get("duration", 0) + subsampling = info.get("subsampling", "4:2:0") + speed = info.get("speed", 6) + max_threads = info.get("max_threads", _get_default_max_threads()) + codec = info.get("codec", "auto") + range_ = info.get("range", "full") + tile_rows_log2 = info.get("tile_rows", 0) + tile_cols_log2 = info.get("tile_cols", 0) + alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) + autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) + + icc_profile = info.get("icc_profile", im.info.get("icc_profile")) + exif = info.get("exif", im.info.get("exif")) + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + + exif_orientation = 0 + if exif: + exif_data = Image.Exif() + try: + exif_data.load(exif) + except SyntaxError: + pass + else: + orientation_tag = next( + k for k, v in ExifTags.TAGS.items() if v == "Orientation" + ) + exif_orientation = exif_data.get(orientation_tag) or 0 + + xmp = info.get("xmp", im.info.get("xmp") or im.info.get("XML:com.adobe.xmp")) + + if isinstance(xmp, str): + xmp = xmp.encode("utf-8") + + advanced = info.get("advanced") + if isinstance(advanced, dict): + advanced = tuple([k, v] for (k, v) in advanced.items()) + if advanced is not None: + try: + advanced = tuple(advanced) + except TypeError: + invalid = True + else: + invalid = all(isinstance(v, tuple) and len(v) == 2 for v in advanced) + if invalid: + msg = ( + "advanced codec options must be a dict of key-value string " + "pairs or a series of key-value two-tuples" + ) + raise ValueError(msg) + advanced = tuple( + [(str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced] + ) + + # Setup the AVIF encoder + enc = _avif.AvifEncoder( + im.size[0], + im.size[1], + subsampling, + qmin, + qmax, + quality, + speed, + max_threads, + codec, + range_, + tile_rows_log2, + tile_cols_log2, + alpha_premultiplied, + autotiling, + icc_profile or b"", + exif or b"", + exif_orientation, + xmp or b"", + advanced, + ) + + # Add each frame + frame_idx = 0 + frame_dur = 0 + cur_idx = im.tell() + try: + for ims in [im] + append_images: + # Get # of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + + # Make sure image mode is supported + frame = ims + rawmode = ims.mode + if ims.mode not in _VALID_AVIF_MODES: + rawmode = "RGBA" if ims.has_transparency_data else "RGB" + frame = ims.convert(rawmode) + + # Update frame duration + if isinstance(duration, (list, tuple)): + frame_dur = duration[frame_idx] + else: + frame_dur = duration + + # Append the frame to the animation encoder + enc.add( + frame.tobytes("raw", rawmode), + frame_dur, + frame.size[0], + frame.size[1], + rawmode, + is_single_frame, + ) + + # Update frame index + frame_idx += 1 + + if not save_all: + break + + finally: + im.seek(cur_idx) + + # Get the final output from the encoder + data = enc.finish() + if data is None: + msg = "cannot write file as AVIF (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +Image.register_open(AvifImageFile.format, AvifImageFile, _accept) +if SUPPORTED: + Image.register_save(AvifImageFile.format, _save) + Image.register_save_all(AvifImageFile.format, _save_all) + Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) + Image.register_mime(AvifImageFile.format, "image/avif") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9b76ce8bd86..f4c41160672 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1549,6 +1549,8 @@ def getexif(self) -> Exif: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + if not xmp_tags and (xmp_tags := self.info.get("xmp")): + xmp_tags = xmp_tags.decode("utf-8") if xmp_tags: match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 09546fe6333..6e4c23f897f 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -25,6 +25,7 @@ _plugins = [ + "AvifImagePlugin", "BlpImagePlugin", "BmpImagePlugin", "BufrStubImagePlugin", diff --git a/src/PIL/_avif.pyi b/src/PIL/_avif.pyi new file mode 100644 index 00000000000..e27843e5338 --- /dev/null +++ b/src/PIL/_avif.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/features.py b/src/PIL/features.py index 75d59e01c40..3429d3f7e46 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -17,6 +17,7 @@ "freetype2": ("PIL._imagingft", "freetype2_version"), "littlecms2": ("PIL._imagingcms", "littlecms_version"), "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), } @@ -287,6 +288,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), ("jpg", "JPEG"), + ("avif", "AVIF"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), ("libtiff", "LIBTIFF"), diff --git a/src/_avif.c b/src/_avif.c new file mode 100644 index 00000000000..d0bb81f4619 --- /dev/null +++ b/src/_avif.c @@ -0,0 +1,998 @@ +#define PY_SSIZE_T_CLEAN + +#include +#include "avif/avif.h" + +typedef struct { + avifPixelFormat subsampling; + int qmin; + int qmax; + int quality; + int speed; + avifCodecChoice codec; + avifRange range; + avifBool alpha_premultiplied; + int tile_rows_log2; + int tile_cols_log2; + avifBool autotiling; +} avifEncOptions; + +// Encoder type +typedef struct { + PyObject_HEAD avifEncoder *encoder; + avifImage *image; + PyObject *icc_bytes; + PyObject *exif_bytes; + PyObject *xmp_bytes; + int frame_index; +} AvifEncoderObject; + +static PyTypeObject AvifEncoder_Type; + +// Decoder type +typedef struct { + PyObject_HEAD avifDecoder *decoder; + PyObject *data; + char *mode; +} AvifDecoderObject; + +static PyTypeObject AvifDecoder_Type; + +static int +normalize_quantize_value(int qvalue) { + if (qvalue < AVIF_QUANTIZER_BEST_QUALITY) { + return AVIF_QUANTIZER_BEST_QUALITY; + } else if (qvalue > AVIF_QUANTIZER_WORST_QUALITY) { + return AVIF_QUANTIZER_WORST_QUALITY; + } else { + return qvalue; + } +} + +static int +normalize_tiles_log2(int value) { + if (value < 0) { + return 0; + } else if (value > 6) { + return 6; + } else { + return value; + } +} + +static PyObject * +exc_type_for_avif_result(avifResult result) { + switch (result) { + case AVIF_RESULT_INVALID_EXIF_PAYLOAD: + case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION: + return PyExc_ValueError; + case AVIF_RESULT_INVALID_FTYP: + case AVIF_RESULT_BMFF_PARSE_FAILED: + case AVIF_RESULT_TRUNCATED_DATA: + case AVIF_RESULT_NO_CONTENT: + return PyExc_SyntaxError; + default: + return PyExc_RuntimeError; + } +} + +static void +exif_orientation_to_irot_imir(avifImage *image, int orientation) { + const avifTransformFlags otherFlags = + image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR); + + // + // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A + // Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 + // sections 6.5.10 and 6.5.12. + switch (orientation) { + case 1: // The 0th row is at the visual top of the image, and the 0th column is + // the visual left-hand side. + image->transformFlags = otherFlags; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + case 2: // The 0th row is at the visual top of the image, and the 0th column is + // the visual right-hand side. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 1; +#else + image->imir.mode = 1; +#endif + return; + case 3: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual right-hand side. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 2; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + case 4: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual left-hand side. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 5: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags = + otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 1; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 6: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 3; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + case 7: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags = + otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 3; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 8: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 1; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + default: // reserved + break; + } + + // The orientation tag is not mandatory (only recommended) according to JEITA + // CP-3451C section 4.6.8.A. The default value is 1 if the orientation tag is + // missing, meaning: + // The 0th row is at the visual top of the image, and the 0th column is the visual + // left-hand side. + image->transformFlags = otherFlags; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif +} + +static int +_codec_available(const char *name, uint32_t flags) { + avifCodecChoice codec = avifCodecChoiceFromName(name); + if (codec == AVIF_CODEC_CHOICE_AUTO) { + return 0; + } + const char *codec_name = avifCodecName(codec, flags); + return (codec_name == NULL) ? 0 : 1; +} + +PyObject * +_decoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_encoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE); + return PyBool_FromLong(is_available); +} + +static void +_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { + Py_ssize_t i, size; + PyObject *keyval, *py_key, *py_val; + char *key, *val; + if (!PyTuple_Check(opts)) { + return; + } + size = PyTuple_GET_SIZE(opts); + + for (i = 0; i < size; i++) { + keyval = PyTuple_GetItem(opts, i); + if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) { + return; + } + py_key = PyTuple_GetItem(keyval, 0); + py_val = PyTuple_GetItem(keyval, 1); + if (!PyBytes_Check(py_key) || !PyBytes_Check(py_val)) { + return; + } + key = PyBytes_AsString(py_key); + val = PyBytes_AsString(py_val); + avifEncoderSetCodecSpecificOption(encoder, key, val); + } +} + +// Encoder functions +PyObject * +AvifEncoderNew(PyObject *self_, PyObject *args) { + unsigned int width, height; + avifEncOptions enc_options; + AvifEncoderObject *self = NULL; + avifEncoder *encoder = NULL; + + char *subsampling; + int qmin; + int qmax; + int quality; + int speed; + int exif_orientation; + int max_threads; + PyObject *icc_bytes; + PyObject *exif_bytes; + PyObject *xmp_bytes; + PyObject *alpha_premultiplied; + PyObject *autotiling; + int tile_rows_log2; + int tile_cols_log2; + + char *codec; + char *range; + + PyObject *advanced; + + if (!PyArg_ParseTuple( + args, + "IIsiiiiissiiOOSSiSO", + &width, + &height, + &subsampling, + &qmin, + &qmax, + &quality, + &speed, + &max_threads, + &codec, + &range, + &tile_rows_log2, + &tile_cols_log2, + &alpha_premultiplied, + &autotiling, + &icc_bytes, + &exif_bytes, + &exif_orientation, + &xmp_bytes, + &advanced + )) { + return NULL; + } + + if (strcmp(subsampling, "4:0:0") == 0) { + enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV400; + } else if (strcmp(subsampling, "4:2:0") == 0) { + enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV420; + } else if (strcmp(subsampling, "4:2:2") == 0) { + enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV422; + } else if (strcmp(subsampling, "4:4:4") == 0) { + enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV444; + } else { + PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); + return NULL; + } + + if (qmin == -1 || qmax == -1) { +#if AVIF_VERSION >= 1000000 + enc_options.qmin = -1; + enc_options.qmax = -1; +#else + enc_options.qmin = normalize_quantize_value(64 - quality); + enc_options.qmax = normalize_quantize_value(100 - quality); +#endif + } else { + enc_options.qmin = normalize_quantize_value(qmin); + enc_options.qmax = normalize_quantize_value(qmax); + } + enc_options.quality = quality; + + if (speed < AVIF_SPEED_SLOWEST) { + speed = AVIF_SPEED_SLOWEST; + } else if (speed > AVIF_SPEED_FASTEST) { + speed = AVIF_SPEED_FASTEST; + } + enc_options.speed = speed; + + if (strcmp(codec, "auto") == 0) { + enc_options.codec = AVIF_CODEC_CHOICE_AUTO; + } else { + enc_options.codec = avifCodecChoiceFromName(codec); + if (enc_options.codec == AVIF_CODEC_CHOICE_AUTO) { + PyErr_Format(PyExc_ValueError, "Invalid codec: %s", codec); + return NULL; + } else { + const char *codec_name = + avifCodecName(enc_options.codec, AVIF_CODEC_FLAG_CAN_ENCODE); + if (codec_name == NULL) { + PyErr_Format(PyExc_ValueError, "AV1 Codec cannot encode: %s", codec); + return NULL; + } + } + } + + if (strcmp(range, "full") == 0) { + enc_options.range = AVIF_RANGE_FULL; + } else if (strcmp(range, "limited") == 0) { + enc_options.range = AVIF_RANGE_LIMITED; + } else { + PyErr_SetString(PyExc_ValueError, "Invalid range"); + return NULL; + } + + // Validate canvas dimensions + if (width <= 0 || height <= 0) { + PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); + return NULL; + } + + enc_options.tile_rows_log2 = normalize_tiles_log2(tile_rows_log2); + enc_options.tile_cols_log2 = normalize_tiles_log2(tile_cols_log2); + + if (alpha_premultiplied == Py_True) { + enc_options.alpha_premultiplied = AVIF_TRUE; + } else { + enc_options.alpha_premultiplied = AVIF_FALSE; + } + + enc_options.autotiling = (autotiling == Py_True) ? AVIF_TRUE : AVIF_FALSE; + + // Create a new animation encoder and picture frame + self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); + if (self) { + self->icc_bytes = NULL; + self->exif_bytes = NULL; + self->xmp_bytes = NULL; + + encoder = avifEncoderCreate(); + + int is_aom_encode = strcmp(codec, "aom") == 0 || + (strcmp(codec, "auto") == 0 && + _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE)); + + encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads; +#if AVIF_VERSION >= 1000000 + if (enc_options.qmin != -1 && enc_options.qmax != -1) { + encoder->minQuantizer = enc_options.qmin; + encoder->maxQuantizer = enc_options.qmax; + } else { + encoder->quality = enc_options.quality; + } +#else + encoder->minQuantizer = enc_options.qmin; + encoder->maxQuantizer = enc_options.qmax; +#endif + encoder->codecChoice = enc_options.codec; + encoder->speed = enc_options.speed; + encoder->timescale = (uint64_t)1000; + encoder->tileRowsLog2 = enc_options.tile_rows_log2; + encoder->tileColsLog2 = enc_options.tile_cols_log2; + +#if AVIF_VERSION >= 110000 + encoder->autoTiling = enc_options.autotiling; +#endif + +#if AVIF_VERSION >= 80200 + _add_codec_specific_options(encoder, advanced); +#endif + + self->encoder = encoder; + + avifImage *image = avifImageCreateEmpty(); + // Set these in advance so any upcoming RGB -> YUV use the proper coefficients + image->yuvRange = enc_options.range; + image->yuvFormat = enc_options.subsampling; + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + image->width = width; + image->height = height; + image->depth = 8; +#if AVIF_VERSION >= 90000 + image->alphaPremultiplied = enc_options.alpha_premultiplied; +#endif + + if (PyBytes_GET_SIZE(icc_bytes)) { + self->icc_bytes = icc_bytes; + Py_INCREF(icc_bytes); + avifImageSetProfileICC( + image, + (uint8_t *)PyBytes_AS_STRING(icc_bytes), + PyBytes_GET_SIZE(icc_bytes) + ); + } else { + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + } + + if (PyBytes_GET_SIZE(exif_bytes)) { + self->exif_bytes = exif_bytes; + Py_INCREF(exif_bytes); + avifImageSetMetadataExif( + image, + (uint8_t *)PyBytes_AS_STRING(exif_bytes), + PyBytes_GET_SIZE(exif_bytes) + ); + } + if (PyBytes_GET_SIZE(xmp_bytes)) { + self->xmp_bytes = xmp_bytes; + Py_INCREF(xmp_bytes); + avifImageSetMetadataXMP( + image, + (uint8_t *)PyBytes_AS_STRING(xmp_bytes), + PyBytes_GET_SIZE(xmp_bytes) + ); + } + exif_orientation_to_irot_imir(image, exif_orientation); + + self->image = image; + self->frame_index = -1; + + return (PyObject *)self; + } + PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); + return NULL; +} + +PyObject * +_encoder_dealloc(AvifEncoderObject *self) { + if (self->encoder) { + avifEncoderDestroy(self->encoder); + } + if (self->image) { + avifImageDestroy(self->image); + } + Py_XDECREF(self->icc_bytes); + Py_XDECREF(self->exif_bytes); + Py_XDECREF(self->xmp_bytes); + Py_RETURN_NONE; +} + +PyObject * +_encoder_add(AvifEncoderObject *self, PyObject *args) { + uint8_t *rgb_bytes; + Py_ssize_t size; + unsigned int duration; + unsigned int width; + unsigned int height; + char *mode; + PyObject *is_single_frame = NULL; + PyObject *ret = Py_None; + + int is_first_frame; + int channels; + avifRGBImage rgb; + avifResult result; + + avifEncoder *encoder = self->encoder; + avifImage *image = self->image; + avifImage *frame = NULL; + + if (!PyArg_ParseTuple( + args, + "z#IIIsO", + (char **)&rgb_bytes, + &size, + &duration, + &width, + &height, + &mode, + &is_single_frame + )) { + return NULL; + } + + is_first_frame = (self->frame_index == -1); + + if ((image->width != width) || (image->height != height)) { + PyErr_Format( + PyExc_ValueError, + "Image sequence dimensions mismatch, %ux%u != %ux%u", + image->width, + image->height, + width, + height + ); + return NULL; + } + + if (is_first_frame) { + // If we don't have an image populated with yuv planes, this is the first frame + frame = image; + } else { + frame = avifImageCreateEmpty(); + + frame->colorPrimaries = image->colorPrimaries; + frame->transferCharacteristics = image->transferCharacteristics; + frame->matrixCoefficients = image->matrixCoefficients; + frame->yuvRange = image->yuvRange; + frame->yuvFormat = image->yuvFormat; + frame->depth = image->depth; +#if AVIF_VERSION >= 90000 + frame->alphaPremultiplied = image->alphaPremultiplied; +#endif + } + + frame->width = width; + frame->height = height; + + memset(&rgb, 0, sizeof(avifRGBImage)); + + avifRGBImageSetDefaults(&rgb, frame); + rgb.depth = 8; + + if (strcmp(mode, "RGBA") == 0) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + channels = 4; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + channels = 3; + } + + avifRGBImageAllocatePixels(&rgb); + + if (rgb.rowBytes * rgb.height != size) { + PyErr_Format( + PyExc_RuntimeError, + "rgb data is incorrect size: %u * %u (%u) != %u", + rgb.rowBytes, + rgb.height, + rgb.rowBytes * rgb.height, + size + ); + ret = NULL; + goto end; + } + + // rgb.pixels is safe for writes + memcpy(rgb.pixels, rgb_bytes, size); + + Py_BEGIN_ALLOW_THREADS result = avifImageRGBToYUV(frame, &rgb); + Py_END_ALLOW_THREADS + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion to YUV failed: %s", + avifResultToString(result) + ); + ret = NULL; + goto end; + } + + uint32_t addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE; + if (PyObject_IsTrue(is_single_frame)) { + addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE; + } + + Py_BEGIN_ALLOW_THREADS result = + avifEncoderAddImage(encoder, frame, duration, addImageFlags); + Py_END_ALLOW_THREADS + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to encode image: %s", + avifResultToString(result) + ); + ret = NULL; + goto end; + } + +end: + avifRGBImageFreePixels(&rgb); + if (!is_first_frame) { + avifImageDestroy(frame); + } + + if (ret == Py_None) { + self->frame_index++; + Py_RETURN_NONE; + } else { + return ret; + } +} + +PyObject * +_encoder_finish(AvifEncoderObject *self) { + avifEncoder *encoder = self->encoder; + + avifRWData raw = AVIF_DATA_EMPTY; + avifResult result; + PyObject *ret = NULL; + + Py_BEGIN_ALLOW_THREADS result = avifEncoderFinish(encoder, &raw); + Py_END_ALLOW_THREADS + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to finish encoding: %s", + avifResultToString(result) + ); + avifRWDataFree(&raw); + return NULL; + } + + ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size); + + avifRWDataFree(&raw); + + return ret; +} + +// Decoder functions +PyObject * +AvifDecoderNew(PyObject *self_, PyObject *args) { + PyObject *avif_bytes; + AvifDecoderObject *self = NULL; + + char *codec_str; + avifCodecChoice codec; + int max_threads; + + avifResult result; + + if (!PyArg_ParseTuple(args, "Ssi", &avif_bytes, &codec_str, &max_threads)) { + return NULL; + } + + if (strcmp(codec_str, "auto") == 0) { + codec = AVIF_CODEC_CHOICE_AUTO; + } else { + codec = avifCodecChoiceFromName(codec_str); + if (codec == AVIF_CODEC_CHOICE_AUTO) { + PyErr_Format(PyExc_ValueError, "Invalid codec: %s", codec_str); + return NULL; + } else { + const char *codec_name = avifCodecName(codec, AVIF_CODEC_FLAG_CAN_DECODE); + if (codec_name == NULL) { + PyErr_Format( + PyExc_ValueError, "AV1 Codec cannot decode: %s", codec_str + ); + return NULL; + } + } + } + + self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); + return NULL; + } + self->decoder = NULL; + + Py_INCREF(avif_bytes); + self->data = avif_bytes; + + self->decoder = avifDecoderCreate(); +#if AVIF_VERSION >= 80400 + self->decoder->maxThreads = max_threads; +#endif +#if AVIF_VERSION >= 90200 + // Turn off libavif's 'clap' (clean aperture) property validation. + self->decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; + // Allow the PixelInformationProperty ('pixi') to be missing in AV1 image + // items. libheif v1.11.0 and older does not add the 'pixi' item property to + // AV1 image items. + self->decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; +#endif + self->decoder->codecChoice = codec; + + avifDecoderSetIOMemory( + self->decoder, + (uint8_t *)PyBytes_AS_STRING(self->data), + PyBytes_GET_SIZE(self->data) + ); + + result = avifDecoderParse(self->decoder); + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode image: %s", + avifResultToString(result) + ); + avifDecoderDestroy(self->decoder); + self->decoder = NULL; + Py_DECREF(self); + return NULL; + } + + if (self->decoder->alphaPresent) { + self->mode = "RGBA"; + } else { + self->mode = "RGB"; + } + + return (PyObject *)self; +} + +PyObject * +_decoder_dealloc(AvifDecoderObject *self) { + if (self->decoder) { + avifDecoderDestroy(self->decoder); + } + Py_XDECREF(self->data); + Py_RETURN_NONE; +} + +PyObject * +_decoder_get_info(AvifDecoderObject *self) { + avifDecoder *decoder = self->decoder; + avifImage *image = decoder->image; + + PyObject *icc = NULL; + PyObject *exif = NULL; + PyObject *xmp = NULL; + PyObject *ret = NULL; + + if (image->xmp.size) { + xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); + } + + if (image->exif.size) { + exif = + PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + } + + if (image->icc.size) { + icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + } + + ret = Py_BuildValue( + "IIIsSSS", + image->width, + image->height, + decoder->imageCount, + self->mode, + NULL == icc ? Py_None : icc, + NULL == exif ? Py_None : exif, + NULL == xmp ? Py_None : xmp + ); + + Py_XDECREF(xmp); + Py_XDECREF(exif); + Py_XDECREF(icc); + + return ret; +} + +PyObject * +_decoder_get_frame(AvifDecoderObject *self, PyObject *args) { + PyObject *bytes; + PyObject *ret; + Py_ssize_t size; + avifResult result; + avifRGBImage rgb; + avifDecoder *decoder; + avifImage *image; + uint32_t frame_index; + uint32_t row_bytes; + + decoder = self->decoder; + + if (!PyArg_ParseTuple(args, "I", &frame_index)) { + return NULL; + } + + result = avifDecoderNthImage(decoder, frame_index); + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode frame %u: %s", + decoder->imageIndex + 1, + avifResultToString(result) + ); + return NULL; + } + + image = decoder->image; + + memset(&rgb, 0, sizeof(rgb)); + avifRGBImageSetDefaults(&rgb, image); + + rgb.depth = 8; + + if (decoder->alphaPresent) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + 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; + } + + avifRGBImageAllocatePixels(&rgb); + + Py_BEGIN_ALLOW_THREADS result = avifImageYUVToRGB(image, &rgb); + Py_END_ALLOW_THREADS + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion from YUV failed: %s", + avifResultToString(result) + ); + avifRGBImageFreePixels(&rgb); + return NULL; + } + + size = rgb.rowBytes * rgb.height; + + bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); + avifRGBImageFreePixels(&rgb); + + ret = Py_BuildValue( + "SKKK", + bytes, + decoder->timescale, + decoder->imageTiming.ptsInTimescales, + decoder->imageTiming.durationInTimescales + ); + + Py_DECREF(bytes); + + return ret; +} + +/* -------------------------------------------------------------------- */ +/* Type Definitions */ +/* -------------------------------------------------------------------- */ + +// AvifEncoder methods +static struct PyMethodDef _encoder_methods[] = { + {"add", (PyCFunction)_encoder_add, METH_VARARGS}, + {"finish", (PyCFunction)_encoder_finish, METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifDecoder type definition +static PyTypeObject AvifEncoder_Type = { + // clang-format off + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "AvifEncoder", + // clang-format on + .tp_basicsize = sizeof(AvifEncoderObject), + .tp_dealloc = (destructor)_encoder_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _encoder_methods, +}; + +// AvifDecoder methods +static struct PyMethodDef _decoder_methods[] = { + {"get_info", (PyCFunction)_decoder_get_info, METH_NOARGS}, + {"get_frame", (PyCFunction)_decoder_get_frame, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifDecoder type definition +static PyTypeObject AvifDecoder_Type = { + // clang-format off + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "AvifDecoder", + // clang-format on + .tp_basicsize = sizeof(AvifDecoderObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)_decoder_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _decoder_methods, +}; + +PyObject * +AvifCodecVersions() { + char codecVersions[256]; + avifCodecVersions(codecVersions); + return PyUnicode_FromString(codecVersions); +} + +/* -------------------------------------------------------------------- */ +/* Module Setup */ +/* -------------------------------------------------------------------- */ + +static PyMethodDef avifMethods[] = { + {"AvifDecoder", AvifDecoderNew, METH_VARARGS}, + {"AvifEncoder", AvifEncoderNew, METH_VARARGS}, + {"AvifCodecVersions", AvifCodecVersions, METH_NOARGS}, + {"decoder_codec_available", _decoder_codec_available, METH_VARARGS}, + {"encoder_codec_available", _encoder_codec_available, METH_VARARGS}, + {NULL, NULL} +}; + +static int +setup_module(PyObject *m) { + PyObject *d = PyModule_GetDict(m); + + PyObject *v = PyUnicode_FromString(avifVersion()); + if (PyDict_SetItemString(d, "libavif_version", v) < 0) { + Py_DECREF(v); + return -1; + } + Py_DECREF(v); + + v = Py_True; + Py_INCREF(v); + if (PyDict_SetItemString(d, "HAVE_AVIF", v) < 0) { + Py_DECREF(v); + return -1; + } + Py_DECREF(v); + + v = Py_BuildValue( + "(iii)", AVIF_VERSION_MAJOR, AVIF_VERSION_MINOR, AVIF_VERSION_PATCH + ); + + if (PyDict_SetItemString(d, "VERSION", v) < 0) { + Py_DECREF(v); + return -1; + } + Py_DECREF(v); + + if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { + return -1; + } + return 0; +} + +PyMODINIT_FUNC +PyInit__avif(void) { + PyObject *m; + + static PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + .m_name = "_avif", + .m_size = -1, + .m_methods = avifMethods, + }; + + m = PyModule_Create(&module_def); + if (setup_module(m) < 0) { + return NULL; + } + + return m; +} diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt new file mode 100644 index 00000000000..875b138ecf6 --- /dev/null +++ b/wheels/dependency_licenses/DAV1D.txt @@ -0,0 +1,23 @@ +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt new file mode 100644 index 00000000000..11bcb969bec --- /dev/null +++ b/wheels/dependency_licenses/LIBAVIF.txt @@ -0,0 +1,387 @@ +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: apps/shared/iccjpeg.* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt new file mode 100644 index 00000000000..c911747a6b5 --- /dev/null +++ b/wheels/dependency_licenses/LIBYUV.txt @@ -0,0 +1,29 @@ +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/PATENTS.txt b/wheels/dependency_licenses/PATENTS.txt new file mode 100644 index 00000000000..bcfb598238c --- /dev/null +++ b/wheels/dependency_licenses/PATENTS.txt @@ -0,0 +1,107 @@ +Alliance for Open Media Patent License 1.0 + +1. License Terms. + +1.1. Patent License. Subject to the terms and conditions of this License, each + Licensor, on behalf of itself and successors in interest and assigns, + grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, + no-charge, royalty-free, irrevocable (except as expressly stated in this + License) patent license to its Necessary Claims to make, use, sell, offer + for sale, import or distribute any Implementation. + +1.2. Conditions. + +1.2.1. Availability. As a condition to the grant of rights to Licensee to make, + sell, offer for sale, import or distribute an Implementation under + Section 1.1, Licensee must make its Necessary Claims available under + this License, and must reproduce this License with any Implementation + as follows: + + a. For distribution in source code, by including this License in the + root directory of the source code with its Implementation. + + b. For distribution in any other form (including binary, object form, + and/or hardware description code (e.g., HDL, RTL, Gate Level Netlist, + GDSII, etc.)), by including this License in the documentation, legal + notices, and/or other written materials provided with the + Implementation. + +1.2.2. Additional Conditions. This license is directly from Licensor to + Licensee. Licensee acknowledges as a condition of benefiting from it + that no rights from Licensor are received from suppliers, distributors, + or otherwise in connection with this License. + +1.3. Defensive Termination. If any Licensee, its Affiliates, or its agents + initiates patent litigation or files, maintains, or voluntarily + participates in a lawsuit against another entity or any person asserting + that any Implementation infringes Necessary Claims, any patent licenses + granted under this License directly to the Licensee are immediately + terminated as of the date of the initiation of action unless 1) that suit + was in response to a corresponding suit regarding an Implementation first + brought against an initiating entity, or 2) that suit was brought to + enforce the terms of this License (including intervention in a third-party + action by a Licensee). + +1.4. Disclaimers. The Reference Implementation and Specification are provided + "AS IS" and without warranty. The entire risk as to implementing or + otherwise using the Reference Implementation or Specification is assumed + by the implementer and user. Licensor expressly disclaims any warranties + (express, implied, or otherwise), including implied warranties of + merchantability, non-infringement, fitness for a particular purpose, or + title, related to the material. IN NO EVENT WILL LICENSOR BE LIABLE TO + ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, + INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF + ACTION OF ANY KIND WITH RESPECT TO THIS LICENSE, WHETHER BASED ON BREACH + OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR + NOT THE OTHER PARTRY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +2. Definitions. + +2.1. Affiliate. “Affiliate” means an entity that directly or indirectly + Controls, is Controlled by, or is under common Control of that party. + +2.2. Control. “Control” means direct or indirect control of more than 50% of + the voting power to elect directors of that corporation, or for any other + entity, the power to direct management of such entity. + +2.3. Decoder. "Decoder" means any decoder that conforms fully with all + non-optional portions of the Specification. + +2.4. Encoder. "Encoder" means any encoder that produces a bitstream that can + be decoded by a Decoder only to the extent it produces such a bitstream. + +2.5. Final Deliverable. “Final Deliverable” means the final version of a + deliverable approved by the Alliance for Open Media as a Final + Deliverable. + +2.6. Implementation. "Implementation" means any implementation, including the + Reference Implementation, that is an Encoder and/or a Decoder. An + Implementation also includes components of an Implementation only to the + extent they are used as part of an Implementation. + +2.7. License. “License” means this license. + +2.8. Licensee. “Licensee” means any person or entity who exercises patent + rights granted under this License. + +2.9. Licensor. "Licensor" means (i) any Licensee that makes, sells, offers + for sale, imports or distributes any Implementation, or (ii) a person + or entity that has a licensing obligation to the Implementation as a + result of its membership and/or participation in the Alliance for Open + Media working group that developed the Specification. + +2.10. Necessary Claims. "Necessary Claims" means all claims of patents or + patent applications, (a) that currently or at any time in the future, + are owned or controlled by the Licensor, and (b) (i) would be an + Essential Claim as defined by the W3C Policy as of February 5, 2004 + (https://www.w3.org/Consortium/Patent-Policy-20040205/#def-essential) + as if the Specification was a W3C Recommendation; or (ii) are infringed + by the Reference Implementation. + +2.11. Reference Implementation. “Reference Implementation” means an Encoder + and/or Decoder released by the Alliance for Open Media as a Final + Deliverable. + +2.12. Specification. “Specification” means the specification designated by + the Alliance for Open Media as a Final Deliverable for which this + License was issued. diff --git a/wheels/dependency_licenses/RAV1E.txt b/wheels/dependency_licenses/RAV1E.txt new file mode 100644 index 00000000000..4c6c3029a96 --- /dev/null +++ b/wheels/dependency_licenses/RAV1E.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2017-2021, the rav1e contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/SVT-AV1.txt b/wheels/dependency_licenses/SVT-AV1.txt new file mode 100644 index 00000000000..532a982b3ff --- /dev/null +++ b/wheels/dependency_licenses/SVT-AV1.txt @@ -0,0 +1,26 @@ +Copyright (c) 2019, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/winbuild/Findrav1e.cmake b/winbuild/Findrav1e.cmake new file mode 100644 index 00000000000..be1618bd4e7 --- /dev/null +++ b/winbuild/Findrav1e.cmake @@ -0,0 +1,10 @@ +file(TO_CMAKE_PATH "${AVIF_RAV1E_ROOT}" RAV1E_ROOT_PATH) +add_library(rav1e::rav1e STATIC IMPORTED GLOBAL) +set_target_properties( + rav1e::rav1e + PROPERTIES IMPORTED_LOCATION "${RAV1E_ROOT_PATH}/lib/rav1e.lib" + AVIF_LOCAL ON + INTERFACE_INCLUDE_DIRECTORIES "${RAV1E_ROOT_PATH}/inc/rav1e" + IMPORTED_SONAME rav1e) +target_link_libraries(rav1e::rav1e INTERFACE ntdll.lib userenv.lib ws2_32.lib + bcrypt.lib) diff --git a/winbuild/build.rst b/winbuild/build.rst index 96b8803b477..a0e6e601606 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -59,6 +59,7 @@ 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 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c8332d11c7e..8ee9cd90e54 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import shutil import struct import subprocess +import sys from typing import Any @@ -121,6 +122,9 @@ def cmd_msbuild( "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", + "MESON": "1.5.2", + "LIBAVIF": "1.1.1", + "RAV1E": "0.7.1", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -396,6 +400,56 @@ def cmd_msbuild( ], "bins": [r"*.dll"], }, + "rav1e": { + "url": ( + f"https://github.com/xiph/rav1e/releases/download/v{V['RAV1E']}/FILENAME" + ), + "filename": f"rav1e-{V['RAV1E']}-windows-msvc-generic.zip", + "dir": "rav1e-windows-msvc-sdk", + "license": "LICENSE", + "build": [ + cmd_xcopy("include", "{inc_dir}"), + ], + "bins": [r"bin\*.dll"], + "libs": [r"lib\*.*"], + }, + "libavif": { + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", + "filename": f"libavif-{V['LIBAVIF']}.zip", + "dir": f"libavif-{V['LIBAVIF']}", + "license": "LICENSE", + "build": [ + cmd_mkdir("build.pillow"), + cmd_cd("build.pillow"), + " ".join( + [ + "{cmake}", + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", + "-DCMAKE_C_COMPILER=cl.exe", + "-DCMAKE_CXX_COMPILER=cl.exe", + "-DCMAKE_C_FLAGS=-nologo", + "-DCMAKE_CXX_FLAGS=-nologo", + "-DBUILD_SHARED_LIBS=OFF", + "-DAVIF_CODEC_AOM=LOCAL", + "-DAVIF_LIBYUV=LOCAL", + "-DAVIF_LIBSHARPYUV=LOCAL", + "-DAVIF_CODEC_RAV1E=SYSTEM", + "-DAVIF_RAV1E_ROOT={build_dir}", + "-DCMAKE_MODULE_PATH={winbuild_dir_cmake}", + "-DAVIF_CODEC_DAV1D=LOCAL", + "-DAVIF_CODEC_SVT=LOCAL", + '-G "Ninja"', + "..", + ] + ), + "ninja -v", + cmd_cd(".."), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": [r"build.pillow\avif.lib"], + }, } @@ -619,12 +673,15 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> None: lines = [r'call "{build_dir}\build_env.cmd"'] gha_groups = "GITHUB_ACTIONS" in os.environ + scripts = ["install_meson.cmd"] for dep_name in DEPS: print() if dep_name in disabled: print(f"Skipping disabled dependency {dep_name}") continue - script = build_dep(dep_name, prefs, verbose) + scripts.append(build_dep(dep_name, prefs, verbose)) + + for script in scripts: if gha_groups: lines.append(f"@echo ::group::Running {script}") lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') @@ -698,6 +755,11 @@ def main() -> None: action="store_true", help="skip LGPL-licensed optional dependency FriBiDi", ) + parser.add_argument( + "--no-avif", + action="store_true", + help="skip optional dependency libavif", + ) args = parser.parse_args() arch_prefs = ARCHITECTURES[args.architecture] @@ -738,12 +800,15 @@ def main() -> None: disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] + if args.no_avif or args.architecture != "AMD64": + disabled += ["rav1e", "libavif"] prefs = { "architecture": args.architecture, **arch_prefs, # Pillow paths "winbuild_dir": winbuild_dir, + "winbuild_dir_cmake": winbuild_dir.replace("\\", "/"), # Build paths "bin_dir": bin_dir, "build_dir": args.build_dir, @@ -765,6 +830,18 @@ def main() -> None: print() write_script(".gitignore", ["*"], prefs, args.verbose) + write_script( + "install_meson.cmd", + [ + r'call "{build_dir}\build_env.cmd"', + "@echo " + ("=" * 70), + f"@echo ==== {'Building meson':<60} ====", + "@echo " + ("=" * 70), + f"{sys.executable} -m pip install meson=={V['MESON']}", + ], + prefs, + args.verbose, + ) build_env(prefs, args.verbose) build_dep_all(disabled, prefs, args.verbose)