mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-04-13 22:04:18 +03:00
Add AVIF plugin (decoder + encoder using libavif) (#5201)
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
This commit is contained in:
parent
999d9a7f0c
commit
7d50816f0a
|
@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||
sway wl-clipboard libopenblas-dev
|
||||
sway wl-clipboard libopenblas-dev nasm
|
||||
fi
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
|
@ -62,6 +62,9 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
# raqm
|
||||
pushd depends && ./install_raqm.sh && popd
|
||||
|
||||
# libavif
|
||||
pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
else
|
||||
|
|
7
.github/workflows/macos-install.sh
vendored
7
.github/workflows/macos-install.sh
vendored
|
@ -6,6 +6,8 @@ if [[ "$ImageOS" == "macos13" ]]; then
|
|||
brew uninstall gradle maven
|
||||
fi
|
||||
brew install \
|
||||
aom \
|
||||
dav1d \
|
||||
freetype \
|
||||
ghostscript \
|
||||
jpeg-turbo \
|
||||
|
@ -14,6 +16,8 @@ brew install \
|
|||
libtiff \
|
||||
little-cms2 \
|
||||
openjpeg \
|
||||
rav1e \
|
||||
svt-av1 \
|
||||
webp
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
|
||||
|
@ -27,5 +31,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
|
||||
|
|
1
.github/workflows/test-mingw.yml
vendored
1
.github/workflows/test-mingw.yml
vendored
|
@ -60,6 +60,7 @@ jobs:
|
|||
mingw-w64-x86_64-gcc \
|
||||
mingw-w64-x86_64-ghostscript \
|
||||
mingw-w64-x86_64-lcms2 \
|
||||
mingw-w64-x86_64-libavif \
|
||||
mingw-w64-x86_64-libimagequant \
|
||||
mingw-w64-x86_64-libjpeg-turbo \
|
||||
mingw-w64-x86_64-libraqm \
|
||||
|
|
6
.github/workflows/test-windows.yml
vendored
6
.github/workflows/test-windows.yml
vendored
|
@ -42,7 +42,7 @@ jobs:
|
|||
# Test the oldest Python on 32-bit
|
||||
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
|
||||
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
|
||||
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
|
||||
|
||||
|
@ -145,6 +145,10 @@ jobs:
|
|||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
run: "& winbuild\\build\\build_dep_libpng.cmd"
|
||||
|
||||
- name: Build dependencies / libavif
|
||||
if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64'
|
||||
run: "& winbuild\\build\\build_dep_libavif.cmd"
|
||||
|
||||
# for FreeType WOFF2 font support
|
||||
- name: Build dependencies / brotli
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
|
|
43
.github/workflows/wheels-dependencies.sh
vendored
43
.github/workflows/wheels-dependencies.sh
vendored
|
@ -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
|
||||
|
@ -51,6 +51,7 @@ LIBWEBP_VERSION=1.5.0
|
|||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.17.0
|
||||
BROTLI_VERSION=1.1.0
|
||||
LIBAVIF_VERSION=1.2.1
|
||||
|
||||
if [[ $MB_ML_VER == 2014 ]]; then
|
||||
function build_xz {
|
||||
|
@ -116,6 +117,45 @@ function build_harfbuzz {
|
|||
touch harfbuzz-stamp
|
||||
}
|
||||
|
||||
function build_libavif {
|
||||
if [ -e libavif-stamp ]; then return; fi
|
||||
|
||||
python3 -m pip install meson ninja
|
||||
|
||||
if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then
|
||||
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
|
||||
fi
|
||||
|
||||
# For rav1e
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
. "$HOME/.cargo/env"
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||
yum install -y perl
|
||||
if [[ "$MB_ML_VER" == 2014 ]]; then
|
||||
yum install -y perl-IPC-Cmd
|
||||
fi
|
||||
fi
|
||||
|
||||
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_POLICY_VERSION_MINIMUM=3.5 cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
|
||||
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DAVIF_LIBSHARPYUV=LOCAL \
|
||||
-DAVIF_LIBYUV=LOCAL \
|
||||
-DAVIF_CODEC_AOM=LOCAL \
|
||||
-DAVIF_CODEC_DAV1D=LOCAL \
|
||||
-DAVIF_CODEC_RAV1E=LOCAL \
|
||||
-DAVIF_CODEC_SVT=LOCAL \
|
||||
-DENABLE_NASM=ON \
|
||||
-DCMAKE_MODULE_PATH=/tmp/cmake/Modules \
|
||||
. \
|
||||
&& make install)
|
||||
touch libavif-stamp
|
||||
}
|
||||
|
||||
function build {
|
||||
build_xz
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||
|
@ -150,6 +190,7 @@ function build {
|
|||
build_tiff
|
||||
fi
|
||||
|
||||
build_libavif
|
||||
build_libpng
|
||||
build_lcms2
|
||||
build_openjpeg
|
||||
|
|
5
.github/workflows/wheels.yml
vendored
5
.github/workflows/wheels.yml
vendored
|
@ -160,6 +160,11 @@ jobs:
|
|||
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
|
||||
shell: pwsh
|
||||
|
||||
- name: Update rust
|
||||
if: matrix.cibw_arch == 'AMD64'
|
||||
run: |
|
||||
rustup update
|
||||
|
||||
- name: Build wheels
|
||||
run: |
|
||||
setlocal EnableDelayedExpansion
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from PIL import features
|
||||
|
@ -9,7 +10,7 @@ from .helper import is_pypy
|
|||
|
||||
|
||||
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:
|
||||
|
@ -19,6 +20,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
|
||||
|
||||
|
||||
|
|
BIN
Tests/images/avif/exif.avif
Normal file
BIN
Tests/images/avif/exif.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper-missing-pixi.avif
Normal file
BIN
Tests/images/avif/hopper-missing-pixi.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper.avif
Normal file
BIN
Tests/images/avif/hopper.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper.heif
Normal file
BIN
Tests/images/avif/hopper.heif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper_avif_write.png
Normal file
BIN
Tests/images/avif/hopper_avif_write.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
Tests/images/avif/icc_profile.avif
Normal file
BIN
Tests/images/avif/icc_profile.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/icc_profile_none.avif
Normal file
BIN
Tests/images/avif/icc_profile_none.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot0mir0.avif
Normal file
BIN
Tests/images/avif/rot0mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot0mir1.avif
Normal file
BIN
Tests/images/avif/rot0mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot1mir0.avif
Normal file
BIN
Tests/images/avif/rot1mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot1mir1.avif
Normal file
BIN
Tests/images/avif/rot1mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot2mir0.avif
Normal file
BIN
Tests/images/avif/rot2mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot2mir1.avif
Normal file
BIN
Tests/images/avif/rot2mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot3mir0.avif
Normal file
BIN
Tests/images/avif/rot3mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot3mir1.avif
Normal file
BIN
Tests/images/avif/rot3mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/star.avifs
Normal file
BIN
Tests/images/avif/star.avifs
Normal file
Binary file not shown.
BIN
Tests/images/avif/star.gif
Normal file
BIN
Tests/images/avif/star.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
Tests/images/avif/star.png
Normal file
BIN
Tests/images/avif/star.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
BIN
Tests/images/avif/transparency.avif
Normal file
BIN
Tests/images/avif/transparency.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/xmp_tags_orientation.avif
Normal file
BIN
Tests/images/avif/xmp_tags_orientation.avif
Normal file
Binary file not shown.
778
Tests/test_file_avif.py
Normal file
778
Tests/test_file_avif.py
Normal file
|
@ -0,0 +1,778 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import gc
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import Generator, Sequence
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
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
|
||||
return "qemu" in init_proc_exe
|
||||
|
||||
|
||||
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:
|
||||
version = features.version_module("avif")
|
||||
assert version is not None
|
||||
assert re.search(r"^\d+\.\d+\.\d+$", version)
|
||||
|
||||
def test_codec_version(self) -> None:
|
||||
assert AvifImagePlugin.get_codec_version("unknown") is None
|
||||
|
||||
for codec_name in ("aom", "dav1d", "rav1e", "svt"):
|
||||
codec_version = AvifImagePlugin.get_codec_version(codec_name)
|
||||
if _avif.decoder_codec_available(
|
||||
codec_name
|
||||
) or _avif.encoder_codec_available(codec_name):
|
||||
assert codec_version is not None
|
||||
assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version)
|
||||
else:
|
||||
assert codec_version is None
|
||||
|
||||
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", 11.5
|
||||
)
|
||||
|
||||
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?
|
||||
"""
|
||||
|
||||
temp_file = tmp_path / "temp.avif"
|
||||
|
||||
im = hopper()
|
||||
im.save(temp_file)
|
||||
with Image.open(temp_file) as reloaded:
|
||||
assert reloaded.mode == "RGB"
|
||||
assert reloaded.size == (128, 128)
|
||||
assert reloaded.format == "AVIF"
|
||||
reloaded.getdata()
|
||||
|
||||
# avifdec hopper.avif avif/hopper_avif_write.png
|
||||
assert_image_similar_tofile(
|
||||
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02
|
||||
)
|
||||
|
||||
# This test asserts that the images are similar. If the average pixel
|
||||
# difference between the two images is less than the epsilon value,
|
||||
# then we're going to accept that it's a reasonable lossy version of
|
||||
# the image.
|
||||
assert_image_similar(reloaded, im, 8.62)
|
||||
|
||||
def test_AvifEncoder_with_invalid_args(self) -> None:
|
||||
"""
|
||||
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_invalid_dimensions(self, tmp_path: Path) -> None:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
im = Image.new("RGB", (0, 0))
|
||||
with pytest.raises(ValueError):
|
||||
im.save(test_file)
|
||||
|
||||
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 = 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:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
|
||||
im.save(tmp_path / "temp.avif")
|
||||
|
||||
@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
|
||||
def test_accept_ftyp_brands(self, major_brand: bytes) -> None:
|
||||
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 = tmp_path / "temp.avif"
|
||||
im.save(out_avif, save_all=True)
|
||||
|
||||
# Save as GIF
|
||||
out_gif = 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 <= 3
|
||||
|
||||
def test_save_single_frame(self, tmp_path: Path) -> None:
|
||||
temp_file = 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 = 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 "icc_profile" not in im.info
|
||||
|
||||
with Image.open("Tests/images/avif/icc_profile.avif") as with_icc:
|
||||
expected_icc = with_icc.info["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 "icc_profile" not in im.info
|
||||
|
||||
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
|
||||
|
||||
@pytest.mark.parametrize("use_bytes", [True, False])
|
||||
@pytest.mark.parametrize("orientation", [1, 2])
|
||||
def test_exif_save(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
use_bytes: bool,
|
||||
orientation: int,
|
||||
) -> None:
|
||||
exif = Image.Exif()
|
||||
exif[274] = orientation
|
||||
exif_data = exif.tobytes()
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
im.save(test_file, exif=exif_data if use_bytes else exif)
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
if orientation == 1:
|
||||
assert "exif" not in reloaded.info
|
||||
else:
|
||||
assert reloaded.info["exif"] == exif_data
|
||||
|
||||
def test_exif_without_orientation(self, tmp_path: Path) -> None:
|
||||
exif = Image.Exif()
|
||||
exif[272] = b"test"
|
||||
exif_data = exif.tobytes()
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
im.save(test_file, exif=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 = tmp_path / "temp.avif"
|
||||
with pytest.raises(SyntaxError):
|
||||
im.save(test_file, exif=b"invalid")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rot, mir, exif_orientation",
|
||||
[
|
||||
(0, 0, 4),
|
||||
(0, 1, 2),
|
||||
(1, 0, 5),
|
||||
(1, 1, 7),
|
||||
(2, 0, 2),
|
||||
(2, 1, 4),
|
||||
(3, 0, 7),
|
||||
(3, 1, 5),
|
||||
],
|
||||
)
|
||||
def test_rot_mir_exif(
|
||||
self, rot: int, mir: int, exif_orientation: int, tmp_path: Path
|
||||
) -> None:
|
||||
with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im:
|
||||
exif = im.getexif()
|
||||
assert exif[274] == exif_orientation
|
||||
|
||||
test_file = tmp_path / "temp.avif"
|
||||
im.save(test_file, exif=exif)
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert reloaded.getexif()[274] == exif_orientation
|
||||
|
||||
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:
|
||||
xmp_arg = "\n".join(
|
||||
[
|
||||
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>',
|
||||
'<x:xmpmeta xmlns:x="adobe:ns:meta/">',
|
||||
' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">',
|
||||
' <rdf:Description rdf:about=""',
|
||||
' xmlns:tiff="http://ns.adobe.com/tiff/1.0/"',
|
||||
' tiff:Orientation="1"/>',
|
||||
" </rdf:RDF>",
|
||||
"</x:xmpmeta>",
|
||||
'<?xpacket end="r"?>',
|
||||
]
|
||||
)
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = 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:2:0", "4:0:0"])
|
||||
def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None:
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = 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 = tmp_path / "temp.avif"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(test_file, subsampling="foo")
|
||||
|
||||
@pytest.mark.parametrize("value", ["full", "limited"])
|
||||
def test_encoder_range(self, tmp_path: Path, value: str) -> None:
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
im.save(test_file, range=value)
|
||||
|
||||
def test_encoder_range_invalid(self, tmp_path: Path) -> None:
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(test_file, range="foo")
|
||||
|
||||
@skip_unless_avif_encoder("aom")
|
||||
def test_encoder_codec_param(self, tmp_path: Path) -> None:
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = 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 = tmp_path / "temp.avif"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(test_file, codec="foo")
|
||||
|
||||
@skip_unless_avif_decoder("dav1d")
|
||||
def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None:
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(test_file, codec="dav1d")
|
||||
|
||||
@skip_unless_avif_encoder("aom")
|
||||
@pytest.mark.parametrize(
|
||||
"advanced",
|
||||
[
|
||||
{
|
||||
"aq-mode": "1",
|
||||
"enable-chroma-deltaq": "1",
|
||||
},
|
||||
(("aq-mode", "1"), ("enable-chroma-deltaq", "1")),
|
||||
[("aq-mode", "1"), ("enable-chroma-deltaq", "1")],
|
||||
],
|
||||
)
|
||||
def test_encoder_advanced_codec_options(
|
||||
self, advanced: dict[str, str] | Sequence[tuple[str, str]]
|
||||
) -> 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=advanced,
|
||||
)
|
||||
assert ctrl_buf.getvalue() != test_buf.getvalue()
|
||||
|
||||
@skip_unless_avif_encoder("aom")
|
||||
@pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234])
|
||||
def test_encoder_advanced_codec_options_invalid(
|
||||
self, tmp_path: Path, advanced: dict[str, str] | int
|
||||
) -> None:
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(test_file, codec="aom", advanced=advanced)
|
||||
|
||||
@skip_unless_avif_decoder("aom")
|
||||
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")
|
||||
def test_encoder_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")
|
||||
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")
|
||||
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 = tmp_path / "temp.avif"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(test_file, quality="invalid")
|
||||
|
||||
@skip_unless_avif_decoder("aom")
|
||||
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")
|
||||
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, tmp_path: Path) -> 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)
|
||||
|
||||
out_png = tmp_path / "temp.png"
|
||||
im.save(out_png, transparency=0)
|
||||
with Image.open(out_png) as im_png:
|
||||
out_avif = tmp_path / "temp.avif"
|
||||
im_png.save(out_avif, quality=100)
|
||||
|
||||
with Image.open(out_avif) as expected:
|
||||
assert_image_similar(im_png.convert("RGBA"), expected, 0.17)
|
||||
|
||||
def test_decoder_strict_flags(self) -> None:
|
||||
# This would fail if full avif strictFlags were enabled
|
||||
with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im:
|
||||
assert im.size == (128, 128)
|
||||
|
||||
@skip_unless_avif_encoder("aom")
|
||||
@pytest.mark.parametrize("speed", [-1, 1, 11])
|
||||
def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
hopper().save(test_file, codec="aom", speed=speed)
|
||||
|
||||
@skip_unless_avif_encoder("svt")
|
||||
def test_svt_optimizations(self, tmp_path: Path) -> None:
|
||||
test_file = tmp_path / "temp.avif"
|
||||
hopper().save(test_file, codec="svt", speed=1)
|
||||
|
||||
|
||||
@skip_unless_feature("avif")
|
||||
class TestAvifAnimation:
|
||||
@contextmanager
|
||||
def star_frames(self) -> Generator[list[Image.Image], None, None]:
|
||||
with Image.open("Tests/images/avif/star.png") as f:
|
||||
yield [f, f.rotate(90), f.rotate(180), f.rotate(270)]
|
||||
|
||||
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_P(self, tmp_path: Path) -> None:
|
||||
"""
|
||||
Convert an animated GIF to animated AVIF, then compare the frame
|
||||
count, and ensure the frames are visually similar to the originals.
|
||||
"""
|
||||
|
||||
with Image.open("Tests/images/avif/star.gif") as original:
|
||||
assert original.n_frames > 1
|
||||
|
||||
temp_file = tmp_path / "temp.avif"
|
||||
original.save(temp_file, save_all=True)
|
||||
with Image.open(temp_file) as im:
|
||||
assert im.n_frames == original.n_frames
|
||||
|
||||
# Compare first frame in P mode to frame from original GIF
|
||||
assert_image_similar(im, original.convert("RGBA"), 2)
|
||||
|
||||
# Compare later frames in RGBA mode to frames from original GIF
|
||||
for frame in range(1, original.n_frames):
|
||||
original.seek(frame)
|
||||
im.seek(frame)
|
||||
assert_image_similar(im, original, 2.54)
|
||||
|
||||
def test_write_animation_RGBA(self, tmp_path: Path) -> None:
|
||||
"""
|
||||
Write an animated AVIF from RGBA frames, and ensure the frames
|
||||
are visually similar to the originals.
|
||||
"""
|
||||
|
||||
def check(temp_file: Path) -> None:
|
||||
with Image.open(temp_file) as im:
|
||||
assert im.n_frames == 4
|
||||
|
||||
# Compare first frame to original
|
||||
assert_image_similar(im, frame1, 2.7)
|
||||
|
||||
# Compare second frame to original
|
||||
im.seek(1)
|
||||
assert_image_similar(im, frame2, 4.1)
|
||||
|
||||
with self.star_frames() as frames:
|
||||
frame1 = frames[0]
|
||||
frame2 = frames[1]
|
||||
temp_file1 = tmp_path / "temp.avif"
|
||||
frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:])
|
||||
check(temp_file1)
|
||||
|
||||
# Test appending using a generator
|
||||
def imGenerator(
|
||||
ims: list[Image.Image],
|
||||
) -> Generator[Image.Image, None, None]:
|
||||
yield from ims
|
||||
|
||||
temp_file2 = 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 = 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/hopper.heif"):
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("alpha_premultiplied", [False, True])
|
||||
def test_alpha_premultiplied(
|
||||
self, tmp_path: Path, alpha_premultiplied: bool
|
||||
) -> None:
|
||||
temp_file = tmp_path / "temp.avif"
|
||||
color = (200, 200, 200, 1)
|
||||
im = Image.new("RGBA", (1, 1), color)
|
||||
im.save(temp_file, alpha_premultiplied=alpha_premultiplied)
|
||||
|
||||
expected = (255, 255, 255, 1) if alpha_premultiplied else color
|
||||
with Image.open(temp_file) as reloaded:
|
||||
assert reloaded.getpixel((0, 0)) == expected
|
||||
|
||||
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 = 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
|
||||
timestamp = 0
|
||||
for frame in range(im.n_frames):
|
||||
im.seek(frame)
|
||||
im.load()
|
||||
assert im.info["duration"] == durations[frame]
|
||||
assert im.info["timestamp"] == timestamp
|
||||
timestamp += 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.
|
||||
"""
|
||||
|
||||
duration = 33
|
||||
temp_file = 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=duration,
|
||||
)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert im.n_frames == 5
|
||||
assert im.is_animated
|
||||
|
||||
# Traverse frames in reverse, checking timestamps and durations
|
||||
timestamp = duration * (im.n_frames - 1)
|
||||
for frame in reversed(range(im.n_frames)):
|
||||
im.seek(frame)
|
||||
im.load()
|
||||
assert im.info["duration"] == duration
|
||||
assert im.info["timestamp"] == timestamp
|
||||
timestamp -= duration
|
||||
|
||||
def test_seek_errors(self) -> None:
|
||||
with Image.open("Tests/images/avif/star.avifs") as im:
|
||||
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)
|
64
depends/install_libavif.sh
Executable file
64
depends/install_libavif.sh
Executable file
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
version=1.2.1
|
||||
|
||||
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
|
||||
|
||||
pushd libavif-$version
|
||||
|
||||
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
|
||||
PREFIX=$(brew --prefix)
|
||||
else
|
||||
PREFIX=/usr
|
||||
fi
|
||||
|
||||
PKGCONFIG=${PKGCONFIG:-pkg-config}
|
||||
|
||||
LIBAVIF_CMAKE_FLAGS=()
|
||||
HAS_DECODER=0
|
||||
HAS_ENCODER=0
|
||||
|
||||
if $PKGCONFIG --exists aom; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
|
||||
HAS_ENCODER=1
|
||||
HAS_DECODER=1
|
||||
fi
|
||||
|
||||
if $PKGCONFIG --exists dav1d; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
|
||||
HAS_DECODER=1
|
||||
fi
|
||||
|
||||
if $PKGCONFIG --exists libgav1; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
|
||||
HAS_DECODER=1
|
||||
fi
|
||||
|
||||
if $PKGCONFIG --exists rav1e; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
|
||||
HAS_ENCODER=1
|
||||
fi
|
||||
|
||||
if $PKGCONFIG --exists SvtAv1Enc; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
|
||||
HAS_ENCODER=1
|
||||
fi
|
||||
|
||||
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
|
||||
fi
|
||||
|
||||
cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=$PREFIX \
|
||||
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_MACOSX_RPATH=OFF \
|
||||
-DAVIF_LIBSHARPYUV=LOCAL \
|
||||
-DAVIF_LIBYUV=LOCAL \
|
||||
"${LIBAVIF_CMAKE_FLAGS[@]}" \
|
||||
.
|
||||
|
||||
sudo make install
|
||||
|
||||
popd
|
|
@ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
|
|||
Fully supported formats
|
||||
-----------------------
|
||||
|
||||
AVIF
|
||||
^^^^
|
||||
|
||||
Pillow reads and writes AVIF files, including AVIF sequence images.
|
||||
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, 0-100, defaults to 75. 0 gives the smallest size and poorest
|
||||
quality, 100 the largest size and best 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 6.
|
||||
|
||||
**max_threads**
|
||||
Limit the number of active threads used. By default, there is no limit. If the aom
|
||||
codec is used, there is a maximum of 64.
|
||||
|
||||
**range**
|
||||
YUV range, either "full" or "limited". Defaults to "full".
|
||||
|
||||
**codec**
|
||||
AV1 codec to use for encoding. Specific values are "aom", "rav1e", and
|
||||
"svt", presuming the chosen codec is available. 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. Ignored if "autotiling" is set to true.
|
||||
|
||||
**autotiling**
|
||||
Split the image up to allow parallelization. Enabled automatically if "tile_rows"
|
||||
and "tile_cols" both have their default values of zero.
|
||||
|
||||
**alpha_premultiplied**
|
||||
Encode the image with premultiplied alpha. Defaults to ``False``.
|
||||
|
||||
**advanced**
|
||||
Codec specific options.
|
||||
|
||||
**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.
|
||||
|
||||
BLP
|
||||
^^^
|
||||
|
||||
|
@ -242,7 +319,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 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.
|
||||
|
|
|
@ -89,6 +89,14 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **libxcb** provides X11 screengrab support.
|
||||
|
||||
* **libavif** provides support for the AVIF format.
|
||||
|
||||
* Pillow requires libavif version **1.0.0** or greater.
|
||||
* 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 +125,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 \
|
||||
|
@ -148,7 +162,15 @@ Many of Pillow's features require external libraries:
|
|||
The easiest way to install external libraries is via `Homebrew
|
||||
<https://brew.sh/>`_. After you install Homebrew, run::
|
||||
|
||||
brew install libjpeg libraqm libtiff little-cms2 openjpeg webp
|
||||
brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp
|
||||
|
||||
If you would like to use libavif with more codecs than just aom, then
|
||||
instead of installing libavif through Homebrew directly, you can use
|
||||
Homebrew to install libavif's build dependencies::
|
||||
|
||||
brew install aom dav1d rav1e svt-av1
|
||||
|
||||
Then see ``depends/install_libavif.sh`` to install libavif.
|
||||
|
||||
.. tab:: Windows
|
||||
|
||||
|
@ -187,7 +209,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
|
||||
|
||||
|
@ -199,7 +222,7 @@ 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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
Plugin reference
|
||||
================
|
||||
|
||||
:mod:`~PIL.AvifImagePlugin` Module
|
||||
----------------------------------
|
||||
|
||||
.. automodule:: PIL.AvifImagePlugin
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL.BmpImagePlugin` Module
|
||||
---------------------------------
|
||||
|
||||
|
|
|
@ -68,3 +68,12 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1,
|
|||
DXT5, BC2, BC3 and BC5 are supported::
|
||||
|
||||
im.save("out.dds", pixel_format="DXT1")
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
Reading and writing AVIF images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Pillow can now read and write AVIF images. If you are building Pillow from source, this
|
||||
will require libavif 1.0.0 or later.
|
||||
|
|
19
setup.py
19
setup.py
|
@ -32,6 +32,7 @@ configuration: dict[str, list[str]] = {}
|
|||
|
||||
|
||||
PILLOW_VERSION = get_version()
|
||||
AVIF_ROOT = None
|
||||
FREETYPE_ROOT = None
|
||||
HARFBUZZ_ROOT = None
|
||||
FRIBIDI_ROOT = None
|
||||
|
@ -306,6 +307,7 @@ class pil_build_ext(build_ext):
|
|||
"jpeg2000",
|
||||
"imagequant",
|
||||
"xcb",
|
||||
"avif",
|
||||
]
|
||||
|
||||
required = {"jpeg", "zlib"}
|
||||
|
@ -481,6 +483,7 @@ class pil_build_ext(build_ext):
|
|||
#
|
||||
# add configured kits
|
||||
for root_name, lib_name in {
|
||||
"AVIF_ROOT": "avif",
|
||||
"JPEG_ROOT": "libjpeg",
|
||||
"JPEG2K_ROOT": "libopenjp2",
|
||||
"TIFF_ROOT": ("libtiff-5", "libtiff-4"),
|
||||
|
@ -846,6 +849,12 @@ class pil_build_ext(build_ext):
|
|||
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"):
|
||||
|
@ -934,6 +943,14 @@ class pil_build_ext(build_ext):
|
|||
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)
|
||||
|
||||
|
@ -976,6 +993,7 @@ class pil_build_ext(build_ext):
|
|||
(feature.get("lcms"), "LITTLECMS2"),
|
||||
(feature.get("webp"), "WEBP"),
|
||||
(feature.get("xcb"), "XCB (X protocol)"),
|
||||
(feature.get("avif"), "LIBAVIF"),
|
||||
]
|
||||
|
||||
all = 1
|
||||
|
@ -1018,6 +1036,7 @@ ext_modules = [
|
|||
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"]),
|
||||
|
|
292
src/PIL/AvifImagePlugin.py
Normal file
292
src/PIL/AvifImagePlugin.py
Normal file
|
@ -0,0 +1,292 @@
|
|||
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"
|
||||
# Decoding is only affected by this for libavif **0.8.4** or greater.
|
||||
DEFAULT_MAX_THREADS = 0
|
||||
|
||||
|
||||
def get_codec_version(codec_name: str) -> str | None:
|
||||
versions = _avif.codec_versions()
|
||||
for version in versions.split(", "):
|
||||
if version.split(" [")[0] == codec_name:
|
||||
return version.split(":")[-1].split(" ")[0]
|
||||
return None
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool | str:
|
||||
if prefix[4:8] != b"ftyp":
|
||||
return False
|
||||
major_brand = prefix[8:12]
|
||||
if major_brand in (
|
||||
# coding brands
|
||||
b"avif",
|
||||
b"avis",
|
||||
# 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.
|
||||
b"mif1",
|
||||
b"msf1",
|
||||
):
|
||||
if not SUPPORTED:
|
||||
return (
|
||||
"image file could not be identified because AVIF support not installed"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_default_max_threads() -> int:
|
||||
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"
|
||||
__frame = -1
|
||||
|
||||
def _open(self) -> None:
|
||||
if not SUPPORTED:
|
||||
msg = "image file could not be opened because AVIF support not installed"
|
||||
raise SyntaxError(msg)
|
||||
|
||||
if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available(
|
||||
DECODE_CODEC_CHOICE
|
||||
):
|
||||
msg = "Invalid opening codec"
|
||||
raise ValueError(msg)
|
||||
self._decoder = _avif.AvifDecoder(
|
||||
self.fp.read(),
|
||||
DECODE_CODEC_CHOICE,
|
||||
_get_default_max_threads(),
|
||||
)
|
||||
|
||||
# Get info from decoder
|
||||
self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = (
|
||||
self._decoder.get_info()
|
||||
)
|
||||
self.is_animated = self.n_frames > 1
|
||||
|
||||
if icc:
|
||||
self.info["icc_profile"] = icc
|
||||
if xmp:
|
||||
self.info["xmp"] = xmp
|
||||
|
||||
if exif_orientation != 1 or exif:
|
||||
exif_data = Image.Exif()
|
||||
if exif:
|
||||
exif_data.load(exif)
|
||||
original_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
|
||||
else:
|
||||
original_orientation = 1
|
||||
if exif_orientation != original_orientation:
|
||||
exif_data[ExifTags.Base.Orientation] = exif_orientation
|
||||
exif = exif_data.tobytes()
|
||||
if exif:
|
||||
self.info["exif"] = exif
|
||||
self.seek(0)
|
||||
|
||||
def seek(self, frame: int) -> None:
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
|
||||
# Set tile
|
||||
self.__frame = frame
|
||||
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
|
||||
|
||||
def load(self) -> Image.core.PixelAccess | None:
|
||||
if self.tile:
|
||||
# We need to load the image data for this frame
|
||||
data, timescale, pts_in_timescales, duration_in_timescales = (
|
||||
self._decoder.get_frame(self.__frame)
|
||||
)
|
||||
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
|
||||
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
|
||||
|
||||
if self.fp and self._exclusive_fp:
|
||||
self.fp.close()
|
||||
self.fp = BytesIO(data)
|
||||
|
||||
return super().load()
|
||||
|
||||
def load_seek(self, pos: int) -> None:
|
||||
pass
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
if codec != "auto" and not _avif.encoder_codec_available(codec):
|
||||
msg = "Invalid saving codec"
|
||||
raise ValueError(msg)
|
||||
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_orientation = 1
|
||||
if exif := info.get("exif"):
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif_data = exif
|
||||
else:
|
||||
exif_data = Image.Exif()
|
||||
exif_data.load(exif)
|
||||
if ExifTags.Base.Orientation in exif_data:
|
||||
exif_orientation = exif_data.pop(ExifTags.Base.Orientation)
|
||||
exif = exif_data.tobytes() if exif_data else b""
|
||||
elif isinstance(exif, Image.Exif):
|
||||
exif = exif_data.tobytes()
|
||||
|
||||
xmp = info.get("xmp")
|
||||
|
||||
if isinstance(xmp, str):
|
||||
xmp = xmp.encode("utf-8")
|
||||
|
||||
advanced = info.get("advanced")
|
||||
if advanced is not None:
|
||||
if isinstance(advanced, dict):
|
||||
advanced = advanced.items()
|
||||
try:
|
||||
advanced = tuple(advanced)
|
||||
except TypeError:
|
||||
invalid = True
|
||||
else:
|
||||
invalid = any(not isinstance(v, tuple) or 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)
|
||||
|
||||
# Setup the AVIF encoder
|
||||
enc = _avif.AvifEncoder(
|
||||
im.size,
|
||||
subsampling,
|
||||
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_duration = 0
|
||||
cur_idx = im.tell()
|
||||
is_single_frame = total == 1
|
||||
try:
|
||||
for ims in [im] + append_images:
|
||||
# Get number 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 {"RGB", "RGBA"}:
|
||||
rawmode = "RGBA" if ims.has_transparency_data else "RGB"
|
||||
frame = ims.convert(rawmode)
|
||||
|
||||
# Update frame duration
|
||||
if isinstance(duration, (list, tuple)):
|
||||
frame_duration = duration[frame_idx]
|
||||
else:
|
||||
frame_duration = duration
|
||||
|
||||
# Append the frame to the animation encoder
|
||||
enc.add(
|
||||
frame.tobytes("raw", rawmode),
|
||||
frame_duration,
|
||||
frame.size,
|
||||
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")
|
|
@ -1520,6 +1520,8 @@ class Image:
|
|||
# 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:
|
||||
|
|
|
@ -25,6 +25,7 @@ del _version
|
|||
|
||||
|
||||
_plugins = [
|
||||
"AvifImagePlugin",
|
||||
"BlpImagePlugin",
|
||||
"BmpImagePlugin",
|
||||
"BufrStubImagePlugin",
|
||||
|
|
3
src/PIL/_avif.pyi
Normal file
3
src/PIL/_avif.pyi
Normal file
|
@ -0,0 +1,3 @@
|
|||
from typing import Any
|
||||
|
||||
def __getattr__(name: str) -> Any: ...
|
|
@ -17,6 +17,7 @@ modules = {
|
|||
"freetype2": ("PIL._imagingft", "freetype2_version"),
|
||||
"littlecms2": ("PIL._imagingcms", "littlecms_version"),
|
||||
"webp": ("PIL._webp", "webpdecoder_version"),
|
||||
"avif": ("PIL._avif", "libavif_version"),
|
||||
}
|
||||
|
||||
|
||||
|
@ -288,6 +289,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
|
|||
("freetype2", "FREETYPE2"),
|
||||
("littlecms2", "LITTLECMS2"),
|
||||
("webp", "WEBP"),
|
||||
("avif", "AVIF"),
|
||||
("jpg", "JPEG"),
|
||||
("jpg_2000", "OPENJPEG (JPEG2000)"),
|
||||
("zlib", "ZLIB (PNG/ZIP)"),
|
||||
|
|
908
src/_avif.c
Normal file
908
src/_avif.c
Normal file
|
@ -0,0 +1,908 @@
|
|||
#define PY_SSIZE_T_CLEAN
|
||||
|
||||
#include <Python.h>
|
||||
#include "avif/avif.h"
|
||||
|
||||
// Encoder type
|
||||
typedef struct {
|
||||
PyObject_HEAD avifEncoder *encoder;
|
||||
avifImage *image;
|
||||
int first_frame;
|
||||
} AvifEncoderObject;
|
||||
|
||||
static PyTypeObject AvifEncoder_Type;
|
||||
|
||||
// Decoder type
|
||||
typedef struct {
|
||||
PyObject_HEAD avifDecoder *decoder;
|
||||
Py_buffer buffer;
|
||||
} AvifDecoderObject;
|
||||
|
||||
static PyTypeObject AvifDecoder_Type;
|
||||
|
||||
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 uint8_t
|
||||
irot_imir_to_exif_orientation(const avifImage *image) {
|
||||
uint8_t axis = image->imir.axis;
|
||||
int imir = image->transformFlags & AVIF_TRANSFORM_IMIR;
|
||||
int irot = image->transformFlags & AVIF_TRANSFORM_IROT;
|
||||
if (irot) {
|
||||
uint8_t angle = image->irot.angle;
|
||||
if (angle == 1) {
|
||||
if (imir) {
|
||||
return axis ? 7 // 90 degrees anti-clockwise then swap left and right.
|
||||
: 5; // 90 degrees anti-clockwise then swap top and bottom.
|
||||
}
|
||||
return 6; // 90 degrees anti-clockwise.
|
||||
}
|
||||
if (angle == 2) {
|
||||
if (imir) {
|
||||
return axis
|
||||
? 4 // 180 degrees anti-clockwise then swap left and right.
|
||||
: 2; // 180 degrees anti-clockwise then swap top and bottom.
|
||||
}
|
||||
return 3; // 180 degrees anti-clockwise.
|
||||
}
|
||||
if (angle == 3) {
|
||||
if (imir) {
|
||||
return axis
|
||||
? 5 // 270 degrees anti-clockwise then swap left and right.
|
||||
: 7; // 270 degrees anti-clockwise then swap top and bottom.
|
||||
}
|
||||
return 8; // 270 degrees anti-clockwise.
|
||||
}
|
||||
}
|
||||
if (imir) {
|
||||
return axis ? 2 // Swap left and right.
|
||||
: 4; // Swap top and bottom.
|
||||
}
|
||||
return 1; // Default orientation ("top-left", no-op).
|
||||
}
|
||||
|
||||
static void
|
||||
exif_orientation_to_irot_imir(avifImage *image, int orientation) {
|
||||
// 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 2: // The 0th row is at the visual top of the image, and the 0th column is
|
||||
// the visual right-hand side.
|
||||
image->transformFlags |= AVIF_TRANSFORM_IMIR;
|
||||
image->imir.axis = 1;
|
||||
break;
|
||||
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 |= AVIF_TRANSFORM_IROT;
|
||||
image->irot.angle = 2;
|
||||
break;
|
||||
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 |= AVIF_TRANSFORM_IMIR;
|
||||
break;
|
||||
case 5: // The 0th row is the visual left-hand side of the image, and the 0th
|
||||
// column is the visual top.
|
||||
image->transformFlags |= 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
|
||||
break;
|
||||
case 6: // The 0th row is the visual right-hand side of the image, and the 0th
|
||||
// column is the visual top.
|
||||
image->transformFlags |= AVIF_TRANSFORM_IROT;
|
||||
image->irot.angle = 3;
|
||||
break;
|
||||
case 7: // The 0th row is the visual right-hand side of the image, and the 0th
|
||||
// column is the visual bottom.
|
||||
image->transformFlags |= 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
|
||||
break;
|
||||
case 8: // The 0th row is the visual left-hand side of the image, and the 0th
|
||||
// column is the visual bottom.
|
||||
image->transformFlags |= AVIF_TRANSFORM_IROT;
|
||||
image->irot.angle = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
_codec_available(const char *name, avifCodecFlags 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);
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_codec_versions(PyObject *self, PyObject *args) {
|
||||
char buffer[256];
|
||||
avifCodecVersions(buffer);
|
||||
return PyUnicode_FromString(buffer);
|
||||
}
|
||||
|
||||
static int
|
||||
_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) {
|
||||
Py_ssize_t i, size;
|
||||
PyObject *keyval, *py_key, *py_val;
|
||||
if (!PyTuple_Check(opts)) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
|
||||
return 1;
|
||||
}
|
||||
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) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
|
||||
return 1;
|
||||
}
|
||||
py_key = PyTuple_GetItem(keyval, 0);
|
||||
py_val = PyTuple_GetItem(keyval, 1);
|
||||
if (!PyUnicode_Check(py_key) || !PyUnicode_Check(py_val)) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
|
||||
return 1;
|
||||
}
|
||||
const char *key = PyUnicode_AsUTF8(py_key);
|
||||
const char *val = PyUnicode_AsUTF8(py_val);
|
||||
if (key == NULL || val == NULL) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
|
||||
return 1;
|
||||
}
|
||||
|
||||
avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
PyErr_Format(
|
||||
exc_type_for_avif_result(result),
|
||||
"Setting advanced codec options failed: %s",
|
||||
avifResultToString(result)
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Encoder functions
|
||||
PyObject *
|
||||
AvifEncoderNew(PyObject *self_, PyObject *args) {
|
||||
unsigned int width, height;
|
||||
AvifEncoderObject *self = NULL;
|
||||
avifEncoder *encoder = NULL;
|
||||
|
||||
char *subsampling;
|
||||
int quality;
|
||||
int speed;
|
||||
int exif_orientation;
|
||||
int max_threads;
|
||||
Py_buffer icc_buffer;
|
||||
Py_buffer exif_buffer;
|
||||
Py_buffer xmp_buffer;
|
||||
int alpha_premultiplied;
|
||||
int autotiling;
|
||||
int tile_rows_log2;
|
||||
int tile_cols_log2;
|
||||
|
||||
char *codec;
|
||||
char *range;
|
||||
|
||||
PyObject *advanced;
|
||||
int error = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(
|
||||
args,
|
||||
"(II)siiissiippy*y*iy*O",
|
||||
&width,
|
||||
&height,
|
||||
&subsampling,
|
||||
&quality,
|
||||
&speed,
|
||||
&max_threads,
|
||||
&codec,
|
||||
&range,
|
||||
&tile_rows_log2,
|
||||
&tile_cols_log2,
|
||||
&alpha_premultiplied,
|
||||
&autotiling,
|
||||
&icc_buffer,
|
||||
&exif_buffer,
|
||||
&exif_orientation,
|
||||
&xmp_buffer,
|
||||
&advanced
|
||||
)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create a new animation encoder and picture frame
|
||||
avifImage *image = avifImageCreateEmpty();
|
||||
if (image == NULL) {
|
||||
PyErr_SetString(PyExc_ValueError, "Image creation failed");
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
// Set these in advance so any upcoming RGB -> YUV use the proper coefficients
|
||||
if (strcmp(range, "full") == 0) {
|
||||
image->yuvRange = AVIF_RANGE_FULL;
|
||||
} else if (strcmp(range, "limited") == 0) {
|
||||
image->yuvRange = AVIF_RANGE_LIMITED;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid range");
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
if (strcmp(subsampling, "4:0:0") == 0) {
|
||||
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
|
||||
} else if (strcmp(subsampling, "4:2:0") == 0) {
|
||||
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420;
|
||||
} else if (strcmp(subsampling, "4:2:2") == 0) {
|
||||
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422;
|
||||
} else if (strcmp(subsampling, "4:4:4") == 0) {
|
||||
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444;
|
||||
} else {
|
||||
PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling);
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
// Validate canvas dimensions
|
||||
if (width == 0 || height == 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions");
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
image->width = width;
|
||||
image->height = height;
|
||||
|
||||
image->depth = 8;
|
||||
image->alphaPremultiplied = alpha_premultiplied ? AVIF_TRUE : AVIF_FALSE;
|
||||
|
||||
encoder = avifEncoderCreate();
|
||||
if (!encoder) {
|
||||
PyErr_SetString(PyExc_MemoryError, "Can't allocate encoder");
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
encoder->quality = quality;
|
||||
|
||||
if (strcmp(codec, "auto") == 0) {
|
||||
encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO;
|
||||
} else {
|
||||
encoder->codecChoice = avifCodecChoiceFromName(codec);
|
||||
}
|
||||
if (speed < AVIF_SPEED_SLOWEST) {
|
||||
speed = AVIF_SPEED_SLOWEST;
|
||||
} else if (speed > AVIF_SPEED_FASTEST) {
|
||||
speed = AVIF_SPEED_FASTEST;
|
||||
}
|
||||
encoder->speed = speed;
|
||||
encoder->timescale = (uint64_t)1000;
|
||||
|
||||
encoder->autoTiling = autotiling ? AVIF_TRUE : AVIF_FALSE;
|
||||
if (!autotiling) {
|
||||
encoder->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2);
|
||||
encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2);
|
||||
}
|
||||
|
||||
if (advanced != Py_None && _add_codec_specific_options(encoder, advanced)) {
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type);
|
||||
if (!self) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "could not create encoder object");
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
self->first_frame = 1;
|
||||
|
||||
avifResult result;
|
||||
if (icc_buffer.len) {
|
||||
result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
PyErr_Format(
|
||||
exc_type_for_avif_result(result),
|
||||
"Setting ICC profile failed: %s",
|
||||
avifResultToString(result)
|
||||
);
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
// colorPrimaries and transferCharacteristics are ignored when an ICC
|
||||
// profile is present, so set them to UNSPECIFIED.
|
||||
image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED;
|
||||
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED;
|
||||
} else {
|
||||
image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709;
|
||||
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
|
||||
}
|
||||
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
|
||||
|
||||
if (exif_buffer.len) {
|
||||
result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
PyErr_Format(
|
||||
exc_type_for_avif_result(result),
|
||||
"Setting EXIF data failed: %s",
|
||||
avifResultToString(result)
|
||||
);
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
if (xmp_buffer.len) {
|
||||
result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
PyErr_Format(
|
||||
exc_type_for_avif_result(result),
|
||||
"Setting XMP data failed: %s",
|
||||
avifResultToString(result)
|
||||
);
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
if (exif_orientation > 1) {
|
||||
exif_orientation_to_irot_imir(image, exif_orientation);
|
||||
}
|
||||
|
||||
self->image = image;
|
||||
self->encoder = encoder;
|
||||
|
||||
end:
|
||||
PyBuffer_Release(&icc_buffer);
|
||||
PyBuffer_Release(&exif_buffer);
|
||||
PyBuffer_Release(&xmp_buffer);
|
||||
|
||||
if (error) {
|
||||
if (image) {
|
||||
avifImageDestroy(image);
|
||||
}
|
||||
if (encoder) {
|
||||
avifEncoderDestroy(encoder);
|
||||
}
|
||||
if (self) {
|
||||
PyObject_Del(self);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return (PyObject *)self;
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_encoder_dealloc(AvifEncoderObject *self) {
|
||||
if (self->encoder) {
|
||||
avifEncoderDestroy(self->encoder);
|
||||
}
|
||||
if (self->image) {
|
||||
avifImageDestroy(self->image);
|
||||
}
|
||||
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;
|
||||
unsigned int is_single_frame;
|
||||
int error = 0;
|
||||
|
||||
avifRGBImage rgb;
|
||||
avifResult result;
|
||||
|
||||
avifEncoder *encoder = self->encoder;
|
||||
avifImage *image = self->image;
|
||||
avifImage *frame = NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(
|
||||
args,
|
||||
"y#I(II)sp",
|
||||
(char **)&rgb_bytes,
|
||||
&size,
|
||||
&duration,
|
||||
&width,
|
||||
&height,
|
||||
&mode,
|
||||
&is_single_frame
|
||||
)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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 (self->first_frame) {
|
||||
// If we don't have an image populated with yuv planes, this is the first frame
|
||||
frame = image;
|
||||
} else {
|
||||
frame = avifImageCreateEmpty();
|
||||
if (image == NULL) {
|
||||
PyErr_SetString(PyExc_ValueError, "Image creation failed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
frame->width = width;
|
||||
frame->height = height;
|
||||
frame->colorPrimaries = image->colorPrimaries;
|
||||
frame->transferCharacteristics = image->transferCharacteristics;
|
||||
frame->matrixCoefficients = image->matrixCoefficients;
|
||||
frame->yuvRange = image->yuvRange;
|
||||
frame->yuvFormat = image->yuvFormat;
|
||||
frame->depth = image->depth;
|
||||
frame->alphaPremultiplied = image->alphaPremultiplied;
|
||||
}
|
||||
|
||||
avifRGBImageSetDefaults(&rgb, frame);
|
||||
|
||||
if (strcmp(mode, "RGBA") == 0) {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||
} else {
|
||||
rgb.format = AVIF_RGB_FORMAT_RGB;
|
||||
}
|
||||
|
||||
result = avifRGBImageAllocatePixels(&rgb);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
PyErr_Format(
|
||||
exc_type_for_avif_result(result),
|
||||
"Pixel allocation failed: %s",
|
||||
avifResultToString(result)
|
||||
);
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (rgb.rowBytes * rgb.height != size) {
|
||||
PyErr_Format(
|
||||
PyExc_RuntimeError,
|
||||
"rgb data has incorrect size: %u * %u (%u) != %u",
|
||||
rgb.rowBytes,
|
||||
rgb.height,
|
||||
rgb.rowBytes * rgb.height,
|
||||
size
|
||||
);
|
||||
error = 1;
|
||||
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)
|
||||
);
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
uint32_t addImageFlags =
|
||||
is_single_frame ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE;
|
||||
|
||||
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)
|
||||
);
|
||||
error = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
end:
|
||||
if (&rgb) {
|
||||
avifRGBImageFreePixels(&rgb);
|
||||
}
|
||||
if (!self->first_frame) {
|
||||
avifImageDestroy(frame);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return NULL;
|
||||
}
|
||||
self->first_frame = 0;
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
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) {
|
||||
Py_buffer buffer;
|
||||
AvifDecoderObject *self = NULL;
|
||||
avifDecoder *decoder;
|
||||
|
||||
char *codec_str;
|
||||
avifCodecChoice codec;
|
||||
int max_threads;
|
||||
|
||||
avifResult result;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (strcmp(codec_str, "auto") == 0) {
|
||||
codec = AVIF_CODEC_CHOICE_AUTO;
|
||||
} else {
|
||||
codec = avifCodecChoiceFromName(codec_str);
|
||||
}
|
||||
|
||||
self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type);
|
||||
if (!self) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "could not create decoder object");
|
||||
PyBuffer_Release(&buffer);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
decoder = avifDecoderCreate();
|
||||
if (!decoder) {
|
||||
PyErr_SetString(PyExc_MemoryError, "Can't allocate decoder");
|
||||
PyBuffer_Release(&buffer);
|
||||
PyObject_Del(self);
|
||||
return NULL;
|
||||
}
|
||||
decoder->maxThreads = max_threads;
|
||||
// Turn off libavif's 'clap' (clean aperture) property validation.
|
||||
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.
|
||||
decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED;
|
||||
decoder->codecChoice = codec;
|
||||
|
||||
result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
PyErr_Format(
|
||||
exc_type_for_avif_result(result),
|
||||
"Setting IO memory failed: %s",
|
||||
avifResultToString(result)
|
||||
);
|
||||
avifDecoderDestroy(decoder);
|
||||
PyBuffer_Release(&buffer);
|
||||
PyObject_Del(self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
result = avifDecoderParse(decoder);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
PyErr_Format(
|
||||
exc_type_for_avif_result(result),
|
||||
"Failed to decode image: %s",
|
||||
avifResultToString(result)
|
||||
);
|
||||
avifDecoderDestroy(decoder);
|
||||
PyBuffer_Release(&buffer);
|
||||
PyObject_Del(self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->decoder = decoder;
|
||||
self->buffer = buffer;
|
||||
|
||||
return (PyObject *)self;
|
||||
}
|
||||
|
||||
PyObject *
|
||||
_decoder_dealloc(AvifDecoderObject *self) {
|
||||
if (self->decoder) {
|
||||
avifDecoderDestroy(self->decoder);
|
||||
}
|
||||
PyBuffer_Release(&self->buffer);
|
||||
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(
|
||||
"(II)IsSSIS",
|
||||
image->width,
|
||||
image->height,
|
||||
decoder->imageCount,
|
||||
decoder->alphaPresent ? "RGBA" : "RGB",
|
||||
NULL == icc ? Py_None : icc,
|
||||
NULL == exif ? Py_None : exif,
|
||||
irot_imir_to_exif_orientation(image),
|
||||
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;
|
||||
|
||||
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",
|
||||
frame_index,
|
||||
avifResultToString(result)
|
||||
);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
image = decoder->image;
|
||||
|
||||
avifRGBImageSetDefaults(&rgb, image);
|
||||
|
||||
rgb.depth = 8;
|
||||
rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB;
|
||||
|
||||
result = avifRGBImageAllocatePixels(&rgb);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
PyErr_Format(
|
||||
exc_type_for_avif_result(result),
|
||||
"Pixel allocation failed: %s",
|
||||
avifResultToString(result)
|
||||
);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) {
|
||||
PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size = rgb.rowBytes * rgb.height;
|
||||
|
||||
bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size);
|
||||
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 */
|
||||
};
|
||||
|
||||
// AvifEncoder type definition
|
||||
static PyTypeObject AvifEncoder_Type = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder",
|
||||
.tp_basicsize = sizeof(AvifEncoderObject),
|
||||
.tp_dealloc = (destructor)_encoder_dealloc,
|
||||
.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 = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder",
|
||||
.tp_basicsize = sizeof(AvifDecoderObject),
|
||||
.tp_dealloc = (destructor)_decoder_dealloc,
|
||||
.tp_methods = _decoder_methods,
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/* Module Setup */
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
static PyMethodDef avifMethods[] = {
|
||||
{"AvifDecoder", AvifDecoderNew, METH_VARARGS},
|
||||
{"AvifEncoder", AvifEncoderNew, METH_VARARGS},
|
||||
{"decoder_codec_available", _decoder_codec_available, METH_VARARGS},
|
||||
{"encoder_codec_available", _encoder_codec_available, METH_VARARGS},
|
||||
{"codec_versions", _codec_versions, METH_NOARGS},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
static int
|
||||
setup_module(PyObject *m) {
|
||||
if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyObject *d = PyModule_GetDict(m);
|
||||
PyObject *v = PyUnicode_FromString(avifVersion());
|
||||
PyDict_SetItemString(d, "libavif_version", v ? v : Py_None);
|
||||
Py_XDECREF(v);
|
||||
|
||||
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) {
|
||||
Py_DECREF(m);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#ifdef Py_GIL_DISABLED
|
||||
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
|
||||
#endif
|
||||
|
||||
return m;
|
||||
}
|
26
wheels/dependency_licenses/AOM.txt
Normal file
26
wheels/dependency_licenses/AOM.txt
Normal file
|
@ -0,0 +1,26 @@
|
|||
Copyright (c) 2016, 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.
|
23
wheels/dependency_licenses/DAV1D.txt
Normal file
23
wheels/dependency_licenses/DAV1D.txt
Normal file
|
@ -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.
|
387
wheels/dependency_licenses/LIBAVIF.txt
Normal file
387
wheels/dependency_licenses/LIBAVIF.txt
Normal file
|
@ -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: third_party/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.
|
29
wheels/dependency_licenses/LIBYUV.txt
Normal file
29
wheels/dependency_licenses/LIBYUV.txt
Normal file
|
@ -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.
|
25
wheels/dependency_licenses/RAV1E.txt
Normal file
25
wheels/dependency_licenses/RAV1E.txt
Normal file
|
@ -0,0 +1,25 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2017-2023, 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.
|
26
wheels/dependency_licenses/SVT-AV1.txt
Normal file
26
wheels/dependency_licenses/SVT-AV1.txt
Normal file
|
@ -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.
|
|
@ -61,6 +61,7 @@ Run ``build_prepare.py`` to configure the build::
|
|||
--no-imagequant skip GPL-licensed optional dependency libimagequant
|
||||
--no-fribidi, --no-raqm
|
||||
skip LGPL-licensed optional dependency FriBiDi
|
||||
--no-avif skip optional dependency libavif
|
||||
|
||||
Arguments can also be supplied using the environment variables PILLOW_BUILD,
|
||||
PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information.
|
||||
|
|
|
@ -116,6 +116,7 @@ V = {
|
|||
"HARFBUZZ": "11.0.0",
|
||||
"JPEGTURBO": "3.1.0",
|
||||
"LCMS2": "2.17",
|
||||
"LIBAVIF": "1.2.1",
|
||||
"LIBIMAGEQUANT": "4.3.4",
|
||||
"LIBPNG": "1.6.47",
|
||||
"LIBWEBP": "1.5.0",
|
||||
|
@ -378,6 +379,26 @@ DEPS: dict[str, dict[str, Any]] = {
|
|||
],
|
||||
"bins": [r"*.dll"],
|
||||
},
|
||||
"libavif": {
|
||||
"url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip",
|
||||
"filename": f"libavif-{V['LIBAVIF']}.zip",
|
||||
"license": "LICENSE",
|
||||
"build": [
|
||||
f"{sys.executable} -m pip install meson",
|
||||
*cmds_cmake(
|
||||
"avif_static",
|
||||
"-DBUILD_SHARED_LIBS=OFF",
|
||||
"-DAVIF_LIBSHARPYUV=LOCAL",
|
||||
"-DAVIF_LIBYUV=LOCAL",
|
||||
"-DAVIF_CODEC_AOM=LOCAL",
|
||||
"-DAVIF_CODEC_DAV1D=LOCAL",
|
||||
"-DAVIF_CODEC_RAV1E=LOCAL",
|
||||
"-DAVIF_CODEC_SVT=LOCAL",
|
||||
),
|
||||
cmd_xcopy("include", "{inc_dir}"),
|
||||
],
|
||||
"libs": ["avif.lib"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@ -683,6 +704,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]
|
||||
|
@ -723,6 +749,8 @@ def main() -> None:
|
|||
disabled += ["libimagequant"]
|
||||
if args.no_fribidi:
|
||||
disabled += ["fribidi"]
|
||||
if args.no_avif or args.architecture != "AMD64":
|
||||
disabled += ["libavif"]
|
||||
|
||||
prefs = {
|
||||
"architecture": args.architecture,
|
||||
|
|
Loading…
Reference in New Issue
Block a user