diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 809e17cfe89..4d689e56fc7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -146,7 +146,7 @@ jobs: run: "& winbuild\\build\\build_dep_libpng.cmd" - name: Build dependencies / libavif - if: steps.build-cache.outputs.cache-hit != 'true' + if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64' run: "& winbuild\\build\\build_dep_libavif.cmd" # for FreeType WOFF2 font support diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 39b8a1535d5..e238842e791 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -51,7 +51,6 @@ 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_pkg_config { if [ -e pkg-config-stamp ]; then return; fi @@ -101,50 +100,22 @@ function build_harfbuzz { function build_libavif { if [ -e libavif-stamp ]; then return; fi - if [[ "$PLAT" == "aarch64" ]]; then - # Once GitHub Actions supports aarch64 without emulation, this will no longer needed as building will be faster - if [[ "$PLAT" == "aarch64" ]]; then - suffix="aarch64" - else - suffix="generic" - fi - - curl -sLo - \ - https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/librav1e-$RAV1E_VERSION-linux-$suffix.tar.gz \ - | tar -C $BUILD_PREFIX -zxf - - - # Force libavif to treat system rav1e as if it were local - mkdir -p /tmp/cmake/Modules - cat < /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 - - rav1e=SYSTEM - else - curl https://sh.rustup.rs -sSf | sh -s -- -y - . "$HOME/.cargo/env" - - if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then - yum install -y perl - if [[ "$MB_ML_VER" == 2014 ]]; then - yum install -y perl-IPC-Cmd - fi - fi - - rav1e=LOCAL - fi - python3 -m pip install meson ninja - if [[ "$PLAT" == "x86_64" ]]; then + if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 fi + # For rav1e + curl https://sh.rustup.rs -sSf | sh -s -- -y + . "$HOME/.cargo/env" + if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then + yum install -y perl + if [[ "$MB_ML_VER" == 2014 ]]; then + yum install -y perl-IPC-Cmd + fi + fi + local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) (cd $out_dir \ && cmake \ @@ -154,7 +125,7 @@ EOF -DBUILD_SHARED_LIBS=OFF \ -DAVIF_LIBSHARPYUV=LOCAL \ -DAVIF_LIBYUV=LOCAL \ - -DAVIF_CODEC_RAV1E=$rav1e \ + -DAVIF_CODEC_RAV1E=LOCAL \ -DAVIF_CODEC_AOM=LOCAL \ -DAVIF_CODEC_DAV1D=LOCAL \ -DAVIF_CODEC_SVT=LOCAL \ diff --git a/Tests/images/avif/star180.png b/Tests/images/avif/star180.png deleted file mode 100644 index 2c5f5222115..00000000000 Binary files a/Tests/images/avif/star180.png and /dev/null differ diff --git a/Tests/images/avif/star270.png b/Tests/images/avif/star270.png deleted file mode 100644 index 8812b9bdeb8..00000000000 Binary files a/Tests/images/avif/star270.png and /dev/null differ diff --git a/Tests/images/avif/star90.png b/Tests/images/avif/star90.png deleted file mode 100644 index 93526260bab..00000000000 Binary files a/Tests/images/avif/star90.png and /dev/null differ diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 3f3e2ad0b1f..85028b7ecb6 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -218,7 +218,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(3)]) - assert difference < 5 + assert difference <= 3 def test_save_single_frame(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.avif") @@ -255,10 +255,10 @@ def test_save_transparent(self, tmp_path: Path) -> None: def test_save_icc_profile(self) -> None: with Image.open("Tests/images/avif/icc_profile_none.avif") as im: - assert im.info.get("icc_profile") is None + assert "icc_profile" not in im.info with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: - expected_icc = with_icc.info.get("icc_profile") + expected_icc = with_icc.info["icc_profile"] assert expected_icc is not None im = roundtrip(im, icc_profile=expected_icc) @@ -278,7 +278,7 @@ def test_roundtrip_icc_profile(self) -> None: def test_roundtrip_no_icc_profile(self) -> None: with Image.open("Tests/images/avif/icc_profile_none.avif") as im: - assert im.info.get("icc_profile") is None + assert "icc_profile" not in im.info im = roundtrip(im) assert "icc_profile" not in im.info @@ -470,14 +470,14 @@ def test_encoder_advanced_codec_options( @skip_unless_avif_encoder("aom") @skip_unless_feature("avif") - @pytest.mark.parametrize("val", [{"foo": "bar"}, 1234]) + @pytest.mark.parametrize("advanced", [{"foo": "bar"}, 1234]) def test_encoder_advanced_codec_options_invalid( - self, tmp_path: Path, val: dict[str, str] | int + self, tmp_path: Path, advanced: dict[str, str] | int ) -> None: 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) + im.save(test_file, codec="aom", advanced=advanced) @skip_unless_avif_decoder("aom") @skip_unless_feature("avif") @@ -545,20 +545,20 @@ def test_decoder_codec_available_cannot_decode(self) -> None: def test_decoder_codec_available_invalid(self) -> None: assert _avif.decoder_codec_available("foo") is False - def test_p_mode_transparency(self) -> None: + def test_p_mode_transparency(self, tmp_path: Path) -> None: im = Image.new("P", size=(64, 64)) draw = ImageDraw.Draw(im) draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) - 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) + out_png = str(tmp_path / "temp.png") + im.save(out_png, transparency=0) + with Image.open(out_png) as im_png: + out_avif = str(tmp_path / "temp.avif") + im_png.save(out_avif, quality=100) - with Image.open(buf_out) as expected: - assert_image_similar(im_png.convert("RGBA"), expected, 0.17) + with Image.open(out_avif) as expected: + assert_image_similar(im_png.convert("RGBA"), expected, 0.17) def test_decoder_strict_flags(self) -> None: # This would fail if full avif strictFlags were enabled @@ -566,27 +566,22 @@ def test_decoder_strict_flags(self) -> None: assert im.size == (480, 270) @skip_unless_avif_encoder("aom") - def test_aom_optimizations(self) -> None: - im = hopper("RGB") - buf = BytesIO() - im.save(buf, format="AVIF", codec="aom", speed=1) + def test_aom_optimizations(self, tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.avif") + hopper().save(test_file, codec="aom", speed=1) @skip_unless_avif_encoder("svt") - def test_svt_optimizations(self) -> None: - im = hopper("RGB") - buf = BytesIO() - im.save(buf, format="AVIF", codec="svt", speed=1) + def test_svt_optimizations(self, tmp_path: Path) -> None: + test_file = str(tmp_path / "temp.avif") + hopper().save(test_file, codec="svt", speed=1) @skip_unless_feature("avif") class TestAvifAnimation: @contextmanager def star_frames(self) -> Generator[list[ImageFile.ImageFile], None, None]: - 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] + with Image.open("Tests/images/avif/star.png") as f: + yield [f, f.rotate(90), f.rotate(180), f.rotate(270)] def test_n_frames(self) -> None: """ @@ -602,10 +597,10 @@ def test_n_frames(self) -> None: assert im.n_frames == 5 assert im.is_animated - def test_write_animation_L(self, tmp_path: Path) -> None: + def test_write_animation_P(self, tmp_path: Path) -> None: """ 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 second-to-last frames to ensure they're visually similar. """ with Image.open("Tests/images/avif/star.gif") as orig: @@ -616,15 +611,17 @@ def test_write_animation_L(self, tmp_path: Path) -> None: 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 - assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 2.25) + # Compare first frame in P mode to frame from original GIF + assert_image_similar(im, orig.convert("RGBA"), 2) + + # Compare second-to-last frame in RGBA mode to frame from original GIF orig.seek(orig.n_frames - 2) im.seek(im.n_frames - 2) - assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 2.54) + assert_image_similar(im, orig, 2.54) - def test_write_animation_RGB(self, tmp_path: Path) -> None: + def test_write_animation_RGBA(self, tmp_path: Path) -> None: """ - Write an animated AVIF from RGB frames, and ensure the frames + Write an animated AVIF from RGBA frames, and ensure the frames are visually similar to the originals. """ @@ -633,11 +630,11 @@ def check(temp_file: str) -> None: assert im.n_frames == 4 # Compare first frame to original - assert_image_similar(im, frame1.convert("RGBA"), 2.7) + assert_image_similar(im, frame1, 2.7) # Compare second frame to original im.seek(1) - assert_image_similar(im, frame2.convert("RGBA"), 4.1) + assert_image_similar(im, frame2, 4.1) with self.star_frames() as frames: frame1 = frames[0] @@ -646,7 +643,7 @@ def check(temp_file: str) -> None: frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) check(temp_file1) - # Tests appending using a generator + # Test appending using a generator def imGenerator( ims: list[ImageFile.ImageFile], ) -> Generator[ImageFile.ImageFile, None, None]: