diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 419f42e77b3..b64b00e4f5c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -86,8 +86,6 @@ 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 diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 524642a40c9..39d69b1f728 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -102,7 +102,7 @@ EOF function build_libavif { install_rav1e - python -m pip install meson ninja + python3 -m pip install meson ninja if [[ "$PLAT" == "x86_64" ]]; then build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/ diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 129d964e0d0..08b20047f0c 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -4,7 +4,6 @@ import os import re import warnings -import xml.etree.ElementTree from collections.abc import Generator from contextlib import contextmanager from io import BytesIO @@ -43,25 +42,13 @@ TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" -def assert_xmp_orientation(xmp: bytes | None, expected: int) -> None: - assert isinstance(xmp, bytes) - root = xml.etree.ElementTree.fromstring(xmp) - orientation = None - for elem in root.iter(): - if elem.tag.endswith("}Description"): - tag_orientation = elem.attrib.get( - "{http://ns.adobe.com/tiff/1.0/}Orientation" - ) - if tag_orientation: - orientation = int(tag_orientation) - break - assert orientation == expected +def assert_xmp_orientation(xmp: bytes, expected: int) -> None: + assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile: out = BytesIO() im.save(out, "AVIF", **options) - out.seek(0) return Image.open(out) @@ -82,7 +69,7 @@ def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: def is_docker_qemu() -> bool: try: init_proc_exe = os.readlink("/proc/1/exe") - except: # noqa: E722 + except (FileNotFoundError, PermissionError): return False else: return "qemu" in init_proc_exe @@ -125,18 +112,16 @@ class TestUnsupportedAvif: def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) - file_path = "Tests/images/avif/hopper.avif" - pytest.warns( - UserWarning, - lambda: pytest.raises(UnidentifiedImageError, Image.open, file_path), - ) + with pytest.warns(UserWarning): + with pytest.raises(UnidentifiedImageError): + with Image.open(TEST_AVIF_FILE): + pass def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) - file_path = "Tests/images/avif/hopper.avif" with pytest.raises(SyntaxError): - AvifImagePlugin.AvifImageFile(file_path) + AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) @skip_unless_feature("avif") @@ -154,12 +139,11 @@ def test_read(self) -> None: Does it have the bits we expect? """ - with Image.open("Tests/images/avif/hopper.avif") as image: + with Image.open(TEST_AVIF_FILE) as image: assert image.mode == "RGB" assert image.size == (128, 128) assert image.format == "AVIF" assert image.get_format_mimetype() == "image/avif" - image.load() image.getdata() # generated with: @@ -176,7 +160,6 @@ def _roundtrip(self, tmp_path: Path, mode: str, epsilon: float) -> None: assert image.mode == "RGB" assert image.size == (128, 128) assert image.format == "AVIF" - image.load() image.getdata() if mode == "RGB": @@ -240,10 +223,10 @@ def finish(self) -> None: im.save(test_file) 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 im: temp_file = str(tmp_path / "temp.avif") with warnings.catch_warnings(): - image.save(temp_file) + im.save(temp_file) @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) def test_accept_ftyp_brands(self, major_brand: bytes) -> None: @@ -272,9 +255,7 @@ def test_background_from_gif(self, tmp_path: Path) -> None: 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)] - ) + difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) assert difference < 5 def test_save_single_frame(self, tmp_path: Path) -> None: @@ -296,7 +277,7 @@ def test_load_transparent_rgb(self) -> None: assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 + assert im.getchannel("A").getcolors()[0] == (876, 0) def test_save_transparent(self, tmp_path: Path) -> None: im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) @@ -305,7 +286,7 @@ def test_save_transparent(self, tmp_path: Path) -> None: test_file = str(tmp_path / "temp.avif") im.save(test_file) - # check if saved image contains same transparency + # check if saved image contains the same transparency with Image.open(test_file) as im: assert_image(im, "RGBA", (10, 10)) assert im.getcolors() == [(100, (0, 0, 0, 0))] @@ -346,7 +327,7 @@ def test_exif(self) -> None: exif = im.getexif() assert exif[274] == 1 - def test_exif_save(self, tmp_path: Path) -> None: + def test_exif_save_default(self, tmp_path: Path) -> None: with Image.open("Tests/images/avif/exif.avif") as im: test_file = str(tmp_path / "temp.avif") im.save(test_file) @@ -355,24 +336,14 @@ def test_exif_save(self, tmp_path: Path) -> None: exif = reloaded.getexif() assert exif[274] == 1 - def test_exif_obj_argument(self, tmp_path: Path) -> None: - exif = Image.Exif() - exif[274] = 1 - exif_data = exif.tobytes() - with Image.open(TEST_AVIF_FILE) as im: - test_file = str(tmp_path / "temp.avif") - im.save(test_file, exif=exif) - - with Image.open(test_file) as reloaded: - assert reloaded.info["exif"] == exif_data - - def test_exif_bytes_argument(self, tmp_path: Path) -> None: + @pytest.mark.parametrize("bytes", [True, False]) + def test_exif_save_argument(self, tmp_path: Path, bytes: bool) -> None: exif = Image.Exif() exif[274] = 1 exif_data = exif.tobytes() with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") - im.save(test_file, exif=exif_data) + im.save(test_file, exif=exif_data if bytes else exif) with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == exif_data @@ -385,7 +356,7 @@ def test_exif_invalid(self, tmp_path: Path) -> None: def test_xmp(self) -> None: with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: - xmp = im.info.get("xmp") + xmp = im.info["xmp"] assert_xmp_orientation(xmp, 3) def test_xmp_save(self, tmp_path: Path) -> None: @@ -394,7 +365,7 @@ def test_xmp_save(self, tmp_path: Path) -> None: im.save(test_file) with Image.open(test_file) as reloaded: - xmp = reloaded.info.get("xmp") + xmp = reloaded.info["xmp"] assert_xmp_orientation(xmp, 3) def test_xmp_save_from_png(self, tmp_path: Path) -> None: @@ -403,7 +374,7 @@ def test_xmp_save_from_png(self, tmp_path: Path) -> None: im.save(test_file) with Image.open(test_file) as reloaded: - xmp = reloaded.info.get("xmp") + xmp = reloaded.info["xmp"] assert_xmp_orientation(xmp, 3) def test_xmp_save_argument(self, tmp_path: Path) -> None: @@ -420,12 +391,12 @@ def test_xmp_save_argument(self, tmp_path: Path) -> None: '', ] ) - with Image.open("Tests/images/avif/hopper.avif") as im: + with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") im.save(test_file, xmp=xmp_arg) with Image.open(test_file) as reloaded: - xmp = reloaded.info.get("xmp") + xmp = reloaded.info["xmp"] assert_xmp_orientation(xmp, 1) def test_tell(self) -> None: @@ -514,33 +485,29 @@ def test_encoder_advanced_codec_options_invalid( @skip_unless_avif_decoder("aom") @skip_unless_feature("avif") - def test_decoder_codec_param(self) -> None: - 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" + def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom") + + with Image.open(TEST_AVIF_FILE) as im: + assert im.size == (128, 128) @skip_unless_avif_encoder("rav1e") @skip_unless_feature("avif") - def test_decoder_codec_cannot_decode(self, tmp_path: Path) -> None: - 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_cannot_decode( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") - def test_decoder_codec_invalid(self) -> None: - AvifImagePlugin.DECODE_CODEC_CHOICE = "foo" - try: - with pytest.raises(ValueError): - with Image.open(TEST_AVIF_FILE): - pass - finally: - AvifImagePlugin.DECODE_CODEC_CHOICE = "auto" + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + + def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass @skip_unless_avif_encoder("aom") @skip_unless_feature("avif") @@ -560,7 +527,7 @@ def test_encoder_codec_available_invalid(self) -> None: assert _avif.encoder_codec_available("foo") is False def test_encoder_quality_valueerror(self, tmp_path: Path) -> None: - with Image.open("Tests/images/avif/hopper.avif") as im: + with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): im.save(test_file, quality="invalid") @@ -583,22 +550,20 @@ def test_decoder_codec_available_invalid(self) -> None: assert _avif.decoder_codec_available("foo") is False @pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"]) - def test_decoder_upsampling(self, upsampling: str) -> None: - AvifImagePlugin.CHROMA_UPSAMPLING = upsampling - try: + def test_decoder_upsampling( + self, monkeypatch: pytest.MonkeyPatch, upsampling: str + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "CHROMA_UPSAMPLING", upsampling) + + with Image.open(TEST_AVIF_FILE): + pass + + def test_decoder_upsampling_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "CHROMA_UPSAMPLING", "foo") + + with pytest.raises(ValueError): with Image.open(TEST_AVIF_FILE): pass - finally: - AvifImagePlugin.CHROMA_UPSAMPLING = "auto" - - def test_decoder_upsampling_invalid(self) -> None: - 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) -> None: im = Image.new("P", size=(64, 64)) @@ -612,7 +577,8 @@ def test_p_mode_transparency(self) -> None: buf_out = BytesIO() im_png.save(buf_out, format="AVIF", quality=100) - assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1) + with Image.open(buf_out) as expected: + assert_image_similar(im_png.convert("RGBA"), expected, 1) def test_decoder_strict_flags(self) -> None: # This would fail if full avif strictFlags were enabled @@ -648,7 +614,7 @@ def test_n_frames(self) -> None: correctly. """ - with Image.open("Tests/images/avif/hopper.avif") as im: + with Image.open(TEST_AVIF_FILE) as im: assert im.n_frames == 1 assert not im.is_animated @@ -671,13 +637,9 @@ def test_write_animation_L(self, tmp_path: Path) -> None: 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: Path) -> None: @@ -691,12 +653,10 @@ def check(temp_file: str) -> None: 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: @@ -725,7 +685,7 @@ def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: 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) + frame1.save(temp_file, save_all=True, append_images=[frame2]) def test_heif_raises_unidentified_image_error(self) -> None: with pytest.raises(UnidentifiedImageError): diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 56a19f96362..bf0b9085e9e 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -72,7 +72,7 @@ def _open(self) -> None: self._size = width, height self.n_frames = n_frames self.is_animated = self.n_frames > 1 - self._mode = self.rawmode = mode + self._mode = mode if icc: self.info["icc_profile"] = icc @@ -103,7 +103,7 @@ def load(self) -> Image.core.PixelAccess | None: if self.fp and self._exclusive_fp: self.fp.close() self.fp = BytesIO(data) - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] return super().load() diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 192dad38587..d7dd3c13310 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import shutil import struct import subprocess +import sys from typing import Any @@ -121,7 +122,7 @@ def cmd_msbuild( "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", - "MESON": "1.5.1", + "MESON": "1.5.2", "LIBAVIF": "1.1.1", "RAV1E": "0.7.1", } @@ -838,7 +839,7 @@ def main() -> None: "@echo " + ("=" * 70), f"@echo ==== {'Building meson':<60} ====", "@echo " + ("=" * 70), - f"python -mpip install meson=={V['MESON']}", + f"{sys.executable} -m pip install meson=={V['MESON']}", ], prefs, args.verbose,