mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-03-13 09:15:46 +03:00
Add AVIF plugin (using libavif)
This commit is contained in:
parent
11c654c187
commit
3878b588a4
|
@ -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
|
||||
|
|
9
.github/workflows/macos-install.sh
vendored
9
.github/workflows/macos-install.sh
vendored
|
@ -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
|
||||
|
|
1
.github/workflows/test-mingw.yml
vendored
1
.github/workflows/test-mingw.yml
vendored
|
@ -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 \
|
||||
|
|
14
.github/workflows/test-windows.yml
vendored
14
.github/workflows/test-windows.yml
vendored
|
@ -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'
|
||||
|
|
82
.github/workflows/wheels-dependencies.sh
vendored
82
.github/workflows/wheels-dependencies.sh
vendored
|
@ -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
44
Tests/check_avif_leaks.py
Normal 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()
|
|
@ -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
|
||||
|
||||
|
||||
|
|
BIN
Tests/images/avif/chimera-missing-pixi.avif
Normal file
BIN
Tests/images/avif/chimera-missing-pixi.avif
Normal file
Binary file not shown.
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.avif
Normal file
BIN
Tests/images/avif/hopper.avif
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/rgba10.heif
Normal file
BIN
Tests/images/avif/rgba10.heif
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/star180.png
Normal file
BIN
Tests/images/avif/star180.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
BIN
Tests/images/avif/star270.png
Normal file
BIN
Tests/images/avif/star270.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
BIN
Tests/images/avif/star90.png
Normal file
BIN
Tests/images/avif/star90.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 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.
809
Tests/test_file_avif.py
Normal file
809
Tests/test_file_avif.py
Normal 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
62
depends/install_libavif.sh
Executable 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
|
|
@ -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
|
||||
-----------------
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
---------------------------------
|
||||
|
||||
|
|
17
setup.py
17
setup.py
|
@ -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
282
src/PIL/AvifImagePlugin.py
Normal 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")
|
|
@ -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:
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
1084
src/_avif.c
Normal file
File diff suppressed because it is too large
Load Diff
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: 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.
|
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.
|
107
wheels/dependency_licenses/PATENTS.txt
Normal file
107
wheels/dependency_licenses/PATENTS.txt
Normal 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.
|
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-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.
|
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.
|
10
winbuild/Findrav1e.cmake
Normal file
10
winbuild/Findrav1e.cmake
Normal 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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user