mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-10 16:22:22 +03:00
Added type hints (#2)
* Added type hints * Updated nasm to 2.16.03 * Removed duplicate meson install * Simplified code * Sort formats alphabetically * tile is already an empty list --------- Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
This commit is contained in:
parent
3878b588a4
commit
e2add24ec5
26
.github/workflows/wheels-dependencies.sh
vendored
26
.github/workflows/wheels-dependencies.sh
vendored
|
@ -66,21 +66,25 @@ function build_harfbuzz {
|
||||||
}
|
}
|
||||||
|
|
||||||
function install_rav1e {
|
function install_rav1e {
|
||||||
if [[ -n "$IS_MACOS" ]] && [[ "$PLAT" == "arm64" ]]; then
|
if [ -n "$IS_MACOS" ]; then
|
||||||
librav1e_tgz=librav1e-${RAV1E_VERSION}-macos-aarch64.tar.gz
|
suffix="macos"
|
||||||
elif [ -n "$IS_MACOS" ]; then
|
if [[ "$PLAT" == "arm64" ]]; then
|
||||||
librav1e_tgz=librav1e-${RAV1E_VERSION}-macos.tar.gz
|
suffix+="-aarch64"
|
||||||
elif [ "$PLAT" == "aarch64" ]; then
|
fi
|
||||||
librav1e_tgz=librav1e-${RAV1E_VERSION}-linux-aarch64.tar.gz
|
|
||||||
else
|
else
|
||||||
librav1e_tgz=librav1e-${RAV1E_VERSION}-linux-generic.tar.gz
|
suffix="linux"
|
||||||
|
if [[ "$PLAT" == "aarch64" ]]; then
|
||||||
|
suffix+="-aarch64"
|
||||||
|
else
|
||||||
|
suffix+="-generic"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
curl -sLo - \
|
curl -sLo - \
|
||||||
https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/$librav1e_tgz \
|
https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/librav1e-$RAV1E_VERSION-$suffix.tar.gz \
|
||||||
| tar -C $BUILD_PREFIX --exclude LICENSE --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf -
|
| tar -C $BUILD_PREFIX --exclude LICENSE --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf -
|
||||||
|
|
||||||
if [ ! -n "$IS_MACOS" ]; then
|
if [ -z "$IS_MACOS" ]; then
|
||||||
sed -i 's/-lgcc_s/-lgcc_eh/g' "${BUILD_PREFIX}/lib/pkgconfig/rav1e.pc"
|
sed -i 's/-lgcc_s/-lgcc_eh/g' "${BUILD_PREFIX}/lib/pkgconfig/rav1e.pc"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -101,7 +105,7 @@ function build_libavif {
|
||||||
python -m pip install meson ninja
|
python -m pip install meson ninja
|
||||||
|
|
||||||
if [[ "$PLAT" == "x86_64" ]]; then
|
if [[ "$PLAT" == "x86_64" ]]; then
|
||||||
build_simple nasm 2.15.05 https://www.nasm.us/pub/nasm/releasebuilds/2.15.05/
|
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local cmake=$(get_modern_cmake)
|
local cmake=$(get_modern_cmake)
|
||||||
|
@ -210,7 +214,7 @@ if [[ -n "$IS_MACOS" ]]; then
|
||||||
brew remove --ignore-dependencies webp aom libavif
|
brew remove --ignore-dependencies webp aom libavif
|
||||||
fi
|
fi
|
||||||
|
|
||||||
brew install meson pkg-config
|
brew install pkg-config
|
||||||
|
|
||||||
# clear bash path cache for curl
|
# clear bash path cache for curl
|
||||||
hash -d curl
|
hash -d curl
|
||||||
|
|
|
@ -20,7 +20,7 @@ pytestmark = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_leak_load():
|
def test_leak_load() -> None:
|
||||||
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
|
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
|
||||||
|
|
||||||
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
|
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
|
||||||
|
@ -30,7 +30,7 @@ def test_leak_load():
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_leak_save():
|
def test_leak_save() -> None:
|
||||||
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
|
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
|
||||||
|
|
||||||
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
|
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
|
||||||
|
|
|
@ -5,13 +5,23 @@ import os
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
|
from collections.abc import Generator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import AvifImagePlugin, Image, ImageDraw, UnidentifiedImageError, features
|
from PIL import (
|
||||||
|
AvifImagePlugin,
|
||||||
|
Image,
|
||||||
|
ImageDraw,
|
||||||
|
ImageFile,
|
||||||
|
UnidentifiedImageError,
|
||||||
|
features,
|
||||||
|
)
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
PillowLeakTestCase,
|
PillowLeakTestCase,
|
||||||
|
@ -33,41 +43,43 @@ except ImportError:
|
||||||
TEST_AVIF_FILE = "Tests/images/avif/hopper.avif"
|
TEST_AVIF_FILE = "Tests/images/avif/hopper.avif"
|
||||||
|
|
||||||
|
|
||||||
def assert_xmp_orientation(xmp, expected):
|
def assert_xmp_orientation(xmp: bytes | None, expected: int) -> None:
|
||||||
assert isinstance(xmp, bytes)
|
assert isinstance(xmp, bytes)
|
||||||
root = xml.etree.ElementTree.fromstring(xmp)
|
root = xml.etree.ElementTree.fromstring(xmp)
|
||||||
orientation = None
|
orientation = None
|
||||||
for elem in root.iter():
|
for elem in root.iter():
|
||||||
if elem.tag.endswith("}Description"):
|
if elem.tag.endswith("}Description"):
|
||||||
orientation = elem.attrib.get("{http://ns.adobe.com/tiff/1.0/}Orientation")
|
tag_orientation = elem.attrib.get(
|
||||||
if orientation:
|
"{http://ns.adobe.com/tiff/1.0/}Orientation"
|
||||||
orientation = int(orientation)
|
)
|
||||||
|
if tag_orientation:
|
||||||
|
orientation = int(tag_orientation)
|
||||||
break
|
break
|
||||||
assert orientation == expected
|
assert orientation == expected
|
||||||
|
|
||||||
|
|
||||||
def roundtrip(im, **options):
|
def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile:
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, "AVIF", **options)
|
im.save(out, "AVIF", **options)
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
return Image.open(out)
|
return Image.open(out)
|
||||||
|
|
||||||
|
|
||||||
def skip_unless_avif_decoder(codec_name):
|
def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator:
|
||||||
reason = f"{codec_name} decode not available"
|
reason = f"{codec_name} decode not available"
|
||||||
return pytest.mark.skipif(
|
return pytest.mark.skipif(
|
||||||
not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason
|
not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def skip_unless_avif_encoder(codec_name):
|
def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator:
|
||||||
reason = f"{codec_name} encode not available"
|
reason = f"{codec_name} encode not available"
|
||||||
return pytest.mark.skipif(
|
return pytest.mark.skipif(
|
||||||
not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason
|
not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_docker_qemu():
|
def is_docker_qemu() -> bool:
|
||||||
try:
|
try:
|
||||||
init_proc_exe = os.readlink("/proc/1/exe")
|
init_proc_exe = os.readlink("/proc/1/exe")
|
||||||
except: # noqa: E722
|
except: # noqa: E722
|
||||||
|
@ -76,7 +88,7 @@ def is_docker_qemu():
|
||||||
return "qemu" in init_proc_exe
|
return "qemu" in init_proc_exe
|
||||||
|
|
||||||
|
|
||||||
def has_alpha_premultiplied(im_bytes):
|
def has_alpha_premultiplied(im_bytes: bytes) -> bool:
|
||||||
stream = BytesIO(im_bytes)
|
stream = BytesIO(im_bytes)
|
||||||
length = len(im_bytes)
|
length = len(im_bytes)
|
||||||
while stream.tell() < length:
|
while stream.tell() < length:
|
||||||
|
@ -110,7 +122,7 @@ def has_alpha_premultiplied(im_bytes):
|
||||||
|
|
||||||
|
|
||||||
class TestUnsupportedAvif:
|
class TestUnsupportedAvif:
|
||||||
def test_unsupported(self, monkeypatch):
|
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
|
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
|
||||||
|
|
||||||
file_path = "Tests/images/avif/hopper.avif"
|
file_path = "Tests/images/avif/hopper.avif"
|
||||||
|
@ -119,7 +131,7 @@ class TestUnsupportedAvif:
|
||||||
lambda: pytest.raises(UnidentifiedImageError, Image.open, file_path),
|
lambda: pytest.raises(UnidentifiedImageError, Image.open, file_path),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_unsupported_open(self, monkeypatch):
|
def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
|
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
|
||||||
|
|
||||||
file_path = "Tests/images/avif/hopper.avif"
|
file_path = "Tests/images/avif/hopper.avif"
|
||||||
|
@ -129,11 +141,14 @@ class TestUnsupportedAvif:
|
||||||
|
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
class TestFileAvif:
|
class TestFileAvif:
|
||||||
def test_version(self):
|
def test_version(self) -> None:
|
||||||
_avif.AvifCodecVersions()
|
_avif.AvifCodecVersions()
|
||||||
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("avif"))
|
|
||||||
|
|
||||||
def test_read(self):
|
version = features.version_module("avif")
|
||||||
|
assert version is not None
|
||||||
|
assert re.search(r"\d+\.\d+\.\d+$", version)
|
||||||
|
|
||||||
|
def test_read(self) -> None:
|
||||||
"""
|
"""
|
||||||
Can we read an AVIF file without error?
|
Can we read an AVIF file without error?
|
||||||
Does it have the bits we expect?
|
Does it have the bits we expect?
|
||||||
|
@ -153,10 +168,10 @@ class TestFileAvif:
|
||||||
image, "Tests/images/avif/hopper_avif_write.png", 12.0
|
image, "Tests/images/avif/hopper_avif_write.png", 12.0
|
||||||
)
|
)
|
||||||
|
|
||||||
def _roundtrip(self, tmp_path, mode, epsilon, args={}):
|
def _roundtrip(self, tmp_path: Path, mode: str, epsilon: float) -> None:
|
||||||
temp_file = str(tmp_path / "temp.avif")
|
temp_file = str(tmp_path / "temp.avif")
|
||||||
|
|
||||||
hopper(mode).save(temp_file, **args)
|
hopper(mode).save(temp_file)
|
||||||
with Image.open(temp_file) as image:
|
with Image.open(temp_file) as image:
|
||||||
assert image.mode == "RGB"
|
assert image.mode == "RGB"
|
||||||
assert image.size == (128, 128)
|
assert image.size == (128, 128)
|
||||||
|
@ -179,7 +194,7 @@ class TestFileAvif:
|
||||||
target = target.convert("RGB")
|
target = target.convert("RGB")
|
||||||
assert_image_similar(image, target, epsilon)
|
assert_image_similar(image, target, epsilon)
|
||||||
|
|
||||||
def test_write_rgb(self, tmp_path):
|
def test_write_rgb(self, tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Can we write a RGB mode file to avif without error?
|
Can we write a RGB mode file to avif without error?
|
||||||
Does it have the bits we expect?
|
Does it have the bits we expect?
|
||||||
|
@ -187,32 +202,34 @@ class TestFileAvif:
|
||||||
|
|
||||||
self._roundtrip(tmp_path, "RGB", 12.5)
|
self._roundtrip(tmp_path, "RGB", 12.5)
|
||||||
|
|
||||||
def test_AvifEncoder_with_invalid_args(self):
|
def test_AvifEncoder_with_invalid_args(self) -> None:
|
||||||
"""
|
"""
|
||||||
Calling encoder functions with no arguments should result in an error.
|
Calling encoder functions with no arguments should result in an error.
|
||||||
"""
|
"""
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
_avif.AvifEncoder()
|
_avif.AvifEncoder()
|
||||||
|
|
||||||
def test_AvifDecoder_with_invalid_args(self):
|
def test_AvifDecoder_with_invalid_args(self) -> None:
|
||||||
"""
|
"""
|
||||||
Calling decoder functions with no arguments should result in an error.
|
Calling decoder functions with no arguments should result in an error.
|
||||||
"""
|
"""
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
_avif.AvifDecoder()
|
_avif.AvifDecoder()
|
||||||
|
|
||||||
def test_encoder_finish_none_error(self, monkeypatch, tmp_path):
|
def test_encoder_finish_none_error(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
"""Save should raise an OSError if AvifEncoder.finish returns None"""
|
"""Save should raise an OSError if AvifEncoder.finish returns None"""
|
||||||
|
|
||||||
class _mock_avif:
|
class _mock_avif:
|
||||||
class AvifEncoder:
|
class AvifEncoder:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args: Any) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add(self, *args, **kwargs):
|
def add(self, *args: Any) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def finish(self):
|
def finish(self) -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif)
|
monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif)
|
||||||
|
@ -222,25 +239,25 @@ class TestFileAvif:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.save(test_file)
|
im.save(test_file)
|
||||||
|
|
||||||
def test_no_resource_warning(self, tmp_path):
|
def test_no_resource_warning(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as image:
|
with Image.open(TEST_AVIF_FILE) as image:
|
||||||
temp_file = str(tmp_path / "temp.avif")
|
temp_file = str(tmp_path / "temp.avif")
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
image.save(temp_file)
|
image.save(temp_file)
|
||||||
|
|
||||||
@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
|
@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
|
||||||
def test_accept_ftyp_brands(self, major_brand):
|
def test_accept_ftyp_brands(self, major_brand: bytes) -> None:
|
||||||
data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand
|
data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand
|
||||||
assert AvifImagePlugin._accept(data) is True
|
assert AvifImagePlugin._accept(data) is True
|
||||||
|
|
||||||
def test_file_pointer_could_be_reused(self):
|
def test_file_pointer_could_be_reused(self) -> None:
|
||||||
with open(TEST_AVIF_FILE, "rb") as blob:
|
with open(TEST_AVIF_FILE, "rb") as blob:
|
||||||
with Image.open(blob) as im:
|
with Image.open(blob) as im:
|
||||||
im.load()
|
im.load()
|
||||||
with Image.open(blob) as im:
|
with Image.open(blob) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
def test_background_from_gif(self, tmp_path):
|
def test_background_from_gif(self, tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/chi.gif") as im:
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
original_value = im.convert("RGB").getpixel((1, 1))
|
original_value = im.convert("RGB").getpixel((1, 1))
|
||||||
|
|
||||||
|
@ -260,20 +277,20 @@ class TestFileAvif:
|
||||||
)
|
)
|
||||||
assert difference < 5
|
assert difference < 5
|
||||||
|
|
||||||
def test_save_single_frame(self, tmp_path):
|
def test_save_single_frame(self, tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.avif")
|
temp_file = str(tmp_path / "temp.avif")
|
||||||
with Image.open("Tests/images/chi.gif") as im:
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
|
|
||||||
def test_invalid_file(self):
|
def test_invalid_file(self) -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
AvifImagePlugin.AvifImageFile(invalid_file)
|
AvifImagePlugin.AvifImageFile(invalid_file)
|
||||||
|
|
||||||
def test_load_transparent_rgb(self):
|
def test_load_transparent_rgb(self) -> None:
|
||||||
test_file = "Tests/images/avif/transparency.avif"
|
test_file = "Tests/images/avif/transparency.avif"
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert_image(im, "RGBA", (64, 64))
|
assert_image(im, "RGBA", (64, 64))
|
||||||
|
@ -281,7 +298,7 @@ class TestFileAvif:
|
||||||
# image has 876 transparent pixels
|
# image has 876 transparent pixels
|
||||||
assert im.getchannel("A").getcolors()[0][0] == 876
|
assert im.getchannel("A").getcolors()[0][0] == 876
|
||||||
|
|
||||||
def test_save_transparent(self, tmp_path):
|
def test_save_transparent(self, tmp_path: Path) -> None:
|
||||||
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
|
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
|
||||||
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||||
|
|
||||||
|
@ -293,7 +310,7 @@ class TestFileAvif:
|
||||||
assert_image(im, "RGBA", (10, 10))
|
assert_image(im, "RGBA", (10, 10))
|
||||||
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||||
|
|
||||||
def test_save_icc_profile(self):
|
def test_save_icc_profile(self) -> None:
|
||||||
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
|
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
|
||||||
assert im.info.get("icc_profile") is None
|
assert im.info.get("icc_profile") is None
|
||||||
|
|
||||||
|
@ -304,32 +321,32 @@ class TestFileAvif:
|
||||||
im = roundtrip(im, icc_profile=expected_icc)
|
im = roundtrip(im, icc_profile=expected_icc)
|
||||||
assert im.info["icc_profile"] == expected_icc
|
assert im.info["icc_profile"] == expected_icc
|
||||||
|
|
||||||
def test_discard_icc_profile(self):
|
def test_discard_icc_profile(self) -> None:
|
||||||
with Image.open("Tests/images/avif/icc_profile.avif") as im:
|
with Image.open("Tests/images/avif/icc_profile.avif") as im:
|
||||||
im = roundtrip(im, icc_profile=None)
|
im = roundtrip(im, icc_profile=None)
|
||||||
assert "icc_profile" not in im.info
|
assert "icc_profile" not in im.info
|
||||||
|
|
||||||
def test_roundtrip_icc_profile(self):
|
def test_roundtrip_icc_profile(self) -> None:
|
||||||
with Image.open("Tests/images/avif/icc_profile.avif") as im:
|
with Image.open("Tests/images/avif/icc_profile.avif") as im:
|
||||||
expected_icc = im.info["icc_profile"]
|
expected_icc = im.info["icc_profile"]
|
||||||
|
|
||||||
im = roundtrip(im)
|
im = roundtrip(im)
|
||||||
assert im.info["icc_profile"] == expected_icc
|
assert im.info["icc_profile"] == expected_icc
|
||||||
|
|
||||||
def test_roundtrip_no_icc_profile(self):
|
def test_roundtrip_no_icc_profile(self) -> None:
|
||||||
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
|
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
|
||||||
assert im.info.get("icc_profile") is None
|
assert im.info.get("icc_profile") is None
|
||||||
|
|
||||||
im = roundtrip(im)
|
im = roundtrip(im)
|
||||||
assert "icc_profile" not in im.info
|
assert "icc_profile" not in im.info
|
||||||
|
|
||||||
def test_exif(self):
|
def test_exif(self) -> None:
|
||||||
# With an EXIF chunk
|
# With an EXIF chunk
|
||||||
with Image.open("Tests/images/avif/exif.avif") as im:
|
with Image.open("Tests/images/avif/exif.avif") as im:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
assert exif[274] == 1
|
assert exif[274] == 1
|
||||||
|
|
||||||
def test_exif_save(self, tmp_path):
|
def test_exif_save(self, tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/avif/exif.avif") as im:
|
with Image.open("Tests/images/avif/exif.avif") as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
im.save(test_file)
|
im.save(test_file)
|
||||||
|
@ -338,7 +355,7 @@ class TestFileAvif:
|
||||||
exif = reloaded.getexif()
|
exif = reloaded.getexif()
|
||||||
assert exif[274] == 1
|
assert exif[274] == 1
|
||||||
|
|
||||||
def test_exif_obj_argument(self, tmp_path):
|
def test_exif_obj_argument(self, tmp_path: Path) -> None:
|
||||||
exif = Image.Exif()
|
exif = Image.Exif()
|
||||||
exif[274] = 1
|
exif[274] = 1
|
||||||
exif_data = exif.tobytes()
|
exif_data = exif.tobytes()
|
||||||
|
@ -349,7 +366,7 @@ class TestFileAvif:
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
assert reloaded.info["exif"] == exif_data
|
assert reloaded.info["exif"] == exif_data
|
||||||
|
|
||||||
def test_exif_bytes_argument(self, tmp_path):
|
def test_exif_bytes_argument(self, tmp_path: Path) -> None:
|
||||||
exif = Image.Exif()
|
exif = Image.Exif()
|
||||||
exif[274] = 1
|
exif[274] = 1
|
||||||
exif_data = exif.tobytes()
|
exif_data = exif.tobytes()
|
||||||
|
@ -360,18 +377,18 @@ class TestFileAvif:
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
assert reloaded.info["exif"] == exif_data
|
assert reloaded.info["exif"] == exif_data
|
||||||
|
|
||||||
def test_exif_invalid(self, tmp_path):
|
def test_exif_invalid(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.save(test_file, exif=b"invalid")
|
im.save(test_file, exif=b"invalid")
|
||||||
|
|
||||||
def test_xmp(self):
|
def test_xmp(self) -> None:
|
||||||
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
|
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
|
||||||
xmp = im.info.get("xmp")
|
xmp = im.info.get("xmp")
|
||||||
assert_xmp_orientation(xmp, 3)
|
assert_xmp_orientation(xmp, 3)
|
||||||
|
|
||||||
def test_xmp_save(self, tmp_path):
|
def test_xmp_save(self, tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
|
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
im.save(test_file)
|
im.save(test_file)
|
||||||
|
@ -380,7 +397,7 @@ class TestFileAvif:
|
||||||
xmp = reloaded.info.get("xmp")
|
xmp = reloaded.info.get("xmp")
|
||||||
assert_xmp_orientation(xmp, 3)
|
assert_xmp_orientation(xmp, 3)
|
||||||
|
|
||||||
def test_xmp_save_from_png(self, tmp_path):
|
def test_xmp_save_from_png(self, tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
im.save(test_file)
|
im.save(test_file)
|
||||||
|
@ -389,7 +406,7 @@ class TestFileAvif:
|
||||||
xmp = reloaded.info.get("xmp")
|
xmp = reloaded.info.get("xmp")
|
||||||
assert_xmp_orientation(xmp, 3)
|
assert_xmp_orientation(xmp, 3)
|
||||||
|
|
||||||
def test_xmp_save_argument(self, tmp_path):
|
def test_xmp_save_argument(self, tmp_path: Path) -> None:
|
||||||
xmp_arg = "\n".join(
|
xmp_arg = "\n".join(
|
||||||
[
|
[
|
||||||
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>',
|
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>',
|
||||||
|
@ -411,11 +428,11 @@ class TestFileAvif:
|
||||||
xmp = reloaded.info.get("xmp")
|
xmp = reloaded.info.get("xmp")
|
||||||
assert_xmp_orientation(xmp, 1)
|
assert_xmp_orientation(xmp, 1)
|
||||||
|
|
||||||
def test_tell(self):
|
def test_tell(self) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
assert im.tell() == 0
|
assert im.tell() == 0
|
||||||
|
|
||||||
def test_seek(self):
|
def test_seek(self) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
|
|
||||||
|
@ -423,23 +440,23 @@ class TestFileAvif:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
|
||||||
@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:0:0"])
|
@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:0:0"])
|
||||||
def test_encoder_subsampling(self, tmp_path, subsampling):
|
def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
im.save(test_file, subsampling=subsampling)
|
im.save(test_file, subsampling=subsampling)
|
||||||
|
|
||||||
def test_encoder_subsampling_invalid(self, tmp_path):
|
def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.save(test_file, subsampling="foo")
|
im.save(test_file, subsampling="foo")
|
||||||
|
|
||||||
def test_encoder_range(self, tmp_path):
|
def test_encoder_range(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
im.save(test_file, range="limited")
|
im.save(test_file, range="limited")
|
||||||
|
|
||||||
def test_encoder_range_invalid(self, tmp_path):
|
def test_encoder_range_invalid(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -447,12 +464,12 @@ class TestFileAvif:
|
||||||
|
|
||||||
@skip_unless_avif_encoder("aom")
|
@skip_unless_avif_encoder("aom")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_encoder_codec_param(self, tmp_path):
|
def test_encoder_codec_param(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
im.save(test_file, codec="aom")
|
im.save(test_file, codec="aom")
|
||||||
|
|
||||||
def test_encoder_codec_invalid(self, tmp_path):
|
def test_encoder_codec_invalid(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -460,7 +477,7 @@ class TestFileAvif:
|
||||||
|
|
||||||
@skip_unless_avif_decoder("dav1d")
|
@skip_unless_avif_decoder("dav1d")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_encoder_codec_cannot_encode(self, tmp_path):
|
def test_encoder_codec_cannot_encode(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -468,7 +485,7 @@ class TestFileAvif:
|
||||||
|
|
||||||
@skip_unless_avif_encoder("aom")
|
@skip_unless_avif_encoder("aom")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_encoder_advanced_codec_options(self):
|
def test_encoder_advanced_codec_options(self) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
ctrl_buf = BytesIO()
|
ctrl_buf = BytesIO()
|
||||||
im.save(ctrl_buf, "AVIF", codec="aom")
|
im.save(ctrl_buf, "AVIF", codec="aom")
|
||||||
|
@ -487,7 +504,9 @@ class TestFileAvif:
|
||||||
@skip_unless_avif_encoder("aom")
|
@skip_unless_avif_encoder("aom")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
@pytest.mark.parametrize("val", [{"foo": "bar"}, 1234])
|
@pytest.mark.parametrize("val", [{"foo": "bar"}, 1234])
|
||||||
def test_encoder_advanced_codec_options_invalid(self, tmp_path, val):
|
def test_encoder_advanced_codec_options_invalid(
|
||||||
|
self, tmp_path: Path, val: dict[str, str] | int
|
||||||
|
) -> None:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -495,7 +514,7 @@ class TestFileAvif:
|
||||||
|
|
||||||
@skip_unless_avif_decoder("aom")
|
@skip_unless_avif_decoder("aom")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_decoder_codec_param(self):
|
def test_decoder_codec_param(self) -> None:
|
||||||
AvifImagePlugin.DECODE_CODEC_CHOICE = "aom"
|
AvifImagePlugin.DECODE_CODEC_CHOICE = "aom"
|
||||||
try:
|
try:
|
||||||
with Image.open(TEST_AVIF_FILE) as im:
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
@ -505,7 +524,7 @@ class TestFileAvif:
|
||||||
|
|
||||||
@skip_unless_avif_encoder("rav1e")
|
@skip_unless_avif_encoder("rav1e")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_decoder_codec_cannot_decode(self, tmp_path):
|
def test_decoder_codec_cannot_decode(self, tmp_path: Path) -> None:
|
||||||
AvifImagePlugin.DECODE_CODEC_CHOICE = "rav1e"
|
AvifImagePlugin.DECODE_CODEC_CHOICE = "rav1e"
|
||||||
try:
|
try:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -514,7 +533,7 @@ class TestFileAvif:
|
||||||
finally:
|
finally:
|
||||||
AvifImagePlugin.DECODE_CODEC_CHOICE = "auto"
|
AvifImagePlugin.DECODE_CODEC_CHOICE = "auto"
|
||||||
|
|
||||||
def test_decoder_codec_invalid(self):
|
def test_decoder_codec_invalid(self) -> None:
|
||||||
AvifImagePlugin.DECODE_CODEC_CHOICE = "foo"
|
AvifImagePlugin.DECODE_CODEC_CHOICE = "foo"
|
||||||
try:
|
try:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -525,22 +544,22 @@ class TestFileAvif:
|
||||||
|
|
||||||
@skip_unless_avif_encoder("aom")
|
@skip_unless_avif_encoder("aom")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_encoder_codec_available(self):
|
def test_encoder_codec_available(self) -> None:
|
||||||
assert _avif.encoder_codec_available("aom") is True
|
assert _avif.encoder_codec_available("aom") is True
|
||||||
|
|
||||||
def test_encoder_codec_available_bad_params(self):
|
def test_encoder_codec_available_bad_params(self) -> None:
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
_avif.encoder_codec_available()
|
_avif.encoder_codec_available()
|
||||||
|
|
||||||
@skip_unless_avif_decoder("dav1d")
|
@skip_unless_avif_decoder("dav1d")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_encoder_codec_available_cannot_decode(self):
|
def test_encoder_codec_available_cannot_decode(self) -> None:
|
||||||
assert _avif.encoder_codec_available("dav1d") is False
|
assert _avif.encoder_codec_available("dav1d") is False
|
||||||
|
|
||||||
def test_encoder_codec_available_invalid(self):
|
def test_encoder_codec_available_invalid(self) -> None:
|
||||||
assert _avif.encoder_codec_available("foo") is False
|
assert _avif.encoder_codec_available("foo") is False
|
||||||
|
|
||||||
def test_encoder_quality_valueerror(self, tmp_path):
|
def test_encoder_quality_valueerror(self, tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/avif/hopper.avif") as im:
|
with Image.open("Tests/images/avif/hopper.avif") as im:
|
||||||
test_file = str(tmp_path / "temp.avif")
|
test_file = str(tmp_path / "temp.avif")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -548,23 +567,23 @@ class TestFileAvif:
|
||||||
|
|
||||||
@skip_unless_avif_decoder("aom")
|
@skip_unless_avif_decoder("aom")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_decoder_codec_available(self):
|
def test_decoder_codec_available(self) -> None:
|
||||||
assert _avif.decoder_codec_available("aom") is True
|
assert _avif.decoder_codec_available("aom") is True
|
||||||
|
|
||||||
def test_decoder_codec_available_bad_params(self):
|
def test_decoder_codec_available_bad_params(self) -> None:
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
_avif.decoder_codec_available()
|
_avif.decoder_codec_available()
|
||||||
|
|
||||||
@skip_unless_avif_encoder("rav1e")
|
@skip_unless_avif_encoder("rav1e")
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
def test_decoder_codec_available_cannot_decode(self):
|
def test_decoder_codec_available_cannot_decode(self) -> None:
|
||||||
assert _avif.decoder_codec_available("rav1e") is False
|
assert _avif.decoder_codec_available("rav1e") is False
|
||||||
|
|
||||||
def test_decoder_codec_available_invalid(self):
|
def test_decoder_codec_available_invalid(self) -> None:
|
||||||
assert _avif.decoder_codec_available("foo") is False
|
assert _avif.decoder_codec_available("foo") is False
|
||||||
|
|
||||||
@pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"])
|
@pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"])
|
||||||
def test_decoder_upsampling(self, upsampling):
|
def test_decoder_upsampling(self, upsampling: str) -> None:
|
||||||
AvifImagePlugin.CHROMA_UPSAMPLING = upsampling
|
AvifImagePlugin.CHROMA_UPSAMPLING = upsampling
|
||||||
try:
|
try:
|
||||||
with Image.open(TEST_AVIF_FILE):
|
with Image.open(TEST_AVIF_FILE):
|
||||||
|
@ -572,7 +591,7 @@ class TestFileAvif:
|
||||||
finally:
|
finally:
|
||||||
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"
|
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"
|
||||||
|
|
||||||
def test_decoder_upsampling_invalid(self):
|
def test_decoder_upsampling_invalid(self) -> None:
|
||||||
AvifImagePlugin.CHROMA_UPSAMPLING = "foo"
|
AvifImagePlugin.CHROMA_UPSAMPLING = "foo"
|
||||||
try:
|
try:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -581,7 +600,7 @@ class TestFileAvif:
|
||||||
finally:
|
finally:
|
||||||
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"
|
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"
|
||||||
|
|
||||||
def test_p_mode_transparency(self):
|
def test_p_mode_transparency(self) -> None:
|
||||||
im = Image.new("P", size=(64, 64))
|
im = Image.new("P", size=(64, 64))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
draw.rectangle(xy=[(0, 0), (32, 32)], fill=255)
|
draw.rectangle(xy=[(0, 0), (32, 32)], fill=255)
|
||||||
|
@ -595,19 +614,19 @@ class TestFileAvif:
|
||||||
|
|
||||||
assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1)
|
assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1)
|
||||||
|
|
||||||
def test_decoder_strict_flags(self):
|
def test_decoder_strict_flags(self) -> None:
|
||||||
# This would fail if full avif strictFlags were enabled
|
# This would fail if full avif strictFlags were enabled
|
||||||
with Image.open("Tests/images/avif/chimera-missing-pixi.avif") as im:
|
with Image.open("Tests/images/avif/chimera-missing-pixi.avif") as im:
|
||||||
assert im.size == (480, 270)
|
assert im.size == (480, 270)
|
||||||
|
|
||||||
@skip_unless_avif_encoder("aom")
|
@skip_unless_avif_encoder("aom")
|
||||||
def test_aom_optimizations(self):
|
def test_aom_optimizations(self) -> None:
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
im.save(buf, format="AVIF", codec="aom", speed=1)
|
im.save(buf, format="AVIF", codec="aom", speed=1)
|
||||||
|
|
||||||
@skip_unless_avif_encoder("svt")
|
@skip_unless_avif_encoder("svt")
|
||||||
def test_svt_optimizations(self):
|
def test_svt_optimizations(self) -> None:
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
im.save(buf, format="AVIF", codec="svt", speed=1)
|
im.save(buf, format="AVIF", codec="svt", speed=1)
|
||||||
|
@ -616,14 +635,14 @@ class TestFileAvif:
|
||||||
@skip_unless_feature("avif")
|
@skip_unless_feature("avif")
|
||||||
class TestAvifAnimation:
|
class TestAvifAnimation:
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def star_frames(self):
|
def star_frames(self) -> Generator[list[ImageFile.ImageFile], None, None]:
|
||||||
with Image.open("Tests/images/avif/star.png") as f1:
|
with Image.open("Tests/images/avif/star.png") as f1:
|
||||||
with Image.open("Tests/images/avif/star90.png") as f2:
|
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/star180.png") as f3:
|
||||||
with Image.open("Tests/images/avif/star270.png") as f4:
|
with Image.open("Tests/images/avif/star270.png") as f4:
|
||||||
yield [f1, f2, f3, f4]
|
yield [f1, f2, f3, f4]
|
||||||
|
|
||||||
def test_n_frames(self):
|
def test_n_frames(self) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure that AVIF format sets n_frames and is_animated attributes
|
Ensure that AVIF format sets n_frames and is_animated attributes
|
||||||
correctly.
|
correctly.
|
||||||
|
@ -637,7 +656,7 @@ class TestAvifAnimation:
|
||||||
assert im.n_frames == 5
|
assert im.n_frames == 5
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
def test_write_animation_L(self, tmp_path):
|
def test_write_animation_L(self, tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Convert an animated GIF to animated AVIF, then compare the frame
|
Convert an animated GIF to animated AVIF, then compare the frame
|
||||||
count, and first and last frames to ensure they're visually similar.
|
count, and first and last frames to ensure they're visually similar.
|
||||||
|
@ -661,13 +680,13 @@ class TestAvifAnimation:
|
||||||
im.load()
|
im.load()
|
||||||
assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0)
|
assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0)
|
||||||
|
|
||||||
def test_write_animation_RGB(self, tmp_path):
|
def test_write_animation_RGB(self, tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Write an animated AVIF from RGB frames, and ensure the frames
|
Write an animated AVIF from RGB frames, and ensure the frames
|
||||||
are visually similar to the originals.
|
are visually similar to the originals.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def check(temp_file):
|
def check(temp_file: str) -> None:
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
assert im.n_frames == 4
|
assert im.n_frames == 4
|
||||||
|
|
||||||
|
@ -688,7 +707,9 @@ class TestAvifAnimation:
|
||||||
check(temp_file1)
|
check(temp_file1)
|
||||||
|
|
||||||
# Tests appending using a generator
|
# Tests appending using a generator
|
||||||
def imGenerator(ims):
|
def imGenerator(
|
||||||
|
ims: list[ImageFile.ImageFile],
|
||||||
|
) -> Generator[ImageFile.ImageFile, None, None]:
|
||||||
yield from ims
|
yield from ims
|
||||||
|
|
||||||
temp_file2 = str(tmp_path / "temp_generator.avif")
|
temp_file2 = str(tmp_path / "temp_generator.avif")
|
||||||
|
@ -699,27 +720,27 @@ class TestAvifAnimation:
|
||||||
)
|
)
|
||||||
check(temp_file2)
|
check(temp_file2)
|
||||||
|
|
||||||
def test_sequence_dimension_mismatch_check(self, tmp_path):
|
def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.avif")
|
temp_file = str(tmp_path / "temp.avif")
|
||||||
frame1 = Image.new("RGB", (100, 100))
|
frame1 = Image.new("RGB", (100, 100))
|
||||||
frame2 = Image.new("RGB", (150, 150))
|
frame2 = Image.new("RGB", (150, 150))
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
frame1.save(temp_file, save_all=True, append_images=[frame2], duration=100)
|
frame1.save(temp_file, save_all=True, append_images=[frame2], duration=100)
|
||||||
|
|
||||||
def test_heif_raises_unidentified_image_error(self):
|
def test_heif_raises_unidentified_image_error(self) -> None:
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
with Image.open("Tests/images/avif/rgba10.heif"):
|
with Image.open("Tests/images/avif/rgba10.heif"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@pytest.mark.parametrize("alpha_premultipled", [False, True])
|
@pytest.mark.parametrize("alpha_premultipled", [False, True])
|
||||||
def test_alpha_premultiplied_true(self, alpha_premultipled):
|
def test_alpha_premultiplied_true(self, alpha_premultipled: bool) -> None:
|
||||||
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
|
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
|
||||||
im_buf = BytesIO()
|
im_buf = BytesIO()
|
||||||
im.save(im_buf, "AVIF", alpha_premultiplied=alpha_premultipled)
|
im.save(im_buf, "AVIF", alpha_premultiplied=alpha_premultipled)
|
||||||
im_bytes = im_buf.getvalue()
|
im_bytes = im_buf.getvalue()
|
||||||
assert has_alpha_premultiplied(im_bytes) is alpha_premultipled
|
assert has_alpha_premultiplied(im_bytes) is alpha_premultipled
|
||||||
|
|
||||||
def test_timestamp_and_duration(self, tmp_path):
|
def test_timestamp_and_duration(self, tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Try passing a list of durations, and make sure the encoded
|
Try passing a list of durations, and make sure the encoded
|
||||||
timestamps and durations are correct.
|
timestamps and durations are correct.
|
||||||
|
@ -748,7 +769,7 @@ class TestAvifAnimation:
|
||||||
assert im.info["timestamp"] == ts
|
assert im.info["timestamp"] == ts
|
||||||
ts += durations[frame]
|
ts += durations[frame]
|
||||||
|
|
||||||
def test_seeking(self, tmp_path):
|
def test_seeking(self, tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Create an animated AVIF file, and then try seeking through frames in
|
Create an animated AVIF file, and then try seeking through frames in
|
||||||
reverse-order, verifying the timestamps and durations are correct.
|
reverse-order, verifying the timestamps and durations are correct.
|
||||||
|
@ -777,7 +798,7 @@ class TestAvifAnimation:
|
||||||
assert im.info["timestamp"] == ts
|
assert im.info["timestamp"] == ts
|
||||||
ts -= dur
|
ts -= dur
|
||||||
|
|
||||||
def test_seek_errors(self):
|
def test_seek_errors(self) -> None:
|
||||||
with Image.open("Tests/images/avif/star.avifs") as im:
|
with Image.open("Tests/images/avif/star.avifs") as im:
|
||||||
with pytest.raises(EOFError):
|
with pytest.raises(EOFError):
|
||||||
im.seek(-1)
|
im.seek(-1)
|
||||||
|
@ -797,11 +818,11 @@ class TestAvifLeaks(PillowLeakTestCase):
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
is_docker_qemu(), reason="Skipping on cross-architecture containers"
|
is_docker_qemu(), reason="Skipping on cross-architecture containers"
|
||||||
)
|
)
|
||||||
def test_leak_load(self):
|
def test_leak_load(self) -> None:
|
||||||
with open(TEST_AVIF_FILE, "rb") as f:
|
with open(TEST_AVIF_FILE, "rb") as f:
|
||||||
im_data = f.read()
|
im_data = f.read()
|
||||||
|
|
||||||
def core():
|
def core() -> None:
|
||||||
with Image.open(BytesIO(im_data)) as im:
|
with Image.open(BytesIO(im_data)) as im:
|
||||||
im.load()
|
im.load()
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
|
@ -235,7 +235,7 @@ following options are available::
|
||||||
**append_images**
|
**append_images**
|
||||||
A list of images to append as additional frames. Each of the
|
A list of images to append as additional frames. Each of the
|
||||||
images in the list can be single or multiframe images.
|
images in the list can be single or multiframe images.
|
||||||
This is currently supported for GIF, PDF, PNG, TIFF, WebP, and AVIF.
|
This is currently supported for AVIF, GIF, PDF, PNG, TIFF and WebP.
|
||||||
|
|
||||||
It is also supported for ICO and ICNS. If images are passed in of relevant
|
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.
|
sizes, they will be used instead of scaling down the main image.
|
||||||
|
@ -1321,7 +1321,7 @@ as 8-bit RGB(A).
|
||||||
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
|
|
||||||
**quality**
|
**quality**
|
||||||
Integer, 1-100, Defaults to 90. 0 gives the smallest size and poorest
|
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
|
quality, 100 the largest and best quality. The value of this setting
|
||||||
controls the ``qmin`` and ``qmax`` encoder options.
|
controls the ``qmin`` and ``qmax`` encoder options.
|
||||||
|
|
||||||
|
@ -1331,19 +1331,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
values are, the worse the quality.
|
values are, the worse the quality.
|
||||||
|
|
||||||
**subsampling**
|
**subsampling**
|
||||||
If present, sets the subsampling for the encoder. Defaults to ``"4:2:0``".
|
If present, sets the subsampling for the encoder. Defaults to ``4:2:0``.
|
||||||
Options include:
|
Options include:
|
||||||
|
|
||||||
* ``"4:0:0"``
|
* ``4:0:0``
|
||||||
* ``"4:2:0"``
|
* ``4:2:0``
|
||||||
* ``"4:2:2"``
|
* ``4:2:2``
|
||||||
* ``"4:4:4"``
|
* ``4:4:4``
|
||||||
|
|
||||||
**speed**
|
**speed**
|
||||||
Quality/speed trade-off (0=slower-better, 10=fastest). Defaults to 8.
|
Quality/speed trade-off (0=slower-better, 10=fastest). Defaults to 8.
|
||||||
|
|
||||||
**range**
|
**range**
|
||||||
YUV range, either "full" or "limited." Defaults to "full"
|
YUV range, either "full" or "limited". Defaults to "full"
|
||||||
|
|
||||||
**codec**
|
**codec**
|
||||||
AV1 codec to use for encoding. Possible values are "aom", "rav1e", and
|
AV1 codec to use for encoding. Possible values are "aom", "rav1e", and
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
from . import ExifTags, Image, ImageFile
|
from . import ExifTags, Image, ImageFile
|
||||||
|
|
||||||
|
@ -20,9 +21,9 @@ DEFAULT_MAX_THREADS = 0
|
||||||
_VALID_AVIF_MODES = {"RGB", "RGBA"}
|
_VALID_AVIF_MODES = {"RGB", "RGBA"}
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix):
|
def _accept(prefix: bytes) -> bool | str:
|
||||||
if prefix[4:8] != b"ftyp":
|
if prefix[4:8] != b"ftyp":
|
||||||
return
|
return False
|
||||||
coding_brands = (b"avif", b"avis")
|
coding_brands = (b"avif", b"avis")
|
||||||
container_brands = (b"mif1", b"msf1")
|
container_brands = (b"mif1", b"msf1")
|
||||||
major_brand = prefix[8:12]
|
major_brand = prefix[8:12]
|
||||||
|
@ -42,6 +43,7 @@ def _accept(prefix):
|
||||||
# Also, because this file might not actually be an AVIF file, we
|
# Also, because this file might not actually be an AVIF file, we
|
||||||
# don't raise an error if AVIF support isn't properly compiled.
|
# don't raise an error if AVIF support isn't properly compiled.
|
||||||
return True
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class AvifImageFile(ImageFile.ImageFile):
|
class AvifImageFile(ImageFile.ImageFile):
|
||||||
|
@ -53,7 +55,7 @@ class AvifImageFile(ImageFile.ImageFile):
|
||||||
def load_seek(self, pos: int) -> None:
|
def load_seek(self, pos: int) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _open(self):
|
def _open(self) -> None:
|
||||||
if not SUPPORTED:
|
if not SUPPORTED:
|
||||||
msg = (
|
msg = (
|
||||||
"image file could not be identified because AVIF "
|
"image file could not be identified because AVIF "
|
||||||
|
@ -71,7 +73,6 @@ class AvifImageFile(ImageFile.ImageFile):
|
||||||
self.n_frames = n_frames
|
self.n_frames = n_frames
|
||||||
self.is_animated = self.n_frames > 1
|
self.is_animated = self.n_frames > 1
|
||||||
self._mode = self.rawmode = mode
|
self._mode = self.rawmode = mode
|
||||||
self.tile = []
|
|
||||||
|
|
||||||
if icc:
|
if icc:
|
||||||
self.info["icc_profile"] = icc
|
self.info["icc_profile"] = icc
|
||||||
|
@ -80,13 +81,13 @@ class AvifImageFile(ImageFile.ImageFile):
|
||||||
if xmp:
|
if xmp:
|
||||||
self.info["xmp"] = xmp
|
self.info["xmp"] = xmp
|
||||||
|
|
||||||
def seek(self, frame):
|
def seek(self, frame: int) -> None:
|
||||||
if not self._seek_check(frame):
|
if not self._seek_check(frame):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.__frame = frame
|
self.__frame = frame
|
||||||
|
|
||||||
def load(self):
|
def load(self) -> Image.core.PixelAccess | None:
|
||||||
if self.__loaded != self.__frame:
|
if self.__loaded != self.__frame:
|
||||||
# We need to load the image data for this frame
|
# We need to load the image data for this frame
|
||||||
data, timescale, tsp_in_ts, dur_in_ts = self._decoder.get_frame(
|
data, timescale, tsp_in_ts, dur_in_ts = self._decoder.get_frame(
|
||||||
|
@ -102,19 +103,21 @@ class AvifImageFile(ImageFile.ImageFile):
|
||||||
if self.fp and self._exclusive_fp:
|
if self.fp and self._exclusive_fp:
|
||||||
self.fp.close()
|
self.fp.close()
|
||||||
self.fp = BytesIO(data)
|
self.fp = BytesIO(data)
|
||||||
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
|
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)]
|
||||||
|
|
||||||
return super().load()
|
return super().load()
|
||||||
|
|
||||||
def tell(self):
|
def tell(self) -> int:
|
||||||
return self.__frame
|
return self.__frame
|
||||||
|
|
||||||
|
|
||||||
def _save_all(im, fp, filename):
|
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
_save(im, fp, filename, save_all=True)
|
_save(im, fp, filename, save_all=True)
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename, save_all=False):
|
def _save(
|
||||||
|
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
|
||||||
|
) -> None:
|
||||||
info = im.encoderinfo.copy()
|
info = im.encoderinfo.copy()
|
||||||
if save_all:
|
if save_all:
|
||||||
append_images = list(info.get("append_images", []))
|
append_images = list(info.get("append_images", []))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user