Skip building libavif on 32-bit Windows (#16)

* Corrected comment

* Reduced difference

* Generate rotated images

* Build rav1e

* Skip building libavif on 32-bit

* Fixed building libavif on oss-fuzz

* Removed unnecessary converts

---------

Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
This commit is contained in:
Andrew Murray 2025-01-21 15:39:42 +11:00 committed by GitHub
parent ce6bf21f15
commit 4b29af49fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 48 additions and 80 deletions

View File

@ -146,7 +146,7 @@ jobs:
run: "& winbuild\\build\\build_dep_libpng.cmd" run: "& winbuild\\build\\build_dep_libpng.cmd"
- name: Build dependencies / libavif - 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" run: "& winbuild\\build\\build_dep_libavif.cmd"
# for FreeType WOFF2 font support # for FreeType WOFF2 font support

View File

@ -51,7 +51,6 @@ BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0 LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0 BROTLI_VERSION=1.1.0
LIBAVIF_VERSION=1.1.1 LIBAVIF_VERSION=1.1.1
RAV1E_VERSION=0.7.1
function build_pkg_config { function build_pkg_config {
if [ -e pkg-config-stamp ]; then return; fi if [ -e pkg-config-stamp ]; then return; fi
@ -101,50 +100,22 @@ function build_harfbuzz {
function build_libavif { function build_libavif {
if [ -e libavif-stamp ]; then return; fi 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 <<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
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 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 build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
fi 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) 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 \ (cd $out_dir \
&& cmake \ && cmake \
@ -154,7 +125,7 @@ EOF
-DBUILD_SHARED_LIBS=OFF \ -DBUILD_SHARED_LIBS=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \ -DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \ -DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_RAV1E=$rav1e \ -DAVIF_CODEC_RAV1E=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \ -DAVIF_CODEC_AOM=LOCAL \
-DAVIF_CODEC_DAV1D=LOCAL \ -DAVIF_CODEC_DAV1D=LOCAL \
-DAVIF_CODEC_SVT=LOCAL \ -DAVIF_CODEC_SVT=LOCAL \

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -218,7 +218,7 @@ class TestFileAvif:
with Image.open(out_gif) as reread: with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1)) reread_value = reread.convert("RGB").getpixel((1, 1))
difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) 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: def test_save_single_frame(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.avif") temp_file = str(tmp_path / "temp.avif")
@ -255,10 +255,10 @@ class TestFileAvif:
def test_save_icc_profile(self) -> None: 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 "icc_profile" not in im.info
with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: 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 assert expected_icc is not None
im = roundtrip(im, icc_profile=expected_icc) im = roundtrip(im, icc_profile=expected_icc)
@ -278,7 +278,7 @@ class TestFileAvif:
def test_roundtrip_no_icc_profile(self) -> None: 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 "icc_profile" not in im.info
im = roundtrip(im) im = roundtrip(im)
assert "icc_profile" not in im.info assert "icc_profile" not in im.info
@ -470,14 +470,14 @@ 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("advanced", [{"foo": "bar"}, 1234])
def test_encoder_advanced_codec_options_invalid( 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: ) -> 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, codec="aom", advanced=val) im.save(test_file, codec="aom", advanced=advanced)
@skip_unless_avif_decoder("aom") @skip_unless_avif_decoder("aom")
@skip_unless_feature("avif") @skip_unless_feature("avif")
@ -545,20 +545,20 @@ class TestFileAvif:
def test_decoder_codec_available_invalid(self) -> None: def test_decoder_codec_available_invalid(self) -> None:
assert _avif.decoder_codec_available("foo") is False 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)) 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)
draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) draw.rectangle(xy=[(32, 32), (64, 64)], fill=255)
buf_png = BytesIO() out_png = str(tmp_path / "temp.png")
im.save(buf_png, format="PNG", transparency=0) im.save(out_png, transparency=0)
im_png = Image.open(buf_png) with Image.open(out_png) as im_png:
buf_out = BytesIO() out_avif = str(tmp_path / "temp.avif")
im_png.save(buf_out, format="AVIF", quality=100) im_png.save(out_avif, quality=100)
with Image.open(buf_out) as expected: with Image.open(out_avif) as expected:
assert_image_similar(im_png.convert("RGBA"), expected, 0.17) assert_image_similar(im_png.convert("RGBA"), expected, 0.17)
def test_decoder_strict_flags(self) -> None: 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
@ -566,27 +566,22 @@ class TestFileAvif:
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) -> None: def test_aom_optimizations(self, tmp_path: Path) -> None:
im = hopper("RGB") test_file = str(tmp_path / "temp.avif")
buf = BytesIO() hopper().save(test_file, 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) -> None: def test_svt_optimizations(self, tmp_path: Path) -> None:
im = hopper("RGB") test_file = str(tmp_path / "temp.avif")
buf = BytesIO() hopper().save(test_file, codec="svt", speed=1)
im.save(buf, format="AVIF", codec="svt", speed=1)
@skip_unless_feature("avif") @skip_unless_feature("avif")
class TestAvifAnimation: class TestAvifAnimation:
@contextmanager @contextmanager
def star_frames(self) -> Generator[list[ImageFile.ImageFile], None, None]: 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 f:
with Image.open("Tests/images/avif/star90.png") as f2: yield [f, f.rotate(90), f.rotate(180), f.rotate(270)]
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) -> None: def test_n_frames(self) -> None:
""" """
@ -602,10 +597,10 @@ 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: Path) -> None: def test_write_animation_P(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 second-to-last frames to ensure they're visually similar.
""" """
with Image.open("Tests/images/avif/star.gif") as orig: with Image.open("Tests/images/avif/star.gif") as orig:
@ -616,15 +611,17 @@ class TestAvifAnimation:
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert im.n_frames == orig.n_frames assert im.n_frames == orig.n_frames
# Compare first and second-to-last frames to the original animated GIF # Compare first frame in P mode to frame from original GIF
assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 2.25) 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) orig.seek(orig.n_frames - 2)
im.seek(im.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. are visually similar to the originals.
""" """
@ -633,11 +630,11 @@ class TestAvifAnimation:
assert im.n_frames == 4 assert im.n_frames == 4
# Compare first frame to original # 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 # Compare second frame to original
im.seek(1) 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: with self.star_frames() as frames:
frame1 = frames[0] frame1 = frames[0]
@ -646,7 +643,7 @@ class TestAvifAnimation:
frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:])
check(temp_file1) check(temp_file1)
# Tests appending using a generator # Test appending using a generator
def imGenerator( def imGenerator(
ims: list[ImageFile.ImageFile], ims: list[ImageFile.ImageFile],
) -> Generator[ImageFile.ImageFile, None, None]: ) -> Generator[ImageFile.ImageFile, None, None]: