Add AVIF plugin (using libavif)

This commit is contained in:
Frankie Dintino 2021-01-03 15:12:01 -05:00
parent 11c654c187
commit 3878b588a4
No known key found for this signature in database
GPG Key ID: 97E295AACFBABD9E
44 changed files with 3219 additions and 11 deletions

View File

@ -23,7 +23,8 @@ if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev
sway wl-clipboard libopenblas-dev\
ninja-build build-essential nasm
fi
python3 -m pip install --upgrade pip
@ -62,6 +63,9 @@ if [[ $(uname) != CYGWIN* ]]; then
# raqm
pushd depends && ./install_raqm.sh && popd
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
else

View File

@ -13,7 +13,11 @@ brew install \
libtiff \
little-cms2 \
openjpeg \
webp
webp \
dav1d \
aom \
rav1e \
ninja
if [[ "$ImageOS" == "macos13" ]]; then
brew install --ignore-dependencies libraqm
else
@ -31,5 +35,8 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
python3 -m pip install numpy
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -61,6 +61,7 @@ jobs:
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-libraqm \
mingw-w64-x86_64-libavif \
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \

View File

@ -86,6 +86,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
python -m pip install meson
choco install ghostscript --version=10.4.0 --no-progress
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
@ -137,6 +139,18 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libpng.cmd"
- name: Build dependencies / rav1e
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_rav1e.cmd"
- name: Build dependencies / meson
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\install_meson.cmd"
- name: Build dependencies / libavif
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libavif.cmd"
# for FreeType WOFF2 font support
- name: Build dependencies / brotli
if: steps.build-cache.outputs.cache-hit != 'true'

View File

@ -37,6 +37,8 @@ LIBWEBP_VERSION=1.4.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0
LIBAVIF_VERSION=1.1.1
RAV1E_VERSION=0.7.1
function build_brotli {
local cmake=$(get_modern_cmake)
@ -63,6 +65,71 @@ function build_harfbuzz {
fi
}
function install_rav1e {
if [[ -n "$IS_MACOS" ]] && [[ "$PLAT" == "arm64" ]]; then
librav1e_tgz=librav1e-${RAV1E_VERSION}-macos-aarch64.tar.gz
elif [ -n "$IS_MACOS" ]; then
librav1e_tgz=librav1e-${RAV1E_VERSION}-macos.tar.gz
elif [ "$PLAT" == "aarch64" ]; then
librav1e_tgz=librav1e-${RAV1E_VERSION}-linux-aarch64.tar.gz
else
librav1e_tgz=librav1e-${RAV1E_VERSION}-linux-generic.tar.gz
fi
curl -sLo - \
https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/$librav1e_tgz \
| tar -C $BUILD_PREFIX --exclude LICENSE --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf -
if [ ! -n "$IS_MACOS" ]; then
sed -i 's/-lgcc_s/-lgcc_eh/g' "${BUILD_PREFIX}/lib/pkgconfig/rav1e.pc"
fi
# Force libavif to treat system rav1e as if it were local
mkdir -p /tmp/cmake/Modules
cat <<EOF > /tmp/cmake/Modules/Findrav1e.cmake
add_library(rav1e::rav1e STATIC IMPORTED GLOBAL)
set_target_properties(rav1e::rav1e PROPERTIES
IMPORTED_LOCATION "$BUILD_PREFIX/lib/librav1e.a"
AVIF_LOCAL ON
INTERFACE_INCLUDE_DIRECTORIES "$BUILD_PREFIX/include/rav1e"
)
EOF
}
function build_libavif {
install_rav1e
python -m pip install meson ninja
if [[ "$PLAT" == "x86_64" ]]; then
build_simple nasm 2.15.05 https://www.nasm.us/pub/nasm/releasebuilds/2.15.05/
fi
local cmake=$(get_modern_cmake)
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
(cd $out_dir \
&& $cmake \
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_RAV1E=SYSTEM \
-DAVIF_CODEC_AOM=LOCAL \
-DAVIF_CODEC_DAV1D=LOCAL \
-DAVIF_CODEC_SVT=LOCAL \
-DENABLE_NASM=ON \
-DCMAKE_MODULE_PATH=/tmp/cmake/Modules \
. \
&& make install)
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
cp /usr/local/lib64/libavif.a /usr/local/lib
cp /usr/local/lib64/pkgconfig/libavif.pc /usr/local/lib/pkgconfig
fi
}
function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
sudo chown -R runner /usr/local
@ -73,6 +140,13 @@ function build {
fi
build_new_zlib
ORIGINAL_LDFLAGS=$LDFLAGS
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
LDFLAGS="${LDFLAGS} -ld64"
fi
build_libavif
LDFLAGS=$ORIGINAL_LDFLAGS
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
@ -127,15 +201,19 @@ if [[ -n "$IS_MACOS" ]]; then
# remove lcms2 and libpng to fix building openjpeg on arm64
# remove jpeg-turbo to avoid inclusion on arm64
# remove webp and zstd to avoid inclusion on x86_64
# remove aom and libavif to fix building on arm64
# curl from brew requires zstd, use system curl
brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
brew remove --ignore-dependencies jpeg-turbo
else
brew remove --ignore-dependencies webp
brew remove --ignore-dependencies webp aom libavif
fi
brew install pkg-config
brew install meson pkg-config
# clear bash path cache for curl
hash -d curl
fi
wrap_wheel_builder build

44
Tests/check_avif_leaks.py Normal file
View File

@ -0,0 +1,44 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image
from .helper import is_win32, skip_unless_feature
# Limits for testing the leak
mem_limit = 1024 * 1048576
stack_size = 8 * 1048576
iterations = int((mem_limit / stack_size) * 2)
test_file = "Tests/images/avif/hopper.avif"
pytestmark = [
pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"),
skip_unless_feature("avif"),
]
def test_leak_load():
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
setrlimit(RLIMIT_AS, (mem_limit, mem_limit))
for _ in range(iterations):
with Image.open(test_file) as im:
im.load()
def test_leak_save():
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
setrlimit(RLIMIT_AS, (mem_limit, mem_limit))
for _ in range(iterations):
with Image.open(test_file) as im:
im.load()
test_output = BytesIO()
im.save(test_output, "AVIF")
test_output.seek(0)
test_output.read()

View File

@ -1,12 +1,14 @@
from __future__ import annotations
import platform
import struct
import sys
from PIL import features
def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"}
# tkinter is not available in cibuildwheel installed CPython on Windows
try:
@ -16,6 +18,11 @@ def test_wheel_modules() -> None:
except ImportError:
expected_modules.remove("tkinter")
# libavif is not available on windows for x86 and ARM64 architectures
if sys.platform == "win32":
if platform.machine() == "ARM64" or struct.calcsize("P") == 4:
expected_modules.remove("avif")
assert set(features.get_supported_modules()) == expected_modules

Binary file not shown.

BIN
Tests/images/avif/exif.avif Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Binary file not shown.

809
Tests/test_file_avif.py Normal file
View File

@ -0,0 +1,809 @@
from __future__ import annotations
import gc
import os
import re
import warnings
import xml.etree.ElementTree
from contextlib import contextmanager
from io import BytesIO
from struct import unpack
import pytest
from PIL import AvifImagePlugin, Image, ImageDraw, 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, expected):
assert isinstance(xmp, bytes)
root = xml.etree.ElementTree.fromstring(xmp)
orientation = None
for elem in root.iter():
if elem.tag.endswith("}Description"):
orientation = elem.attrib.get("{http://ns.adobe.com/tiff/1.0/}Orientation")
if orientation:
orientation = int(orientation)
break
assert orientation == expected
def roundtrip(im, **options):
out = BytesIO()
im.save(out, "AVIF", **options)
out.seek(0)
return Image.open(out)
def skip_unless_avif_decoder(codec_name):
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):
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():
try:
init_proc_exe = os.readlink("/proc/1/exe")
except: # noqa: E722
return False
else:
return "qemu" in init_proc_exe
def has_alpha_premultiplied(im_bytes):
stream = BytesIO(im_bytes)
length = len(im_bytes)
while stream.tell() < length:
start = stream.tell()
size, boxtype = unpack(">L4s", stream.read(8))
if not all(0x20 <= c <= 0x7E for c in boxtype):
# Not ascii
return False
if size == 1: # 64bit size
(size,) = unpack(">Q", stream.read(8))
end = start + size
version, _ = unpack(">B3s", stream.read(4))
if boxtype in (b"ftyp", b"hdlr", b"pitm", b"iloc", b"iinf"):
# Skip these boxes
stream.seek(end)
continue
elif boxtype == b"meta":
# Container box possibly including iref prem, continue to parse boxes
# inside it
continue
elif boxtype == b"iref":
while stream.tell() < end:
_, iref_type = unpack(">L4s", stream.read(8))
version, _ = unpack(">B3s", stream.read(4))
if iref_type == b"prem":
return True
stream.read(2 if version == 0 else 4)
else:
return False
return False
class TestUnsupportedAvif:
def test_unsupported(self, monkeypatch):
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
file_path = "Tests/images/avif/hopper.avif"
pytest.warns(
UserWarning,
lambda: pytest.raises(UnidentifiedImageError, Image.open, file_path),
)
def test_unsupported_open(self, monkeypatch):
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
file_path = "Tests/images/avif/hopper.avif"
with pytest.raises(SyntaxError):
AvifImagePlugin.AvifImageFile(file_path)
@skip_unless_feature("avif")
class TestFileAvif:
def test_version(self):
_avif.AvifCodecVersions()
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("avif"))
def test_read(self):
"""
Can we read an AVIF file without error?
Does it have the bits we expect?
"""
with Image.open("Tests/images/avif/hopper.avif") as image:
assert image.mode == "RGB"
assert image.size == (128, 128)
assert image.format == "AVIF"
assert image.get_format_mimetype() == "image/avif"
image.load()
image.getdata()
# generated with:
# avifdec hopper.avif hopper_avif_write.png
assert_image_similar_tofile(
image, "Tests/images/avif/hopper_avif_write.png", 12.0
)
def _roundtrip(self, tmp_path, mode, epsilon, args={}):
temp_file = str(tmp_path / "temp.avif")
hopper(mode).save(temp_file, **args)
with Image.open(temp_file) as image:
assert image.mode == "RGB"
assert image.size == (128, 128)
assert image.format == "AVIF"
image.load()
image.getdata()
if mode == "RGB":
# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
image, "Tests/images/avif/hopper_avif_write.png", 12.0
)
# This test asserts that the images are similar. If the average pixel
# difference between the two images is less than the epsilon value,
# then we're going to accept that it's a reasonable lossy version of
# the image.
target = hopper(mode)
if mode != "RGB":
target = target.convert("RGB")
assert_image_similar(image, target, epsilon)
def test_write_rgb(self, tmp_path):
"""
Can we write a RGB mode file to avif without error?
Does it have the bits we expect?
"""
self._roundtrip(tmp_path, "RGB", 12.5)
def test_AvifEncoder_with_invalid_args(self):
"""
Calling encoder functions with no arguments should result in an error.
"""
with pytest.raises(TypeError):
_avif.AvifEncoder()
def test_AvifDecoder_with_invalid_args(self):
"""
Calling decoder functions with no arguments should result in an error.
"""
with pytest.raises(TypeError):
_avif.AvifDecoder()
def test_encoder_finish_none_error(self, monkeypatch, tmp_path):
"""Save should raise an OSError if AvifEncoder.finish returns None"""
class _mock_avif:
class AvifEncoder:
def __init__(self, *args, **kwargs):
pass
def add(self, *args, **kwargs):
pass
def finish(self):
return None
monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif)
im = Image.new("RGB", (150, 150))
test_file = str(tmp_path / "temp.avif")
with pytest.raises(OSError):
im.save(test_file)
def test_no_resource_warning(self, tmp_path):
with Image.open(TEST_AVIF_FILE) as image:
temp_file = str(tmp_path / "temp.avif")
with warnings.catch_warnings():
image.save(temp_file)
@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
def test_accept_ftyp_brands(self, major_brand):
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):
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):
with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1))
# Save as AVIF
out_avif = str(tmp_path / "temp.avif")
im.save(out_avif, save_all=True)
# Save as GIF
out_gif = str(tmp_path / "temp.gif")
with Image.open(out_avif) as im:
im.save(out_gif)
with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1))
difference = sum(
[abs(original_value[i] - reread_value[i]) for i in range(0, 3)]
)
assert difference < 5
def test_save_single_frame(self, tmp_path):
temp_file = str(tmp_path / "temp.avif")
with Image.open("Tests/images/chi.gif") as im:
im.save(temp_file)
with Image.open(temp_file) as im:
assert im.n_frames == 1
def test_invalid_file(self):
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
AvifImagePlugin.AvifImageFile(invalid_file)
def test_load_transparent_rgb(self):
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][0] == 876
def test_save_transparent(self, tmp_path):
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
assert im.getcolors() == [(100, (0, 0, 0, 0))]
test_file = str(tmp_path / "temp.avif")
im.save(test_file)
# check if saved image contains 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):
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
assert im.info.get("icc_profile") is None
with Image.open("Tests/images/avif/icc_profile.avif") as with_icc:
expected_icc = with_icc.info.get("icc_profile")
assert expected_icc is not None
im = roundtrip(im, icc_profile=expected_icc)
assert im.info["icc_profile"] == expected_icc
def test_discard_icc_profile(self):
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):
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):
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
assert im.info.get("icc_profile") is None
im = roundtrip(im)
assert "icc_profile" not in im.info
def test_exif(self):
# With an EXIF chunk
with Image.open("Tests/images/avif/exif.avif") as im:
exif = im.getexif()
assert exif[274] == 1
def test_exif_save(self, tmp_path):
with Image.open("Tests/images/avif/exif.avif") as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file)
with Image.open(test_file) as reloaded:
exif = reloaded.getexif()
assert exif[274] == 1
def test_exif_obj_argument(self, tmp_path):
exif = Image.Exif()
exif[274] = 1
exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, exif=exif)
with Image.open(test_file) as reloaded:
assert reloaded.info["exif"] == exif_data
def test_exif_bytes_argument(self, tmp_path):
exif = Image.Exif()
exif[274] = 1
exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, exif=exif_data)
with Image.open(test_file) as reloaded:
assert reloaded.info["exif"] == exif_data
def test_exif_invalid(self, tmp_path):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, exif=b"invalid")
def test_xmp(self):
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
xmp = im.info.get("xmp")
assert_xmp_orientation(xmp, 3)
def test_xmp_save(self, tmp_path):
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file)
with Image.open(test_file) as reloaded:
xmp = reloaded.info.get("xmp")
assert_xmp_orientation(xmp, 3)
def test_xmp_save_from_png(self, tmp_path):
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file)
with Image.open(test_file) as reloaded:
xmp = reloaded.info.get("xmp")
assert_xmp_orientation(xmp, 3)
def test_xmp_save_argument(self, tmp_path):
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("Tests/images/avif/hopper.avif") as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, xmp=xmp_arg)
with Image.open(test_file) as reloaded:
xmp = reloaded.info.get("xmp")
assert_xmp_orientation(xmp, 1)
def test_tell(self):
with Image.open(TEST_AVIF_FILE) as im:
assert im.tell() == 0
def test_seek(self):
with Image.open(TEST_AVIF_FILE) as im:
im.seek(0)
with pytest.raises(EOFError):
im.seek(1)
@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:0:0"])
def test_encoder_subsampling(self, tmp_path, subsampling):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, subsampling=subsampling)
def test_encoder_subsampling_invalid(self, tmp_path):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, subsampling="foo")
def test_encoder_range(self, tmp_path):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, range="limited")
def test_encoder_range_invalid(self, tmp_path):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, range="foo")
@skip_unless_avif_encoder("aom")
@skip_unless_feature("avif")
def test_encoder_codec_param(self, tmp_path):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, codec="aom")
def test_encoder_codec_invalid(self, tmp_path):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, codec="foo")
@skip_unless_avif_decoder("dav1d")
@skip_unless_feature("avif")
def test_encoder_codec_cannot_encode(self, tmp_path):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, codec="dav1d")
@skip_unless_avif_encoder("aom")
@skip_unless_feature("avif")
def test_encoder_advanced_codec_options(self):
with Image.open(TEST_AVIF_FILE) as im:
ctrl_buf = BytesIO()
im.save(ctrl_buf, "AVIF", codec="aom")
test_buf = BytesIO()
im.save(
test_buf,
"AVIF",
codec="aom",
advanced={
"aq-mode": "1",
"enable-chroma-deltaq": "1",
},
)
assert ctrl_buf.getvalue() != test_buf.getvalue()
@skip_unless_avif_encoder("aom")
@skip_unless_feature("avif")
@pytest.mark.parametrize("val", [{"foo": "bar"}, 1234])
def test_encoder_advanced_codec_options_invalid(self, tmp_path, val):
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, codec="aom", advanced=val)
@skip_unless_avif_decoder("aom")
@skip_unless_feature("avif")
def test_decoder_codec_param(self):
AvifImagePlugin.DECODE_CODEC_CHOICE = "aom"
try:
with Image.open(TEST_AVIF_FILE) as im:
assert im.size == (128, 128)
finally:
AvifImagePlugin.DECODE_CODEC_CHOICE = "auto"
@skip_unless_avif_encoder("rav1e")
@skip_unless_feature("avif")
def test_decoder_codec_cannot_decode(self, tmp_path):
AvifImagePlugin.DECODE_CODEC_CHOICE = "rav1e"
try:
with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass
finally:
AvifImagePlugin.DECODE_CODEC_CHOICE = "auto"
def test_decoder_codec_invalid(self):
AvifImagePlugin.DECODE_CODEC_CHOICE = "foo"
try:
with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass
finally:
AvifImagePlugin.DECODE_CODEC_CHOICE = "auto"
@skip_unless_avif_encoder("aom")
@skip_unless_feature("avif")
def test_encoder_codec_available(self):
assert _avif.encoder_codec_available("aom") is True
def test_encoder_codec_available_bad_params(self):
with pytest.raises(TypeError):
_avif.encoder_codec_available()
@skip_unless_avif_decoder("dav1d")
@skip_unless_feature("avif")
def test_encoder_codec_available_cannot_decode(self):
assert _avif.encoder_codec_available("dav1d") is False
def test_encoder_codec_available_invalid(self):
assert _avif.encoder_codec_available("foo") is False
def test_encoder_quality_valueerror(self, tmp_path):
with Image.open("Tests/images/avif/hopper.avif") as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(ValueError):
im.save(test_file, quality="invalid")
@skip_unless_avif_decoder("aom")
@skip_unless_feature("avif")
def test_decoder_codec_available(self):
assert _avif.decoder_codec_available("aom") is True
def test_decoder_codec_available_bad_params(self):
with pytest.raises(TypeError):
_avif.decoder_codec_available()
@skip_unless_avif_encoder("rav1e")
@skip_unless_feature("avif")
def test_decoder_codec_available_cannot_decode(self):
assert _avif.decoder_codec_available("rav1e") is False
def test_decoder_codec_available_invalid(self):
assert _avif.decoder_codec_available("foo") is False
@pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"])
def test_decoder_upsampling(self, upsampling):
AvifImagePlugin.CHROMA_UPSAMPLING = upsampling
try:
with Image.open(TEST_AVIF_FILE):
pass
finally:
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"
def test_decoder_upsampling_invalid(self):
AvifImagePlugin.CHROMA_UPSAMPLING = "foo"
try:
with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass
finally:
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"
def test_p_mode_transparency(self):
im = Image.new("P", size=(64, 64))
draw = ImageDraw.Draw(im)
draw.rectangle(xy=[(0, 0), (32, 32)], fill=255)
draw.rectangle(xy=[(32, 32), (64, 64)], fill=255)
buf_png = BytesIO()
im.save(buf_png, format="PNG", transparency=0)
im_png = Image.open(buf_png)
buf_out = BytesIO()
im_png.save(buf_out, format="AVIF", quality=100)
assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1)
def test_decoder_strict_flags(self):
# This would fail if full avif strictFlags were enabled
with Image.open("Tests/images/avif/chimera-missing-pixi.avif") as im:
assert im.size == (480, 270)
@skip_unless_avif_encoder("aom")
def test_aom_optimizations(self):
im = hopper("RGB")
buf = BytesIO()
im.save(buf, format="AVIF", codec="aom", speed=1)
@skip_unless_avif_encoder("svt")
def test_svt_optimizations(self):
im = hopper("RGB")
buf = BytesIO()
im.save(buf, format="AVIF", codec="svt", speed=1)
@skip_unless_feature("avif")
class TestAvifAnimation:
@contextmanager
def star_frames(self):
with Image.open("Tests/images/avif/star.png") as f1:
with Image.open("Tests/images/avif/star90.png") as f2:
with Image.open("Tests/images/avif/star180.png") as f3:
with Image.open("Tests/images/avif/star270.png") as f4:
yield [f1, f2, f3, f4]
def test_n_frames(self):
"""
Ensure that AVIF format sets n_frames and is_animated attributes
correctly.
"""
with Image.open("Tests/images/avif/hopper.avif") as im:
assert im.n_frames == 1
assert not im.is_animated
with Image.open("Tests/images/avif/star.avifs") as im:
assert im.n_frames == 5
assert im.is_animated
def test_write_animation_L(self, tmp_path):
"""
Convert an animated GIF to animated AVIF, then compare the frame
count, and first and last frames to ensure they're visually similar.
"""
with Image.open("Tests/images/avif/star.gif") as orig:
assert orig.n_frames > 1
temp_file = str(tmp_path / "temp.avif")
orig.save(temp_file, save_all=True)
with Image.open(temp_file) as im:
assert im.n_frames == orig.n_frames
# Compare first and second-to-last frames to the original animated GIF
orig.load()
im.load()
assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0)
orig.seek(orig.n_frames - 2)
im.seek(im.n_frames - 2)
orig.load()
im.load()
assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0)
def test_write_animation_RGB(self, tmp_path):
"""
Write an animated AVIF from RGB frames, and ensure the frames
are visually similar to the originals.
"""
def check(temp_file):
with Image.open(temp_file) as im:
assert im.n_frames == 4
# Compare first frame to original
im.load()
assert_image_similar(im, frame1.convert("RGBA"), 25.0)
# Compare second frame to original
im.seek(1)
im.load()
assert_image_similar(im, frame2.convert("RGBA"), 25.0)
with self.star_frames() as frames:
frame1 = frames[0]
frame2 = frames[1]
temp_file1 = str(tmp_path / "temp.avif")
frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:])
check(temp_file1)
# Tests appending using a generator
def imGenerator(ims):
yield from ims
temp_file2 = str(tmp_path / "temp_generator.avif")
frames[0].copy().save(
temp_file2,
save_all=True,
append_images=imGenerator(frames[1:]),
)
check(temp_file2)
def test_sequence_dimension_mismatch_check(self, tmp_path):
temp_file = str(tmp_path / "temp.avif")
frame1 = Image.new("RGB", (100, 100))
frame2 = Image.new("RGB", (150, 150))
with pytest.raises(ValueError):
frame1.save(temp_file, save_all=True, append_images=[frame2], duration=100)
def test_heif_raises_unidentified_image_error(self):
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/avif/rgba10.heif"):
pass
@pytest.mark.parametrize("alpha_premultipled", [False, True])
def test_alpha_premultiplied_true(self, alpha_premultipled):
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
im_buf = BytesIO()
im.save(im_buf, "AVIF", alpha_premultiplied=alpha_premultipled)
im_bytes = im_buf.getvalue()
assert has_alpha_premultiplied(im_bytes) is alpha_premultipled
def test_timestamp_and_duration(self, tmp_path):
"""
Try passing a list of durations, and make sure the encoded
timestamps and durations are correct.
"""
durations = [1, 10, 20, 30, 40]
temp_file = str(tmp_path / "temp.avif")
with self.star_frames() as frames:
frames[0].save(
temp_file,
save_all=True,
append_images=(frames[1:] + [frames[0]]),
duration=durations,
)
with Image.open(temp_file) as im:
assert im.n_frames == 5
assert im.is_animated
# Check that timestamps and durations match original values specified
ts = 0
for frame in range(im.n_frames):
im.seek(frame)
im.load()
assert im.info["duration"] == durations[frame]
assert im.info["timestamp"] == ts
ts += durations[frame]
def test_seeking(self, tmp_path):
"""
Create an animated AVIF file, and then try seeking through frames in
reverse-order, verifying the timestamps and durations are correct.
"""
dur = 33
temp_file = str(tmp_path / "temp.avif")
with self.star_frames() as frames:
frames[0].save(
temp_file,
save_all=True,
append_images=(frames[1:] + [frames[0]]),
duration=dur,
)
with Image.open(temp_file) as im:
assert im.n_frames == 5
assert im.is_animated
# Traverse frames in reverse, checking timestamps and durations
ts = dur * (im.n_frames - 1)
for frame in reversed(range(im.n_frames)):
im.seek(frame)
im.load()
assert im.info["duration"] == dur
assert im.info["timestamp"] == ts
ts -= dur
def test_seek_errors(self):
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):
with open(TEST_AVIF_FILE, "rb") as f:
im_data = f.read()
def core():
with Image.open(BytesIO(im_data)) as im:
im.load()
gc.collect()
self._test_leak(core)

62
depends/install_libavif.sh Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -eo pipefail
version=1.1.1
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
pushd libavif-$version
if uname -s | grep -q Darwin; then
PREFIX=$(brew --prefix)
else
PREFIX=/usr
fi
PKGCONFIG=${PKGCONFIG:-pkg-config}
LIBAVIF_CMAKE_FLAGS=()
HAS_DECODER=0
HAS_ENCODER=0
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists SvtAv1Enc; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi
cmake -G Ninja -S . -B build \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DAVIF_LIBYUV=LOCAL \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
-DCMAKE_MACOSX_RPATH=OFF \
"${LIBAVIF_CMAKE_FLAGS[@]}"
sudo ninja -C build install
popd

View File

@ -235,7 +235,7 @@ following options are available::
**append_images**
A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images.
This is currently supported for GIF, PDF, PNG, TIFF, and WebP.
This is currently supported for GIF, PDF, PNG, TIFF, WebP, and AVIF.
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.
@ -1311,6 +1311,79 @@ XBM
Pillow reads and writes X bitmap files (mode ``1``).
AVIF
^^^^
Pillow reads and writes AVIF files, including AVIF sequence images. Currently,
it is only possible to save 8-bit AVIF images, and all AVIF images are decoded
as 8-bit RGB(A).
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**quality**
Integer, 1-100, Defaults to 90. 0 gives the smallest size and poorest
quality, 100 the largest and best quality. The value of this setting
controls the ``qmin`` and ``qmax`` encoder options.
**qmin** / **qmax**
Integer, 0-63. The quality of images created by an AVIF encoder are
controlled by minimum and maximum quantizer values. The higher these
values are, the worse the quality.
**subsampling**
If present, sets the subsampling for the encoder. Defaults to ``"4:2:0``".
Options include:
* ``"4:0:0"``
* ``"4:2:0"``
* ``"4:2:2"``
* ``"4:4:4"``
**speed**
Quality/speed trade-off (0=slower-better, 10=fastest). Defaults to 8.
**range**
YUV range, either "full" or "limited." Defaults to "full"
**codec**
AV1 codec to use for encoding. Possible values are "aom", "rav1e", and
"svt", depending on what codecs were compiled with libavif. Defaults to
"auto", which will choose the first available codec in the order of the
preceding list.
**tile_rows** / **tile_cols**
For tile encoding, the (log 2) number of tile rows and columns to use.
Valid values are 0-6, default 0.
**alpha_premultiplied**
Encode the image with premultiplied alpha, defaults ``False``
**icc_profile**
The ICC Profile to include in the saved file.
**exif**
The exif data to include in the saved file.
**xmp**
The XMP data to include in the saved file.
Saving sequences
~~~~~~~~~~~~~~~~~
When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
options will also be available.
**append_images**
A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images.
**duration**
The display duration of each frame, in milliseconds. Pass a single
integer for a constant duration, or a list or tuple to set the
duration for each frame separately.
Read-only formats
-----------------

View File

@ -89,6 +89,16 @@ Many of Pillow's features require external libraries:
* **libxcb** provides X11 screengrab support.
* **libavif** provides support for the AVIF format.
* Pillow requires libavif version **0.8.0** or greater, which is when
AVIF image sequence support was added.
* libavif is merely an API that wraps AVIF codecs. If you are compiling
libavif from source, you will also need to install both an AVIF encoder
and decoder, such as rav1e and dav1d, or libaom, which both encodes and
decodes AVIF images.
.. tab:: Linux
If you didn't build Python from source, make sure you have Python's
@ -117,6 +127,12 @@ Many of Pillow's features require external libraries:
To install libraqm, ``sudo apt-get install meson`` and then see
``depends/install_raqm.sh``.
Build prerequisites for libavif on Ubuntu are installed with::
sudo apt-get install cmake ninja-build nasm
Then see ``depends/install_libavif.sh`` to build and install libavif.
Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
@ -156,6 +172,12 @@ Many of Pillow's features require external libraries:
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
To install libavif on macOS use Homebrew to install its build dependencies::
brew install aom dav1d rav1e
Then see ``depends/install_libavif.sh`` to install libavif.
.. tab:: Windows
We recommend you use prebuilt wheels from PyPI.
@ -193,7 +215,8 @@ Many of Pillow's features require external libraries:
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libraqm
mingw-w64-x86_64-libraqm \
mingw-w64-x86_64-libavif
https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with
MSYS2. To workaround this, before installing Pillow you must run::
@ -210,9 +233,10 @@ Many of Pillow's features require external libraries:
Prerequisites are installed on **FreeBSD 10 or 11** with::
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
See ``depends/install_raqm_cmake.sh`` to install libraqm and
``depends/install_libavif.sh`` to install libavif.
.. tab:: Android

View File

@ -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

View File

@ -1,6 +1,14 @@
Plugin reference
================
:mod:`~PIL.AvifImagePlugin` Module
----------------------------------
.. automodule:: PIL.AvifImagePlugin
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.BmpImagePlugin` Module
---------------------------------

View File

@ -305,6 +305,7 @@ class pil_build_ext(build_ext):
"jpeg2000",
"imagequant",
"xcb",
"avif",
]
required = {"jpeg", "zlib"}
@ -839,6 +840,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"):
@ -927,6 +934,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)
@ -969,6 +984,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
@ -1011,6 +1027,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"]),

282
src/PIL/AvifImagePlugin.py Normal file
View File

@ -0,0 +1,282 @@
from __future__ import annotations
from io import BytesIO
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"
CHROMA_UPSAMPLING = "auto"
DEFAULT_MAX_THREADS = 0
_VALID_AVIF_MODES = {"RGB", "RGBA"}
def _accept(prefix):
if prefix[4:8] != b"ftyp":
return
coding_brands = (b"avif", b"avis")
container_brands = (b"mif1", b"msf1")
major_brand = prefix[8:12]
if major_brand in coding_brands:
if not SUPPORTED:
return (
"image file could not be identified because AVIF "
"support not installed"
)
return True
if major_brand in container_brands:
# We accept files with AVIF container brands; we can't yet know if
# the ftyp box has the correct compatible brands, but if it doesn't
# then the plugin will raise a SyntaxError which Pillow will catch
# before moving on to the next plugin that accepts the file.
#
# Also, because this file might not actually be an AVIF file, we
# don't raise an error if AVIF support isn't properly compiled.
return True
class AvifImageFile(ImageFile.ImageFile):
format = "AVIF"
format_description = "AVIF image"
__loaded = -1
__frame = 0
def load_seek(self, pos: int) -> None:
pass
def _open(self):
if not SUPPORTED:
msg = (
"image file could not be identified because AVIF "
"support not installed"
)
raise SyntaxError(msg)
self._decoder = _avif.AvifDecoder(
self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING, DEFAULT_MAX_THREADS
)
# Get info from decoder
width, height, n_frames, mode, icc, exif, xmp = self._decoder.get_info()
self._size = width, height
self.n_frames = n_frames
self.is_animated = self.n_frames > 1
self._mode = self.rawmode = mode
self.tile = []
if icc:
self.info["icc_profile"] = icc
if exif:
self.info["exif"] = exif
if xmp:
self.info["xmp"] = xmp
def seek(self, frame):
if not self._seek_check(frame):
return
self.__frame = frame
def load(self):
if self.__loaded != self.__frame:
# We need to load the image data for this frame
data, timescale, tsp_in_ts, dur_in_ts = self._decoder.get_frame(
self.__frame
)
timestamp = round(1000 * (tsp_in_ts / timescale))
duration = round(1000 * (dur_in_ts / timescale))
self.info["timestamp"] = timestamp
self.info["duration"] = duration
self.__loaded = self.__frame
# Set tile
if self.fp and self._exclusive_fp:
self.fp.close()
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
return super().load()
def tell(self):
return self.__frame
def _save_all(im, fp, filename):
_save(im, fp, filename, save_all=True)
def _save(im, fp, filename, save_all=False):
info = im.encoderinfo.copy()
if save_all:
append_images = list(info.get("append_images", []))
else:
append_images = []
total = 0
for ims in [im] + append_images:
total += getattr(ims, "n_frames", 1)
is_single_frame = total == 1
qmin = info.get("qmin", -1)
qmax = info.get("qmax", -1)
quality = info.get("quality", 75)
if not isinstance(quality, int) or quality < 0 or quality > 100:
msg = "Invalid quality setting"
raise ValueError(msg)
duration = info.get("duration", 0)
subsampling = info.get("subsampling", "4:2:0")
speed = info.get("speed", 6)
max_threads = info.get("max_threads", DEFAULT_MAX_THREADS)
codec = info.get("codec", "auto")
range_ = info.get("range", "full")
tile_rows_log2 = info.get("tile_rows", 0)
tile_cols_log2 = info.get("tile_cols", 0)
alpha_premultiplied = bool(info.get("alpha_premultiplied", False))
autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0))
icc_profile = info.get("icc_profile", im.info.get("icc_profile"))
exif = info.get("exif", im.info.get("exif"))
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
exif_orientation = 0
if exif:
exif_data = Image.Exif()
try:
exif_data.load(exif)
except SyntaxError:
pass
else:
orientation_tag = next(
k for k, v in ExifTags.TAGS.items() if v == "Orientation"
)
exif_orientation = exif_data.get(orientation_tag) or 0
xmp = info.get("xmp", im.info.get("xmp") or im.info.get("XML:com.adobe.xmp"))
if isinstance(xmp, str):
xmp = xmp.encode("utf-8")
advanced = info.get("advanced")
if isinstance(advanced, dict):
advanced = tuple([k, v] for (k, v) in advanced.items())
if advanced is not None:
try:
advanced = tuple(advanced)
except TypeError:
invalid = True
else:
invalid = all(isinstance(v, tuple) and len(v) == 2 for v in advanced)
if invalid:
msg = (
"advanced codec options must be a dict of key-value string "
"pairs or a series of key-value two-tuples"
)
raise ValueError(msg)
advanced = tuple(
[(str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced]
)
# Setup the AVIF encoder
enc = _avif.AvifEncoder(
im.size[0],
im.size[1],
subsampling,
qmin,
qmax,
quality,
speed,
max_threads,
codec,
range_,
tile_rows_log2,
tile_cols_log2,
alpha_premultiplied,
autotiling,
icc_profile or b"",
exif or b"",
exif_orientation,
xmp or b"",
advanced,
)
# Add each frame
frame_idx = 0
frame_dur = 0
cur_idx = im.tell()
try:
for ims in [im] + append_images:
# Get # of frames in this image
nfr = getattr(ims, "n_frames", 1)
for idx in range(nfr):
ims.seek(idx)
ims.load()
# Make sure image mode is supported
frame = ims
rawmode = ims.mode
if ims.mode not in _VALID_AVIF_MODES:
alpha = (
"A" in ims.mode
or "a" in ims.mode
or (ims.mode == "P" and "A" in ims.im.getpalettemode())
or (
ims.mode == "P"
and ims.info.get("transparency", None) is not None
)
)
rawmode = "RGBA" if alpha else "RGB"
frame = ims.convert(rawmode)
# Update frame duration
if isinstance(duration, (list, tuple)):
frame_dur = duration[frame_idx]
else:
frame_dur = duration
# Append the frame to the animation encoder
enc.add(
frame.tobytes("raw", rawmode),
frame_dur,
frame.size[0],
frame.size[1],
rawmode,
is_single_frame,
)
# Update frame index
frame_idx += 1
if not save_all:
break
finally:
im.seek(cur_idx)
# Get the final output from the encoder
data = enc.finish()
if data is None:
msg = "cannot write file as AVIF (encoder returned None)"
raise OSError(msg)
fp.write(data)
Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
if SUPPORTED:
Image.register_save(AvifImageFile.format, _save)
Image.register_save_all(AvifImageFile.format, _save_all)
Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"])
Image.register_mime(AvifImageFile.format, "image/avif")

View File

@ -1548,7 +1548,9 @@ class Image:
# XMP tags
if ExifTags.Base.Orientation not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
xmp_tags = self.info.get("XML:com.adobe.xmp") or self.info.get("xmp")
if isinstance(xmp_tags, bytes):
xmp_tags = xmp_tags.decode("utf-8")
if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match:

View File

@ -25,6 +25,7 @@ del _version
_plugins = [
"AvifImagePlugin",
"BlpImagePlugin",
"BmpImagePlugin",
"BufrStubImagePlugin",

3
src/PIL/_avif.pyi Normal file
View File

@ -0,0 +1,3 @@
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -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"),
}
@ -287,6 +288,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
("littlecms2", "LITTLECMS2"),
("webp", "WEBP"),
("jpg", "JPEG"),
("avif", "AVIF"),
("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"),
("libtiff", "LIBTIFF"),

1084
src/_avif.c Normal file

File diff suppressed because it is too large Load Diff

View 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.

View 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: apps/shared/iccjpeg.*
In plain English:
1. We don't promise that this software works. (But if you find any bugs,
please let us know!)
2. You can use this software for whatever you want. You don't have to pay us.
3. You may not pretend that you wrote this software. If you use it in a
program, you must acknowledge somewhere in your documentation that
you've used the IJG code.
In legalese:
The authors make NO WARRANTY or representation, either express or implied,
with respect to this software, its quality, accuracy, merchantability, or
fitness for a particular purpose. This software is provided "AS IS", and you,
its user, assume the entire risk as to its quality and accuracy.
This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding.
All Rights Reserved except as specified below.
Permission is hereby granted to use, copy, modify, and distribute this
software (or portions thereof) for any purpose, without fee, subject to these
conditions:
(1) If any part of the source code for this software is distributed, then this
README file must be included, with this copyright and no-warranty notice
unaltered; and any additions, deletions, or changes to the original files
must be clearly indicated in accompanying documentation.
(2) If only executable code is distributed, then the accompanying
documentation must state that "this software is based in part on the work of
the Independent JPEG Group".
(3) Permission for use of this software is granted only if the user accepts
full responsibility for any undesirable consequences; the authors accept
NO LIABILITY for damages of any kind.
These conditions apply to any software derived from or based on the IJG code,
not just to the unmodified library. If you use our work, you ought to
acknowledge us.
Permission is NOT granted for the use of any IJG author's name or company name
in advertising or publicity relating to this software or products derived from
it. This software may be referred to only as "the Independent JPEG Group's
software".
We specifically permit and encourage the use of this software as the basis of
commercial products, provided that all warranty or liability claims are
assumed by the product vendor.
The Unix configuration script "configure" was produced with GNU Autoconf.
It is copyright by the Free Software Foundation but is freely distributable.
The same holds for its supporting scripts (config.guess, config.sub,
ltmain.sh). Another support script, install-sh, is copyright by X Consortium
but is also freely distributable.
The IJG distribution formerly included code to read and write GIF files.
To avoid entanglement with the Unisys LZW patent, GIF reading support has
been removed altogether, and the GIF writer has been simplified to produce
"uncompressed GIFs". This technique does not use the LZW algorithm; the
resulting GIF files are larger than usual, but are readable by all standard
GIF decoders.
We are required to state that
"The Graphics Interchange Format(c) is the Copyright property of
CompuServe Incorporated. GIF(sm) is a Service Mark property of
CompuServe Incorporated."
------------------------------------------------------------------------------
Files: contrib/gdk-pixbuf/*
Copyright 2020 Emmanuel Gil Peyrot. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
------------------------------------------------------------------------------
Files: android_jni/gradlew*
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
------------------------------------------------------------------------------
Files: third_party/libyuv/*
Copyright 2011 The LibYuv Project Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Google nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View 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.

View File

@ -0,0 +1,107 @@
Alliance for Open Media Patent License 1.0
1. License Terms.
1.1. Patent License. Subject to the terms and conditions of this License, each
Licensor, on behalf of itself and successors in interest and assigns,
grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as expressly stated in this
License) patent license to its Necessary Claims to make, use, sell, offer
for sale, import or distribute any Implementation.
1.2. Conditions.
1.2.1. Availability. As a condition to the grant of rights to Licensee to make,
sell, offer for sale, import or distribute an Implementation under
Section 1.1, Licensee must make its Necessary Claims available under
this License, and must reproduce this License with any Implementation
as follows:
a. For distribution in source code, by including this License in the
root directory of the source code with its Implementation.
b. For distribution in any other form (including binary, object form,
and/or hardware description code (e.g., HDL, RTL, Gate Level Netlist,
GDSII, etc.)), by including this License in the documentation, legal
notices, and/or other written materials provided with the
Implementation.
1.2.2. Additional Conditions. This license is directly from Licensor to
Licensee. Licensee acknowledges as a condition of benefiting from it
that no rights from Licensor are received from suppliers, distributors,
or otherwise in connection with this License.
1.3. Defensive Termination. If any Licensee, its Affiliates, or its agents
initiates patent litigation or files, maintains, or voluntarily
participates in a lawsuit against another entity or any person asserting
that any Implementation infringes Necessary Claims, any patent licenses
granted under this License directly to the Licensee are immediately
terminated as of the date of the initiation of action unless 1) that suit
was in response to a corresponding suit regarding an Implementation first
brought against an initiating entity, or 2) that suit was brought to
enforce the terms of this License (including intervention in a third-party
action by a Licensee).
1.4. Disclaimers. The Reference Implementation and Specification are provided
"AS IS" and without warranty. The entire risk as to implementing or
otherwise using the Reference Implementation or Specification is assumed
by the implementer and user. Licensor expressly disclaims any warranties
(express, implied, or otherwise), including implied warranties of
merchantability, non-infringement, fitness for a particular purpose, or
title, related to the material. IN NO EVENT WILL LICENSOR BE LIABLE TO
ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF
ACTION OF ANY KIND WITH RESPECT TO THIS LICENSE, WHETHER BASED ON BREACH
OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR
NOT THE OTHER PARTRY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2. Definitions.
2.1. Affiliate. “Affiliate” means an entity that directly or indirectly
Controls, is Controlled by, or is under common Control of that party.
2.2. Control. “Control” means direct or indirect control of more than 50% of
the voting power to elect directors of that corporation, or for any other
entity, the power to direct management of such entity.
2.3. Decoder. "Decoder" means any decoder that conforms fully with all
non-optional portions of the Specification.
2.4. Encoder. "Encoder" means any encoder that produces a bitstream that can
be decoded by a Decoder only to the extent it produces such a bitstream.
2.5. Final Deliverable. “Final Deliverable” means the final version of a
deliverable approved by the Alliance for Open Media as a Final
Deliverable.
2.6. Implementation. "Implementation" means any implementation, including the
Reference Implementation, that is an Encoder and/or a Decoder. An
Implementation also includes components of an Implementation only to the
extent they are used as part of an Implementation.
2.7. License. “License” means this license.
2.8. Licensee. “Licensee” means any person or entity who exercises patent
rights granted under this License.
2.9. Licensor. "Licensor" means (i) any Licensee that makes, sells, offers
for sale, imports or distributes any Implementation, or (ii) a person
or entity that has a licensing obligation to the Implementation as a
result of its membership and/or participation in the Alliance for Open
Media working group that developed the Specification.
2.10. Necessary Claims. "Necessary Claims" means all claims of patents or
patent applications, (a) that currently or at any time in the future,
are owned or controlled by the Licensor, and (b) (i) would be an
Essential Claim as defined by the W3C Policy as of February 5, 2004
(https://www.w3.org/Consortium/Patent-Policy-20040205/#def-essential)
as if the Specification was a W3C Recommendation; or (ii) are infringed
by the Reference Implementation.
2.11. Reference Implementation. “Reference Implementation” means an Encoder
and/or Decoder released by the Alliance for Open Media as a Final
Deliverable.
2.12. Specification. “Specification” means the specification designated by
the Alliance for Open Media as a Final Deliverable for which this
License was issued.

View File

@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2017-2021, the rav1e contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View 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.

10
winbuild/Findrav1e.cmake Normal file
View File

@ -0,0 +1,10 @@
file(TO_CMAKE_PATH "${AVIF_RAV1E_ROOT}" RAV1E_ROOT_PATH)
add_library(rav1e::rav1e STATIC IMPORTED GLOBAL)
set_target_properties(
rav1e::rav1e
PROPERTIES IMPORTED_LOCATION "${RAV1E_ROOT_PATH}/lib/rav1e.lib"
AVIF_LOCAL ON
INTERFACE_INCLUDE_DIRECTORIES "${RAV1E_ROOT_PATH}/inc/rav1e"
IMPORTED_SONAME rav1e)
target_link_libraries(rav1e::rav1e INTERFACE ntdll.lib userenv.lib ws2_32.lib
bcrypt.lib)

View File

@ -59,6 +59,7 @@ Run ``build_prepare.py`` to configure the build::
build architecture (default: same as host Python)
--nmake build dependencies using NMake instead of Ninja
--no-imagequant skip GPL-licensed optional dependency libimagequant
--no-avif skip optional dependency libavif
--no-fribidi, --no-raqm
skip LGPL-licensed optional dependency FriBiDi

View File

@ -121,6 +121,9 @@ V = {
"TIFF": "4.6.0",
"XZ": "5.6.3",
"ZLIB": "1.3.1",
"MESON": "1.5.1",
"LIBAVIF": "1.1.1",
"RAV1E": "0.7.1",
}
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
@ -397,6 +400,57 @@ DEPS: dict[str, dict[str, Any]] = {
],
"bins": [r"*.dll"],
},
"rav1e": {
"url": (
f"https://github.com/xiph/rav1e/releases/download/v{V['RAV1E']}/"
f"rav1e-{V['RAV1E']}-windows-msvc-generic.zip"
),
"filename": f"rav1e-{V['RAV1E']}-windows-msvc-generic.zip",
"dir": "rav1e-windows-msvc-sdk",
"license": "LICENSE",
"build": [
cmd_xcopy("include", "{inc_dir}"),
],
"bins": [r"bin\*.dll"],
"libs": [r"lib\*.*"],
},
"libavif": {
"url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip",
"filename": f"libavif-{V['LIBAVIF']}.zip",
"dir": f"libavif-{V['LIBAVIF']}",
"license": "LICENSE",
"build": [
cmd_mkdir("build.pillow"),
cmd_cd("build.pillow"),
" ".join(
[
"{cmake}",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_VERBOSE_MAKEFILE=ON",
"-DCMAKE_RULE_MESSAGES:BOOL=OFF",
"-DCMAKE_C_COMPILER=cl.exe",
"-DCMAKE_CXX_COMPILER=cl.exe",
"-DCMAKE_C_FLAGS=-nologo",
"-DCMAKE_CXX_FLAGS=-nologo",
"-DBUILD_SHARED_LIBS=OFF",
"-DAVIF_CODEC_AOM=LOCAL",
"-DAVIF_LIBYUV=LOCAL",
"-DAVIF_LIBSHARPYUV=LOCAL",
"-DAVIF_CODEC_RAV1E=SYSTEM",
"-DAVIF_RAV1E_ROOT={build_dir}",
"-DCMAKE_MODULE_PATH={winbuild_dir_cmake}",
"-DAVIF_CODEC_DAV1D=LOCAL",
"-DAVIF_CODEC_SVT=LOCAL",
'-G "Ninja"',
"..",
]
),
"ninja -v",
cmd_cd(".."),
cmd_xcopy("include", "{inc_dir}"),
],
"libs": [r"build.pillow\avif.lib"],
},
}
@ -620,12 +674,15 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str:
def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> None:
lines = [r'call "{build_dir}\build_env.cmd"']
gha_groups = "GITHUB_ACTIONS" in os.environ
scripts = ["install_meson.cmd"]
for dep_name in DEPS:
print()
if dep_name in disabled:
print(f"Skipping disabled dependency {dep_name}")
continue
script = build_dep(dep_name, prefs, verbose)
scripts.append(build_dep(dep_name, prefs, verbose))
for script in scripts:
if gha_groups:
lines.append(f"@echo ::group::Running {script}")
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
@ -699,6 +756,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]
@ -739,12 +801,15 @@ def main() -> None:
disabled += ["libimagequant"]
if args.no_fribidi:
disabled += ["fribidi"]
if args.no_avif or args.architecture != "AMD64":
disabled += ["rav1e", "libavif"]
prefs = {
"architecture": args.architecture,
**arch_prefs,
# Pillow paths
"winbuild_dir": winbuild_dir,
"winbuild_dir_cmake": winbuild_dir.replace("\\", "/"),
# Build paths
"bin_dir": bin_dir,
"build_dir": args.build_dir,
@ -766,6 +831,18 @@ def main() -> None:
print()
write_script(".gitignore", ["*"], prefs, args.verbose)
write_script(
"install_meson.cmd",
[
r'call "{build_dir}\build_env.cmd"',
"@echo " + ("=" * 70),
f"@echo ==== {'Building meson':<60} ====",
"@echo " + ("=" * 70),
f"python -mpip install meson=={V['MESON']}",
],
prefs,
args.verbose,
)
build_env(prefs, args.verbose)
build_dep_all(disabled, prefs, args.verbose)