Skip to content

Add AVIF plugin (decoder + encoder using libavif) #5201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 64 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3878b58
Add AVIF plugin (using libavif)
fdintino Jan 3, 2021
e2add24
Added type hints (#2)
radarhere Oct 16, 2024
e5494a2
Fix PLAT envvar in cibuildwheel container
fdintino Oct 16, 2024
8b8bbba
Update Tests/check_avif_leaks.py
fdintino Oct 18, 2024
58ef692
Simplified code (#3)
radarhere Oct 19, 2024
d6a0a15
Merge branch 'main' into libavif-plugin
radarhere Nov 8, 2024
50b993a
Set default max threads in Python (#4)
radarhere Nov 11, 2024
671e3c8
Removed unused upsampling setting (#5)
radarhere Nov 12, 2024
658cdf3
Merge branch 'main' into libavif-plugin
radarhere Nov 18, 2024
7225cb9
Merge branch 'main' into libavif-plugin
radarhere Nov 24, 2024
3730bf2
Merge branch 'main' into libavif-plugin
radarhere Nov 30, 2024
c40bcbf
Improved error handling
radarhere Dec 2, 2024
d76ae2f
Do not ignore SyntaxError when saving EXIF data (#8)
radarhere Dec 7, 2024
9ad8311
Allow libavif to install rav1e, except on manylinux2014 and aarch64 (#7)
radarhere Dec 7, 2024
de4c6c1
Removed ld64 flag (#6)
radarhere Dec 7, 2024
524d802
fix: set exif orientation from irot/imir when decoding AVIF
fdintino Dec 9, 2024
7b73d77
Merge branch 'main' into libavif-plugin
radarhere Dec 13, 2024
a56acd8
Removed unittest mock (#10)
radarhere Dec 13, 2024
f5dc957
Use cmds_cmake (#9)
radarhere Dec 13, 2024
8d77678
chore(docs): update quality and speed with correct defaults
fdintino Dec 13, 2024
4eaa6b7
Merge branch 'main' into libavif-plugin
radarhere Dec 14, 2024
bdb24f9
Removed `_avif.HAVE_AVIF` and `_avif.VERSION` (#11)
radarhere Dec 15, 2024
ddc8e7e
Use "rav1e" if available as default ("auto") avif encoder
fdintino Dec 15, 2024
b585f9e
Simplified EXIF code (#12)
radarhere Dec 15, 2024
da2e18d
Revert "Use "rav1e" if available as default ("auto") avif encoder"
fdintino Dec 17, 2024
9b6e575
Merge branch 'main' into libavif-plugin
radarhere Dec 18, 2024
3a9a3ab
Reduced epsilons (#13)
radarhere Dec 24, 2024
9328932
Removed avifEncOptions (#14)
radarhere Jan 3, 2025
be02830
Merge branch 'main' into libavif-plugin
radarhere Jan 3, 2025
4135664
Merge branch 'main' into libavif-plugin
radarhere Jan 4, 2025
29c158d
Merge branch 'main' into libavif-plugin
radarhere Jan 8, 2025
4c63ea6
Fixed series of tuples as advanced argument (#15)
radarhere Jan 15, 2025
ce6bf21
Merge branch 'main' into libavif-plugin
radarhere Jan 17, 2025
4b29af4
Skip building libavif on 32-bit Windows (#16)
radarhere Jan 21, 2025
38f0d10
Merge branch 'main' into libavif-plugin
radarhere Jan 25, 2025
1410d23
Removed qmin and qmax (#17)
radarhere Jan 25, 2025
6cbad27
Merge branch 'main' into libavif-plugin
radarhere Feb 1, 2025
19ba2dd
Use rgb.rowBytes in overflow check (#18)
radarhere Feb 3, 2025
4508f37
Use aom LICENSE instead of PATENTS (#19)
radarhere Feb 3, 2025
7de1212
Merge branch 'main' into libavif-plugin
radarhere Feb 7, 2025
e1509ee
Removed memset and ignoreAlpha (#20)
radarhere Feb 9, 2025
0590f08
Handle avifDecoderCreate and avifEncoderCreate errors (#21)
radarhere Feb 12, 2025
5761b44
Merge branch 'main' into libavif-plugin
radarhere Feb 14, 2025
38b9941
Sort formats alphabetically
radarhere Feb 15, 2025
10dfa63
Simplified code
radarhere Feb 15, 2025
9abfdbc
Merge branch 'main' into libavif-plugin
radarhere Mar 3, 2025
d80ac3c
Use default PyTypeObject values
radarhere Feb 8, 2025
b4eec64
Merge branch 'main' into libavif-plugin
radarhere Mar 17, 2025
d552087
Updated libavif to 1.2.1 (#26)
radarhere Mar 17, 2025
79f7339
Updated wording
radarhere Mar 19, 2025
46f4508
Added version comments
radarhere Mar 19, 2025
9bebf37
Sort alphabetically
radarhere Mar 19, 2025
5da2113
Simplified code
radarhere Mar 19, 2025
0732554
Merge branch 'main' into libavif-plugin
radarhere Mar 21, 2025
9ea5e3d
Remove support for libavif < 1.0.0
radarhere Mar 21, 2025
fca6df2
Added get_codec_version() (#29)
radarhere Mar 21, 2025
024a894
Merge branch 'radarhere-avif_1' into libavif-plugin
fdintino Mar 21, 2025
2ba9356
Update rust (#33)
radarhere Mar 25, 2025
fdc68e6
Merge branch 'main' into libavif-plugin
radarhere Mar 31, 2025
9e63868
Continue to build libyuv on macOS
radarhere Mar 31, 2025
eff2680
Added release notes
radarhere Mar 31, 2025
fb096e1
Derive some avif test images from existing Pillow test images
fdintino Mar 31, 2025
1276543
Updated size
radarhere Mar 31, 2025
c8d0408
Continue to build libyuv on Linux
radarhere Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Derive some avif test images from existing Pillow test images
Replace chimera-missing-pixi.avif and rgba10.heif with images derived
from the pillow "hopper" test image.
  • Loading branch information
fdintino committed Mar 31, 2025
commit fb096e15cc374c2d59b2e0d0e2f6fb64eaf0c3ec
Binary file removed Tests/images/avif/chimera-missing-pixi.avif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The star.avifs test file is licensed as CC-BY

Sorry if this has already been covered, can these images be distributed under Pillow's MIT-CMU licence?

  • chimera-missing-pixi.avif
  • rgba10.heif
  • rot*mir*.avif

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chimera-missing-pixi.avif is from the netflix opencontent AV1 test images. It can be downloaded here: http://download.opencontent.netflix.com.s3.amazonaws.com/AV1/Chimera/AVIF/Chimera-AV1-8bit-1280x720-3363kbps-100.avif and is licensed under Creative Commons-Attribution-NonCommercial-NoDerivatives (see here). Is there a README or licenses file somewhere where I could put the attribution? If not, I could try to source or create a test image with a non-attribution license.

I'm not sure about rgba10.heif. But any heif whatsoever will do, so it should be fine to replace it with a public domain / liberally licensed heif image file.

rot*mir*.avif files were released under CC0 1.0 (no copyright). See here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replaced rgba10.heif and chimera-missing-pixi.avif with images based off of 'hopper.png'.

Binary file not shown.
Binary file added Tests/images/avif/hopper-missing-pixi.avif
Binary file not shown.
Binary file added Tests/images/avif/hopper.heif
Binary file not shown.
Binary file removed Tests/images/avif/rgba10.heif
Binary file not shown.
4 changes: 2 additions & 2 deletions Tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
try:
from PIL import _avif

HAVE_AVIF = True

Check warning on line 36 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L36

Added line #L36 was not covered by tests
except ImportError:
HAVE_AVIF = False

Expand All @@ -42,13 +42,13 @@


def assert_xmp_orientation(xmp: bytes, expected: int) -> None:
assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected

Check warning on line 45 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L45

Added line #L45 was not covered by tests


def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile:
out = BytesIO()
im.save(out, "AVIF", **options)
return Image.open(out)

Check warning on line 51 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L49-L51

Added lines #L49 - L51 were not covered by tests


def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator:
Expand Down Expand Up @@ -92,22 +92,22 @@
@skip_unless_feature("avif")
class TestFileAvif:
def test_version(self) -> None:
version = features.version_module("avif")
assert version is not None
assert re.search(r"^\d+\.\d+\.\d+$", version)

Check warning on line 97 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L95-L97

Added lines #L95 - L97 were not covered by tests

def test_codec_version(self) -> None:
assert AvifImagePlugin.get_codec_version("unknown") is None

Check warning on line 100 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L100

Added line #L100 was not covered by tests

for codec_name in ("aom", "dav1d", "rav1e", "svt"):
codec_version = AvifImagePlugin.get_codec_version(codec_name)
if _avif.decoder_codec_available(

Check warning on line 104 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L102-L104

Added lines #L102 - L104 were not covered by tests
codec_name
) or _avif.encoder_codec_available(codec_name):
assert codec_version is not None
assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version)

Check warning on line 108 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L107-L108

Added lines #L107 - L108 were not covered by tests
else:
assert codec_version is None

Check warning on line 110 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L110

Added line #L110 was not covered by tests

def test_read(self) -> None:
"""
Expand All @@ -115,16 +115,16 @@
Does it have the bits we expect?
"""

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.getdata()

Check warning on line 123 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L118-L123

Added lines #L118 - L123 were not covered by tests

# generated with:
# avifdec hopper.avif hopper_avif_write.png
assert_image_similar_tofile(

Check warning on line 127 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L127

Added line #L127 was not covered by tests
image, "Tests/images/avif/hopper_avif_write.png", 11.5
)

Expand All @@ -134,18 +134,18 @@
Does it have the bits we expect?
"""

temp_file = tmp_path / "temp.avif"

Check warning on line 137 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L137

Added line #L137 was not covered by tests

im = hopper()
im.save(temp_file)
with Image.open(temp_file) as reloaded:
assert reloaded.mode == "RGB"
assert reloaded.size == (128, 128)
assert reloaded.format == "AVIF"
reloaded.getdata()

Check warning on line 145 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L139-L145

Added lines #L139 - L145 were not covered by tests

# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(

Check warning on line 148 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L148

Added line #L148 was not covered by tests
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02
)

Expand All @@ -153,160 +153,160 @@
# difference between the two images is less than the epsilon value,
# then we're going to accept that it's a reasonable lossy version of
# the image.
assert_image_similar(reloaded, im, 8.62)

Check warning on line 156 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L156

Added line #L156 was not covered by tests

def test_AvifEncoder_with_invalid_args(self) -> None:
"""
Calling encoder functions with no arguments should result in an error.
"""
with pytest.raises(TypeError):
_avif.AvifEncoder()

Check warning on line 163 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L162-L163

Added lines #L162 - L163 were not covered by tests

def test_AvifDecoder_with_invalid_args(self) -> None:
"""
Calling decoder functions with no arguments should result in an error.
"""
with pytest.raises(TypeError):
_avif.AvifDecoder()

Check warning on line 170 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L169-L170

Added lines #L169 - L170 were not covered by tests

def test_invalid_dimensions(self, tmp_path: Path) -> None:
test_file = tmp_path / "temp.avif"
im = Image.new("RGB", (0, 0))
with pytest.raises(ValueError):
im.save(test_file)

Check warning on line 176 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L173-L176

Added lines #L173 - L176 were not covered by tests

def test_encoder_finish_none_error(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Save should raise an OSError if AvifEncoder.finish returns None"""

class _mock_avif:
class AvifEncoder:
def __init__(self, *args: Any) -> None:
pass

Check warning on line 186 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L183-L186

Added lines #L183 - L186 were not covered by tests

def add(self, *args: Any) -> None:
pass

Check warning on line 189 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L188-L189

Added lines #L188 - L189 were not covered by tests

def finish(self) -> None:
return None

Check warning on line 192 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L191-L192

Added lines #L191 - L192 were not covered by tests

monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif)

Check warning on line 194 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L194

Added line #L194 was not covered by tests

im = Image.new("RGB", (150, 150))
test_file = tmp_path / "temp.avif"
with pytest.raises(OSError):
im.save(test_file)

Check warning on line 199 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L196-L199

Added lines #L196 - L199 were not covered by tests

def test_no_resource_warning(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
with warnings.catch_warnings():
warnings.simplefilter("error")

Check warning on line 204 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L202-L204

Added lines #L202 - L204 were not covered by tests

im.save(tmp_path / "temp.avif")

Check warning on line 206 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L206

Added line #L206 was not covered by tests

@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
def test_accept_ftyp_brands(self, major_brand: bytes) -> None:
data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand
assert AvifImagePlugin._accept(data) is True

Check warning on line 211 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L210-L211

Added lines #L210 - L211 were not covered by tests

def test_file_pointer_could_be_reused(self) -> None:
with open(TEST_AVIF_FILE, "rb") as blob:
with Image.open(blob) as im:
im.load()
with Image.open(blob) as im:
im.load()

Check warning on line 218 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L214-L218

Added lines #L214 - L218 were not covered by tests

def test_background_from_gif(self, tmp_path: Path) -> None:
with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1))

Check warning on line 222 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L221-L222

Added lines #L221 - L222 were not covered by tests

# Save as AVIF
out_avif = tmp_path / "temp.avif"
im.save(out_avif, save_all=True)

Check warning on line 226 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L225-L226

Added lines #L225 - L226 were not covered by tests

# Save as GIF
out_gif = tmp_path / "temp.gif"
with Image.open(out_avif) as im:
im.save(out_gif)

Check warning on line 231 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L229-L231

Added lines #L229 - L231 were not covered by tests

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 <= 3

Check warning on line 236 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L233-L236

Added lines #L233 - L236 were not covered by tests

def test_save_single_frame(self, tmp_path: Path) -> None:
temp_file = tmp_path / "temp.avif"
with Image.open("Tests/images/chi.gif") as im:
im.save(temp_file)
with Image.open(temp_file) as im:
assert im.n_frames == 1

Check warning on line 243 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L239-L243

Added lines #L239 - L243 were not covered by tests

def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg"

Check warning on line 246 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L246

Added line #L246 was not covered by tests

with pytest.raises(SyntaxError):
AvifImagePlugin.AvifImageFile(invalid_file)

Check warning on line 249 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L248-L249

Added lines #L248 - L249 were not covered by tests

def test_load_transparent_rgb(self) -> None:
test_file = "Tests/images/avif/transparency.avif"
with Image.open(test_file) as im:
assert_image(im, "RGBA", (64, 64))

Check warning on line 254 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L252-L254

Added lines #L252 - L254 were not covered by tests

# image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0] == (876, 0)

Check warning on line 257 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L257

Added line #L257 was not covered by tests

def test_save_transparent(self, tmp_path: Path) -> None:
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
assert im.getcolors() == [(100, (0, 0, 0, 0))]

Check warning on line 261 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L260-L261

Added lines #L260 - L261 were not covered by tests

test_file = tmp_path / "temp.avif"
im.save(test_file)

Check warning on line 264 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L263-L264

Added lines #L263 - L264 were not covered by tests

# 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))]

Check warning on line 269 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L267-L269

Added lines #L267 - L269 were not covered by tests

def test_save_icc_profile(self) -> None:
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
assert "icc_profile" not in im.info

Check warning on line 273 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L272-L273

Added lines #L272 - L273 were not covered by tests

with Image.open("Tests/images/avif/icc_profile.avif") as with_icc:
expected_icc = with_icc.info["icc_profile"]
assert expected_icc is not None

Check warning on line 277 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L275-L277

Added lines #L275 - L277 were not covered by tests

im = roundtrip(im, icc_profile=expected_icc)
assert im.info["icc_profile"] == expected_icc

Check warning on line 280 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L279-L280

Added lines #L279 - L280 were not covered by tests

def test_discard_icc_profile(self) -> None:
with Image.open("Tests/images/avif/icc_profile.avif") as im:
im = roundtrip(im, icc_profile=None)
assert "icc_profile" not in im.info

Check warning on line 285 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L283-L285

Added lines #L283 - L285 were not covered by tests

def test_roundtrip_icc_profile(self) -> None:
with Image.open("Tests/images/avif/icc_profile.avif") as im:
expected_icc = im.info["icc_profile"]

Check warning on line 289 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L288-L289

Added lines #L288 - L289 were not covered by tests

im = roundtrip(im)
assert im.info["icc_profile"] == expected_icc

Check warning on line 292 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L291-L292

Added lines #L291 - L292 were not covered by tests

def test_roundtrip_no_icc_profile(self) -> None:
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
assert "icc_profile" not in im.info

Check warning on line 296 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L295-L296

Added lines #L295 - L296 were not covered by tests

im = roundtrip(im)
assert "icc_profile" not in im.info

Check warning on line 299 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L298-L299

Added lines #L298 - L299 were not covered by tests

def test_exif(self) -> None:
# With an EXIF chunk
with Image.open("Tests/images/avif/exif.avif") as im:
exif = im.getexif()
assert exif[274] == 1

Check warning on line 305 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L303-L305

Added lines #L303 - L305 were not covered by tests

with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
exif = im.getexif()
assert exif[274] == 3

Check warning on line 309 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L307-L309

Added lines #L307 - L309 were not covered by tests

@pytest.mark.parametrize("use_bytes", [True, False])
@pytest.mark.parametrize("orientation", [1, 2])
Expand All @@ -316,35 +316,35 @@
use_bytes: bool,
orientation: int,
) -> None:
exif = Image.Exif()
exif[274] = orientation
exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, exif=exif_data if use_bytes else exif)

Check warning on line 324 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L319-L324

Added lines #L319 - L324 were not covered by tests

with Image.open(test_file) as reloaded:
if orientation == 1:
assert "exif" not in reloaded.info

Check warning on line 328 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L326-L328

Added lines #L326 - L328 were not covered by tests
else:
assert reloaded.info["exif"] == exif_data

Check warning on line 330 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L330

Added line #L330 was not covered by tests

def test_exif_without_orientation(self, tmp_path: Path) -> None:
exif = Image.Exif()
exif[272] = b"test"
exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, exif=exif)

Check warning on line 338 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L333-L338

Added lines #L333 - L338 were not covered by tests

with Image.open(test_file) as reloaded:
assert reloaded.info["exif"] == exif_data

Check warning on line 341 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L340-L341

Added lines #L340 - L341 were not covered by tests

def test_exif_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(SyntaxError):
im.save(test_file, exif=b"invalid")

Check warning on line 347 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L344-L347

Added lines #L344 - L347 were not covered by tests

@pytest.mark.parametrize(
"rot, mir, exif_orientation",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are rot and mir short for rotation and mirror? Let's the full names, they're more descriptive. It's okay to keep the rotXmirY.avif filenames.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/AOMediaCodec/libavif/blob/main/doc/avifenc.1.md

--irot ANGLE : Add irot property (rotation) in 0..3. Makes (90 * ANGLE) degree rotation anti-clockwise.

--imir AXIS : Add imir property (mirroring). 0=top-to-bottom, 1=left-to-right.

They're either short for rotation and mirroring, or for irot and imir.

Expand All @@ -362,22 +362,22 @@
def test_rot_mir_exif(
self, rot: int, mir: int, exif_orientation: int, tmp_path: Path
) -> None:
with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im:
exif = im.getexif()
assert exif[274] == exif_orientation

Check warning on line 367 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L365-L367

Added lines #L365 - L367 were not covered by tests

test_file = tmp_path / "temp.avif"
im.save(test_file, exif=exif)
with Image.open(test_file) as reloaded:
assert reloaded.getexif()[274] == exif_orientation

Check warning on line 372 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L369-L372

Added lines #L369 - L372 were not covered by tests

def test_xmp(self) -> None:
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
xmp = im.info["xmp"]
assert_xmp_orientation(xmp, 3)

Check warning on line 377 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L375-L377

Added lines #L375 - L377 were not covered by tests

def test_xmp_save(self, tmp_path: Path) -> None:
xmp_arg = "\n".join(

Check warning on line 380 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L380

Added line #L380 was not covered by tests
[
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>',
'<x:xmpmeta xmlns:x="adobe:ns:meta/">',
Expand All @@ -390,67 +390,67 @@
'<?xpacket end="r"?>',
]
)
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, xmp=xmp_arg)

Check warning on line 395 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L393-L395

Added lines #L393 - L395 were not covered by tests

with Image.open(test_file) as reloaded:
xmp = reloaded.info["xmp"]
assert_xmp_orientation(xmp, 1)

Check warning on line 399 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L397-L399

Added lines #L397 - L399 were not covered by tests

def test_tell(self) -> None:
with Image.open(TEST_AVIF_FILE) as im:
assert im.tell() == 0

Check warning on line 403 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L402-L403

Added lines #L402 - L403 were not covered by tests

def test_seek(self) -> None:
with Image.open(TEST_AVIF_FILE) as im:
im.seek(0)

Check warning on line 407 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L406-L407

Added lines #L406 - L407 were not covered by tests

with pytest.raises(EOFError):
im.seek(1)

Check warning on line 410 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L409-L410

Added lines #L409 - L410 were not covered by tests

@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"])
def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, subsampling=subsampling)

Check warning on line 416 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L414-L416

Added lines #L414 - L416 were not covered by tests

def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, subsampling="foo")

Check warning on line 422 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L419-L422

Added lines #L419 - L422 were not covered by tests

@pytest.mark.parametrize("value", ["full", "limited"])
def test_encoder_range(self, tmp_path: Path, value: str) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, range=value)

Check warning on line 428 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L426-L428

Added lines #L426 - L428 were not covered by tests

def test_encoder_range_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, range="foo")

Check warning on line 434 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L431-L434

Added lines #L431 - L434 were not covered by tests

@skip_unless_avif_encoder("aom")
def test_encoder_codec_param(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, codec="aom")

Check warning on line 440 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L438-L440

Added lines #L438 - L440 were not covered by tests

def test_encoder_codec_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="foo")

Check warning on line 446 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L443-L446

Added lines #L443 - L446 were not covered by tests

@skip_unless_avif_decoder("dav1d")
def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="dav1d")

Check warning on line 453 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L450-L453

Added lines #L450 - L453 were not covered by tests

@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize(
Expand All @@ -467,126 +467,126 @@
def test_encoder_advanced_codec_options(
self, advanced: dict[str, str] | Sequence[tuple[str, str]]
) -> None:
with Image.open(TEST_AVIF_FILE) as im:
ctrl_buf = BytesIO()
im.save(ctrl_buf, "AVIF", codec="aom")
test_buf = BytesIO()
im.save(

Check warning on line 474 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L470-L474

Added lines #L470 - L474 were not covered by tests
test_buf,
"AVIF",
codec="aom",
advanced=advanced,
)
assert ctrl_buf.getvalue() != test_buf.getvalue()

Check warning on line 480 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L480

Added line #L480 was not covered by tests

@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234])
def test_encoder_advanced_codec_options_invalid(
self, tmp_path: Path, advanced: dict[str, str] | int
) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="aom", advanced=advanced)

Check warning on line 490 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L487-L490

Added lines #L487 - L490 were not covered by tests

@skip_unless_avif_decoder("aom")
def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom")

Check warning on line 494 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L494

Added line #L494 was not covered by tests

with Image.open(TEST_AVIF_FILE) as im:
assert im.size == (128, 128)

Check warning on line 497 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L496-L497

Added lines #L496 - L497 were not covered by tests

@skip_unless_avif_encoder("rav1e")
def test_encoder_codec_cannot_decode(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e")

Check warning on line 503 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L503

Added line #L503 was not covered by tests

with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass

Check warning on line 507 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L505-L507

Added lines #L505 - L507 were not covered by tests

def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo")

Check warning on line 510 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L510

Added line #L510 was not covered by tests

with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass

Check warning on line 514 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L512-L514

Added lines #L512 - L514 were not covered by tests

@skip_unless_avif_encoder("aom")
def test_encoder_codec_available(self) -> None:
assert _avif.encoder_codec_available("aom") is True

Check warning on line 518 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L518

Added line #L518 was not covered by tests

def test_encoder_codec_available_bad_params(self) -> None:
with pytest.raises(TypeError):
_avif.encoder_codec_available()

Check warning on line 522 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L521-L522

Added lines #L521 - L522 were not covered by tests

@skip_unless_avif_decoder("dav1d")
def test_encoder_codec_available_cannot_decode(self) -> None:
assert _avif.encoder_codec_available("dav1d") is False

Check warning on line 526 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L526

Added line #L526 was not covered by tests

def test_encoder_codec_available_invalid(self) -> None:
assert _avif.encoder_codec_available("foo") is False

Check warning on line 529 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L529

Added line #L529 was not covered by tests

def test_encoder_quality_valueerror(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, quality="invalid")

Check warning on line 535 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L532-L535

Added lines #L532 - L535 were not covered by tests

@skip_unless_avif_decoder("aom")
def test_decoder_codec_available(self) -> None:
assert _avif.decoder_codec_available("aom") is True

Check warning on line 539 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L539

Added line #L539 was not covered by tests

def test_decoder_codec_available_bad_params(self) -> None:
with pytest.raises(TypeError):
_avif.decoder_codec_available()

Check warning on line 543 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L542-L543

Added lines #L542 - L543 were not covered by tests

@skip_unless_avif_encoder("rav1e")
def test_decoder_codec_available_cannot_decode(self) -> None:
assert _avif.decoder_codec_available("rav1e") is False

Check warning on line 547 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L547

Added line #L547 was not covered by tests

def test_decoder_codec_available_invalid(self) -> None:
assert _avif.decoder_codec_available("foo") is False

Check warning on line 550 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L550

Added line #L550 was not covered by tests

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)

Check warning on line 556 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L553-L556

Added lines #L553 - L556 were not covered by tests

out_png = tmp_path / "temp.png"
im.save(out_png, transparency=0)
with Image.open(out_png) as im_png:
out_avif = tmp_path / "temp.avif"
im_png.save(out_avif, quality=100)

Check warning on line 562 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L558-L562

Added lines #L558 - L562 were not covered by tests

with Image.open(out_avif) as expected:
assert_image_similar(im_png.convert("RGBA"), expected, 0.17)

Check warning on line 565 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L564-L565

Added lines #L564 - L565 were not covered by tests

def test_decoder_strict_flags(self) -> None:
# 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/hopper-missing-pixi.avif") as im:
assert im.size == (480, 270)

Check warning on line 570 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L569-L570

Added lines #L569 - L570 were not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it possible that someone could build libavif with those strict flags enabled, and then build Pillow from source to use it? So in that scenario, this test would fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strictFlags are passed at runtime, they're not set at compile time. By default it strictly enforces various specification rules, but two in particular are often found in the wild and so are regularly disabled:

Pillow/src/_avif.c

Lines 797 to 802 in 8b8bbba

// Turn off libavif's 'clap' (clean aperture) property validation.
self->decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID;
// Allow the PixelInformationProperty ('pixi') to be missing in AV1 image
// items. libheif v1.11.0 and older does not add the 'pixi' item property to
// AV1 image items.
self->decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED;

Chromium disables these two flags and WebKit disables AVIF_STRICT_PIXI_REQUIRED.
The test_decoder_strict_flags test verifies that the strictFlags are set appropriately so that these somewhat common images can load without throwing an error.


@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize("speed", [-1, 1, 11])
def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None:
test_file = tmp_path / "temp.avif"
hopper().save(test_file, codec="aom", speed=speed)

Check warning on line 576 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L575-L576

Added lines #L575 - L576 were not covered by tests

@skip_unless_avif_encoder("svt")
def test_svt_optimizations(self, tmp_path: Path) -> None:
test_file = tmp_path / "temp.avif"
hopper().save(test_file, codec="svt", speed=1)

Check warning on line 581 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L580-L581

Added lines #L580 - L581 were not covered by tests


@skip_unless_feature("avif")
class TestAvifAnimation:
@contextmanager
def star_frames(self) -> Generator[list[Image.Image], None, None]:
with Image.open("Tests/images/avif/star.png") as f:
yield [f, f.rotate(90), f.rotate(180), f.rotate(270)]

Check warning on line 589 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L588-L589

Added lines #L588 - L589 were not covered by tests

def test_n_frames(self) -> None:
"""
Expand All @@ -594,13 +594,13 @@
correctly.
"""

with Image.open(TEST_AVIF_FILE) as im:
assert im.n_frames == 1
assert not im.is_animated

Check warning on line 599 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L597-L599

Added lines #L597 - L599 were not covered by tests

with Image.open("Tests/images/avif/star.avifs") as im:
assert im.n_frames == 5
assert im.is_animated

Check warning on line 603 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L601-L603

Added lines #L601 - L603 were not covered by tests

def test_write_animation_P(self, tmp_path: Path) -> None:
"""
Expand All @@ -608,22 +608,22 @@
count, and ensure the frames are visually similar to the originals.
"""

with Image.open("Tests/images/avif/star.gif") as original:
assert original.n_frames > 1

Check warning on line 612 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L611-L612

Added lines #L611 - L612 were not covered by tests

temp_file = tmp_path / "temp.avif"
original.save(temp_file, save_all=True)
with Image.open(temp_file) as im:
assert im.n_frames == original.n_frames

Check warning on line 617 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L614-L617

Added lines #L614 - L617 were not covered by tests

# Compare first frame in P mode to frame from original GIF
assert_image_similar(im, original.convert("RGBA"), 2)

Check warning on line 620 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L620

Added line #L620 was not covered by tests

# Compare later frames in RGBA mode to frames from original GIF
for frame in range(1, original.n_frames):
original.seek(frame)
im.seek(frame)
assert_image_similar(im, original, 2.54)

Check warning on line 626 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L623-L626

Added lines #L623 - L626 were not covered by tests

def test_write_animation_RGBA(self, tmp_path: Path) -> None:
"""
Expand All @@ -631,62 +631,62 @@
are visually similar to the originals.
"""

def check(temp_file: Path) -> None:
with Image.open(temp_file) as im:
assert im.n_frames == 4

Check warning on line 636 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L634-L636

Added lines #L634 - L636 were not covered by tests

# Compare first frame to original
assert_image_similar(im, frame1, 2.7)

Check warning on line 639 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L639

Added line #L639 was not covered by tests

# Compare second frame to original
im.seek(1)
assert_image_similar(im, frame2, 4.1)

Check warning on line 643 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L642-L643

Added lines #L642 - L643 were not covered by tests

with self.star_frames() as frames:
frame1 = frames[0]
frame2 = frames[1]
temp_file1 = tmp_path / "temp.avif"
frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:])
check(temp_file1)

Check warning on line 650 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L645-L650

Added lines #L645 - L650 were not covered by tests

# Test appending using a generator
def imGenerator(

Check warning on line 653 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L653

Added line #L653 was not covered by tests
ims: list[Image.Image],
) -> Generator[Image.Image, None, None]:
yield from ims

Check warning on line 656 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L656

Added line #L656 was not covered by tests

temp_file2 = tmp_path / "temp_generator.avif"
frames[0].copy().save(

Check warning on line 659 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L658-L659

Added lines #L658 - L659 were not covered by tests
temp_file2,
save_all=True,
append_images=imGenerator(frames[1:]),
)
check(temp_file2)

Check warning on line 664 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L664

Added line #L664 was not covered by tests

def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None:
temp_file = tmp_path / "temp.avif"
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])

Check warning on line 671 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L667-L671

Added lines #L667 - L671 were not covered by tests

def test_heif_raises_unidentified_image_error(self) -> None:
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/avif/rgba10.heif"):
with Image.open("Tests/images/avif/hopper.heif"):
pass

Check warning on line 676 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L674-L676

Added lines #L674 - L676 were not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the relevance of this test?

Copy link
Contributor Author

@fdintino fdintino Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is intended to test this bit of logic:

if major_brand in container_brands:
# We accept files with AVIF container brands; we can't yet know if
# the ftyp box has the correct compatible brands, but if it doesn't
# then the plugin will raise a SyntaxError which Pillow will catch
# before moving on to the next plugin that accepts the file.
#
# Also, because this file might not actually be an AVIF file, we
# don't raise an error if AVIF support isn't properly compiled.
return True

If the ftyp box is avif or avis then this is definitely an AVIF image, and so it is sensible to return a string from the accept function to trigger an error. But mif1 and msf1 are valid for both AVIF and HEIF, and the initial bytes passed to the accept function are not long enough to determine which of the two it is. In order to be interoperable with pyheif I cannot raise an error in the accept, and must instead raise a SyntaxError in _open if AVIF is not compiled. This would cause Pillow to failover to any other plugins that accept the file, or—if none are found—cause it to raise an UnidentifiedImageError. Perhaps the test would be clearer in purpose and more useful if instead I created a mock plugin that would serve as the failover, and then asserted that it is called.


@pytest.mark.parametrize("alpha_premultiplied", [False, True])
def test_alpha_premultiplied(
self, tmp_path: Path, alpha_premultiplied: bool
) -> None:
temp_file = tmp_path / "temp.avif"
color = (200, 200, 200, 1)
im = Image.new("RGBA", (1, 1), color)
im.save(temp_file, alpha_premultiplied=alpha_premultiplied)

Check warning on line 685 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L682-L685

Added lines #L682 - L685 were not covered by tests

expected = (255, 255, 255, 1) if alpha_premultiplied else color
with Image.open(temp_file) as reloaded:
assert reloaded.getpixel((0, 0)) == expected

Check warning on line 689 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L687-L689

Added lines #L687 - L689 were not covered by tests

def test_timestamp_and_duration(self, tmp_path: Path) -> None:
"""
Expand All @@ -694,28 +694,28 @@
timestamps and durations are correct.
"""

durations = [1, 10, 20, 30, 40]
temp_file = tmp_path / "temp.avif"
with self.star_frames() as frames:
frames[0].save(

Check warning on line 700 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L697-L700

Added lines #L697 - L700 were not covered by tests
temp_file,
save_all=True,
append_images=(frames[1:] + [frames[0]]),
duration=durations,
)

with Image.open(temp_file) as im:
assert im.n_frames == 5
assert im.is_animated

Check warning on line 709 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L707-L709

Added lines #L707 - L709 were not covered by tests

# Check that timestamps and durations match original values specified
timestamp = 0
for frame in range(im.n_frames):
im.seek(frame)
im.load()
assert im.info["duration"] == durations[frame]
assert im.info["timestamp"] == timestamp
timestamp += durations[frame]

Check warning on line 718 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L712-L718

Added lines #L712 - L718 were not covered by tests

def test_seeking(self, tmp_path: Path) -> None:
"""
Expand All @@ -723,36 +723,36 @@
reverse-order, verifying the timestamps and durations are correct.
"""

duration = 33
temp_file = tmp_path / "temp.avif"
with self.star_frames() as frames:
frames[0].save(

Check warning on line 729 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L726-L729

Added lines #L726 - L729 were not covered by tests
temp_file,
save_all=True,
append_images=(frames[1:] + [frames[0]]),
duration=duration,
)

with Image.open(temp_file) as im:
assert im.n_frames == 5
assert im.is_animated

Check warning on line 738 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L736-L738

Added lines #L736 - L738 were not covered by tests

# Traverse frames in reverse, checking timestamps and durations
timestamp = duration * (im.n_frames - 1)
for frame in reversed(range(im.n_frames)):
im.seek(frame)
im.load()
assert im.info["duration"] == duration
assert im.info["timestamp"] == timestamp
timestamp -= duration

Check warning on line 747 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L741-L747

Added lines #L741 - L747 were not covered by tests

def test_seek_errors(self) -> None:
with Image.open("Tests/images/avif/star.avifs") as im:
with pytest.raises(EOFError):
im.seek(-1)

Check warning on line 752 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L750-L752

Added lines #L750 - L752 were not covered by tests

with pytest.raises(EOFError):
im.seek(42)

Check warning on line 755 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L754-L755

Added lines #L754 - L755 were not covered by tests


MAX_THREADS = os.cpu_count() or 1
Expand All @@ -767,12 +767,12 @@
is_docker_qemu(), reason="Skipping on cross-architecture containers"
)
def test_leak_load(self) -> None:
with open(TEST_AVIF_FILE, "rb") as f:
im_data = f.read()

Check warning on line 771 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L770-L771

Added lines #L770 - L771 were not covered by tests

def core() -> None:
with Image.open(BytesIO(im_data)) as im:
im.load()
gc.collect()

Check warning on line 776 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L773-L776

Added lines #L773 - L776 were not covered by tests

self._test_leak(core)

Check warning on line 778 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L778

Added line #L778 was not covered by tests
Loading