From 6c7917d7a6031ae22e1d9eaccc2e536123ea25c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Mar 2025 07:54:47 +1100 Subject: [PATCH 01/10] Revert to zlib on macOS < 10.15 --- .github/workflows/wheels-dependencies.sh | 7 ++++++- Tests/check_wheel.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index e9c54536e..e0c18d6c2 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,6 +45,7 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 +ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.4 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 @@ -106,7 +107,11 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_zlib_ng + if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then + build_new_zlib + else + build_zlib_ng + fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 563be0b74..8ba40ba3f 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,9 +1,12 @@ from __future__ import annotations +import platform import sys from PIL import features +from .helper import is_pypy + def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} @@ -40,5 +43,7 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") + elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm": + expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features From 14fb62e36c5e933faf9f88e9c0418f71be883a9c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 18:42:46 +1100 Subject: [PATCH 02/10] Assert image type (#8619) --- Tests/test_file_apng.py | 44 ++++++++++++++++++++++++++++++++ Tests/test_file_dcx.py | 2 ++ Tests/test_file_eps.py | 6 +++++ Tests/test_file_fli.py | 8 ++++++ Tests/test_file_fpx.py | 3 ++- Tests/test_file_gif.py | 25 ++++++++++++++++++ Tests/test_file_icns.py | 4 +++ Tests/test_file_ico.py | 5 ++++ Tests/test_file_im.py | 2 ++ Tests/test_file_jpeg.py | 10 ++++++++ Tests/test_file_jpeg2k.py | 2 ++ Tests/test_file_libtiff.py | 21 +++++++++++++++ Tests/test_file_mic.py | 5 +++- Tests/test_file_mpo.py | 7 ++++- Tests/test_file_png.py | 7 +++++ Tests/test_file_psd.py | 7 +++++ Tests/test_file_spider.py | 1 + Tests/test_file_tiff.py | 44 +++++++++++++++++++++++++++++--- Tests/test_file_tiff_metadata.py | 20 +++++++++++++++ Tests/test_file_webp_animated.py | 9 ++++++- Tests/test_file_webp_metadata.py | 3 ++- Tests/test_file_wmf.py | 3 +++ Tests/test_file_xpm.py | 1 + Tests/test_imagesequence.py | 4 ++- Tests/test_shell_injection.py | 1 + Tests/test_tiff_ifdrational.py | 1 + 26 files changed, 236 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index abd7d510b..a5734c202 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin # (referenced from https://wiki.mozilla.org/APNG_Specification) def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated assert im.n_frames == 1 assert im.get_format_mimetype() == "image/apng" @@ -20,6 +21,7 @@ def test_apng_basic() -> None: assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/single_frame_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.is_animated assert im.n_frames == 2 assert im.get_format_mimetype() == "image/apng" @@ -52,6 +54,7 @@ def test_apng_basic() -> None: ) def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -59,31 +62,37 @@ def test_apng_fdat(filename: str) -> None: def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_background.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) @@ -91,21 +100,25 @@ def test_apng_dispose() -> None: def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 255, 255) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -132,6 +145,7 @@ def test_apng_dispose_op_previous_frame() -> None: # ], # ) with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (255, 0, 0, 255) @@ -145,26 +159,31 @@ def test_apng_dispose_op_background_p_mode() -> None: def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 2) assert im.getpixel((64, 32)) == (0, 255, 0, 2) with Image.open("Tests/images/apng/blend_op_over.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 97) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -178,6 +197,7 @@ def test_apng_blend_transparency() -> None: def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -233,24 +253,28 @@ def test_apng_num_plays() -> None: def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "RGBA" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 128, 191) assert im.getpixel((64, 32)) == (0, 0, 128, 191) with Image.open("Tests/images/apng/mode_grayscale.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "L" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == 128 assert im.getpixel((64, 32)) == 255 with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "LA" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (128, 191) assert im.getpixel((64, 32)) == (128, 191) with Image.open("Tests/images/apng/mode_palette.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGB") @@ -258,6 +282,7 @@ def test_apng_mode() -> None: assert im.getpixel((64, 32)) == (0, 255, 0) with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") @@ -265,6 +290,7 @@ def test_apng_mode() -> None: assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") @@ -274,25 +300,31 @@ def test_apng_mode() -> None: def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) @@ -300,26 +332,31 @@ def test_apng_chunk_errors() -> None: def test_apng_syntax_errors() -> None: with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.raises(OSError): im.load() with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated im.load() # we can handle this case gracefully with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated im.load() @@ -339,6 +376,7 @@ def test_apng_syntax_errors() -> None: def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() @@ -349,6 +387,7 @@ def test_apng_save(tmp_path: Path) -> None: im.save(test_file, save_all=True) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.load() assert not im.is_animated assert im.n_frames == 1 @@ -364,6 +403,7 @@ def test_apng_save(tmp_path: Path) -> None: ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.load() assert im.is_animated assert im.n_frames == 2 @@ -403,6 +443,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: append_images=frames, ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() @@ -445,6 +486,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 1 assert "duration" not in im.info @@ -456,6 +498,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: duration=[500, 100, 150], ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 @@ -466,6 +509,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: frame.info["duration"] = 300 frame.save(test_file, save_all=True, append_images=[frame, different_frame]) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index ab6b9f983..e9d88dd39 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -69,12 +69,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index f5acc532c..b484a8cfa 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -86,6 +86,8 @@ simple_eps_file_with_long_binary_data = ( def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: + assert isinstance(image, EpsImagePlugin.EpsImageFile) + image.load(scale=scale) assert image.mode == "RGB" assert image.size == expected_size @@ -227,6 +229,8 @@ def test_showpage() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_transparency() -> None: with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: + assert isinstance(plot_image, EpsImagePlugin.EpsImageFile) + plot_image.load(transparency=True) assert plot_image.mode == "RGBA" @@ -308,6 +312,7 @@ def test_render_scale2() -> None: # Zero bounding box with Image.open(FILE1) as image1_scale2: + assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) image1_scale2.load(scale=2) with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: image1_scale2_compare = image1_scale2_compare.convert("RGB") @@ -316,6 +321,7 @@ def test_render_scale2() -> None: # Non-zero bounding box with Image.open(FILE2) as image2_scale2: + assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: image2_scale2_compare = image2_scale2_compare.convert("RGB") diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 2f39adc69..81df1ab0b 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -22,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" def test_sanity() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + im.load() assert im.mode == "P" assert im.size == (128, 128) @@ -29,6 +31,8 @@ def test_sanity() -> None: assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + assert im.mode == "P" assert im.size == (320, 200) assert im.format == "FLI" @@ -112,16 +116,19 @@ def test_palette_chunk_second() -> None: def test_n_frames() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 384 assert im.is_animated def test_eoferror() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -166,6 +173,7 @@ def test_seek_tell() -> None: def test_seek() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) im.seek(50) assert_image_equal_tofile(im, "Tests/images/a_fli.png") diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index e32f30a01..8d8064692 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -22,10 +22,11 @@ def test_sanity() -> None: def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: - pass + assert isinstance(im, FpxImagePlugin.FpxImageFile) assert im.ole.fp.closed im = Image.open("Tests/images/input_bw_one_band.fpx") + assert isinstance(im, FpxImagePlugin.FpxImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 376eab0c6..20d58a9dd 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -402,6 +402,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None: def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) frame_count = 0 try: while True: @@ -446,10 +447,12 @@ def test_seek_rewind() -> None: def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated == (n_frames != 1) # Test is_animated after n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -459,6 +462,7 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 5 assert_image_equal(im, expected) @@ -466,17 +470,20 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(3) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated assert_image_equal(im, expected) with Image.open("Tests/images/comment_after_only_frame.gif") as im: expected = Image.new("P", (1, 1)) + assert isinstance(im, GifImagePlugin.GifImageFile) assert not im.is_animated assert_image_equal(im, expected) def test_eoferror() -> None: with Image.open(TEST_GIF) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -495,6 +502,7 @@ def test_first_frame_transparency() -> None: def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -518,6 +526,7 @@ def test_dispose_none_load_end() -> None: def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -571,6 +580,7 @@ def test_transparent_dispose( def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -608,6 +618,7 @@ def test_save_dispose(tmp_path: Path) -> None: for method in range(4): im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for _ in range(2): img.seek(img.tell() + 1) assert img.disposal_method == method @@ -621,6 +632,7 @@ def test_save_dispose(tmp_path: Path) -> None: ) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -743,6 +755,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) with Image.open(out) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 3 @@ -924,6 +937,8 @@ def test_identical_frames(tmp_path: Path) -> None: out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that the first three frames were combined assert reread.n_frames == 2 @@ -953,6 +968,8 @@ def test_identical_frames_to_single_frame( im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that all frames were combined assert reread.n_frames == 1 @@ -1139,12 +1156,14 @@ def test_append_images(tmp_path: Path) -> None: im.copy().save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Test append_images without save_all im.copy().save(out, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending using a generator @@ -1154,6 +1173,7 @@ def test_append_images(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=im_generator(ims)) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending single and multiple frame images @@ -1162,6 +1182,7 @@ def test_append_images(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 10 @@ -1262,6 +1283,7 @@ def test_bbox(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1274,6 +1296,7 @@ def test_bbox_alpha(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1425,6 +1448,7 @@ def test_extents( ) -> None: monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) with Image.open("Tests/images/" + test_file) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.size == (100, 100) # Check that n_frames does not change the size @@ -1472,4 +1496,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: im1.save(out, save_all=True, append_images=[im2], **params) with Image.open(out) as reloaded: + assert isinstance(reloaded, GifImagePlugin.GifImageFile) assert reloaded.n_frames == 2 diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index b6dc4bc19..2dabfd2f3 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None: assert_image_similar_tofile(im, temp_file, 1) with Image.open(temp_file) as reread: + assert isinstance(reread, IcnsImagePlugin.IcnsImageFile) reread.size = (16, 16) reread.load(2) assert_image_equal(reread, provided_im) @@ -90,6 +91,7 @@ def test_sizes() -> None: # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected with Image.open(TEST_FILE) as im: + assert isinstance(im, IcnsImagePlugin.IcnsImageFile) for w, h, r in im.info["sizes"]: wr = w * r hr = h * r @@ -118,6 +120,7 @@ def test_older_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow2.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h) im2.load(r) assert im2.mode == "RGBA" @@ -135,6 +138,7 @@ def test_jp2_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow3.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h) im2.load(r) assert im2.mode == "RGBA" diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37bfd3f1f..5d2ace35e 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -77,6 +77,7 @@ def test_save_to_bytes() -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert im.mode == reloaded.mode @@ -94,6 +95,7 @@ def test_getpixel(tmp_path: Path) -> None: im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) with Image.open(temp_file) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.load() reloaded.size = (32, 32) @@ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert "RGBA" == reloaded.mode @@ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: + assert isinstance(im, IcoImagePlugin.IcoImageFile) with pytest.raises(ValueError): im.size = (1, 1) @@ -219,6 +223,7 @@ def test_save_append_images(tmp_path: Path) -> None: im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) with Image.open(outfile) as reread: + assert isinstance(reread, IcoImagePlugin.IcoImageFile) assert_image_equal(reread, hopper("RGBA")) reread.size = (32, 32) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 235914a2b..55c6b7305 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -68,12 +68,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 8ab853b85..79f0ec1a8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -91,6 +91,7 @@ class TestFileJpeg: def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") assert im.applist[1] == ( "COM", @@ -316,6 +317,8 @@ class TestFileJpeg: def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise a TypeError im._getexif() @@ -500,6 +503,7 @@ class TestFileJpeg: def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im._getmp() is None def test_quality_keep(self, tmp_path: Path) -> None: @@ -558,12 +562,14 @@ class TestFileJpeg: with Image.open(test_file) as im: im.save(b, "JPEG", qtables=[[n] * 64] * n) with Image.open(b) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization assert max(reloaded.quantization[0]) <= 255 with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) qtables = im.quantization reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) assert im.quantization == reloaded.quantization @@ -663,6 +669,7 @@ class TestFileJpeg: def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 @@ -705,6 +712,7 @@ class TestFileJpeg: @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self) -> None: with Image.open(TEST_FILE) as img: + assert isinstance(img, JpegImagePlugin.JpegImageFile) img.load_djpeg() assert_image_similar_tofile(img, TEST_FILE, 5) @@ -909,6 +917,7 @@ class TestFileJpeg: def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert "photoshop" in im.info assert 24 == len(im.info["photoshop"]) apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] @@ -1084,6 +1093,7 @@ class TestFileJpeg: def test_deprecation(self) -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) with pytest.warns(DeprecationWarning): assert im.huffman_ac == {} with pytest.warns(DeprecationWarning): diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 916df2586..4095bfaf2 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None: out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 1 im.load() assert_image_similar(im, card, 13) out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 3 im.load() assert_image_similar(im, card, 0.4) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9e63e9c10..9916215fb 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -36,6 +36,7 @@ class LibTiffTestCase: im.load() im.getdata() + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._compression == "group4" # can we write it back out, in a different form. @@ -153,6 +154,7 @@ class TestFileLibTiff(LibTiffTestCase): """Test metadata writing through libtiff""" f = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) img.save(f, tiffinfo=img.tag) if legacy_api: @@ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase): ] with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) if legacy_api: reloaded = loaded.tag.named() else: @@ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase): # Exclude ones that have special meaning # that we're already testing them with Image.open("Tests/images/hopper_g4.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) for tag in im.tag_v2: try: del core_items[tag] @@ -317,6 +321,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) for tag, value in tiffinfo.items(): reloaded_value = reloaded.tag_v2[tag] if ( @@ -349,12 +354,14 @@ class TestFileLibTiff(LibTiffTestCase): def test_osubfiletype(self, tmp_path: Path) -> None: outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[OSUBFILETYPE] = 1 im.save(outfile) def test_subifd(self, tmp_path: Path) -> None: outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[SUBIFD] = 10000 # Should not segfault @@ -369,6 +376,7 @@ class TestFileLibTiff(LibTiffTestCase): hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" @@ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: + assert isinstance(orig, TiffImagePlugin.TiffImageFile) + out = tmp_path / "temp.tif" orig.tag[269] = "temp.tif" orig.save(out) with Image.open(out) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] @@ -541,6 +552,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: # colormap/palette tag + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) @@ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im.size == (10, 10) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) @@ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase): # issue #862 monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) frames = im.n_frames assert frames == 3 for _ in range(frames): @@ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase): def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert not im.tag.next im.load() assert not im.tag.next @@ -690,21 +705,25 @@ class TestFileLibTiff(LibTiffTestCase): im.save(outfile, compression="jpeg") with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) def test_exif_ifd(self) -> None: out = io.BytesIO() with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[34665] == 125456 im.save(out, "TIFF") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 34665 not in reloaded.tag_v2 im.save(out, "TIFF", tiffinfo={34665: 125456}) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 @@ -786,6 +805,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) @@ -1090,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 274 in im.tag_v2 im.load() diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 9a6f13ea3..9aeb306e4 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -30,11 +30,13 @@ def test_sanity() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.n_frames == 1 def test_is_animated() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert not im.is_animated @@ -55,10 +57,11 @@ def test_seek() -> None: def test_close() -> None: with Image.open(TEST_FILE) as im: - pass + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.ole.fp.closed im = Image.open(TEST_FILE) + assert isinstance(im, MicImagePlugin.MicImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 6b4f6423b..73838ef44 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -6,7 +6,7 @@ from typing import Any import pytest -from PIL import Image, ImageFile, MpoImagePlugin +from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin from .helper import ( assert_image_equal, @@ -80,6 +80,7 @@ def test_context_manager() -> None: def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.applist[0][0] == "APP1" assert im.applist[1][0] == "APP2" assert im.applist[1][1].startswith( @@ -220,12 +221,14 @@ def test_seek(test_file: str) -> None: def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -239,6 +242,8 @@ def test_eoferror() -> None: def test_adopt_jpeg() -> None: with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + with pytest.raises(ValueError): MpoImagePlugin.MpoImageFile.adopt(im) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c969bd502..0f0886ab8 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -576,6 +576,7 @@ class TestFilePng: def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.private_chunks == [(b"orNT", b"\x01")] def test_roundtrip_private_chunk(self) -> None: @@ -598,6 +599,7 @@ class TestFilePng: def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/hopper.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert "comment" in im.text for k, v in { "date:create": "2014-09-04T09:37:08+03:00", @@ -607,15 +609,19 @@ class TestFilePng: # Raises a SyntaxError in load_end with Image.open("Tests/images/broken_data_stream.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(OSError): assert isinstance(im.text, dict) # Raises an EOFError in load_end with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} # Raises a UnicodeDecodeError in load_end with Image.open("Tests/images/truncated_image.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + # The file is truncated with pytest.raises(OSError): im.text @@ -726,6 +732,7 @@ class TestFilePng: im.save(test_file) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) assert reloaded._getexif() is None # Test passing in exif diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 1793c269d..38a88cd17 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -59,17 +59,21 @@ def test_invalid_file() -> None: def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 assert not im.is_animated for path in [test_file, "Tests/images/negative_layer_count.psd"]: with Image.open(path) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -119,11 +123,13 @@ def test_rgba() -> None: def test_negative_top_left_layer() -> None: with Image.open("Tests/images/negative_top_left_layer.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.layers[0][2] == (-50, -50, 50, 50) def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 @@ -175,5 +181,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None: def test_layer_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) with pytest.raises(SyntaxError): im.layers diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index b64a629f5..3b3c3b4a5 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -96,6 +96,7 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, SpiderImagePlugin.SpiderImageFile) assert im.n_frames == 1 assert not im.is_animated diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 6962a5c98..502d9df9a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -9,7 +9,13 @@ from types import ModuleType import pytest -from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + TiffImagePlugin, + UnidentifiedImageError, +) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -113,6 +119,7 @@ class TestFileTiff: with Image.open("Tests/images/hopper_bigtiff.tif") as im: outfile = tmp_path / "temp.tif" + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) def test_bigtiff_save(self, tmp_path: Path) -> None: @@ -121,11 +128,13 @@ class TestFileTiff: im.save(outfile, big_tiff=True) with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2._bigtiff is True im.save(outfile, save_all=True, append_images=[im], big_tiff=True) with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2._bigtiff is True def test_seek_too_large(self) -> None: @@ -140,6 +149,8 @@ class TestFileTiff: def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -153,6 +164,8 @@ class TestFileTiff: def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -167,6 +180,8 @@ class TestFileTiff: def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -181,6 +196,7 @@ class TestFileTiff: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) @@ -198,6 +214,7 @@ class TestFileTiff: with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) with Image.open(b) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 @@ -213,10 +230,12 @@ class TestFileTiff: TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self) -> None: - with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + with Image.open("Tests/images/hopper_bad_exif.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise struct.error. with pytest.warns(UserWarning): - i._getexif() + im._getexif() def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") @@ -307,11 +326,13 @@ class TestFileTiff: ) def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -355,19 +376,24 @@ class TestFileTiff: def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 1 # A frame can't progress to a frame that has already been read with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 2 # Frames don't have to be in sequence with Image.open("Tests/images/multipage_out_of_order.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Act ret = str(im.ifd) @@ -378,6 +404,8 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 interface v2_tags = { 256: 55, @@ -417,6 +445,7 @@ class TestFileTiff: def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) len_before = len(dict(im.ifd)) del im.ifd[256] len_after = len(dict(im.ifd)) @@ -449,6 +478,7 @@ class TestFileTiff: def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: @@ -537,6 +567,7 @@ class TestFileTiff: im = hopper(mode) im.save(filename, tiffinfo={262: 0}) with Image.open(filename) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) @@ -615,6 +646,8 @@ class TestFileTiff: filename = tmp_path / "temp.tif" hopper("RGB").save(filename, "TIFF", **kwargs) with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 @@ -701,6 +734,7 @@ class TestFileTiff: def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._planar_configuration == 2 outfile = tmp_path / "temp.tif" @@ -733,6 +767,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 # Test appending images @@ -743,6 +778,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 # Test appending using a generator @@ -754,6 +790,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 def test_fixoffsets(self) -> None: @@ -864,6 +901,7 @@ class TestFileTiff: def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert list(im.get_photoshop_blocks().keys()) == [ 1061, 1002, diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 0734d1db1..884868345 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None: img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None: info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, @@ -128,6 +131,7 @@ def test_read_metadata() -> None: def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) f = tmp_path / "temp.tiff" del img.tag[278] img.save(f, tiffinfo=img.tag) @@ -135,6 +139,7 @@ def test_write_metadata(tmp_path: Path) -> None: original = img.tag_v2.named() with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) reloaded = loaded.tag_v2.named() ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] @@ -165,6 +170,7 @@ def test_write_metadata(tmp_path: Path) -> None: def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) info = im.tag_v2 del info[278] @@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG @@ -231,6 +238,7 @@ def test_writing_other_types_to_ascii( im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[271] == expected @@ -248,6 +256,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[700] == b"\x01" @@ -267,6 +276,7 @@ def test_writing_other_types_to_undefined( im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[33723] == b"1" @@ -311,6 +321,7 @@ def test_iccprofile_binary() -> None: # but probably won't be able to save it. with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.tagtype[34675] == 1 assert im.info["icc_profile"] @@ -336,6 +347,7 @@ def test_exif_div_zero(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 0 == reloaded.tag_v2[41988].numerator assert 0 == reloaded.tag_v2[41988].denominator @@ -355,6 +367,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -367,6 +380,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -385,6 +399,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -397,6 +412,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -410,6 +426,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert -1 == reloaded.tag_v2[37380].denominator @@ -424,6 +441,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[37000] == -60000 @@ -444,11 +462,13 @@ def test_empty_values() -> None: def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) out = tmp_path / "temp.tiff" im.save(out) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[34377]) == 70 assert isinstance(reloaded.tag_v2[34377], bytes) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index d4b1fda97..503761374 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import GifImagePlugin, Image, WebPImagePlugin, features from .helper import ( assert_image_equal, @@ -22,10 +22,12 @@ def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open("Tests/images/iss634.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 42 assert im.is_animated @@ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None: """ with Image.open("Tests/images/iss634.gif") as orig: + assert isinstance(orig, GifImagePlugin.GifImageFile) assert orig.n_frames > 1 temp_file = tmp_path / "temp.webp" orig.save(temp_file, save_all=True) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == orig.n_frames # Compare first and last frames to the original animated GIF @@ -69,6 +73,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: def check(temp_file: Path) -> None: with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 2 # Compare first frame to original @@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated @@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index d1d3421ec..7543d22da 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -6,7 +6,7 @@ from types import ModuleType import pytest -from PIL import Image +from PIL import Image, WebPImagePlugin from .helper import mark_if_feature_version, skip_unless_feature @@ -110,6 +110,7 @@ def test_read_no_exif() -> None: test_buffer.seek(0) with Image.open(test_buffer) as webp_image: + assert isinstance(webp_image, WebPImagePlugin.WebPImageFile) assert not webp_image._getexif() diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index a752f8013..dcf5f000f 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -89,6 +89,7 @@ def test_load_float_dpi() -> None: def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) assert im.size == (82, 82) if hasattr(Image.core, "drawwmf"): @@ -102,10 +103,12 @@ def test_load_set_dpi() -> None: if not hasattr(Image.core, "drawwmf"): return + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) im.load(im.info["dpi"]) assert im.size == (1625, 1625) with Image.open("Tests/images/drawing.emf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) im.load((72, 144)) assert im.size == (82, 164) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 26afe93f4..73c62a44d 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -30,6 +30,7 @@ def test_invalid_file() -> None: def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: + assert isinstance(im, XpmImagePlugin.XpmImageFile) dummy_bytes = 1 # Act diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index da9e71692..7b9ac80bc 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from PIL import Image, ImageSequence, TiffImagePlugin +from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin from .helper import assert_image_equal, hopper, skip_unless_feature @@ -31,6 +31,7 @@ def test_sanity(tmp_path: Path) -> None: def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) i = ImageSequence.Iterator(im) for index in range(im.n_frames): assert i[index] == next(i) @@ -42,6 +43,7 @@ def test_iterator() -> None: def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 4fd3aab5d..03e92b5b9 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -39,6 +39,7 @@ class TestShellInjection: shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im.load_djpeg() @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 30dc73654..42d06b896 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -72,4 +72,5 @@ def test_ifd_rational_save( im.save(out, dpi=(res, res), compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) From 4236b583a1578c089cd06538a9ded20bcba1a863 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Mar 2025 22:16:16 +1100 Subject: [PATCH 03/10] Do not import TYPE_CHECKING --- src/PIL/GifImagePlugin.py | 3 ++- src/PIL/Image.py | 10 ++-------- src/PIL/ImageDraw.py | 3 ++- src/PIL/ImageFile.py | 3 ++- src/PIL/ImageFilter.py | 3 ++- src/PIL/ImageFont.py | 3 ++- src/PIL/ImagePalette.py | 3 ++- src/PIL/ImageQt.py | 3 ++- src/PIL/ImageTk.py | 3 ++- src/PIL/JpegImagePlugin.py | 3 ++- src/PIL/PSDraw.py | 5 ++++- src/PIL/PdfParser.py | 3 ++- src/PIL/PngImagePlugin.py | 3 ++- src/PIL/SpiderImagePlugin.py | 4 +++- src/PIL/TiffImagePlugin.py | 3 ++- src/PIL/_typing.py | 3 ++- 16 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 045ab1027..4392c4cb9 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union +from typing import IO, Any, Literal, NamedTuple, Union from . import ( Image, @@ -47,6 +47,7 @@ from ._binary import o8 from ._binary import o16le as o16 from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging from ._typing import Buffer diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 662afadf4..19b22342a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,14 +41,7 @@ import warnings from collections.abc import Callable, Iterator, MutableMapping, Sequence from enum import IntEnum from types import ModuleType -from typing import ( - IO, - TYPE_CHECKING, - Any, - Literal, - Protocol, - cast, -) +from typing import IO, Any, Literal, Protocol, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -218,6 +211,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): # -------------------------------------------------------------------- # Registries +TYPE_CHECKING = False if TYPE_CHECKING: import mmap from xml.etree.ElementTree import Element diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c2ed9034d..e6c7b0298 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -35,7 +35,7 @@ import math import struct from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast +from typing import Any, AnyStr, Callable, Union, cast from . import Image, ImageColor from ._deprecate import deprecate @@ -44,6 +44,7 @@ from ._typing import Coords # experimental access to the outline API Outline: Callable[[], Image.core._Outline] = Image.core.outline +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageDraw2, ImageFont diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a7848c369..c5d6383a5 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -34,12 +34,13 @@ import itertools import logging import os import struct -from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast +from typing import IO, Any, NamedTuple, cast from . import ExifTags, Image from ._deprecate import deprecate from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import StrOrBytesPath diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 05829d0c6..b9ed54ab2 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -20,8 +20,9 @@ import abc import functools from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import Any, Callable, cast +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging from ._typing import NumpyArray diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c8f05fbb7..ebe510ba9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,12 +34,13 @@ import warnings from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast +from typing import IO, Any, BinaryIO, TypedDict, cast from . import Image, features from ._typing import StrOrBytesPath from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageFile from ._imaging import ImagingFont diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 183f85526..103697117 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -19,10 +19,11 @@ from __future__ import annotations import array from collections.abc import Sequence -from typing import IO, TYPE_CHECKING +from typing import IO from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile +TYPE_CHECKING = False if TYPE_CHECKING: from . import Image diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 2cc40f855..df7a57b65 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,11 +19,12 @@ from __future__ import annotations import sys from io import BytesIO -from typing import TYPE_CHECKING, Any, Callable, Union +from typing import Any, Callable, Union from . import Image from ._util import is_path +TYPE_CHECKING = False if TYPE_CHECKING: import PyQt6 import PySide6 diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index e6a9d8eea..3a4cb81e9 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -28,10 +28,11 @@ from __future__ import annotations import tkinter from io import BytesIO -from typing import TYPE_CHECKING, Any +from typing import Any from . import Image, ImageFile +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import CapsuleType diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 9465d8e2d..cc1d54b93 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,7 @@ import subprocess import sys import tempfile import warnings -from typing import IO, TYPE_CHECKING, Any +from typing import IO, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -52,6 +52,7 @@ from ._binary import o16be as o16 from ._deprecate import deprecate from .JpegPresets import presets +TYPE_CHECKING = False if TYPE_CHECKING: from .MpoImagePlugin import MpoImageFile diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 02939d26b..7fd4c5c94 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,10 +17,13 @@ from __future__ import annotations import sys -from typing import IO, TYPE_CHECKING +from typing import IO from . import EpsImagePlugin +TYPE_CHECKING = False + + ## # Simple PostScript graphics interface. diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 41b38ebbf..73d8c21c0 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,7 +8,7 @@ import os import re import time import zlib -from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union +from typing import IO, Any, NamedTuple, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -251,6 +251,7 @@ class PdfArray(list[Any]): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" +TYPE_CHECKING = False if TYPE_CHECKING: _DictBase = collections.UserDict[Union[str, bytes], Any] else: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 3e3cf6526..f3815a122 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -40,7 +40,7 @@ import warnings import zlib from collections.abc import Callable from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast +from typing import IO, Any, NamedTuple, NoReturn, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -50,6 +50,7 @@ from ._binary import o16be as o16 from ._binary import o32be as o32 from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 62fa7be03..868019e80 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -37,11 +37,13 @@ from __future__ import annotations import os import struct import sys -from typing import IO, TYPE_CHECKING, Any, cast +from typing import IO, Any, cast from . import Image, ImageFile from ._util import DeferredError +TYPE_CHECKING = False + def isInt(f: Any) -> int: try: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ebe599cca..88af9162e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,7 +50,7 @@ import warnings from collections.abc import Iterator, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast +from typing import IO, Any, Callable, NoReturn, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -61,6 +61,7 @@ from ._typing import StrOrBytesPath from ._util import DeferredError, is_path from .TiffTags import TYPES +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import Buffer, IntegralLike diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 34a9a81e1..373938e71 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -3,8 +3,9 @@ from __future__ import annotations import os import sys from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union +from typing import Any, Protocol, TypeVar, Union +TYPE_CHECKING = False if TYPE_CHECKING: from numbers import _IntegralLike as IntegralLike From 81be8d54103d008dcfb7d75edc5ca43d0f78afd5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 31 Mar 2025 05:16:25 +1100 Subject: [PATCH 04/10] Fixed unclosed file warning (#8847) --- Tests/test_image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index f18d8489c..c2e850c36 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -230,10 +230,10 @@ class TestImage: assert_image_similar(im, reloaded, 20) def test_unknown_extension(self, tmp_path: Path) -> None: - im = hopper() temp_file = tmp_path / "temp.unknown" - with pytest.raises(ValueError): - im.save(temp_file) + with hopper() as im: + with pytest.raises(ValueError): + im.save(temp_file) def test_internals(self) -> None: im = Image.new("L", (100, 100)) From f673f3e543881ae283b25ef7db06fa828a4e353a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 31 Mar 2025 05:16:50 +1100 Subject: [PATCH 05/10] Close file handle on error (#8846) --- src/PIL/TarIO.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 779288b1c..86490a496 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,16 @@ class TarIO(ContainerIO.ContainerIO[bytes]): while True: s = self.fh.read(512) if len(s) != 512: + self.fh.close() + msg = "unexpected end of tar file" raise OSError(msg) name = s[:100].decode("utf-8") i = name.find("\0") if i == 0: + self.fh.close() + msg = "cannot find subfile" raise OSError(msg) if i > 0: From 999d9a7f0cab002c78c93c3790307474256f7d90 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:09:09 +1100 Subject: [PATCH 06/10] Updated xz to 5.8.0 on manylinux2014 by removing po4a dependency (#8848) --- .github/workflows/wheels-dependencies.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4858f6d69..2e842df64 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,11 +42,7 @@ HARFBUZZ_VERSION=11.0.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 -if [[ $MB_ML_VER == 2014 ]]; then - XZ_VERSION=5.6.4 -else - XZ_VERSION=5.8.0 -fi +XZ_VERSION=5.8.0 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 @@ -56,6 +52,20 @@ BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +if [[ $MB_ML_VER == 2014 ]]; then + function build_xz { + if [ -e xz-stamp ]; then return; fi + yum install -y gettext-devel + fetch_unpack https://tukaani.org/xz/xz-$XZ_VERSION.tar.gz + (cd xz-$XZ_VERSION \ + && ./autogen.sh --no-po4a \ + && ./configure --prefix=$BUILD_PREFIX \ + && make -j4 \ + && make install) + touch xz-stamp + } +fi + function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi # This essentially duplicates the Homebrew recipe From 7d50816f0a6e607b04f9bdc8af7482a29ba578e3 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Tue, 1 Apr 2025 00:13:21 -0400 Subject: [PATCH 07/10] Add AVIF plugin (decoder + encoder using libavif) (#5201) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .ci/install.sh | 5 +- .github/workflows/macos-install.sh | 7 + .github/workflows/test-mingw.yml | 1 + .github/workflows/test-windows.yml | 6 +- .github/workflows/wheels-dependencies.sh | 43 +- .github/workflows/wheels.yml | 5 + Tests/check_wheel.py | 8 +- Tests/images/avif/exif.avif | Bin 0 -> 16078 bytes Tests/images/avif/hopper-missing-pixi.avif | Bin 0 -> 5435 bytes Tests/images/avif/hopper.avif | Bin 0 -> 3077 bytes Tests/images/avif/hopper.heif | Bin 0 -> 3555 bytes Tests/images/avif/hopper_avif_write.png | Bin 0 -> 30311 bytes Tests/images/avif/icc_profile.avif | Bin 0 -> 6460 bytes Tests/images/avif/icc_profile_none.avif | Bin 0 -> 3303 bytes Tests/images/avif/rot0mir0.avif | Bin 0 -> 16357 bytes Tests/images/avif/rot0mir1.avif | Bin 0 -> 17157 bytes Tests/images/avif/rot1mir0.avif | Bin 0 -> 17182 bytes Tests/images/avif/rot1mir1.avif | Bin 0 -> 16588 bytes Tests/images/avif/rot2mir0.avif | Bin 0 -> 17001 bytes Tests/images/avif/rot2mir1.avif | Bin 0 -> 16387 bytes Tests/images/avif/rot3mir0.avif | Bin 0 -> 16568 bytes Tests/images/avif/rot3mir1.avif | Bin 0 -> 17290 bytes Tests/images/avif/star.avifs | Bin 0 -> 29724 bytes Tests/images/avif/star.gif | Bin 0 -> 2900 bytes Tests/images/avif/star.png | Bin 0 -> 3844 bytes Tests/images/avif/transparency.avif | Bin 0 -> 6441 bytes Tests/images/avif/xmp_tags_orientation.avif | Bin 0 -> 6686 bytes Tests/test_file_avif.py | 778 +++++++++++++++++ depends/install_libavif.sh | 64 ++ docs/handbook/image-file-formats.rst | 79 +- docs/installation/building-from-source.rst | 29 +- docs/reference/features.rst | 1 + docs/reference/plugins.rst | 8 + docs/releasenotes/11.2.0.rst | 9 + setup.py | 19 + src/PIL/AvifImagePlugin.py | 292 +++++++ src/PIL/Image.py | 2 + src/PIL/__init__.py | 1 + src/PIL/_avif.pyi | 3 + src/PIL/features.py | 2 + src/_avif.c | 908 ++++++++++++++++++++ wheels/dependency_licenses/AOM.txt | 26 + wheels/dependency_licenses/DAV1D.txt | 23 + wheels/dependency_licenses/LIBAVIF.txt | 387 +++++++++ wheels/dependency_licenses/LIBYUV.txt | 29 + wheels/dependency_licenses/RAV1E.txt | 25 + wheels/dependency_licenses/SVT-AV1.txt | 26 + winbuild/build.rst | 1 + winbuild/build_prepare.py | 28 + 49 files changed, 2807 insertions(+), 8 deletions(-) create mode 100644 Tests/images/avif/exif.avif create mode 100644 Tests/images/avif/hopper-missing-pixi.avif create mode 100644 Tests/images/avif/hopper.avif create mode 100644 Tests/images/avif/hopper.heif create mode 100644 Tests/images/avif/hopper_avif_write.png create mode 100644 Tests/images/avif/icc_profile.avif create mode 100644 Tests/images/avif/icc_profile_none.avif create mode 100644 Tests/images/avif/rot0mir0.avif create mode 100644 Tests/images/avif/rot0mir1.avif create mode 100644 Tests/images/avif/rot1mir0.avif create mode 100644 Tests/images/avif/rot1mir1.avif create mode 100644 Tests/images/avif/rot2mir0.avif create mode 100644 Tests/images/avif/rot2mir1.avif create mode 100644 Tests/images/avif/rot3mir0.avif create mode 100644 Tests/images/avif/rot3mir1.avif create mode 100644 Tests/images/avif/star.avifs create mode 100644 Tests/images/avif/star.gif create mode 100644 Tests/images/avif/star.png create mode 100644 Tests/images/avif/transparency.avif create mode 100644 Tests/images/avif/xmp_tags_orientation.avif create mode 100644 Tests/test_file_avif.py create mode 100755 depends/install_libavif.sh create mode 100644 src/PIL/AvifImagePlugin.py create mode 100644 src/PIL/_avif.pyi create mode 100644 src/_avif.c create mode 100644 wheels/dependency_licenses/AOM.txt create mode 100644 wheels/dependency_licenses/DAV1D.txt create mode 100644 wheels/dependency_licenses/LIBAVIF.txt create mode 100644 wheels/dependency_licenses/LIBYUV.txt create mode 100644 wheels/dependency_licenses/RAV1E.txt create mode 100644 wheels/dependency_licenses/SVT-AV1.txt diff --git a/.ci/install.sh b/.ci/install.sh index 62677005e..83d5df01c 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ - sway wl-clipboard libopenblas-dev + sway wl-clipboard libopenblas-dev nasm fi python3 -m pip install --upgrade pip @@ -62,6 +62,9 @@ if [[ $(uname) != CYGWIN* ]]; then # raqm pushd depends && ./install_raqm.sh && popd + # libavif + pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd + # extra test images pushd depends && ./install_extra_test_images.sh && popd else diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 6aa59a4ac..099f4a582 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -6,6 +6,8 @@ if [[ "$ImageOS" == "macos13" ]]; then brew uninstall gradle maven fi brew install \ + aom \ + dav1d \ freetype \ ghostscript \ jpeg-turbo \ @@ -14,6 +16,8 @@ brew install \ libtiff \ little-cms2 \ openjpeg \ + rav1e \ + svt-av1 \ webp export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" @@ -27,5 +31,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install numpy +# libavif +pushd depends && ./install_libavif.sh && popd + # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bb6d7dc37..5a83c16c3 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,6 +60,7 @@ jobs: mingw-w64-x86_64-gcc \ mingw-w64-x86_64-ghostscript \ mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libavif \ mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libraqm \ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a780c7835..0c3f44e96 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -42,7 +42,7 @@ jobs: # Test the oldest Python on 32-bit - { python-version: "3.9", architecture: "x86", os: "windows-2019" } - timeout-minutes: 30 + timeout-minutes: 45 name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) @@ -145,6 +145,10 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" + - name: Build dependencies / libavif + if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64' + run: "& winbuild\\build\\build_dep_libavif.cmd" + # for FreeType WOFF2 font support - name: Build dependencies / brotli if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2e842df64..2f2e75b6c 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -25,7 +25,7 @@ else MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} fi -PLAT=$CIBW_ARCHS +PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" # Define custom utilities source wheels/multibuild/common_utils.sh @@ -51,6 +51,7 @@ LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +LIBAVIF_VERSION=1.2.1 if [[ $MB_ML_VER == 2014 ]]; then function build_xz { @@ -116,6 +117,45 @@ function build_harfbuzz { touch harfbuzz-stamp } +function build_libavif { + if [ -e libavif-stamp ]; then return; fi + + python3 -m pip install meson ninja + + 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_POLICY_VERSION_MINIMUM=3.5 cmake \ + -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ + -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + -DAVIF_CODEC_AOM=LOCAL \ + -DAVIF_CODEC_DAV1D=LOCAL \ + -DAVIF_CODEC_RAV1E=LOCAL \ + -DAVIF_CODEC_SVT=LOCAL \ + -DENABLE_NASM=ON \ + -DCMAKE_MODULE_PATH=/tmp/cmake/Modules \ + . \ + && make install) + touch libavif-stamp +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then @@ -150,6 +190,7 @@ function build { build_tiff fi + build_libavif build_libpng build_lcms2 build_openjpeg diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 1fe6badae..2a8594f49 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -160,6 +160,11 @@ jobs: & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh + - name: Update rust + if: matrix.cibw_arch == 'AMD64' + run: | + rustup update + - name: Build wheels run: | setlocal EnableDelayedExpansion diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 8ba40ba3f..582fc92c2 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,6 +1,7 @@ from __future__ import annotations import platform +import struct import sys from PIL import features @@ -9,7 +10,7 @@ from .helper import is_pypy def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} # tkinter is not available in cibuildwheel installed CPython on Windows try: @@ -19,6 +20,11 @@ def test_wheel_modules() -> None: except ImportError: expected_modules.remove("tkinter") + # libavif is not available on Windows for x86 and ARM64 architectures + if sys.platform == "win32": + if platform.machine() == "ARM64" or struct.calcsize("P") == 4: + expected_modules.remove("avif") + assert set(features.get_supported_modules()) == expected_modules diff --git a/Tests/images/avif/exif.avif b/Tests/images/avif/exif.avif new file mode 100644 index 0000000000000000000000000000000000000000..07964487f3cb9ae2a801dfaf63a01f0ab570cf09 GIT binary patch literal 16078 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 Z_~Q~NBps!GIgGP2wZlMPQgBD`{{tjqpyL1l literal 0 HcmV?d00001 diff --git a/Tests/images/avif/hopper-missing-pixi.avif b/Tests/images/avif/hopper-missing-pixi.avif new file mode 100644 index 0000000000000000000000000000000000000000..c793ade1c21da30de4937836db18887d311b739f GIT binary patch literal 5435 zcmXv}1y~eZuwA;lJ4Cu0SC9}XNs&fkSzyVP4(XEaZjh9vnA?zxU4l z?wpx9^PRaD001yrx_Cm(+#r^KXZ&M(h^2r%#LQAvMnLAdwQdh~G5e=IgWcM~&gp+s z000UBx%_|rkL@7l|8Ij2g}B)Nry)H@dWfAP=wI~~06=;^{~`bh3jiP$e9qY+5QqPE z|EEwt7hvRn!_PBjZu|mrj&_d!t<;A=ouJQsAOs3>d}dOJGZg%9@EQL9cX7?!1mpr3 zpDO@>1mtMvWu`GCl#@tJFWRwZ~BDdTH@5NKzuZ(;bs{-&Z zvM##SJWN>@y=nonity8Y7UZC_Z(k)-3SA_)$5C6#Yw=E}x0R!|KE{uASxjFY5c#5| z+F8o6x#${T)uyTxV5hO_pp!9BW(!h;zts4Ryj$fdr_r==&OH`Yx5AYP)^aWT z7LjdIT!a|!FA`~5*l%m+nddS=TuXmsl!j5Z&!Tq@(8o zn*nd2W`gK6w(#OG)&a)6=3KT+rd=Y@K3#?G@cjVoq54X3a}~-?_xe260kjagx5gPW z*O{?8=Pw(>2a~Mz@ur9KO%I13(-O{Rcvop!HwJ)(WuJ7jZ$Dvxazod2?P?YY-Hx7O zwl$d7w3&ZYUmG^PhVPdu; znN)39p0Ekm-#iUkaLcq(0TY+a|JIsdsGAgBb6sPYx<#~)d3X*ISVLjZq)wedQ&+*% zizDk?2H`#%iN)}{C1pSO{em!~uPxp=GYGwR_oySe>^q8a>^a6epywV`k1#J#RLQFX z6!ulSOzRavz5V6KyLEAUFxo~fSn-lk5;lF*vC<%T3=i7~OE^vZCt zBMl-A5 z5doLyRc3n?7xm%`I#J)P{jEdgiQ6%0DV$Eo-`-Br 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 Tojj2C+Idjr^nh=P!vB8&%2-KI literal 0 HcmV?d00001 diff --git a/Tests/images/avif/hopper.avif b/Tests/images/avif/hopper.avif new file mode 100644 index 0000000000000000000000000000000000000000..87e4394f0596e22b50895bef01121f676d731ed1 GIT binary patch literal 3077 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 ey3`G4a>V#pn^&w7}i~P z*R-M{CRCK5B*`$u0Ve0zId|nZJ@MXq&hL+>s=7Mh?|VPh^?B>9r`~>^lk>sJsXM&4 zMNu?ckx~lKrddx>(u#K>M0qiAK98b6z&huQjdFm^a_FT(M4%uH62e%VFS22~ z0Y0Dq;UBv5SAO~b{l*tQytgAu{eho7@QJTJ`Imq7iB5M0oU_YMy>R>BSKjqAzxg+x z{>sD0$GTU3-y44Dhu-+cD|SpYJI>n`X10ixr^lZ=^Y)+mm8iM>ssp$6Gw+n30uyTQ zB{41Ql=tB<8@5|iR;jQFqL@J(APYKS03fi4xfZRYYcTX!2w3m{07P045fv+BFM_}z zC;%)6cNWLA_9bny8a*} z0U!|p5D^ja%nrecco8om0ECD@$m~&J{eFEhh-c=?=5Y`iW1K6)IGUcCz5o6Pzwm`G zZu_2>z2`lDcGQh+zvksvUwz=%v2*!wI2koVT{;VdVG@lkF7*EH10O#0>-o36 z_{x3P?9hrt#DO%!SXE_z;j9}jCptn+EB%#GHjJVuOvWP9a1zE%ZG9RkR8gE&TsR-b z5dxwYLJ7ivh<(Z4IYCq!E&v%K5CQ-xWD!CH79c9#WHvXkB9<7b8=Vi>{xepX)p}KW?5tbDJ34N3cxD>3d!iex?Dg30w4kq0tm2( zD5ch*i2w`$BK1XxgviVr;I3h{0rdKv`n8B4)wn?b@!k_5ix3fSLLNf>f)G)&R3aM) zIRCjw9>D`TKo0-_1cCr4$VhEGyrcu5sfdGRrsj6aOv6qf)oQ%Kkz3*LJUA^wW4gc~_A5?ab zL?#<7vafD?{_}3UVZXQKqmMkiu((7h*~)ORdTjL6!#l5fiP&naG1gyQ*4hw}2#81! z1wjy01q}v+xDgT7q6rZQ02qW=LlYAvCL$mJLcUgVGhZzwE_- z@Q44WF*yzx-FermKKr>ZL=){>UUKU-HywEMo8Arv-|*TOeC{)!`?X*BKPsn_Pkil) z{kM#@Cs%UoQ3sti1!0=Iyeb1jUXbrnJY1fUQU zf&xJi1*`xFghV8cWADASRw)D_21IQDRa&H^G0!SzL2KP;#6d{?!E$BO%BBFEW<$?K ztF48~@#&^RclhM7*Z$kr|HOazNu5Nj>1{vpmOua7KOGcHANbop>n)w!zIXe-|D~U< zy#KE6x$Dhu`%&>V^jkF4kQHY!sStrpX zx-2a!ZA@HQ3K|VzO4CuOR1!Dm=4P_dFv~KnlPD%@hbZoque$T!|Mc7T?A>lu@IQX% ze_OWu5A5??z4a|WxOeZCv**ry-nH}&4Jx{ z;qHI%@bhlD{tI{C{m&nM|4+Q*O)tLfy1ar1A7B3Rw;p-s$hndAT1N=tK^Q>T3u>tq z8!i280ao(&F*gycT|2YC8pdf5W9$5dbH3Aa6`G?;8 zsZV{npJ!g2Qc9dx28{`e61{WHMx)H3@Rd@EosYxF79*pg%2$X~Ri3mD!$^dc#Du;{FF8d)2F7P6RK$b>aoL-to{Ay$}D>7v~pOAczabqJ&0g zY^0fUp7mb%`VZKt$gA4Tji201=7vL_}mnM8MZNS!#A+ zV0Hk2z=(*b06>GdX3}``4}Jf+b7$_q_uhr&l^{gxAd2EF&5JAxqKKJC=`actKncUx zT3fS%Qp%PV2}7ev*n_v$YE4Pd5I16Wg>~Wp1%VMj0n{D_Gc#d;KuCUh>GW-Pyy!ju z_YY>L#`l+bW>1zoXv`j}DexBt}J#Z2DVY(XL-c*D&vE>9n8 zHiBv}G#a*VnK^TI3Bu^m!DsKd^W|{>gRE#K(X9u%d*;6HoA*8W>AUaEGoJ)4v1O|n zS5;M(Hc29H3lv&2Qh-pXR>x}sc^v^8XR`^k$&6h4v1SVaSuqnZ25izg1P zo;@+vOk7r=r>}hJ>)U3x5BsA$&C>a$u;OxPPT$L@CvN8+>Il}5CLx)ufFGweLwluf3tI@sUZ)jROHHhG(J9_WrZ+^pa^c( ztefTgh069k$!~nGkJ3mAkOWD9h=>pskOESI#6S$S%Aqx#VUz?027`r(iSYZs|FtiE z(anGHzCU^K3!kTvP=FnK?@fUJ_KmM=cLR`N96=Bwl2w|St*i2M*b4)4Huqcx0U`OK zC`1HNF|$G+7(pxnOC%PsNS+9SAWR3t?c1l`_r5={#Uj8Ecr-Y7^vfUp=dXVDvqgVd zTz=;CaqlJ1TyHgh&!7H9((Vq6BAK0=xZ=v$-B(|I;MUn~dy~njsqI_0?bv$X{og)$ z^2BJhzjW?w@7$^5&pe(kor$SJscgCmPymj?pt||M#5>;dhEC*t*<+h^JB?m%#RP#9 z_QFmWJd0=W%*^7s4g_p=gEtuBqO(TP>-w2D6b9jD{c#Z|=_sTk>#-j;n()*A;m7kT zKYZlS>dJ!AU@K1oN|Dj>;uqg|;^dQG{n|IOY^jm7iy{~C&87|lGrzFd3dhRIYpn$& zGP;t2h^op}c|piwzyPW6K%mKDkr)-^Ss6vqU%u~;HI0H;@nG>2ANuFzb1Mty7Vo?N zo5#-`8x=N=TT$GoDrZ4`_xJo;(c%4%d|NkXcI=*(0GKc^Ey!1Vk>2;fH(&O$SG@EU zx8ME62W+mILF3_XeC>Vz+kV#_ubADoedenDz}8iaMyfo$W>@2T@4D^p{^<*)P*IWz z8qQa1h7T~H)Pe{%8AOP+e~g>#OIepW8_apz-rfD)!eF>kmU*|mrQcgPcW&^P?|a_|{{F9Sf5od`dFSh{d)_Ux zTX$msNkRhd1<%{}rEh%u+|VqIippV_v}{?C5CYLUhzWoZM0m5&vT-)^#Kt>yL|_v; zoY&7AcF}q9Rhy6h^LKvZt#A9`b~kD?br5iA2ai7Xz~bWBAT(O*EX%B|;w0#_qvzdx z^-ErQ^Yu6E`}05lgP;GopITZxDO@HEQ&uA+J3i6*x8M8viHWYYRz%o?(II#g4;mE- z0T4nUtU-)OXk9ctu?tNn@8`e$oB#9f&wb@1ANZG(Cl(x2zkt`i@s96%^A9#BxZ|wc2Aeiw%BvnLZx!72 z#y40>zIM-pzwk4^^s7Jr%OCsezj*xa&pi2&4-JQ-{I&xq5{8t}*@WC*S(!4B+vXL|+!>@nc?O*%S zCmtP~RRjWTOgqgB5g^nEK&Z|;H5d$R1ns$URK53a-v6U zH@u-k;fi*mp=Q6uikTCqtSTPOJ5>B zT<&F!RRT$6x*3xBb`u_N&g)Gfy8HAK$xm>ovy?KB}k+^zfQ%uKdwAf8RBGXGQWTWMx;f ze(~Nvd9S8cT2?_E4~L5^ezI$3I`D?Oo`3N1ySMM$_R`y4^_kCoTLlGarNh91Eu2?U znt)xNiw+hf`o^ z8_mU~1w#M+_rHH(e&wrQy|=PKw>z7TEQ9~j7e1df)fIc@-ukv5{n59*`Q5*DW!P$b z^$VX%8s@Rb9{b9dKBUk*^Tc!ae(TfM9XRmpvq%5e@BThx_jUi~O@nH5^RCHs=`pK) z>LS94%pyi{Rh3bMt}ICafVI+GQ@02rZbTpu>##Fy_*v_;C0NHf5!D*z2ne--P2?{C z%m|36S;BMk{ey>2I;Zcu_tAI%x8EuXH|*t(d3vn*b8r8Tcir`pZCj_yau9?uJ6DzV zJ%9KI^JnHAM~G%=X?e?*sdTUq8v!2Oar@0A|L%FWzTk6T_-21~Sw+r!rxYRypa-;s zPKm3W{roq+{h?2NycHYLuBh@(yPY}zE5H8hKl&qgbvow#@Biyx{?EU-clY*3AAQI< zcjJvWH#-wQ_jCW5;)zG^fBfSg`|QC(#~*#<>6w|CTW{U}<{$Zi1GnC62a85amh}%k z`N+#({vux#Pe1zTnPbN~ow05!`N)SqxUjJF_Mdp?nfdg0e*1sS&g?EMJpSOsm|G*>LcTN-OZQG`;%~@2Q7B7FP(DJ%uP?t965OE_x{Hp-~RHKbUMu!zTif!@$S!j>%afE|GoE$J&5?*|Ls4{ zOzD@s>=i%yBX3<=TKV~({mPFP}WSG{~}2jP6c9@z{emzi6vbVFezs zjsO9X0v1^#*zfr(pxu$o-wb1GYZZ=+MtrP2Lbu~Y6YW{0q{kEcr znX_UfzKY}U6aVs=d%pRVeOK*$*Sr4nEjQmF$ek|U_LAwBzx=U}e&TbjB$=3L-E`B| zo36j|u2ETs;8Cio%Dv0P<*WIikr;(oxvH>L z7@B6YTRI=Nrp_+(PMs{^ln?_2rNK%a_0V&bzKWaNt89``hJV zKhKtn(uZ*}8f0jT6NeA(yRIL!6A~d55f8|Sgg|Rigmsg269*swAkjtO-mDKU`f)P{ zoYw(i+!jFV^*6rpz~Q5(0JImcOypTs2SXDymsf|+9y;xPoM!l2zx6-ITH|-T;;y&7 z_5XR`!KaTNKcc|eeAtRjRi^#k%FN8n%5v|)haV*}0s)ctt}+VP=Ndiu%oYQ&C5me# zp>?SsMM(r9RHZA~j|QW$i3zJ(fb6jG=(DGiBo=mQnucNMxrm~~=@6o%8fIauwUUj( z$cU!n%l*J8XJxq3r-jv4tNG#2er4CLU2lH#n}6p$fBs*8<<}m4?6Hr0{!96%KI2$O zN92)~5MFE9%Kui6vj)q>qE9(Z- zrPdSx=+#~}Hr`lWP5<#9{<*3kih|N+A`&&40!}NAl6W{QQ8({@@Yzp)_G{@N`+6dKQ*ycMDjc> zDi1*OiwlHW5X!1_D8qErh$BRGZn@uVCG6ZF_uu&T!&!y@=8ZqRw6v6EnTf()nul>x zigjfX>Lg6!ych;iQwwPYdD_$90YqtHteq}Jq&CNS)?t0IIND$fSVzF-vu<%-Abd7( zpquRuX}#JX4$^FLdfR9;uzskFvc59LEH1B3OpZ01Q;3a!_}~XdnE*u;B;UICAyQ%j zOw(at^vZI2?8K>dXZ-QUohzo z0$m8A(hfbLR-h4;h}#Tqs0IHz01@e;KCgdj+Q1~35h0<_ZS4}atXrH|r9)E}+%vJ*&AE7Da}X|3z@1%qcK5HA3g z^>G{sD6UH9yw*XWl_FAFGg@YrHAhlut(##|mL;MPfd~PBAl2a}Yb^nw)FA{yL{umO z&U=j{gu>dfcg_)NrA%d+ts@LjfiOE?sz66UP!t8SD5Xei#yY5ZUjEk*Tqp4Jmdu5s z2{v8xuQ|_~b%{n{0uew01q{Flo`G3}YhmZTw^fzp4w1vSSvZS;fC3~40Yd081Js}R z)Ypz2KH2I_o?SR;1zMd%M9M0yY|&{oz4xGy1sFUCB5&(;<+v)?c`X7PH3MtKYJ`AV(HdC-1GBJzFo=XE@Z!abA|0B5K?I1AA_6O5 zP)C|Pi}l`nZ`l?_QHv2!bxBGMciC*fY=-Q*6uKZlEg~B^;f*)zFsjG}nSB7Lt6(&s zud5tH>dLn@;T34jB4t^IfzjGH$I2K+sB5|E|APU2U8RBW)Kf>=-H9w8=pYVth{8oa z8jqEDj-vno6oK`^2q*#|^#dDHAk0N2-X}^mk`Sb@UVQ1P%9J8ps~vRfSwKQW=Y36U z2*BRfM2{$>5D^q2B3bV&J1@ZEy;D|%0Yzz#gpLI&FB;GXnmj4XQdN~wNCY0dfM;=} zl+q}UHaGy_;ujm-aFKs-(Zv_Nv)LTdC?1(n>H-0U2q>r)esJm2>@mzp5(I#(S>=gD7F3<2Y(2ajOvqp;1buMUG@h zg-)oS76*?Wf8^1}pE`ObFAE@}j6oz2aO}i85kE=?EDnH`LQo2b1VDsIormli<-#Sx zWDVTsgVmSgn9Y168-|l66ar)c6hKDgH6zS9r%iyU)-5U^B4?QhkO@SSkRnS^G7unp z&Z??Xs-snsbWvQIjf&}Zk_2H{WQI_@2k?LZOdxpjbwwjHfha9XDQm6wj_QOms9Hyn zFo2NqNfJ3*1tv_>6|Fgnnq|=sqa;mpZHTOB@F;+uQHZS_Z#Lsd5+er=?0o5qZoX>A z?xxZqRJO=$Su~qTyWI{0O@v6|*qcbjacH8jGirM;#u&EFATc`xHb&_{A%gQ&VXMJl z5UGG!%EEiB!j`^j@ATO0#LN|YzxM4%9(&}`tjIm;3{Zd-5^1diQ`JT7!ib1aS2jx> zd$@?MQGwLfE_kfVsf7T5){ApRctb^RLQEZ)+oS_-z4?lR&z%`oIxdj>{Ag%AU$h*qKXt_qX3tGKi-Zglc=)dN#thNX)dtvs`+ z3W7>273hc@T(NE2b$fQpU}b77y!rWiXSZ}ZEesVw!Obwyq=_^;@q}ayk&Z&Gf<{#p zU{RYSFqkAw@H`s!J7Zk{C~Y+;%I@TZQi4T3-kR+#Ek%f~7&N1BSd8LWqN2QNdOW^) z|Fz@#x#K6l`PeZ<0UFAyDrhG{kq}naCqW3FK`UkfIPXbq2oUBqo2RaAT&qQ_t)#We ztn()QecWnSEMUE& zC^TxYI?B8Q0);d-F}r&D6gw%40tL9T21SFLY3iA4b}A@y4)1^FL#y7MOaJL7f9tp3 z+wO#K|95Y_^4hC@?&tq~l?~h7u`mciG{Q(?j800Q3ybp=5e2wjO%GZ?g9ISTJ4R5X zgt=6TjP_DmrKPAc_7EhkvK&QGvSoJS%H2C%IT)6MU6bwUxd}ja&c%iVAdC{Flu`kK zDl0!KM$S0~X*6QeN+F4$_w1}FEzGdEvKpE|SH2o%BCv&XEK`1D&ch8PhSUGfV>CDPrz7Yp|b5pZ51 zZ&3FIR*(>^rzbD^5U4BTUiaEpKlspNZ+pj2Z{4=9uRNhjk|b&*TeoeA8xf(g6?^B6f-p3Nt%m&+ zW02DKmzSV&%S(&7w>mV7%V*o&4mHP+XveM{3rmY>Sx7Ce1Iqe^4$QV4TlcM|qerrp zJdXoKC>oS=;*b#)djRpEE->3KGW+>7?(hCae`KG0 z;(`0_{d$>?!hn*H-|-Va`1LP+X=Sw-re&5@v)d-q(phncbv<|8##H|wNT`|D>fr>u za|ICyxgsBqb!U`P&STQ-h6cfxaUc`jxZ5yjBzINZ45P5Az-Zx#sF|nzVP(NOKvDrU zLKO!p>n#K{t}$>T&bcs%Nn2kQow0V-Plqdgan1k?R+djK&WkYxyTM>o=5}g&w$)}( z$>iM3ky9tFaI@8JAw6~Yn2V-O5Y0}``W4$qf>3M3pt2qaO%REoa}{2aO}x~~+z3I7 zNL{PEIr>)DC#t%|qgKD`^;?LjyX~>zXjrj$kN@!Ze^aD`iEd~9%&DpIMkAJ=`pF;q zo!|NWC_sD}$w_m7=IXh=`O%BDms;UB)PO zv2&Gkj$1a;ionZSLzT~pblB@J^jAk!rH}?`cKF!oFg#jWpV>;sNr4cyTA}5^)lnV7$}T2G|p65JeOk?>&MNtey9Zt3NNWi2haYck^YkDTTSFghzvZm_)WH zlPIxe6`A1NxwC=6es7T|f8!fp@z5jpyz-9kz3G;h{MFxnV0C2?AV3xnPa;SjHG8g@ zL8+jXbc@mt(t@F=D}jKe+idlEtBOe2RoO7=bg$Sp-3Vkb7!?qZX^aJpQo=GFv?ZuS zQh-ILm%S=00)RMB-LTnhHjbT|w;pGwAPyrXh!twqtx%K8trUad>L42oicxOp_|npe zQwuXQGqbZ>rpG2mMQKTv^X%}EgLW`@)hl1Wvbgf_L-$W@-EN9T9FJ)X+Hp8O)+Xf2 zS1if`JDYp!v{GiRuu#?m`s;F?6u{cA>U!$+*R`yNbxjKZ>bw)D*4{^yI&i8@$WbE> z;?{C6f7iQy_3c0Y4ri;;U~y`)Tju?4EBK9f|I)%jaqzjvKJbALlvyPv074P~6d(q% zz&VI?+mo|1?ZL1t%f9nWq)^yb-WIMEH3F@CnomtMx6h7`wY1?PP&63kC(oTYdU|E4 zx43fpOvk~}{AxxZA%$UVNI6%bcbz00Z#MSq*b*z;GSQhH?*f2l3r3-#kcW{HLc7{u ze)#dHt^zu?a z30ln-&Q4C2tar{#b`oPjOAuBHN^gNVh$685BI=e)I%F;-_6uLHDdCHh^O_RA|DkU@ z|3$YSJ>E+i)6-LPKlgLL@E?ESr?0(c-%q^#?X6~WG+1#Afa-7m_HTVj$V~+?2#JTt z&~P-k?PWL4&p*e^Kk_4QpP1PBU*7eb0r|41v{G!@sDLeac1>fNA#@_RVgK%#@#ezm zm41JCcDeubq4|lKxv&v{iWU~mzWIm0|IWK!v%K0re*E~#$_f!|nVmGi4}AL@S%2wU zU-`Tn&G(nL@87>)DH<#vwXPiYdh_%1hmM>~TI28h$)9@cxx;6chwuH1ze$HhsPt@S zyxVPGbN$r|CysylSy84G=ST4YmK62vtuDwgY{L8;N z=*!-#54_+7&%gclR~$X^L|~#Kue_8XbZ&nB{`>DO@&-}p9VlY}914el0-xRXl3S*x zCih>x`%nM${iESxQE?JQo*l3V5`nfZSM2ufx?;z4IMYeWQ7`THdn>C&&ZALoWoeKO zcFuI)_O`dipzgo#;b)#Z`0TUKj?y7>9kk@96RfA$P%#UP7| z3$Dz1i;J_}wvc(?o8MeX{g>{!cl)lrk3IbO*i@(AU+piS%Z5u+TgIP#>cNkH=tEmN zlig6CIq~e`@{-efCc){M?v4BQhugOtOM{0`_ZrPD1ir8~R5}Qvswg)c+RdcBIr+YE zF47H~jns=cU_*20EW;Bg`-cuL0(GAH#8q&}mU^xK*VAqaHn99OmkcJt>>?YnBcw{q^YpZ&xWk3USvfg)>5rBo0^QlTQ#>WnMz zLvL@m^4iIU>7P2ivM|4VZrSO^fqnb4%F<$W`%7P%4f+zqk3M(w$%BWpEVH&$3c=Ac z$4>V9pSgMeRZyfouJ-QP*FDMl54f*002F|;6C}uuMbmgOqe8- zc|J_@R8jVkfByLEUiX@2vvua&!t~tsAAHlBKl_D;&n_*Sa7+auDkT=8C}_9aNs&7V0s-tHvSaEPbNh1cAE`{F2{m>A!`FE7fRiz3m`4ppS7-AOuAV`JMVr@B#c z{gq2gD}&XQeDOp-T{-#eldF$C{#~stiM}GUPB{TWEoxY#Q4;gz?3#-N{Y9GK0_TA7 zBC3~7M?dl8La$e}T3Z&DhNxB2Y$=xr+uk4(FdzEJ=l}Fi-}~a*Uh&mSN-+-Kb#kibQL!e>s@7SMB00I z(AvrAnSFcrunM+JPfgBDE-#&0I(xbsQt!mkD?3qXeHiOK2lmcxnFC-2e%Ey8#_ca} zHzI|s0`t_fPwao*bvV{GN+x{q%u~<4oxxGxXOwn>JrS8`5d5=Rh`n>8GDP+v|^*qomaWLWZn#o&=h$aiZoIzi{u$Dt+N| z5B|$%ADEmS!+?Y#FSAgaFiOD1&cmrQi`%wsyWz&?zh<%e{1?B;q6knKzDa=H}8o z?JqAiW5Y$hb#|&THMx5Bv|}zS=Zj1wjd*P8n(J=LtnGB$tAim1eAE7Gv#Q*)XKOQw z?I>GaT3k4DddrqM4D`yVCq`MsBDd^;9A9(A-Y|}rSB6`rCP*AY0)R>@6My^O$G`Tqry9we zaC0zN9u-Te&0Fn`AbDnB0SC`MckHQW9&WW-&%d=lw`F%2KY^rs*-97(qJhQhFwg<8 zhchQmnXV~^dH1UBFddX-9t8;b%2<1RrbVXFq^YrTP^wmAYIbUN=e8?$?*PPBBT5YQ zmKM{|C=Oysnu`mob939q#-}Lg6vH$MqNp|Y%wtd7dh<;?uiYajjvqY!^sZeyLT$6v zK{-ramI}Dkl|f=Sih>|v2cmRmOa>Vzac6et*1{fFWEzbYh7p4q+6qm;1O+<BQEZ>fqs}!61pl-8bDZx3YBP@WI8!#nr{7Ru~V5 zLv6yUx3qP#6=Kw19EdUnAv2G)$Deufuva>2v{j=)aZ@%>=3o;n|G#*= zw4SWLAR4g|j^^_?Sn~rapOOyPLu49SsB#_!k#&~wp+l#Ln!u@OtX#t(iZYbi+CyTwJ)Q zL4g30tZNGPz@C}eqcAbnWh$PE!*b8gtEe~E-fNpRXMSoBuS?;UJw@OCbv#EI*sw!ZQZFYQEN;! z+7M{SGFRq$dS>D2r}kX4&%}vG&_xA}2$*a>n%jNF>hkK+((3fIp+*G#j8Qi_<6=zE zXedG@ob`uUni^wBs&nT~?7QM>GYzMXoLE{|=@8>E2sSShYO3jr;t1+}O+ z2*o<8!$crekrH@PkQbR{fAbAhnWWvws8DCc5J6c0vaI*AdYmmQ}tHQEY45P&Z#g68;PtxtU`jS+A=fy#AA=I zE-u3;htiP_fw4^UVw6@{o()G?np$h~vRYbMZFk0i^wrl~vvc>ZcDuQ}vg%ZliOC&S z9zf7JwywaQ)V$=dBu&`8Q zqja?T&2N1J*_UZ%C^SKg09Ah&G(w7F0BSZGWnO&xU;gEJH{CowIgVOcUja!wX(f$_ zodX9;=~9;GOgaoAtxeE~4%~3yx#Q1*;7EcT6Ng4rP*ZBJlFSFRp|*ACp)^(mG=zT=Zfyde(c#05p-J8o(gTj@h!3jEIV-%OJUo_%~0)=rf>i1jY3zbrHGqb19EPnONU-1r`%@zou_*2J^-F4TUTW7YWdFHHD=?HZouB^G= zIY&k1tlf9z)ek@P!1V{NZ?&4Gts1Qa6f7?mR)WxtU;(?(~Un3$A0iY*P8a=Y*DljOnY8d#gm%iYRm%X@AKJE+W@>CHQm7iZ; zYTInAJ*`Tc7Aa{`fpLiJTo4+~n-`J-a$nQ;Nb?qg~|XsgozNGM(Bod*bw| zQJHrqr?j|gpT_G9eL+4yoRRGXzv{P$Q zk$3=r`NeZP_v~sk<7%KJ>M%8dLJ(wjPCS;*dkI4Wmz-L-Z=PCq{(ZPOEU_tza1kcz z+EDL+07Z*pTF+jt5BC#h5D~z&0f{IKRsdMM7wu~%@yMQ}^rbR}3|3XH!Crr0-|N5o zRaL)N@F-HEL6k6RTI#O`f$EP&Q!~@6gVnUivmz&fFo;YXF!;2ztJx^j@%ZFqE9qz* zoH=#+=<#E#tIJ=y`zvP_=1mf9+qGLqQFm;@m=IBHRh>L?EFX@Zdi3$_b6d_FJ5glm z%=Fak+#DmP#V86QLCC8-ZX`vPp|#Gr2kyWB<^u;Nx?_iqoQP)kk9v?P6b(vY7-&TR zsGM_`g8l-#ev>#_Gf6#M_IyVUuvOk}cdL?JRgO=!i{&18&;dmz>aV8lPB#d`s`4zh zs;W2)m6E_{uT)m0fL?22k&=tRM2^eAkW%>Kn{WNT*S>PHnS_)p7H3PN0zw&%MhJ>Y z=^#3M_^=aq|054xv1P|(lz?{%vC(RQWTmYFljLP4g8h~L!w){Ry1M*5|K{~aPaJ>r znWy{`oLN|mn$2db0|2(FTv?5G$5s{=ckkZ)vfFNZ?12X!xc9-%Sf{M2xEU8VZ;f@c zB2$qD1xjJJ)9MdKNs=5oa_~eDZQXhG;oP%{P(=U;!Xlon7gCDIvqMxHL;GZ{I1Ukj z5bL2$8w%NZPj&w=09=?W-Vj9_477%TZYwB@9$PaxG2UA_XOu<=`u&mWbUU3+Ri@Tj z6QFmVNLN)A2o*(TIY$y8FYF1a(TIwojG_pMU-#NOU;FYG#atQk#14fOpe8|PWNEcp z#EtI#IKCe6hcH&Uzm>KQ=bDy4*8?a=GoVF0Zci&YeAd;|(`=+nsB! zxn}njdzr9u?%&XpMLs*haWOgI5#&F zCc$Vl^2DKzoO8~(X1lZJihaw=i$MrWy`kz%r1RQ~S|jZ`V2nb-$cPt9b=ZLEMZWF^ ze{B4?A)_wU85{3tUoM3@X|%>Br^d^CWpQPx(Kh2_V?~y!BuN@AMTW((6Ay@hAmF_R z@F=X2P^`DPMom(+lGacE?7zQi&-TID<1-T-<16qESXdk>E$qZ|v)QVa7Drh%J2{yY zNvqQiDT=qnqtVDqrgdPH4o#5u2B8X;&z^nesb{xO&+gr|+ZCmAz8NQ@JR57YZn)~2 zozrtmH?4$WI6Xa`=Xs!2#eR8pX>Qxr+irjP=RfnAr=EJMEc2wDv?s=z-4=r&p$?RD zE>H7LXZ+;3qwU#kOGUI?vDc)81r)9&z}80kZ5~0k*$3WyE|e_1t}?K&r1mi8|q{0rcJ;eiXK|7M)6eqZxZ&Mqxm# z%8Ho~3@WeK^5W9+s`%YIb{M7EQ<|0c-1k7I-RLBZ-l#vjwOjNDaXWtY$rH!UoY=W% zmp9}CMS+gn?Xq&M#)Pe^B#z=vXDrJADKdsZ=9ibWqOjX$>w+M-?Y7$hcr+Mx$J*H_ zRZ7K;C~mh_Mx(`*)vYr+Y_v`;%=ZXVJyWQT7bNkXgh2?1f$On87mq5tJYAm`FY6;K z>lv_hT&NyGx=9KN10Vu{2wi>ETr=V0N1jG+!MkP>yQ+^1Gw+t!YNQ`wU z5(ps^R%{U=P!zHxo_CIoz3P@*_fE~U5RJF(IP$j2iZn=K3XBi}AuAC?V~qEbR@MCS z$~W)5_vo<`{r=z!cYpQ$ANuFVo_%hmKbV-Fa+Vv7hKQ799!1erS6!85qbQ0Pftjtf zK@gyTcn?581R_BY7@+C#iLp*sug%H8Fb;0G;RX|^-8*+pPfm`Fby|&Pzuy;;M;?Cs z)1UtQ(PO9PS9%b)Ts%I)Lkk}w`72)W?4g5YH5!hFfZawCI$M-Qsp3#HAu@OlN!nr5QKn0j zo;|x`-`?D_&x^WAh(a^$rvwg&SYhR?rVtT>u+g(i!!&>Txo3&hJ@-C1Dqv8SX<4PC z+#roL+qsvS?s&XYH%v&R^8PR@-N_@zv%HLg*q22!3U_VW-U!3h<)y)BxH=q!?Is#k zI2!~(x6{c+#q{*__U+r-?e=(gOf;~FLR#nz&do0_pDTJr`O-JselXPwBQ7wC>iNz9 zjI>6cpacvqe)jO0v*+TtL7+&0*dOeU#bL0xFzQ!Nm@pStwy`5s@0{Hl|eo>F}vf69ou&8EUGNadyQsf*$=a{pQaQ+>4)1U$4A++ zZjk3j3DNSxQlmKmjc~}W)1H|QCeNN71YuHCMkpjP1VVzIz@vC^TCH^zZWO!K*<7VG zul17FJh3Kikkkgk>hZX1e;}KaktploYyGKDf4w_Cz2~a^d#=3d-UlCu+RfF`@|G=g zgHhJ&9go9t&rs$r3`55j!D|p<782A-c}2`5dFaaCspDJs?O=AmN(2=Vu@H*}6vv22 z3VNf_^2)sS#h$sb)y3myPY#b~(3l)+MNJ)q>8NCuvMOwu1)9PjnAtYFvaopd{;Rxo zNf;9bS_kdcShLx#O7@3nIJ(5w%5;s z_FR8ZuB1b!P`45v#28QKWlUN*%dA6@fS3s!0N109>j~d@IW^$D-)sRCHtWgn!-gmP zofF8L5pbRZKJkUGe(~-vf6wc`?}l4n@Wi3RtE2wZ)b#UidY*Ui=z~uHdGy4#3>u-U z94Y`(p4p1m$VdY00R+82bM)B$^p=>#gLv^sgwCUNAStWTqDeswOP^KQjPWyxp165? zLQm@xQu=v zRR{xu67b$yu&li=(OQcn%_M2?2weZF?cE)7x4dLE|Khiwc>3_rdssfVJRDSG%{VX8 zmF2~;c84ony0UU*qa6+US%0+R9kSq=g>!l7%gR+nS!KB~D(SS0!SPNsw`E2fljV8o zflyafb^7#Kg~4DoEuyH`AB=1nb;nB`4SZ-<-Rej~Ck6@>iqxaLg|TielFQe=Z6@l= z{dk$Iz(vU0{3G=%@nOS~$r&AD<+*b910VRn;{1HO-LCgaL{Y@-jY3l3$pLwd9QZ16 zRRcvU0AR)b#52$0YA>mrW=3EXaO_Kk1vs&JVDkpJrePqgthGhX!=>w{^?!K7i+|>g z*IYX%=Z>u|oJ)s;LL99w_OrBFT^%+W9q*;GGCnamIW^NBp8&1eAxMGHKQBcCx%G3pX0&Rpr6q!eXz{osK5AtW>7QNzcWmJ&h`I z&H;Et56X)sRD`IsD8*EFO7fPSfM%FLQgG{{{TBt=!(vWi0$ z=!UKQ=f8M&S+D?9&ZF^OSckfD6@w_@*t;gcBvA)Rkz?dQndKA57oRyiece8tXwV`W z9Q!~Emkuq5EZ#F0rEsOS)(9ZpuFf-=xkmFP*X)U0&y9+F6ikjyq-A>U++x4q-?d|> zA+@@?y0YAll1PK6vJ%U^==e0H#{gUwn zmqvRw+pp`d&ohlC9af`4tFYN@IwRR=H7|PYM)UB|BS~{isg`9C=N$kcIa`tfEQ_|l zHWO|KGbW7QfkHs>d3p4~hi7JH^i)$4#Y7%FNfnZhC{&)HV&x!I7$T*El-P@6L2vU^ zRaMg$VO37FCzqD{A`-OQvokHvq!dlhZbOkGuPX0n=fYpy`w{6`E~PJn4e*+yyA!i!-72;1P+zh$s{y zAO(y_%)YkcFT%lvR?XT4N~z72yc_&*X)tbsnJCZ8cDoBKWl;?K14Qg}IzpJVJC?;G zu`#|*6C)$}fL%~z?W){j^cEAgyr6fafSnH!&pmqxdjpdfnw^M8apFdak%m!Nd8&LP z2x6msl{;SrK|opoVvA)mOLfTm89ZOMJ4|=&bI6c3ZTCbx<0km`90ToR_b~-6 z&>?BHHfy_{fq_(S$=RGtzX*JYSX)6Gl4nEHUgV~2{AzP(X!Frp#8;&z1n;6S@SIy~ zfshDUo7_22#@7Q#oQn~HBHtb-d&6)BsZr)=%ThT+QGnz)R`mG2_gy>lifrVZr_o>~ z42P4`GmEEIhox~Ap(+~VVO9bGFgRvw!p1OnQFn5sJbGqf;g%wsoSm6Jd%BUti>r%a zpjTFx#>dCvX5^vr-WTOi^%)1)X^$yo`YS0a9R`7-09cBAG#sX-gO%0(aF`Z@-o*HL zKhH4El=9BXp?xCYokkco;{%#0Ty5ZO^Dwi8njsnUl{k_)B&)L zkBfA}X7p$!phOga)T66GfCLDNs)`y7kucA!unQC`;@G=s9n6v%7ifx&*s+LVv}-jh zq!3RYJ34jdWOMhHfRd#_kH_*b293pF`D|5+*b-QIZ<~|N`O}BG?bsv%vl=m`aWZ$^ z?rnSao;!DJnC4|!a;Gg44YFJZ!Niy;ZB-P7WlvckB9&uXNtXIXvtt}AuJ%Yu5jV`r zrPaY|KR{9rVvGuAG`*9t_iy$5uPt_Wql$=uAyKduZX{iNpW+k-vV? zcfGOM8ZVI90kdj0rn0;b2&Ak+0A%121U(?YWfH>mIiDlDM80n}hc1dBT?PTPK9Zf* zlLZK>9-e9Q!dEKvoQN|n3o+jon>nSr!ijhV6@aftv@;`WC8+8i-BU*n9k}H>5#Nm> zC7x}{QZg~4DypiqRS-u(1Ln^yb;l>>bA7f9KXmugGB$SLr7!);2mWk2Zca|lo;r2z z(7~h63{SS(!C0p~Nd4r*ln#TUw8J#(tq#a&qoX8oGC#21vM-es;EJrsM_D<)*jw!l zyRG2XTb@5LJ!Ktxgxr;fk34f~`LDj~&L7;l=a#fgF~Bl+QP{Aq9_(y!ZN@yXkTA>T z;dTKBY*;wo3CeIWb8Pa21(AS2zzZ@$T_uNzz@kAykbqsYs#;J@x6~D#W@G3Y-UV0@ zI985Dm=%*Q8H8EDiRNNuH9I@s4Ymqr%{CHP6~_e?C50uI3N;CqmNSlZxc$@+|L}tk zEf(+-@A!d3#}1!5b*8&>4n$qC`^qA>_ul^~4{TQS2gRvIYmk>N%gf=Ys5~c)W~0$i zYB?PZN239;ZN<=zO(P0b5C%awF|JBG8J3xZWo4ozLY=qfHRs)Ial`K0ymAK>+|k@_<|qsRjs1 zYY&0Ti?`S7;Eo|9BS*IBkIV8uz4#6XN0H?GEHioLKXve01>dclacAh%7G&>nV z7`IwBO_$45yi=%K%S(d<0^8Ve@8Qu0KmEvx#gg>?>z}u{aQ4K>qc>l%tq^WD8@qPx zT<)zpFQxUCVX2>=I=h%vuJnLpq;CP26^O96m5UVUsd#p(+ikZ1NHE{AYuBzFyNf(; zx4Rj$F~%6v&r%b%*!I5k=?@k~{`{BSWmI!&YAnkOM5Q1SvB2O79Ft=o5Pl~d)L^~r zkhsl=xg2wB<^UiM2;kS2L)KVh6!2oprfN(!TN5syQ8-@Zw!ey)kRl_#zRCeq02N`S zFf>Yogi1GxxS;?!`ap7!koyW8+U1=3UD-N2iW;Y49%#Ll{ zIvpFGrKt&p{OF$W+Cz}gsgVH80mA#et%-3YFl-Lf-E z)Y(&Uy43O>!XT|2fHp_~V1*cbiLO9#!l6p4ET4%RemGoNT*~v(3VCe1Dqw09qDoW) ztCl!9HVWVOmtRT+Mk*>R69!S94dYg8e0=+;%qw3B2@yBKpwo=g(V#m%6D86CQ#rr9 zIYWZv+>EHI8d1mVntlbQaVfN0sR=YhrJ>_h{NMOvB*WLJ)k9|Q0jjXV7 z6tU#aXU#@<_?i3u^3Trh+yBBByy%rW=~C20AO=y0Mk!+qSi1>aRL6P*08y>?WS?(> z6p_to_svuX+0j4nJ+Hp&)|U}>j% z-+VTQpn%wGQ;G5no-)-2Z0I0q1>NqM<<)MhjcAN3g38S#Ev_mRgr&99f!0Q~Cc6XY z=51css;Q`{eK)XUx|(W+iNU3nfi}=;g<88b&ysj@dgqm9ddCV!xo(nU=PD#rsF{5P z!u>N(e)Ch0eD%|FJNDl4{FhvJ;|m(?$zhgbNnz5k1m2@()>;#355mqnVZ=4_pT&u1 z0H!+3jJ0MK5WSw(7eEGI&%p8kgkTUAhyoz3#M{aNK@e(Vv=?yB?%FZ=6K{F*P5bu2 zAag$5bJfn?GtU43ks1}bF+qT_A*B^*L5)=j&gG?vgTzEoxs{?SG3Zwkj?F!Mc;y?< zo_z4p=k9xGakT)iAY}z6aijzlARswyM7W;U7I@I-mU?qjTZV4MWqB)%71Hu>Xtc7{ zDOa__Kr;w(sRPqZCW9bwt`(YzZmSuYvKX-|I_++^9S=r>_QYhR_0_k$aA+FmmirE< z9uQ3of+#EsSV^J48WW6`j(+}=C%^io&t3n#7rpo;cT7x7udFWXs0l<0iN$;80F^O1 zF-hsH0FwZLX63~5x`?SqJ1Rt48*wW2jK?}xUoX}Z0I1ixXkcW|Vnht)Hgmr0G=kT@ z>Xlbtc}1rcFR!flNF8|5tq=89@{`9YFdP`COyw1OaFr7;1ZWr=aZ8m{6dVK%)rxF@ zUwY`VXO>PrbNn1@NDHVKbgVFFDo+lGKm!1XCj{fcmX>lKwZnz8i}SNf*_L!}Vrs>@ zUVkYqhwX`0QF_nTvkl|OGKVDNKob*VO#m*kFfef(E2X63*r;YBENqo@##|8X+;=@B zW4?r8zfazZc&_K3ZUhM!5IqTJ!(I@!G?Wi~{qB3d{N?Mezxk!NzhZiBXUSd|br6C! zj>Xy{s|wPJJy-;#6rciH+pl5E0uk!MB9wY@Gl-xfEg%fMJ~9vMSzOwX_P|!yp|5-% zYH26YOP_ypq4dWu7nWc&poJgiCpG za(@0CWc`IxLt;;qhl5KJwh-lXKf|dBIDry>`E^%6#acrA+Y`^ikayWeAtM?=l8mCB`lAsT;gmBJRePC&rXKlY4TI)F1=%UsNR|P8t zF^Vz9paB%t3IU)jb4@5*k6+XTib&6&TjhPhFTd;E@BaCBo;o?dcgsYage!wZ3?MWi zk%&-Lv1g1physe0nF=FfTM>KEQD786#nxyfqNvqA*~@R(f0GXyq)lG9!_PhwD)hoa zBvQvJu{H^n2(hm!OQho{(aKb=a79L>duQ!O-v2~*a_Z&Z^@^Krc|jOqQKklz%PK}f zWk?wfCW@!Oh@#fy zq>ZDo-MgL_C5Mlk9GzWZ%OhK^`eMmf%VL+okAR&wETEKTQXUDEE=UP!k`RPEu(g)i znE(|bi)AKN@AM^c%oXF~Pd{_h4c8rb{!736_22j86Aw?cLhl?Yh@xPWj$++*O2RPA z@*;`?^wcpX)>x(`R8|Ghj6yFq&r@o~6h$ZImv-Lpf{AUrd!+=8=D{Z)%F`^2quR=@ z|IsGPMm$GaDQ$qQwH1hjWGJMg*|m;En%@*`jU(r0(>x$3&>Z`yIil}XZJ0zk38 z^p1hSXsr+#1%zaS&ex0|srMw-qvd36b|M3yu2?Y8L;$t~?@P|YRx^qMV}ucO7%^j| zL)9D`Z%>##yYD@IG-W$-)reh=P#I-)z(h)!s&q<-op{H9<=-HDed!?Pm;Cp}YN8kOed(N(`ZkuWJ)5WTvHIpU+iT42Py-Sit zl~+cCLMD(vD>Nnwb!i=H#X8IwecZa~rMFiqVu`Y<`r;SA6h)D%cl3h51tmNPfFk0( zN3_NmMd&??lwv(HlJkfvAaGgl(31}yeeOv#(Ztl2x$Qfy*n9QN?AD~!rocGwsBD|ioVFnSX4OS5V(V~eNyl3(dDvcfxB`xxq?&K)V;$}C`SvOmSHiw@( zboWzFJzHd@25Z2p&@1vFp22$wi5v@Qt&I*5tEvivAWes@Rw&|al>>m$#NIQr)_Q#_ zx;SAWjFLEWRnTtaj>oRN>Y7_$aL@f;d(F#ko|u~JFE6;fY&4q8t|s0vFfM0QC?q-v ziHN~hMON6tIxh06H=4ieO+OZlPnQ%5Xoc$j`|r2bqap;xdV1>mf5r)T6V*nAtgR2{1ANDdV`T(qgg7SLRPXcK^4%KzD3%-!<3lzwY|kxt+N!2}1xO zBt~=$!XRs_P3y4-yrBa`M3d6iR)&ZH$qNXvV8L90s5oX(40!a!+2co!|K)rCyg%v} zN;?FPAVT6miD&^7QY9NQBJ9~Y0Iwjm83qDafmo#kgut@`B-Csnh@#3_Map@vwaJQN zm=4J3K~Xh2-8aAUoj?87uN^)+f91@0C+V&(uh_EcwmWIQ92k`j2aAjIQA#;1H)*3+#8xE@5W1c0U1EFOWtajn(>P`I98zCOa#fDi#8i7lJ}s=$OwGl)^N zxG*1v1}aySRb%V+-+s?~{_NeqGTUJhX}895?}I4RT8~DfW}JBMkDoqcjA4tdC|MZ{ zcZ_$vG6#>IxatKjf7Kh_M4jo}QKQvxz2U!n;^SpmRMwMGur?fcqx)A7wIC1~AQV)L zR4)SqK>}9wq684A$KxUaI1P#jobzHa&?YcsnV}ryEA~?#{ovi7|NJXoedo2;-;mqV zmsJ>qWtE`{1=RX@Q4;pGCLrNTqi0(v0fnTLDXr5cnV6hgTpawDUwL<4xV*51we9h# zqOwMGcb-us__%}9LRpo@CW?+GhJ(mK& zgdV}I1+xKI5NaEN5RE_p5C8@&iiJGXdWZ-J1;jJ30Q|JDxalT7qO|>-Hdu8qH>>+bwKmfm_|Nvf>Ic>W=GHH`ig`>tFcw|E@dJVRwc} zg>hV#<;jyLpL*&kK}3xV9#IjfdYyP3d9D+}P8_dC{}GXq07w|urfM_P?Q$Z50w5mP zX=0-g#ClhND-B8#Q9DBI({iwQ@ac#C==Xm6{eST%gOvqEjKatQEE_Y;*2=kJbjYPP z4CG7i8~}TIy4NnlUL*hDr^k0@lo1gM~{_Q8!!Z34iEo=_Abp(v=IM4(=O08}sFV1#-ZhxNih48R~>fB}I} zA*0K>2^h%9j+D>Oo;>umFMTc`P*N|)S_{HSy=53d&yKfGojXU$fY#0z2%?m+rTfIk zJ}C@m&Mk~hP3J|`8}uWCTB(9P011l{Kml5J_s$RY5@G;B5E20rW&~ezeeHSxAtERO zKxA-UBv9Hj8x!Am?}NvW9@}%}-eEtB8<7qYaQQ7i@|LBQ#lxo$?cF&yJ=Haf5CkG%fublr`iYM#6EHjR9+Vb9K>^PKw7#K4 z0Kgc9s1T$cd@Ia;t)WigM)Ng8T{~1Seew0I2tp(UA{BF07G%pZlkV8~sB+Fa0D1b! zN6wx((Vf_Vh)U3{MMKvP-SLtaj5R_5>(Gk^j{pGumHwj-JQ8RfH=($xJUGWN}~cH(t-xC9s*Q%G6<4r0)-yM30J}uh*RQ~@Vc)? zb%nG5v7iTZ3=Yr%dPWcAfvli)+-k-KD&*Xj18_wYkXG{01NS0%lC|(P0064`)AXzF zcV2qCu_gaVn}Z3c{zJd0LaoMp>hqXrj{jC};rTYkuOLAN}(` zuABFZ)wvQ}cb)2_qZeRdBC2QBJ8Ojy5ls-J`N%{;nx-0ctJ|FK zt(bV?;4_cC`p!43#fR2{5DX#!h>v{o!;d`irn42M-vA;jUe~o_+2)UoUQ` z5hehh0YEgd2#D09Uu%sbc)mn?;|c5bS&g(&TC=Xk)=!4t#wC_9(~0tUQwq5y{oFK2*cRIm^YwN%^(l7Nj z>q<}@OQi@k7)D^RPNZbxp#9FD|81SjdI~TM%W9+pRh7;dquE#6<|bcw^Yvf5=iA0a zAjoXaE-bn=2i+sAMKBPQ2S&#ASc+`!y}2~+T<~5 znzR@gfV|ZEPnZNqyha3ZPe1i$U-q(>Ub%OVcn-BO z#ta5MxTK)hZz_Y?T=%kOPOVo6UxbE@g!LwR!OP}IYs&K8-}m4HgLH(5VG`KF=>V71>GdDaU;&E5l$F`>1NLPO3Ltwwy!)qA&YnFaQFkrK*!z3OFv{;!|`E|EnS z{kWMlU=v$x93u6|V~?GkpYIPx)_P^&o$q|--+$l(o;iwRYi+OBD{N7^()y~|Zk1&j zn5fliB}tN|>CvM{fA(j8ZfR-xAOG=#-g6Bm5pm8TDrUZjCN5=&izudsa?P3-0e$0K z{2_uDJSj7XEsNeG4}V*0MsVPrQJPt_3W(SSghf6q^L+o+S1Tk}+0AfS4`iQ*;HA)q zO`_(~v-yGzCfNMU8^2dh$A;Y+NBh>`E2waVXZwb zQ%WZ&UXg#`-fuKQh29!tG?_B5q9`HnDuuqvb8pM-JGKIH7zW;Mm}_f+s0-U)FIEEp za1k?pCz8J`m2Lcrh#Y$$yysir^uoXVpMLe@pZH{+=V_YO5I3P#hOD*B42lX{kygs+ zD2f1Ja&q$AxpQF{KKbO6bq9;qS}DaKq}7J$a#=#TRIAB)vB^cOzX9}(>o@;)1B>fn znfiE*c4_I<+0(}eog!pr1O-485P^7N-|e)bz=*R^5LjD^h?jruT=u3n-z8i^^_wZE zzR`_hrVag;B+1Ik%HhL@-}SC{z4yKEMZ_=+>klC!=bW{+o>*9S&2Ml6YPGVmGBq_- z|6h5YhhdnesfgIhBcio-Go@d2&l~))0mpU8V>cv^zvNu11I{xuZmb>!1h!Rb$Ugqa zy_#6O73bqPcGi>hV9KwQWw$JWt*o& zq^8ZymG$)pR@R#!s+|415B$9#=)e?35d=XT$3YMnr8TK$v&qbH98OM-w_2_7@$t!- zSwt0u@hHvKz@f;oua#jGMIcmf8{W9p&2njN_9t_#Fc48|U6(S_+Fo1ly%q6LrxOV3 z&7XBBvVMP~P745JW7H@rBGLp=pn%K62cJ}6>pjB&;6XH!7=70Ax8-L(+uY29z zy;n4w%^Jk$004$rzP!BrrMtiUaD;~VSbQ{NM;$>#G`CLp4KzeulM2-C}3 zt{3UK_4ger4IolCY}Xs-83Y;GA#pZZJ$mHeb={pBA!nWsv}ZWZh@qxZtusUPL3`31_fLI@O1W ztaHecBngPGs!A&wYj?`BeBE8I*}i4#j;-4O00m4Kh-Wei{92^pfd?Ka%TlYrSw0`> zM1=Zg1QD^83+oMdb)`nVg!4Qy)}YT$MD#kxUxbF*5V(})m>nRm%}Eixwq^#9P`pT_ zY2nOiA^`D3irEn&`t@$V<>h4&tX&7bWXK9$XntLEHoK9V;rpG=r4&@Zu8kb6wY64j zokX#(Y@X*kckaCHw%aBrCsm-)v$wW}ny=h&I4X+bzWeTb>Zzv?F^ZyEg>2ThmwlGa z*KAk>8?=8}NByGvzxb(;1-wZ8eNFIDT?i^(xFVFnpr2CF~-*({IYqrj9 z0WVUIn+0aa6zXc|78e))`mg_5M3hqA``Rt2IY8um8R+Kw+;n3X$o5Uv|Yh*(qo zMGtim0kJS*jfVBQueIEZM*$-8%p!JWacQPCE8vMV3A>Hyn1j&>Pys2zdXY8|AOWt| zVT)gjIm_nnxGBdC7s{f4oeA)gqmYzRMLH6ZR;v|;vGs2I&YeH`lRx#s7r$uh)~zZq zpwO2Uuz&|Ny66{YkDfSw=*R>2-WO;ShGAKjWVFzl{8_hXxc<5z=0mlufg4>e=vml_ z2!p3ONRFa5*oZFpl6AKifk8ZrTYKI*H&ydAcn{(QkpitE@T^Jodn<$>o)L*$o`VB zqL={1uZ0Q&V_@A6xKubO0!3hqsilAkqMv%_zdvx}4Kp({7>I zwS;0c1zH=WG?7BoL{ShJQiH*u7RqrP$8kIu44Td6W}UYA*s6@8s4R-AsytxaY@IoK zRw-3g6)2=20QMq)2t`pGIdb&A`ycq^r#^l1%$dBZthGuhAXSfbud5JHE{xPNYB1s@ z3F0USfH?|G5*Y2IAxLGFh$>e4iC3Y6$YYCwfH984lB>emv@C!{34mvDUYr$?npc!2 zROHu-LP3k&rBrRhudc2tr9{{$ z(jwx88NphcWf`vLAeqN!}K8<0tp&s&2rNsH zW{*Gi*zu#s+EF|;){UDDw7$&pa|`ni-go~ak378C8=PEP1Y?LuJow6MQ6gT12zV{C zg$$qt1%zBBaS#{1Sed{OwGyDU#tH#KtxT}>#(n?&lYcn`&pAgJ zpsk`P$*W8V1sw$;gL2HukWyr;jW7alY;{8fSsSXt%)ZxO5q1Jj$m@CmIg#!UdtR7` zTs;~c7%p)F0bpfub#8hti5iW#iIoG=M-ClbT3DW)ofQvxTC8#}2%=7>({6UgI^#1_ z(}J-7nrnb)a(wDzpZNIl^0HD&hXGt<+ts`2K$WEpf`~{5h$4P88a?#D16OR_)?e+l zqbSeQ`T51KfAgM4A9;LgV)C8`A1R#ITBBnFs>sXGgsS#LkpPfM1c=Eqh%mOsJEK7+ zwlV=FCdh{acKq7gUfTQQ*SA%Qi;4lD5sOGzLed;tDe$&iU-4(3d;kBhtZVC$<2ue& zbuQgKGd;UpQsi>Al(>;dJ5U5hqF`HzC}1r_vVb56U6i&%g?Cb>Ns;1mxfky4%f5Pef8B4>Wun^ z(G(^~YYK`GV^a%2L#6`BC4zH^CK-ZGgC3?%1Y`+G;l+&gWPdaoNVgM%BTkOKM4KC1 zgc=nc=G(*IFpCcY01{)b(_LCxs;cVYA0F&Hed?O_n`ghlAi8v&pwr!c&YNN?qxpIY>H#gRIwx10L zgWET6ZEtM8wz6`hKexWVjzGwOL6WtG;ulnVgb~tT2L@n-;be$L7UsJcT~&{BYjac! zMTuk^ATotE1qo?3Mp@Se)9D3}vdlUXx~xd(8&TR0zO`U0*a=BfT?aQ6}r#OFA=dnH#a}Opp-dz z^5w?0&mQf(vi$0J{8F{Fy+;a^{2OP-u&+B1?SwOFCIMl;)|D0E}cF5&A5k%h-yO(8-g);gKKS7M?|%N<-`%6HL zY?olF!T8k6%Xhwfc>0|)|GE3{t@G#p=C!}79@X~QxU=~rR>P)K zDYZWs+`D)0@}*x}o7YXPOddnv81p=L-ZOwA4I~VbTC=nyU1osEa6#EWS~&K{ufOq^ zs~@fY_21sr$A9@B7hievPj5VYu(bN#li&RISZ87P!NaBZzjgJ}_>~VY{NxuuIy%?? z{(JA=t#8bkdi38L=Qi>%6r#aI7blQrLP20M5sRG`vuQPD7i_eE6+4=|Ef8HKIyiG{h@c zt_+96;QZaYckkc-Vs&-(lTSWjX2on2dEX);5Vh<;BOsuq);AoYB{WFrqGISE^t`-& zw0G|K@^Z28mYIhZkJOacY#gY-y&}(h7Us373!m52$I7tnb@jIA&1WY>vbySE$g9aK zfs3{EaEy*0U&?$iMmfq`OqmDi`Md!HN_9QZ48X8j?d|Odcm=G3FoLS8rfHn#nAVy| zrBaBdc)_&O3=nJAe17lu_wGMP~Rj2g>K&s_fm{3?a&$55{*B^cR{M$Ef-26Qe1nxpW>OZ{`F7mxS4RPFrYAO7*wzy0#w z+O{vdyL(&bzw^%ZyVojiHbz^g&MaU2uV0zGxPJAsb~MJVy-Pp-N#}`s6_p+BPe!}S z=9{-~Ki$4(&z-+{{p!d~d=`m_qbMK%Btqc?S0oq%ghhn4RzYNRu&+r0iVjhP)b3z! zTs3Jhj0h7VM@E!{9g46ZrX;MbVV1}q6l8&%?C8vuk{6&&jKS3KQfGd| z6_**rpvkz%k`9Wq4$`>WP!6S6}K6dg| zjas!;eQOFgalq#UqhXDct)>`jd=NslJw5w1Pps)x?1b%V#MZ}4;Z96|d zZ;MmEz%7YY2w=hHA#kTKqG{jy=!0#YLCr?zd5%7~QKiv#y8Q~{@L*6F#VCME z(6H_L$UrK~>~LHg+cCzF8WVv*A6+Y=Q0pn4_5vF%B7`bNXNZ^?H8S&)$6uaYUeSrc zXqqM=0nCib$`s!x6hNd@2niA*I_D7KkaR_y_K)CKnZ?6>kaU?MQdL#ATUu-LJg@7T zX4mB_Y;6kr(?>qL4uY6OK8iqF<7=mtLWay1qyPnM1cPK)ab(ZFmFOt4QD_K=oUoRZ znN46NtQ=zuobX2=L4jc-Lfv05nS{KSv<$CbgX+XGjd+(-j^!!dO-rL+%HRiJdi z0^W=0tg802!Z;LVtoJEgD}ZD>l*&0qL>~g!DF72=+}_?+O8p-OACWyN=mk~)0000< KMNUMnLSTYt*17Qj literal 0 HcmV?d00001 diff --git a/Tests/images/avif/icc_profile.avif b/Tests/images/avif/icc_profile.avif new file mode 100644 index 0000000000000000000000000000000000000000..658cfec176e8576bfd28711d879c2cfff4564915 GIT binary patch literal 6460 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 HF5mwF+?@?J literal 0 HcmV?d00001 diff --git a/Tests/images/avif/icc_profile_none.avif b/Tests/images/avif/icc_profile_none.avif new file mode 100644 index 0000000000000000000000000000000000000000..c73e70a3a525c68a16f5c2bbc075b69ab2704cd5 GIT binary patch literal 3303 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 PMJg=(O0BBIq_F-6#F!Ng literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot0mir0.avif b/Tests/images/avif/rot0mir0.avif new file mode 100644 index 0000000000000000000000000000000000000000..f57203093656d8c0bf5caaa95928f2447e9e8992 GIT binary patch literal 16357 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 RmVbqbo3jf9uwz-@Eq)%)BzFJ+ literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot1mir1.avif b/Tests/images/avif/rot1mir1.avif new file mode 100644 index 0000000000000000000000000000000000000000..0181b088cec5012953258419b42cbc90ea44b948 GIT binary patch literal 16588 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot2mir0.avif b/Tests/images/avif/rot2mir0.avif new file mode 100644 index 0000000000000000000000000000000000000000..ddaa02f3f890f0cd65035a89294893956c2a30fd GIT binary patch literal 17001 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot3mir1.avif b/Tests/images/avif/rot3mir1.avif new file mode 100644 index 0000000000000000000000000000000000000000..ccd8861d5ae9378bcb6613f6391f2c4270c5f706 GIT binary patch literal 17290 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/star.avifs b/Tests/images/avif/star.avifs new file mode 100644 index 0000000000000000000000000000000000000000..f2753395f8c3c131267eb4d1846ca9a21a6a47a3 GIT binary patch literal 29724 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/star.png b/Tests/images/avif/star.png new file mode 100644 index 0000000000000000000000000000000000000000..468dcde005da39fc6807cfda46036dc78a1efe1c GIT binary patch literal 3844 zcmV+f5BuIr?fN3wSlaXSgJjniUG{R$dEc}9{yuv5Jn!GH{`;Kw{(_?-RM&gj-~BPw z^`2=ham7_!rSva;1avcn#a}TkC9a5R0dYk0xeuYd1SEkW;7Unnw;7fdJ6z_g1Z4vw z#sg>o{@S3L*y1u@nP@(DBg%{CeBiNCnzAoMuPDB_%vT1gD(E;_PI**S-H*8pocYS3 zzo#9zZp_b=YbbQz8Lgmr<9?|nqMA#P=QZHc3BL9QDVB<4i&5!{GcNO$N}f(8HXgtt z`n4?zIkkW@UkQx1<<^z-Ws$#dZNJiR<|~Cl_wB&a62E;p`P{8x%BdBc`AR`~xV#7C ztKkT#Eu8r#K?~h0f$PhB>^jYF`kgTJ)Dq5olTp+^ujo0I%ATtuT*G(*^kD89JpQxd z0G@ZXG_`LDP*P3dGT%fRCw9f0OEdphZ0YQfcaXJ1AYX zgv)&6QPlgXzJ3z;-r6AK)FLkPje#D?H8aEx;Jg6B4U!yQAdPn)3?Nob;WFP?oX~p- z#&`hJDJH%cP^_B5Wxlh}J)5U9?bvppIk=$gXON5KqU^E2V%7jI^PSDK<9|Sq#sd&8 zUpfJ%=^zLXxSeii;ws~G3YXo9urQR^$_FVdx+9d>F~ynhbW|S< zH@-X&YW$et%r}bmbzcvBB;;al(7ughFT<}xocT_n(PSmhyFZe0Vt^ZJ0kn{NKR#~( zi3o~wQ*NWU%m?7h14LsyfCR7iVjB0GT@5f(d0GC5xwKhYOqnbzSl zpD$k`TH^ssXSn!LK zSHDwkb(!)tzwvU^`xUnQ|Ah}rD*HdCWkgMVWE!WQEJsG4ni8p57GKtyQY(-$Je zUPg|ywS^(Hc!^|IziV1Zl+<75^M==Am+=4^5buvoi-?-K%6y|!Y1@HWrnQX05xk)V z()7u^X(3TjUzu;1`>@k^0JG3!U1h(z$$Wa?pJtJGZwHVvtz`oGDNemunm>NTw2&yM zpUjsy{w3@-9zYuJMBOYnsEf>}@BIBVX3yxw)I4r^>|=n|df4MzADORl=DozjcmQ*l zDQirNh=Q8We0unY8HR^{4WvzLDT6+|p@p^H^Q~F)sph88?_YuEeGTPi(|Ri4In;lI zwD+<|_JnCMVGwKcsph6|*jvDGu@&(yLHt&HX+^jMScH9p$(1k!yn*r?M0!DcQSBv} z=%vx$5ocpsYd+Q76b=^8ry((yqO{^^3o3IFZAEEeqz$)#=r;b?N*>sYum`0V)xC)9 z!Pj0Sv6rF2?K0=n1Exieg%R_qJ~Nd=$L8Vt^MLulJn%0BnU69LVIDBYl)f56fqe+O zQ1*cC1aB9x8`z2G?Pm6QyG3q$FKS_-{BgDLWC|Ku@wEkHE=a4(Sk`l921R=jZ!blE z56NUNv(r08RtyJOU`6ITWv-&MB7Q6CwczPolvZFd!5ysVu!kYwU4-6qUhm0duQavw zmK$(F^XdNFOcMSQ#9Ick40IW=1p8+S9MO{pwt;K|-HQ5KDSEHU1=*wHJ#dcswC|~l zkmS9fzt6}X2iNX$REh$x0zXC*e=C``U8jGTJm>3P@=jFCylc$i7!zUS!o;OWaX0X- z(?2|SdabGd+rTd)k>;omg#RKn^SfvN^Eu`dSu=zdyMP^$2y@gr+wl`^V-4kuxv)So ztKY@*R|0!Nm*%KtcH{Xso!!52;?IB&l>4ls zX4#JC-&F1)=VkBVkW||ahFK15EB9GPP4WglH&k-Ee#P^)dMKC1=U-5+sWgTo_Pk63 zzbCV@2P%2E;@xX9JKGNpw*dcMX$(iKc@aIaqEh2Qm0KqzbytDZ%$pIOt|pEncKkB~ z+$NcO4p$Q-kmXXUxda9MHY%SFBA&x$^a)ZId@hJp&4I1eQ&l0~^C-CGST&2v-;i!! zD4DH|aFvz2( zb<`<}>enRGx!#lj<9~F`Z~6=>-(#$!I~-wAMD>f3={)BYfhaL>jZw&NvMR&s1@*jSbMPU5y_2F9Bs z+ZydN9$1?BPNGM0%?$B8Bd1$B%3~|Z;g!;O_d(M_#>2|YcM_SEJ-{%lfZv$b5-XIq z7#dz>v+=<4%y$wkY`g;B`)|`aVuR;jS;{Fg3vVN#V^_i*Uj~>OX_bZ1XQp~jd7IOPD-=U*RaKh4`;rL5bNvB>JG=q9$yXvQ%!4$A;(GO8%5R( znHDl0c4xjm|8ku1DrFjl%o5WgCcc?s)Z9af`i^)(fCo6mJNszdCqKfBE*uCq}U zmUq^y?QrP++)R=bfP`r+F{a3gvch+-bAdeAwMsjtv%fI;`w)=%HMiA_uWlU(8SvoIi3)s9o5Z z`G)msobiG}7}6_Dix>+#&F9(MNRB0z)~B83v$&BQODwHVJIx1Inifz~SUOW=o%!^@ zQ!NmiP2xq<0!rjU-JhFdTEy8{XFie%>sR0+{1jysDW;=x3(E7RB~40FtgBc1Hs<(p z6~SC&9R|MueOOYRzc}sRo&%oK{@e|~2H<)#(k5YKS^bYp%QzkTGM}9@8*ieLCDpN9 z(q|a&6q3&VT+-R!#@CyHtr6>+91-i<(e|0ouFl48RQ`|@OC;0LEi!pZmh(G*MymNr zRKEc1i9lniSohksY9LqhPhE^A-Z8BM7zFty#VKEx=8qq#G>GomJe_HSe**HCz^nlE zRVXNNk!0F-nU--9yE30|ytAPQ-AH1wq&wDCV>|$9xqVR5og4631bhdG{C=}D@Ps4ss7B?iw-q;~oRM%%^-f!qNsoHZaU$NoRjhN@pSnL_{Us@gxIW z3c3!c>eXH*R%JeGIvm=cn-2u~x*M-3gdRxzSpp6(ddUTO^%b4tfU=-f)v(&4wa-%!h_?6xvZlzRCa}lvKwPQMK!;LDHR@8Q?NR z{|X3U%cDJ~yxP>}dyC*M`WtE2DGy3IyECAek#kym@N)#Wjr0vkXBP()t17H8pYD5N z22-XTCYVh8BI4aIsrFw55X%ZiZ(w>HxG{imMUJ1>C@r@S1`w+%?0D@qOj|~^T9mgC zy-U*B6}4nM0BP!aS<>0xMWhXQyUK70rW~{4^eQ{dhgNfbZ~=53>6uF<)%lNsNwN@0 zb^MT2ei6c2pipt3g#1=;sw&40^GQWF44pvv2Fc_HB;C0|mM|2==PWU_0e@e_Tasj?Vpy!)V}JHHGpq})Xd;>`E| zD5X_~@;n|_ODda{rmkw+J~e@)vpY`>QY$Z%_>?o>DP&Ufy^cy>ED?DeLat9Rq1< z-y)gp%F&h7lXbY9`2e)=WC}=)^YJ#2)udiuDw&S%D0Rh^k(Jc1Uj=$M@ai~UYu3K+ zH%6&zER2uyjk9|B6+|AV>9y~RtT(#=GsdW@kk5S#k*}bfc|NlRtD4R4Uh^rA{sesB z-%uWtO!ir`qH2TD>BP;zX6^6374#uS2C4-uT0hko37XH{HM*qA5i;7Jo3HuYU8Z$7 z91e%W;cz${4u`|xa5x+ehr{7;I2;a#!{Kl^91e%$4E_%R{vO%sunm9!0000-6bF)1Jd2yEhSw;4LJ;5(%mK94bt7+B`qCF3DOP1hrZu?oge3{ z>)HFhSL{_k9{>PAY2oYvF>wW30AA%U+JP8gU*D?$t;}qp z|DgZ?1PpTiFaL|SVAKDm!GM6B?f&^-UT;RQtpn(npE&pBO z{2(w0!tdna*l4;? zTohdBy)whbBf}<+2hY^amuG87i6-+gZX+AK zm&0z!pQz*MsL^sTrhNV;fym^&O7WX78eylmOb zuObh1BVU56(|hr~FcR`nu@+_q+I!8N)w9YpRLy^i9(nGFc)V;8Kox02VC6{iC8 zl;riD8VQI;&0#{Fu77y6tB+IqxJK8610!)`_>^_Ce03g~t!8+9$p*DRJ&uy`K1s_T z5VDrl$u}KJbGCToqPJv4wpVDhcV!rc-JItPJ<5`ZMJP3LP^=Pu-}c8_HK0o{8~Y@a z7vyc*$&n6rhYF3J@lPR$Jq?fLX=`$w< z8-%bh_L*4YP4uZq=@b6`-l9;!y;sLWH)VFRxt6ky@%6346z_JgSgi5}a!oEuPio)d zl)NX{R;7ao4VQ77S)*=)(6NzI7pj%wljo)}Xj@N+hTiZ?Cc{|8K@Ls0A~I0yv*1A{ z9e!}bG)g`!rEY}Fn&sD-xvI+o8nEg$yR<2XXA1aRrp%kAZk$liA_4=lU>?PBH#c#m^bKq(>QCc3&p3WoIpcN(}az0cK7ACnNm_pm(kcdxGnNKkk zCy(ML#Z~uk9Qh8Oe3@K}RJVEjl2Vb}33Cs3m|db1+t3%Qfh25C<)zxBJe=LIFX+L9 zi=ORsK8QQY-g})PzdnLLH?ujGY_THtmuv#7<1o=xg@tt>G9E-pJgV%VcGv}6=o-8_yK8*MO^Y*6blmna zrx2$>F9zV%OFdfIPn4Ao13**1q?HkZ07aD82o&{_L@erAl)54nDH<&cehdGe4BO9e z0~tSlq;p1u<69!s9q0kRiAwC<@4Oky=)1c+%vDukNR1ToeqyZH{VffJtxk%bGuITJ z(7sma-%^p`o3E>VuCgRgnkQ{HIh$TyR^xX40&qW&%RW&Tl$%XzZeR zu&%oc4GaOo+KQfMgy|W-?5e2YumT@F%6gSPjKbxMf_%!CMTL(*T>hCDlT`RzdB~ii z92hj_uIIx?V~$n;P2HsAQCi^OIMS(7wVHPE(`84m&(#{7+6U>Dx%ehzggGn_my&be zYO`Md!=Kq(EMmif9AwS~iZ<;l9CbVDI;yK&o;VE}+`N7% z)v`nnfjRuYjQM%mYScGxNiT}4Q}-oyBGcEdEu`9a)UIGupAM*gxflv#Fn2G94@&cl zjQg1ql|s@3l7g?e%Sgh-4W20Z$06Z|o+Fv73HY7bLoYg0dY@zw^{}@BtzhYPZ^h?$ z=RG7{!$>4*Hl|W`Ch&^)akAL*i5s1M1pxB@d^hd|1-OZHgXb#SeG?49mRI?x-Pi^G z4S`KuH^U<&0xs1u9+`Ep3ZJ2jgs#zhAV0}P+z(~KQN>qewr>*vU3l8yZ6Vg&OXrC$ zYGeLD9mLVD6vcgiz@k<_qhv3_HNE=azT39=%S&c`_sx+8FWGTM-bnfWUxq-@F#p*l8{CXGSC>(FJ#kd z5w^s`Xg|H67zP=lyweR`V&GJ@UZyI*cPw70N+E{=(F)+8F{8ne+5Xvs)^sdUPMC5N zfi3S+KD_U2S<7H~!8@r5{3Jo*;etmu({xzE5n8q!S`&K|R(P)KBzRIeHsGLf=O!Sd zlFnd0r#X+|y$rI{V9p`u51FGLygjRYIaquHr)ciiQ9D24d=Sb#FUB?fM1hR#`^21^ zC{qWejb}`-)7t;gg#V5K?OhX#ymf!GfRKdfrh(;`8o*Vhb@B)O9eH!-`a#yuc4c&* z;o-bIA~b{#!0-aP1eN}FxSJC`v*hGY=7C^_>qe)&eF}8crYPTn>adiZQ37rD>$~eM(PM14h zRhFOk&aW(~cStB#Pj8MngOtaYFIar4wk#i25#!>2%AQ&dwsjUxNq*h3K|+~tRU63> zOF)`@@7U~Y!mFxJOgrn^v;4Mc=p|Hy<*dm0?%8V){{2!xyKVbaztmCs^^B5&r+_Bn zQ3E~L@v7g)Pr<-VasmZSvQKV;3HoblMuoH$dW@AUBM)NkOnpg9ab2Ah&G}Olt*LfG z6HC{^xP3hRCvooQxLoB|ev9WKB(bPfH;dqc3y3 z*I0^27$HdlYLal@DFK%UyMKRFEQNvld&5xitGRcazHM~`8FOx1Gib;1ouV~px{c5u zK|zy?r@)?6-Hs2eM%Mr-ngh?dc<dU5Grn7g70g@hz?dp{Bzl3&`RIcZ}i;{ zPnp{xj#$$`kO40yB31(>QONERaj;}OEBuyM$RozhS8&4yKu%fdgz)JadY4=lE3S1J30iXq7*rPJ~SIB-D%`FTwaWu zklnl#UQKLHtjOr^UZq=D;QYy1H0!e7^c;HE>s-(jI6N;G1dgQXZEb#_|o09!FwWAUQRdH?!gl zH(LIZ+7p!%SjM-Z_pQw^0;`U-+B}+(WzW;KCbkiz*VWvdM_N7={PtGRsT8xjttYc6 zW=mXwCCJHLkT?to>CUxI{+>9XyW@Y4;}icwzaz$HB?R}xV@&o(G`WXgE(VD6&#?kd zXx#>06GOCgk_3aU>ISHo*s!o)m!|7icMc-T1TDm*>5+_p-Tr#Z{v6UdZqpzm5P4g!f>Uj4yyP$Q;_eXvIpktnZzdVkCqxbN zF73;A5BA)dy5~_8=V0e3ddS6+q@p^holCfg4tnTdg*h)*-@!kvc*;e{72Jf4_26Hg zcuprC|CJ@Wb2kp*7;npk! zNr_B1w~tGC_Zn^!)BF38L6|o{*iAN?LSrk&_p4+GGT%G7oYje$j|8=5OwL&&Ls5f@ zN0jPJKT%0}TR9e=j{6S!A9C1k=W7@%)}Z>SBZ>A8FX;&~dlk(6pu$q3SE<0|@zX>T z97J|=CZNc%!%3F4Y1a=A8|fJ%IT2BaY0Ej5$j}f<<XI83KZnv+mhhsu}{;?*^p1^zs1@2L=CVQBt1gQ&NDW&qPK53f2t zS?Fxq6a|MQYd_`DrXv}HwWuX|{+nFmJfx0rc(Jk0bTVFyj?@nkHZ0T=SOkYBqM14{ zrR3{Ch0)1M>?^M^P|w0Hp}(*9%Yg&}WSR7R@1f*bnDnwGLyyA$0r(ux*wD|2i-4yT zua}&i)^96)*utFY{Vm`Ej(o!YPwqG}1jRMxVn%(=Gvs;>>;2#mCpY z9XQJb{9XLbCy5QY;Um_imZ-Ceq2nqi>0o4j0|TAiCw{Iw_%7P(u_rERfd}M@TX1Y3`CQ01?Q7skV##s8141wH%vbk z*z!dg(NQIjU6SJ(zoLi0E~hOk$p20C4ws?WV^#%;;3K!~YM!pJf20zr>|_w2(3s0_ zc8{Jn&{ha@Mt)l9v%VesI!D$p9}ktu@FI+*B5*e0IXK!<6Jkb21^!Rvs$s= z^VFz?VICnO;E%8!6Dsp&E^4pIvTS5wE%D%pr2Egw6WI<7Qr**91Op97nz}smFAz!K zAx1ok`ol}9-_mbb?Wj`6EUhOR%wWle7-RF?)4jhy#>G>8nK9n?h8DPrA&l((Xx0_4 zYG!NY;5pj3m&5;ri}9?Hm-CTs>C#*5A`4c$(|#cUs%=8zca&Nr3+sz9do*17=DsU&YhVViY_4AEx)Y zAA}o@KENV^)2e(Z^1<2za5mR4#X*PqyrPBSnC!9CypL(jO078YSi(&*kPs zf@-L;HZC>n)ivp~E0RAenkX@*Rh>kc)r$}f5!CUJMXyV$z^Say-N9@?(BD^Gk-W|J zdMg({rRgtQ&cxlU#$qO&jP!8um5%IkBUYk|J6x^<0|r#ljd4!IoMHRJ_?wewYcb-K zsiqYwRgK}-b2fG9U~6A_g;FC^|L+S;*gb|FiNR?-MGXvnR909VtTBuhPf^e4CWwj# zlp7Zw&7Vh$mODmxiCNJVeOp{+sezR<)1+;CI_B7PHr@Jx1LkKPQTG$;kGivaU)$)N z2JSpheU~73u#&vFl&h0PhX;pNJ5i0z+IC z+~ceBXyj6t;ySrPv-A3|ZzYv^h|^2dtt`R&l3#^l1lrfaV+(LIKMppN;};I@co8M4u%h|_Bpu{iP^@l##AB}YXi7uf6z6mw zIc*IN&Nls<`&IqD%T1^QT3tzpwon58jD%&fS>CK$>Jjn{qwQ^z@p}CmENCY^KhA*P z&kyvi>#VWaUghSOA8yAiWKP(0f9!(mT?7RUiovN(doH(S!630@4MMj(~JQx*)wHO0R+< zy$T43H{qP~dH>vX?~glcP4=_PJhNx-nRl%K005vP+7D^}2<`~L46JmAI|{qQ?HzTM zgp~mRJTG?`+8*n}3?65w8|rTe03hKI^uHJ@-QW)Y(vTzJX!mO$4(3+??&bl(YN`ML zE&yx6Ond_XfMx`vUu$?TFybu+o510S>rAda3>XE54Z*IkeIJ#Z0>g-#21lZh80iB? zLOd{$G;l8@47&ugp*m)gVgMx)?hD6S@Q8?rFoM+nk+4b-0PDgPfOxo}kSGrfJ1!8r zJ1!+A8w46MCbO365h!;}Hz(5Df^{>R@KjUQbx;lUfK`hrF z`~L}%Kw+4Z0RWen35Ot2nBL$xV=%i98i~cR7);~fdW~OUF$$9)#$JcTPJiRR>zKds z{B?|pkqRbeAA`vquJLs@$esSin9~Bt|JfG<0TkE$(X~fIoPn4UU`_wO0}b=NzH=%b zNIw+Z$r%k)!c+Tfo-%)IfAaZb3w8wn)G@UqkoaRW zVg~?9`~U#PsXsP0O#pz369A|NLcCFr{$aww83q9UHysm8jlC-fh?_6w_Td5u0x>3f zIMUtzUm5@oJ`Qfk4SWFLc8ojJ9(_;27ikZ1g`t5CFef-dj*Sfnhsv>;i|Y#MB2{3{ za7}*{%-CPg1mf=sk%qD<$dlajmGN~)V&5webM+BkGQM(b_D~N8m<$GE&0sd*bqLy3 zjt%o*fGQp+7*HG}1`=YECjs8WycroIRrP-sV>CH7XEYiq0|xu}_<(#wK^`b4u&}hW zG+0OkEFvO^i4gSiL!j+_1rc8CSc<7&E}C|rBv+E9zN`C+1Do@0?H*9>UvmLxLuO;@d_7zSe6<%gmQYcC9S-BH zX(RalT+01#@9d+4qE)Fiel~8D)~tP@?@dCUpZKz$l2gi)+UotLZ&S+4OwS6Rd^CQ_ z%ufgYsAxicLdo&qYB1KS_odnb^^abnSlbxdMWEBKn||eSqk5*Pje2PoC<%&fSv1j6 zJUK4t;`^+bXo%FfF{Qzq!=_G~z4)E2oZ{!4#9;wAO8K>s6Q$g1{nbf!aE8_x8^_mj z(r?p4WZORfCPvqnaQ~!lh(}9g`nUHL-=_%-$79Zlk@DbQY@4>VE&ehm|8nq>#;BL^ zVvs#MFJgz5r`R|5=e9=TVahy5=A9{US54QrH6(SQnh*-a2l9JGxp0hjsWGd(w7Fe> z!0X*vUZx*)k9yK{?QZp6#|Ls*{p2GBcc9i#fJvRcHX|KtS>9+1sqEB{r(J-S?~v`& z(9(h4stckaif3$LUB+F4r1@RN6z=!V;OnvDZ=`mE5B8IL$AUBHrkgmX;>B(>t1I8_ zC`1wJTcvdl%0o3Bx{_mZ@~A!DT%_{G@zeF! zaC>GrE`UsGNk^M(A27{>Rt$nug@^q_NpM4?Sefc@-m2Y##Z>m!dUA6O)h?o)SIMN{ zPhZ}kvI(QsEE=03CR5rnoc6EK&kof_Mu4c}M0y>hNvC*Jl&H(3UsUkukqsU|aO#z= zQqM+0@~eY$aL?2Q*C#f&oEZ>xi`;rIkyd{9B_}su@7U`mW|ku=Wsgf7L*JDVFDVZOt+v`&YabA#81=3r~aaY?Ni z{-U2-WZ$N($sk;K1J_JK3A$xp<_VQ$R9-TBalho)Tl3=aRlM%o z=lRDD31zySCy`)a4EJe2a8YeSytpbN=Q!3odiPR4V+sFTX)(J= z8THbRlU+|c8~F^0YV^YK+(lZh7jX`~D)0#nbl>lBbMsZ7XQINJ6o2jD)h(;Picp&x zJZdib8zRxq!%w6Som5Dq3zlUy!ybQ$D4T%L-Cw4Y$lJM`oZXSg1=Su59ipikl$UIu5-oU`7@w5YML^Pd{?8(IMs zuWZgu;uwmEb9}EKofK9V1^TCxh_&R=8n?6w?n1pV%DO4ANti%Bx&Z(W4Xa}KfU8} zmA%|rp7AR!b(GCARkd*#gL_15>Nar5>o=RE#ck-zl?Obx+Tc^voQYLy6+RRN{EaM= zoWs%b&YI`!6b|x}79D8>wIaVLfXN-&H43NE$two z&V!&+?-zNb4i{;+zFSW<;pCDgF0XIv2*ko{eFtrwC%dZE3%waDOceqg`2=SSvmzcR zR@UH|@C^Ub&N-5*F^A0u)U~Ra3Q_SCpyK&=UP4rpKIn+?4-N+&RtM$udg^q}rZk*{ zhExeHeutX%qi?9>-mb!DmzdHXCDT)rc+9xUAtB4M7EBye^jYfMd)d<3=9}JK!jZ`| zXo+O;UK!U6w3%8?c+GA~FgR@1Sh%=q_GkYp8EzU0o&x(3cZg9#&_hw?`}70K%W-wb^1rrN4Ri*g2>eU&k4A(Qih{%ZlgRF*h13P( zvI@0--B2l0jQoA4u<&^l5O>cp(DGLM&&Z|j7oaag?GtA~oJ_=0T8M8{Ui*Ur>s6e^ z^OhI^i;waxVJr}4KTl3P49fB)Wg+>;W$#!mqVKynC0 zyb^VtCn6WO`Wz;EL|B@qcRVd+u8n?_RP0My8Ij7i4=g&+N`*cm@*d_MW7#J` zRTcKhb8`KuG!A4MrpuvZCXDS~)~n#DtZCHE+mh-DCF2K3o=mo=J8d5`g@i@}!rv`1 zj5J9FK4viAg=)zBXbK;7k)907<`#3^B8(Nr4@{cW8C>~*yVKtB!9kH__45hLIXI-F5fOBjou_k|+vr%X@T!4tw=Z0iQ$EtC!&Lm` zDOej`^s2-TFzCnc$jLLcX#A62RtUzpA3Qb(SH!Ci`5j9*JbL1_0(&aLRSVitJ1vSS zME7}&s~a?^uw(R%0 z6)!B(Ozg-E{Ls2_UDi$P86c4g-Z+=gc=HR7cl)=gO-P+W4Ofiv+M;o?D=FR=#r0U1 zqQ1J2=Q4KK9Z?h__p?LSGw2Fcj~R=Vt9${eGaF+{#ykQ-l{$0I&CGZy_^(qTUJET! z)4XJIQwqt1o#qGVc*4Ndm|8q)*kw3lF+jy8dGAW`Wa6afLn)n_m4&VI9`Vq}+0(xF z&b}!#2~J6e_#+$}Yf&rTGSyC!?{O7@liV~KV;$D5O(fgpk{8q;4sr1XKO}k2g^Hse zUu!0u%6)jhr@WkL&Nw;PtG5De#Vx{G7v&@XJ6Ts-FOy-goNFmqwv|0fXCeJE$edto z6{yet9tR#(h#u%HZ80CJWl^>u+_%kUd^+C3?ekcy?8E-8MRPxzym%YOe!XvqyJ3dK z?L}cTuC8=m!RBL>11D^$>47b19S4)ms*lgtVXhj#>k&z<9gCH5ptq4Vt>=q%<(*@` z`IAbr?~igw5bI9CL-Od!DZ~Bz`XWfd^zsA|KuFZXkaq9H*gqbvaj& ztAAVE&q4%dDH7TA*pTdX#P6>qsFi{RcDKBJ8L^i3h<@C!?C?G2{s1pEZmq_jdtvu? zDlEtK0v}v}*%rk=5ARJ+29pqG*^$k-kuLaWRavhvz06r)`Bpp9*U;A_V3|E8;*)+U z>E?bSi;@~I!tW)0>yaOUc>Se&Ch&-WS)4F(we^`I=drv=f+L=mO_s@h7WK2tx*ja{r2&;QK}?&$=vN{#ZBtZ1m3JXf`xXa2YG6k^jczC zd%^I1bGrRQN_)aL2a85^!x9ymtk;`Q9|F2qD(ZtEj)5amQ*w2Qem=JJPN8Ig)Yr5I zFrb26tEMY4f(R56W7AfX)!(Wan?9&3;7KF_?YT)_aY3@aQ;tTl^QZ)tLtjWKc39@` zuD^X87;>B7FCM+Gt3-0R?u>BB)B7!hsne8Z{&}@otu09ih&lYo4Yv(p- zKau5p{8nlzD+YWO<3%ziY7x7Jr>2eMhTaRfs>{1!qWq6$!}@<0riwzb#9e=fE;J%aHo57^C;5_{ zjefJk8APOav6#8cMeI;gs$~#Bw(J0HMQIb0^de(6$eL}-jd?hB-T5`&3%$2B#}}D~ z_YFss9~6>VDumc{A7f1pF>$!nj~Kp`!jptEFIUlt+`sUauld8qLUb zPB#d&9S6q61>&ZY7maJ(d|#U|{^VZewRCoHO^Ru+Y=nPgAv;6Q4PJTouv-%0vpqz# zN>sk#`{+FJ@&uG$r#VnNL{jX#(Um&y!Ol~NOV>_N6-s6MGp@wmPFT7?ZOuk%z96_J z{fH(uWm)q5daATzdQLdWna$;Sr{A{~)=Lw@cP}h&JKI4L5jVB{o{oO;?6C=NHq@XL zEa80gW7CC9Zp~R)e|J$?+dc;n!^^g?!N|*MC#6C8%okF%75o03cXR6r}8#j=H)L zUS?cPGnzjxkfUro_P9%noj=dYWIab=$1Y{;P_{o+WNtsBjfR9v+XCU%FB4>B)md~V z`o<14u7pqi=4xkVq|w^6wN_gAG*U>Lqp|tB=VxG6df<6sgjvx%Pr+fiWD@!GTSi_r zKo9g++bCSPvRL!EJmG{ocxy?crTg`=ok#CHtE09KgEMg8;o?1kMDT&YL^0Ws3YYc7 ze5J|9Hm23!%I?^HVkIhQBf{jqywdE(TwmDP(_A}_0qN%jYo>w4F6x|2W>&(p1I+L1 z`E;z9BzDgQ#aClv1IK5GR|W%5?(2SWUNQyIljqa_IOS#x{`~e^z_?g`dWd6VfoB)% zR0QMc9>CsjPIu;UZEN`?TMbjg$U;C2Q|v(Q;_Y< z4)KrAF@Mq*FRJ(PT)6P%9^R=5gzg>81)jcNOj@hi#}!w+P-wA&?LCmBVf=h12`t;K zmxdD1qO%H3J>NLj43C2SO%gtcCDVc2ly1C8s25T9I+@(NB4f-bN`QX_;=z@URGO!u}kC`<3keEKWns7;?#-LwT2^v|5f@dO%6dd$uTm7tz@vP#i~V;W}m+-LE@XQ$9T zM^c;j&tULKZ}I6{Vn3jePp(Wo$2I1n4T#u%y;4;%yseDy#dH1AZ&!e30~gUI)4#%7 z&k^l5`P0;ArPCq^zHV+u`Ho;CU~JO`I!)Fn{I6HJua*mFwxC@$*EkB&x_VND2vxE;Q_0Y9vUs{`+j{WXHWtBl@pZVrx7;`p*^fS_bZbq z-)8z$zSa0qaec#rRaBFk{|C(yyOZq4B~CGB+2V%<{0dRN;D*S1(*mo{pJi4@9|~#r zKt)aBhe%^uI5#)Wp3&3}e91_BH1)gT4t^%y4^D7bls35IiB#i>LBwPo85g{3%2eq_ zt{JoP8^mJVsKuor4ed`FKlY^#lpQw_DM?uhQgcPw)9uPKhl>D(bN46NQj@by!hZpe CD~X2y literal 0 HcmV?d00001 diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py new file mode 100644 index 000000000..392a4bbd5 --- /dev/null +++ b/Tests/test_file_avif.py @@ -0,0 +1,778 @@ +from __future__ import annotations + +import gc +import os +import re +import warnings +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from io import BytesIO +from pathlib import Path +from typing import Any + +import pytest + +from PIL import ( + AvifImagePlugin, + Image, + ImageDraw, + ImageFile, + UnidentifiedImageError, + features, +) + +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +try: + from PIL import _avif + + HAVE_AVIF = True +except ImportError: + HAVE_AVIF = False + + +TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" + + +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) + return Image.open(out) + + +def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} decode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason + ) + + +def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} encode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason + ) + + +def is_docker_qemu() -> bool: + try: + init_proc_exe = os.readlink("/proc/1/exe") + except (FileNotFoundError, PermissionError): + return False + return "qemu" in init_proc_exe + + +class TestUnsupportedAvif: + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + 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) + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) + + +@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) + + def test_codec_version(self) -> None: + assert AvifImagePlugin.get_codec_version("unknown") is None + + for codec_name in ("aom", "dav1d", "rav1e", "svt"): + codec_version = AvifImagePlugin.get_codec_version(codec_name) + if _avif.decoder_codec_available( + 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) + else: + assert codec_version is None + + def test_read(self) -> None: + """ + Can we read an AVIF file without error? + 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() + + # generated with: + # avifdec hopper.avif hopper_avif_write.png + assert_image_similar_tofile( + image, "Tests/images/avif/hopper_avif_write.png", 11.5 + ) + + def test_write_rgb(self, tmp_path: Path) -> None: + """ + Can we write a RGB mode file to avif without error? + Does it have the bits we expect? + """ + + temp_file = tmp_path / "temp.avif" + + 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() + + # avifdec hopper.avif avif/hopper_avif_write.png + assert_image_similar_tofile( + reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02 + ) + + # This test asserts that the images are similar. If the average pixel + # 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) + + 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() + + 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() + + 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) + + 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 + + def add(self, *args: Any) -> None: + pass + + def finish(self) -> None: + return None + + monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) + + im = Image.new("RGB", (150, 150)) + test_file = tmp_path / "temp.avif" + with pytest.raises(OSError): + im.save(test_file) + + 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") + + im.save(tmp_path / "temp.avif") + + @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 + + 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() + + 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)) + + # Save as AVIF + out_avif = tmp_path / "temp.avif" + im.save(out_avif, save_all=True) + + # Save as GIF + out_gif = tmp_path / "temp.gif" + with Image.open(out_avif) as im: + im.save(out_gif) + + 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 + + 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 + + def test_invalid_file(self) -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(invalid_file) + + 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)) + + # image has 876 transparent pixels + 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)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + test_file = tmp_path / "temp.avif" + im.save(test_file) + + # 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))] + + 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 + + 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 + + im = roundtrip(im, icc_profile=expected_icc) + assert im.info["icc_profile"] == expected_icc + + 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 + + def test_roundtrip_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + expected_icc = im.info["icc_profile"] + + im = roundtrip(im) + assert im.info["icc_profile"] == expected_icc + + 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 + + im = roundtrip(im) + assert "icc_profile" not in im.info + + 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 + + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + exif = im.getexif() + assert exif[274] == 3 + + @pytest.mark.parametrize("use_bytes", [True, False]) + @pytest.mark.parametrize("orientation", [1, 2]) + def test_exif_save( + self, + tmp_path: Path, + 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) + + with Image.open(test_file) as reloaded: + if orientation == 1: + assert "exif" not in reloaded.info + else: + assert reloaded.info["exif"] == exif_data + + 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) + + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == exif_data + + 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") + + @pytest.mark.parametrize( + "rot, mir, exif_orientation", + [ + (0, 0, 4), + (0, 1, 2), + (1, 0, 5), + (1, 1, 7), + (2, 0, 2), + (2, 1, 4), + (3, 0, 7), + (3, 1, 5), + ], + ) + 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 + + 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 + + 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) + + def test_xmp_save(self, tmp_path: Path) -> None: + xmp_arg = "\n".join( + [ + '', + '', + ' ', + ' ', + " ", + "", + '', + ] + ) + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, xmp=xmp_arg) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info["xmp"] + assert_xmp_orientation(xmp, 1) + + def test_tell(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + assert im.tell() == 0 + + def test_seek(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + im.seek(0) + + with pytest.raises(EOFError): + im.seek(1) + + @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) + + 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") + + @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) + + 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") + + @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") + + 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") + + @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") + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize( + "advanced", + [ + { + "aq-mode": "1", + "enable-chroma-deltaq": "1", + }, + (("aq-mode", "1"), ("enable-chroma-deltaq", "1")), + [("aq-mode", "1"), ("enable-chroma-deltaq", "1")], + ], + ) + 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( + test_buf, + "AVIF", + codec="aom", + advanced=advanced, + ) + assert ctrl_buf.getvalue() != test_buf.getvalue() + + @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) + + @skip_unless_avif_decoder("aom") + 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") + def test_encoder_codec_cannot_decode( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") + + 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") + def test_encoder_codec_available(self) -> None: + assert _avif.encoder_codec_available("aom") is True + + def test_encoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _avif.encoder_codec_available() + + @skip_unless_avif_decoder("dav1d") + def test_encoder_codec_available_cannot_decode(self) -> None: + assert _avif.encoder_codec_available("dav1d") is False + + 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(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, quality="invalid") + + @skip_unless_avif_decoder("aom") + def test_decoder_codec_available(self) -> None: + assert _avif.decoder_codec_available("aom") is True + + def test_decoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _avif.decoder_codec_available() + + @skip_unless_avif_encoder("rav1e") + def test_decoder_codec_available_cannot_decode(self) -> None: + assert _avif.decoder_codec_available("rav1e") is False + + def test_decoder_codec_available_invalid(self) -> None: + assert _avif.decoder_codec_available("foo") is False + + 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) + + 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) + + 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 + with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im: + assert im.size == (128, 128) + + @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) + + @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) + + +@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)] + + def test_n_frames(self) -> None: + """ + Ensure that AVIF format sets n_frames and is_animated attributes + correctly. + """ + + with Image.open(TEST_AVIF_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated + + with Image.open("Tests/images/avif/star.avifs") as im: + assert im.n_frames == 5 + assert im.is_animated + + def test_write_animation_P(self, tmp_path: Path) -> None: + """ + Convert an animated GIF to animated AVIF, then compare the frame + 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 + + 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 + + # Compare first frame in P mode to frame from original GIF + assert_image_similar(im, original.convert("RGBA"), 2) + + # 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) + + def test_write_animation_RGBA(self, tmp_path: Path) -> None: + """ + Write an animated AVIF from RGBA frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file: Path) -> None: + with Image.open(temp_file) as im: + assert im.n_frames == 4 + + # Compare first frame to original + assert_image_similar(im, frame1, 2.7) + + # Compare second frame to original + im.seek(1) + assert_image_similar(im, frame2, 4.1) + + 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) + + # Test appending using a generator + def imGenerator( + ims: list[Image.Image], + ) -> Generator[Image.Image, None, None]: + yield from ims + + temp_file2 = tmp_path / "temp_generator.avif" + frames[0].copy().save( + temp_file2, + save_all=True, + append_images=imGenerator(frames[1:]), + ) + check(temp_file2) + + 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]) + + def test_heif_raises_unidentified_image_error(self) -> None: + with pytest.raises(UnidentifiedImageError): + with Image.open("Tests/images/avif/hopper.heif"): + pass + + @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) + + expected = (255, 255, 255, 1) if alpha_premultiplied else color + with Image.open(temp_file) as reloaded: + assert reloaded.getpixel((0, 0)) == expected + + def test_timestamp_and_duration(self, tmp_path: Path) -> None: + """ + Try passing a list of durations, and make sure the encoded + 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( + 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 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] + + def test_seeking(self, tmp_path: Path) -> None: + """ + Create an animated AVIF file, and then try seeking through frames in + 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( + 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 + + # 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 + + def test_seek_errors(self) -> None: + with Image.open("Tests/images/avif/star.avifs") as im: + with pytest.raises(EOFError): + im.seek(-1) + + with pytest.raises(EOFError): + im.seek(42) + + +MAX_THREADS = os.cpu_count() or 1 + + +@skip_unless_feature("avif") +class TestAvifLeaks(PillowLeakTestCase): + mem_limit = MAX_THREADS * 3 * 1024 + iterations = 100 + + @pytest.mark.skipif( + 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() + + def core() -> None: + with Image.open(BytesIO(im_data)) as im: + im.load() + gc.collect() + + self._test_leak(core) diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh new file mode 100755 index 000000000..fc10d3e54 --- /dev/null +++ b/depends/install_libavif.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -eo pipefail + +version=1.2.1 + +./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + +pushd libavif-$version + +if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then + PREFIX=$(brew --prefix) +else + PREFIX=/usr +fi + +PKGCONFIG=${PKGCONFIG:-pkg-config} + +LIBAVIF_CMAKE_FLAGS=() +HAS_DECODER=0 +HAS_ENCODER=0 + +if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) + HAS_ENCODER=1 + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) + HAS_ENCODER=1 +fi + +if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) + HAS_ENCODER=1 +fi + +if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) +fi + +cmake \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MACOSX_RPATH=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + "${LIBAVIF_CMAKE_FLAGS[@]}" \ + . + +sudo make install + +popd diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index c0b1a9d4e..bfa462c04 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``. Fully supported formats ----------------------- +AVIF +^^^^ + +Pillow reads and writes AVIF files, including AVIF sequence images. +It is only possible to save 8-bit AVIF images, and all AVIF images are decoded +as 8-bit RGB(A). + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**quality** + Integer, 0-100, defaults to 75. 0 gives the smallest size and poorest + quality, 100 the largest size and best quality. + +**subsampling** + If present, sets the subsampling for the encoder. Defaults to ``4:2:0``. + Options include: + + * ``4:0:0`` + * ``4:2:0`` + * ``4:2:2`` + * ``4:4:4`` + +**speed** + Quality/speed trade-off (0=slower/better, 10=fastest). Defaults to 6. + +**max_threads** + Limit the number of active threads used. By default, there is no limit. If the aom + codec is used, there is a maximum of 64. + +**range** + YUV range, either "full" or "limited". Defaults to "full". + +**codec** + AV1 codec to use for encoding. Specific values are "aom", "rav1e", and + "svt", presuming the chosen codec is available. Defaults to "auto", which + will choose the first available codec in the order of the preceding list. + +**tile_rows** / **tile_cols** + For tile encoding, the (log 2) number of tile rows and columns to use. + Valid values are 0-6, default 0. Ignored if "autotiling" is set to true. + +**autotiling** + Split the image up to allow parallelization. Enabled automatically if "tile_rows" + and "tile_cols" both have their default values of zero. + +**alpha_premultiplied** + Encode the image with premultiplied alpha. Defaults to ``False``. + +**advanced** + Codec specific options. + +**icc_profile** + The ICC Profile to include in the saved file. + +**exif** + The exif data to include in the saved file. + +**xmp** + The XMP data to include in the saved file. + +Saving sequences +~~~~~~~~~~~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +options will also be available. + +**append_images** + A list of images to append as additional frames. Each of the + images in the list can be single or multiframe images. + +**duration** + The display duration of each frame, in milliseconds. Pass a single + integer for a constant duration, or a list or tuple to set the + duration for each frame separately. + BLP ^^^ @@ -242,7 +319,7 @@ following options are available:: **append_images** A list of images to append as additional frames. Each of the images in the list can be single or multiframe images. - This is currently supported for GIF, PDF, PNG, TIFF, and WebP. + This is supported for AVIF, GIF, PDF, PNG, TIFF and WebP. It is also supported for ICO and ICNS. If images are passed in of relevant sizes, they will be used instead of scaling down the main image. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 2790bc2e6..9f953e718 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -89,6 +89,14 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. +* **libavif** provides support for the AVIF format. + + * Pillow requires libavif version **1.0.0** or greater. + * libavif is merely an API that wraps AVIF codecs. If you are compiling + libavif from source, you will also need to install both an AVIF encoder + and decoder, such as rav1e and dav1d, or libaom, which both encodes and + decodes AVIF images. + .. tab:: Linux If you didn't build Python from source, make sure you have Python's @@ -117,6 +125,12 @@ Many of Pillow's features require external libraries: To install libraqm, ``sudo apt-get install meson`` and then see ``depends/install_raqm.sh``. + Build prerequisites for libavif on Ubuntu are installed with:: + + sudo apt-get install cmake ninja-build nasm + + Then see ``depends/install_libavif.sh`` to build and install libavif. + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ @@ -148,7 +162,15 @@ Many of Pillow's features require external libraries: The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - brew install libjpeg libraqm libtiff little-cms2 openjpeg webp + brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp + + If you would like to use libavif with more codecs than just aom, then + instead of installing libavif through Homebrew directly, you can use + Homebrew to install libavif's build dependencies:: + + brew install aom dav1d rav1e svt-av1 + + Then see ``depends/install_libavif.sh`` to install libavif. .. tab:: Windows @@ -187,7 +209,8 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libavif .. tab:: FreeBSD @@ -199,7 +222,7 @@ Many of Pillow's features require external libraries: Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif Then see ``depends/install_raqm_cmake.sh`` to install libraqm. diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 0e173fe87..c5d89b838 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -21,6 +21,7 @@ Support for the following modules can be checked: * ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. * ``webp``: WebP image support. +* ``avif``: AVIF image support. .. autofunction:: PIL.features.check_module .. autofunction:: PIL.features.version_module diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 454b94d8c..c789f5757 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,6 +1,14 @@ Plugin reference ================ +:mod:`~PIL.AvifImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.AvifImagePlugin + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL.BmpImagePlugin` Module --------------------------------- diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index d40d86f21..dbaa8a4a4 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -68,3 +68,12 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT5, BC2, BC3 and BC5 are supported:: im.save("out.dds", pixel_format="DXT1") + +Other Changes +============= + +Reading and writing AVIF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now read and write AVIF images. If you are building Pillow from source, this +will require libavif 1.0.0 or later. diff --git a/setup.py b/setup.py index 9fac993b1..9d69b1d6e 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ configuration: dict[str, list[str]] = {} PILLOW_VERSION = get_version() +AVIF_ROOT = None FREETYPE_ROOT = None HARFBUZZ_ROOT = None FRIBIDI_ROOT = None @@ -306,6 +307,7 @@ class pil_build_ext(build_ext): "jpeg2000", "imagequant", "xcb", + "avif", ] required = {"jpeg", "zlib"} @@ -481,6 +483,7 @@ class pil_build_ext(build_ext): # # add configured kits for root_name, lib_name in { + "AVIF_ROOT": "avif", "JPEG_ROOT": "libjpeg", "JPEG2K_ROOT": "libopenjp2", "TIFF_ROOT": ("libtiff-5", "libtiff-4"), @@ -846,6 +849,12 @@ class pil_build_ext(build_ext): if _find_library_file(self, "xcb"): feature.set("xcb", "xcb") + if feature.want("avif"): + _dbg("Looking for avif") + if _find_include_file(self, "avif/avif.h"): + if _find_library_file(self, "avif"): + feature.set("avif", "avif") + for f in feature: if not feature.get(f) and feature.require(f): if f in ("jpeg", "zlib"): @@ -934,6 +943,14 @@ class pil_build_ext(build_ext): else: self._remove_extension("PIL._webp") + if feature.get("avif"): + libs = [feature.get("avif")] + if sys.platform == "win32": + libs.extend(["ntdll", "userenv", "ws2_32", "bcrypt"]) + self._update_extension("PIL._avif", libs) + else: + self._remove_extension("PIL._avif") + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] self._update_extension("PIL._imagingtk", tk_libs) @@ -976,6 +993,7 @@ class pil_build_ext(build_ext): (feature.get("lcms"), "LITTLECMS2"), (feature.get("webp"), "WEBP"), (feature.get("xcb"), "XCB (X protocol)"), + (feature.get("avif"), "LIBAVIF"), ] all = 1 @@ -1018,6 +1036,7 @@ ext_modules = [ Extension("PIL._imagingft", ["src/_imagingft.c"]), Extension("PIL._imagingcms", ["src/_imagingcms.c"]), Extension("PIL._webp", ["src/_webp.c"]), + Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), Extension("PIL._imagingmath", ["src/_imagingmath.c"]), Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py new file mode 100644 index 000000000..b2c5ab15d --- /dev/null +++ b/src/PIL/AvifImagePlugin.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +import os +from io import BytesIO +from typing import IO + +from . import ExifTags, Image, ImageFile + +try: + from . import _avif + + SUPPORTED = True +except ImportError: + SUPPORTED = False + +# Decoder options as module globals, until there is a way to pass parameters +# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) +DECODE_CODEC_CHOICE = "auto" +# Decoding is only affected by this for libavif **0.8.4** or greater. +DEFAULT_MAX_THREADS = 0 + + +def get_codec_version(codec_name: str) -> str | None: + versions = _avif.codec_versions() + for version in versions.split(", "): + if version.split(" [")[0] == codec_name: + return version.split(":")[-1].split(" ")[0] + return None + + +def _accept(prefix: bytes) -> bool | str: + if prefix[4:8] != b"ftyp": + return False + major_brand = prefix[8:12] + if major_brand in ( + # coding brands + b"avif", + b"avis", + # 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. + b"mif1", + b"msf1", + ): + if not SUPPORTED: + return ( + "image file could not be identified because AVIF support not installed" + ) + return True + return False + + +def _get_default_max_threads() -> int: + if DEFAULT_MAX_THREADS: + return DEFAULT_MAX_THREADS + if hasattr(os, "sched_getaffinity"): + return len(os.sched_getaffinity(0)) + else: + return os.cpu_count() or 1 + + +class AvifImageFile(ImageFile.ImageFile): + format = "AVIF" + format_description = "AVIF image" + __frame = -1 + + def _open(self) -> None: + if not SUPPORTED: + msg = "image file could not be opened because AVIF support not installed" + raise SyntaxError(msg) + + if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( + DECODE_CODEC_CHOICE + ): + msg = "Invalid opening codec" + raise ValueError(msg) + self._decoder = _avif.AvifDecoder( + self.fp.read(), + DECODE_CODEC_CHOICE, + _get_default_max_threads(), + ) + + # Get info from decoder + self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( + self._decoder.get_info() + ) + self.is_animated = self.n_frames > 1 + + if icc: + self.info["icc_profile"] = icc + if xmp: + self.info["xmp"] = xmp + + if exif_orientation != 1 or exif: + exif_data = Image.Exif() + if exif: + exif_data.load(exif) + original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) + else: + original_orientation = 1 + if exif_orientation != original_orientation: + exif_data[ExifTags.Base.Orientation] = exif_orientation + exif = exif_data.tobytes() + if exif: + self.info["exif"] = exif + self.seek(0) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + # Set tile + self.__frame = frame + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] + + def load(self) -> Image.core.PixelAccess | None: + if self.tile: + # We need to load the image data for this frame + data, timescale, pts_in_timescales, duration_in_timescales = ( + self._decoder.get_frame(self.__frame) + ) + self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) + self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) + + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + + return super().load() + + def load_seek(self, pos: int) -> None: + pass + + def tell(self) -> int: + return self.__frame + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: + info = im.encoderinfo.copy() + if save_all: + append_images = list(info.get("append_images", [])) + else: + append_images = [] + + total = 0 + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) + + quality = info.get("quality", 75) + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + + duration = info.get("duration", 0) + subsampling = info.get("subsampling", "4:2:0") + speed = info.get("speed", 6) + max_threads = info.get("max_threads", _get_default_max_threads()) + codec = info.get("codec", "auto") + if codec != "auto" and not _avif.encoder_codec_available(codec): + msg = "Invalid saving codec" + raise ValueError(msg) + range_ = info.get("range", "full") + tile_rows_log2 = info.get("tile_rows", 0) + tile_cols_log2 = info.get("tile_cols", 0) + alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) + autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) + + icc_profile = info.get("icc_profile", im.info.get("icc_profile")) + exif_orientation = 1 + if exif := info.get("exif"): + if isinstance(exif, Image.Exif): + exif_data = exif + else: + exif_data = Image.Exif() + exif_data.load(exif) + if ExifTags.Base.Orientation in exif_data: + exif_orientation = exif_data.pop(ExifTags.Base.Orientation) + exif = exif_data.tobytes() if exif_data else b"" + elif isinstance(exif, Image.Exif): + exif = exif_data.tobytes() + + xmp = info.get("xmp") + + if isinstance(xmp, str): + xmp = xmp.encode("utf-8") + + advanced = info.get("advanced") + if advanced is not None: + if isinstance(advanced, dict): + advanced = advanced.items() + try: + advanced = tuple(advanced) + except TypeError: + invalid = True + else: + invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) + if invalid: + msg = ( + "advanced codec options must be a dict of key-value string " + "pairs or a series of key-value two-tuples" + ) + raise ValueError(msg) + + # Setup the AVIF encoder + enc = _avif.AvifEncoder( + im.size, + subsampling, + quality, + speed, + max_threads, + codec, + range_, + tile_rows_log2, + tile_cols_log2, + alpha_premultiplied, + autotiling, + icc_profile or b"", + exif or b"", + exif_orientation, + xmp or b"", + advanced, + ) + + # Add each frame + frame_idx = 0 + frame_duration = 0 + cur_idx = im.tell() + is_single_frame = total == 1 + try: + for ims in [im] + append_images: + # Get number of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + + # Make sure image mode is supported + frame = ims + rawmode = ims.mode + if ims.mode not in {"RGB", "RGBA"}: + rawmode = "RGBA" if ims.has_transparency_data else "RGB" + frame = ims.convert(rawmode) + + # Update frame duration + if isinstance(duration, (list, tuple)): + frame_duration = duration[frame_idx] + else: + frame_duration = duration + + # Append the frame to the animation encoder + enc.add( + frame.tobytes("raw", rawmode), + frame_duration, + frame.size, + rawmode, + is_single_frame, + ) + + # Update frame index + frame_idx += 1 + + if not save_all: + break + + finally: + im.seek(cur_idx) + + # Get the final output from the encoder + data = enc.finish() + if data is None: + msg = "cannot write file as AVIF (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +Image.register_open(AvifImageFile.format, AvifImageFile, _accept) +if SUPPORTED: + Image.register_save(AvifImageFile.format, _save) + Image.register_save_all(AvifImageFile.format, _save_all) + Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) + Image.register_mime(AvifImageFile.format, "image/avif") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 19b22342a..60850f4ff 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1520,6 +1520,8 @@ class Image: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + if not xmp_tags and (xmp_tags := self.info.get("xmp")): + xmp_tags = xmp_tags.decode("utf-8") if xmp_tags: match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 09546fe63..6e4c23f89 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -25,6 +25,7 @@ del _version _plugins = [ + "AvifImagePlugin", "BlpImagePlugin", "BmpImagePlugin", "BufrStubImagePlugin", diff --git a/src/PIL/_avif.pyi b/src/PIL/_avif.pyi new file mode 100644 index 000000000..e27843e53 --- /dev/null +++ b/src/PIL/_avif.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/features.py b/src/PIL/features.py index ae7ea4255..573f1d412 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -17,6 +17,7 @@ modules = { "freetype2": ("PIL._imagingft", "freetype2_version"), "littlecms2": ("PIL._imagingcms", "littlecms_version"), "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), } @@ -288,6 +289,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), + ("avif", "AVIF"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), diff --git a/src/_avif.c b/src/_avif.c new file mode 100644 index 000000000..eabd9958e --- /dev/null +++ b/src/_avif.c @@ -0,0 +1,908 @@ +#define PY_SSIZE_T_CLEAN + +#include +#include "avif/avif.h" + +// Encoder type +typedef struct { + PyObject_HEAD avifEncoder *encoder; + avifImage *image; + int first_frame; +} AvifEncoderObject; + +static PyTypeObject AvifEncoder_Type; + +// Decoder type +typedef struct { + PyObject_HEAD avifDecoder *decoder; + Py_buffer buffer; +} AvifDecoderObject; + +static PyTypeObject AvifDecoder_Type; + +static int +normalize_tiles_log2(int value) { + if (value < 0) { + return 0; + } else if (value > 6) { + return 6; + } else { + return value; + } +} + +static PyObject * +exc_type_for_avif_result(avifResult result) { + switch (result) { + case AVIF_RESULT_INVALID_EXIF_PAYLOAD: + case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION: + return PyExc_ValueError; + case AVIF_RESULT_INVALID_FTYP: + case AVIF_RESULT_BMFF_PARSE_FAILED: + case AVIF_RESULT_TRUNCATED_DATA: + case AVIF_RESULT_NO_CONTENT: + return PyExc_SyntaxError; + default: + return PyExc_RuntimeError; + } +} + +static uint8_t +irot_imir_to_exif_orientation(const avifImage *image) { + uint8_t axis = image->imir.axis; + int imir = image->transformFlags & AVIF_TRANSFORM_IMIR; + int irot = image->transformFlags & AVIF_TRANSFORM_IROT; + if (irot) { + uint8_t angle = image->irot.angle; + if (angle == 1) { + if (imir) { + return axis ? 7 // 90 degrees anti-clockwise then swap left and right. + : 5; // 90 degrees anti-clockwise then swap top and bottom. + } + return 6; // 90 degrees anti-clockwise. + } + if (angle == 2) { + if (imir) { + return axis + ? 4 // 180 degrees anti-clockwise then swap left and right. + : 2; // 180 degrees anti-clockwise then swap top and bottom. + } + return 3; // 180 degrees anti-clockwise. + } + if (angle == 3) { + if (imir) { + return axis + ? 5 // 270 degrees anti-clockwise then swap left and right. + : 7; // 270 degrees anti-clockwise then swap top and bottom. + } + return 8; // 270 degrees anti-clockwise. + } + } + if (imir) { + return axis ? 2 // Swap left and right. + : 4; // Swap top and bottom. + } + return 1; // Default orientation ("top-left", no-op). +} + +static void +exif_orientation_to_irot_imir(avifImage *image, int orientation) { + // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A + // Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 + // sections 6.5.10 and 6.5.12. + switch (orientation) { + case 2: // The 0th row is at the visual top of the image, and the 0th column is + // the visual right-hand side. + image->transformFlags |= AVIF_TRANSFORM_IMIR; + image->imir.axis = 1; + break; + case 3: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual right-hand side. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 2; + break; + case 4: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual left-hand side. + image->transformFlags |= AVIF_TRANSFORM_IMIR; + break; + case 5: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 1; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 + break; + case 6: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 3; + break; + case 7: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 3; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 + break; + case 8: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 1; + break; + } +} + +static int +_codec_available(const char *name, avifCodecFlags flags) { + avifCodecChoice codec = avifCodecChoiceFromName(name); + if (codec == AVIF_CODEC_CHOICE_AUTO) { + return 0; + } + const char *codec_name = avifCodecName(codec, flags); + return (codec_name == NULL) ? 0 : 1; +} + +PyObject * +_decoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_encoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_codec_versions(PyObject *self, PyObject *args) { + char buffer[256]; + avifCodecVersions(buffer); + return PyUnicode_FromString(buffer); +} + +static int +_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { + Py_ssize_t i, size; + PyObject *keyval, *py_key, *py_val; + if (!PyTuple_Check(opts)) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + size = PyTuple_GET_SIZE(opts); + + for (i = 0; i < size; i++) { + keyval = PyTuple_GetItem(opts, i); + if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + py_key = PyTuple_GetItem(keyval, 0); + py_val = PyTuple_GetItem(keyval, 1); + if (!PyUnicode_Check(py_key) || !PyUnicode_Check(py_val)) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + const char *key = PyUnicode_AsUTF8(py_key); + const char *val = PyUnicode_AsUTF8(py_val); + if (key == NULL || val == NULL) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + + avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting advanced codec options failed: %s", + avifResultToString(result) + ); + return 1; + } + } + return 0; +} + +// Encoder functions +PyObject * +AvifEncoderNew(PyObject *self_, PyObject *args) { + unsigned int width, height; + AvifEncoderObject *self = NULL; + avifEncoder *encoder = NULL; + + char *subsampling; + int quality; + int speed; + int exif_orientation; + int max_threads; + Py_buffer icc_buffer; + Py_buffer exif_buffer; + Py_buffer xmp_buffer; + int alpha_premultiplied; + int autotiling; + int tile_rows_log2; + int tile_cols_log2; + + char *codec; + char *range; + + PyObject *advanced; + int error = 0; + + if (!PyArg_ParseTuple( + args, + "(II)siiissiippy*y*iy*O", + &width, + &height, + &subsampling, + &quality, + &speed, + &max_threads, + &codec, + &range, + &tile_rows_log2, + &tile_cols_log2, + &alpha_premultiplied, + &autotiling, + &icc_buffer, + &exif_buffer, + &exif_orientation, + &xmp_buffer, + &advanced + )) { + return NULL; + } + + // Create a new animation encoder and picture frame + avifImage *image = avifImageCreateEmpty(); + if (image == NULL) { + PyErr_SetString(PyExc_ValueError, "Image creation failed"); + error = 1; + goto end; + } + + // Set these in advance so any upcoming RGB -> YUV use the proper coefficients + if (strcmp(range, "full") == 0) { + image->yuvRange = AVIF_RANGE_FULL; + } else if (strcmp(range, "limited") == 0) { + image->yuvRange = AVIF_RANGE_LIMITED; + } else { + PyErr_SetString(PyExc_ValueError, "Invalid range"); + error = 1; + goto end; + } + if (strcmp(subsampling, "4:0:0") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; + } else if (strcmp(subsampling, "4:2:0") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420; + } else if (strcmp(subsampling, "4:2:2") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422; + } else if (strcmp(subsampling, "4:4:4") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; + } else { + PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); + error = 1; + goto end; + } + + // Validate canvas dimensions + if (width == 0 || height == 0) { + PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); + error = 1; + goto end; + } + image->width = width; + image->height = height; + + image->depth = 8; + image->alphaPremultiplied = alpha_premultiplied ? AVIF_TRUE : AVIF_FALSE; + + encoder = avifEncoderCreate(); + if (!encoder) { + PyErr_SetString(PyExc_MemoryError, "Can't allocate encoder"); + error = 1; + goto end; + } + + int is_aom_encode = strcmp(codec, "aom") == 0 || + (strcmp(codec, "auto") == 0 && + _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE)); + encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads; + + encoder->quality = quality; + + if (strcmp(codec, "auto") == 0) { + encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO; + } else { + encoder->codecChoice = avifCodecChoiceFromName(codec); + } + if (speed < AVIF_SPEED_SLOWEST) { + speed = AVIF_SPEED_SLOWEST; + } else if (speed > AVIF_SPEED_FASTEST) { + speed = AVIF_SPEED_FASTEST; + } + encoder->speed = speed; + encoder->timescale = (uint64_t)1000; + + encoder->autoTiling = autotiling ? AVIF_TRUE : AVIF_FALSE; + if (!autotiling) { + encoder->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2); + encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2); + } + + if (advanced != Py_None && _add_codec_specific_options(encoder, advanced)) { + error = 1; + goto end; + } + + self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); + error = 1; + goto end; + } + self->first_frame = 1; + + avifResult result; + if (icc_buffer.len) { + result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting ICC profile failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + // colorPrimaries and transferCharacteristics are ignored when an ICC + // profile is present, so set them to UNSPECIFIED. + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + } else { + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + } + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + + if (exif_buffer.len) { + result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting EXIF data failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + } + + if (xmp_buffer.len) { + result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting XMP data failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + } + + if (exif_orientation > 1) { + exif_orientation_to_irot_imir(image, exif_orientation); + } + + self->image = image; + self->encoder = encoder; + +end: + PyBuffer_Release(&icc_buffer); + PyBuffer_Release(&exif_buffer); + PyBuffer_Release(&xmp_buffer); + + if (error) { + if (image) { + avifImageDestroy(image); + } + if (encoder) { + avifEncoderDestroy(encoder); + } + if (self) { + PyObject_Del(self); + } + return NULL; + } + + return (PyObject *)self; +} + +PyObject * +_encoder_dealloc(AvifEncoderObject *self) { + if (self->encoder) { + avifEncoderDestroy(self->encoder); + } + if (self->image) { + avifImageDestroy(self->image); + } + Py_RETURN_NONE; +} + +PyObject * +_encoder_add(AvifEncoderObject *self, PyObject *args) { + uint8_t *rgb_bytes; + Py_ssize_t size; + unsigned int duration; + unsigned int width; + unsigned int height; + char *mode; + unsigned int is_single_frame; + int error = 0; + + avifRGBImage rgb; + avifResult result; + + avifEncoder *encoder = self->encoder; + avifImage *image = self->image; + avifImage *frame = NULL; + + if (!PyArg_ParseTuple( + args, + "y#I(II)sp", + (char **)&rgb_bytes, + &size, + &duration, + &width, + &height, + &mode, + &is_single_frame + )) { + return NULL; + } + + if (image->width != width || image->height != height) { + PyErr_Format( + PyExc_ValueError, + "Image sequence dimensions mismatch, %ux%u != %ux%u", + image->width, + image->height, + width, + height + ); + return NULL; + } + + if (self->first_frame) { + // If we don't have an image populated with yuv planes, this is the first frame + frame = image; + } else { + frame = avifImageCreateEmpty(); + if (image == NULL) { + PyErr_SetString(PyExc_ValueError, "Image creation failed"); + return NULL; + } + + frame->width = width; + frame->height = height; + frame->colorPrimaries = image->colorPrimaries; + frame->transferCharacteristics = image->transferCharacteristics; + frame->matrixCoefficients = image->matrixCoefficients; + frame->yuvRange = image->yuvRange; + frame->yuvFormat = image->yuvFormat; + frame->depth = image->depth; + frame->alphaPremultiplied = image->alphaPremultiplied; + } + + avifRGBImageSetDefaults(&rgb, frame); + + if (strcmp(mode, "RGBA") == 0) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + } + + result = avifRGBImageAllocatePixels(&rgb); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Pixel allocation failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + + if (rgb.rowBytes * rgb.height != size) { + PyErr_Format( + PyExc_RuntimeError, + "rgb data has incorrect size: %u * %u (%u) != %u", + rgb.rowBytes, + rgb.height, + rgb.rowBytes * rgb.height, + size + ); + error = 1; + goto end; + } + + // rgb.pixels is safe for writes + memcpy(rgb.pixels, rgb_bytes, size); + + Py_BEGIN_ALLOW_THREADS; + result = avifImageRGBToYUV(frame, &rgb); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion to YUV failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + + uint32_t addImageFlags = + is_single_frame ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE; + + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderAddImage(encoder, frame, duration, addImageFlags); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to encode image: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + +end: + if (&rgb) { + avifRGBImageFreePixels(&rgb); + } + if (!self->first_frame) { + avifImageDestroy(frame); + } + + if (error) { + return NULL; + } + self->first_frame = 0; + Py_RETURN_NONE; +} + +PyObject * +_encoder_finish(AvifEncoderObject *self) { + avifEncoder *encoder = self->encoder; + + avifRWData raw = AVIF_DATA_EMPTY; + avifResult result; + PyObject *ret = NULL; + + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderFinish(encoder, &raw); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to finish encoding: %s", + avifResultToString(result) + ); + avifRWDataFree(&raw); + return NULL; + } + + ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size); + + avifRWDataFree(&raw); + + return ret; +} + +// Decoder functions +PyObject * +AvifDecoderNew(PyObject *self_, PyObject *args) { + Py_buffer buffer; + AvifDecoderObject *self = NULL; + avifDecoder *decoder; + + char *codec_str; + avifCodecChoice codec; + int max_threads; + + avifResult result; + + if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) { + return NULL; + } + + if (strcmp(codec_str, "auto") == 0) { + codec = AVIF_CODEC_CHOICE_AUTO; + } else { + codec = avifCodecChoiceFromName(codec_str); + } + + self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); + PyBuffer_Release(&buffer); + return NULL; + } + + decoder = avifDecoderCreate(); + if (!decoder) { + PyErr_SetString(PyExc_MemoryError, "Can't allocate decoder"); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + decoder->maxThreads = max_threads; + // Turn off libavif's 'clap' (clean aperture) property validation. + 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. + decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; + decoder->codecChoice = codec; + + result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting IO memory failed: %s", + avifResultToString(result) + ); + avifDecoderDestroy(decoder); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + + result = avifDecoderParse(decoder); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode image: %s", + avifResultToString(result) + ); + avifDecoderDestroy(decoder); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + + self->decoder = decoder; + self->buffer = buffer; + + return (PyObject *)self; +} + +PyObject * +_decoder_dealloc(AvifDecoderObject *self) { + if (self->decoder) { + avifDecoderDestroy(self->decoder); + } + PyBuffer_Release(&self->buffer); + Py_RETURN_NONE; +} + +PyObject * +_decoder_get_info(AvifDecoderObject *self) { + avifDecoder *decoder = self->decoder; + avifImage *image = decoder->image; + + PyObject *icc = NULL; + PyObject *exif = NULL; + PyObject *xmp = NULL; + PyObject *ret = NULL; + + if (image->xmp.size) { + xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); + } + + if (image->exif.size) { + exif = + PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + } + + if (image->icc.size) { + icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + } + + ret = Py_BuildValue( + "(II)IsSSIS", + image->width, + image->height, + decoder->imageCount, + decoder->alphaPresent ? "RGBA" : "RGB", + NULL == icc ? Py_None : icc, + NULL == exif ? Py_None : exif, + irot_imir_to_exif_orientation(image), + NULL == xmp ? Py_None : xmp + ); + + Py_XDECREF(xmp); + Py_XDECREF(exif); + Py_XDECREF(icc); + + return ret; +} + +PyObject * +_decoder_get_frame(AvifDecoderObject *self, PyObject *args) { + PyObject *bytes; + PyObject *ret; + Py_ssize_t size; + avifResult result; + avifRGBImage rgb; + avifDecoder *decoder; + avifImage *image; + uint32_t frame_index; + + decoder = self->decoder; + + if (!PyArg_ParseTuple(args, "I", &frame_index)) { + return NULL; + } + + result = avifDecoderNthImage(decoder, frame_index); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode frame %u: %s", + frame_index, + avifResultToString(result) + ); + return NULL; + } + + image = decoder->image; + + avifRGBImageSetDefaults(&rgb, image); + + rgb.depth = 8; + rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + + result = avifRGBImageAllocatePixels(&rgb); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Pixel allocation failed: %s", + avifResultToString(result) + ); + return NULL; + } + + Py_BEGIN_ALLOW_THREADS; + result = avifImageYUVToRGB(image, &rgb); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion from YUV failed: %s", + avifResultToString(result) + ); + avifRGBImageFreePixels(&rgb); + return NULL; + } + + if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); + return NULL; + } + + size = rgb.rowBytes * rgb.height; + + bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); + avifRGBImageFreePixels(&rgb); + + ret = Py_BuildValue( + "SKKK", + bytes, + decoder->timescale, + decoder->imageTiming.ptsInTimescales, + decoder->imageTiming.durationInTimescales + ); + + Py_DECREF(bytes); + + return ret; +} + +/* -------------------------------------------------------------------- */ +/* Type Definitions */ +/* -------------------------------------------------------------------- */ + +// AvifEncoder methods +static struct PyMethodDef _encoder_methods[] = { + {"add", (PyCFunction)_encoder_add, METH_VARARGS}, + {"finish", (PyCFunction)_encoder_finish, METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifEncoder type definition +static PyTypeObject AvifEncoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder", + .tp_basicsize = sizeof(AvifEncoderObject), + .tp_dealloc = (destructor)_encoder_dealloc, + .tp_methods = _encoder_methods, +}; + +// AvifDecoder methods +static struct PyMethodDef _decoder_methods[] = { + {"get_info", (PyCFunction)_decoder_get_info, METH_NOARGS}, + {"get_frame", (PyCFunction)_decoder_get_frame, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifDecoder type definition +static PyTypeObject AvifDecoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder", + .tp_basicsize = sizeof(AvifDecoderObject), + .tp_dealloc = (destructor)_decoder_dealloc, + .tp_methods = _decoder_methods, +}; + +/* -------------------------------------------------------------------- */ +/* Module Setup */ +/* -------------------------------------------------------------------- */ + +static PyMethodDef avifMethods[] = { + {"AvifDecoder", AvifDecoderNew, METH_VARARGS}, + {"AvifEncoder", AvifEncoderNew, METH_VARARGS}, + {"decoder_codec_available", _decoder_codec_available, METH_VARARGS}, + {"encoder_codec_available", _encoder_codec_available, METH_VARARGS}, + {"codec_versions", _codec_versions, METH_NOARGS}, + {NULL, NULL} +}; + +static int +setup_module(PyObject *m) { + if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { + return -1; + } + + PyObject *d = PyModule_GetDict(m); + PyObject *v = PyUnicode_FromString(avifVersion()); + PyDict_SetItemString(d, "libavif_version", v ? v : Py_None); + Py_XDECREF(v); + + return 0; +} + +PyMODINIT_FUNC +PyInit__avif(void) { + PyObject *m; + + static PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + .m_name = "_avif", + .m_size = -1, + .m_methods = avifMethods, + }; + + m = PyModule_Create(&module_def); + if (setup_module(m) < 0) { + Py_DECREF(m); + return NULL; + } + +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + + return m; +} diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt new file mode 100644 index 000000000..3a2e46c26 --- /dev/null +++ b/wheels/dependency_licenses/AOM.txt @@ -0,0 +1,26 @@ +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt new file mode 100644 index 000000000..875b138ec --- /dev/null +++ b/wheels/dependency_licenses/DAV1D.txt @@ -0,0 +1,23 @@ +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt new file mode 100644 index 000000000..350eb9d15 --- /dev/null +++ b/wheels/dependency_licenses/LIBAVIF.txt @@ -0,0 +1,387 @@ +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt new file mode 100644 index 000000000..c911747a6 --- /dev/null +++ b/wheels/dependency_licenses/LIBYUV.txt @@ -0,0 +1,29 @@ +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/RAV1E.txt b/wheels/dependency_licenses/RAV1E.txt new file mode 100644 index 000000000..3d6c825c4 --- /dev/null +++ b/wheels/dependency_licenses/RAV1E.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2017-2023, the rav1e contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/SVT-AV1.txt b/wheels/dependency_licenses/SVT-AV1.txt new file mode 100644 index 000000000..532a982b3 --- /dev/null +++ b/wheels/dependency_licenses/SVT-AV1.txt @@ -0,0 +1,26 @@ +Copyright (c) 2019, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/winbuild/build.rst b/winbuild/build.rst index aae78ce12..3c20c7d17 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -61,6 +61,7 @@ Run ``build_prepare.py`` to configure the build:: --no-imagequant skip GPL-licensed optional dependency libimagequant --no-fribidi, --no-raqm skip LGPL-licensed optional dependency FriBiDi + --no-avif skip optional dependency libavif Arguments can also be supplied using the environment variables PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2e9e18719..e4901859e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,6 +116,7 @@ V = { "HARFBUZZ": "11.0.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", + "LIBAVIF": "1.2.1", "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.47", "LIBWEBP": "1.5.0", @@ -378,6 +379,26 @@ DEPS: dict[str, dict[str, Any]] = { ], "bins": [r"*.dll"], }, + "libavif": { + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", + "filename": f"libavif-{V['LIBAVIF']}.zip", + "license": "LICENSE", + "build": [ + f"{sys.executable} -m pip install meson", + *cmds_cmake( + "avif_static", + "-DBUILD_SHARED_LIBS=OFF", + "-DAVIF_LIBSHARPYUV=LOCAL", + "-DAVIF_LIBYUV=LOCAL", + "-DAVIF_CODEC_AOM=LOCAL", + "-DAVIF_CODEC_DAV1D=LOCAL", + "-DAVIF_CODEC_RAV1E=LOCAL", + "-DAVIF_CODEC_SVT=LOCAL", + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": ["avif.lib"], + }, } @@ -683,6 +704,11 @@ def main() -> None: action="store_true", help="skip LGPL-licensed optional dependency FriBiDi", ) + parser.add_argument( + "--no-avif", + action="store_true", + help="skip optional dependency libavif", + ) args = parser.parse_args() arch_prefs = ARCHITECTURES[args.architecture] @@ -723,6 +749,8 @@ def main() -> None: disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] + if args.no_avif or args.architecture != "AMD64": + disabled += ["libavif"] prefs = { "architecture": args.architecture, From 81412212016a70eb160460e26dc552a0f8a8c153 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 08:35:19 +1100 Subject: [PATCH 08/10] Allow cmake<4 when building libtiff --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e4901859e..b45148ee8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -235,6 +235,7 @@ DEPS: dict[str, dict[str, Any]] = { "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ) ], "headers": [r"libtiff\tiff*.h"], From 348bf6550d3937d14bbd04251c12bed6dfed9eec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 16:33:55 +1100 Subject: [PATCH 09/10] Allow cmake<4 when building libavif --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b45148ee8..e118cd994 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -395,6 +395,7 @@ DEPS: dict[str, dict[str, Any]] = { "-DAVIF_CODEC_DAV1D=LOCAL", "-DAVIF_CODEC_RAV1E=LOCAL", "-DAVIF_CODEC_SVT=LOCAL", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ), cmd_xcopy("include", "{inc_dir}"), ], From 5c76e7ec17813eefaa1fdd8948e0165ee644e11f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 1 Apr 2025 07:10:45 +0100 Subject: [PATCH 10/10] Image -> Arrow support (#8330) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .ci/install.sh | 3 + .github/workflows/macos-install.sh | 3 + .github/workflows/test-windows.yml | 4 + Tests/test_arrow.py | 164 ++++++++++++++++ Tests/test_pyarrow.py | 112 +++++++++++ docs/reference/Image.rst | 3 + docs/reference/arrow_support.rst | 88 +++++++++ docs/reference/block_allocator.rst | 3 + docs/reference/internal_design.rst | 1 + pyproject.toml | 5 + setup.py | 1 + src/PIL/Image.py | 80 ++++++++ src/_imaging.c | 115 +++++++++++ src/libImaging/Arrow.c | 299 +++++++++++++++++++++++++++++ src/libImaging/Arrow.h | 48 +++++ src/libImaging/Imaging.h | 38 ++++ src/libImaging/Storage.c | 199 ++++++++++++++++++- 17 files changed, 1165 insertions(+), 1 deletion(-) create mode 100644 Tests/test_arrow.py create mode 100644 Tests/test_pyarrow.py create mode 100644 docs/reference/arrow_support.rst create mode 100644 src/libImaging/Arrow.c create mode 100644 src/libImaging/Arrow.h diff --git a/.ci/install.sh b/.ci/install.sh index 83d5df01c..ba32eab04 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -36,6 +36,9 @@ python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma +# optional test dependency, only install if there's a binary package. +# fails on beta 3.14 and PyPy +python3 -m pip install --only-binary=:all: pyarrow || true if [[ $(uname) != CYGWIN* ]]; then python3 -m pip install numpy diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 099f4a582..94e3d5d08 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -30,6 +30,9 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install numpy +# optional test dependency, only install if there's a binary package. +# fails on beta 3.14 and PyPy +python3 -m pip install --only-binary=:all: pyarrow || true # libavif pushd depends && ./install_libavif.sh && popd diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0c3f44e96..bf8ec2f2c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -88,6 +88,10 @@ jobs: run: | python3 -m pip install PyQt6 + - name: Install PyArrow dependency + run: | + python3 -m pip install --only-binary=:all: pyarrow || true + - name: Install dependencies id: install run: | diff --git a/Tests/test_arrow.py b/Tests/test_arrow.py new file mode 100644 index 000000000..b86c77b9a --- /dev/null +++ b/Tests/test_arrow.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import hopper + + +@pytest.mark.parametrize( + "mode, dest_modes", + ( + ("L", ["I", "F", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), + ("I", ["L", "F"]), # Technically I;32 can work for any 4x8bit storage. + ("F", ["I", "L", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), + ("LA", ["L", "F"]), + ("RGB", ["L", "F"]), + ("RGBA", ["L", "F"]), + ("RGBX", ["L", "F"]), + ("CMYK", ["L", "F"]), + ("YCbCr", ["L", "F"]), + ("HSV", ["L", "F"]), + ), +) +def test_invalid_array_type(mode: str, dest_modes: list[str]) -> None: + img = hopper(mode) + for dest_mode in dest_modes: + with pytest.raises(ValueError): + Image.fromarrow(img, dest_mode, img.size) + + +def test_invalid_array_size() -> None: + img = hopper("RGB") + + assert img.size != (10, 10) + with pytest.raises(ValueError): + Image.fromarrow(img, "RGB", (10, 10)) + + +def test_release_schema() -> None: + # these should not error out, valgrind should be clean + img = hopper("L") + schema = img.__arrow_c_schema__() + del schema + + +def test_release_array() -> None: + # these should not error out, valgrind should be clean + img = hopper("L") + array, schema = img.__arrow_c_array__() + del array + del schema + + +def test_readonly() -> None: + img = hopper("L") + reloaded = Image.fromarrow(img, img.mode, img.size) + assert reloaded.readonly == 1 + reloaded._readonly = 0 + assert reloaded.readonly == 1 + + +def test_multiblock_l_image() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + + with pytest.raises(ValueError): + (schema, arr) = img.__arrow_c_array__() + + +def test_multiblock_rgba_image() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + + with pytest.raises(ValueError): + (schema, arr) = img.__arrow_c_array__() + + +def test_multiblock_l_schema() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + + with pytest.raises(ValueError): + img.__arrow_c_schema__() + + +def test_multiblock_rgba_schema() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + + with pytest.raises(ValueError): + img.__arrow_c_schema__() + + +def test_singleblock_l_image() -> None: + Image.core.set_use_block_allocator(1) + + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, 2 * (block_size // 4096)) + img = Image.new("L", size, 128) + assert img.im.isblock() + + (schema, arr) = img.__arrow_c_array__() + assert schema + assert arr + + Image.core.set_use_block_allocator(0) + + +def test_singleblock_rgba_image() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + assert img.im.isblock() + + (schema, arr) = img.__arrow_c_array__() + assert schema + assert arr + Image.core.set_use_block_allocator(0) + + +def test_singleblock_l_schema() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + assert img.im.isblock() + + schema = img.__arrow_c_schema__() + assert schema + Image.core.set_use_block_allocator(0) + + +def test_singleblock_rgba_schema() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + assert img.im.isblock() + + schema = img.__arrow_c_schema__() + assert schema + Image.core.set_use_block_allocator(0) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py new file mode 100644 index 000000000..ece9f8f26 --- /dev/null +++ b/Tests/test_pyarrow.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import Any # undone + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, +) + +pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None +) -> None: + assert img.height * img.width == len(arr) + px = img.load() + assert px is not None + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + if mask: + for ix, elt in enumerate(mask): + pixel = px[x, y] + assert isinstance(pixel, tuple) + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +# really hard to get a non-nullable list type +fl_uint8_4_type = pyarrow.field( + "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) +).type + + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", pyarrow.uint8(), None), + ("I", pyarrow.int32(), None), + ("F", pyarrow.float32(), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = pyarrow.array(img) + _test_img_equals_pyarray(img, arr, mask) + assert arr.type == dtype + + reloaded = Image.fromarrow(arr, mode, img.size) + + assert reloaded + + assert_image_equal(img, reloaded) + + +def test_lifetime() -> None: + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = pyarrow.array(img) + arr_2 = pyarrow.array(img) + + del img + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + +def test_lifetime2() -> None: + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = pyarrow.array(img) + arr_2 = pyarrow.array(img) + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert px # make mypy happy + assert isinstance(px[0, 0], int) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index bc3758218..a3ba8cfd8 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -79,6 +79,7 @@ Constructing images .. autofunction:: new .. autofunction:: fromarray +.. autofunction:: fromarrow .. autofunction:: frombytes .. autofunction:: frombuffer @@ -370,6 +371,8 @@ Protocols .. autoclass:: SupportsArrayInterface :show-inheritance: +.. autoclass:: SupportsArrowArrayInterface + :show-inheritance: .. autoclass:: SupportsGetData :show-inheritance: diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst new file mode 100644 index 000000000..4a5c45e86 --- /dev/null +++ b/docs/reference/arrow_support.rst @@ -0,0 +1,88 @@ +.. _arrow-support: + +============= +Arrow Support +============= + +`Arrow `__ +is an in-memory data exchange format that is the spiritual +successor to the NumPy array interface. It provides for zero-copy +access to columnar data, which in our case is ``Image`` data. + +The goal with Arrow is to provide native zero-copy interoperability +with any Arrow provider or consumer in the Python ecosystem. + +.. warning:: Zero-copy does not mean zero allocation -- the internal + memory layout of Pillow images contains an allocation for row + pointers, so there is a non-zero, but significantly smaller than a + full-copy memory cost to reading an Arrow image. + + +Data Formats +============ + +Pillow currently supports exporting Arrow images in all modes +**except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to +line-length packing in these modes making for non-continuous memory. + +For single-band images, the exported array is width*height elements, +with each pixel corresponding to the appropriate Arrow type. + +For multiband images, the exported array is width*height fixed-length +four-element arrays of uint8. This is memory compatible with the raw +image storage of four bytes per pixel. + +Mode ``1`` images are exported as one uint8 byte/pixel, as this is +consistent with the internal storage. + +Pillow will accept, but not produce, one other format. For any +multichannel image with 32-bit storage per pixel, Pillow will accept +an array of width*height int32 elements, which will then be +interpreted using the mode-specific interpretation of the bytes. + +The image mode must match the Arrow band format when reading single +channel images. + +Memory Allocator +================ + +Pillow's default memory allocator, the :ref:`block_allocator`, +allocates up to a 16 MB block for images by default. Larger images +overflow into additional blocks. Arrow requires a single continuous +memory allocation, so images allocated in multiple blocks cannot be +exported in the Arrow format. + +To enable the single block allocator:: + + from PIL import Image + Image.core.set_use_block_allocator(1) + +Note that this is a global setting, not a per-image setting. + +Unsupported Features +==================== + +* Table/dataframe protocol. We support a single array. +* Null markers, producing or consuming. Null values are inferred from + the mode, e.g. RGB images are stored in the first three bytes of + each 32-bit pixel, and the last byte is an implied null. +* Schema negotiation. There is an optional schema for the requested + datatype in the Arrow source interface. We ignore that + parameter. +* Array metadata. + +Internal Details +================ + +Python Arrow C interface: +https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html + +The memory that is exported from the Arrow interface is shared -- not +copied, so the lifetime of the memory allocation is no longer strictly +tied to the life of the Python object. + +The core imaging struct now has a refcount associated with it, and the +lifetime of the core image struct is now divorced from the Python +image object. Creating an arrow reference to the image increments the +refcount, and the imaging struct is only released when the refcount +reaches zero. diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index 1abe5280f..f4d27e24e 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -1,3 +1,6 @@ + +.. _block_allocator: + Block Allocator =============== diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 99a18e9ea..041177953 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -9,3 +9,4 @@ Internal Reference block_allocator internal_modules c_extension_debugging + arrow_support diff --git a/pyproject.toml b/pyproject.toml index 780a938a3..856419215 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,10 @@ optional-dependencies.fpx = [ optional-dependencies.mic = [ "olefile", ] +optional-dependencies.test-arrow = [ + "pyarrow", +] + optional-dependencies.tests = [ "check-manifest", "coverage>=7.4.2", @@ -67,6 +71,7 @@ optional-dependencies.tests = [ "pytest-timeout", "trove-classifiers>=2024.10.12", ] + optional-dependencies.typing = [ "typing-extensions; python_version<'3.10'", ] diff --git a/setup.py b/setup.py index 9d69b1d6e..5ecd6b816 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path") _LIB_IMAGING = ( "Access", "AlphaComposite", + "Arrow", "Resample", "Reduce", "Bands", diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 60850f4ff..233df592c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -577,6 +577,14 @@ class Image: def mode(self) -> str: return self._mode + @property + def readonly(self) -> int: + return (self._im and self._im.readonly) or self._readonly + + @readonly.setter + def readonly(self, readonly: int) -> None: + self._readonly = readonly + def _new(self, im: core.ImagingCore) -> Image: new = Image() new.im = im @@ -728,6 +736,16 @@ class Image: new["shape"], new["typestr"] = _conv_type_shape(self) return new + def __arrow_c_schema__(self) -> object: + self.load() + return self.im.__arrow_c_schema__() + + def __arrow_c_array__( + self, requested_schema: object | None = None + ) -> tuple[object, object]: + self.load() + return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__()) + def __getstate__(self) -> list[Any]: im_data = self.tobytes() # load image first return [self.info, self.mode, self.size, self.getpalette(), im_data] @@ -3201,6 +3219,18 @@ class SupportsArrayInterface(Protocol): raise NotImplementedError() +class SupportsArrowArrayInterface(Protocol): + """ + An object that has an ``__arrow_c_array__`` method corresponding to the arrow c + data interface. + """ + + def __arrow_c_array__( + self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 + ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 + raise NotImplementedError() + + def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: """ Creates an image memory from an object exporting the array interface @@ -3289,6 +3319,56 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) +def fromarrow(obj: SupportsArrowArrayInterface, mode, size) -> Image: + """Creates an image with zero-copy shared memory from an object exporting + the arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + + If the data representation of the ``obj`` is not compatible with + Pillow internal storage, a ValueError is raised. + + Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + + As with array support, when converting Pillow images to arrays, + only pixel values are transferred. This means that P and PA mode + images will lose their palette. + + :param obj: Object with an arrow_c_array interface + :param mode: Image mode. + :param size: Image size. This must match the storage of the arrow object. + :returns: An Image object + + Note that according to the Arrow spec, both the producer and the + consumer should consider the exported array to be immutable, as + unsynchronized updates will potentially cause inconsistent data. + + See: :ref:`arrow-support` for more detailed information + + .. versionadded:: 11.2.0 + + """ + if not hasattr(obj, "__arrow_c_array__"): + msg = "arrow_c_array interface not found" + raise ValueError(msg) + + (schema_capsule, array_capsule) = obj.__arrow_c_array__() + _im = core.new_arrow(mode, size, schema_capsule, array_capsule) + if _im: + return Image()._new(_im) + + msg = "new_arrow returned None without an exception" + raise ValueError(msg) + + def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: """Creates an image instance from a QImage image""" from . import ImageQt diff --git a/src/_imaging.c b/src/_imaging.c index 330a7eef4..72f122143 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -230,6 +230,93 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); } +/* -------------------------------------------------------------------- */ +/* Arrow HANDLING */ +/* -------------------------------------------------------------------- */ + +PyObject * +ArrowError(int err) { + if (err == IMAGING_CODEC_MEMORY) { + return ImagingError_MemoryError(); + } + if (err == IMAGING_ARROW_INCOMPATIBLE_MODE) { + return ImagingError_ValueError("Incompatible Pillow mode for Arrow array"); + } + if (err == IMAGING_ARROW_MEMORY_LAYOUT) { + return ImagingError_ValueError( + "Image is in multiple array blocks, use imaging_new_block for zero copy" + ); + } + return ImagingError_ValueError("Unknown error"); +} + +void +ReleaseArrowSchemaPyCapsule(PyObject *capsule) { + struct ArrowSchema *schema = + (struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema"); + if (schema->release != NULL) { + schema->release(schema); + } + free(schema); +} + +PyObject * +ExportArrowSchemaPyCapsule(ImagingObject *self) { + struct ArrowSchema *schema = + (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + int err = export_imaging_schema(self->image, schema); + if (err == 0) { + return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule); + } + free(schema); + return ArrowError(err); +} + +void +ReleaseArrowArrayPyCapsule(PyObject *capsule) { + struct ArrowArray *array = + (struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array"); + if (array->release != NULL) { + array->release(array); + } + free(array); +} + +PyObject * +ExportArrowArrayPyCapsule(ImagingObject *self) { + struct ArrowArray *array = + (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + int err = export_imaging_array(self->image, array); + if (err == 0) { + return PyCapsule_New(array, "arrow_array", ReleaseArrowArrayPyCapsule); + } + free(array); + return ArrowError(err); +} + +static PyObject * +_new_arrow(PyObject *self, PyObject *args) { + char *mode; + int xsize, ysize; + PyObject *schema_capsule, *array_capsule; + PyObject *ret; + + if (!PyArg_ParseTuple( + args, "s(ii)OO", &mode, &xsize, &ysize, &schema_capsule, &array_capsule + )) { + return NULL; + } + + // ImagingBorrowArrow is responsible for retaining the array_capsule + ret = + PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ); + if (!ret) { + return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); + } + return ret; +} + /* -------------------------------------------------------------------- */ /* EXCEPTION REROUTING */ /* -------------------------------------------------------------------- */ @@ -3655,6 +3742,10 @@ static struct PyMethodDef methods[] = { /* Misc. */ {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, + /* arrow */ + {"__arrow_c_schema__", (PyCFunction)ExportArrowSchemaPyCapsule, METH_VARARGS}, + {"__arrow_c_array__", (PyCFunction)ExportArrowArrayPyCapsule, METH_VARARGS}, + {NULL, NULL} /* sentinel */ }; @@ -3722,6 +3813,11 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { ); } +static PyObject * +_getattr_readonly(ImagingObject *self, void *closure) { + return PyLong_FromLong(self->image->read_only); +} + static struct PyGetSetDef getsetters[] = { {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, @@ -3729,6 +3825,7 @@ static struct PyGetSetDef getsetters[] = { {"id", (getter)_getattr_id}, {"ptr", (getter)_getattr_ptr}, {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, + {"readonly", (getter)_getattr_readonly}, {NULL} }; @@ -3983,6 +4080,21 @@ _set_blocks_max(PyObject *self, PyObject *args) { Py_RETURN_NONE; } +static PyObject * +_set_use_block_allocator(PyObject *self, PyObject *args) { + int use_block_allocator; + if (!PyArg_ParseTuple(args, "i:set_use_block_allocator", &use_block_allocator)) { + return NULL; + } + ImagingMemorySetBlockAllocator(&ImagingDefaultArena, use_block_allocator); + Py_RETURN_NONE; +} + +static PyObject * +_get_use_block_allocator(PyObject *self, PyObject *args) { + return PyLong_FromLong(ImagingDefaultArena.use_block_allocator); +} + static PyObject * _clear_cache(PyObject *self, PyObject *args) { int i = 0; @@ -4104,6 +4216,7 @@ static PyMethodDef functions[] = { {"fill", (PyCFunction)_fill, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS}, {"new_block", (PyCFunction)_new_block, METH_VARARGS}, + {"new_arrow", (PyCFunction)_new_arrow, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ @@ -4190,9 +4303,11 @@ static PyMethodDef functions[] = { {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS}, {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS}, {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS}, + {"get_use_block_allocator", (PyCFunction)_get_use_block_allocator, METH_VARARGS}, {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS}, {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS}, {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS}, + {"set_use_block_allocator", (PyCFunction)_set_use_block_allocator, METH_VARARGS}, {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, {NULL, NULL} /* sentinel */ diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c new file mode 100644 index 000000000..33ff2ce77 --- /dev/null +++ b/src/libImaging/Arrow.c @@ -0,0 +1,299 @@ + +#include "Arrow.h" +#include "Imaging.h" +#include + +/* struct ArrowSchema* */ +/* _arrow_schema_channel(char* channel, char* format) { */ + +/* } */ + +static void +ReleaseExportedSchema(struct ArrowSchema *array) { + // This should not be called on already released array + // assert(array->release != NULL); + + if (!array->release) { + return; + } + if (array->format) { + free((void *)array->format); + array->format = NULL; + } + if (array->name) { + free((void *)array->name); + array->name = NULL; + } + if (array->metadata) { + free((void *)array->metadata); + array->metadata = NULL; + } + + // Release children + for (int64_t i = 0; i < array->n_children; ++i) { + struct ArrowSchema *child = array->children[i]; + if (child->release != NULL) { + child->release(child); + child->release = NULL; + } + // UNDONE -- should I be releasing the children? + } + + // Release dictionary + struct ArrowSchema *dict = array->dictionary; + if (dict != NULL && dict->release != NULL) { + dict->release(dict); + dict->release = NULL; + } + + // TODO here: release and/or deallocate all data directly owned by + // the ArrowArray struct, such as the private_data. + + // Mark array released + array->release = NULL; +} + +int +export_named_type(struct ArrowSchema *schema, char *format, char *name) { + char *formatp; + char *namep; + size_t format_len = strlen(format) + 1; + size_t name_len = strlen(name) + 1; + + formatp = calloc(format_len, 1); + + if (!formatp) { + return IMAGING_CODEC_MEMORY; + } + + namep = calloc(name_len, 1); + if (!namep) { + free(formatp); + return IMAGING_CODEC_MEMORY; + } + + strncpy(formatp, format, format_len); + strncpy(namep, name, name_len); + + *schema = (struct ArrowSchema){// Type description + .format = formatp, + .name = namep, + .metadata = NULL, + .flags = 0, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &ReleaseExportedSchema + }; + return 0; +} + +int +export_imaging_schema(Imaging im, struct ArrowSchema *schema) { + int retval = 0; + + if (strcmp(im->arrow_band_format, "") == 0) { + return IMAGING_ARROW_INCOMPATIBLE_MODE; + } + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->bands == 1) { + return export_named_type(schema, im->arrow_band_format, im->band_names[0]); + } + + retval = export_named_type(schema, "+w:4", ""); + if (retval != 0) { + return retval; + } + // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. + schema->n_children = 1; + schema->children = calloc(1, sizeof(struct ArrowSchema *)); + schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); + if (retval != 0) { + free(schema->children[0]); + schema->release(schema); + return retval; + } + return 0; +} + +static void +release_const_array(struct ArrowArray *array) { + Imaging im = (Imaging)array->private_data; + + if (array->n_children == 0) { + ImagingDelete(im); + } + + // Free the buffers and the buffers array + if (array->buffers) { + free(array->buffers); + array->buffers = NULL; + } + if (array->children) { + // undone -- does arrow release all the children recursively? + for (int i = 0; i < array->n_children; i++) { + if (array->children[i]->release) { + array->children[i]->release(array->children[i]); + array->children[i]->release = NULL; + free(array->children[i]); + } + } + free(array->children); + array->children = NULL; + } + // Mark released + array->release = NULL; +} + +int +export_single_channel_array(Imaging im, struct ArrowArray *array) { + int length = im->xsize * im->ysize; + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->lines_per_block && im->lines_per_block < im->ysize) { + length = im->xsize * im->lines_per_block; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + // Initialize primitive fields + *array = (struct ArrowArray){// Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + // Allocate list of buffers + array->buffers = (const void **)malloc(sizeof(void *) * array->n_buffers); + // assert(array->buffers != NULL); + array->buffers[0] = NULL; // no nulls, null bitmap can be omitted + + if (im->block) { + array->buffers[1] = im->block; + } else { + array->buffers[1] = im->blocks[0].ptr; + } + return 0; +} + +int +export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { + int length = im->xsize * im->ysize; + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->lines_per_block && im->lines_per_block < im->ysize) { + length = im->xsize * im->lines_per_block; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + // Initialize primitive fields + // Fixed length arrays are 1 buffer of validity, and the length in pixels. + // Data is in a child array. + *array = (struct ArrowArray){// Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 1, + .n_children = 1, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + // Allocate list of buffers + array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers); + if (!array->buffers) { + goto err; + } + // assert(array->buffers != NULL); + array->buffers[0] = NULL; // no nulls, null bitmap can be omitted + + // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. + array->n_children = 1; + array->children = calloc(1, sizeof(struct ArrowArray *)); + if (!array->children) { + goto err; + } + array->children[0] = (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + if (!array->children[0]) { + goto err; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + *array->children[0] = (struct ArrowArray){// Data description + .length = length * 4, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + array->children[0]->buffers = + (const void **)calloc(2, sizeof(void *) * array->n_buffers); + + if (im->block) { + array->children[0]->buffers[1] = im->block; + } else { + array->children[0]->buffers[1] = im->blocks[0].ptr; + } + return 0; + +err: + if (array->children[0]) { + free(array->children[0]); + } + if (array->children) { + free(array->children); + } + if (array->buffers) { + free(array->buffers); + } + return IMAGING_CODEC_MEMORY; +} + +int +export_imaging_array(Imaging im, struct ArrowArray *array) { + if (strcmp(im->arrow_band_format, "") == 0) { + return IMAGING_ARROW_INCOMPATIBLE_MODE; + } + + if (im->bands == 1) { + return export_single_channel_array(im, array); + } + + return export_fixed_pixel_array(im, array); +} diff --git a/src/libImaging/Arrow.h b/src/libImaging/Arrow.h new file mode 100644 index 000000000..0b285fe80 --- /dev/null +++ b/src/libImaging/Arrow.h @@ -0,0 +1,48 @@ +#include +#include + +// Apache License 2.0. +// Source apache arrow project +// https://arrow.apache.org/docs/format/CDataInterface.html + +#ifndef ARROW_C_DATA_INTERFACE +#define ARROW_C_DATA_INTERFACE + +#define ARROW_FLAG_DICTIONARY_ORDERED 1 +#define ARROW_FLAG_NULLABLE 2 +#define ARROW_FLAG_MAP_KEYS_SORTED 4 + +struct ArrowSchema { + // Array type description + const char *format; + const char *name; + const char *metadata; + int64_t flags; + int64_t n_children; + struct ArrowSchema **children; + struct ArrowSchema *dictionary; + + // Release callback + void (*release)(struct ArrowSchema *); + // Opaque producer-specific data + void *private_data; +}; + +struct ArrowArray { + // Array data description + int64_t length; + int64_t null_count; + int64_t offset; + int64_t n_buffers; + int64_t n_children; + const void **buffers; + struct ArrowArray **children; + struct ArrowArray *dictionary; + + // Release callback + void (*release)(struct ArrowArray *); + // Opaque producer-specific data + void *private_data; +}; + +#endif // ARROW_C_DATA_INTERFACE diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 0fc191d15..234f9943c 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -20,6 +20,8 @@ extern "C" { #define M_PI 3.1415926535897932384626433832795 #endif +#include "Arrow.h" + /* -------------------------------------------------------------------- */ /* @@ -104,6 +106,21 @@ struct ImagingMemoryInstance { /* Virtual methods */ void (*destroy)(Imaging im); + + /* arrow */ + int refcount; /* Number of arrow arrays that have been allocated */ + char band_names[4][3]; /* names of bands, max 2 char + null terminator */ + char arrow_band_format[2]; /* single character + null terminator */ + + int read_only; /* flag for read-only. set for arrow borrowed arrays */ + PyObject *arrow_array_capsule; /* upstream arrow array source */ + + int blocks_count; /* Number of blocks that have been allocated */ + int lines_per_block; /* Number of lines in a block have been allocated */ + +#ifdef Py_GIL_DISABLED + PyMutex mutex; +#endif }; #define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)]) @@ -161,6 +178,7 @@ typedef struct ImagingMemoryArena { int stats_reallocated_blocks; /* Number of blocks which were actually reallocated after retrieving */ int stats_freed_blocks; /* Number of freed blocks */ + int use_block_allocator; /* don't use arena, use block allocator */ #ifdef Py_GIL_DISABLED PyMutex mutex; #endif @@ -174,6 +192,8 @@ extern int ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max); extern void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); +extern void +ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator); extern Imaging ImagingNew(const char *mode, int xsize, int ysize); @@ -187,6 +207,15 @@ ImagingDelete(Imaging im); extern Imaging ImagingNewBlock(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNewArrow( + const char *mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +); + extern Imaging ImagingNewPrologue(const char *mode, int xsize, int ysize); extern Imaging @@ -700,6 +729,13 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence); extern Py_ssize_t _imaging_tell_pyFd(PyObject *fd); +/* Arrow */ + +extern int +export_imaging_array(Imaging im, struct ArrowArray *array); +extern int +export_imaging_schema(Imaging im, struct ArrowSchema *schema); + /* Errcodes */ #define IMAGING_CODEC_END 1 #define IMAGING_CODEC_OVERRUN -1 @@ -707,6 +743,8 @@ _imaging_tell_pyFd(PyObject *fd); #define IMAGING_CODEC_UNKNOWN -3 #define IMAGING_CODEC_CONFIG -8 #define IMAGING_CODEC_MEMORY -9 +#define IMAGING_ARROW_INCOMPATIBLE_MODE -10 +#define IMAGING_ARROW_MEMORY_LAYOUT -11 #include "ImagingUtils.h" extern UINT8 *clip8_lookups; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 522e9f375..4fa4ecd1c 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -58,19 +58,22 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { /* Setup image descriptor */ im->xsize = xsize; im->ysize = ysize; - + im->refcount = 1; im->type = IMAGING_TYPE_UINT8; + strcpy(im->arrow_band_format, "C"); if (strcmp(mode, "1") == 0) { /* 1-bit images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "1"); } else if (strcmp(mode, "P") == 0) { /* 8-bit palette mapped images */ im->bands = im->pixelsize = 1; im->linesize = xsize; im->palette = ImagingPaletteNew("RGB"); + strcpy(im->band_names[0], "P"); } else if (strcmp(mode, "PA") == 0) { /* 8-bit palette with alpha */ @@ -78,23 +81,36 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; im->palette = ImagingPaletteNew("RGB"); + strcpy(im->band_names[0], "P"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "L") == 0) { /* 8-bit grayscale (luminance) images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "L"); } else if (strcmp(mode, "LA") == 0) { /* 8-bit grayscale (luminance) with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "La") == 0) { /* 8-bit grayscale (luminance) with premultiplied alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "a"); } else if (strcmp(mode, "F") == 0) { /* 32-bit floating point images */ @@ -102,6 +118,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; im->linesize = xsize * 4; im->type = IMAGING_TYPE_FLOAT32; + strcpy(im->arrow_band_format, "f"); + strcpy(im->band_names[0], "F"); } else if (strcmp(mode, "I") == 0) { /* 32-bit integer images */ @@ -109,6 +127,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; im->linesize = xsize * 4; im->type = IMAGING_TYPE_INT32; + strcpy(im->arrow_band_format, "i"); + strcpy(im->band_names[0], "I"); } else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { @@ -118,12 +138,18 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = xsize * 2; im->type = IMAGING_TYPE_SPECIAL; + strcpy(im->arrow_band_format, "s"); + strcpy(im->band_names[0], "I"); } else if (strcmp(mode, "RGB") == 0) { /* 24-bit true colour images */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ @@ -132,6 +158,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "BGR;16") == 0) { /* EXPERIMENTAL */ @@ -140,6 +168,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "BGR;24") == 0) { /* EXPERIMENTAL */ @@ -148,32 +178,54 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 3; im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "RGBA") == 0) { /* 32-bit true colour images with alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "RGBa") == 0) { /* 32-bit true colour images with premultiplied alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "a"); } else if (strcmp(mode, "CMYK") == 0) { /* 32-bit colour separation */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "C"); + strcpy(im->band_names[1], "M"); + strcpy(im->band_names[2], "Y"); + strcpy(im->band_names[3], "K"); } else if (strcmp(mode, "YCbCr") == 0) { /* 24-bit video format */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "Y"); + strcpy(im->band_names[1], "Cb"); + strcpy(im->band_names[2], "Cr"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "LAB") == 0) { /* 24-bit color, luminance, + 2 color channels */ @@ -181,6 +233,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "a"); + strcpy(im->band_names[2], "b"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "HSV") == 0) { /* 24-bit color, luminance, + 2 color channels */ @@ -188,6 +244,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "H"); + strcpy(im->band_names[1], "S"); + strcpy(im->band_names[2], "V"); + strcpy(im->band_names[3], "X"); } else { free(im); @@ -218,6 +278,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { break; } + // UNDONE -- not accurate for arrow MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.stats_new_count += 1; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); @@ -238,8 +299,18 @@ ImagingDelete(Imaging im) { return; } + MUTEX_LOCK(&im->mutex); + im->refcount--; + + if (im->refcount > 0) { + MUTEX_UNLOCK(&im->mutex); + return; + } + MUTEX_UNLOCK(&im->mutex); + if (im->palette) { ImagingPaletteDelete(im->palette); + im->palette = NULL; } if (im->destroy) { @@ -270,6 +341,7 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, // Stats + 0, // use_block_allocator #ifdef Py_GIL_DISABLED {0}, #endif @@ -302,6 +374,11 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) { return 1; } +void +ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator) { + arena->use_block_allocator = use_block_allocator; +} + void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) { while (arena->blocks_cached > new_size) { @@ -396,11 +473,13 @@ ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_ if (lines_per_block == 0) { lines_per_block = 1; } + im->lines_per_block = lines_per_block; blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block; // printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n", // im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count); /* One extra pointer is always NULL */ + im->blocks_count = blocks_count; im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1); if (!im->blocks) { return (Imaging)ImagingError_MemoryError(); @@ -487,6 +566,58 @@ ImagingAllocateBlock(Imaging im) { return im; } +/* Borrowed Arrow Storage Type */ +/* --------------------------- */ +/* Don't allocate the image. */ + +static void +ImagingDestroyArrow(Imaging im) { + // Rely on the internal Python destructor for the array capsule. + if (im->arrow_array_capsule) { + Py_DECREF(im->arrow_array_capsule); + im->arrow_array_capsule = NULL; + } +} + +Imaging +ImagingBorrowArrow( + Imaging im, + struct ArrowArray *external_array, + int offset_width, + PyObject *arrow_capsule +) { + // offset_width is the number of char* for a single offset from arrow + Py_ssize_t y, i; + + char *borrowed_buffer = NULL; + struct ArrowArray *arr = external_array; + + if (arr->n_children == 1) { + arr = arr->children[0]; + } + if (arr->n_buffers == 2) { + // buffer 0 is the null list + // buffer 1 is the data + borrowed_buffer = (char *)arr->buffers[1] + (offset_width * arr->offset); + } + + if (!borrowed_buffer) { + return (Imaging + )ImagingError_ValueError("Arrow Array, exactly 2 buffers required"); + } + + for (y = i = 0; y < im->ysize; y++) { + im->image[y] = borrowed_buffer + i; + i += im->linesize; + } + im->read_only = 1; + Py_INCREF(arrow_capsule); + im->arrow_array_capsule = arrow_capsule; + im->destroy = ImagingDestroyArrow; + + return im; +} + /* -------------------------------------------------------------------- * Create a new, internally allocated, image. */ @@ -529,11 +660,17 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { Imaging ImagingNew(const char *mode, int xsize, int ysize) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 0); } Imaging ImagingNewDirty(const char *mode, int xsize, int ysize) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 1); } @@ -558,6 +695,66 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) { return NULL; } +Imaging +ImagingNewArrow( + const char *mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +) { + /* A borrowed arrow array */ + Imaging im; + struct ArrowSchema *schema = + (struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema"); + + struct ArrowArray *external_array = + (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array"); + + if (xsize < 0 || ysize < 0) { + return (Imaging)ImagingError_ValueError("bad image size"); + } + + im = ImagingNewPrologue(mode, xsize, ysize); + if (!im) { + return NULL; + } + + int64_t pixels = (int64_t)xsize * (int64_t)ysize; + + // fmt:off // don't reformat this + if (((strcmp(schema->format, "I") == 0 // int32 + && im->pixelsize == 4 // 4xchar* storage + && im->bands >= 2) // INT32 into any INT32 Storage mode + || // (()||()) && + (strcmp(schema->format, im->arrow_band_format) == 0 // same mode + && im->bands == 1)) // Single band match + && pixels == external_array->length) { + // one arrow element per, and it matches a pixelsize*char + if (ImagingBorrowArrow(im, external_array, im->pixelsize, array_capsule)) { + return im; + } + } + if (strcmp(schema->format, "+w:4") == 0 // 4 up array + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children > 0 // make sure schema is well formed. + && schema->children // make sure schema is well formed + && strcmp(schema->children[0]->format, "C") == 0 // Expected format + && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && pixels == external_array->length // expected length + && external_array->n_children == 1 // array is well formed + && external_array->children // array is well formed + && 4 * pixels == external_array->children[0]->length) { + // 4 up element of char into pixelsize == 4 + if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { + return im; + } + } + // fmt: on + ImagingDelete(im); + return NULL; +} + Imaging ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { /* allocate or validate output image */