From 44b1aedb22a31b6ce3e9fd7d78c3e785a27178cd Mon Sep 17 00:00:00 2001 From: lazerg Date: Tue, 20 Oct 2026 19:25:31 +0500 Subject: [PATCH 1/3] Convert grayscale images to RGB when saving AVIF with libavif < 1.3.0 --- Tests/test_file_avif.py | 9 +++++++++ src/PIL/AvifImagePlugin.py | 8 ++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 1b3eb7d8b0b..c10125864a3 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -141,6 +141,13 @@ def test_write_l(self) -> None: assert reloaded.mode == "L" assert_image_similar(reloaded, im, 1.69) + @pytest.mark.parametrize("mode", ("1", "L", "I", "I;16", "F")) + def test_write_grayscale(self, mode: str) -> None: + im = hopper(mode) + reloaded = roundtrip(im) + + assert_image_similar(reloaded.convert("L"), im.convert("L"), 1.69) + def test_write_rgb(self, tmp_path: Path) -> None: """ Can we write a RGB mode file to avif without error? @@ -193,6 +200,8 @@ def test_encoder_finish_none_error( """Save should raise an OSError if AvifEncoder.finish returns None""" class _mock_avif: + libavif_version = "1.3.0" + class AvifEncoder: def __init__(self, *args: Any) -> None: pass diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index dd87a508313..86578e215ac 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -239,6 +239,10 @@ def _save( advanced, ) + supported_modes = {"RGB", "RGBA"} + if tuple(int(x) for x in _avif.libavif_version.split(".")) >= (1, 3): + supported_modes.add("L") + # Add each frame frame_idx = 0 frame_duration = 0 @@ -249,10 +253,10 @@ def _save( for frame in ImageSequence.Iterator(ims): # Make sure image mode is supported rawmode = frame.mode - if ims.mode not in {"L", "RGB", "RGBA"}: + if ims.mode not in supported_modes: if ims.has_transparency_data: rawmode = "RGBA" - elif ims.mode in grayscale_modes: + elif ims.mode in grayscale_modes and "L" in supported_modes: rawmode = "L" else: rawmode = "RGB" From 44535289228ce1d429704527993ff0d234dd01fc Mon Sep 17 00:00:00 2001 From: lazerg Date: Tue, 20 Oct 2026 19:27:46 +0500 Subject: [PATCH 2/3] Use list for parametrize --- Tests/test_file_avif.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index c10125864a3..babc9297a7e 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -141,7 +141,7 @@ def test_write_l(self) -> None: assert reloaded.mode == "L" assert_image_similar(reloaded, im, 1.69) - @pytest.mark.parametrize("mode", ("1", "L", "I", "I;16", "F")) + @pytest.mark.parametrize("mode", ["1", "L", "I", "I;16", "F"]) def test_write_grayscale(self, mode: str) -> None: im = hopper(mode) reloaded = roundtrip(im) From f19705881a0f874afd7cb1945f8bf63039cb361c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Oct 2026 20:31:40 +1100 Subject: [PATCH 3/3] Merge previous test into new test --- Tests/test_file_avif.py | 15 +++++---------- src/PIL/AvifImagePlugin.py | 3 ++- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index babc9297a7e..191591bac5a 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -25,6 +25,7 @@ assert_image, assert_image_similar, assert_image_similar_tofile, + has_feature_version, hopper, skip_if_tsan, skip_unless_feature, @@ -133,20 +134,14 @@ def test_read(self) -> None: image, "Tests/images/avif/hopper_avif_write.png", 11.5 ) - @skip_unless_feature_version("avif", "1.3.0") - def test_write_l(self) -> None: - im = hopper("L") - reloaded = roundtrip(im) - - assert reloaded.mode == "L" - assert_image_similar(reloaded, im, 1.69) - @pytest.mark.parametrize("mode", ["1", "L", "I", "I;16", "F"]) def test_write_grayscale(self, mode: str) -> None: im = hopper(mode) - reloaded = roundtrip(im) + reloaded: Image.Image = roundtrip(im) - assert_image_similar(reloaded.convert("L"), im.convert("L"), 1.69) + if not has_feature_version("avif", "1.3.0"): + reloaded = reloaded.convert("L") + assert_image_similar(reloaded, im.convert("L"), 1.69) def test_write_rgb(self, tmp_path: Path) -> None: """ diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 86578e215ac..4222ac6add2 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -240,7 +240,8 @@ def _save( ) supported_modes = {"RGB", "RGBA"} - if tuple(int(x) for x in _avif.libavif_version.split(".")) >= (1, 3): + if tuple(int(v) for v in _avif.libavif_version.split(".")) >= (1, 3): + # AVIF_RGB_FORMAT_GRAY was only added in libavif 1.3.0 supported_modes.add("L") # Add each frame