From 3878b588a4c71500e29695e139e32803edd9e4a8 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Sun, 3 Jan 2021 15:12:01 -0500 Subject: [PATCH 01/22] Add AVIF plugin (using libavif) --- .ci/install.sh | 6 +- .github/workflows/macos-install.sh | 9 +- .github/workflows/test-mingw.yml | 1 + .github/workflows/test-windows.yml | 14 + .github/workflows/wheels-dependencies.sh | 82 +- Tests/check_avif_leaks.py | 44 + Tests/check_wheel.py | 9 +- Tests/images/avif/chimera-missing-pixi.avif | Bin 0 -> 9717 bytes Tests/images/avif/exif.avif | Bin 0 -> 16078 bytes Tests/images/avif/hopper.avif | Bin 0 -> 3077 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/rgba10.heif | Bin 0 -> 7371 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/star180.png | Bin 0 -> 9211 bytes Tests/images/avif/star270.png | Bin 0 -> 9395 bytes Tests/images/avif/star90.png | Bin 0 -> 9272 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 | 809 ++++++++++++++ depends/install_libavif.sh | 62 ++ docs/handbook/image-file-formats.rst | 75 +- docs/installation/building-from-source.rst | 30 +- docs/reference/features.rst | 1 + docs/reference/plugins.rst | 8 + setup.py | 17 + src/PIL/AvifImagePlugin.py | 282 +++++ src/PIL/Image.py | 4 +- src/PIL/__init__.py | 1 + src/PIL/_avif.pyi | 3 + src/PIL/features.py | 2 + src/_avif.c | 1084 +++++++++++++++++++ wheels/dependency_licenses/DAV1D.txt | 23 + wheels/dependency_licenses/LIBAVIF.txt | 387 +++++++ wheels/dependency_licenses/LIBYUV.txt | 29 + wheels/dependency_licenses/PATENTS.txt | 107 ++ wheels/dependency_licenses/RAV1E.txt | 25 + wheels/dependency_licenses/SVT-AV1.txt | 26 + winbuild/Findrav1e.cmake | 10 + winbuild/build.rst | 1 + winbuild/build_prepare.py | 79 +- 44 files changed, 3219 insertions(+), 11 deletions(-) create mode 100644 Tests/check_avif_leaks.py create mode 100644 Tests/images/avif/chimera-missing-pixi.avif create mode 100644 Tests/images/avif/exif.avif create mode 100644 Tests/images/avif/hopper.avif 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/rgba10.heif 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/star180.png create mode 100644 Tests/images/avif/star270.png create mode 100644 Tests/images/avif/star90.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/DAV1D.txt create mode 100644 wheels/dependency_licenses/LIBAVIF.txt create mode 100644 wheels/dependency_licenses/LIBYUV.txt create mode 100644 wheels/dependency_licenses/PATENTS.txt create mode 100644 wheels/dependency_licenses/RAV1E.txt create mode 100644 wheels/dependency_licenses/SVT-AV1.txt create mode 100644 winbuild/Findrav1e.cmake diff --git a/.ci/install.sh b/.ci/install.sh index e85e6bdc575..f24768d788e 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -23,7 +23,8 @@ if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ - sway wl-clipboard libopenblas-dev + sway wl-clipboard libopenblas-dev\ + ninja-build build-essential nasm fi python3 -m pip install --upgrade pip @@ -62,6 +63,9 @@ if [[ $(uname) != CYGWIN* ]]; then # raqm pushd depends && ./install_raqm.sh && popd + # libavif + pushd depends && ./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 ddb4212301d..4e54f49922f 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -13,7 +13,11 @@ brew install \ libtiff \ little-cms2 \ openjpeg \ - webp + webp \ + dav1d \ + aom \ + rav1e \ + ninja if [[ "$ImageOS" == "macos13" ]]; then brew install --ignore-dependencies libraqm else @@ -31,5 +35,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 c7a73439ca9..29820f60e79 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -61,6 +61,7 @@ jobs: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libavif \ mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c8842e37b2c..419f42e77b3 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -86,6 +86,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH + python -m pip install meson + choco install ghostscript --version=10.4.0 --no-progress echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH @@ -137,6 +139,18 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" + - name: Build dependencies / rav1e + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_rav1e.cmd" + + - name: Build dependencies / meson + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\install_meson.cmd" + + - name: Build dependencies / libavif + if: steps.build-cache.outputs.cache-hit != 'true' + 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 97c1adf098a..2e7d5a23290 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -37,6 +37,8 @@ LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +LIBAVIF_VERSION=1.1.1 +RAV1E_VERSION=0.7.1 function build_brotli { local cmake=$(get_modern_cmake) @@ -63,6 +65,71 @@ function build_harfbuzz { fi } +function install_rav1e { + if [[ -n "$IS_MACOS" ]] && [[ "$PLAT" == "arm64" ]]; then + librav1e_tgz=librav1e-${RAV1E_VERSION}-macos-aarch64.tar.gz + elif [ -n "$IS_MACOS" ]; then + librav1e_tgz=librav1e-${RAV1E_VERSION}-macos.tar.gz + elif [ "$PLAT" == "aarch64" ]; then + librav1e_tgz=librav1e-${RAV1E_VERSION}-linux-aarch64.tar.gz + else + librav1e_tgz=librav1e-${RAV1E_VERSION}-linux-generic.tar.gz + fi + + curl -sLo - \ + https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/$librav1e_tgz \ + | tar -C $BUILD_PREFIX --exclude LICENSE --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf - + + if [ ! -n "$IS_MACOS" ]; then + sed -i 's/-lgcc_s/-lgcc_eh/g' "${BUILD_PREFIX}/lib/pkgconfig/rav1e.pc" + fi + + # Force libavif to treat system rav1e as if it were local + mkdir -p /tmp/cmake/Modules + cat < /tmp/cmake/Modules/Findrav1e.cmake + add_library(rav1e::rav1e STATIC IMPORTED GLOBAL) + set_target_properties(rav1e::rav1e PROPERTIES + IMPORTED_LOCATION "$BUILD_PREFIX/lib/librav1e.a" + AVIF_LOCAL ON + INTERFACE_INCLUDE_DIRECTORIES "$BUILD_PREFIX/include/rav1e" + ) +EOF +} + +function build_libavif { + install_rav1e + python -m pip install meson ninja + + if [[ "$PLAT" == "x86_64" ]]; then + build_simple nasm 2.15.05 https://www.nasm.us/pub/nasm/releasebuilds/2.15.05/ + fi + + local cmake=$(get_modern_cmake) + 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 \ + -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + -DAVIF_CODEC_RAV1E=SYSTEM \ + -DAVIF_CODEC_AOM=LOCAL \ + -DAVIF_CODEC_DAV1D=LOCAL \ + -DAVIF_CODEC_SVT=LOCAL \ + -DENABLE_NASM=ON \ + -DCMAKE_MODULE_PATH=/tmp/cmake/Modules \ + . \ + && make install) + + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libavif.a /usr/local/lib + cp /usr/local/lib64/pkgconfig/libavif.pc /usr/local/lib/pkgconfig + fi +} + function build { if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then sudo chown -R runner /usr/local @@ -73,6 +140,13 @@ function build { fi build_new_zlib + ORIGINAL_LDFLAGS=$LDFLAGS + if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then + LDFLAGS="${LDFLAGS} -ld64" + fi + build_libavif + LDFLAGS=$ORIGINAL_LDFLAGS + build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto @@ -127,15 +201,19 @@ if [[ -n "$IS_MACOS" ]]; then # remove lcms2 and libpng to fix building openjpeg on arm64 # remove jpeg-turbo to avoid inclusion on arm64 # remove webp and zstd to avoid inclusion on x86_64 + # remove aom and libavif to fix building on arm64 # curl from brew requires zstd, use system curl brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd if [[ "$CIBW_ARCHS" == "arm64" ]]; then brew remove --ignore-dependencies jpeg-turbo else - brew remove --ignore-dependencies webp + brew remove --ignore-dependencies webp aom libavif fi - brew install pkg-config + brew install meson pkg-config + + # clear bash path cache for curl + hash -d curl fi wrap_wheel_builder build diff --git a/Tests/check_avif_leaks.py b/Tests/check_avif_leaks.py new file mode 100644 index 00000000000..de6f370d971 --- /dev/null +++ b/Tests/check_avif_leaks.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from io import BytesIO + +import pytest + +from PIL import Image + +from .helper import is_win32, skip_unless_feature + +# Limits for testing the leak +mem_limit = 1024 * 1048576 +stack_size = 8 * 1048576 +iterations = int((mem_limit / stack_size) * 2) +test_file = "Tests/images/avif/hopper.avif" + +pytestmark = [ + pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), + skip_unless_feature("avif"), +] + + +def test_leak_load(): + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + + +def test_leak_save(): + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + test_output = BytesIO() + im.save(test_output, "AVIF") + test_output.seek(0) + test_output.read() diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f58a..002dccde62a 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,12 +1,14 @@ from __future__ import annotations +import platform +import struct import sys from PIL import features 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: @@ -16,6 +18,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/chimera-missing-pixi.avif b/Tests/images/avif/chimera-missing-pixi.avif new file mode 100644 index 0000000000000000000000000000000000000000..4039547d57413457c9b3c6573e9a42b378e00a41 GIT binary patch literal 9717 zcmY*z5jy5J?9Q9zQY@n>D>&s%lugNZFKm6%T44rc({*KjsdM0& z{0h)@^SZvRq$4Jtn-D~Ml(-0pu7HB1y=|26iCyu1hB)Ak!a3^@Kz+4B6a-&zyB#PA z1LflX%aik8I}sxP@me@z72ALlE_7?T#v3+jXW>yYZ1+zAygxkJZ0QmrPEpmgLR0m0 zKR#Rk6z_#9?7;(ljyzh_pD$Zc#}ceS-;pd&!-)kybC0DjZ z3CRqox%NnyJTOn^mIo1wQdBl8tU~&}MMa7sqD0wv!r3+Nweu>gWwlh)Bf>eTTl!V@ zCqs<4Q`IR|YB#w$+C1sSR=FR3d!Phg{1BcLontzteUFOiD0v@E$heO2V*UwCwC^vK zkDUu9vC7t>JgbOXyU7{`$z*wh$+FG?w9oA!knpgyo=NUusEL>X-fftVo+C~(6o4Ws_mJ=1b(kQS%<4^PO@23>LjFd_fWO)%ms~rCPx~fKH`F*G zLlx^ZOLE!cJyI(!mrUF>o&bAfrA5EfgArw`W-BHm6sFOloxzU`PO{5Wbic0Tc7#@B z_j)bFh{hKwYAMFQluCD_Ueezo%r0^Z_DqoWanV;(<8UwgEHn0A=z{-T?yK%%v&ueC zU*7d?MZa8WL%X2dFJDo<5O-AY!R6M0saUQ6;CIw?5i*B9^}i!BqXus$u%5=~rh> zTSz@+#d8N6C`)=0j3?>M?f36^uA@BLZXDil;0+)^`4=7`%GTjNMJdH;oZbp2<-zwP zCq{C6W*eh5J1)4s5&R^g7$5NwI+nKjUL=q;vyO=LBm7nqIi&f3N7^Y;9>DrNTOmB)cU1^+l27q4knHD%PWGzk~Qe*=v+A6#2F1J!tbUh0ZN@4+(}ss7N{ zBha%zqnY@t(XG>Dq?xU*mf=Z0L+A(HT&Lr`R>PBsBof8bMfpBDSxGo$JglEHFc<)E znTHv&AnnW#0!lp%9^Z7?VpHjj4R?)HwG{E!Ul%6q0?geJ%K&K5(pHq zxp{h_rl|<&3EOP-#G_|60x5Ud;k(-yntj9^KFx7msvxTdMxxL~?Z*Ma+LEQ+YlC5X zs<>%GloDWg-mZw`J0BdzEa8;Yu$;KgFgQ8P*iuIg^Z=4^vs>qGr&VmJmTvarI8FY$ zT_s}{;7@TudQLrY_HShOw;UvRQ8pCf?>md=btjA>xTJnAd)=dDmWZ{|HM{E>4)0qpL>2J1EqAbe*6eH@(rZDxy;l2UV zEkWQ)J65hql&R#QVH^V;=E((PB$udkkI|!h6eYPl?0X=JCuO96eNWD?>?~3$2>S6I z^R55vAN7$$k(VDA-XD}E9W`pSV-C5CH11=d z1S7l3x+T{3>xLLNf5Tp8WeKgD8W0?Po7@*P^VH^ zb%vHnP|1+qwXqSd#{lH`n|_>gRVOxEEphzs@vN95Slsb}YzlByn2ZzAM+bL3H=_ZO z`d`|d`qJ~b+(LBYd3oHM!vGVVJ!DVgQ`oBr-@O4d1XY`2vK;w3yWVvNZq_}*{2f`{ zwVLGT8V}|fd?|h6U_QeXLP3DONezHw@me|7SD!{O;EJLF9|?}nNr$3d_I6v%k4)Q~ zq}xL@+a7k4$ZT*Yp*RROhaUm-EcPDq`$P70_@YI5mR-rUMgq~4zi27HRXyi%9x6C9 zpap4HpEwD9nGn?*JytUUyw~JjG08xs!YXf>HW_DxEOk`d>PQlvQ-Fq7aE;)p2!})Y zz>4s3y6Al;<#;FAo7&P}5CUR5Q|KtyGTY+!?oRm48_SsgdDn_+r4ol!BINtIkF~1s zRzIa?Ek*~zu^}#8Q7Yj0RGw0ewEi`56OESif2|7 zeC^KA8+=Gq-#YK&2_^uk1TGiFFN>l)dXCJ-UlnLYFJg)p-jlQ^-FxR$8WUOMvx;h(;)kC6>+4;69L1Ll1Lz=!VzMph3K zJg>4AgI$%YodzKG;-n2m*uF5HaRIG2y&35hm1AME5RnXzBe5evb-C`8?L}poT2JQE z#JdSp=U}$eaTY$v@%!~wt#mP-?P{C_L=VasedW|UOERBS8!b-j zYO%h=H0BkCRCKUU_EGTA=0A*Le8GKav=;!(>UsOom<7+9fc8X_iF^dLgpINtSek*2 zx5y~EP(I|-_d2dM*cpv?-yIWkPBV5GxxF9$UX@dv?UW_J8(DXgtk#Pz2`WXD`BaDX zDU?z6Eo7a<{!K#E#OwREOliqPpZqS&umV`s>u{N&w^w&RViUmH=H&a5`tUExD9qKQ z!Z*sb7rMKE?KCgaDRyCoRB)KIXxn7LnAyinz5x$)0- z?O%hA4v1Cw#2mAl{M15O#YRxhYP4LD4|qchBdOggT^mJ*%O5L4;Bu*eK>fDnKBI}- zq?BAr+vl36MX-#B@@hmO!<)$^l$-web zTLm*u#AV6^iYHD%HIZ9Im&E2_4Y2tY@frY;xr~Mqi1HdXhB$XEEqfT5?V7PqwfTn7 zidL;qg8?Qe#iTJp2m6s2Gm}v`TkdP6tp#_aoT-M)csN*=Ub7MhK#1|lq>6^ZRB9xQZ@QH$iGN2qaR7-ChO4w|4l z*~o)z{fQiFLAv(v=lC4?ik74_*6X&uX>mK_b_utCZq-fodXsE+pJ<0BS&z#?fy)4-=Pdktb|W zyz(CEcMN+4axiIJYAEE9Gv$^ozZjrDH?wB)J~k8g#;ULr1oH)xJDAV~<6Oy@232+$ zb(fuUCkXj)A@mr6Axa`JJA!v=;-?QAyEhXk`Q(do7Arg1ZhPn8GRD5OB5ub1xy z5FC?R4r@8p)?LDTjV0Io+PLruy>Z#NCpC{uE8~}@C1VzRLxJ(sjDUkk>`XX>-$ip& z161mBCkct@rdA^(lqY~&HTaDl`@y#}dzgz)2XlO8;Rn|k1qst~U1yC;{K4JaXTVnD zp&&zp7%_Q6N|}_Tu>M>nD6hiov$Wdz9#98DK3cTQti7TbAGVWpIscrZmPoar;J3& zulW!*BC^K_MkQ+m$E(%`birrmexJ?hWkg2kKQK&abbZK(KLo{KBU6T#sV#o$;Zb+f z>xg#idzrZKmMLwC#@nA(g8mB7>JPU3JrB=fChW;}0NPX~+h7TiiZ0b7$xhOTVa&M9 zSAK0E_Sg}4R=Ix)vd^h(QT9|D zoE^fQ#m3Mt7fqilrwE?DIzSkb38&&i1X=S-+tm0g4x3}WJ{OM=S#5QN}C0Qw3&i1!KBVo&aB|QYdx!M{Kr`H;x{|sb|q+}`8>H^g3n6f zksjaA+$~=GSl3sjI*eHbJ_e2^nlze6Wa4h)j3(}(R$LxkbHDJSv1nuv9an0aZ5dg3 z=j*IXCL_adiDt~@f)5o(NuhVv>>t*c&tXj-)a;w}9bot?bcp-dV)4`m8N@HP=Q!72 zuR@WyCwu-2?^g?h%3?NWB;hbe7_u?x-+rYmY^J>Ac`n>^iF-uHi&PY>ji zK5}wruf{cR(^S(lC|1s>L}|69B5C%j{XI!)NVdB6({Fvli;XCnJ&ndl{(__DBotjU zY3mka(f(3DgcD->gL*=;H?}PMnAoZ~`PFx{G{heL9#h-Q|H2Vsu#x|B&ygU_lzJgF z)t}n0P|~#CPCXAM_#F5i;oP#CkylvZPt_3K-$+LMbNhK+mBSP6qdpYgHauZ2N6$a0 z1+u2~DK$-!<5)Kl)ojtPKH2BDsi#j63+SO=dwEp8m5?%bE}*P zP&9Rs(|F0ThQczkcP}Ye6qS$9%v+Pj*%qNx@-sBse{L~IqLPZHnT|F~P5H2qROA*d zj<1iup2V(#v!r%(xfX6dOMZH#1FO}c6#Dk0Dkq6KL@S|;e`Dn6yHO$FHA4cW9+FMo?-2q zdatV}7;mn`_-Z8Y^;vItG=ubNo@y*n>)Gm*l5}=UyRux*Z&wKH9vVD*mP$k1_vko+ zGzDlFlaIq}tTkx3To4C&1rKd>?)GJIW%)L*RsgX3vIilODwJ1Q`6Z^I(Qup0w5&Y2 zSUJ0KDUF{3P;I>V_^)ci(GbnWG3QL*1yisJkrZ9xy)d(yUgxh|`loiKVlaxwqr~fi zki*2dI$D{rqT)q3U~B_jtcYzvlhKT;#zg!GNxDPVMER?C>`KT;z9O8cSOa4Q=~^psu;^IX;;S> z$Rp)KMP=5{qfGT(8Pu^8-HMNnlHt{91AVmA+s&D1M@xXu<+jl5yWx9iUD)k3LoP6J z2nW1za@1zJWaWBV27Jv|q_cQUiI1|=kpgo+7VkE(2=(7+v(XnMw+(DT_I*}m-8#vT zeJtCZ2Atdu;E&yalt6{^o^4Gv?Qz7&&LwHA+0!D3(0$?4b zTy8{QFxXq~Q3Jx{{hKIgc9&05nyh`|mmKQfKTyJWg3)FdY zLi>X(jOv!CvZ%L3zEC-+H=I(2{0pV3a0JX)Wlot>XGBLV8}>+OIfv&M9|;Hg?w24d zhs6u0W_MNqDdP0}1}1UZiD{Tu%Q(uvw!QcET;JZ_QZ`!6B*czpgklCVv z{;mE!94BttC3c%+f%S8f|FFWBHDyV!vp@5yB?^lrjowdzbC-SN7xTTz8Z3&DoAsd; z#q$yPVCj0F?J>b}x&41wc2yBOu|8R=nRxw>C@6%`^;Xc zzP_G2r-iT34=a?J_ImN+NG*!5e+Vd1q<2p+i1fwZRrc)0`33HAzcu>P&&(lz)2h#j zl`scIe?v}w|3O&!LLJhoJ{Y$Y-xi) z6AEzUJ(w2Efi2jGwFc7c>Kd?{wi;&h@HCVdd00jHpuaN|lIKwUp$4fC=NLV&J|Qzz zpmjqCNs0VOsh^QSq_zv4eqfedvpe=+&n~TgJ_4J4RW2^^FH<8|tV&w;7J!9fb(35ne234+!f|c?#d

Sh@MyxRQd~_ba6|+ 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 YcWHekFfHtD48Qn4e7zDRI7HC@03D=sOaK4? literal 0 HcmV?d00001 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.avif b/Tests/images/avif/hopper.avif new file mode 100644 index 0000000000000000000000000000000000000000..87e4394f0596e22b50895bef01121f676d731ed1 GIT binary patch literal 3077 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 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/rgba10.heif b/Tests/images/avif/rgba10.heif new file mode 100644 index 0000000000000000000000000000000000000000..8429a8b01eaf7ce7df3a1389ee0a4769b809d6e8 GIT binary patch literal 7371 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%0AGJXrce2^)7TjIy``@XvS zo^;|Py_5N1Rpv<q5(EG`lkgOCyzMA6{d#?F5SId~=z6!8(!F1H;0mCi>AwhM@UXQ=XyjcE@K*M}$B z)g~sU6NPba8yPH=K7LIjD^u`xIh-$dGScik)V?DaPt|>b?J?YPF1x#%b9XrKxFh;= zAf-d|ste)wj!)t&P~X(9P1F$8=7$d0+aIx|a^5ab0`wcF1DIAf)X1OTjdKRUibXHS z)u#b%H6vpub)34ss9q2gTgWt&$PSAyB9A(V`QjeYx#f7~VG+kj2#A;GJpQ0A1N|n! ztAz5Sl%mrs+(Mww@-J=b5|w}@a#&**6S~0b zuhAHPE(MvI-QQ)z%nQHn;_3$QZc>Af?|^4!tm`*PmMS=M04nJSQjYY=hyd{hx$bj@vy%S@aV{S*I{F;p5XS^hI@K;((%_V zJGUWW<7tXW-4aA-g`tdRU&rFsfZ86(Rd>lxd)MU}Sg(FKpjy`!K4xbY!{L;$Q&eUO zYamw4q=oI~=>>s2BO%2POKJ&@dd$KyFnH@ah|R-$iL#qsd<2!Js4XT<8gzQ5!ar;Ry3DHg>SQx!z9iWM_cqmMX?-C^Ivp@9d`M>@gi6mKcdYLs$?Xs(xyt3tMI%y)kYsBV2Ae5wqMA@3aJhr65>L zVWJzLvS>m;ectK9A=SW5iXJtsQcG0=N_Y~Fh{5g+Dg z$0oym)5W4DkrM=ijkf9DU27ea-?iZnZ&5gTC@HDB(;1XVoo3&)(sawku(A=O4ubu- zS_a?<4kNYS$;(--AyN=SFY*yvg8pV6^K08P!$$zTk00SqQq7RwKC0%(t$sI12S*c; zwhDKjMj$8VLGWe?C;9Xv(NPcjrl}WKX}hR3;d)%uTa_)VLJY4Ic2CFOnaqRer7n)2 z+@{Je?<|x%Jy{_j1&>i3+}UwBL7P$S0TI8P09Zz>09Boho1G2eOla_;f2`lC+PwtM z88CgBizwNawql}jgimp~egCiEV$PYy9j^RqYJoEXU8K7Tx8t3{(yop;=I&5UXR~{w z_9#o;0*Q2P?cDP2+9RM?BvT}q-@Hq`jOs|t1#fGgU{d%vcR`LDk04&PfmNhw_$Th^x=G4NhxJ}ZbN1oIyAZBkh<6v0@(8}-Ony3se2XHsnhKR@n z&KxM|7^Y2b3lJ!#QL5oG%XRRK9(}g0H_kOp9?AbYnN?vBm=@{k^P&A=cV#Dq1mwMW06K8(Y#Vikx&AFOSp5^oH59_FPj z`W0KFn*`tcj*!7#f|x{)^3K-cRXrKH=;qXS-#Wn$>l&(LliKU?tD3r>0;K^`0k2qT zruoT+J>PR)!@Y0a(!60(@n7=6$-T%2OnS$`g0}Doo}F*a&&hyloAt_u*#h`TSYII$ zL$MTGV1^fNgpV(%dUm(+3@?K`%)0okY{skA9PwYmu1JjV8>3J`y&Y^h0Pick=o0Vi zQzW>zcm=OmF$OLLTpwTN1>^e45v15Bz!qH!mY*$7UMD81W-!D~HmhThqkwocHaD6U1)0X}y)%c>8sW zrFgdd$<(KHTX^?0K{Er4Q1b2FLpJy4BmH=j!WMC~SWA4Hi|m4XUX$n)Lng~*V$2mk zEN8WENi=hFtKve1$^ma)VN>~^w)3wYJ){pKV0uPiTnOfRXRJ&RHXF3AJtx1fGheY0 zb9;b@F5w3m5At*4Q$9wx{+S}J*lY9pf=aZ~oRJR?Kug7%Ke6es{toD zu!{ys_|1s11zxI}juCVk?osImnbW7(%7Zoi-rAb4{M2;uueNRnQPiVY3)6K3+^41o z(6JFXLg`qW^CdVSCF`b9NZCYOn2#2XMA3x z;1QeQKUxn3Rbbv|BYHhlBCGk>3cM>BUYDSxcgUnyn4wr6_o7t-=SK9LaU;1^7wjs& zva2yX?~W?PZso~~*uD~D81}|_dhmx{t*n@DckeHClUS4(j|X)UIG+ad-VtGF%3kO? z4&xFSejH(6_j0ZPR!V&W`pkab*xY@vQz3eeLoQVxlvNN8PWF&TI=Ut%6TefUmLPB_ zm*#y8-_EIV58>Q=KzL2jE@eRpzFA2dnMt1)3uAdhT1KT}?P8g*hT5RZ&cSjXmX!+Z zrFZQKCSei$8A?<4jNbLsY`pCAfDM%gCR;60zDL4bE$Yhv*WtE(Tu3$DQUajGM(OE$ zH7+HhT9TYm)|I(*yEm#1>u7W9(XuU3yd&a?9e6t9kL5x=u|#(zZo`aUN-I+FM6s$S z1;8^j@fxgVRS?Nk+*X)0H3F{%ytM}6NM>(JK562g-^1D-0r#DjNyhfr3K}Xa+ro>V zUyX0O`shawz;Hf$W4&WeF3Q8MKBk|M&Lgyb@BJXF;BP?}OADT|#j*6aPuAY;zQP3k zyVeW+9)TmQ$3NZmD6jcA>_A^#*u1bBA_Tqm^98y*FP-5mKZ$8KKHQ2w7xCEtxhC?e zd^A#Sg*c2n=##SoF>Rol9!1l5Dh&}IeTJg&Gr6l-6J4B&;Gt8|3!KJ5mGpuo7FTXR z<>r^j{cc9y$8@Bf;F*S~T+hLc@!dy^L{D4B>~dt`5=hYdUg^1Fw1MAt;2w?!h?X>9 z@{U}%sw=^%=|A0+pN~HP^C^rIFuV6$D0z6l|Hidda8lX%$ydKrPq9M~4bu@Y9zGTk z_Z*fwz9D|xns!EdAFc?#_XL>K79v>7^}HpjtF?tBF7ii6I8&(DLK(lmOAK5|I_uFX zK-_y{mN{G0AeUC~&ADwDkMtbZpw7x=B=b%gV>J>y>wIgK++zL(MUf|7bBHffD;N1uFqtv5)L(o7 zBV2CmJ(CB)k3*SAB+r=ZLQ<_1oxdLVwW>xDJZY+zuA2YOslk7^xsUVFVPutx~CW6 zaF}V2V?K+l%MD_YXLAZP)ae)D6KbUxeTG4h?w|#~YEDSc3;&zdjbn!E+apaJp*9uug;($%vMl-* zs4m%D&ulg~E@s6Rd+TDMl3K!Un>T7IhfR9uP2Ea=J%5UMbAgS6NDFmZ(&aVddBPV0pCAhDT&%4c zS))6yL+P{hj0-u$-= zs0jez0yAd6!S7nXdn#<}X!)mG|5XBP;dhCsgRR4#YvWouoBh=S0Dx%Z=4lJ83-aeW zzpNac9f55%SUH+F08?};7e}+-jRRfIKLWC&k;`u#V1NDuY$JDO(Lew`02US~0qeZE ze!u<*L|nj@MS&^6&DoyE&e6=`Pb9))VrAmUUe$22008Vy;Cg2Tijjc<*zTVd_*)BDp8yX?ov8l>b^Z(lsQZ6N z9RRxPUo!~*XJ7bpZBX?8O**muJDq^_f3LTHxqqV*@t?K+hg5CI$_zy0^MFJo@QvZbit6T&=Zvb#^zz_)xdces;0*3$K zqU1N=Hd<_FYUKLwJ3dP$3Npwiz)f4i?4F)Oi<2O5zf&V3KpF5E>hz7DCP10Sm+16n zZ39Yt57XAU1KywV?KuZvyqn5f-h$dRLk~lW!f7K~-cq$SGoy#07qdJKG$~vzk16jT zlkAf6-u+V#+wa&8=y8S6FNiiB;fY811|X5s*<)IYlNOLzS0mG8PDY9NQb~vMAMS&d zjmLEOaTsmC_3H%J{2Zh8xCOIw>kr7*Dr0p-O5O<@?QZJR5&qTKuRQ1>F}*g{O|lYd zB(6=wjlS1=mok;Cm%u7Msr5V>+KRDb_yBJnHo;kl4DZ8|b7~2`n*-;NBtVLKU5nbR;7EVf&>ov!A!xwb zHnd^+Ca?cBXZ##d6Zw9f41w1e1w2vPyv{$nLd_pdeMk46!+5}-M@|`~YXs9rfQn!v z2?3jpB08Bf>48lS8Ed)^S=l2vdw8GB!2Y`zis?tnJ}mY)?5uHG73iFE+wl#(afEGE zkjb?*!u2hFH@qGZoQRTk-s6LXyVH@b>WiC z!~@$UvY`{(xF6W|oP`}`~ShkP@SrL`r5LDa6CAQBL$ zCwxO#IYCj$;84_M#RT!4wjSfrobv;bY)7EOwOdq|hTA_6_7uIFm@c&Vw)O&ckj0^t zG%)oy3JMYSKx85HhGN;F71JK_;)Ibh3@)6h?N7{Fud+h8{7HvTAF)aKyc@mP)$%E3 z5%#}P|3s8hm^77al?sbj`brEPS4P#9GpX>^HW@sRk=RY=RacW{r&9ov6-l(tRsWTZ z&+J)D_do>B1dXX2nT~!fSF@tGl-2Xq?A2eg6#*;J1q#4os&L@~F{VqITXhR|+l%la zcYlWB!9WHbVLu5MWqE~ycTsOA%fmU@Es+!B+0{&`jaW7pa-DrA`rs{=QRpb!Hwv$6 z9WeNX5_}&OfjQ20<`|kq)$N+R>4Njb)US-O@XBOQqRnsl__)%^w6w?$ptt3t({*v| z>Nx2isp@Wb!5^w}8)yJdb9i8ZcBgv#g+0dFL8KK*KiE2Yx>|e=0^2w1e!kohkr=wV zk;YBwAeLIUas-blogi`1%n-%424ha&y5y~r@sYDlN<0tp=NDnt;V{?s86zBOl8pEl z(tf);E#t0Ny%%BE__5NWgVk49%GFfxKI@d^eV>hP!X`s09;4G~XiY*b>|GV72*R+2 znkSWq7>755IYYCBhd5vZ#Ar5teslxI#uCD!u zohWVThRQh$bB_6KHl-ho2_^1gES@j$f_lJYo?`--Vm%uV9+ZWV=-tymOSUeqnaDcD8QsLfQ+^M3s(4m0lv2UZS9&AeJ`C zV?W5@??*N9ez!*KDb1~D7pHaX%x$-?kb1ccbCv!?liI(B(oQ1as_|4jx8Jh?dsy_e z@StIQz3zxthf$QYndkK?vFC^Fy@-NoXp$CIi&8FYKd+FF%TF;t5~0Du5VPnk;UR!f zW}r6Xyo`4h?QizPib>D?VK$yua>wF_lP%@Rg=3l~n42^I+3sEt3IwWJ%^a`hkF$ZnSjk^PL`tPtzT0A*q{+H}>;JF4-Ktp03W4 zqfpsf(h&?Q+WRN($3aXOHTJZo61w-7A;yqR-;nIvr|sm=q?h3-x7%FdY`ir%19JUS2sOy} zeTXjDhjmb#!X_F_b3t*Cf=VV@^wt3n)SMz;(J`5POu+1&9oMhD^TAN)312U=G}6z|ImbB7v(xmb;q{hCc;l!sU!cb6{Rd!_(QHq zJ>(#7x3T7ZO6Ky-&BQ*V9DSk9n!BrA0IHqzC4WyPMlll7DF7`iTG^E9O6o-B@wkDV zc?2`PWJz5}55rq6uqnw3Re@v*dZp2@nnpY#*Olyg@psc7m~-K+C7pkCLWz~vU0<$c;u#W9d& zS0S0P?D`IlVx+DOS7NWCd~>KqULQz3f=i51&=P3YL@Yjh?C;VHr=OdmI>ty3sKmAX zRGjrh^kRyQ)8vHVfT3g~O`<2q>)x1oA>fy*a?=Mg*_My3mR!|4rUSA|>R}UicF?-= z^;&lYlYLl?uxKY)M1!j9eR(1^c<74f$N`bbnL^9nW%<*t@cFp_We*u_*|Z^2L&eB%!}a9bWn+@%q3BNEAM6E?9sfJ?k)<5>_iq)z zL!KRbB^L^fF}nL4&^~)qJjZhSm>@;|)yV_P$pc-AJV8nWC`mG#Pu+#iW)*7u@y$|s zv&rE7eV#aD<#;u`3=mS@SX8E6A+7xhrm?8Jaq5uMzJO&XVB7v&4)gQ`Mw)$Sg-XO< z2;Nd9Oen1^W1bk1>9!o2Yu~eD-EDbm6D#8|3BJ}SjvrhlsL@vYX^f?-VYEPC$D$-Y z$JG&%T+d|y<$lTKo6MzjF?%Z{_4xMXED1~ZM;2Eq7N%Ms#%hl5VzD^kTLJHdT2_X+ zEAua#qB7(ukd+Y#ljv+%05&WzhsOu1g3azD{b(s03X(=ylfAVf%LPcL8L>Tn3rdGW zhUCv54gy+3d*%T(5Nb*=+qF?WDl4Qujt`&l^}gv~!d8-nsy`q}IV#_h7_dkgusmqMJNFFM}Z5dzC%r5dkp%<1zEr5rmKNuc=ynITWY3FY=sZcOl0fjz8Myuv8lbptT(0 zx$g_5nOM^5TiI(%NmHu;+@-XuA--^o#crkz-r4JWoBB4MD0nVztA0NYwrJiZDY(z- z{{cY)wM4WO>5f0^0;LnJ^LDu=tbI- zu@pU2zdh6(VsO`hac11OPMlPrn*(Tcl7DBe)I$1F1_RvwvNm0^C)^c#^KlS)K55H_ zl8q;j<$wo5O!f6x@t^iVEIv@z*~UKUL8-MunHhM_=43^IKf6Z+%Z%5ove#5N;gJZU zI8rPcOH#WPAVQ_6GF7j4jx&~{r-rEfKCPq=@IDJQZ zk%w7e64LUgs<3z1CUa$G#xlB(R)>oW5#MAvX05s#i}7@^)B?zc9{Oyuo}0wmE2#P+ z1nr)E3rO7OGQz`|d?ijJK^g1Ve@lhdLdT(Bpq6;TBZq#Z+|9gHG;rSI@YAAW#ucZ` zF+hQPfNl&E?0Ufpmtu;y&!7KzBaNqOoIuP=+u$%6A6_C<;R(lmkCg6^H_AL%Y1r72 zE%&{eTWV5us7z(1NKnt@d3>yFjGUyn2;noG=jGuBg;mNDfChrL0Lhxca%G$-AGxE} zm;S+oRhDWUjr6NETW_MTW1@CuWdffw4)mfMCY3e!Ub4iz6-^u`MF<}EIBeVp-qgio znc;LuT1l4h{`msNy!*Ip>sIT{`zY76tst#$gER3z96BDoAR7GJ5^pwW@s5>J9@V=6 zU(u{q-CS@fmU)Z$LkGrGaQAdY*AkLbk}5z#QwKV=loO9Yj%U|{!Kq|E#+<3aM0m_A zj`jekoh2;>{aEH4h(x?>NXhx!)NznI3gR6cZMt7}L=I*iE%af34tI^rq8a?Q zD_W-KxnjOkcDk2BuO&^rx_@C!in$b5MXm3Jzji~UM#N6z@eFy8m@>$Tkzi*>GKrzs z1a_j({@^%OYmX2Nepvs>_5gZOl&Cd@@8j$8QTY2Z39pS6p=?7*lMgfQ*s~c;M5*BN zQ+&zD91XURVv!*f3Yt)e6Xta>s${zu9pB2eu!b#~esU862U5Cekd-aSkwyMg+iCEZ z;nd5*A2I`)Lk|SL3QZ3Iy(!V#VsbE=e5s;@Kj3_lQ&{^wMI{<_srKtmb9TQ0-}Qlkb^BUvtlat#9|vG0I*68DfUjM=>qL`Y}0-79osKYyc)a@UIAau3PKfXlfT!)GgRS zU9t=&qxRU2peGK8>ldXTxrD1Qo#^57KXZwG#+{-Eof@SU*+&Okk3utk#BH6hN1Rjl z)u3tEasYwd)`_-f@8yi20PvxRC zt34dj4n=lJ%9~?Dinv=8;p>1iC6!qJ894{HhJnff;(Zg1&y12wP_YIEhN_$da6!ue zFgL6lFd;fgV!+|>!LG#!sZa`!K63Z;)F`PFpTPGao!*Cb>|}XQbhKJ~RCs)iZo4lo zna6-v;E!qTG!*8qv|^*kPqq!KX`vbRJWS?scGj$!XYW{)UkvwJ?YDBnU>2;-wR)4I zqV!i-OhCl#N#UjO(3+!ZufAG>XiU(9;^uvpYza-@^1}2tbnC8>|Ex_W!jT=#^4^=7 zC65WK$yH+?)Ft16q@By+UIfjqeb%Z*$B!GKT1V3L$qiFycPS?WwfRJb+ba!ZV(2|V zzH$TGucPbdxJF!eVuS^A?1~Nw1*p$b*wTghKzmdh|B)i0&wiw@>q#!bmnORcs!HhL zXV@s3rT#eKDDI@}2A%uu6QQ-+6>7{7L}muRUqpZp;yP@Cn!58GG0*Tlj8hY`%jql_ z*9$di{kut>-?RF5k0n@soK!!HT9HijtcSEgk9B|#z;JJ^w-yVQc( z%3Z5QI_Sr_>X+)-wujcn`-B*(J8wu@998%u+fIw{dDe2Y2~FK9-d<$FmF9V}7ZrG$ zdxY^C7zb8r>7lZ8`1>PzO~Ni>ZSx|=Lm|;-{qPa?N-1eOd#nU)+lF!8;P#R9rU{<> zOHweRf4Ti{6nNroC8qI<|4GB5TiDBV`L7zgCD} zb4{*Ta1o@%v@K!qa(BlT?sifUhVIMrL4m66)rTM^ zifm8Gt|FY;S|;vUrEbr+ZQZ3U8Skc0GGh#RDe%yr19Sf%%7~msy(Kkmetl-ZoY7+C z^|^|2X1e<5vJMmo?C9~B8}N{6W@4%>%?krIJ3UE=Ag+R}#CrXQDpKO3b+?C7Sf%wo zl!@Xoewo3eHu+I_ zvmtBHCkDx*d#*gDz`oFI7_sYx5(f8`D(EgR?M$?R{a(y4$Z%1BvbMI|R^ zW_G4j@IHMyfwZa^rEdNOGkJdR@)T8FARb|Jl8ix^vT~cWiXO)-ha0j$T9>h)Adh4g zP{^=CFptT9K_9d!Z2|Kb9$Pja5E6{sK1|@b#Gnl)M)MTHG@N8FQIt0!rY`O%c(!Ag zw&@o0P*p zQQmqCI5USXqn$H*vi)2s9G{{xK{Zf<8B^_odPp3vb$}JG_)*D}iRzY_;TUt`Y z=y`YvwkQcVKglz`BI1k2g+by?djx!i3gUQFnMN>rWHM|m*m$>4L3zNU>KAD?Q()La zL*3(;I#e_Cwzm)ZF%3}_9a53AZxc3d8QXvUs#N>8<@C4Z^ta{ox8?M=<@C4Z^ta{o zx8?M=<@C4Z^ta{ox8?M=<@En%IkAjjGJ%2sN*E+Q0fYdL4lL<2Ox~p$?vNis?)ok% zQ#^fPVbwZo96X$mY^*l9a<{~atq9M)#Gf71f(JD@@@eHnd`8Ve#y`77#m+OxAhkIw zM5vqjVeWpFqpS4(X+HvD=YT>R&Kn1GGJ1H=4 zK6-T4Q?rCNG!MF7X!cN*>|h)`ncHtBw*p>LPngXs;F(c>Nu`P8p^nZkA^jOusu88Z zDSL{6#b$t>ZIX+LJF%duNQU=J$!y8KNCzco;!qv({9(?EW_e`j*63K%lMchkKq4k6 zKoOq=J8=Yby`aZwG!Gojrp|WJa^b|6W{zUNVlH~f8J8c$u$MR2)%0?Xl3M5?P*gm4 zc5iib`Kgykq{*I`*HFs5#F$;-NVd4heV1JnQ$-EMLdoFfh3zslG1iv8dg}7_SG}1O z%XHcX#~f~oIMN)j8FWc*SQ6}*{`;vL_%pEV?QPwvsL z^7zIuMG6w#HUaqwLW5@;G)G;VJAUfbUbXTTt-URXT`x%Z5`VYl(dETQaIf`r&z|vp zBA&fQ-G{TmM&zv9>kGELHH#QMWkGInhJY5Q>~{eE1}GJU{vNxMPu56}Bp z*Du}%#bK`Zv7Yymj$e9Rk9cxriXoF3;l~_H7RW~03csA}&4_(vOK*~-I9HPxQ49k? zZux2!Xi)(9Wwr_FOl=OI@*=neA@`pCmruEd15`8o_RI(>X_qI7L{Gub{1v5OqS=Nl zPA`nm+}a^?iDWr+T9LFO56n?f7(FogB_UTI$E00)w9HPWOQ(D;Tu@nAmMb>< zS5Lo+0dz8ByikHkdB6u`hYbi?E#NhEO~`DcLHwD}t#TRXTD0Ohm6+cX>p!-g+2X!9 z4D{P6gb?pNG|LP(y2i1t{wj+J#+>@d=qwQ6sGE&4NlJO3WJ}m=vyHqbPg^0u71&9- zP_(8!F57lde)5%Ws*|}C7g0- z2(oh6u1)BJk7JT`dN-{1Avj~<5M46~nuT>x2fi8!j=*E~^hT!n^i0Eu-4iD4k_vtn zVT^c9#`y$G$VSYoCl6Iw)a_w{gzy9ZP6KQSMrGX15l7U8nCGtHekVL6doq=7N)`Ln ztvv93O$zZ z3K~LMa8L@~k}0a2h-Z62U@en1hMx&ls!$z_Q8Tn%z#~mpLo74b-JhfnsonA>dWb$C z9yNPbe~~{c&ZnbMCh_C@+>C$@M_O?nCYCO0Zp09IIW0!mQbbq>%ePxaS|)bwM_GxY z@Sz+E!Xpf0?wtF6nN~UIs8^_8*%J4T-St)q%1Hu5oG5}Ztg9D>5BFk)uSwuiz<~Ys zhF`dA*7(duV?&c$;9bi`R_FQUv1`1}hGAq#vH%qL#im9iV)9kFxQ->$Mg`ugBF>1BjzC=eH}Txyju>j4D>mr z7bQxq%;#B=X=f8aXwp=ENE;O5H#{yGSdt&+TCQR#mFRmSFn2G=Rdc>!)9A~L(;mz1 zEwyws0xZ#)i{kLE4p7k&;_IC9;29(y)ExyMi2CL_b7VTb5cAXCA1vN9VZ_n4G(APC z!>McZN(<^txsAu5{l{(S!AU7plQk9YTUAQ|)!NQ`quxy%IkvlfSNcy=_B_VP<(YkU zCf%aQktgmqs<}N6!kso-1mTTT8=rNCdH~D6)<(FiE|%Bvyi8TM%nQdb9~*Kw{M8b; z<(GtDZ<`CUH$QZOPJ3^Z%Ko$|gj@c=EgD&BZth_<@A9h+wG#guG`x%15R*XR`>#Sq z)*q3hBi&|QDf>HoZ40@k3SIh?x*kjj;I0A2N+zBSx3&d#+w=xWUp2uhT;7Y;9{_3i z1|lnCu4aOr4#Jq{XDd`J7;B~vEfj7O^#f*VGF!9+I7v3=A&8)PA8RWRzI*E3+`%50 zaV80T)pcgsR;elvXZL-xcRuK7OXG)Q%X*_3d@-@tm}-ducR7@{7FX8X=P zSZiuwhQQj9=HZ{(Hbn6DtCQ#@xt=h2+dM>N`jN`v&@4LAv>4>8{p#KJam}FAO19~2 z_>(Y;M5vlPYww~cPa*6@xLCNLBGwXOC+WzUg^Nyl(Z+0VFF4dAo)q(W!O4a`Te|&2 z+|RXNFIP{_`1XFHppxWfhi4_*FagQYWiwvQcLHy&%x0leEV`Uhysjq4^EL?IdJ<;b zL|O&pe>LHEi_TBOp6wjKBBBrdl;NMe3iRP%hAlf4^ew?ixXP%v3xb3iD$}m)xX>;l z6H@g7gn#JtB3=U_Y(}lzBzTl_{W(ltX3;MjJhIR7o#`2JNzhROYH7TOI7O7cLtxGG zIJ07s9&^y07whnY-wYU%-=WXvX3k>(L;2S<(~zO=Nfk^8oX;OVhy!%&gPqWL73Jf* z`w^}Pxg99rb1lpYZ!i3;lny@zMdEs~62*|P$v-5(c(1AKa+@-DubDy5Mv!g~<7)^b zP@Suk=i}}0;i#uIeSr0wTIFoCTX|B#lR|QCSK)<|GI;Ej9lI-j2p@!i?mfX=DcG1%Y?a) z5%0#gUJ@hsealB2Lq+`gFk$Ecz{sI=WHC1ZL!*IR`%MDT9NMejDm)ho_N#-L!RPNZ zwu^@Gnh;L4E=8RIrSq zv8L9shRv7GZ^I?A6_UGgu_9J1h!^H`rV?sZ27bN)h$xB12l9~k;c4Go(Ht9hN?qL(ywm7-r{mfV@O+mFVuD4QJYY8YuIjhmc7C{x4#tPNxL=*F{Z zHipmKo@HOj1iD6{$%GV+Syjkks1e8YQq)I^7>q1>jN_WR$e2qs(kIr6?EEB1Vk-fy zEJj$}Q9T8AeUbdbUu}LDi4N7;1fdm%Z?to?rwxp#Wj2Ut=Kw8Nou2TBcAz$1jD#pP zJ?Q=l!PI??!S{5buMFFSZjmOCd8vsf`GfU2x&mD)2qqm!F^=pTv!I|!gaM^_@E0zc zQjNFR6ny<^?w0vnmIVwZa6lLqgFgWD6X1aYD=LrI`#f#2MYVq07&lj3e1HKa#MDZW z4CKoUbX>)4&G<7*=bO`-YI^HihKjIKeHp$ATrD)lb2V3HqxnZ(grQC5X+Vha8ojbw zAv*b?F~VR0D8YK(O!V%0q^EOIJRU6ft3`XS3>(Wp&=?-8snlxUN%=a&`S%-$%E<)b za@AD2uDYjHP69a2u;kplFV|Sj-cH76!fR(iQ|rGL%3Yz93{R>chxrF1vWjS*gAz5LD>Y{GP2WTiA^1`Z6q_Q3 z44SGu939m$F){bLLdGKP+T8oI;1yd8T~W~ToCC`BI1!HRL8L{~`#DbqhG!xS_fYT)8ycWS}L&iQk4jT%~zG35=9^vKqAw zdXBVP$4Yj;nNVZQ^seEiPh7dE=JoGU3alMtzu7J9o_RAIe1l=TzT#J=AFQ4Eq;|6E z(`cUr8lxEd6AN6Q&bXEkroX5wuxrdf~t}N-v$d+VYeKrJ5je*PwKo8hgs)M zBx%iEL@Wp~^cPLM5}m=$A>~%6&SjIy5+SE(h(nkU`M!0H$}~_vjBkk)7Q8TC^g0{v zka~Xxm9_-Q&VXAPCo$CL-03aV6IZJU%F~KmHuofYLt_ZYx{$_9|DrVfDWMqOOB8nF za*kGHERI806^np^jC3N>8;0-r>;MVwXD7H!UfpEx8NJN4f8HsTlQ`s7v*v4iKQAHG zU~gH|gBx$hq35X~bSZ4010-IGKsShRJgfJ3PWRkS;k-tGeYyIyMne3N79eM<*~PN> z2ZZ{}B)A5~3tl4RE!U>R;aTo|(!$Z%5JOynA}YE@H{wS)J#y;^kq<(9e&Qrl18o^O zNP5Pp3YwgEydm5-wWhE{khHbBT~C7zh`qHKQS%b1{z{JEe55wPXWi;~655r@;h%8L zVff)5zjIBK(FHg?5ab?m$yl8&!e3P|4zip(ueMwvTYrUMoAA=-K5+yesjI2Qk>W^W z)Lpys+CTCXyA09HV7mLZjjmM|wIv%33R`EY9fx(xn!*@s8`@uSMsHf>f&b$VFrcqAikHEQ+E`N}~V7KR2#LU6@YH1N;PNSM=$_u8XU{R^?WHQ|Sem6Nf zToF`B(^ppsJ6!RrmZq+FNc%FF`9<^0^IqeXJb7?&5t;*w z7JA}4O#g&xlu8k6MCQ$6d5^l))HC)q9(OuH)|op$4sP3-yG*QO8)3CpWA`uGbiCBRao7=XhPf1HlaKosHaoA z@ab#;8#{v_&+N6G7H{#bTO8^T$zC6oYLX!kc<8x9iq3}!UaG!Ot>~oFkd+cs1*S1r zSM74Y83me#9I!}{Sc015^R(0KWY7E-!(bq;+Lumm%hkzLC+Hf^-PmnfR5D==(hP z{f5o=Dl2t4X2^-zH9z;DO>{D`%jce5a6Ry@Nk+h z=4`rjp(7v-e*Jo^!5I=Erj^<^!44roVq-#RK`i+K!>Hz!eoc7WL=$tU0`DlHL!#e6 zU5UV_1&Kr*q_u=vl0OQj$h^LpT3Q^sAei|?rZwtXm>1g)5>qK2B9jxeJQIYr%ZwJ;RMinprrHlJ>C35lqowXWgv^0xTL};RT@`L$o4&V0$x>=Mzl)7=Y(yZnarAr^=ZlKD?Tlp`Il{je3sK* zoYeg`XtU}ur97hyt)ZWTkz&5?^nud23CkAS4Wh(O6;{lbo+BO4Od2Hya3QKbiGNje zgIH)~R5UQ8W?*_D-VV&%up8YWEMu0%`fRl9$gOiR#aO8~ayg82Uro-aRP6cehZ{6( zdr~PVyLs1dZPaI`ds~X{w66)FNo;P6WQ$ zNN&MQcfDuKg$VWqE0nnvOb~r+$JX|C*k`*K8tpk_kf?gDkrXN-ZaSA_VEvZg>1%E} zjI?i8iLzIf+YMU}f?|lu@Ww|PbIZGS1n8|KkAV7%)8HQi~QbU^_vHXCK(f$CVFEz88d~$?wS{ zoq#sWrqmzyiFfpt8ui&khoc#M`%vToQI`35okaizLHMvu7o`~CY{ z9e2}V)#%%F7IS2i>I*fyB+Q(Q&56;- zP0WmkL#t_9+mTobvrx_w#Y+Ymcu693ARbx@-5eoD!fQpjfnxRKOmSktwVy|oBosX` zI+wO)v$87AaRj~*;Za-@!iib}UVPf^h$hP!iV4=&w+{BNqv%XazH!wjXkDNV&G8!s zRPBr%wb1xrB_p44UQ}iDPVf5N)O&_Npn!4jV5u&A)3(W6KUjT6XQPrVYZhKCOmj1r z-4N;{?UW{c1>I$r4Jsuc*b``t+=QsN{4#4*6RW(gi8M2J9 z+`nv6`w&2u6FeD(D2zvB+?l8oxDqmqG1t7Xn6Xh|0!KV_UzH(kn$zUzb}(V)P_!<2 zU)wJDb8OkHpR#>8AJw-QG&_y8Uk>lnJ++%xKTg3F7{H}|$^g5vc0#cl_>3X=#=&{_ zS!FZ1S%hoEPBQ!l|GPXvGFD}LQOwGQJr-t%wa!i$W?bt#atYa)N}vY+&at{37ZnS( zbJDIBXO^6ET7LhUs~II4E9$lvqLX#OrZ-vj0^Xy%4n%RDhlFGYSr1DsG7}^~4C)_0 zY%!qGVDp!YU;tYCYeY0jNDy35*)Uw6lKO6{3DX5{; z+B-*cC|8aY&TR5Y8$k`s5cRAW#;{Ar7jDFO03o#upJWmXam;-qfBi)r!`d~X){&PuM6q2@4!B(IFd04Yk>^cGCFw^_bWFfiYspfRO^XPAQG z%n7gor73)zU7|(kL9Yi&eg`@;6>Qn+moaX7iSR43D+S`0oQ>QQtT~*xHiep1ebW{4 zzIfHRQ9e&Vyz&kB&v|g*XKUWY8m-SXvb9>#Vh_I2z1qE~Aqf@Yq3JGbp`bN1;Li-P zSBCGutS)1TdEeD1ZVy^Tq@iCMseN$BBj!90dsOd~>zTqrDLfDa2&H=1Hk#bSxWpx= z^!?veL{?E4_c4CleO#cpL0WJhO0LcR0=5C|N7=$26YDQPlq}pO{H_+eE&-E~9_QLu z&KLBv0TRW~x+=bpm--5(PM0toKNNdD@>64EP&uZyR3|)?iYgjseOsZHGQsRy=tQrz zujRh&Wf_&s+OiL8P=PWJf3R{{zmIBNO=VSo0*}m{F*FFO-eqR68sdRwYLs!h^#_vz z{x^8_+~KL+kJp&2JW~5}nFP`1pEy$buLHq^HpT}#=|9M=70=2lkja%*oY)QHcCl4d zJo@I>;Ij?t&Nv6Sk}@{J?`wU3)u3dER&F9%vX{##yi*$}D@H@psR-6*eiQ49Mj?QZ zGBZ>z3P8D4VTHqT=6f6x12L?k|4wtMX$iNw;1KktrhVWlO1;FmB5iw?c;h7CKxX|D zo}~yt?C27-RF~{PQy{j zRr?aqP3q+oTQ(AniYe`@&!P}(`?t}Yl4C8uda$DRddaJ9WDq3e2L>UKA@(D?MT5BL zSDOIIy15|WV$k9t_D_vFw|+y(6;okEQGqz*FK?(_`RbYvdGJ_y#tZK>jCYRQAM(R1 zi)k1|A*Me+uJ-5!oYS2MNUUC|1@6Y6B9 z!Rn8xdVyZeXXZru(rY9zJs445GBBVU4!?siQiWfP?BmgAbfc;zWoK4%A&_|AO{@O0 zPCoEH*5GcrEPi~DY;osaV6z)H==R~d=}2qeNeJX7+VlQggEhyzf=;{2N!sQxP@ezx z%v!@dxecWRGKQJ|8MQ64W-Wtf(=u|x%75wF)YkRtn*Q-?7!-fb2kYv@YWJ1|BM~Ha z%%Zv~gA$`U(Ak+IC+ei7jI` z*xM81!m6cVc#X3nAbuMv+=pK=iK)eSblcxD3o2vU-}!HqkTX5AhVyZlkvj)>VFX2g zp?b^P#O?`?;*(`qm|zBrSo?TR-RTeBu-H791jd1M24p0puKBlY4jdTC6@M?yxET;G zyk9Yd4Da3%*5bzcdM!#4d&Kq$+@oOTW6N+2>j+wYhG?A)M?Hv4hAl~HSG)1i%j`g= zcYA$iTxNXr9YW2_%rmG}9iVHwvqe}YW)rT?&T$cMb-p1L*_h+LplM?{g4H8n;iBb* zea528P4%KXtlHc-E<`8+MI3Q<6P2^|79)>HTD>=Ci&v8@m#7 zdN+})>_!>;6?%1!6w6gOt52y?#4CvwG?JPLviBww5wT_;00z!r8z0>De7T-0&VqER<8 z-->tUr9W+DWK=k$1{m-X2!t9;7t=D{f?zAiOG$$+NS3U{mGc4``hxJkHeNQ`cs;$c zgHwOClIl-ILP;y-CEy^?rFyulUl zwM%);zCgZf!yV2-a>I}^u~vq~znn|!8SP?-dVuCy-gr=f4}H3aFrh}C%$|Z=n7k}= zej|1=f5#|0R^*GV(vJZ%D5UO6TsAs3cRRvLI*33Fs;pT`YxD$%l$ZX{b5vghXacO{ z-gf~90Nb#yjZ&4-POJ;=Vx(T;G$LzukLuEj9!m|040_`r2n6oN{^Q^Q&T+- zyL5A~oNWeuC17?C$AZWN{jYy3FRdE?eGJ3j$1wbT48z~YF#LTC!{5g+{Cy0=-^Vcg z|M(aNmaBig5(hlh41&m!M{J~ODVpA&d_sI0}Ieeu#YvqCm0R%qrTXDHg#`@QowS1b$?# zHzP#~2{8~aMXD6@cYKo`_+|I*xmN0{RqYeK?cEjZO5j`M$5Dpu(c0y=p}6nx@A#-l zM|U+g2tBk2eHb;WS{@uhaNP_!JtXxIP<<1e9v&V(@5{t=vq13Mg*)kJ)I4I0-8(Tk zIYDt_$@^Bs-q$E6DsU`pe{}ua+mR`x`neCESn&E?2%gwaHFLko^*!#a*lYEB6(8IR z9F~VqxJl-K%rIN5jP@x1jn1$wEN{VhpUr)5lBX}ZXrPtG=g!7JK*%dDd-G>(r`o7( zS+;}ehEP1{sp#K+M$s>7;hk21?KE3EjkVw zG)p+WICs%c(u2318RQ~sWG>ws4ZlunXZHw7V~jF=``b5Gnq7;1ZFOHhmKWm@G6etN zN5Lj5N#NcR7U4tWu&JhrzCB5+nSSlV^WXb81~vA?vXI4Jktq0Z8`limvg@yL2u9ot zdqL%|vFjg43wmHjvpzk!y}XJ;W9<6vh;cM}<#pU=%9OW=JdH!u4c_U&IPRnE6tvKH zY8^{6GLPfSA$&Q&XZSbHS!Erape1^^*xKW0c9A9epsCi1h}KzEdL1;yYlTWpU84^V^>t$e{g9=G#0#q>MCxLYiGJpckM@Z+^Wi#1Usc$A@{qm^nE$ps9Y zcdXI2WTMd+}Zphe?3DTwG1+L zDM!Lk=Ifbq_)azER4|TB&5&8WC81y>W16mMWniS= zxda`KYRaN@#%Isx%#p66d;=|$^sDA%;s{ayFq2X+?aq2%mE>j_6e$j-#UGID!HedA z!F&s`XSuVb5^-GP&E`hG8cYN>K3V_Blsv%TIs;vG9Qpb}1g~qeg~4DS8fRD7*WD4` z+7*Yr9C@%7+1eF-2ol}emFSsclF!gLUYV%(^eG}>e@%)K5rc+QOO*)<7Wjeh%~!eY z1MyEbn|Z2b2}2V@7Ny7%21jO%bG3@L2&QXTyEgUm*=nht5s?(K$XmLBN^TGenpAxs z^|%G2KD{7@-7-teSjNP&Jjoq$79O)+h~2=euqaL#2ij6Y;8U??jePOQ_hr(KJpfyu zd0wAI`~-}D0|>@%uT1(P-m)ccsu^F8(&BLBas@^p;Vb9Qz?xwny6!^k`%OFRC37FQ zEn@1dTA^h{%Y&{bPzxo)K*v9S($iZ%!8s&dKN?h{5}k)olYJ|Rtm=mu&@MN6R8!O1 zAbHeuq0yV*^Zo9KfU$zEsN^DfhyfXpX!txF1Jj8F7wRpM)OqgKr7 zeQT1$r=)EtQA4_eFj=|g?vLgd3s<1P13EkXufRyI=#EL)t$$K=-uIre|1g8DF&E7WK=otGmcgtJk;Hu?^2R&g}!&Pp^R=Al!lKCPK&CLdR|g80KtNrA*8G?%+W{}lA#LxC?ThmnVjP8xazcy2$}ihn_+TAJm> zeMfZJ;>cs=ml(n6g-1!RA!&8rJS>&#Z$Z*NBFi_9WhsAt>wt%4Rs#`pbAkdPkAxnq zsGW}sG&gpq#8fi-?aluQOlBqnkRXQO*Uv+tUi0)Fe|$MV2l`NG`?m)N8cXeK8-^SZ z?E8S36X9`>IJ{3nx`U!ppIUrS+zX`?rLL#KC_^t9ke&v5Y&tt_c?P1Sd*Eu@jtBZG z$-lvktV@%QWXw9}7&sk#J0L#r2^R?6W(yHzC zazYlLSCSA4=O8`J+fp*}X({xHHs9d7hXw|^A&CRvU=UJ&MyLLqz~r%pCf)Iak*%AG zIqwo$d=mmb7#81n&bxT=Kq>Ca3kG=dQ%P%3!vJw|9_0WxIdOQMa|5Sg;~JUnR>qCjM7M$*oqTC1rqVK@vI9xoF6EUO zsj>s*Z{NmQ2lonFr*bbC{LJ6~gRlSi4;bdV9_QQiR(lvtb8ynfFYHXVQErmnsB4s+ zw^!YeD%Op+UUK`M9vznMRk{=o^yQed$aTYCNrz;eQ|R39>jN|@fxt)OhY!`Q|1+*X zF-ToFKg4*E8*bSe{{{9{dT_l}Y<6Grv-bCbuzZpE@5&G&Q;o2JHb^@IQ=!fd{5Dtb z|K?vA@46Z9m!)Txd!7&4C68mlf?M#QL4rGj>p+0u?gV#tC%6W;-~@LG5D38`*x>Fk zxCR?+cJh1g?QZR!`>8(P>Z)6H|F~86-0pL_hlAieX$48XX8SR<>`Sb>9Uq7ll6bv2 z=_SlGFm;p9RX#E*$r*OM43NX%IhIz%l~d)b35a$X22zuMX2GcEr^a{{JAXTZsAC=z zc%fLOQoe~tqA{wJzw@jysu;fTOh1RkZ9_8I>oS`4$et6E_avld#Vl6ysW3f4YSTXY z_GdGW>>m$mqDlt0+GJ5al_jAuS7#}?C06;0wOn&Gy#4Y3f3$`iELAJBis;1x@0J&j`>)Y{k{&tu4L6T*R=N$s(g_D@p1ZUzS-R}QZtPn^-lu!- zn_yAz63I=4kp;CA=7u~P(d*w-DpEX}r^EbItKrErTwBSGY0w-2^hG}bbcHK;=i~Ws z#^DlGL5cKl$aHtevIUj`*@vBDyzf$E!II5&b2)CdBU%ERJ&JnhWl-d7WYm{$iy&mw$_yVG|wM4eNRtgfzw)=KVqdg3J^e!fp?N0dU>!1YHu z?l15@;ofe^=7Px9Gu3y|T-e@MYk8M`!=iCw##gMH#~wYr>aPumdQ|H3g$ZAy{pqnM zd@V|A{Az3`&I4@h_|Dh?s137!MeDZ3?Ka{czc zOsmqPQG8D5q#2Kzoqwqhm*QQpR?2KU70yapU_2GxlcLk`5RfQJMrC|LTe1vV%>*eu zY-_7C3z5;xCNqo_et!(^63I8nCJXxh_;1?qRluTI$4vNRW?zH{nK1p?Iah+K)n%R0 zPDW+?3`CX|*5lNt-h=^6R0s;S;4vbL9SEECU1|ouldd{AjIG=cYn$A8dAV&K3s81= zGB|mbnR#f3c$KLX{A`ptgAyY3&Ws`(Z9_Fv_I-# z8sj<3k2WvxOXqgQt%7NXwO7aXma`%=Hg3DD^JuhWlJC}_c6EMzUSdTi8O#hObd)?@ zz*hK81c@DQqL4uTzgQE$B3Ltzr>zE@I*P2xI@jm8vATKoE6jGJ%J45WkA4WuZS&HX zN0i`8QWVgEezim_3f)Oy6TPt)?S%bq2yE{hh=v`He&p4MEkdDtJbLNltiltaDQLzAUCeUuShvkJEu@%7N(EMW+j3y}9`3fz^;jXUmGq z@_VvIC(g@-5F!W*dcFx0aM+-i^DVeYi3R2!hmfIsRR6dApKP9iFI+tw+`Ou7iuJ>h zyo7UP2BUGoLyDQBV{OA2fkyg=T4Tg;^QIz|cng!Jl{@>xZES=cuSpLcgu&ke<2vHX z+eXWO$NnRRHN4Q`io7@QKds9u%XHwm=lEMU7=}{UX0r+&7z31e@CvUdddFD?+OKEo zDHjd8hMDTj@7@VC3z}4dqIX?UO5Aa0We+LL{c*uZaVeIBvl+Ung0Gf(-94q-{C%}^ z4-`+zoqvUX%uk^G;l~|YBtk}XnA075RWv@IQ0e}C05zW`A%A0f$n|%~NcesL5P4Cn z@>_>D2Ak$S;plYB zZ0us9S7l}{`}|GsMmU==>R)%Fc{7QAfgP&yp@iL&a`-0EyCVKj^otMOyu9RCPEYou zxRG4TWbDJ6c3+hAwxEQzoe05cE<~xbvqM=83)$MUza!N`Cg1F@rxP(!P>YzEm>tv- z`i!%m2rF6Zy{hEBs>o6_77yXD-&jpRg5|jNbw>NmZNKGrXEF~0oDoTh5hfN9z663k zL{Gk2x*dluJo}v>%1)d-=i6^Q=Z+VOYZn8{k0XUy@#<}b}yaUrnjs%D> z5xdVj`wS9T%&*i+5?IWbjo&4(m<-&~Dy$3|E*ln#X~YYl`x5##>~;8)P0(EoUYZo| zN9dpaS@}e-Sh4mfqi6&;GyC3rARUcef{H_>ijr7#wBeLGF)4;OCB>2=vS5Hqk*Pcufg&SJ=p^yy9b_LU6@ zGV#7)(uwX_$6%*Z$%(Ch7^DBopHk6FoP=1tOKb;YcL%$l=B0DmzYP|5C>g<`DJMov zU`oB61X)kHZ+1VU7-Mj(|AMQV3$_-n&*I)Pg^bXFmqs90Ouqly>o7NK|TI1&?v@H92h9 z2{k$KHDN=*+qH(V`g!(wPKqnO3NQ8y>yGT37Td`*4TiLFH2Y+hxL{CN!Vf!1;U1X} znle<7J&MxA+2;zfe)o7=2;>RzJwqUW1XDAHte+vz^Zj_#rVW#-LjwQq9 zh-p@|X8Pt74uCBkuyJT|*5B!K=+%X$p9*={F)zi53 ztclfLJ+N``yF1uTUwt^!mtgoo`FmPKZZE#Xb_|yRQ}hGAdstk2uL}h#q^~MtracPN zcv#8x!5u=Pn9ZMLwSE5um4hLg+hFfJt$?`ptEM_4EsODjk6vo07#IZxse`a`n882TqWslb_ZHN4Q({`H|#`Acy- z)?!NAxtFi4PY}d`icI68vJLqcTA%8Ilk`Ex=C0NkEZ`CgT?sdWhM|h_OYID@1!BLY za+at$Bn484C4>>R5hvaqCxMy-vwzurecz8okL{bOA3%<_+es*Iq9ylJa#u2JcQwR40PcrRwjknY z2-?ULaYHHdOqFu9zJ9oEn!9Bf^W55&)H$y88za0%wZEtjvbLy6t zn=Ix%ig9S4lS7bQiaTkMOQ!;q+-#W!Q#*}Z>~*`=qQ|!om>h}v=CIOCE!iMjq{kKy zfOo~F2|Q2n*TzbF^&LBmKJ1N^?$(sRvw*sYG&P|W?@xmnYn_x^zcFJ_qY^1%9-4^+W<@+fBJ zwh_&YcT72y607H0Ci>TIzyDc^M+Lx258wPsrqgB)k0To(cNsWg?CE?zKrYbJN&G90 zvb$A+;nYxeP*`KQkx<2;pnE!QgEZvC6H5Wzyd_B1auw57>P6~B=j{o3UP~0e7~X+? z&VE{-HfC9yWT)D=WjaCHP$1com2N($wCoYEoQOaz7U2wv$Br;N~G z#@MK#8=eunQ4yvmhr7}wVZK$ue$J8$ARo!^7%6p+y!$(xUTi{W2=Cz2^ZnJX$}#Gb@g zT|%^EB_!?c!DUDxefTV%({`RsCcA-9Q#nF98^X>8?N9XVrKT#qaZ-9ah1FcFXyL#8RD4#GID<&ywW zsg-wQv7?^t0BvF31Ax6ly5=Rcn(i5kW7<;KDu}D@l^7YMrqK{Q_zxV%0qA&L?mcQtQvrh&%Axz!`wk$*oE zrNYf-ea$|%$5@DMoEZk?z8j^~6ZDqRA?#X|QohxrXdnr9tr6^Bj z^-YOq_KzB}zbh(LqxN`~gCZi=6@={yBEanfvoN@m&)HM>eTek%=?2I!ba|zS+mH38 zOmnGSGY2|MYdU^z$j$35d2otg&h|8=7O6$uK4GrOUZteu?h>8KsJbOz2$(4zq%BqCI6?OS z*5k|QGxbW|Sk-6;&6DE#&-a=G=<9Q+{N=69yFlqc;I3ns-$!Y8;4VxC(nmECE7HcA zhLkiWccRBtdx@7m=52+7+nWha{XzjzYqZ&ZxMU@+v7#<>Tde)sRTGe?i2P1O1)1vD z%ieoS{!f2m-AC3B$Ia7XHPT}J7yeop)-Ccnt!lGLuZOdfdZE7<-iXMX|fvL+i=C&Y)6@UAN3&3MGEL{Z;0=z09hk^j`lTT0> zAeJKc_70&Y>(4ntXk}=!5nB^}$c&E>q99tBBlYHPnStlv zSK{6^8*N5e!1u0NxB0T^Z?(1=v9=k}{g)V&W5|ucS+ZXd>DcG=*VOAGNRQ%}e9z55 zUF!p*Unrgv-yR-_F@I7iKf~F=!p9oqMUN{#BUELMZ$)Nuw$&lB5a91LZLX}6GQ*vc zB&W`th3G8AdoNQ%l6TawN1QXz{vR>~OQlAa$8fGv#X7KP7-d(DbNnDw}K_=hp)?$im5lE$+adKtg{U%`h98Gi92r7m}KO^d< zKETQ^!=wY6VcmO9f!SJ0L9A?CHkW=%mMrF$_?bmXETr9G6nSWZ`Kk zf8`-Yw0a**nD?+XA)^-*6!0u?z6|XiQF%sb^>a*eKuV-@1|o$O^VJjT++QRo1%ZyNL~>*zFO<#yQyUr z$K4)SOupP~F&24>R|^#$zNZ~a{5v^$d9-BqCG3Ph^Ad0rzNWK^|2x^DzPK=d3xuP- zUKMMx?dpx5ipFDd?(zCk;y?IN`i$d=i?Tev2rjCUt}JFyF{=lX{I03{G$5z^>g0k# zhx)14v6}CJpD8@BY}cE9^9wUB+w3Mnsoz~ne8F-y8WlRb$Egb?Fjtf7l6g{2VVMhXO27UnmpaS2Ap z)X$2X8Ot2)7N^j-qL~*fE(F2kH1Vsey43DR=qxM`-;z7@2pu0Eh2`XR#|>6nX(K(E zyf7}ajis!AY^~J-I(~d$@i*J&%cGJ>O8D?AX)M7RQ6?i@KDpXy<85#vnmFeMv+~(w zGp-h)a(_ll7Bth#UjVFO0#Xyl#GXBcKl9|m{Lj8vc(|pEO<3nLIZIay3wbvuB@0+X zL@HSudkZQRS0{6iPZs7>UN-L5RR8oiQe+YA$ma+4#| zU- zIilQd(&;GwOodAAUirY7NgEph%S!6CdFeC@iApNBm~8P8d+UQb0$(M z>OCLTGD?@ER-_x;or5N*^DY418lY1)F{`1O2GOJ$sq? zX;Fc9S$%=6vC7(zeazgxur=|w!tR$nsb4=1DEqbGV|0TA9z|(wel=Mh?i~rBfOWZh za+=s3RS+`(keqpVs!i3qh2{0KAPB51nj~6jZZGA$6azo*Vj^naS$2(OC(y{)yl?lJHHJ~vgP(tY(n;rj}@*e7!8TE`BrQ?qO| zC8aEXDF+v^yFF8UZ?3U>xg0Vq$=MxS5o;}^MAaEMa;KM3lPDf6`29|Q%O&guV$|iR@2J;zoEi?*_wQ9aRosy8 zX_$zuu)zWCty2XgN@VyXGe<1+LtaBPKgvc)obB&D0oLG>Y?M5ILycSx6+-@al>y!? zpO%x{9*})dz5k3MC$%s!pfHBw74#||ewgX4Cu#IvGJ7R6-S;9%DJ2X@n6e}% zsW~MIDTwj&T{^+`Je67eB$78%bhJFO@=typZx+^%TEi+0Z2rtD3!y8qaS>DgY<~y10R56+&@on`2 z)$F-~9TJI|eiFE|0yh1#pVl-#OdnbPTO)vyE4Pb29&XB3l-shujE{-52Tx+DPyhTWE1@2bj8uV3j&=$xfB{PBV!BtBb|?Sh z(H|(S+rpZ*VYfZ&ty|@EhzAmKN=BGWc)qF8eQ!*SDuax%WXUAu*sweZvR7|0fQ3%N z*ugYcw_>xB8wf)Lb`2N3ARcg-ce~6lhL0_5Km6ID6j}e?oM8G@AoWZFukuQHDmcLJ zsOZ1ITfY^z#px*zxuznS`;rpI2-GExS4Uj0b80wyAN*@VjN;apTq}@BW~pK^S79Fl zpdSx#$ARy9q|ZR67p?gcBP?T5xUJGt>{8q;vXg@I@{J#4+ZTA6U~|woy#9S+s#OA+ zqEqE{lUAr%R~`BI$puo~t)_Cd=EtpV^-4U?do26gmFTrM0gM+iozHn3Mh3^;JZ*gU zN8h{Yfls3+ne(}e^Q@&C!z3^*FhRA z#qah2sGm8!63+T}mk-~zfnAb-tENmr?$ep*WI?dDu*K2RW3H)6=qZbY6Hq^^bmhNF zL^I9>@_PxcD`}C^arl*TA7zrZW0pLXpPbb+oGFu~MPNE==+wWo%~c--%I3*yn966X zK~PJc*Gbk3r!9W+!wZ$CclnBJ54ixv=57q1OdMs`C-g>+e_w%+dUxiNK#!h5NcSRuw5fQ~dZ4Dzpl6Q~yc$ znhGWrMh?RoDqyXtJE!Un1?^~&&aXCsAJsK?5=@>MQwOXLu}UwgzGIohrng+fx_fG)l(j{WdfiL3U?>-j;c zM_q9%3(_;A8=K^bmF#csC->~)X7>S2%p=#1p&kYD91oMKR`V!N2Z?M=sT<~vi}>V{I6rmAV(Uaq8@Dta!6E0XXmbqVKZ`mNiQXkdS=!vsLzgswxf+iJvj+ zF0l>m$V-J~z@r+rR<$0oBi8Y|KB&T~CjT7Bc;MH?0Xva>=9L3&z*|>+J+_G5 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 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/star90.png b/Tests/images/avif/star90.png new file mode 100644 index 0000000000000000000000000000000000000000..93526260bab9d4a6247ca27d2e04f0ab949a6b1b GIT binary patch literal 9272 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 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 00000000000..7474172ab55 --- /dev/null +++ b/Tests/test_file_avif.py @@ -0,0 +1,809 @@ +from __future__ import annotations + +import gc +import os +import re +import warnings +import xml.etree.ElementTree +from contextlib import contextmanager +from io import BytesIO +from struct import unpack + +import pytest + +from PIL import AvifImagePlugin, Image, ImageDraw, 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, expected): + assert isinstance(xmp, bytes) + root = xml.etree.ElementTree.fromstring(xmp) + orientation = None + for elem in root.iter(): + if elem.tag.endswith("}Description"): + orientation = elem.attrib.get("{http://ns.adobe.com/tiff/1.0/}Orientation") + if orientation: + orientation = int(orientation) + break + assert orientation == expected + + +def roundtrip(im, **options): + out = BytesIO() + im.save(out, "AVIF", **options) + out.seek(0) + return Image.open(out) + + +def skip_unless_avif_decoder(codec_name): + 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): + 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(): + try: + init_proc_exe = os.readlink("/proc/1/exe") + except: # noqa: E722 + return False + else: + return "qemu" in init_proc_exe + + +def has_alpha_premultiplied(im_bytes): + stream = BytesIO(im_bytes) + length = len(im_bytes) + while stream.tell() < length: + start = stream.tell() + size, boxtype = unpack(">L4s", stream.read(8)) + if not all(0x20 <= c <= 0x7E for c in boxtype): + # Not ascii + return False + if size == 1: # 64bit size + (size,) = unpack(">Q", stream.read(8)) + end = start + size + version, _ = unpack(">B3s", stream.read(4)) + if boxtype in (b"ftyp", b"hdlr", b"pitm", b"iloc", b"iinf"): + # Skip these boxes + stream.seek(end) + continue + elif boxtype == b"meta": + # Container box possibly including iref prem, continue to parse boxes + # inside it + continue + elif boxtype == b"iref": + while stream.tell() < end: + _, iref_type = unpack(">L4s", stream.read(8)) + version, _ = unpack(">B3s", stream.read(4)) + if iref_type == b"prem": + return True + stream.read(2 if version == 0 else 4) + else: + return False + return False + + +class TestUnsupportedAvif: + def test_unsupported(self, monkeypatch): + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + file_path = "Tests/images/avif/hopper.avif" + pytest.warns( + UserWarning, + lambda: pytest.raises(UnidentifiedImageError, Image.open, file_path), + ) + + def test_unsupported_open(self, monkeypatch): + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + file_path = "Tests/images/avif/hopper.avif" + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(file_path) + + +@skip_unless_feature("avif") +class TestFileAvif: + def test_version(self): + _avif.AvifCodecVersions() + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("avif")) + + def test_read(self): + """ + Can we read an AVIF file without error? + Does it have the bits we expect? + """ + + with Image.open("Tests/images/avif/hopper.avif") as image: + assert image.mode == "RGB" + assert image.size == (128, 128) + assert image.format == "AVIF" + assert image.get_format_mimetype() == "image/avif" + image.load() + image.getdata() + + # generated with: + # avifdec hopper.avif hopper_avif_write.png + assert_image_similar_tofile( + image, "Tests/images/avif/hopper_avif_write.png", 12.0 + ) + + def _roundtrip(self, tmp_path, mode, epsilon, args={}): + temp_file = str(tmp_path / "temp.avif") + + hopper(mode).save(temp_file, **args) + with Image.open(temp_file) as image: + assert image.mode == "RGB" + assert image.size == (128, 128) + assert image.format == "AVIF" + image.load() + image.getdata() + + if mode == "RGB": + # avifdec hopper.avif avif/hopper_avif_write.png + assert_image_similar_tofile( + image, "Tests/images/avif/hopper_avif_write.png", 12.0 + ) + + # 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. + target = hopper(mode) + if mode != "RGB": + target = target.convert("RGB") + assert_image_similar(image, target, epsilon) + + def test_write_rgb(self, tmp_path): + """ + Can we write a RGB mode file to avif without error? + Does it have the bits we expect? + """ + + self._roundtrip(tmp_path, "RGB", 12.5) + + def test_AvifEncoder_with_invalid_args(self): + """ + Calling encoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifEncoder() + + def test_AvifDecoder_with_invalid_args(self): + """ + Calling decoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifDecoder() + + def test_encoder_finish_none_error(self, monkeypatch, tmp_path): + """Save should raise an OSError if AvifEncoder.finish returns None""" + + class _mock_avif: + class AvifEncoder: + def __init__(self, *args, **kwargs): + pass + + def add(self, *args, **kwargs): + pass + + def finish(self): + return None + + monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) + + im = Image.new("RGB", (150, 150)) + test_file = str(tmp_path / "temp.avif") + with pytest.raises(OSError): + im.save(test_file) + + def test_no_resource_warning(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as image: + temp_file = str(tmp_path / "temp.avif") + with warnings.catch_warnings(): + image.save(temp_file) + + @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) + def test_accept_ftyp_brands(self, major_brand): + 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): + 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): + with Image.open("Tests/images/chi.gif") as im: + original_value = im.convert("RGB").getpixel((1, 1)) + + # Save as AVIF + out_avif = str(tmp_path / "temp.avif") + im.save(out_avif, save_all=True) + + # Save as GIF + out_gif = str(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(0, 3)] + ) + assert difference < 5 + + def test_save_single_frame(self, tmp_path): + temp_file = str(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): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(invalid_file) + + def test_load_transparent_rgb(self): + 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][0] == 876 + + def test_save_transparent(self, tmp_path): + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + test_file = str(tmp_path / "temp.avif") + im.save(test_file) + + # check if saved image contains 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): + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert im.info.get("icc_profile") is None + + with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: + expected_icc = with_icc.info.get("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): + 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): + 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): + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert im.info.get("icc_profile") is None + + im = roundtrip(im) + assert "icc_profile" not in im.info + + def test_exif(self): + # With an EXIF chunk + with Image.open("Tests/images/avif/exif.avif") as im: + exif = im.getexif() + assert exif[274] == 1 + + def test_exif_save(self, tmp_path): + with Image.open("Tests/images/avif/exif.avif") as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file) + + with Image.open(test_file) as reloaded: + exif = reloaded.getexif() + assert exif[274] == 1 + + def test_exif_obj_argument(self, tmp_path): + exif = Image.Exif() + exif[274] = 1 + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(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_bytes_argument(self, tmp_path): + exif = Image.Exif() + exif[274] = 1 + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file, exif=exif_data) + + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == exif_data + + def test_exif_invalid(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + with pytest.raises(ValueError): + im.save(test_file, exif=b"invalid") + + def test_xmp(self): + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + xmp = im.info.get("xmp") + assert_xmp_orientation(xmp, 3) + + def test_xmp_save(self, tmp_path): + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info.get("xmp") + assert_xmp_orientation(xmp, 3) + + def test_xmp_save_from_png(self, tmp_path): + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info.get("xmp") + assert_xmp_orientation(xmp, 3) + + def test_xmp_save_argument(self, tmp_path): + xmp_arg = "\n".join( + [ + '', + '', + ' ', + ' ', + " ", + "", + '', + ] + ) + with Image.open("Tests/images/avif/hopper.avif") as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file, xmp=xmp_arg) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info.get("xmp") + assert_xmp_orientation(xmp, 1) + + def test_tell(self): + with Image.open(TEST_AVIF_FILE) as im: + assert im.tell() == 0 + + def test_seek(self): + 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:0:0"]) + def test_encoder_subsampling(self, tmp_path, subsampling): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file, subsampling=subsampling) + + def test_encoder_subsampling_invalid(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + with pytest.raises(ValueError): + im.save(test_file, subsampling="foo") + + def test_encoder_range(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file, range="limited") + + def test_encoder_range_invalid(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + with pytest.raises(ValueError): + im.save(test_file, range="foo") + + @skip_unless_avif_encoder("aom") + @skip_unless_feature("avif") + def test_encoder_codec_param(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + im.save(test_file, codec="aom") + + def test_encoder_codec_invalid(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + with pytest.raises(ValueError): + im.save(test_file, codec="foo") + + @skip_unless_avif_decoder("dav1d") + @skip_unless_feature("avif") + def test_encoder_codec_cannot_encode(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + with pytest.raises(ValueError): + im.save(test_file, codec="dav1d") + + @skip_unless_avif_encoder("aom") + @skip_unless_feature("avif") + def test_encoder_advanced_codec_options(self): + 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={ + "aq-mode": "1", + "enable-chroma-deltaq": "1", + }, + ) + assert ctrl_buf.getvalue() != test_buf.getvalue() + + @skip_unless_avif_encoder("aom") + @skip_unless_feature("avif") + @pytest.mark.parametrize("val", [{"foo": "bar"}, 1234]) + def test_encoder_advanced_codec_options_invalid(self, tmp_path, val): + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(tmp_path / "temp.avif") + with pytest.raises(ValueError): + im.save(test_file, codec="aom", advanced=val) + + @skip_unless_avif_decoder("aom") + @skip_unless_feature("avif") + def test_decoder_codec_param(self): + AvifImagePlugin.DECODE_CODEC_CHOICE = "aom" + try: + with Image.open(TEST_AVIF_FILE) as im: + assert im.size == (128, 128) + finally: + AvifImagePlugin.DECODE_CODEC_CHOICE = "auto" + + @skip_unless_avif_encoder("rav1e") + @skip_unless_feature("avif") + def test_decoder_codec_cannot_decode(self, tmp_path): + AvifImagePlugin.DECODE_CODEC_CHOICE = "rav1e" + try: + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + finally: + AvifImagePlugin.DECODE_CODEC_CHOICE = "auto" + + def test_decoder_codec_invalid(self): + AvifImagePlugin.DECODE_CODEC_CHOICE = "foo" + try: + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + finally: + AvifImagePlugin.DECODE_CODEC_CHOICE = "auto" + + @skip_unless_avif_encoder("aom") + @skip_unless_feature("avif") + def test_encoder_codec_available(self): + assert _avif.encoder_codec_available("aom") is True + + def test_encoder_codec_available_bad_params(self): + with pytest.raises(TypeError): + _avif.encoder_codec_available() + + @skip_unless_avif_decoder("dav1d") + @skip_unless_feature("avif") + def test_encoder_codec_available_cannot_decode(self): + assert _avif.encoder_codec_available("dav1d") is False + + def test_encoder_codec_available_invalid(self): + assert _avif.encoder_codec_available("foo") is False + + def test_encoder_quality_valueerror(self, tmp_path): + with Image.open("Tests/images/avif/hopper.avif") as im: + test_file = str(tmp_path / "temp.avif") + with pytest.raises(ValueError): + im.save(test_file, quality="invalid") + + @skip_unless_avif_decoder("aom") + @skip_unless_feature("avif") + def test_decoder_codec_available(self): + assert _avif.decoder_codec_available("aom") is True + + def test_decoder_codec_available_bad_params(self): + with pytest.raises(TypeError): + _avif.decoder_codec_available() + + @skip_unless_avif_encoder("rav1e") + @skip_unless_feature("avif") + def test_decoder_codec_available_cannot_decode(self): + assert _avif.decoder_codec_available("rav1e") is False + + def test_decoder_codec_available_invalid(self): + assert _avif.decoder_codec_available("foo") is False + + @pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"]) + def test_decoder_upsampling(self, upsampling): + AvifImagePlugin.CHROMA_UPSAMPLING = upsampling + try: + with Image.open(TEST_AVIF_FILE): + pass + finally: + AvifImagePlugin.CHROMA_UPSAMPLING = "auto" + + def test_decoder_upsampling_invalid(self): + AvifImagePlugin.CHROMA_UPSAMPLING = "foo" + try: + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + finally: + AvifImagePlugin.CHROMA_UPSAMPLING = "auto" + + def test_p_mode_transparency(self): + im = Image.new("P", size=(64, 64)) + draw = ImageDraw.Draw(im) + draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) + draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) + + buf_png = BytesIO() + im.save(buf_png, format="PNG", transparency=0) + im_png = Image.open(buf_png) + buf_out = BytesIO() + im_png.save(buf_out, format="AVIF", quality=100) + + assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1) + + def test_decoder_strict_flags(self): + # This would fail if full avif strictFlags were enabled + with Image.open("Tests/images/avif/chimera-missing-pixi.avif") as im: + assert im.size == (480, 270) + + @skip_unless_avif_encoder("aom") + def test_aom_optimizations(self): + im = hopper("RGB") + buf = BytesIO() + im.save(buf, format="AVIF", codec="aom", speed=1) + + @skip_unless_avif_encoder("svt") + def test_svt_optimizations(self): + im = hopper("RGB") + buf = BytesIO() + im.save(buf, format="AVIF", codec="svt", speed=1) + + +@skip_unless_feature("avif") +class TestAvifAnimation: + @contextmanager + def star_frames(self): + with Image.open("Tests/images/avif/star.png") as f1: + with Image.open("Tests/images/avif/star90.png") as f2: + with Image.open("Tests/images/avif/star180.png") as f3: + with Image.open("Tests/images/avif/star270.png") as f4: + yield [f1, f2, f3, f4] + + def test_n_frames(self): + """ + Ensure that AVIF format sets n_frames and is_animated attributes + correctly. + """ + + with Image.open("Tests/images/avif/hopper.avif") 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_L(self, tmp_path): + """ + Convert an animated GIF to animated AVIF, then compare the frame + count, and first and last frames to ensure they're visually similar. + """ + + with Image.open("Tests/images/avif/star.gif") as orig: + assert orig.n_frames > 1 + + temp_file = str(tmp_path / "temp.avif") + orig.save(temp_file, save_all=True) + with Image.open(temp_file) as im: + assert im.n_frames == orig.n_frames + + # Compare first and second-to-last frames to the original animated GIF + orig.load() + im.load() + assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) + orig.seek(orig.n_frames - 2) + im.seek(im.n_frames - 2) + orig.load() + im.load() + assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) + + def test_write_animation_RGB(self, tmp_path): + """ + Write an animated AVIF from RGB frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file): + with Image.open(temp_file) as im: + assert im.n_frames == 4 + + # Compare first frame to original + im.load() + assert_image_similar(im, frame1.convert("RGBA"), 25.0) + + # Compare second frame to original + im.seek(1) + im.load() + assert_image_similar(im, frame2.convert("RGBA"), 25.0) + + with self.star_frames() as frames: + frame1 = frames[0] + frame2 = frames[1] + temp_file1 = str(tmp_path / "temp.avif") + frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) + check(temp_file1) + + # Tests appending using a generator + def imGenerator(ims): + yield from ims + + temp_file2 = str(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): + temp_file = str(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], duration=100) + + def test_heif_raises_unidentified_image_error(self): + with pytest.raises(UnidentifiedImageError): + with Image.open("Tests/images/avif/rgba10.heif"): + pass + + @pytest.mark.parametrize("alpha_premultipled", [False, True]) + def test_alpha_premultiplied_true(self, alpha_premultipled): + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + im_buf = BytesIO() + im.save(im_buf, "AVIF", alpha_premultiplied=alpha_premultipled) + im_bytes = im_buf.getvalue() + assert has_alpha_premultiplied(im_bytes) is alpha_premultipled + + def test_timestamp_and_duration(self, tmp_path): + """ + Try passing a list of durations, and make sure the encoded + timestamps and durations are correct. + """ + + durations = [1, 10, 20, 30, 40] + temp_file = str(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 + ts = 0 + for frame in range(im.n_frames): + im.seek(frame) + im.load() + assert im.info["duration"] == durations[frame] + assert im.info["timestamp"] == ts + ts += durations[frame] + + def test_seeking(self, tmp_path): + """ + Create an animated AVIF file, and then try seeking through frames in + reverse-order, verifying the timestamps and durations are correct. + """ + + dur = 33 + temp_file = str(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=dur, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Traverse frames in reverse, checking timestamps and durations + ts = dur * (im.n_frames - 1) + for frame in reversed(range(im.n_frames)): + im.seek(frame) + im.load() + assert im.info["duration"] == dur + assert im.info["timestamp"] == ts + ts -= dur + + def test_seek_errors(self): + 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): + with open(TEST_AVIF_FILE, "rb") as f: + im_data = f.read() + + def core(): + 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 00000000000..09646c4ad78 --- /dev/null +++ b/depends/install_libavif.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -eo pipefail + +version=1.1.1 + +./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + +pushd libavif-$version + +if uname -s | grep -q Darwin; then + PREFIX=$(brew --prefix) +else + PREFIX=/usr +fi + +PKGCONFIG=${PKGCONFIG:-pkg-config} + +LIBAVIF_CMAKE_FLAGS=() +HAS_DECODER=0 +HAS_ENCODER=0 + +if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=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 $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) + HAS_ENCODER=1 + HAS_DECODER=1 +fi + +if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) +fi + +cmake -G Ninja -S . -B build \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DAVIF_LIBYUV=LOCAL \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_MACOSX_RPATH=OFF \ + "${LIBAVIF_CMAKE_FLAGS[@]}" + +sudo ninja -C build install + +popd diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8183473e4f5..af8cd8818f7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -235,7 +235,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 currently supported for GIF, PDF, PNG, TIFF, WebP, and AVIF. 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. @@ -1311,6 +1311,79 @@ XBM Pillow reads and writes X bitmap files (mode ``1``). +AVIF +^^^^ + +Pillow reads and writes AVIF files, including AVIF sequence images. Currently, +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, 1-100, Defaults to 90. 0 gives the smallest size and poorest + quality, 100 the largest and best quality. The value of this setting + controls the ``qmin`` and ``qmax`` encoder options. + +**qmin** / **qmax** + Integer, 0-63. The quality of images created by an AVIF encoder are + controlled by minimum and maximum quantizer values. The higher these + values are, the worse the 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 8. + +**range** + YUV range, either "full" or "limited." Defaults to "full" + +**codec** + AV1 codec to use for encoding. Possible values are "aom", "rav1e", and + "svt", depending on what codecs were compiled with libavif. 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. + +**alpha_premultiplied** + Encode the image with premultiplied alpha, defaults ``False`` + +**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. + Read-only formats ----------------- diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 4b517582786..f2d23bef232 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -89,6 +89,16 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. + +* **libavif** provides support for the AVIF format. + + * Pillow requires libavif version **0.8.0** or greater, which is when + AVIF image sequence support was added. + * 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 +127,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 \ @@ -156,6 +172,12 @@ Many of Pillow's features require external libraries: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + To install libavif on macOS use Homebrew to install its build dependencies:: + + brew install aom dav1d rav1e + + Then see ``depends/install_libavif.sh`` to install libavif. + .. tab:: Windows We recommend you use prebuilt wheels from PyPI. @@ -193,7 +215,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 https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with MSYS2. To workaround this, before installing Pillow you must run:: @@ -210,9 +233,10 @@ 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. + See ``depends/install_raqm_cmake.sh`` to install libraqm and + ``depends/install_libavif.sh`` to install libavif. .. tab:: Android diff --git a/docs/reference/features.rst b/docs/reference/features.rst index fcff9673567..d1e4f7470eb 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 454b94d8ce7..c789f575700 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/setup.py b/setup.py index def3417845d..ddd534cf0eb 100644 --- a/setup.py +++ b/setup.py @@ -305,6 +305,7 @@ class ext_feature: "jpeg2000", "imagequant", "xcb", + "avif", ] required = {"jpeg", "zlib"} @@ -839,6 +840,12 @@ def build_extensions(self) -> None: 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"): @@ -927,6 +934,14 @@ def build_extensions(self) -> None: 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) @@ -969,6 +984,7 @@ def summary_report(self, feature: ext_feature) -> None: (feature.get("lcms"), "LITTLECMS2"), (feature.get("webp"), "WEBP"), (feature.get("xcb"), "XCB (X protocol)"), + (feature.get("avif"), "LIBAVIF"), ] all = 1 @@ -1011,6 +1027,7 @@ def debug_build() -> bool: 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 00000000000..06edf230620 --- /dev/null +++ b/src/PIL/AvifImagePlugin.py @@ -0,0 +1,282 @@ +from __future__ import annotations + +from io import BytesIO + +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" +CHROMA_UPSAMPLING = "auto" +DEFAULT_MAX_THREADS = 0 + +_VALID_AVIF_MODES = {"RGB", "RGBA"} + + +def _accept(prefix): + if prefix[4:8] != b"ftyp": + return + coding_brands = (b"avif", b"avis") + container_brands = (b"mif1", b"msf1") + major_brand = prefix[8:12] + if major_brand in coding_brands: + if not SUPPORTED: + return ( + "image file could not be identified because AVIF " + "support not installed" + ) + return True + if major_brand in container_brands: + # We accept files with AVIF container brands; we can't yet know if + # the ftyp box has the correct compatible brands, but if it doesn't + # then the plugin will raise a SyntaxError which Pillow will catch + # before moving on to the next plugin that accepts the file. + # + # Also, because this file might not actually be an AVIF file, we + # don't raise an error if AVIF support isn't properly compiled. + return True + + +class AvifImageFile(ImageFile.ImageFile): + format = "AVIF" + format_description = "AVIF image" + __loaded = -1 + __frame = 0 + + def load_seek(self, pos: int) -> None: + pass + + def _open(self): + if not SUPPORTED: + msg = ( + "image file could not be identified because AVIF " + "support not installed" + ) + raise SyntaxError(msg) + + self._decoder = _avif.AvifDecoder( + self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING, DEFAULT_MAX_THREADS + ) + + # Get info from decoder + width, height, n_frames, mode, icc, exif, xmp = self._decoder.get_info() + self._size = width, height + self.n_frames = n_frames + self.is_animated = self.n_frames > 1 + self._mode = self.rawmode = mode + self.tile = [] + + if icc: + self.info["icc_profile"] = icc + if exif: + self.info["exif"] = exif + if xmp: + self.info["xmp"] = xmp + + def seek(self, frame): + if not self._seek_check(frame): + return + + self.__frame = frame + + def load(self): + if self.__loaded != self.__frame: + # We need to load the image data for this frame + data, timescale, tsp_in_ts, dur_in_ts = self._decoder.get_frame( + self.__frame + ) + timestamp = round(1000 * (tsp_in_ts / timescale)) + duration = round(1000 * (dur_in_ts / timescale)) + self.info["timestamp"] = timestamp + self.info["duration"] = duration + self.__loaded = self.__frame + + # Set tile + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] + + return super().load() + + def tell(self): + return self.__frame + + +def _save_all(im, fp, filename): + _save(im, fp, filename, save_all=True) + + +def _save(im, fp, filename, save_all=False): + 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) + + is_single_frame = total == 1 + + qmin = info.get("qmin", -1) + qmax = info.get("qmax", -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", DEFAULT_MAX_THREADS) + codec = info.get("codec", "auto") + 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 = info.get("exif", im.info.get("exif")) + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + + exif_orientation = 0 + if exif: + exif_data = Image.Exif() + try: + exif_data.load(exif) + except SyntaxError: + pass + else: + orientation_tag = next( + k for k, v in ExifTags.TAGS.items() if v == "Orientation" + ) + exif_orientation = exif_data.get(orientation_tag) or 0 + + xmp = info.get("xmp", im.info.get("xmp") or im.info.get("XML:com.adobe.xmp")) + + if isinstance(xmp, str): + xmp = xmp.encode("utf-8") + + advanced = info.get("advanced") + if isinstance(advanced, dict): + advanced = tuple([k, v] for (k, v) in advanced.items()) + if advanced is not None: + try: + advanced = tuple(advanced) + except TypeError: + invalid = True + else: + invalid = all(isinstance(v, tuple) and 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) + advanced = tuple( + [(str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced] + ) + + # Setup the AVIF encoder + enc = _avif.AvifEncoder( + im.size[0], + im.size[1], + subsampling, + qmin, + qmax, + 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_dur = 0 + cur_idx = im.tell() + try: + for ims in [im] + append_images: + # Get # of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + ims.load() + + # Make sure image mode is supported + frame = ims + rawmode = ims.mode + if ims.mode not in _VALID_AVIF_MODES: + alpha = ( + "A" in ims.mode + or "a" in ims.mode + or (ims.mode == "P" and "A" in ims.im.getpalettemode()) + or ( + ims.mode == "P" + and ims.info.get("transparency", None) is not None + ) + ) + rawmode = "RGBA" if alpha else "RGB" + frame = ims.convert(rawmode) + + # Update frame duration + if isinstance(duration, (list, tuple)): + frame_dur = duration[frame_idx] + else: + frame_dur = duration + + # Append the frame to the animation encoder + enc.add( + frame.tobytes("raw", rawmode), + frame_dur, + frame.size[0], + frame.size[1], + 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 44270392c4d..a49a38739f8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1548,7 +1548,9 @@ def getexif(self) -> Exif: # XMP tags if ExifTags.Base.Orientation not in self._exif: - xmp_tags = self.info.get("XML:com.adobe.xmp") + xmp_tags = self.info.get("XML:com.adobe.xmp") or self.info.get("xmp") + if isinstance(xmp_tags, bytes): + 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 09546fe6333..6e4c23f897f 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -25,6 +25,7 @@ _plugins = [ + "AvifImagePlugin", "BlpImagePlugin", "BmpImagePlugin", "BufrStubImagePlugin", diff --git a/src/PIL/_avif.pyi b/src/PIL/_avif.pyi new file mode 100644 index 00000000000..e27843e5338 --- /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 75d59e01c40..3429d3f7e46 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -17,6 +17,7 @@ "freetype2": ("PIL._imagingft", "freetype2_version"), "littlecms2": ("PIL._imagingcms", "littlecms_version"), "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), } @@ -287,6 +288,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), ("jpg", "JPEG"), + ("avif", "AVIF"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), ("libtiff", "LIBTIFF"), diff --git a/src/_avif.c b/src/_avif.c new file mode 100644 index 00000000000..5365d38b660 --- /dev/null +++ b/src/_avif.c @@ -0,0 +1,1084 @@ +#define PY_SSIZE_T_CLEAN + +#include +#include "avif/avif.h" + +#if AVIF_VERSION < 80300 +#define AVIF_CHROMA_UPSAMPLING_AUTOMATIC AVIF_CHROMA_UPSAMPLING_BILINEAR +#define AVIF_CHROMA_UPSAMPLING_BEST_QUALITY AVIF_CHROMA_UPSAMPLING_BILINEAR +#define AVIF_CHROMA_UPSAMPLING_FASTEST AVIF_CHROMA_UPSAMPLING_NEAREST +#endif + +typedef struct { + avifPixelFormat subsampling; + int qmin; + int qmax; + int quality; + int speed; + avifCodecChoice codec; + avifRange range; + avifBool alpha_premultiplied; + int tile_rows_log2; + int tile_cols_log2; + avifBool autotiling; +} avifEncOptions; + +// Encoder type +typedef struct { + PyObject_HEAD avifEncoder *encoder; + avifImage *image; + PyObject *icc_bytes; + PyObject *exif_bytes; + PyObject *xmp_bytes; + int frame_index; +} AvifEncoderObject; + +static PyTypeObject AvifEncoder_Type; + +// Decoder type +typedef struct { + PyObject_HEAD avifDecoder *decoder; + PyObject *data; + char *mode; +} AvifDecoderObject; + +static PyTypeObject AvifDecoder_Type; + +static int default_max_threads = 0; + +static void +init_max_threads(void) { + PyObject *os = NULL; + PyObject *n = NULL; + long num_cpus; + + os = PyImport_ImportModule("os"); + if (os == NULL) { + goto error; + } + + if (PyObject_HasAttrString(os, "sched_getaffinity")) { + n = PyObject_CallMethod(os, "sched_getaffinity", "i", 0); + if (n == NULL) { + goto error; + } + num_cpus = PySet_Size(n); + } else { + n = PyObject_CallMethod(os, "cpu_count", NULL); + if (n == NULL) { + goto error; + } + num_cpus = PyLong_AsLong(n); + } + + if (num_cpus < 1) { + goto error; + } + + default_max_threads = (int)num_cpus; + +done: + Py_XDECREF(os); + Py_XDECREF(n); + return; + +error: + if (PyErr_Occurred()) { + PyErr_Clear(); + } + PyErr_WarnEx( + PyExc_RuntimeWarning, "could not get cpu count: using max_threads=1", 1 + ); + goto done; +} + +static int +normalize_quantize_value(int qvalue) { + if (qvalue < AVIF_QUANTIZER_BEST_QUALITY) { + return AVIF_QUANTIZER_BEST_QUALITY; + } else if (qvalue > AVIF_QUANTIZER_WORST_QUALITY) { + return AVIF_QUANTIZER_WORST_QUALITY; + } else { + return qvalue; + } +} + +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 void +exif_orientation_to_irot_imir(avifImage *image, int orientation) { + const avifTransformFlags otherFlags = + image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR); + + // + // 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 1: // The 0th row is at the visual top of the image, and the 0th column is + // the visual left-hand side. + image->transformFlags = otherFlags; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + 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 = otherFlags | AVIF_TRANSFORM_IMIR; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 1; +#else + image->imir.mode = 1; +#endif + return; + 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 = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 2; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + 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 = otherFlags | AVIF_TRANSFORM_IMIR; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 5: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags = + otherFlags | 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 +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 6: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 3; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + case 7: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags = + otherFlags | 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 +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 8: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 1; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + default: // reserved + break; + } + + // The orientation tag is not mandatory (only recommended) according to JEITA + // CP-3451C section 4.6.8.A. The default value is 1 if the orientation tag is + // missing, meaning: + // The 0th row is at the visual top of the image, and the 0th column is the visual + // left-hand side. + image->transformFlags = otherFlags; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif +} + +static int +_codec_available(const char *name, uint32_t 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); +} + +static void +_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { + Py_ssize_t i, size; + PyObject *keyval, *py_key, *py_val; + char *key, *val; + if (!PyTuple_Check(opts)) { + return; + } + 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) { + return; + } + py_key = PyTuple_GetItem(keyval, 0); + py_val = PyTuple_GetItem(keyval, 1); + if (!PyBytes_Check(py_key) || !PyBytes_Check(py_val)) { + return; + } + key = PyBytes_AsString(py_key); + val = PyBytes_AsString(py_val); + avifEncoderSetCodecSpecificOption(encoder, key, val); + } +} + +// Encoder functions +PyObject * +AvifEncoderNew(PyObject *self_, PyObject *args) { + unsigned int width, height; + avifEncOptions enc_options; + AvifEncoderObject *self = NULL; + avifEncoder *encoder = NULL; + + char *subsampling = "4:2:0"; + int qmin = AVIF_QUANTIZER_BEST_QUALITY; // =0 + int qmax = 10; // "High Quality", but not lossless + int quality = 75; + int speed = 8; + int exif_orientation = 0; + int max_threads = default_max_threads; + PyObject *icc_bytes; + PyObject *exif_bytes; + PyObject *xmp_bytes; + PyObject *alpha_premultiplied = NULL; + PyObject *autotiling = NULL; + int tile_rows_log2 = 0; + int tile_cols_log2 = 0; + + char *codec = "auto"; + char *range = "full"; + + PyObject *advanced; + + if (!PyArg_ParseTuple( + args, + "IIsiiiiissiiOOSSiSO", + &width, + &height, + &subsampling, + &qmin, + &qmax, + &quality, + &speed, + &max_threads, + &codec, + &range, + &tile_rows_log2, + &tile_cols_log2, + &alpha_premultiplied, + &autotiling, + &icc_bytes, + &exif_bytes, + &exif_orientation, + &xmp_bytes, + &advanced + )) { + return NULL; + } + + if (strcmp(subsampling, "4:0:0") == 0) { + enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV400; + } else if (strcmp(subsampling, "4:2:0") == 0) { + enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV420; + } else if (strcmp(subsampling, "4:2:2") == 0) { + enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV422; + } else if (strcmp(subsampling, "4:4:4") == 0) { + enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV444; + } else { + PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); + return NULL; + } + + if (qmin == -1 || qmax == -1) { +#if AVIF_VERSION >= 1000000 + enc_options.qmin = -1; + enc_options.qmax = -1; +#else + enc_options.qmin = normalize_quantize_value(64 - quality); + enc_options.qmax = normalize_quantize_value(100 - quality); +#endif + } else { + enc_options.qmin = normalize_quantize_value(qmin); + enc_options.qmax = normalize_quantize_value(qmax); + } + enc_options.quality = quality; + + if (speed < AVIF_SPEED_SLOWEST) { + speed = AVIF_SPEED_SLOWEST; + } else if (speed > AVIF_SPEED_FASTEST) { + speed = AVIF_SPEED_FASTEST; + } + enc_options.speed = speed; + + if (strcmp(codec, "auto") == 0) { + enc_options.codec = AVIF_CODEC_CHOICE_AUTO; + } else { + enc_options.codec = avifCodecChoiceFromName(codec); + if (enc_options.codec == AVIF_CODEC_CHOICE_AUTO) { + PyErr_Format(PyExc_ValueError, "Invalid codec: %s", codec); + return NULL; + } else { + const char *codec_name = + avifCodecName(enc_options.codec, AVIF_CODEC_FLAG_CAN_ENCODE); + if (codec_name == NULL) { + PyErr_Format(PyExc_ValueError, "AV1 Codec cannot encode: %s", codec); + return NULL; + } + } + } + + if (strcmp(range, "full") == 0) { + enc_options.range = AVIF_RANGE_FULL; + } else if (strcmp(range, "limited") == 0) { + enc_options.range = AVIF_RANGE_LIMITED; + } else { + PyErr_SetString(PyExc_ValueError, "Invalid range"); + return NULL; + } + + // Validate canvas dimensions + if (width <= 0 || height <= 0) { + PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); + return NULL; + } + + enc_options.tile_rows_log2 = normalize_tiles_log2(tile_rows_log2); + enc_options.tile_cols_log2 = normalize_tiles_log2(tile_cols_log2); + + if (alpha_premultiplied == Py_True) { + enc_options.alpha_premultiplied = AVIF_TRUE; + } else { + enc_options.alpha_premultiplied = AVIF_FALSE; + } + + enc_options.autotiling = (autotiling == Py_True) ? AVIF_TRUE : AVIF_FALSE; + + // Create a new animation encoder and picture frame + self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); + if (self) { + self->icc_bytes = NULL; + self->exif_bytes = NULL; + self->xmp_bytes = NULL; + + encoder = avifEncoderCreate(); + + if (max_threads == 0) { + if (default_max_threads == 0) { + init_max_threads(); + } + max_threads = default_max_threads; + } + + 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; +#if AVIF_VERSION >= 1000000 + if (enc_options.qmin != -1 && enc_options.qmax != -1) { + encoder->minQuantizer = enc_options.qmin; + encoder->maxQuantizer = enc_options.qmax; + } else { + encoder->quality = enc_options.quality; + } +#else + encoder->minQuantizer = enc_options.qmin; + encoder->maxQuantizer = enc_options.qmax; +#endif + encoder->codecChoice = enc_options.codec; + encoder->speed = enc_options.speed; + encoder->timescale = (uint64_t)1000; + encoder->tileRowsLog2 = enc_options.tile_rows_log2; + encoder->tileColsLog2 = enc_options.tile_cols_log2; + +#if AVIF_VERSION >= 110000 + encoder->autoTiling = enc_options.autotiling; +#endif + +#if AVIF_VERSION >= 80200 + _add_codec_specific_options(encoder, advanced); +#endif + + self->encoder = encoder; + + avifImage *image = avifImageCreateEmpty(); + // Set these in advance so any upcoming RGB -> YUV use the proper coefficients + image->yuvRange = enc_options.range; + image->yuvFormat = enc_options.subsampling; + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + image->width = width; + image->height = height; + image->depth = 8; +#if AVIF_VERSION >= 90000 + image->alphaPremultiplied = enc_options.alpha_premultiplied; +#endif + + if (PyBytes_GET_SIZE(icc_bytes)) { + self->icc_bytes = icc_bytes; + Py_INCREF(icc_bytes); + avifImageSetProfileICC( + image, + (uint8_t *)PyBytes_AS_STRING(icc_bytes), + PyBytes_GET_SIZE(icc_bytes) + ); + } else { + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + } + + if (PyBytes_GET_SIZE(exif_bytes)) { + self->exif_bytes = exif_bytes; + Py_INCREF(exif_bytes); + avifImageSetMetadataExif( + image, + (uint8_t *)PyBytes_AS_STRING(exif_bytes), + PyBytes_GET_SIZE(exif_bytes) + ); + } + if (PyBytes_GET_SIZE(xmp_bytes)) { + self->xmp_bytes = xmp_bytes; + Py_INCREF(xmp_bytes); + avifImageSetMetadataXMP( + image, + (uint8_t *)PyBytes_AS_STRING(xmp_bytes), + PyBytes_GET_SIZE(xmp_bytes) + ); + } + exif_orientation_to_irot_imir(image, exif_orientation); + + self->image = image; + self->frame_index = -1; + + return (PyObject *)self; + } + PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); + return NULL; +} + +PyObject * +_encoder_dealloc(AvifEncoderObject *self) { + if (self->encoder) { + avifEncoderDestroy(self->encoder); + } + if (self->image) { + avifImageDestroy(self->image); + } + Py_XDECREF(self->icc_bytes); + Py_XDECREF(self->exif_bytes); + Py_XDECREF(self->xmp_bytes); + 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; + PyObject *is_single_frame = NULL; + PyObject *ret = Py_None; + + int is_first_frame; + int channels; + avifRGBImage rgb; + avifResult result; + + avifEncoder *encoder = self->encoder; + avifImage *image = self->image; + avifImage *frame = NULL; + + if (!PyArg_ParseTuple( + args, + "z#IIIsO", + (char **)&rgb_bytes, + &size, + &duration, + &width, + &height, + &mode, + &is_single_frame + )) { + return NULL; + } + + is_first_frame = (self->frame_index == -1); + + 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 (is_first_frame) { + // If we don't have an image populated with yuv planes, this is the first frame + frame = image; + } else { + frame = avifImageCreateEmpty(); + + frame->colorPrimaries = image->colorPrimaries; + frame->transferCharacteristics = image->transferCharacteristics; + frame->matrixCoefficients = image->matrixCoefficients; + frame->yuvRange = image->yuvRange; + frame->yuvFormat = image->yuvFormat; + frame->depth = image->depth; +#if AVIF_VERSION >= 90000 + frame->alphaPremultiplied = image->alphaPremultiplied; +#endif + } + + frame->width = width; + frame->height = height; + + memset(&rgb, 0, sizeof(avifRGBImage)); + + avifRGBImageSetDefaults(&rgb, frame); + rgb.depth = 8; + + if (strcmp(mode, "RGBA") == 0) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + channels = 4; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + channels = 3; + } + + avifRGBImageAllocatePixels(&rgb); + + if (rgb.rowBytes * rgb.height != size) { + PyErr_Format( + PyExc_RuntimeError, + "rgb data is incorrect size: %u * %u (%u) != %u", + rgb.rowBytes, + rgb.height, + rgb.rowBytes * rgb.height, + size + ); + ret = NULL; + 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) + ); + ret = NULL; + goto end; + } + + uint32_t addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE; + if (PyObject_IsTrue(is_single_frame)) { + addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE; + } + + 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) + ); + ret = NULL; + goto end; + } + +end: + avifRGBImageFreePixels(&rgb); + if (!is_first_frame) { + avifImageDestroy(frame); + } + + if (ret == Py_None) { + self->frame_index++; + Py_RETURN_NONE; + } else { + return ret; + } +} + +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) { + PyObject *avif_bytes; + AvifDecoderObject *self = NULL; + + char *upsampling_str; + char *codec_str; + avifCodecChoice codec; + avifChromaUpsampling upsampling; + int max_threads = 0; + + avifResult result; + + if (!PyArg_ParseTuple( + args, "Sssi", &avif_bytes, &codec_str, &upsampling_str, &max_threads + )) { + return NULL; + } + + if (!strcmp(upsampling_str, "auto")) { + upsampling = AVIF_CHROMA_UPSAMPLING_AUTOMATIC; + } else if (!strcmp(upsampling_str, "fastest")) { + upsampling = AVIF_CHROMA_UPSAMPLING_FASTEST; + } else if (!strcmp(upsampling_str, "best")) { + upsampling = AVIF_CHROMA_UPSAMPLING_BEST_QUALITY; + } else if (!strcmp(upsampling_str, "nearest")) { + upsampling = AVIF_CHROMA_UPSAMPLING_NEAREST; + } else if (!strcmp(upsampling_str, "bilinear")) { + upsampling = AVIF_CHROMA_UPSAMPLING_BILINEAR; + } else { + PyErr_Format(PyExc_ValueError, "Invalid upsampling option: %s", upsampling_str); + return NULL; + } + + if (strcmp(codec_str, "auto") == 0) { + codec = AVIF_CODEC_CHOICE_AUTO; + } else { + codec = avifCodecChoiceFromName(codec_str); + if (codec == AVIF_CODEC_CHOICE_AUTO) { + PyErr_Format(PyExc_ValueError, "Invalid codec: %s", codec_str); + return NULL; + } else { + const char *codec_name = avifCodecName(codec, AVIF_CODEC_FLAG_CAN_DECODE); + if (codec_name == NULL) { + PyErr_Format( + PyExc_ValueError, "AV1 Codec cannot decode: %s", codec_str + ); + return NULL; + } + } + } + + self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); + return NULL; + } + self->decoder = NULL; + + Py_INCREF(avif_bytes); + self->data = avif_bytes; + + self->decoder = avifDecoderCreate(); +#if AVIF_VERSION >= 80400 + if (max_threads == 0) { + if (default_max_threads == 0) { + init_max_threads(); + } + max_threads = default_max_threads; + } + self->decoder->maxThreads = max_threads; +#endif +#if AVIF_VERSION >= 90200 + // Turn off libavif's 'clap' (clean aperture) property validation. + self->decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; + // Allow the PixelInformationProperty ('pixi') to be missing in AV1 image + // items. libheif v1.11.0 and older does not add the 'pixi' item property to + // AV1 image items. + self->decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; +#endif + self->decoder->codecChoice = codec; + + avifDecoderSetIOMemory( + self->decoder, + (uint8_t *)PyBytes_AS_STRING(self->data), + PyBytes_GET_SIZE(self->data) + ); + + result = avifDecoderParse(self->decoder); + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode image: %s", + avifResultToString(result) + ); + avifDecoderDestroy(self->decoder); + self->decoder = NULL; + Py_DECREF(self); + return NULL; + } + + if (self->decoder->alphaPresent) { + self->mode = "RGBA"; + } else { + self->mode = "RGB"; + } + + return (PyObject *)self; +} + +PyObject * +_decoder_dealloc(AvifDecoderObject *self) { + if (self->decoder) { + avifDecoderDestroy(self->decoder); + } + Py_XDECREF(self->data); + 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( + "IIIsSSS", + image->width, + image->height, + decoder->imageCount, + self->mode, + NULL == icc ? Py_None : icc, + NULL == exif ? Py_None : exif, + 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; + uint32_t row_bytes; + + 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", + decoder->imageIndex + 1, + avifResultToString(result) + ); + return NULL; + } + + image = decoder->image; + + memset(&rgb, 0, sizeof(rgb)); + avifRGBImageSetDefaults(&rgb, image); + + rgb.depth = 8; + + if (decoder->alphaPresent) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + rgb.ignoreAlpha = AVIF_TRUE; + } + + row_bytes = rgb.width * avifRGBImagePixelSize(&rgb); + + if (rgb.height > PY_SSIZE_T_MAX / row_bytes) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); + return NULL; + } + + avifRGBImageAllocatePixels(&rgb); + + 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; + } + + 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 */ +}; + +// AvifDecoder type definition +static PyTypeObject AvifEncoder_Type = { + // clang-format off + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "AvifEncoder", + // clang-format on + .tp_basicsize = sizeof(AvifEncoderObject), + .tp_dealloc = (destructor)_encoder_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .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 = { + // clang-format off + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "AvifDecoder", + // clang-format on + .tp_basicsize = sizeof(AvifDecoderObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)_decoder_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _decoder_methods, +}; + +PyObject * +AvifCodecVersions() { + char codecVersions[256]; + avifCodecVersions(codecVersions); + return PyUnicode_FromString(codecVersions); +} + +/* -------------------------------------------------------------------- */ +/* Module Setup */ +/* -------------------------------------------------------------------- */ + +static PyMethodDef avifMethods[] = { + {"AvifDecoder", AvifDecoderNew, METH_VARARGS}, + {"AvifEncoder", AvifEncoderNew, METH_VARARGS}, + {"AvifCodecVersions", AvifCodecVersions, METH_NOARGS}, + {"decoder_codec_available", _decoder_codec_available, METH_VARARGS}, + {"encoder_codec_available", _encoder_codec_available, METH_VARARGS}, + {NULL, NULL} +}; + +static int +setup_module(PyObject *m) { + PyObject *d = PyModule_GetDict(m); + + PyObject *v = PyUnicode_FromString(avifVersion()); + if (PyDict_SetItemString(d, "libavif_version", v) < 0) { + Py_DECREF(v); + return -1; + } + Py_DECREF(v); + + v = Py_True; + Py_INCREF(v); + if (PyDict_SetItemString(d, "HAVE_AVIF", v) < 0) { + Py_DECREF(v); + return -1; + } + Py_DECREF(v); + + v = Py_BuildValue( + "(iii)", AVIF_VERSION_MAJOR, AVIF_VERSION_MINOR, AVIF_VERSION_PATCH + ); + + if (PyDict_SetItemString(d, "VERSION", v) < 0) { + Py_DECREF(v); + return -1; + } + Py_DECREF(v); + + if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { + return -1; + } + 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) { + return NULL; + } + + return m; +} diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt new file mode 100644 index 00000000000..875b138ecf6 --- /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 00000000000..11bcb969bec --- /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: apps/shared/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 00000000000..c911747a6b5 --- /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/PATENTS.txt b/wheels/dependency_licenses/PATENTS.txt new file mode 100644 index 00000000000..bcfb598238c --- /dev/null +++ b/wheels/dependency_licenses/PATENTS.txt @@ -0,0 +1,107 @@ +Alliance for Open Media Patent License 1.0 + +1. License Terms. + +1.1. Patent License. Subject to the terms and conditions of this License, each + Licensor, on behalf of itself and successors in interest and assigns, + grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, + no-charge, royalty-free, irrevocable (except as expressly stated in this + License) patent license to its Necessary Claims to make, use, sell, offer + for sale, import or distribute any Implementation. + +1.2. Conditions. + +1.2.1. Availability. As a condition to the grant of rights to Licensee to make, + sell, offer for sale, import or distribute an Implementation under + Section 1.1, Licensee must make its Necessary Claims available under + this License, and must reproduce this License with any Implementation + as follows: + + a. For distribution in source code, by including this License in the + root directory of the source code with its Implementation. + + b. For distribution in any other form (including binary, object form, + and/or hardware description code (e.g., HDL, RTL, Gate Level Netlist, + GDSII, etc.)), by including this License in the documentation, legal + notices, and/or other written materials provided with the + Implementation. + +1.2.2. Additional Conditions. This license is directly from Licensor to + Licensee. Licensee acknowledges as a condition of benefiting from it + that no rights from Licensor are received from suppliers, distributors, + or otherwise in connection with this License. + +1.3. Defensive Termination. If any Licensee, its Affiliates, or its agents + initiates patent litigation or files, maintains, or voluntarily + participates in a lawsuit against another entity or any person asserting + that any Implementation infringes Necessary Claims, any patent licenses + granted under this License directly to the Licensee are immediately + terminated as of the date of the initiation of action unless 1) that suit + was in response to a corresponding suit regarding an Implementation first + brought against an initiating entity, or 2) that suit was brought to + enforce the terms of this License (including intervention in a third-party + action by a Licensee). + +1.4. Disclaimers. The Reference Implementation and Specification are provided + "AS IS" and without warranty. The entire risk as to implementing or + otherwise using the Reference Implementation or Specification is assumed + by the implementer and user. Licensor expressly disclaims any warranties + (express, implied, or otherwise), including implied warranties of + merchantability, non-infringement, fitness for a particular purpose, or + title, related to the material. IN NO EVENT WILL LICENSOR BE LIABLE TO + ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, + INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF + ACTION OF ANY KIND WITH RESPECT TO THIS LICENSE, WHETHER BASED ON BREACH + OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR + NOT THE OTHER PARTRY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +2. Definitions. + +2.1. Affiliate. “Affiliate” means an entity that directly or indirectly + Controls, is Controlled by, or is under common Control of that party. + +2.2. Control. “Control” means direct or indirect control of more than 50% of + the voting power to elect directors of that corporation, or for any other + entity, the power to direct management of such entity. + +2.3. Decoder. "Decoder" means any decoder that conforms fully with all + non-optional portions of the Specification. + +2.4. Encoder. "Encoder" means any encoder that produces a bitstream that can + be decoded by a Decoder only to the extent it produces such a bitstream. + +2.5. Final Deliverable. “Final Deliverable” means the final version of a + deliverable approved by the Alliance for Open Media as a Final + Deliverable. + +2.6. Implementation. "Implementation" means any implementation, including the + Reference Implementation, that is an Encoder and/or a Decoder. An + Implementation also includes components of an Implementation only to the + extent they are used as part of an Implementation. + +2.7. License. “License” means this license. + +2.8. Licensee. “Licensee” means any person or entity who exercises patent + rights granted under this License. + +2.9. Licensor. "Licensor" means (i) any Licensee that makes, sells, offers + for sale, imports or distributes any Implementation, or (ii) a person + or entity that has a licensing obligation to the Implementation as a + result of its membership and/or participation in the Alliance for Open + Media working group that developed the Specification. + +2.10. Necessary Claims. "Necessary Claims" means all claims of patents or + patent applications, (a) that currently or at any time in the future, + are owned or controlled by the Licensor, and (b) (i) would be an + Essential Claim as defined by the W3C Policy as of February 5, 2004 + (https://www.w3.org/Consortium/Patent-Policy-20040205/#def-essential) + as if the Specification was a W3C Recommendation; or (ii) are infringed + by the Reference Implementation. + +2.11. Reference Implementation. “Reference Implementation” means an Encoder + and/or Decoder released by the Alliance for Open Media as a Final + Deliverable. + +2.12. Specification. “Specification” means the specification designated by + the Alliance for Open Media as a Final Deliverable for which this + License was issued. diff --git a/wheels/dependency_licenses/RAV1E.txt b/wheels/dependency_licenses/RAV1E.txt new file mode 100644 index 00000000000..4c6c3029a96 --- /dev/null +++ b/wheels/dependency_licenses/RAV1E.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2017-2021, 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 00000000000..532a982b3ff --- /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/Findrav1e.cmake b/winbuild/Findrav1e.cmake new file mode 100644 index 00000000000..be1618bd4e7 --- /dev/null +++ b/winbuild/Findrav1e.cmake @@ -0,0 +1,10 @@ +file(TO_CMAKE_PATH "${AVIF_RAV1E_ROOT}" RAV1E_ROOT_PATH) +add_library(rav1e::rav1e STATIC IMPORTED GLOBAL) +set_target_properties( + rav1e::rav1e + PROPERTIES IMPORTED_LOCATION "${RAV1E_ROOT_PATH}/lib/rav1e.lib" + AVIF_LOCAL ON + INTERFACE_INCLUDE_DIRECTORIES "${RAV1E_ROOT_PATH}/inc/rav1e" + IMPORTED_SONAME rav1e) +target_link_libraries(rav1e::rav1e INTERFACE ntdll.lib userenv.lib ws2_32.lib + bcrypt.lib) diff --git a/winbuild/build.rst b/winbuild/build.rst index 96b8803b477..a0e6e601606 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -59,6 +59,7 @@ Run ``build_prepare.py`` to configure the build:: build architecture (default: same as host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant + --no-avif skip optional dependency libavif --no-fribidi, --no-raqm skip LGPL-licensed optional dependency FriBiDi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a21fbef9148..192dad38587 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,6 +121,9 @@ def cmd_msbuild( "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", + "MESON": "1.5.1", + "LIBAVIF": "1.1.1", + "RAV1E": "0.7.1", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -397,6 +400,57 @@ def cmd_msbuild( ], "bins": [r"*.dll"], }, + "rav1e": { + "url": ( + f"https://github.com/xiph/rav1e/releases/download/v{V['RAV1E']}/" + f"rav1e-{V['RAV1E']}-windows-msvc-generic.zip" + ), + "filename": f"rav1e-{V['RAV1E']}-windows-msvc-generic.zip", + "dir": "rav1e-windows-msvc-sdk", + "license": "LICENSE", + "build": [ + cmd_xcopy("include", "{inc_dir}"), + ], + "bins": [r"bin\*.dll"], + "libs": [r"lib\*.*"], + }, + "libavif": { + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", + "filename": f"libavif-{V['LIBAVIF']}.zip", + "dir": f"libavif-{V['LIBAVIF']}", + "license": "LICENSE", + "build": [ + cmd_mkdir("build.pillow"), + cmd_cd("build.pillow"), + " ".join( + [ + "{cmake}", + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", + "-DCMAKE_C_COMPILER=cl.exe", + "-DCMAKE_CXX_COMPILER=cl.exe", + "-DCMAKE_C_FLAGS=-nologo", + "-DCMAKE_CXX_FLAGS=-nologo", + "-DBUILD_SHARED_LIBS=OFF", + "-DAVIF_CODEC_AOM=LOCAL", + "-DAVIF_LIBYUV=LOCAL", + "-DAVIF_LIBSHARPYUV=LOCAL", + "-DAVIF_CODEC_RAV1E=SYSTEM", + "-DAVIF_RAV1E_ROOT={build_dir}", + "-DCMAKE_MODULE_PATH={winbuild_dir_cmake}", + "-DAVIF_CODEC_DAV1D=LOCAL", + "-DAVIF_CODEC_SVT=LOCAL", + '-G "Ninja"', + "..", + ] + ), + "ninja -v", + cmd_cd(".."), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": [r"build.pillow\avif.lib"], + }, } @@ -620,12 +674,15 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> None: lines = [r'call "{build_dir}\build_env.cmd"'] gha_groups = "GITHUB_ACTIONS" in os.environ + scripts = ["install_meson.cmd"] for dep_name in DEPS: print() if dep_name in disabled: print(f"Skipping disabled dependency {dep_name}") continue - script = build_dep(dep_name, prefs, verbose) + scripts.append(build_dep(dep_name, prefs, verbose)) + + for script in scripts: if gha_groups: lines.append(f"@echo ::group::Running {script}") lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') @@ -699,6 +756,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] @@ -739,12 +801,15 @@ def main() -> None: disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] + if args.no_avif or args.architecture != "AMD64": + disabled += ["rav1e", "libavif"] prefs = { "architecture": args.architecture, **arch_prefs, # Pillow paths "winbuild_dir": winbuild_dir, + "winbuild_dir_cmake": winbuild_dir.replace("\\", "/"), # Build paths "bin_dir": bin_dir, "build_dir": args.build_dir, @@ -766,6 +831,18 @@ def main() -> None: print() write_script(".gitignore", ["*"], prefs, args.verbose) + write_script( + "install_meson.cmd", + [ + r'call "{build_dir}\build_env.cmd"', + "@echo " + ("=" * 70), + f"@echo ==== {'Building meson':<60} ====", + "@echo " + ("=" * 70), + f"python -mpip install meson=={V['MESON']}", + ], + prefs, + args.verbose, + ) build_env(prefs, args.verbose) build_dep_all(disabled, prefs, args.verbose) From e2add24ec5e8279b61945e1e2d2e31c6997cd9f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:41:14 +1100 Subject: [PATCH 02/22] Added type hints (#2) * Added type hints * Updated nasm to 2.16.03 * Removed duplicate meson install * Simplified code * Sort formats alphabetically * tile is already an empty list --------- Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 26 +-- Tests/check_avif_leaks.py | 4 +- Tests/test_file_avif.py | 197 +++++++++++++---------- docs/handbook/image-file-formats.rst | 16 +- src/PIL/AvifImagePlugin.py | 23 +-- 5 files changed, 147 insertions(+), 119 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2e7d5a23290..3e5b84ed87e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -66,21 +66,25 @@ function build_harfbuzz { } function install_rav1e { - if [[ -n "$IS_MACOS" ]] && [[ "$PLAT" == "arm64" ]]; then - librav1e_tgz=librav1e-${RAV1E_VERSION}-macos-aarch64.tar.gz - elif [ -n "$IS_MACOS" ]; then - librav1e_tgz=librav1e-${RAV1E_VERSION}-macos.tar.gz - elif [ "$PLAT" == "aarch64" ]; then - librav1e_tgz=librav1e-${RAV1E_VERSION}-linux-aarch64.tar.gz + if [ -n "$IS_MACOS" ]; then + suffix="macos" + if [[ "$PLAT" == "arm64" ]]; then + suffix+="-aarch64" + fi else - librav1e_tgz=librav1e-${RAV1E_VERSION}-linux-generic.tar.gz + suffix="linux" + if [[ "$PLAT" == "aarch64" ]]; then + suffix+="-aarch64" + else + suffix+="-generic" + fi fi curl -sLo - \ - https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/$librav1e_tgz \ + https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/librav1e-$RAV1E_VERSION-$suffix.tar.gz \ | tar -C $BUILD_PREFIX --exclude LICENSE --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf - - if [ ! -n "$IS_MACOS" ]; then + if [ -z "$IS_MACOS" ]; then sed -i 's/-lgcc_s/-lgcc_eh/g' "${BUILD_PREFIX}/lib/pkgconfig/rav1e.pc" fi @@ -101,7 +105,7 @@ function build_libavif { python -m pip install meson ninja if [[ "$PLAT" == "x86_64" ]]; then - build_simple nasm 2.15.05 https://www.nasm.us/pub/nasm/releasebuilds/2.15.05/ + build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/ fi local cmake=$(get_modern_cmake) @@ -210,7 +214,7 @@ if [[ -n "$IS_MACOS" ]]; then brew remove --ignore-dependencies webp aom libavif fi - brew install meson pkg-config + brew install pkg-config # clear bash path cache for curl hash -d curl diff --git a/Tests/check_avif_leaks.py b/Tests/check_avif_leaks.py index de6f370d971..e59c4fd8c55 100644 --- a/Tests/check_avif_leaks.py +++ b/Tests/check_avif_leaks.py @@ -20,7 +20,7 @@ ] -def test_leak_load(): +def test_leak_load() -> None: from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit setrlimit(RLIMIT_STACK, (stack_size, stack_size)) @@ -30,7 +30,7 @@ def test_leak_load(): im.load() -def test_leak_save(): +def test_leak_save() -> None: from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit setrlimit(RLIMIT_STACK, (stack_size, stack_size)) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 7474172ab55..129d964e0d0 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -5,13 +5,23 @@ import re import warnings import xml.etree.ElementTree +from collections.abc import Generator from contextlib import contextmanager from io import BytesIO +from pathlib import Path from struct import unpack +from typing import Any import pytest -from PIL import AvifImagePlugin, Image, ImageDraw, UnidentifiedImageError, features +from PIL import ( + AvifImagePlugin, + Image, + ImageDraw, + ImageFile, + UnidentifiedImageError, + features, +) from .helper import ( PillowLeakTestCase, @@ -33,41 +43,43 @@ TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" -def assert_xmp_orientation(xmp, expected): +def assert_xmp_orientation(xmp: bytes | None, expected: int) -> None: assert isinstance(xmp, bytes) root = xml.etree.ElementTree.fromstring(xmp) orientation = None for elem in root.iter(): if elem.tag.endswith("}Description"): - orientation = elem.attrib.get("{http://ns.adobe.com/tiff/1.0/}Orientation") - if orientation: - orientation = int(orientation) + tag_orientation = elem.attrib.get( + "{http://ns.adobe.com/tiff/1.0/}Orientation" + ) + if tag_orientation: + orientation = int(tag_orientation) break assert orientation == expected -def roundtrip(im, **options): +def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile: out = BytesIO() im.save(out, "AVIF", **options) out.seek(0) return Image.open(out) -def skip_unless_avif_decoder(codec_name): +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): +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(): +def is_docker_qemu() -> bool: try: init_proc_exe = os.readlink("/proc/1/exe") except: # noqa: E722 @@ -76,7 +88,7 @@ def is_docker_qemu(): return "qemu" in init_proc_exe -def has_alpha_premultiplied(im_bytes): +def has_alpha_premultiplied(im_bytes: bytes) -> bool: stream = BytesIO(im_bytes) length = len(im_bytes) while stream.tell() < length: @@ -110,7 +122,7 @@ def has_alpha_premultiplied(im_bytes): class TestUnsupportedAvif: - def test_unsupported(self, monkeypatch): + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) file_path = "Tests/images/avif/hopper.avif" @@ -119,7 +131,7 @@ def test_unsupported(self, monkeypatch): lambda: pytest.raises(UnidentifiedImageError, Image.open, file_path), ) - def test_unsupported_open(self, monkeypatch): + def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) file_path = "Tests/images/avif/hopper.avif" @@ -129,11 +141,14 @@ def test_unsupported_open(self, monkeypatch): @skip_unless_feature("avif") class TestFileAvif: - def test_version(self): + def test_version(self) -> None: _avif.AvifCodecVersions() - assert re.search(r"\d+\.\d+\.\d+$", features.version_module("avif")) - def test_read(self): + version = features.version_module("avif") + assert version is not None + assert re.search(r"\d+\.\d+\.\d+$", version) + + def test_read(self) -> None: """ Can we read an AVIF file without error? Does it have the bits we expect? @@ -153,10 +168,10 @@ def test_read(self): image, "Tests/images/avif/hopper_avif_write.png", 12.0 ) - def _roundtrip(self, tmp_path, mode, epsilon, args={}): + def _roundtrip(self, tmp_path: Path, mode: str, epsilon: float) -> None: temp_file = str(tmp_path / "temp.avif") - hopper(mode).save(temp_file, **args) + hopper(mode).save(temp_file) with Image.open(temp_file) as image: assert image.mode == "RGB" assert image.size == (128, 128) @@ -179,7 +194,7 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}): target = target.convert("RGB") assert_image_similar(image, target, epsilon) - def test_write_rgb(self, tmp_path): + 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? @@ -187,32 +202,34 @@ def test_write_rgb(self, tmp_path): self._roundtrip(tmp_path, "RGB", 12.5) - def test_AvifEncoder_with_invalid_args(self): + 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): + 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_encoder_finish_none_error(self, monkeypatch, tmp_path): + 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, **kwargs): + def __init__(self, *args: Any) -> None: pass - def add(self, *args, **kwargs): + def add(self, *args: Any) -> None: pass - def finish(self): + def finish(self) -> None: return None monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) @@ -222,25 +239,25 @@ def finish(self): with pytest.raises(OSError): im.save(test_file) - def test_no_resource_warning(self, tmp_path): + def test_no_resource_warning(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as image: temp_file = str(tmp_path / "temp.avif") with warnings.catch_warnings(): image.save(temp_file) @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) - def test_accept_ftyp_brands(self, major_brand): + 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): + 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): + 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)) @@ -260,20 +277,20 @@ def test_background_from_gif(self, tmp_path): ) assert difference < 5 - def test_save_single_frame(self, tmp_path): + def test_save_single_frame(self, tmp_path: Path) -> None: temp_file = str(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): + 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): + 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)) @@ -281,7 +298,7 @@ def test_load_transparent_rgb(self): # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_transparent(self, tmp_path): + 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))] @@ -293,7 +310,7 @@ def test_save_transparent(self, tmp_path): assert_image(im, "RGBA", (10, 10)) assert im.getcolors() == [(100, (0, 0, 0, 0))] - def test_save_icc_profile(self): + def test_save_icc_profile(self) -> None: with Image.open("Tests/images/avif/icc_profile_none.avif") as im: assert im.info.get("icc_profile") is None @@ -304,32 +321,32 @@ def test_save_icc_profile(self): im = roundtrip(im, icc_profile=expected_icc) assert im.info["icc_profile"] == expected_icc - def test_discard_icc_profile(self): + 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): + 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): + def test_roundtrip_no_icc_profile(self) -> None: with Image.open("Tests/images/avif/icc_profile_none.avif") as im: assert im.info.get("icc_profile") is None im = roundtrip(im) assert "icc_profile" not in im.info - def test_exif(self): + 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 - def test_exif_save(self, tmp_path): + def test_exif_save(self, tmp_path: Path) -> None: with Image.open("Tests/images/avif/exif.avif") as im: test_file = str(tmp_path / "temp.avif") im.save(test_file) @@ -338,7 +355,7 @@ def test_exif_save(self, tmp_path): exif = reloaded.getexif() assert exif[274] == 1 - def test_exif_obj_argument(self, tmp_path): + def test_exif_obj_argument(self, tmp_path: Path) -> None: exif = Image.Exif() exif[274] = 1 exif_data = exif.tobytes() @@ -349,7 +366,7 @@ def test_exif_obj_argument(self, tmp_path): with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == exif_data - def test_exif_bytes_argument(self, tmp_path): + def test_exif_bytes_argument(self, tmp_path: Path) -> None: exif = Image.Exif() exif[274] = 1 exif_data = exif.tobytes() @@ -360,18 +377,18 @@ def test_exif_bytes_argument(self, tmp_path): with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == exif_data - def test_exif_invalid(self, tmp_path): + def test_exif_invalid(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): im.save(test_file, exif=b"invalid") - def test_xmp(self): + def test_xmp(self) -> None: with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: xmp = im.info.get("xmp") assert_xmp_orientation(xmp, 3) - def test_xmp_save(self, tmp_path): + def test_xmp_save(self, tmp_path: Path) -> None: with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: test_file = str(tmp_path / "temp.avif") im.save(test_file) @@ -380,7 +397,7 @@ def test_xmp_save(self, tmp_path): xmp = reloaded.info.get("xmp") assert_xmp_orientation(xmp, 3) - def test_xmp_save_from_png(self, tmp_path): + def test_xmp_save_from_png(self, tmp_path: Path) -> None: with Image.open("Tests/images/xmp_tags_orientation.png") as im: test_file = str(tmp_path / "temp.avif") im.save(test_file) @@ -389,7 +406,7 @@ def test_xmp_save_from_png(self, tmp_path): xmp = reloaded.info.get("xmp") assert_xmp_orientation(xmp, 3) - def test_xmp_save_argument(self, tmp_path): + def test_xmp_save_argument(self, tmp_path: Path) -> None: xmp_arg = "\n".join( [ '', @@ -411,11 +428,11 @@ def test_xmp_save_argument(self, tmp_path): xmp = reloaded.info.get("xmp") assert_xmp_orientation(xmp, 1) - def test_tell(self): + def test_tell(self) -> None: with Image.open(TEST_AVIF_FILE) as im: assert im.tell() == 0 - def test_seek(self): + def test_seek(self) -> None: with Image.open(TEST_AVIF_FILE) as im: im.seek(0) @@ -423,23 +440,23 @@ def test_seek(self): im.seek(1) @pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:0:0"]) - def test_encoder_subsampling(self, tmp_path, subsampling): + def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") im.save(test_file, subsampling=subsampling) - def test_encoder_subsampling_invalid(self, tmp_path): + def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): im.save(test_file, subsampling="foo") - def test_encoder_range(self, tmp_path): + def test_encoder_range(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") im.save(test_file, range="limited") - def test_encoder_range_invalid(self, tmp_path): + def test_encoder_range_invalid(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): @@ -447,12 +464,12 @@ def test_encoder_range_invalid(self, tmp_path): @skip_unless_avif_encoder("aom") @skip_unless_feature("avif") - def test_encoder_codec_param(self, tmp_path): + def test_encoder_codec_param(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") im.save(test_file, codec="aom") - def test_encoder_codec_invalid(self, tmp_path): + def test_encoder_codec_invalid(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): @@ -460,7 +477,7 @@ def test_encoder_codec_invalid(self, tmp_path): @skip_unless_avif_decoder("dav1d") @skip_unless_feature("avif") - def test_encoder_codec_cannot_encode(self, tmp_path): + def test_encoder_codec_cannot_encode(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): @@ -468,7 +485,7 @@ def test_encoder_codec_cannot_encode(self, tmp_path): @skip_unless_avif_encoder("aom") @skip_unless_feature("avif") - def test_encoder_advanced_codec_options(self): + def test_encoder_advanced_codec_options(self) -> None: with Image.open(TEST_AVIF_FILE) as im: ctrl_buf = BytesIO() im.save(ctrl_buf, "AVIF", codec="aom") @@ -487,7 +504,9 @@ def test_encoder_advanced_codec_options(self): @skip_unless_avif_encoder("aom") @skip_unless_feature("avif") @pytest.mark.parametrize("val", [{"foo": "bar"}, 1234]) - def test_encoder_advanced_codec_options_invalid(self, tmp_path, val): + def test_encoder_advanced_codec_options_invalid( + self, tmp_path: Path, val: dict[str, str] | int + ) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): @@ -495,7 +514,7 @@ def test_encoder_advanced_codec_options_invalid(self, tmp_path, val): @skip_unless_avif_decoder("aom") @skip_unless_feature("avif") - def test_decoder_codec_param(self): + def test_decoder_codec_param(self) -> None: AvifImagePlugin.DECODE_CODEC_CHOICE = "aom" try: with Image.open(TEST_AVIF_FILE) as im: @@ -505,7 +524,7 @@ def test_decoder_codec_param(self): @skip_unless_avif_encoder("rav1e") @skip_unless_feature("avif") - def test_decoder_codec_cannot_decode(self, tmp_path): + def test_decoder_codec_cannot_decode(self, tmp_path: Path) -> None: AvifImagePlugin.DECODE_CODEC_CHOICE = "rav1e" try: with pytest.raises(ValueError): @@ -514,7 +533,7 @@ def test_decoder_codec_cannot_decode(self, tmp_path): finally: AvifImagePlugin.DECODE_CODEC_CHOICE = "auto" - def test_decoder_codec_invalid(self): + def test_decoder_codec_invalid(self) -> None: AvifImagePlugin.DECODE_CODEC_CHOICE = "foo" try: with pytest.raises(ValueError): @@ -525,22 +544,22 @@ def test_decoder_codec_invalid(self): @skip_unless_avif_encoder("aom") @skip_unless_feature("avif") - def test_encoder_codec_available(self): + def test_encoder_codec_available(self) -> None: assert _avif.encoder_codec_available("aom") is True - def test_encoder_codec_available_bad_params(self): + def test_encoder_codec_available_bad_params(self) -> None: with pytest.raises(TypeError): _avif.encoder_codec_available() @skip_unless_avif_decoder("dav1d") @skip_unless_feature("avif") - def test_encoder_codec_available_cannot_decode(self): + def test_encoder_codec_available_cannot_decode(self) -> None: assert _avif.encoder_codec_available("dav1d") is False - def test_encoder_codec_available_invalid(self): + def test_encoder_codec_available_invalid(self) -> None: assert _avif.encoder_codec_available("foo") is False - def test_encoder_quality_valueerror(self, tmp_path): + def test_encoder_quality_valueerror(self, tmp_path: Path) -> None: with Image.open("Tests/images/avif/hopper.avif") as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): @@ -548,23 +567,23 @@ def test_encoder_quality_valueerror(self, tmp_path): @skip_unless_avif_decoder("aom") @skip_unless_feature("avif") - def test_decoder_codec_available(self): + def test_decoder_codec_available(self) -> None: assert _avif.decoder_codec_available("aom") is True - def test_decoder_codec_available_bad_params(self): + def test_decoder_codec_available_bad_params(self) -> None: with pytest.raises(TypeError): _avif.decoder_codec_available() @skip_unless_avif_encoder("rav1e") @skip_unless_feature("avif") - def test_decoder_codec_available_cannot_decode(self): + def test_decoder_codec_available_cannot_decode(self) -> None: assert _avif.decoder_codec_available("rav1e") is False - def test_decoder_codec_available_invalid(self): + def test_decoder_codec_available_invalid(self) -> None: assert _avif.decoder_codec_available("foo") is False @pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"]) - def test_decoder_upsampling(self, upsampling): + def test_decoder_upsampling(self, upsampling: str) -> None: AvifImagePlugin.CHROMA_UPSAMPLING = upsampling try: with Image.open(TEST_AVIF_FILE): @@ -572,7 +591,7 @@ def test_decoder_upsampling(self, upsampling): finally: AvifImagePlugin.CHROMA_UPSAMPLING = "auto" - def test_decoder_upsampling_invalid(self): + def test_decoder_upsampling_invalid(self) -> None: AvifImagePlugin.CHROMA_UPSAMPLING = "foo" try: with pytest.raises(ValueError): @@ -581,7 +600,7 @@ def test_decoder_upsampling_invalid(self): finally: AvifImagePlugin.CHROMA_UPSAMPLING = "auto" - def test_p_mode_transparency(self): + def test_p_mode_transparency(self) -> None: im = Image.new("P", size=(64, 64)) draw = ImageDraw.Draw(im) draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) @@ -595,19 +614,19 @@ def test_p_mode_transparency(self): assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1) - def test_decoder_strict_flags(self): + def test_decoder_strict_flags(self) -> None: # This would fail if full avif strictFlags were enabled with Image.open("Tests/images/avif/chimera-missing-pixi.avif") as im: assert im.size == (480, 270) @skip_unless_avif_encoder("aom") - def test_aom_optimizations(self): + def test_aom_optimizations(self) -> None: im = hopper("RGB") buf = BytesIO() im.save(buf, format="AVIF", codec="aom", speed=1) @skip_unless_avif_encoder("svt") - def test_svt_optimizations(self): + def test_svt_optimizations(self) -> None: im = hopper("RGB") buf = BytesIO() im.save(buf, format="AVIF", codec="svt", speed=1) @@ -616,14 +635,14 @@ def test_svt_optimizations(self): @skip_unless_feature("avif") class TestAvifAnimation: @contextmanager - def star_frames(self): + def star_frames(self) -> Generator[list[ImageFile.ImageFile], None, None]: with Image.open("Tests/images/avif/star.png") as f1: with Image.open("Tests/images/avif/star90.png") as f2: with Image.open("Tests/images/avif/star180.png") as f3: with Image.open("Tests/images/avif/star270.png") as f4: yield [f1, f2, f3, f4] - def test_n_frames(self): + def test_n_frames(self) -> None: """ Ensure that AVIF format sets n_frames and is_animated attributes correctly. @@ -637,7 +656,7 @@ def test_n_frames(self): assert im.n_frames == 5 assert im.is_animated - def test_write_animation_L(self, tmp_path): + def test_write_animation_L(self, tmp_path: Path) -> None: """ Convert an animated GIF to animated AVIF, then compare the frame count, and first and last frames to ensure they're visually similar. @@ -661,13 +680,13 @@ def test_write_animation_L(self, tmp_path): im.load() assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) - def test_write_animation_RGB(self, tmp_path): + def test_write_animation_RGB(self, tmp_path: Path) -> None: """ Write an animated AVIF from RGB frames, and ensure the frames are visually similar to the originals. """ - def check(temp_file): + def check(temp_file: str) -> None: with Image.open(temp_file) as im: assert im.n_frames == 4 @@ -688,7 +707,9 @@ def check(temp_file): check(temp_file1) # Tests appending using a generator - def imGenerator(ims): + def imGenerator( + ims: list[ImageFile.ImageFile], + ) -> Generator[ImageFile.ImageFile, None, None]: yield from ims temp_file2 = str(tmp_path / "temp_generator.avif") @@ -699,27 +720,27 @@ def imGenerator(ims): ) check(temp_file2) - def test_sequence_dimension_mismatch_check(self, tmp_path): + def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: temp_file = str(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], duration=100) - def test_heif_raises_unidentified_image_error(self): + def test_heif_raises_unidentified_image_error(self) -> None: with pytest.raises(UnidentifiedImageError): with Image.open("Tests/images/avif/rgba10.heif"): pass @pytest.mark.parametrize("alpha_premultipled", [False, True]) - def test_alpha_premultiplied_true(self, alpha_premultipled): + def test_alpha_premultiplied_true(self, alpha_premultipled: bool) -> None: im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) im_buf = BytesIO() im.save(im_buf, "AVIF", alpha_premultiplied=alpha_premultipled) im_bytes = im_buf.getvalue() assert has_alpha_premultiplied(im_bytes) is alpha_premultipled - def test_timestamp_and_duration(self, tmp_path): + 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. @@ -748,7 +769,7 @@ def test_timestamp_and_duration(self, tmp_path): assert im.info["timestamp"] == ts ts += durations[frame] - def test_seeking(self, tmp_path): + 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. @@ -777,7 +798,7 @@ def test_seeking(self, tmp_path): assert im.info["timestamp"] == ts ts -= dur - def test_seek_errors(self): + def test_seek_errors(self) -> None: with Image.open("Tests/images/avif/star.avifs") as im: with pytest.raises(EOFError): im.seek(-1) @@ -797,11 +818,11 @@ class TestAvifLeaks(PillowLeakTestCase): @pytest.mark.skipif( is_docker_qemu(), reason="Skipping on cross-architecture containers" ) - def test_leak_load(self): + def test_leak_load(self) -> None: with open(TEST_AVIF_FILE, "rb") as f: im_data = f.read() - def core(): + def core() -> None: with Image.open(BytesIO(im_data)) as im: im.load() gc.collect() diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index af8cd8818f7..62ef57920b6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -235,7 +235,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, WebP, and AVIF. + This is currently 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. @@ -1321,7 +1321,7 @@ as 8-bit RGB(A). The :py:meth:`~PIL.Image.Image.save` method supports the following options: **quality** - Integer, 1-100, Defaults to 90. 0 gives the smallest size and poorest + Integer, 1-100, defaults to 90. 0 gives the smallest size and poorest quality, 100 the largest and best quality. The value of this setting controls the ``qmin`` and ``qmax`` encoder options. @@ -1331,19 +1331,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: values are, the worse the quality. **subsampling** - If present, sets the subsampling for the encoder. Defaults to ``"4:2:0``". + 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"`` + * ``4:0:0`` + * ``4:2:0`` + * ``4:2:2`` + * ``4:4:4`` **speed** Quality/speed trade-off (0=slower-better, 10=fastest). Defaults to 8. **range** - YUV range, either "full" or "limited." Defaults to "full" + YUV range, either "full" or "limited". Defaults to "full" **codec** AV1 codec to use for encoding. Possible values are "aom", "rav1e", and diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 06edf230620..56a19f96362 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from typing import IO from . import ExifTags, Image, ImageFile @@ -20,9 +21,9 @@ _VALID_AVIF_MODES = {"RGB", "RGBA"} -def _accept(prefix): +def _accept(prefix: bytes) -> bool | str: if prefix[4:8] != b"ftyp": - return + return False coding_brands = (b"avif", b"avis") container_brands = (b"mif1", b"msf1") major_brand = prefix[8:12] @@ -42,6 +43,7 @@ def _accept(prefix): # Also, because this file might not actually be an AVIF file, we # don't raise an error if AVIF support isn't properly compiled. return True + return False class AvifImageFile(ImageFile.ImageFile): @@ -53,7 +55,7 @@ class AvifImageFile(ImageFile.ImageFile): def load_seek(self, pos: int) -> None: pass - def _open(self): + def _open(self) -> None: if not SUPPORTED: msg = ( "image file could not be identified because AVIF " @@ -71,7 +73,6 @@ def _open(self): self.n_frames = n_frames self.is_animated = self.n_frames > 1 self._mode = self.rawmode = mode - self.tile = [] if icc: self.info["icc_profile"] = icc @@ -80,13 +81,13 @@ def _open(self): if xmp: self.info["xmp"] = xmp - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return self.__frame = frame - def load(self): + def load(self) -> Image.core.PixelAccess | None: if self.__loaded != self.__frame: # We need to load the image data for this frame data, timescale, tsp_in_ts, dur_in_ts = self._decoder.get_frame( @@ -102,19 +103,21 @@ def load(self): if self.fp and self._exclusive_fp: self.fp.close() self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)] return super().load() - def tell(self): + def tell(self) -> int: return self.__frame -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: _save(im, fp, filename, save_all=True) -def _save(im, fp, filename, save_all=False): +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", [])) From e5494a2ce83589d93b05d5a4b2f9805295099baf Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Wed, 16 Oct 2024 09:43:32 -0400 Subject: [PATCH 03/22] Fix PLAT envvar in cibuildwheel container --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3e5b84ed87e..524642a40c9 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -5,7 +5,7 @@ if [ -z "$IS_MACOS" ]; then export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} export MB_ML_VER=${AUDITWHEEL_POLICY:9} fi -export PLAT=$CIBW_ARCHS +export PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh if [ -z "$IS_MACOS" ]; then From 8b8bbba0f77c4ed5d1479d3f6e0c1b79dfb4f867 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Fri, 18 Oct 2024 09:33:55 -0400 Subject: [PATCH 04/22] Update Tests/check_avif_leaks.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/check_avif_leaks.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/check_avif_leaks.py b/Tests/check_avif_leaks.py index e59c4fd8c55..343349dd6a1 100644 --- a/Tests/check_avif_leaks.py +++ b/Tests/check_avif_leaks.py @@ -36,9 +36,8 @@ def test_leak_save() -> None: setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) for _ in range(iterations): + test_output = BytesIO() with Image.open(test_file) as im: - im.load() - test_output = BytesIO() im.save(test_output, "AVIF") - test_output.seek(0) - test_output.read() + test_output.seek(0) + test_output.read() From 58ef69228dc5c92fd89a0f68d9afeace915dc325 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 19 Oct 2024 16:15:53 +1100 Subject: [PATCH 05/22] Simplified code (#3) * Removed unnecessary meson install * Use the same Python as the build script * Use python3 * Simplified code * Updated meson --------- Co-authored-by: Andrew Murray --- .github/workflows/test-windows.yml | 2 - .github/workflows/wheels-dependencies.sh | 2 +- Tests/test_file_avif.py | 158 +++++++++-------------- src/PIL/AvifImagePlugin.py | 4 +- winbuild/build_prepare.py | 5 +- 5 files changed, 65 insertions(+), 106 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 419f42e77b3..b64b00e4f5c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -86,8 +86,6 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - python -m pip install meson - choco install ghostscript --version=10.4.0 --no-progress echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 524642a40c9..39d69b1f728 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -102,7 +102,7 @@ EOF function build_libavif { install_rav1e - python -m pip install meson ninja + python3 -m pip install meson ninja if [[ "$PLAT" == "x86_64" ]]; then build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/ diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 129d964e0d0..08b20047f0c 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -4,7 +4,6 @@ import os import re import warnings -import xml.etree.ElementTree from collections.abc import Generator from contextlib import contextmanager from io import BytesIO @@ -43,25 +42,13 @@ TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" -def assert_xmp_orientation(xmp: bytes | None, expected: int) -> None: - assert isinstance(xmp, bytes) - root = xml.etree.ElementTree.fromstring(xmp) - orientation = None - for elem in root.iter(): - if elem.tag.endswith("}Description"): - tag_orientation = elem.attrib.get( - "{http://ns.adobe.com/tiff/1.0/}Orientation" - ) - if tag_orientation: - orientation = int(tag_orientation) - break - assert orientation == expected +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) - out.seek(0) return Image.open(out) @@ -82,7 +69,7 @@ def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: def is_docker_qemu() -> bool: try: init_proc_exe = os.readlink("/proc/1/exe") - except: # noqa: E722 + except (FileNotFoundError, PermissionError): return False else: return "qemu" in init_proc_exe @@ -125,18 +112,16 @@ class TestUnsupportedAvif: def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) - file_path = "Tests/images/avif/hopper.avif" - pytest.warns( - UserWarning, - lambda: pytest.raises(UnidentifiedImageError, Image.open, file_path), - ) + 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) - file_path = "Tests/images/avif/hopper.avif" with pytest.raises(SyntaxError): - AvifImagePlugin.AvifImageFile(file_path) + AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) @skip_unless_feature("avif") @@ -154,12 +139,11 @@ def test_read(self) -> None: Does it have the bits we expect? """ - with Image.open("Tests/images/avif/hopper.avif") as image: + 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.load() image.getdata() # generated with: @@ -176,7 +160,6 @@ def _roundtrip(self, tmp_path: Path, mode: str, epsilon: float) -> None: assert image.mode == "RGB" assert image.size == (128, 128) assert image.format == "AVIF" - image.load() image.getdata() if mode == "RGB": @@ -240,10 +223,10 @@ def finish(self) -> None: im.save(test_file) def test_no_resource_warning(self, tmp_path: Path) -> None: - with Image.open(TEST_AVIF_FILE) as image: + with Image.open(TEST_AVIF_FILE) as im: temp_file = str(tmp_path / "temp.avif") with warnings.catch_warnings(): - image.save(temp_file) + im.save(temp_file) @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) def test_accept_ftyp_brands(self, major_brand: bytes) -> None: @@ -272,9 +255,7 @@ def test_background_from_gif(self, tmp_path: Path) -> None: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) - difference = sum( - [abs(original_value[i] - reread_value[i]) for i in range(0, 3)] - ) + difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) assert difference < 5 def test_save_single_frame(self, tmp_path: Path) -> None: @@ -296,7 +277,7 @@ def test_load_transparent_rgb(self) -> None: assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 + 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)) @@ -305,7 +286,7 @@ def test_save_transparent(self, tmp_path: Path) -> None: test_file = str(tmp_path / "temp.avif") im.save(test_file) - # check if saved image contains same transparency + # 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))] @@ -346,7 +327,7 @@ def test_exif(self) -> None: exif = im.getexif() assert exif[274] == 1 - def test_exif_save(self, tmp_path: Path) -> None: + def test_exif_save_default(self, tmp_path: Path) -> None: with Image.open("Tests/images/avif/exif.avif") as im: test_file = str(tmp_path / "temp.avif") im.save(test_file) @@ -355,24 +336,14 @@ def test_exif_save(self, tmp_path: Path) -> None: exif = reloaded.getexif() assert exif[274] == 1 - def test_exif_obj_argument(self, tmp_path: Path) -> None: - exif = Image.Exif() - exif[274] = 1 - exif_data = exif.tobytes() - with Image.open(TEST_AVIF_FILE) as im: - test_file = str(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_bytes_argument(self, tmp_path: Path) -> None: + @pytest.mark.parametrize("bytes", [True, False]) + def test_exif_save_argument(self, tmp_path: Path, bytes: bool) -> None: exif = Image.Exif() exif[274] = 1 exif_data = exif.tobytes() with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") - im.save(test_file, exif=exif_data) + im.save(test_file, exif=exif_data if bytes else exif) with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == exif_data @@ -385,7 +356,7 @@ def test_exif_invalid(self, tmp_path: Path) -> None: def test_xmp(self) -> None: with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: - xmp = im.info.get("xmp") + xmp = im.info["xmp"] assert_xmp_orientation(xmp, 3) def test_xmp_save(self, tmp_path: Path) -> None: @@ -394,7 +365,7 @@ def test_xmp_save(self, tmp_path: Path) -> None: im.save(test_file) with Image.open(test_file) as reloaded: - xmp = reloaded.info.get("xmp") + xmp = reloaded.info["xmp"] assert_xmp_orientation(xmp, 3) def test_xmp_save_from_png(self, tmp_path: Path) -> None: @@ -403,7 +374,7 @@ def test_xmp_save_from_png(self, tmp_path: Path) -> None: im.save(test_file) with Image.open(test_file) as reloaded: - xmp = reloaded.info.get("xmp") + xmp = reloaded.info["xmp"] assert_xmp_orientation(xmp, 3) def test_xmp_save_argument(self, tmp_path: Path) -> None: @@ -420,12 +391,12 @@ def test_xmp_save_argument(self, tmp_path: Path) -> None: '', ] ) - with Image.open("Tests/images/avif/hopper.avif") as im: + with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") im.save(test_file, xmp=xmp_arg) with Image.open(test_file) as reloaded: - xmp = reloaded.info.get("xmp") + xmp = reloaded.info["xmp"] assert_xmp_orientation(xmp, 1) def test_tell(self) -> None: @@ -514,33 +485,29 @@ def test_encoder_advanced_codec_options_invalid( @skip_unless_avif_decoder("aom") @skip_unless_feature("avif") - def test_decoder_codec_param(self) -> None: - AvifImagePlugin.DECODE_CODEC_CHOICE = "aom" - try: - with Image.open(TEST_AVIF_FILE) as im: - assert im.size == (128, 128) - finally: - AvifImagePlugin.DECODE_CODEC_CHOICE = "auto" + 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") @skip_unless_feature("avif") - def test_decoder_codec_cannot_decode(self, tmp_path: Path) -> None: - AvifImagePlugin.DECODE_CODEC_CHOICE = "rav1e" - try: - with pytest.raises(ValueError): - with Image.open(TEST_AVIF_FILE): - pass - finally: - AvifImagePlugin.DECODE_CODEC_CHOICE = "auto" + def test_decoder_codec_cannot_decode( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") - def test_decoder_codec_invalid(self) -> None: - AvifImagePlugin.DECODE_CODEC_CHOICE = "foo" - try: - with pytest.raises(ValueError): - with Image.open(TEST_AVIF_FILE): - pass - finally: - AvifImagePlugin.DECODE_CODEC_CHOICE = "auto" + 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") @skip_unless_feature("avif") @@ -560,7 +527,7 @@ 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("Tests/images/avif/hopper.avif") as im: + with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") with pytest.raises(ValueError): im.save(test_file, quality="invalid") @@ -583,22 +550,20 @@ def test_decoder_codec_available_invalid(self) -> None: assert _avif.decoder_codec_available("foo") is False @pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"]) - def test_decoder_upsampling(self, upsampling: str) -> None: - AvifImagePlugin.CHROMA_UPSAMPLING = upsampling - try: + def test_decoder_upsampling( + self, monkeypatch: pytest.MonkeyPatch, upsampling: str + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "CHROMA_UPSAMPLING", upsampling) + + with Image.open(TEST_AVIF_FILE): + pass + + def test_decoder_upsampling_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "CHROMA_UPSAMPLING", "foo") + + with pytest.raises(ValueError): with Image.open(TEST_AVIF_FILE): pass - finally: - AvifImagePlugin.CHROMA_UPSAMPLING = "auto" - - def test_decoder_upsampling_invalid(self) -> None: - AvifImagePlugin.CHROMA_UPSAMPLING = "foo" - try: - with pytest.raises(ValueError): - with Image.open(TEST_AVIF_FILE): - pass - finally: - AvifImagePlugin.CHROMA_UPSAMPLING = "auto" def test_p_mode_transparency(self) -> None: im = Image.new("P", size=(64, 64)) @@ -612,7 +577,8 @@ def test_p_mode_transparency(self) -> None: buf_out = BytesIO() im_png.save(buf_out, format="AVIF", quality=100) - assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1) + with Image.open(buf_out) as expected: + assert_image_similar(im_png.convert("RGBA"), expected, 1) def test_decoder_strict_flags(self) -> None: # This would fail if full avif strictFlags were enabled @@ -648,7 +614,7 @@ def test_n_frames(self) -> None: correctly. """ - with Image.open("Tests/images/avif/hopper.avif") as im: + with Image.open(TEST_AVIF_FILE) as im: assert im.n_frames == 1 assert not im.is_animated @@ -671,13 +637,9 @@ def test_write_animation_L(self, tmp_path: Path) -> None: assert im.n_frames == orig.n_frames # Compare first and second-to-last frames to the original animated GIF - orig.load() - im.load() assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) orig.seek(orig.n_frames - 2) im.seek(im.n_frames - 2) - orig.load() - im.load() assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) def test_write_animation_RGB(self, tmp_path: Path) -> None: @@ -691,12 +653,10 @@ def check(temp_file: str) -> None: assert im.n_frames == 4 # Compare first frame to original - im.load() assert_image_similar(im, frame1.convert("RGBA"), 25.0) # Compare second frame to original im.seek(1) - im.load() assert_image_similar(im, frame2.convert("RGBA"), 25.0) with self.star_frames() as frames: @@ -725,7 +685,7 @@ def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: 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], duration=100) + frame1.save(temp_file, save_all=True, append_images=[frame2]) def test_heif_raises_unidentified_image_error(self) -> None: with pytest.raises(UnidentifiedImageError): diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 56a19f96362..bf0b9085e9e 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -72,7 +72,7 @@ def _open(self) -> None: self._size = width, height self.n_frames = n_frames self.is_animated = self.n_frames > 1 - self._mode = self.rawmode = mode + self._mode = mode if icc: self.info["icc_profile"] = icc @@ -103,7 +103,7 @@ def load(self) -> Image.core.PixelAccess | None: if self.fp and self._exclusive_fp: self.fp.close() self.fp = BytesIO(data) - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] return super().load() diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 192dad38587..d7dd3c13310 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import shutil import struct import subprocess +import sys from typing import Any @@ -121,7 +122,7 @@ def cmd_msbuild( "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", - "MESON": "1.5.1", + "MESON": "1.5.2", "LIBAVIF": "1.1.1", "RAV1E": "0.7.1", } @@ -838,7 +839,7 @@ def main() -> None: "@echo " + ("=" * 70), f"@echo ==== {'Building meson':<60} ====", "@echo " + ("=" * 70), - f"python -mpip install meson=={V['MESON']}", + f"{sys.executable} -m pip install meson=={V['MESON']}", ], prefs, args.verbose, From 50b993a0cb990d1526d2f40899bb6c548998de43 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 12 Nov 2024 04:22:54 +1100 Subject: [PATCH 06/22] Set default max threads in Python (#4) * Removed unused C values * Set default max threads in Python --------- Co-authored-by: Andrew Murray --- src/PIL/AvifImagePlugin.py | 17 +++++++- src/_avif.c | 89 ++++++-------------------------------- 2 files changed, 29 insertions(+), 77 deletions(-) diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index bf0b9085e9e..e9749f36cc2 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from io import BytesIO from typing import IO @@ -46,6 +47,15 @@ def _accept(prefix: bytes) -> bool | str: return False +def _get_default_max_threads(): + 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" @@ -64,7 +74,10 @@ def _open(self) -> None: raise SyntaxError(msg) self._decoder = _avif.AvifDecoder( - self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING, DEFAULT_MAX_THREADS + self.fp.read(), + DECODE_CODEC_CHOICE, + CHROMA_UPSAMPLING, + _get_default_max_threads(), ) # Get info from decoder @@ -140,7 +153,7 @@ def _save( duration = info.get("duration", 0) subsampling = info.get("subsampling", "4:2:0") speed = info.get("speed", 6) - max_threads = info.get("max_threads", DEFAULT_MAX_THREADS) + max_threads = info.get("max_threads", _get_default_max_threads()) codec = info.get("codec", "auto") range_ = info.get("range", "full") tile_rows_log2 = info.get("tile_rows", 0) diff --git a/src/_avif.c b/src/_avif.c index 5365d38b660..5e993e35447 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -44,54 +44,6 @@ typedef struct { static PyTypeObject AvifDecoder_Type; -static int default_max_threads = 0; - -static void -init_max_threads(void) { - PyObject *os = NULL; - PyObject *n = NULL; - long num_cpus; - - os = PyImport_ImportModule("os"); - if (os == NULL) { - goto error; - } - - if (PyObject_HasAttrString(os, "sched_getaffinity")) { - n = PyObject_CallMethod(os, "sched_getaffinity", "i", 0); - if (n == NULL) { - goto error; - } - num_cpus = PySet_Size(n); - } else { - n = PyObject_CallMethod(os, "cpu_count", NULL); - if (n == NULL) { - goto error; - } - num_cpus = PyLong_AsLong(n); - } - - if (num_cpus < 1) { - goto error; - } - - default_max_threads = (int)num_cpus; - -done: - Py_XDECREF(os); - Py_XDECREF(n); - return; - -error: - if (PyErr_Occurred()) { - PyErr_Clear(); - } - PyErr_WarnEx( - PyExc_RuntimeWarning, "could not get cpu count: using max_threads=1", 1 - ); - goto done; -} - static int normalize_quantize_value(int qvalue) { if (qvalue < AVIF_QUANTIZER_BEST_QUALITY) { @@ -306,23 +258,23 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { AvifEncoderObject *self = NULL; avifEncoder *encoder = NULL; - char *subsampling = "4:2:0"; - int qmin = AVIF_QUANTIZER_BEST_QUALITY; // =0 - int qmax = 10; // "High Quality", but not lossless - int quality = 75; - int speed = 8; - int exif_orientation = 0; - int max_threads = default_max_threads; + char *subsampling; + int qmin; + int qmax; + int quality; + int speed; + int exif_orientation; + int max_threads; PyObject *icc_bytes; PyObject *exif_bytes; PyObject *xmp_bytes; - PyObject *alpha_premultiplied = NULL; - PyObject *autotiling = NULL; - int tile_rows_log2 = 0; - int tile_cols_log2 = 0; + PyObject *alpha_premultiplied; + PyObject *autotiling; + int tile_rows_log2; + int tile_cols_log2; - char *codec = "auto"; - char *range = "full"; + char *codec; + char *range; PyObject *advanced; @@ -438,13 +390,6 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { encoder = avifEncoderCreate(); - if (max_threads == 0) { - if (default_max_threads == 0) { - init_max_threads(); - } - max_threads = default_max_threads; - } - int is_aom_encode = strcmp(codec, "aom") == 0 || (strcmp(codec, "auto") == 0 && _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE)); @@ -730,7 +675,7 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { char *codec_str; avifCodecChoice codec; avifChromaUpsampling upsampling; - int max_threads = 0; + int max_threads; avifResult result; @@ -785,12 +730,6 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { self->decoder = avifDecoderCreate(); #if AVIF_VERSION >= 80400 - if (max_threads == 0) { - if (default_max_threads == 0) { - init_max_threads(); - } - max_threads = default_max_threads; - } self->decoder->maxThreads = max_threads; #endif #if AVIF_VERSION >= 90200 From 671e3c8b578d0d9bf46c6383d1ac9c98701ad6e2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 13 Nov 2024 05:33:18 +1100 Subject: [PATCH 07/22] Removed unused upsampling setting (#5) * Use filename placeholder in URL * Removed unused upsampling setting * Use has_transparency_data * Removed unnecessary load() * Test getexif() change --------- Co-authored-by: Andrew Murray --- Tests/test_file_avif.py | 20 ++++---------------- src/PIL/AvifImagePlugin.py | 14 +------------- src/PIL/Image.py | 4 ++-- src/_avif.c | 27 +-------------------------- winbuild/build_prepare.py | 3 +-- 5 files changed, 9 insertions(+), 59 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 08b20047f0c..32dd694f580 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -327,6 +327,10 @@ def test_exif(self) -> None: 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 + def test_exif_save_default(self, tmp_path: Path) -> None: with Image.open("Tests/images/avif/exif.avif") as im: test_file = str(tmp_path / "temp.avif") @@ -549,22 +553,6 @@ def test_decoder_codec_available_cannot_decode(self) -> None: def test_decoder_codec_available_invalid(self) -> None: assert _avif.decoder_codec_available("foo") is False - @pytest.mark.parametrize("upsampling", ["fastest", "best", "nearest", "bilinear"]) - def test_decoder_upsampling( - self, monkeypatch: pytest.MonkeyPatch, upsampling: str - ) -> None: - monkeypatch.setattr(AvifImagePlugin, "CHROMA_UPSAMPLING", upsampling) - - with Image.open(TEST_AVIF_FILE): - pass - - def test_decoder_upsampling_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(AvifImagePlugin, "CHROMA_UPSAMPLING", "foo") - - with pytest.raises(ValueError): - with Image.open(TEST_AVIF_FILE): - pass - def test_p_mode_transparency(self) -> None: im = Image.new("P", size=(64, 64)) draw = ImageDraw.Draw(im) diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index e9749f36cc2..1c90f428a09 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -16,7 +16,6 @@ # 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" -CHROMA_UPSAMPLING = "auto" DEFAULT_MAX_THREADS = 0 _VALID_AVIF_MODES = {"RGB", "RGBA"} @@ -76,7 +75,6 @@ def _open(self) -> None: self._decoder = _avif.AvifDecoder( self.fp.read(), DECODE_CODEC_CHOICE, - CHROMA_UPSAMPLING, _get_default_max_threads(), ) @@ -238,22 +236,12 @@ def _save( for idx in range(nfr): ims.seek(idx) - ims.load() # Make sure image mode is supported frame = ims rawmode = ims.mode if ims.mode not in _VALID_AVIF_MODES: - alpha = ( - "A" in ims.mode - or "a" in ims.mode - or (ims.mode == "P" and "A" in ims.im.getpalettemode()) - or ( - ims.mode == "P" - and ims.info.get("transparency", None) is not None - ) - ) - rawmode = "RGBA" if alpha else "RGB" + rawmode = "RGBA" if ims.has_transparency_data else "RGB" frame = ims.convert(rawmode) # Update frame duration diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a49a38739f8..c00ab8a886d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1548,8 +1548,8 @@ def getexif(self) -> Exif: # XMP tags if ExifTags.Base.Orientation not in self._exif: - xmp_tags = self.info.get("XML:com.adobe.xmp") or self.info.get("xmp") - if isinstance(xmp_tags, bytes): + 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) diff --git a/src/_avif.c b/src/_avif.c index 5e993e35447..d0bb81f4619 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -3,12 +3,6 @@ #include #include "avif/avif.h" -#if AVIF_VERSION < 80300 -#define AVIF_CHROMA_UPSAMPLING_AUTOMATIC AVIF_CHROMA_UPSAMPLING_BILINEAR -#define AVIF_CHROMA_UPSAMPLING_BEST_QUALITY AVIF_CHROMA_UPSAMPLING_BILINEAR -#define AVIF_CHROMA_UPSAMPLING_FASTEST AVIF_CHROMA_UPSAMPLING_NEAREST -#endif - typedef struct { avifPixelFormat subsampling; int qmin; @@ -671,32 +665,13 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { PyObject *avif_bytes; AvifDecoderObject *self = NULL; - char *upsampling_str; char *codec_str; avifCodecChoice codec; - avifChromaUpsampling upsampling; int max_threads; avifResult result; - if (!PyArg_ParseTuple( - args, "Sssi", &avif_bytes, &codec_str, &upsampling_str, &max_threads - )) { - return NULL; - } - - if (!strcmp(upsampling_str, "auto")) { - upsampling = AVIF_CHROMA_UPSAMPLING_AUTOMATIC; - } else if (!strcmp(upsampling_str, "fastest")) { - upsampling = AVIF_CHROMA_UPSAMPLING_FASTEST; - } else if (!strcmp(upsampling_str, "best")) { - upsampling = AVIF_CHROMA_UPSAMPLING_BEST_QUALITY; - } else if (!strcmp(upsampling_str, "nearest")) { - upsampling = AVIF_CHROMA_UPSAMPLING_NEAREST; - } else if (!strcmp(upsampling_str, "bilinear")) { - upsampling = AVIF_CHROMA_UPSAMPLING_BILINEAR; - } else { - PyErr_Format(PyExc_ValueError, "Invalid upsampling option: %s", upsampling_str); + if (!PyArg_ParseTuple(args, "Ssi", &avif_bytes, &codec_str, &max_threads)) { return NULL; } diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d19eb978fe9..8ee9cd90e54 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -402,8 +402,7 @@ def cmd_msbuild( }, "rav1e": { "url": ( - f"https://github.com/xiph/rav1e/releases/download/v{V['RAV1E']}/" - f"rav1e-{V['RAV1E']}-windows-msvc-generic.zip" + f"https://github.com/xiph/rav1e/releases/download/v{V['RAV1E']}/FILENAME" ), "filename": f"rav1e-{V['RAV1E']}-windows-msvc-generic.zip", "dir": "rav1e-windows-msvc-sdk", From c40bcbfc874242a2d7f5463cf9a13684a68e3f9e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 10:31:57 +1100 Subject: [PATCH 08/22] Improved error handling --- .ci/install.sh | 3 +- .github/workflows/wheels-dependencies.sh | 15 +-- Tests/check_wheel.py | 2 +- Tests/test_file_avif.py | 2 - docs/handbook/image-file-formats.rst | 9 +- docs/installation/building-from-source.rst | 9 +- src/PIL/AvifImagePlugin.py | 38 +++--- src/_avif.c | 147 +++++++++++++-------- winbuild/build_prepare.py | 46 ++++--- 9 files changed, 155 insertions(+), 116 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index f24768d788e..b247440496a 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -23,8 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ - sway wl-clipboard libopenblas-dev\ - ninja-build build-essential nasm + sway wl-clipboard libopenblas-dev nasm fi python3 -m pip install --upgrade pip diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 956793b586d..69a2e3c34f3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -114,7 +114,7 @@ function install_rav1e { curl -sLo - \ https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/librav1e-$RAV1E_VERSION-$suffix.tar.gz \ - | tar -C $BUILD_PREFIX --exclude LICENSE --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf - + | tar -C $BUILD_PREFIX --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf - if [ -z "$IS_MACOS" ]; then sed -i 's/-lgcc_s/-lgcc_eh/g' "${BUILD_PREFIX}/lib/pkgconfig/rav1e.pc" @@ -133,6 +133,7 @@ EOF } function build_libavif { + if [ -e libavif-stamp ]; then return; fi install_rav1e python3 -m pip install meson ninja @@ -140,13 +141,11 @@ function build_libavif { build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/ fi - local cmake=$(get_modern_cmake) 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 \ + && cmake \ -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ - -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ + -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=OFF \ -DAVIF_LIBSHARPYUV=LOCAL \ @@ -159,11 +158,7 @@ function build_libavif { -DCMAKE_MODULE_PATH=/tmp/cmake/Modules \ . \ && make install) - - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libavif.a /usr/local/lib - cp /usr/local/lib64/pkgconfig/libavif.pc /usr/local/lib/pkgconfig - fi + touch libavif-stamp } function build { diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 002dccde62a..4c3f634a6c3 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -18,7 +18,7 @@ def test_wheel_modules() -> None: except ImportError: expected_modules.remove("tkinter") - # libavif is not available on windows for x86 and ARM64 architectures + # 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") diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 32dd694f580..9a3bb6b73c6 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -127,8 +127,6 @@ def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: @skip_unless_feature("avif") class TestFileAvif: def test_version(self) -> None: - _avif.AvifCodecVersions() - version = features.version_module("avif") assert version is not None assert re.search(r"\d+\.\d+\.\d+$", version) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 0660c8e829b..68999084055 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1370,17 +1370,16 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: YUV range, either "full" or "limited". Defaults to "full" **codec** - AV1 codec to use for encoding. Possible values are "aom", "rav1e", and - "svt", depending on what codecs were compiled with libavif. Defaults to - "auto", which will choose the first available codec in the order of the - preceding list. + 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. **alpha_premultiplied** - Encode the image with premultiplied alpha, defaults ``False`` + Encode the image with premultiplied alpha. Defaults to ``False`` **icc_profile** The ICC Profile to include in the saved file. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 7020f3457ce..1447b049e3d 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -164,9 +164,11 @@ 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 - To install libavif on macOS use Homebrew to install its build dependencies:: + 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 @@ -224,8 +226,7 @@ Many of Pillow's features require external libraries: sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif - See ``depends/install_raqm_cmake.sh`` to install libraqm and - ``depends/install_libavif.sh`` to install libavif. + See ``depends/install_raqm_cmake.sh`` to install libraqm. .. tab:: Android diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 1c90f428a09..c92e2534ef9 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -24,17 +24,11 @@ def _accept(prefix: bytes) -> bool | str: if prefix[4:8] != b"ftyp": return False - coding_brands = (b"avif", b"avis") - container_brands = (b"mif1", b"msf1") major_brand = prefix[8:12] - if major_brand in coding_brands: - if not SUPPORTED: - return ( - "image file could not be identified because AVIF " - "support not installed" - ) - return True - if major_brand in container_brands: + 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 @@ -42,6 +36,14 @@ def _accept(prefix: bytes) -> bool | str: # # 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 @@ -72,6 +74,11 @@ def _open(self) -> None: ) 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, @@ -104,10 +111,8 @@ def load(self) -> Image.core.PixelAccess | None: data, timescale, tsp_in_ts, dur_in_ts = self._decoder.get_frame( self.__frame ) - timestamp = round(1000 * (tsp_in_ts / timescale)) - duration = round(1000 * (dur_in_ts / timescale)) - self.info["timestamp"] = timestamp - self.info["duration"] = duration + self.info["timestamp"] = round(1000 * (tsp_in_ts / timescale)) + self.info["duration"] = round(1000 * (dur_in_ts / timescale)) self.__loaded = self.__frame # Set tile @@ -153,6 +158,9 @@ def _save( 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) @@ -199,7 +207,7 @@ def _save( ) raise ValueError(msg) advanced = tuple( - [(str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced] + (str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced ) # Setup the AVIF encoder diff --git a/src/_avif.c b/src/_avif.c index d0bb81f4619..1d4bb74010e 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -218,30 +218,43 @@ _encoder_codec_available(PyObject *self, PyObject *args) { return PyBool_FromLong(is_available); } -static void +static int _add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { Py_ssize_t i, size; PyObject *keyval, *py_key, *py_val; char *key, *val; if (!PyTuple_Check(opts)) { - return; + 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) { - return; + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; } py_key = PyTuple_GetItem(keyval, 0); py_val = PyTuple_GetItem(keyval, 1); if (!PyBytes_Check(py_key) || !PyBytes_Check(py_val)) { - return; + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; } key = PyBytes_AsString(py_key); val = PyBytes_AsString(py_val); - avifEncoderSetCodecSpecificOption(encoder, key, val); + + 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 @@ -336,17 +349,6 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { enc_options.codec = AVIF_CODEC_CHOICE_AUTO; } else { enc_options.codec = avifCodecChoiceFromName(codec); - if (enc_options.codec == AVIF_CODEC_CHOICE_AUTO) { - PyErr_Format(PyExc_ValueError, "Invalid codec: %s", codec); - return NULL; - } else { - const char *codec_name = - avifCodecName(enc_options.codec, AVIF_CODEC_FLAG_CAN_ENCODE); - if (codec_name == NULL) { - PyErr_Format(PyExc_ValueError, "AV1 Codec cannot encode: %s", codec); - return NULL; - } - } } if (strcmp(range, "full") == 0) { @@ -410,9 +412,18 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { encoder->autoTiling = enc_options.autotiling; #endif + if (advanced != Py_None) { #if AVIF_VERSION >= 80200 - _add_codec_specific_options(encoder, advanced); + if (_add_codec_specific_options(encoder, advanced)) { + return NULL; + } +#else + PyErr_SetString( + PyExc_ValueError, "Advanced codec options require libavif >= 0.8.2" + ); + return NULL; #endif + } self->encoder = encoder; @@ -430,14 +441,24 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { image->alphaPremultiplied = enc_options.alpha_premultiplied; #endif + avifResult result; if (PyBytes_GET_SIZE(icc_bytes)) { self->icc_bytes = icc_bytes; Py_INCREF(icc_bytes); - avifImageSetProfileICC( + + result = avifImageSetProfileICC( image, (uint8_t *)PyBytes_AS_STRING(icc_bytes), PyBytes_GET_SIZE(icc_bytes) ); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting ICC profile failed: %s", + avifResultToString(result) + ); + return NULL; + } } else { image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; @@ -446,20 +467,38 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { if (PyBytes_GET_SIZE(exif_bytes)) { self->exif_bytes = exif_bytes; Py_INCREF(exif_bytes); - avifImageSetMetadataExif( + + result = avifImageSetMetadataExif( image, (uint8_t *)PyBytes_AS_STRING(exif_bytes), PyBytes_GET_SIZE(exif_bytes) ); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting EXIF data failed: %s", + avifResultToString(result) + ); + return NULL; + } } if (PyBytes_GET_SIZE(xmp_bytes)) { self->xmp_bytes = xmp_bytes; Py_INCREF(xmp_bytes); - avifImageSetMetadataXMP( + + result = avifImageSetMetadataXMP( image, (uint8_t *)PyBytes_AS_STRING(xmp_bytes), PyBytes_GET_SIZE(xmp_bytes) ); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting XMP data failed: %s", + avifResultToString(result) + ); + return NULL; + } } exif_orientation_to_irot_imir(image, exif_orientation); @@ -498,7 +537,6 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { PyObject *ret = Py_None; int is_first_frame; - int channels; avifRGBImage rgb; avifResult result; @@ -561,13 +599,19 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { if (strcmp(mode, "RGBA") == 0) { rgb.format = AVIF_RGB_FORMAT_RGBA; - channels = 4; } else { rgb.format = AVIF_RGB_FORMAT_RGB; - channels = 3; } - avifRGBImageAllocatePixels(&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; + } if (rgb.rowBytes * rgb.height != size) { PyErr_Format( @@ -679,18 +723,6 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { codec = AVIF_CODEC_CHOICE_AUTO; } else { codec = avifCodecChoiceFromName(codec_str); - if (codec == AVIF_CODEC_CHOICE_AUTO) { - PyErr_Format(PyExc_ValueError, "Invalid codec: %s", codec_str); - return NULL; - } else { - const char *codec_name = avifCodecName(codec, AVIF_CODEC_FLAG_CAN_DECODE); - if (codec_name == NULL) { - PyErr_Format( - PyExc_ValueError, "AV1 Codec cannot decode: %s", codec_str - ); - return NULL; - } - } } self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type); @@ -717,14 +749,24 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { #endif self->decoder->codecChoice = codec; - avifDecoderSetIOMemory( + result = avifDecoderSetIOMemory( self->decoder, (uint8_t *)PyBytes_AS_STRING(self->data), PyBytes_GET_SIZE(self->data) ); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting IO memory failed: %s", + avifResultToString(result) + ); + avifDecoderDestroy(self->decoder); + self->decoder = NULL; + Py_DECREF(self); + return NULL; + } result = avifDecoderParse(self->decoder); - if (result != AVIF_RESULT_OK) { PyErr_Format( exc_type_for_avif_result(result), @@ -815,7 +857,6 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { } result = avifDecoderNthImage(decoder, frame_index); - if (result != AVIF_RESULT_OK) { PyErr_Format( exc_type_for_avif_result(result), @@ -847,7 +888,15 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { return NULL; } - avifRGBImageAllocatePixels(&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 @@ -893,10 +942,7 @@ static struct PyMethodDef _encoder_methods[] = { // AvifDecoder type definition static PyTypeObject AvifEncoder_Type = { - // clang-format off - PyVarObject_HEAD_INIT(NULL, 0) - .tp_name = "AvifEncoder", - // clang-format on + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder", .tp_basicsize = sizeof(AvifEncoderObject), .tp_dealloc = (destructor)_encoder_dealloc, .tp_flags = Py_TPFLAGS_DEFAULT, @@ -912,10 +958,7 @@ static struct PyMethodDef _decoder_methods[] = { // AvifDecoder type definition static PyTypeObject AvifDecoder_Type = { - // clang-format off - PyVarObject_HEAD_INIT(NULL, 0) - .tp_name = "AvifDecoder", - // clang-format on + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder", .tp_basicsize = sizeof(AvifDecoderObject), .tp_itemsize = 0, .tp_dealloc = (destructor)_decoder_dealloc, @@ -923,13 +966,6 @@ static PyTypeObject AvifDecoder_Type = { .tp_methods = _decoder_methods, }; -PyObject * -AvifCodecVersions() { - char codecVersions[256]; - avifCodecVersions(codecVersions); - return PyUnicode_FromString(codecVersions); -} - /* -------------------------------------------------------------------- */ /* Module Setup */ /* -------------------------------------------------------------------- */ @@ -937,7 +973,6 @@ AvifCodecVersions() { static PyMethodDef avifMethods[] = { {"AvifDecoder", AvifDecoderNew, METH_VARARGS}, {"AvifEncoder", AvifEncoderNew, METH_VARARGS}, - {"AvifCodecVersions", AvifCodecVersions, METH_NOARGS}, {"decoder_codec_available", _decoder_codec_available, METH_VARARGS}, {"encoder_codec_available", _encoder_codec_available, METH_VARARGS}, {NULL, NULL} diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 8ee9cd90e54..4ebf1f04226 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -122,7 +122,7 @@ def cmd_msbuild( "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", - "MESON": "1.5.2", + "MESON": "1.6.0", "LIBAVIF": "1.1.1", "RAV1E": "0.7.1", } @@ -673,21 +673,24 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> None: lines = [r'call "{build_dir}\build_env.cmd"'] gha_groups = "GITHUB_ACTIONS" in os.environ - scripts = ["install_meson.cmd"] for dep_name in DEPS: print() if dep_name in disabled: print(f"Skipping disabled dependency {dep_name}") continue + + scripts = [] + if dep_name == "libavif": + scripts.append("install_meson.cmd") scripts.append(build_dep(dep_name, prefs, verbose)) - for script in scripts: - if gha_groups: - lines.append(f"@echo ::group::Running {script}") - lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') - lines.append("if errorlevel 1 echo Build failed! && exit /B 1") - if gha_groups: - lines.append("@echo ::endgroup::") + for script in scripts: + if gha_groups: + lines.append(f"@echo ::group::Running {script}") + lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') + lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + if gha_groups: + lines.append("@echo ::endgroup::") print() lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines, prefs, verbose) @@ -830,18 +833,19 @@ def main() -> None: print() write_script(".gitignore", ["*"], prefs, args.verbose) - write_script( - "install_meson.cmd", - [ - r'call "{build_dir}\build_env.cmd"', - "@echo " + ("=" * 70), - f"@echo ==== {'Building meson':<60} ====", - "@echo " + ("=" * 70), - f"{sys.executable} -m pip install meson=={V['MESON']}", - ], - prefs, - args.verbose, - ) + if "libavif" not in disabled: + write_script( + "install_meson.cmd", + [ + r'call "{build_dir}\build_env.cmd"', + "@echo " + ("=" * 70), + f"@echo ==== {'Building meson':<60} ====", + "@echo " + ("=" * 70), + f"{sys.executable} -m pip install meson=={V['MESON']}", + ], + prefs, + args.verbose, + ) build_env(prefs, args.verbose) build_dep_all(disabled, prefs, args.verbose) From d76ae2f10cc2d2d8185db3cd26ac8e378f3278d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 8 Dec 2024 04:28:31 +1100 Subject: [PATCH 09/22] Do not ignore SyntaxError when saving EXIF data (#8) * Do not ignore SyntaxError when saving EXIF data * Do not save orientation in EXIF data * Do not save XMP and EXIF data from info dictionary --------- Co-authored-by: Andrew Murray --- Tests/test_file_avif.py | 31 ++----------------------------- src/PIL/AvifImagePlugin.py | 25 ++++++++++--------------- src/_avif.c | 15 --------------- 3 files changed, 12 insertions(+), 59 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 9a3bb6b73c6..cc9f0a3a088 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -329,17 +329,8 @@ def test_exif(self) -> None: exif = im.getexif() assert exif[274] == 3 - def test_exif_save_default(self, tmp_path: Path) -> None: - with Image.open("Tests/images/avif/exif.avif") as im: - test_file = str(tmp_path / "temp.avif") - im.save(test_file) - - with Image.open(test_file) as reloaded: - exif = reloaded.getexif() - assert exif[274] == 1 - @pytest.mark.parametrize("bytes", [True, False]) - def test_exif_save_argument(self, tmp_path: Path, bytes: bool) -> None: + def test_exif_save(self, tmp_path: Path, bytes: bool) -> None: exif = Image.Exif() exif[274] = 1 exif_data = exif.tobytes() @@ -353,7 +344,7 @@ def test_exif_save_argument(self, tmp_path: Path, bytes: bool) -> None: def test_exif_invalid(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") - with pytest.raises(ValueError): + with pytest.raises(SyntaxError): im.save(test_file, exif=b"invalid") def test_xmp(self) -> None: @@ -362,24 +353,6 @@ def test_xmp(self) -> None: assert_xmp_orientation(xmp, 3) def test_xmp_save(self, tmp_path: Path) -> None: - with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: - test_file = str(tmp_path / "temp.avif") - im.save(test_file) - - with Image.open(test_file) as reloaded: - xmp = reloaded.info["xmp"] - assert_xmp_orientation(xmp, 3) - - def test_xmp_save_from_png(self, tmp_path: Path) -> None: - with Image.open("Tests/images/xmp_tags_orientation.png") as im: - test_file = str(tmp_path / "temp.avif") - im.save(test_file) - - with Image.open(test_file) as reloaded: - xmp = reloaded.info["xmp"] - assert_xmp_orientation(xmp, 3) - - def test_xmp_save_argument(self, tmp_path: Path) -> None: xmp_arg = "\n".join( [ '', diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index c92e2534ef9..2b205c4a53a 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -168,24 +168,19 @@ def _save( autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) icc_profile = info.get("icc_profile", im.info.get("icc_profile")) - exif = info.get("exif", im.info.get("exif")) - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - - exif_orientation = 0 + exif = info.get("exif") if exif: - exif_data = Image.Exif() - try: - exif_data.load(exif) - except SyntaxError: - pass + if isinstance(exif, Image.Exif): + exif_data = exif + exif = exif.tobytes() else: - orientation_tag = next( - k for k, v in ExifTags.TAGS.items() if v == "Orientation" - ) - exif_orientation = exif_data.get(orientation_tag) or 0 + exif_data = Image.Exif() + exif_data.load(exif) + exif_orientation = exif_data.pop(ExifTags.Base.Orientation, 1) + else: + exif_orientation = 1 - xmp = info.get("xmp", im.info.get("xmp") or im.info.get("XML:com.adobe.xmp")) + xmp = info.get("xmp") if isinstance(xmp, str): xmp = xmp.encode("utf-8") diff --git a/src/_avif.c b/src/_avif.c index 1d4bb74010e..ac2ec283bc1 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -170,22 +170,7 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { image->imir.mode = 0; // ignored #endif return; - default: // reserved - break; } - - // The orientation tag is not mandatory (only recommended) according to JEITA - // CP-3451C section 4.6.8.A. The default value is 1 if the orientation tag is - // missing, meaning: - // The 0th row is at the visual top of the image, and the 0th column is the visual - // left-hand side. - image->transformFlags = otherFlags; - image->irot.angle = 0; // ignored -#if AVIF_VERSION_MAJOR >= 1 - image->imir.axis = 0; // ignored -#else - image->imir.mode = 0; // ignored -#endif } static int From 9ad83119cfd0685559b722c0ebc343217d6e854f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 8 Dec 2024 04:42:38 +1100 Subject: [PATCH 10/22] Allow libavif to install rav1e, except on manylinux2014 and aarch64 (#7) * Allow libavif to install rav1e, except on manylinux2014 and aarch64 * Allow libavif to install rav1e on Windows --------- Co-authored-by: Andrew Murray --- .github/workflows/test-windows.yml | 4 -- .github/workflows/wheels-dependencies.sh | 67 +++++++++++----------- docs/installation/building-from-source.rst | 2 +- winbuild/Findrav1e.cmake | 10 ---- winbuild/build_prepare.py | 19 +----- 5 files changed, 37 insertions(+), 65 deletions(-) delete mode 100644 winbuild/Findrav1e.cmake diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 1430e91e137..fab90454a54 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -139,10 +139,6 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" - - name: Build dependencies / rav1e - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_rav1e.cmd" - - name: Build dependencies / meson if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\install_meson.cmd" diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 69a2e3c34f3..a9fb277e50c 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -97,48 +97,49 @@ function build_harfbuzz { touch harfbuzz-stamp } -function install_rav1e { - if [ -n "$IS_MACOS" ]; then - suffix="macos" - if [[ "$PLAT" == "arm64" ]]; then - suffix+="-aarch64" - fi - else - suffix="linux" +function build_libavif { + if [ -e libavif-stamp ]; then return; fi + + if [[ -z "$IS_MACOS" ]] && ([[ "$MB_ML_VER" == 2014 ]] || [[ "$PLAT" == "aarch64" ]]); then + # Once Amazon 2 is EOL on 30 June 2025, manylinux2014 will no longer be needed + # Once GitHub Actions supports aarch64 without emulation, this will no longer needed as building will be faster if [[ "$PLAT" == "aarch64" ]]; then - suffix+="-aarch64" + suffix="aarch64" else - suffix+="-generic" + suffix="generic" fi - fi - curl -sLo - \ - https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/librav1e-$RAV1E_VERSION-$suffix.tar.gz \ - | tar -C $BUILD_PREFIX --exclude LICENSE --exclude '*.so' --exclude '*.dylib' -zxf - + curl -sLo - \ + https://github.com/xiph/rav1e/releases/download/v$RAV1E_VERSION/librav1e-$RAV1E_VERSION-linux-$suffix.tar.gz \ + | tar -C $BUILD_PREFIX -zxf - + + # Force libavif to treat system rav1e as if it were local + mkdir -p /tmp/cmake/Modules + cat < /tmp/cmake/Modules/Findrav1e.cmake + add_library(rav1e::rav1e STATIC IMPORTED GLOBAL) + set_target_properties(rav1e::rav1e PROPERTIES + IMPORTED_LOCATION "$BUILD_PREFIX/lib/librav1e.a" + AVIF_LOCAL ON + INTERFACE_INCLUDE_DIRECTORIES "$BUILD_PREFIX/include/rav1e" + ) +EOF - if [ -z "$IS_MACOS" ]; then - sed -i 's/-lgcc_s/-lgcc_eh/g' "${BUILD_PREFIX}/lib/pkgconfig/rav1e.pc" - fi + rav1e=SYSTEM + else + curl https://sh.rustup.rs -sSf | sh -s -- -y + . "$HOME/.cargo/env" - # Force libavif to treat system rav1e as if it were local - mkdir -p /tmp/cmake/Modules - cat < /tmp/cmake/Modules/Findrav1e.cmake - add_library(rav1e::rav1e STATIC IMPORTED GLOBAL) - set_target_properties(rav1e::rav1e PROPERTIES - IMPORTED_LOCATION "$BUILD_PREFIX/lib/librav1e.a" - AVIF_LOCAL ON - INTERFACE_INCLUDE_DIRECTORIES "$BUILD_PREFIX/include/rav1e" - ) -EOF -} + if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then + yum install -y perl + fi + + rav1e=LOCAL + fi -function build_libavif { - if [ -e libavif-stamp ]; then return; fi - install_rav1e python3 -m pip install meson ninja if [[ "$PLAT" == "x86_64" ]]; then - build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/ + build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 fi local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) @@ -150,7 +151,7 @@ function build_libavif { -DBUILD_SHARED_LIBS=OFF \ -DAVIF_LIBSHARPYUV=LOCAL \ -DAVIF_LIBYUV=LOCAL \ - -DAVIF_CODEC_RAV1E=SYSTEM \ + -DAVIF_CODEC_RAV1E=$rav1e \ -DAVIF_CODEC_AOM=LOCAL \ -DAVIF_CODEC_DAV1D=LOCAL \ -DAVIF_CODEC_SVT=LOCAL \ diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 1447b049e3d..37b7d1e9d28 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -226,7 +226,7 @@ Many of Pillow's features require external libraries: sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif - See ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. .. tab:: Android diff --git a/winbuild/Findrav1e.cmake b/winbuild/Findrav1e.cmake deleted file mode 100644 index be1618bd4e7..00000000000 --- a/winbuild/Findrav1e.cmake +++ /dev/null @@ -1,10 +0,0 @@ -file(TO_CMAKE_PATH "${AVIF_RAV1E_ROOT}" RAV1E_ROOT_PATH) -add_library(rav1e::rav1e STATIC IMPORTED GLOBAL) -set_target_properties( - rav1e::rav1e - PROPERTIES IMPORTED_LOCATION "${RAV1E_ROOT_PATH}/lib/rav1e.lib" - AVIF_LOCAL ON - INTERFACE_INCLUDE_DIRECTORIES "${RAV1E_ROOT_PATH}/inc/rav1e" - IMPORTED_SONAME rav1e) -target_link_libraries(rav1e::rav1e INTERFACE ntdll.lib userenv.lib ws2_32.lib - bcrypt.lib) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4ebf1f04226..c199d8889b9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -124,7 +124,6 @@ def cmd_msbuild( "ZLIB": "1.3.1", "MESON": "1.6.0", "LIBAVIF": "1.1.1", - "RAV1E": "0.7.1", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -400,19 +399,6 @@ def cmd_msbuild( ], "bins": [r"*.dll"], }, - "rav1e": { - "url": ( - f"https://github.com/xiph/rav1e/releases/download/v{V['RAV1E']}/FILENAME" - ), - "filename": f"rav1e-{V['RAV1E']}-windows-msvc-generic.zip", - "dir": "rav1e-windows-msvc-sdk", - "license": "LICENSE", - "build": [ - cmd_xcopy("include", "{inc_dir}"), - ], - "bins": [r"bin\*.dll"], - "libs": [r"lib\*.*"], - }, "libavif": { "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", "filename": f"libavif-{V['LIBAVIF']}.zip", @@ -435,8 +421,7 @@ def cmd_msbuild( "-DAVIF_CODEC_AOM=LOCAL", "-DAVIF_LIBYUV=LOCAL", "-DAVIF_LIBSHARPYUV=LOCAL", - "-DAVIF_CODEC_RAV1E=SYSTEM", - "-DAVIF_RAV1E_ROOT={build_dir}", + "-DAVIF_CODEC_RAV1E=LOCAL", "-DCMAKE_MODULE_PATH={winbuild_dir_cmake}", "-DAVIF_CODEC_DAV1D=LOCAL", "-DAVIF_CODEC_SVT=LOCAL", @@ -804,7 +789,7 @@ def main() -> None: if args.no_fribidi: disabled += ["fribidi"] if args.no_avif or args.architecture != "AMD64": - disabled += ["rav1e", "libavif"] + disabled += ["libavif"] prefs = { "architecture": args.architecture, From de4c6c19760b848cc37f6b3056c357080e1d4226 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 8 Dec 2024 04:46:08 +1100 Subject: [PATCH 11/22] Removed ld64 flag (#6) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index a9fb277e50c..c38e8ea7453 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -169,12 +169,7 @@ function build { fi build_new_zlib - ORIGINAL_LDFLAGS=$LDFLAGS - if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then - LDFLAGS="${LDFLAGS} -ld64" - fi build_libavif - LDFLAGS=$ORIGINAL_LDFLAGS build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then From 524d802eda37e23bc1f438b205f40e95ba37aeab Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Sun, 8 Dec 2024 22:00:34 -0500 Subject: [PATCH 12/22] fix: set exif orientation from irot/imir when decoding AVIF --- 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/test_file_avif.py | 50 +++++++++++++++++++++++++++++--- src/PIL/AvifImagePlugin.py | 23 +++++++++++++-- src/_avif.c | 47 ++++++++++++++++++++++++++++-- 11 files changed, 110 insertions(+), 10 deletions(-) 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 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 zcmXteV~`*`)9u){?b)$y+p}Zawr$(CZQHhO8+V`g*4L?|PfmAr=TEAVbN~PV2uz&Z z?etyDO#uGMf7;sIgu&We-$X`$K@b1{5X{=xN&i3FKcO%)vU2$U5CFi=+|cR&@PFFM z+~EJhz}cBQS^v)l_;18Bx3V?-ZzlXN+`snU2mnY60Kk{?4^x<%+x##0|14PlCdNSj zIsfy~ccEtxvbD1PUrHZyI|sXeytcWWq3u77V(w^X{Ga1L*Gd2YAisYA!Oq;x{67X5 z0s`Wng3)(j5b_6v`Hw<2w6$`uF|=~~7a0}+%ZT`O`nC5oY`u~Xk zmughZkgG8zDY8ZRpa z0)-eM%^S%v6%80cJ%q*kvqggm+^W-?TtzAwnd^~Ud37^GUp3JeOr1J+?~f?k zBUONnJ<#u2*0<~HscK^wq{$#kZqwxluE0bQBNiIT3rLI{a<=$#BSM)hx0%2H>rx& z{@}4(F307eAW^#{J*2I061%r8)RF2pQEJc6l0J;Ybf5kS`-c9Ar2{Q5F#M}Ok{RvwH}&3$ z$i|eGtq!Af`uR|k_R~WtKZ380uCD3FeOIM`zN*r#pVvT`cSyq5)p|D;x0|@oc*`JK z%Q=P+qm@;q!G_nk2;8&z=+CJtiZUFHkW;$I5^@r}8Tu{*uzGqG^aEeeu;_xIF&QYO z{sWP;ef~`4!>4g96+OVinYL26EjVpZ)RJ>m%%FKZat(2irA_=W=j^A35bGNMi(>f@>=^N+{$=^Y^AJcR*LIK_Qz(DTUM>(5JW) zFXG60I`!uv);aZMC2CpX610b!z<-kE14;Op!aiiS&TUZ5ZG6iOrr~y_a7Cs%`+q3P zMH}KKhFYmDP`NbItlz|kukvNhSy*P-LGXa~M4CdB%E&;ifAh+L8B0h(852TSeJ#9K z0{(U<!^)Xt+)18%<0=T3p64Z#_^Akb(9C9{aPzDi5T7cxTgsFw^BQ zO(P&REbCVuLkYb)5ZLMK&G+1QPaj3=NkT-zCf#TQ7wt3TA1vecvNedL)CO=Ar>IKe(NY*ePv9gDhUG2^d9VI?CRe=;)Eu*Cqy1HG+VbXu+B}UvpvNQ3QzTrsnj_|YkDZp`;ulPOZ z#a5^UY%hH9hPL*~X%!7-_{+o-#Gc%B8b^OQk1MxLibAeDNruvA=rY|2{q!1r4utd? zVP5k(k|f5RXB%<3_As_}f=M{Od7y zFQg*73&APgEPLoQWK!KA=dngw!62UF7`BjfD^2V`{SmFd3HVkh&_y;D)ka%-Lu04q zMW~PZN;1a!<*vp_yHymHBi-sp@;cS561P#ePzizfsfIktkMs*bDD4ywh(@_t)Dy5q@*AaUSw zN-A=<{lVA$jYIvhNw!I_iJRH}^4iPg>0&tnw&jICZGYZHPA}!^mMJiZcDv&~ULNfj zMzPi4Zy%}%tph8_0Gs6$>e7ZpTHkR{BvC&ei;YEJD-j_piTBiBj0YqSkdJzh)Q+eq zWW|$-;pOf$VWQ!wjz8~$hgK`2gKAe~LR3qx-0ajQcvzjI5v~(YThkX(`%FVwl8!L_ zaXxpa@k{I^(CAD`V}o}#JHfZsZNpN7J0N<#M%W>b^bRnFH0CuGWMh>h8Y$yXP>D%u z-JCz+cL5B&R7sS`EK#d562QWICX8P@kXlw;@c?U6XiZ!!{<=FV4ZD~hK6m)%OXV{Z znJv5*r=8nmCFj&;{0(x()oYEEgut-z%)Utz>7J!B zY0Z@)?CR!>C8#pDyA`n-z3{EC1ZfKpukGt)6EOV}6zExklR~g}Ahos=eh;-=SGFFK zv7G5s6a0*44jpOmS$f6#_1F;ksZr_i_^DH-EA32v>j1&EJTZ?n<}6WvN|b0cqhj8Z z#)4K3U@pN~h6X_!Rar`0rRJr;DiMhY44u|pTMYOV)YO_FQY&;Zbb7*tPtoP0uYjP0 z=$W~5h@I7dWfp>NY@L8VB*5}v9r%Mw;C%X7A`aYNBXIM&SxXr{l=yK~J>1>LtU%6b zQyQ{9od~#qYad5kVtQFNhIxe2pXH#B@T(w!dMa}Lws&Gwj7#HGjv|~WVAdP znf>K8H*>k0Y$)c5^npzM*3S)}GCSYuE+ZrDkF?cf%0=AG>&SbLf=>I+*QoAuvM*=m zKgb^1+6DxX^fg&3&WHlB4oAkxtaG>i<_BOxqY|V7_=nSVPt@&EN*IypF6FM7EO4zk zu^mdMN~H0Ca%1Bl;%yYUlxd*`0(>~yt0X{LolqMnwuTgq2GE}1HQ#k;@21ovm5FAB zNU?T7tYMox3@^qrGXc4bBq=VjLwj*Cwcuq)KO>KxzKmDinWTagLFn1K5*T&$sxq!U z{|3VduC>oi_UF~uVL+F3$-aZRNkoI|WD~1{sSm^9YZ0kO(Pb6sz=-Ulkshv zvUcED&h)!S8%1U-lyH4Gk`IKGE&<(+QSnxvwa zC^C!f{fFa@z2Vs|&qTOj82>W*%10ZdG}NX}bpq%K0FEsfTC$7cK_PfKER4L^qoxi* zNWEuQUo2V~y5l>oYo>O#f|iX5m+uD+vy?Ed*Bfk-ehus{wJP9HibmSKEJcIyABF|y z!#p%sshSAQ7uVXtU@l2pdZ@g#57Rx`pn`Q-WIHz)a z4v&Y0{7ryk4PueGL3C^D5Zpz|-4g|N2$XHnBTnF0oiW2McXOjW1@Uc~m`HD$J@3w& zhZ_F7n6w`qR%62pSdO3$<+o-_&Xc9&E&6fziJ)P6iq;4cecfXg&>B{0r&MZVm21qsE83LP-&CJ`y;gAB+3N97YV$S#dv8_Y9} z%4N-DjVU}&n~=kv`=wuTx0HPH9F2!R~Ky-)zC?i=XBL=VHfF9g8--!d{YBykf=ol@i&+T2tBC^j;l0rBRCI zNjQ>PD-R4C!z_(sSmc}OBBu2x-NBNJld*d!lAiF*bRErH+ACBWX6xi^=!MC9l#4Tk zAq(nB@y%cEFL+&y;IM%Y@pkt6<_q+7qj*kW;=+d@Y7K>V+SbG20wBcmGlpp9BzMQF zT{FIy<+Rt`%dMxWoq{rAsjAvhz-xIc%nBlQG}NCw*%x~aEWCq{kmg1itvF5IOI{KX z1V^d8yA>@VbF8MFFSLQ~YswrSU;BVYKk6_~0VAbigMg&4*8-DgA(Z@R#^SrXAu)g^ z>5QXufJ^T}SwD(I&)3XrD!)gXKt~bN)uJnGZmrs&eQ3spFr25$QNt)4`Sx{m<;TsL zk($ao4)~HcC-J$8(nuPzm7|NK~f{m&S2fq=gQnaIcP{+vA=nVzXqc9jb2* zFP66qs*0}$`0cVWu(1W>O!vU!~Gd;Vv^-0nCLx-nRDR}D#A zyS>3PvlQmPs)rou5I9YQRBR-IH!W+a#)R^OqnwzT_6uaT-0D&^?q3RwV=S`*pujZO z{L~|J{6>PpL*4caK2?d&Ad6|G+2tVD(B3bdDREP$N}In`Hv5o^!J0b?jw-sxXLO9etUQdNf?BTRm4Zb@TG=6=M<{b%`^TflQ_DtwB1 z{5^R7UDb1<{YjO-8{&9#QPJiFS}zjX$s;`BaSJNlHw07f2>L2Ki#RW88XI{ZRhSxU zJMm%?S;KHpEh~7wex_HD{0aH&H%5gT^)AK^YWWb`WZE!&#^~{_zxbZpxw$_Jw~oxH zjE>kB0?e7HMo-vhF6Y&^<1ZJnMU4@ApstjwPU0UG2%Queg+}ZYLt#@@g3W&%-(lmF zEy3JuYS2=hAi?tVW6*M(9ky7X%qvu;(yT(kOH-Bi96a~qb4KKue@X{Zt^kok_kylZ zZ?a&EgzBgQa57lr~$2u9#4+w-YR4xlocd_ zaXEH_*VIS9Ux5>4KMLu#Zyt`72OZO9aU1fl;*=ef1?JEQ^a$zcR@}}R2B!{9uD1}~>WW*0q53oR zm8GFR9?NCVaD1=GSLBUv)fS(?!E`WuOH4s3<+&=`OGeR=jRX&~d0m1-#mIyLdckly z7FlJaqk>sr!2L8)E>BD=o7)^6;X1C1iA(;4pcso6Ah=QRrR&cMD(tfU#ZmdOHFja@Tpp5=~sZ z-^^8FlWB5et627F1c8Q!$yp=xAv=2#bLa~*Q?V>)ZKX>mj0^QD;j!l)9P#%F3U^cC z89ez?02w=PP4Gzyh$z zPQcTw65c$I>7ch$g5nS%X|(0)<9e5TVgkl&8p`XTEK8l2klKKhlZMDgm{AcWCa7Yi z-bY06dt_%Y$hHB1b{?})~f-Dr2Xle-1ANC$s zd@4yGFj}}_9pf18eOx<#S~<0n=Lg9$l!gru#}aMDC;*8V(+%n>NZqTHH4cSNU!`ZIbHmc$Hf-X_@%eNXF^OY`Inqe|B0{`#c|B=; zC^PK)pxL*j9L%pw4_>n!WPwHl=4^?hIu}<5y*cW%JyKgoBK0=I;f(9L2qkc|gOw-& zO*%h2{R6k9TSxPoAiCBS=#GCV1_hBvBRx_U1tM9dkbQ8v@Pp_v`?%BmV=QQsN`rM- za!Sf^*Gt9YFI`@ScG>RJb1XYymg-AIc=1KQFs3J(TOfy+weH^6(E35Ll=mI~vAN~l zKyJ1`c8gi$)YyJC1G!bAk@c9IQ?1^H_IgeiKQYzr-eV`7sP6=jN@w$y%Ruv7)tLT< zfP=JFoGL@mr1^;aB*(8C6Jok)bZfSvT9=x_7(a}i5SkiQ1`R_V%9$M8UioezEsxS+ zkEI_0`rTnf;%bA_Q?(G08UT;%T&-N2@|)nT?)(;3Wu70TU5mrUyLf`+;oaPuC`6k@ z9O>({IakK?qln2`RJbHeZdO2%=b%sj{AY1CsV9F(m~#|S>~~CVgtF^&URoW*sl-Nl z4W~p+3!v8jRIyB9la`5>aaE=1ub{@nmEp*1WSXxnty)|4fmKgcPtuYrYCV`uESyKJ z)C%gJ#$$GeYkm1x+j3QfwNsYWs%$`~%JzH0vqP#D$!W+QNy46(G*d*Vt`wCO+kFC; z0@WqKU-5e9D$2hyhFTk-5U&}dB|yi=>jIuoPCEImGct+c7hX+PA-tdfTd0eyaUX^P zjUx}??@b2CL?`jc@uze;1wysTsqpBKbJAPp7lDMqo#}i6iI)NXm5A<*UqjATIK0$R z%5hYe1L@sIAk<-J8T=KAA*P}sQ}JjX1$HLM&u0Ai-cnZCBTS-ri&NMgXw~ObF*ub5 z3AFW%$yE?UvRU1PFY~+$hwqrdN03IU!Ey@~smZ6uv)AA8kuIKkn4Tz+BZPNyOSfW# zbHi)g(q%~q8JYyy!}7?zlQ%K8IN>giBH(nuS7wFjFE?SZ9B}z-@q1v$1c^|#s z!)U}Kr8BxRf5?+-n$05AWD6#B`X)8f?Ks_*+Sh_&g0h&tHQ`BqQYCJjwhnT0Qu)d} ze52o%ta0a8mkJlIZs3A|J#Ou;11NwSLsDl0}#^ZY>&WNCk5u6xIA5* z*1h>hziZQ;)1W=OnxK_^A#N*{okc}yQ@|L%#Q=aHc6qbrJMXOaE#YEiggf_`AFMvZ z8Z1Y&elJjSDC$5Ch-3!vnmeOo;Z>nuHCVay`qILSw8E(IbIOx=FrR!PrBsUGTG||{ zXL`e(JN|}2wf+d_&>b(#Xw2ar&GLMj6Q-jMThrDAOqc5u*+nw0Yi&J~eZNn_0 zQ_PMspV3K!A40>%Q7(N#(2y_o3_`=32P75D(M!`L%+CgQx3xaMtCq|VO3x8XF)5Q63GH^Dxj^9bL84(S$qBy-lJWl^O6;0dXz&2O|satES6Xdm`n`4vE*esL^mY$olzkyXj~hjX=wN zS-kFp4Pn02(0LF6enBl4kDzN8Dbc=C+y^MVkwL0L1WD3*dWKe5*SO4hLQO#i*({93 z?BsPTs|HHhcgaGY0DV*S!2Ky10+X<2y-==J@Fwc@c;qfJJ&ME zfvoq^XdvD1Mf>jl$Gtn63iAmlU52@c*v+Tj$%zl$XC^d{nNiNzWG4^}O6#g|UxF*j zZQ{kVfyEZ@`i&fh1Z9JypB$6L=LMz%`(D}je9Z7}#x>^CD$g+n5~MI4y)|dYR*WLC z65clnr4ylObEZ`tWz0 zyYjk#^m=EdUJq6|1ODEjP^vT)K%XGK8S}VRR+=M4-cojk3&h1;HQA8#$~DcM+=z8Q zA}Yt+{#^2_3y3k&w{@&3iu<6N*xdi2xbg{b=8WC}FX9cDYYtc3Z>#Gv@O?-(`Sz+q zz55CsKbjKkC%{ib@C?Nxy-VK5J_C~#09gXc3N=PQ3P@ULm!1~Za~)Iiy1DY;qTaXf zLMp!KfB*QtdMtDxWp>n3a*2P^I7ry&7{60~uVrJ8#REyO{d4SR6vTJ8p~BvFS2Ws zcEcKb$xRGu)W_~&;j!sh&7yU2Y+)6|< zdW8=3;*V1M!UAd4#7AtB!Hf&M`yUhV<{w_L7PvvdFgGo(@ zW@=cZb8RE>i7~%Us&O+vlR29TYb5-ZAXr?#k+-nnT1W1Za@|?pv=*uW;>ybhIDjqN z2VFJ>%n#1?xm@C^C4@u=mt^%jD&;gnM2H<8hYKFuV+%*r)hy)&u*>^5OB@WU4{2_4D%Lv*E~=Hc<)1yh3`{`Md{VRlp>xj}xlnxkH`IiRc2h?0Q3|5%Wy5ZlwEQ7^& zcwPZ=d|*B^6xIt_vvA&RPPzJgv#3=@p5DJheVbHlVkg9$v9eJYfUNul&@A?+o|k{u z#<2oArViVQDjvKK?Yx>qIS#qj#mumB3xXUTPSJYySIe8cy~uR|&|ogKUSqHwGIDhj z#`sjaa56(fyfGS!12dm=Z32a60ea(;)ox-pyR+uf%FBd%2elQ0c46G+Mc^Ki!`-Q@ z!(+|hn^``_iEO zLufPSfR1v$cZQ=Y#PTPJZC?r$I;7FrpMNj4kmpzo?hMH){cIENGBZFhQ=h4&q;`nd zLXf6UMtS>(>xHzSC$j868%HM-wa(PDrT3N8ch0kHb087JzJ&188x;c8I!JQLOU~Wm zkeuk?`Pp=UlJSj{-$WJr)K+dRO8hd|Q${EH<01*^ijVZ4r_^#`(<^M>Je)Z0y`goJ zC35iFYh91w??hRHGoB>vKXfo0QG`>Jiw{|fH&)=WD}_w+Z%>{;s#Jact!3~#fyv3Q^bpOEce1Orcpvx3Pd9_l&LE*LfxmAZ4;O6bLj}KitAGcM9uk@ zA7EN(A-wdmC9Ga`f-}Idg4*Dg?W&mo`|~WX(gGoV(sI4=K$A@;PJG}aKo=VEEie=T7@ZPoH6c4hl z-{ujFJ#yw{q|N?4?EhUJe-VucmOmZPz`CEDMm%ZC!6a#$|1y;WEwE1hHXWaehnZ2l z9F>Q9Lt@^oqtMZ9I0#%YBYL7%s_A5nb!Lx5OH#QxbFXv(7M!}*E_Jwc0chEb7&vjR zB`bt!^_1sEB*2ERJ`r}++HcANAPo3SU5mOXXf^@Qr^M?R^(i+bi9no7I#Y%|M`jiz+ zBr8j4S;$C1EkUF|SL{w1)xZnH`Hq-vyRJZI&C~PjP?beb&Tvb_F{$5oO$^I>fFmH(+z%1ZL5n zpqqcAtujipA%nL^C#tR|rY4BiN*h#8`cl)V%N-P2WT*dfX6-K+fK!dPZ_LcNa4RL8 z=Lp~rb%71Cm>)btmhJ|=HISp;RZ14OPi3)q;g5$boUU|B&%mD=e1;w#F_SJCV~9P+ z#58(PnzQq<_yx4jU`U@$(?()GdSoa+j;73e^b2aNh8Uect$WdYambo=2!%#Z`uV|Z z;e`yh6DSgpRd7{ zo-7G{8^7zHZz;H*cw2pnwTPhbj`-RPFCp%|G>`EPnpCdpM@oD4)>f?gFh13tzUt7yUL4O zBWho}P<;P6mt=_fQ8>Ju7z$f%E4!4q#CDkxtOi@kt9;Q&b|G<_tcWevh;M2OD2S~m zB?6pcY6epf%tGuukZLg}Yn`qp3sdGvD}6XrWw&LFeb7=J+F?cAw{RI*(un8x#F6n3 zrSC!Cy_(*r*Lbor5@#7yl@~V!L=73Wd+VGN{|)*D15T=8$XBGU11^vJtS7|xySeEG z8v_^;hMO<)0atXt|IsweSHF|)N&MpC{#zJMCku7p;43tu?=7$;Sw!|0BEAR=V4K>zM z0^tK0|4`o6jm}^0YyFc%H20HxKsIP|MlL`rdMk()cMjuHhY+(Ie zbRsjaKSex(TT4#xbScmyexyA>&?VDg&rjVNy4mb)Eg`yWHN@Bd9t%9#8zco<8}FxtO^S~2{1Jsaw(qDj^Pa-3v*ZxV z&!EzTm(P^}rO>5Izq_Al^t5ILn^gWJOq<(`j*vkzIBj};ghIPWTD*!$PW zcS6gXVH<5iC-XHuO1JEs_#Nq~e%4Pt2IOR=vpgvSl4M@Hcf5L))dEU+@NUpbX74U% zosag_)EtZ;D+>qp3p^2&RS>VmrRpt2U#U?=S~XV_Zz_l_kFVR$AZey4DDMs<*O4d? z&GsAf5p|p58Z_U{k4ObQ!(PHV))+l_1YmRm&lMsFCa8jc%R4k!$vBH=0Th^&Z4(LR8}-qsojwuvEVVNk|!BYtN? zdvFdIsQr&3UfBDmYEp~?J*9LD*YRc~O3!`pSg`{IOXHuxp-T}+QySCTC9go=S`9~ zhoi|*Pe=t#$avel;TSb$;0{CCbVTe<;o|D!oQ`PLzD$K^#Sv+RUblq>U+0N~$=uM* zxB8$NRyv@ekO4CE?oQH90L_p%Q(^qr=de6opLsd^V3a)oO$qK-k z`qXY0N_sWYO+qb{mf1Cy%B2Y&{Hy8p<4dp>RbLp5NQ2-zQWHjOta(36h{MvDtuh^@ zBU|K$KtEU0Z;tAL2m<03gyGQmV)Cv&i+USSa(bAZO#BE;q-l~S>9^8j^FK7LJIs`F zZQRD-Ps2_n=7R(vR+%Z;t7gb`KoZ8edMcQZ>~H5qgUh@Ud7d1{H8E$^ftsifqA#HJ z_|PL0+DaHRPx}-Me4P&Y+8lA!YU|-wO3X7Vr;ZPp^*gR|&8)X%);5;>M_69hj~H}t zx2+CSDfI@^HBLd|YfI!=#;#6Gcx0yZSeQ9$kwG9!taooAk$86BwNNdXEF!^_N-CC$h zr?RMTAB}D_gr~>LL4XA9GedrlU(TZvg`+H&5`BY<<2lKQ@s%=jnlfsDz)dj zWMO;1J7cda$r-9zOX|D@AucUQu6fcgSehFBYWCrrOr=y{tnP1Bcqx8+K6?9OIdHEmdn7i|Poj*EYLJ+QLT^pzRfxo0dLPfmaj}|POuqI!#-agv6hoVfckChAyoxvEv zFSZLfG$73j5c-Cx{(KxUyD5bz&oZDllvsaNv_Y6Orw-)5&+}akIBeKYB0Xb)X+DE zRHi9r7x7thUt8OZB}0R6E&*Aw@>fc96_8*hs}OqR-1TBeLtXj7zO(Kz}#>6oaVEZMK9i=Fhh-iBDlJPue(YaJshshc~ou^m?wqX_#?$ zP@z6MqLk{+Pw(sgqXX5GS>oms%CCkal=k6zyRlB9ZmzSJ9g%N%FescY)pRWIch7?{ z3GxkSV&w*(%3s`j7K)EG&4U9k{9Kz0_Tr+^sUEQNpesQ|pT8q@tk`0-KRP^=)qNWEVA%_H0BTL@R*4x4E9)mP-Kt}Aa!08ppQC&w zy+#A!As((w?H-CsWkX`y3|9TgQxz^DIMTV{ZW@$rG7C=HF2)BY6kOey>9wwOHxsy` z$a)5le!QvJ$qihr`;U$Sm@>#lRZk`{>w)|s+FiWs8<()y zt)O!{*O~k;vg=Vi!DLv659Er31Io|Wc>3SB{AEyU%ZhN%<;`K_dp`sT<2UeCiy3Js zx-OaYKUF7Vb+d?#CG4<*trG2Q1Q&auB+o_l?-*K+Hep4F1tt({mfuRo57|^xN;2iu8_#6#Z6?vL0PL$ zl0gE~MG&}qQFDS&$@*|^_$msWw4U2jFGQ03dn39IcBao8f%zn^x} zh7Z&uegN>jZ5cR>?bAIWX@u3LaK+(r9@ww^f`}uW{D9EDcOS~ioo4MrSR47yqby92 z8qPk8SJ=Z{Lzv)L4h4K@!*7U2z48J;J?)+PnNDeJ8656tsM4z;(>Bp!kAFR@*cKVp zPoMIlj;sV^5Su>B=OUE42HT307We|%rk|m~FlB=47-xm{{?NH(6BaHL8G?Hi0I-g) z#a3dP7armQ-f@696-Yn_H&PRk+h=Lr+Tz{qeY7w<`g2>kEw}JlH%39k-0c3PtfWJ5 z3a2zQMzKzyW?>k|!{{iO=Xs5V-VLVc?P|7WVElGwD~Fj~J~?1@P~z)9-Ho%eSITOg zdNWH*vDS`YNbW?a^?tL77RE2t@74({9lXP(x`dpDkio5; z5~&D&Ea+W`0Xp}XM`+>SL?bL+?J~B*glpJ)oMaxN^+?WybNpZwV#L)MO?k+r)i$Fw zek)Wjzq5`w6B>PxI8oNxvL653H{`Z5u3Y3Aptt%S)C0#opUoVMGmewot?kp;Cf z6)TMIB|3Y;b^)&k-7*jSf)nimzR&s6?>OXf3>qATICxtq*cWm0O`m<^VOW2vD-=ZW6+(qda522DZRR%FTDnf4CrAmtA(&xx+pM}wHxb5mKN#VuQWuJXNpCp5%A-2}MEUIibZ=tGHt|o$z}&b%^y|YVL%&~ulbB-zO9HXB&XG2Y^%bW_(U7aQNAxR=*Zi8We|GZW4zOHi;)X_WK zLclhenB)Jm@Ok;8O{2Mt?T95FLmF(dj2 z3(r8>{>yzu`aq{Ji|HN_%oU+ROj0xoCodL5?#Gg=>t-00khOZ_(%dz#1_4O!#*M5; z*$D;GUaOBGP2@Xm+ZG_o$-iqSc`k}R{sf~M@FJC-xp*q>9LXFwD^%}VZE4@2u| za7bAPE&kw5v{(J`if)SSZlzS4OA!|2o0UvC57c2xpkMfXA_|{^BK33D3u(rvIVf*D`ddgB49ce-lLEkP;rgTaeuFykt!igc)){B%uQtM9WP|J&0K;KqrfojY+50G+masOOnx)={_mZc``0rvo2ch{It8NVN0EnlUlPy?wT8Mg$ zT#RXaQwoFZ^Q%RG{1r*)NmM_wFjz`r3^$TA^jMuukcu*Wlh$~mP%I{Jc^wE zfU5&8dlVI&6tlP-?V>}`+eor(Jy#%=??lD4_avRqXM^t5aJ)|qZ;r}y2@AC9EGDFJ zV{#QxN>--C{w*pL;02cw_1B}uu%E3YR0kU|0Gf|GI5x50511}~>ih6yp8s0Xad(;2 zr6+=i_Nvw8bWy>g=lElQ8lQz)2sRvtK@%KVKu`w*o%6VsvCL~_1FIM@lco}UnwA!X ztLUat->1WO?WyzQS)@IGO@$N~5;<+N(dS3aPZI{uhd1ro9c9`tjx1Mx84|Jvjj^id z$O*}Dn4MNvYW@6oFcHG?Jh8&9>GUi6VAP9QRb=0iFu}O&#JdZpGH_DiZT(@4Wh2XN zb_uBqe+>PaCfvco4eYxOAWTwDmDBFc(oVI0QiB+RgikGS#@^Ur0vU(Wc&9(Z+Jz>K zP{wx|FD`!xA0C*FZwyC)e7&fwqUlg6nJPI}~TY0WJY284vNN={(- z&&tf+s2qj1p?3cKtHG0L}*ySs&Q23NMHBkp(ZqS=G77Ct;XTqu?& zg0xWWG<<4;of*bisEr9G&Lk2cA4HYX?;lHv?bX6qISM65kj>DXLoJ7LfvgH+yAF<=?W z$4l7V8p=a8efr3>JRwCE3B@O(3aek^8k;G0M5CrG0D~&Velf4}xXQkP1k~U~a>S3m2j1hcXLj9Y$}#sPn*xPa=(t>` zic*t&Q6AY@d84?cwLiQsp`X{yHOrddtQ2d-rE-0}uCNxH3t1m0IvVmiE)i+{olUEh zC`y^?i&^pbOJOnzb6j~&qj8N}>o|QKaTfTon(B4=5Fx4!%32i!yZs@oo*p}67NG_7 z`z&B#g{lf51|Ff4G>hy^Mpf55un?pO76au)DwlNF59@6)3%tHeFucv4n`@-LJTPmj z&Q5K1&KDY7X^`KnXvXbT#w_2l-#(1lZsy`W1lBpZ-@T>S09<6%=i21xojb@e-1dBI zyZhEuLN*A{aHZ`?F0I{KcK5HammI4&W$ct7a*5vnX@~tm5i5!nbU%B^ZC7aNv*Z@D zvh8I*H#-HU+PGgvN^6BzlS1)MfO3SBo|ltI>!|2-0u@*C2z8(xelv;iI82H{!1u4% zyJCaBe2Z{_)Utv1=ZtDrl}mhmNog;Xzye@>RKKP;|d^j{%`wjT=8iVVXt?aZAzUC7Is<|ieVrwJn5QW zDTYhBK3O@2C1TFC&LIWspI#Ru0q>TvWQkAk^z>f*sPpqmbcQQln`l$MNrPns!+wIJ zK30A$l`oh;16CLSYxNBfoNN!9_~(3Oys7Y?&|)bhHott&g%)FxUpZ&Sn1D^IBqhvT zfEya?^vj{A*(T#q*FR%4P{O991l6%1{Gt(HOyBVLG2!?0BI5w+P|>+Ut!ce*3sPuJ zYkMh(!yuz@j|R=-c-kh7hYw>~aUK4sFTLE+6Z6R%%DiPMiT#+F+~$a39A@lj63 zo{HpyS`xNd(>+uDWbuKiXi;LjgG!@rAc4(BA=#qRskLK7AT^=W%2iZ+XBPgEK}^JG z(qDu$!gz5wr9LFMXg+$z!FK4~M~)vyJr@RVgZ=GRjLlC|(>!oJ7l0B(U?$}r`GfJJ zD>%R2;3k+1!?n~Nt={ zf-N^$Q$(bouQHBZ-Xhq?X{&=zyM z<`;H6{3_KbQI*a!#~c(U%ae?nhX&B$Vh{yv1ttzUZri@c#-!7*5yJoe+I9}v!tBWs z@wQzu`oBc7#~c2?08Rk0|CQf4LOQOm^37CL^cQNHB-{i6<*WZQZG}26oO$E(>E$dm u*&?RhOEdm4o;#FCgQK69Ard5P@=NAftWnI`j72a~Qg>H>4-IBk^$lP+9t7I} 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 zcmXteV~`-s&hFT@ZS3sWwr$(CZQHhO+qP}nGk4!}>q}R4k|(Jozq-=h0RR9XFmZCX z(|0j90r*G%acgrEdTVoi6KQ^W0RR9%Fl%Ec{r_zLh{DXs%HjWu004I8hED${|HrM& z4gN0-oSnIo_5W;u|8_icD_g_=G@*at{;mIZ0Kh*000LV7WD0Y0oB!qhp9SlmVhr@3 z^FJSbm%sFawpOBtJKKaNZ$zw0167jz*+H=1cV6-fng#D5XA;TPA!B&+yp8b0HBy? z_j`0SSS2Ev9W7 zI^)K0c>*#VFss$k0CKJ--IaMERtm+H(YFgVIAxsOSXSkjJ2>h*--WXU% zboOX*K`$3b#1vIXo*~^D`LX#+vdWZ;ul7hX5*WCT`|BC6UpuYe!V^RNVt)9z*g$fV zyq+JE9PIL&)VC`kT>isBVKB-71}MtgQ-;BOBl%6U(l6>M*9S+$@Klu+;!=1=H!tg4 zR0k0}`enW4AzxNbyxnUaflK8Km5OaQzaylocR62%3{N2;6X`epHB`&J?SBz2m5?JvfN(D_s1EtC?TWwJW}Y;c#RV0sC8ow-GD5*QthRGu zT^vgJgCmD7LtH0hB4YKSoEX-A)X^++XJ4e=pmZW;9Kj6*xGY{vEJ+34{r=+EuQ#X< zhx}O3Fz$N~T$UO_29L#hkAVdptX%z!=*9AG#!V>O4QOWbCOzO^%2n7X4$>;R#s)6g zb3ge*tQD`W7)h2lO_5JPLZrMR@Mq|y5w9V0$V?c++^yR075g@PPzXLC6T!&1|NcM?jT?{reb24^A( zh37koi@dB)3HTJ@^wX0CHiDq-e7v#l0V24Tz`@hzdOXwvu#+r>x7TTC{cfc zEJCbXQ+n~~wH;T4yOqfpBju$Cl9>pm4~zsFF!JwcyxN*4Z`$P%+*JY#UGO zsD|+12jb)~1C$uA&e{y(tSyp%;v`gv*UPcw@ci)+7A^I?Y-i!d&i`{$82Xd%^P^EQOY^!v$J zAE|OTY+cs-klh5|G{Y|dntm2vDcT-YqV#bE`bU+-8t6IkK7?IZm+5ii;50%u{8Msr z1+!!ln-#$Ri-`>acjMQP6AZ(43 z5eS-H^o+1?4+Ox~NQ2Cj0?jt`OYwMm>;I-QzQRqsatX2Wa^3cFQ!*IU4cXkX65Ws1 zy0HKnx}j0jq?{D8?Kv537gVvCWVB6Mp(Z7#C$UE9h)*o)$-E{l?%ZPY`Mfy= zy6sl&H1IK7vTX;X#1`*x@z1ox+YF7ZD|+P!<784pnDHZ2zXzl8h`RY1H`Nf9$I2|h ziA(9BXe}lCLSGc_^#E7J&q`$+_$FhKb-5$on<7v!Rh{Ah1Sv$bpj`9yb$U-89t3Pe z5t_a4o>!(z1NnnruhT~-DBoOo<1QnIrG6&D|&4RjPCYKQS3={sTD1n zmRtZLH+K7QZ-+0{!CWmk&mdMFg%d+wbVxL31jG!LIvJ3O=hc^8Bm|VY7>&*4ne38L zHTfFcVR?`gi&Vp`?jF}o+_r*0$#5eUQx)T%SS{va0c8(#AC0_^>C)rv8hj7{3v)iD z+lTdMwg|$JEX3tfhsTU<@Jxl|Q5hKyjasCDxqT=iIYdLUD3&Q$plz}@)Cv$1D3UdY z8f^W9GzAQY21#ikYPT~pk};!;`X|e1f`bYPQ%>C!JCl1j{O(0>5953hs|nHdLx6e z)i^s@7S=aKfdZGX34i9k&&C>J4mX#b_LxVF-@92Vgu+X$#u#{GVF1b|=kD6B7Cq9r zzdA=s(NL*J)tkd=+OCW2sfI^^gQ~Z}tYd>#)1RsVu1nM}8wb50au(m#$tsSQQ6$9` zd7cF9F=Z>A8?{#N>KI9MyO~@158(Xy2D&XB6`W$^9up~!(Rv}h@sK8Nu~`uFL$2Y& zsY>)!x{o`?zI-;XFi3ABA#V&Dv+_otf-N{G%$*61<{GdPR(AL1HQF)_W2d)q^6s4M zZ<~x%}w5!>*HZ7cwQA!h#ltqGA&J|HX4hFi>NsZIPoK9xd`j@FK0t|*@xo8eWdvo zemSPSwV8x5__{H*%K^4r^MJ~LN)&pq%b6=3tPEu8w}83OKHuJc1`D2H3j-E@G}2XxfY4SiHK&V09LZ5 z4FAJG+$)4V2olt0BHCCO@pMm;JGc?3auU~_&}7YNri!EnQERF9=2mcR(|Yi~15f=t z(F95^Id=JsQwQwHFZMIw57Y3M6hTuv$p%-s*T!9M^6UNC9K_6_wo&oQ;yjJx+Jo1C z8r}W@MuXD?c+GOCqpt->=v0e^c7Uk4z3I*Z)3*6we@fF#-! zbZyGAB(W@Dtsg2&D;J&lnc7qQ-C#`x?ON&xV6`4S*P%TBjCj#!^6jyoc|ckm+?nKb zGb{_v*8YB{&Xw$bdSHTi9x?G+b^y&VUZK(v++SZ`NBYR>R)OKN6Ej>Tltj_A26VE$ct#fZgLfzi&IcR{2 zG+mnHj0xQ3Jp1HU{`jc-mD|U18~ZBAJ{H3}+j&1xe&^=~6^nO{b!zoJb}fQQq4>O3 z;hoBxMMk;844*y=<<>_r6O~r&+~Wq!i(%@X!sUM^s*0*5bAz${m`~g|$w45sgpzz9 zP#aL`E_~%U$0XXnaH`D2E?l!C;ED4Nid!*}|sRdh8tG-orj>zC%@c&+P?W`XcubR~d z7CVJucqi8H68=za;PgPhJn1#PpkA&Dr`{7xv9%v&L$};R<44WvdsJNtVhslda51$f zD64`onybRmw}Jvk11gDmb;B*N7vonXhLA%|qt6Ku#LH@{uTfmBsi)3$hu^Rn*?Z}l zM=iyTwX}mf_S+^oBOcZaOcuWO&4tfbdZfTIIjwXBhmb0Y9^UG!I452RvSPTHA3&^0 z-Fp)dQM3nNjz?K%lTrv%vJKxV+l=}6GM`t2X?D=7(jOGYuYo2JPLy6iYFyrgOADev z#^7hpO>5m*vNXn6hbBKs8$W{)9hByqkKHz$eFtnR+W??|S52{7$b0Qi{RLXm z+*!vw(cxPaiYzM0zKxYcC!0}qLay+S|ALe-t(8`lZH9lXx&RfLXjQm*EOlz+BXSMVw0b?T? zY-tM}@WBZLZxSO7^8T;)6ybbWF!xWRJ1NFObzic^wp?N?I%M zR(Qv+o6(gZY$QN~?IiLz{DqZ;`T%_N;3F;Wj=qdGTCH}px}ZSdxe~2nzBxeu99B~}8#1Lj?l?$SJM$B(EqWEtWensEQ9+0r zl2!>EqT%7ayq@htb$WcTuDVWcXchbV?ssKlFJg4lYg{28m`rjSyd}q?$ z$~jg63B`@LuLW@`C$!#d$EHz@sGD`OA98n0W33vYnKHZPVRc}((I$3Priu+78+75B z{)RI`l2Y^dCSGZ4V0kVHYFWvtrTn7qzCQL6e4s2tf96fMT*yYT?A_EP>F@Ntq}L-6 z0le)Rh^v}clOX81i)VorAQPY`?}~cK1$=b>xjt}O+E%eNNwAM148d_9LGDyu(GAEA zphpuMmRJ~Waya_fxnssIz!alc9D;Az3a87A?NjM!9DDv5YiBSDMaDHh& zF21$o22M#0cind;&Tzeq*&qD5>5f!l;=Z=?+VqG@-*g{(@3(F=r@qKtBC*g8 zt`I4oKEN%~JV-118L3>S3X5Mx6VG>)7^$p_;BcSY!&qUyr*S`W)v$;KD=q?mH)s>) zzOUX@{hs&%Ffv34t%%zFMB19g0ZWL&<0d+sWkdcH<1*wA@#F7&r!I*nb;`7;tCRq+ z!pbk^m)`ibo(FL}sDCqZ%wpQB8#+fo=xBqEsv4hjUC2OB^>q)k7EUI{p4xc%Xp z>7F&Q-5n)Rl5tquS+}uuc}4fv3dMUWS%7nw04}TMyPi?#9YBP znlQa4XjevD&hPf*34JsO##iaKhA{-d3oNCzKf(Zgso+47{N{%=VPmA2_wxh!nKA07 z(n^G3o1HHX7iHmn@JHq!y~Ncf?LJ%5;!XzNK)kxRKPUpz58Ojhz{glP1lo0ORUhPO zJnBx$Eq(VrSdmv$J1Wx)7C9TgVU6p;dxhG-qi!P_}Bz)pA zzVT9sZZDy4+|WHI0eAdtIF#TD&70`?iOjdFDnGIMvg&hqDE?^KQH`kZB1Nx{v5YQp z-k8SO1|45(W-8j+`)ni!M6zOqKp_`TXBvLtrY<8MH*J+xV5;6=3G9ny&ygi>vKn=^ z6K~JTCS$Ji%?fzA6y2zW-rCJ>TK5QWcU^<)D>3!CNKZn0%k5J$&)g%n1Ml>WK4pJC zBdg)~$HKE`#=D97CbE(Oq|qK^yLnYn_K+Zykk@7b#`%*ODl>5O1h+);p;^*R+4H@wqSA+5-qll;NA#%G(@!*>x~S%b(Sp+ zw@p8KNtzN3IH_1j*VI34F{)?+sl|p!LPH?3o*B9K(hWe*3(fo(F`F@x^eRKRx}ssZ zx$$AQOJF+{UW1WGe}Cg-tcN?QseA95_C1fK9T*eG@odQd@>xY6*iN;<)mZ55rYt{f zdN1+ezdb}Pcb?#9GC?&;fOHz3_c!%Sy!Vv_rwB+Fdd-+2T(1sI7lG9-O6%9;3=?Ik z5h3bB@P(+tkx(a_TQosoJ5+X;1XuvJeI7SOi-+>sNlt=xEI*@LLnN+zJLJiyyTV!5Eg-ydbR-@-g~ND5r5MPS6C(@6mvgoh$ye~JQVbMZiN9Q1yepA5f zt{F=M1)JLzO)?>ylXA{iPoZS>jQw3AxLDF+d^`G}F3JY>DRr{Ur6II^b;dt72fCKt z#r|H@adCs=#e~>tVe#a}JUnB6yoT>8T3gU^PWsakLyW|&_jl!SMPRR((rf|I;TrR5^s*YJxfEHsayZ+ zqSfx(M&kf}kYA-tizVaCp)T3L`r*3$IL^Cf5l6jFPepRb zZKn?P&xr_fV3iy(-k9}W)}TuINg^YW6w){Ie))##me45YVyjOLiut)3`zFq$$(H_c z#Rb@nOpHfxofH+;1n@aAzqk72e){h~SC^+mg=5&iCv?BZ)%yrmI~TYCqh7{Z&;i7~ z)jBBf_Cym>Z#a)jZiCTzydU)`7I1-%PdE(j)YSJqEtXcw>%j~Wzm><0ehy#K z(r}L+}<;(oCj%8cmQlllWPvbkjA(KfJ!Z4zFz=y_Sf~`P3 zPq#!#XAW%?C0OKB{&PdRmnb7`?kv4TAy#wVrC9qG$9z4IDwVk#Y|?fUXQ4J?4HAl* z)>J7G7yEMV3UH5wph8b_$xEe1qqYJXC&++ z(rSRPm#8K#^5qsSj#D{Hd7dVq5{LX^)9jDRv1_>S%Y?FL*{wrb^-z!^9tZp0`w5r2 z0t#^eiH66<+px8n72djmwq-Qoluc9q9(6iY5zlhG=v5lOENZ7!pdRb|y@X70C8BHn zM7bAuTvZOj!qEbEl3yWW+pIv7jz}+V*+uB{G+Z`Br9ng~CP|ohOu?oSp`OeR$a}4G ztM48BO%|)@%*9OF0r>iOhJO-wFY6C-|0*T-y3l1(a!*tuSeo?V!A>pLykwkn|5}aS z?ccC{{#CQVURClLS5qPhw0ZMA|9jq`^--JYr8&IMtnetX?bXrif?e_y5(2}A1)wbD!?pT(3_TnDLuHA?$91vxLe|TI)E-SC9^)VLM^;7lOki{1IF^-OMu||# zlZCX0V9^7<@fnWwF>u!`=QLl&L)h=aPe*`^xSkn2Ey{i<4vH-tS?aC6<}F33S~>}x{MX^8jHC*|oRQiT9K83g_t4>`(jyItrj ziANWPwvUV*rsbDc)KXKJmRFL1i75e5Ta$_FjO!kxyrk)%nnENUG4EA|7vVNTWzWDW>Xb~El{)6Makl4*SVEX=0FcB?Q z`LX}`#_g-%VsAypr3$Efi(%5XIkCZju-O5r6I;{NoA;Q*4VFeD{t?jrS2=oCv?wLF zS!AAzZoWjFj8lpCumg=n;x1(VOb@N^r5I02mV`X_kb;BQ7u{-k0(E(WFgR&25J%^k zjhdI$nu(xZXqnCT>n@tw3vV zy?K%uP{D{Ga}005S!{I%F0;{33*%JJ&SD5vHo5Gn6j;9SDmJZajNtFlAY_|o3JCz{ zYlgSKAZ=TfuyGhI{FBpZ^#qI0KQvD*@J+dIY3=9yN|9q|E z9wm7_a3boMvwKUfCniFnz7(`2)PRW5Mi6J7#FY1gAf{U2icFN)E+jJemF6;6D?oP; zmR)ZvHJ@~2XJ)S%zVSvk{&0?!>E0RAqz2vP-G)iDwN*|{u6nj{2Ihq`jpfYwZ6jN3 z90dvJVfXl&96=(?$4rh~RYwbz(~t`F)?!w2+eRcgu5!Pt_P* z^;8!>+(oL!+w(ze{tq4(etHEf`mpq5kQUKHXH~Tm&6Gzg9T^LrrR?H#Gdi|9{S>Z? zMwJVvDl+BG9RM_TokmKAvJK;=<_wJubT7WVK+FA}9hP1dX>Z{yj+K`uee*q~UveSg z)Ctk&+&2d6VT3pBWC-(}s(HY!b2=pepDt1CNj`M9z6+l=~*un{|(nLR-$Dh77XgX+>|ubim6E z%tptku~hC#sonAF8xgZYEK3vyiB>Wn(Q19KpZ)M`JE!CBJ__C51^yhqSUS)D^<;i*aVAX~X;0Qvf8%iFFG0uJNM@F%!Ke-VJ#k38m!I8~|9x_F6Ua z9dyCVmUCvvqKWsilhBNkiPtcC%X*yCNyjXwn7>?3o&gelcWxScGPHAHc5Z1Q>90bj zuCM#(yaD9?O9BoDK=}`}dLVS~FMqwmBnBC-pjMbd1?Qd@248o@$XH?U^+h^_Ca_+Y~e3hmLkEi z4@}R%JB_ft7l3%sEbb9)<(cYr*823?45&zmaOi<;-kzcHG<-C?(^n>dh)J@ulyC`o z8_z3Nz@e-tt57{r%D z7<<)`8gPp7sQ}ViS>;oGpcu08|L99NSmQY;SeR)z3l`yX_NQmA*xzk z5l~@S`Tkh6rKlif!(KQ$I2!?PqM{i${!A8v4f8&*-3_u2TD*WVM{y6ZYes*IBCS)> zI<|Z^Fo$GvaaJk$AzS|C*mv0V--LRo@YE-w9^ggFmJtV>WJ}x1t^DfBYY^0N zfB=s`Rz5jlxtbtY?EEAn+KVfjRd(CHQ{?!gMK0+fq2Y9XPFhshW}1nVpAngtvLJv~ z6IQ{CpGz0(TEr~=$3jDW)$L>^osL!NY^kO(q^&{+-dPD=?3MyzF;!RXS*>zm<2EG-tFV<%)!`#4PKFP~WV3NedHRsUOT2)XLC!CN9*~OnMgn)@G!nfCy`|OfV-!42u$%QQiSQHwDdTY!i#Z!L#v3q9>;DFa9!_#Vpg7L_vAK=- z3{8803Vtw;1?=LPg!>I9FXbmk8)d|E8qY~yk49UZqmpM&v4i~#)AexyQIdGhyNdT3 z6Xw9?mBv1JozKQ@_yERh9-GI|T(wEcZ#icEor)gVO)$ti&GR6~t+?cMH=@}FH;UeW z$nOGl4}tvPW17H?VP)0FLS;awq5W+A3E_pHWB3L98>0 z2?mI6=#mOQ%{fNE9I6>06!ASq2zlP=AQ{=YSz-L|2C#Md(8%D(Jx?@pED?Gi8P56{ znye2y5*BF|`(EjnG;lc=e;pVL7dJshE^VuspKq!6NYSi{C; zeCrr&)p*7tE1?M%75DI zq=Dz@Cse(+(HZe;wgB4^=Js6iat@Y}{&aF8(e837r)yRhE&1K({T9K4ju+f{(Kg%n z%b3Vq%Vb)oBFHJ(fA+jYND{E6qvwqGRP)58-Dr9)BVE+{K~J2sIK){L)q-uV>e;<4 z{hAo_s{USI<UVq6itUPX=6sp|qj&up?5;!C#XMyJ0-_|2J5 zE|f~ayE^n*g?*+xFK$&5PP4&;uTf>jq`Tn5JReofP^E4!R5hliBo!6CjS2l{UjX)R<3IW^oxG=Zqk#2F&fXhRkVu8Wh`&bdDx zLm0v)RL}$BftO}d)8Zgb9Gf{zIH>Bs^D|oG%=1_W0^@?9T!Yj`8E4x!cC~5L>%o=L z{cJ=&iaYsF6=o7q3tY?Ec1IxB>7rs|;N8YF0y$~UA!Hatoc)dd?Y(y;v`(%z)1e{5 ze&>fDU&L5D4CQ&jnV>Iu99Tv6oqkkn5yLb)^R7BnU0K&|l|jo>xtVt-uE#PaW&d;e zQ-6EURvkknUIC(AD8`o4!~lO3BduOI0h!2lqy)!M7K(U<5~!4K*X!%oI^@U>613a^ zLZX-O8n^{e_}Zyigk*BKGUHkNUWxPO;J&Z^Gr0r>B|UpbFDXjG|7!{QmaUvVJB<1h z_|6i1W;*bmQE`9&alVDu%$pmw_!s;~bo`)-2@O!V>tU zR?HRcM1qOj_|cw)J!3f94@6*wxPLbrrJ3i{OdtUz4A>+Im(U>(sYYJcuInYJpE_b< z6U+XX6rHC|f%jFSAI4gZ4h_V#f_F$J)Wjl;V;>=J$tVKefh#7+u z3Q%W6%O>Tse<|^UbO}Xa6p5%YaL6bJGtE&Wt7a2q<%El2R>?N3$^fMK>ie#ma1cl5 z;Q`;d9Ce~DyuU=uT(@{4t6oO6l@Ert3+0XGpLrD1AGfy^^h@vRJOy4*ag&C-24<4Z zJOsqV!L(FU3L|8fO{9`I01v812oG6kF7A@a-m(&NbrT+HZWB+Zx_k2;KshPt9_G+n)OQ}fxTyv<9%jLH@ni>`| zj0~((PcR7E@vsAkSz#zRlN}a7Aj72cGh}54ZfdinHu9kuknQ1lqHc?p^MJ8Bn6%vD zaZ%M@g(EKp_|EP`ees8AS@k!)B9jj5ekl_rA5lD~pk{)BVG$;CwUt$RffwTI%1W}= zX$NDr_7;;kQNDF_O=D=eZ$LZRWf~!S=T904xs;RFQix;tykK|R{P>$)Z3EPeo4$v1 zqDKOk_7gCzA=xMs(^@R=xcy!^C=D5aYTvjWmL(SvCNa+}Q(mGo5!;00rTuvb0PpSH zR&9kuq&T**+e!wN6*k+>fvA{{YOJK>;)CvUv1!+Z_2$5a0~#ahXa%LMCkDn8VIuFP z@agi6zlB$@G8emny0<2En90so$#AxXPuy6q>K;FR$2FEnbBs?To|7&W5~yKaFzQ-Lox7?LpZj^~nk`&H>RC%m>J2`3Io` zS96d<47Jm4Z?|F;yh~K4caYo78JOn90AryS{!2UIHY>*wjxD0=HUitnswEb*3}(3N_6@*_So}2q^}a_P_6T>CX)x1@F`v&>D7LnVH%x7I^ebdTZ|h>NrOY|IEH zEghg(>ZL_OAX|)&dO8-YuqH%xqsas2TgnfJn==KR+!o!WA5vEIg?x#L=+C*T)iP)? zw3ErmK{pX(FSR`LpPpbvqhxaF5$gwuP{R00V|#O2Gg#^YW;qL4G`j5o9L~&*^P9RR zxSMzJRK&+m*8B^sQa?f*us43_{(y{X@xzEWk)9%m7zbt$t#S(FD$NckCXIDM{6Ll{ zCo*op6adlwA`|rIC4wN?!fhr$fuG;5q}d5!_0K+lE25IeCc$h6p+j`Cb>QT@9AoXV z84CTb)yn*SC|+W&T-9tax7OcX3vTodEU}xboX2T|9i>B;7E|hmLul-KNYcjY(t3MM zwWjTfC?2jO-e_T4fPsdyQc^Fy8Z$IW#xoIhT=jDH=imSlN`^CToNEWoN9{L;uLh#)8T{e=4{QP6s+F&^ z2ut`PyF!qR!LGkpDYV|=byI4XkUW$ETFU+eFInN|cPRB$5<>9egGA6r2G7ntYC`-M zCzLk++9V&=QBGPvCq6>pk#C}o6^CkH4S&p4Obe5Mu73*|RS5#? zsXXuivAKMWM7IcHmrH}UsPbbl)}75tTTbET6Hib-IMgz`Kh!mGIqR6@6t{X;L#p4u8L5HOC8R_ z=sV2wzI8y4F_H2a)YEHCal~}snz)L#HWPkn<>%X+(vJ8!NIlo;NF(?oN%|I^?|T*A zDDJIQYLLjd&?qV#HVr2cHT|Vc6SrxL9t;@+y%nkoOFEx)+X?Ge+r_e3b~?wdnVB6t zzvSNcWui1LonbwnX2;}Hlb?;}`5+9uexz-R1wic5Wm;ffcDad}P^GpmRUbFy zhp)G@YFK|0{tV>N)1nDuQ8jK_|7H4XMY(c_IepieEg=c0a5{(SiT74B|87uDV$TB< zl3G=^<|R{{Zq~39%97afucd!U<3Q9#w3k|B%GrytWjp<{s>6%1g%!B={maWz-+B7h zg=e^Z`K5%T#B{$(gKr$o;UsG{O;BQ=C3vQnh0_giK{3$|vz3^IzNLhe?wjVw3lJI7 zY^2EU!e(sD3~MlIj2E*F;ADhYU3eK2haHcIF33F2wMqqtWUvyePw~F=AI{gm!V;f!DNej1}LGyGhe{UzRP%TG!^e)K+9af z#u+YeFqE8c)crTZ6}}7EjAqnE!U1S7Cl5ne@Xl9YXQ?JJ@Q%aKktfpe(8C8;G8$P1 z|H8&FqSoW4HGmrM`)(BJ*C8J0v3r8U)L7)@q1@`8-RG}rk3lmWmvucdr%q5zyIc{2 zCwc{TXA5kPyZ@E4W*<*HoEa1Vb8DfEmhKmkcG83UD3WQ#+scPW#&9!DhLNAT+s$R3&TL zf$Pza$jX_NGNnI!y3Rr|P9<7Zwc2;7o+TIPThOMP6q%u8X(Ly*=ZW6($PH(h)nU+` z>u@2Q;L9b@j@G46eXPq2iy`heY&_G;P?CAXw%|ihK^s2>lc4yML-`$_GWr^R1sP%Y zaSiRZ#66kvjb+#B+~UJz)cxAR`Ortf~#!y90b{Q52Mb)!T7z zo08dj)2>k&JsmEf^)0oJNVguo6L?1f$pM+Ts<+!verpuD>D)3{YX_^oX@WNI{0~(J zd0?dN{<+eo@$cdeBQbxO=XcoRFCp>H-3F8xLsHtkQhHI*@8NE9yLR09zq{Ik9=r(4 zgX5E3a1a%6xAF*Hf)xLvzMBk)yxRyG8S&FLR7r;Rz=$ew!Bk;n5?yJ+0Mhg_hDKJC+gK-Dy}8pVh0V&h>JK1I-7(wbX_N65`28_(HkR$g=Y<_X3k6gHEjz&!T74w>8z}u z&lMS$qLbKxj^&Z%WOt2h=U-L>-$u1RiJ%Pm#?NlzJnk`_zVik35!kazw&7H5*(zKk z@VU^D%A7yhxywe0FKUeVdPIKIuBcfxoL_|_!B(JF-0A$`N+)NKoaaoM=P)|3fB1S} zGe2y*p3yg-0FRu2zKIr_%Y39pc+gN#8c?$Yn;!+?CilSCPBjdVRJqnZRrpG<=kVgF zIt=%AY54X5l9dm_{xf&c`ZWhSAv?L{KX|(50|opk5oLESv0a+wyXR;++y7qQ@t!=Q zEr{c;Gk7(Y2MmMXYaI(U1fC!MNr8KWQ#A))zr>Y0tzA+gh?^l?w^)~n3NLmKs2VBp z+u3jq;#xCTs90Iaiq4{psrQF zMHjcK&YhpsO2tKTltJMXi#7uEx#zWJB&oL}r!b5=zV_F327H^j#}?>!;`O|0Q0HMb-2N&oG0M_{UmBl&vV zBBqV*yQJta32S+oN__S5vqNbjrU*suq{)Kg&y?UyUj2wX5b9PM58iG2HTYbjOm2q= zR;zVw@1UcJf@sS=EQ%w;6sc(;bwMsb`T1424ShEYO`ptwnb`W9pr|P2r|Pa9*f2*0 zWK-4s<_nG!N>O$hYN43ND{kEk36hRW=D)#H7<-33tT(jw92VVYgTCRr_ip!7g~}ik zTxEm9Z15uJnu0gSL+%YbJ{Sya%cY~ILeUQxDf*8)vlzZ)yT(I_XfTZuh!k$org*KAkCrg{q>8ME^nzZk&>tr@=)UO=ygE58Mzdr8PZ{VpR=2QUK zJvVK>2OOqHYOqt^^0iEVI^Uxa=Ku6&*yWMT*wa<)si61ihk&2?CIFwTa0I9P{ja&Oe))TI{|K)s{lzfw(AgD#ulVLTY4;w;sC0)5aeMR|wp zfvF#%Bboe>jk!!j*xu#4OkM`W#w2XqRLf!8nRS8Pfj^YS(SjrP81hNObg4!ZygX{4 z43QY*7`ukyzW4~p-`4~uQ^T_DopC}Z9~T`S54iS!2W8#u3!}-ny4QR40rf#56I zHt7WcPh6N-OgrH4e-J*9aBFy>&lLn}W0VxwdA5QyGo-;zWPR-i9l8-)@Mr$+jaq|9!QskvDV!^+ncoX$`DPC zizub9g;4$3!*)`;S-jTVFQkyohL`^GGhu?cl~Cylv=a=2^_Qk9{7WPfK==pi6kV<^ zO9P9nE}6qhuumMWFY>%vRlm*4>%5perqGs4(pM||FlACGE6tbD#9OPJm9+!iWQ(B# zy_3+kNLd^B@xDi%o*nUaeAlYT7EklV|wHN#!P&PXKsCSB>Y{2@<@+Bwieb~#9AU3cs3Mn^%? z4w=e=2VJFH#|vyGcwcE?3Nlf*tCPEYBISe#*Pc465YO_>xF0ma5K)N@kyb3;hNWOr z3$wfnBtLaiBQnoA=OcD^)=0rs1u|C|oBCaZT*g2Np3_U{Dn5ZYFS+qqy=V*zv2GfR zZghXrUXTjefaV`77@VK-@GXM8fg)|H`NnRE0e!|y^kqg?-^5^@0L>uq+>DH`6avW< z6p1Uz9!hvhs|4M~IbK@)27MZY>XHt2$kMX6Q#VGl3uj>elM|16mQo9>GzJ0qm%bGs zL&&uHIKPdcQa7*5Y^_lxY{>YT$z%yv_(IV4by=-W7n^)+CIp5yjC-U>o-(pSr}62M zc~qt#Wn=(BCoB_gS;=?I5St9g&^eXyKr!*rgc0|kw1DBMoG*V7pMTY_&(ftfMjT$) z$(?&Xwvq&IKgj%(X|R!ST*qv&>Bd(EE!ED|?1-D2dj=b?9d$&=(G!Ev9&F2hh2sgz z!COVHF`j=>B!sBsx}Af8+phvjPWFhqQ5z-3g4pD3hrZ*MN6dRtT=Cq*Nu12{O1P`p zD_wYPMjiWGX8<@QZA5FgZOL86;bd>NA^y(pGsBM+N;0XY)hbDSjOGfA%47(K)oDJQ zO9=q4^GlQ}DtKuhVD(eP(WpEk``j}-FH`8DI2J64t^4;A8kmLbi~`5 zRAh$_zxaGFkWS9Ca8`&}*3{)e>Ui%4+;{*AIu&Az>m|fDOiPRKxl(yVZvpYecMpw2 z{Jnqm2Cgutjg2>8lVcc$UM+ZdXxO3VNqG3rpPR^boRyq$$3+J{i1?jVj_|tEv8uLn zJRvVU#TIvvXdHaqh(0z}zMc?%1M;XQ@3#Vgxerh6MFn~GRmDp_J z+wXnjj%?#I#^%^ZBJ5~FU{dtWNZ%{fzB)&bU+h3gNw7r9mlYh(7+yz3sBP?U_6DyE zEMEv^6ii8ICiQsPB4=ckd9;xLyl~p2ooy2fSsgdQOIzB#>9*QC# z>)}7*L4vJ4OR)HuNff^3AP*cc0Fu#r;$edeN@kPcADR0;-XpGV!YaD&uN3xX-%7S<-b5^&oGl*yBtFE3F+{M?xqtK*XyEQ%azcuZq?eu-ny6r^$#b= z$UH?uP;`rM2kx(x%9DPmcs|Bi+P|X1ZqI7!T9A1@1L;Q&jbIOJB|mk>=q!e2|KbSJPp1u zP9?8k_u5c_TFxg^c&q;x3j_50>7I_?gnVX|-ZuJ0*c0pgS0HsQ^d9LQ>ildULj4rV z7~$kDf&(s!9>~lb6ARH&xw7UPRBXgsQ$z>(rdaybz#RaASEAC?Ja-Z}yba-F-Tf)jyC+!<>HZrukRcb%y7ZzsuXU`-Qe$?*0sUyvn>{iwIw;yp)^jco zsw0%*_wW*`$W?hBa5mppc_1hH31cq%Ff&N>bAcek&@DGBxnu7cwsvS{Yo&UfGhESG z#(nfQYukHn}Zka!6AFgmWHF{UuVpU@cje zay0{Nkl5F0VLi4 literal 0 HcmV?d00001 diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index cc9f0a3a088..1d56b3be0bf 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -10,6 +10,7 @@ from pathlib import Path from struct import unpack from typing import Any +from unittest import mock import pytest @@ -329,17 +330,29 @@ def test_exif(self) -> None: exif = im.getexif() assert exif[274] == 3 - @pytest.mark.parametrize("bytes", [True, False]) - def test_exif_save(self, tmp_path: Path, bytes: bool) -> None: + @pytest.mark.parametrize("bytes,orientation", [(True, 1), (False, 2)]) + def test_exif_save( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + bytes: bool, + orientation: int, + ) -> None: + mock_avif_encoder = mock.Mock(wraps=_avif.AvifEncoder) + monkeypatch.setattr(_avif, "AvifEncoder", mock_avif_encoder) exif = Image.Exif() - exif[274] = 1 + exif[274] = orientation exif_data = exif.tobytes() with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") im.save(test_file, exif=exif_data if bytes else exif) with Image.open(test_file) as reloaded: - assert reloaded.info["exif"] == exif_data + if orientation == 1: + assert "exif" not in reloaded.info + else: + assert reloaded.info["exif"] == exif_data + mock_avif_encoder.mock_calls[0].args[16:17] == (b"", orientation) def test_exif_invalid(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: @@ -347,6 +360,35 @@ def test_exif_invalid(self, tmp_path: Path) -> None: 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.info["exif"] + test_file = str(tmp_path / "temp.avif") + im.save(test_file, exif=exif) + + exif_data = Image.Exif() + exif_data.load(exif) + assert exif_data[274] == exif_orientation + with Image.open(test_file) as reloaded: + exif_data = Image.Exif() + exif_data.load(reloaded.info["exif"]) + assert exif_data[274] == exif_orientation + def test_xmp(self) -> None: with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: xmp = im.info["xmp"] diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 2b205c4a53a..3b34e52e576 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -86,7 +86,9 @@ def _open(self) -> None: ) # Get info from decoder - width, height, n_frames, mode, icc, exif, xmp = self._decoder.get_info() + width, height, n_frames, mode, icc, exif, xmp, exif_orientation = ( + self._decoder.get_info() + ) self._size = width, height self.n_frames = n_frames self.is_animated = self.n_frames > 1 @@ -99,6 +101,16 @@ def _open(self) -> None: if xmp: self.info["xmp"] = xmp + if exif_orientation != 1 or exif is not None: + exif_data = Image.Exif() + orig_orientation = 1 + if exif is not None: + exif_data.load(exif) + orig_orientation = exif_data.get(ExifTags.Base.Orientation, 1) + if exif_orientation != orig_orientation: + exif_data[ExifTags.Base.Orientation] = exif_orientation + self.info["exif"] = exif_data.tobytes() + def seek(self, frame: int) -> None: if not self._seek_check(frame): return @@ -176,9 +188,14 @@ def _save( else: exif_data = Image.Exif() exif_data.load(exif) - exif_orientation = exif_data.pop(ExifTags.Base.Orientation, 1) + exif_orientation = exif_data.pop(ExifTags.Base.Orientation, 0) + if exif_orientation != 0: + if len(exif_data): + exif = exif_data.tobytes() + else: + exif = None else: - exif_orientation = 1 + exif_orientation = 0 xmp = info.get("xmp") diff --git a/src/_avif.c b/src/_avif.c index ac2ec283bc1..f8041b40107 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -76,6 +76,44 @@ exc_type_for_avif_result(avifResult result) { } } +static uint8_t +irot_imir_to_exif_orientation(const avifImage *image) { +#if AVIF_VERSION_MAJOR >= 1 + uint8_t axis = image->imir.axis; +#else + uint8_t axis = image->imir.mode; +#endif + uint8_t angle = image->irot.angle; + int irot = !!(image->transformFlags & AVIF_TRANSFORM_IROT); + int imir = !!(image->transformFlags & AVIF_TRANSFORM_IMIR); + if (irot && 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 (irot && 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 (irot && 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) { const avifTransformFlags otherFlags = @@ -485,7 +523,9 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { return NULL; } } - exif_orientation_to_irot_imir(image, exif_orientation); + if (exif_orientation > 0) { + exif_orientation_to_irot_imir(image, exif_orientation); + } self->image = image; self->frame_index = -1; @@ -806,14 +846,15 @@ _decoder_get_info(AvifDecoderObject *self) { } ret = Py_BuildValue( - "IIIsSSS", + "IIIsSSSI", image->width, image->height, decoder->imageCount, self->mode, NULL == icc ? Py_None : icc, NULL == exif ? Py_None : exif, - NULL == xmp ? Py_None : xmp + NULL == xmp ? Py_None : xmp, + irot_imir_to_exif_orientation(image) ); Py_XDECREF(xmp); From a56acd86c400bf474193b99778f48fcdfda52bdc Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 14 Dec 2024 02:44:38 +1100 Subject: [PATCH 13/22] Removed unittest mock (#10) * Removed unittest mock * Updated license * Increased timeout --------- Co-authored-by: Andrew Murray --- .github/workflows/test-windows.yml | 2 +- Tests/test_file_avif.py | 5 ----- wheels/dependency_licenses/LIBAVIF.txt | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index fab90454a54..83e01992754 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -37,7 +37,7 @@ jobs: matrix: python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] - timeout-minutes: 30 + timeout-minutes: 45 name: Python ${{ matrix.python-version }} diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 1d56b3be0bf..1bc7299b62e 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -10,7 +10,6 @@ from pathlib import Path from struct import unpack from typing import Any -from unittest import mock import pytest @@ -334,12 +333,9 @@ def test_exif(self) -> None: def test_exif_save( self, tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, bytes: bool, orientation: int, ) -> None: - mock_avif_encoder = mock.Mock(wraps=_avif.AvifEncoder) - monkeypatch.setattr(_avif, "AvifEncoder", mock_avif_encoder) exif = Image.Exif() exif[274] = orientation exif_data = exif.tobytes() @@ -352,7 +348,6 @@ def test_exif_save( assert "exif" not in reloaded.info else: assert reloaded.info["exif"] == exif_data - mock_avif_encoder.mock_calls[0].args[16:17] == (b"", orientation) def test_exif_invalid(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt index 11bcb969bec..350eb9d15ce 100644 --- a/wheels/dependency_licenses/LIBAVIF.txt +++ b/wheels/dependency_licenses/LIBAVIF.txt @@ -51,7 +51,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------------------------------------------------------ -Files: apps/shared/iccjpeg.* +Files: third_party/iccjpeg/* In plain English: From f5dc957079480b76610d4307cf79b1c390c88caf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 14 Dec 2024 02:47:18 +1100 Subject: [PATCH 14/22] Use cmds_cmake (#9) * Use cmds_cmake * Added libsharpyuv * Combine meson into libavif install script * Simplified condition --------- Co-authored-by: Andrew Murray --- .github/workflows/test-windows.yml | 4 -- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_libavif.sh | 1 + winbuild/build_prepare.py | 72 ++++++------------------ 4 files changed, 20 insertions(+), 59 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 83e01992754..39223089d36 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -139,10 +139,6 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" - - name: Build dependencies / meson - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\install_meson.cmd" - - name: Build dependencies / libavif if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libavif.cmd" diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f728d0e5520..0efda07eac6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -100,7 +100,7 @@ function build_harfbuzz { function build_libavif { if [ -e libavif-stamp ]; then return; fi - if [[ -z "$IS_MACOS" ]] && ([[ "$MB_ML_VER" == 2014 ]] || [[ "$PLAT" == "aarch64" ]]); then + if [[ "$MB_ML_VER" == 2014 ]] || [[ "$PLAT" == "aarch64" ]]; then # Once Amazon 2 is EOL on 30 June 2025, manylinux2014 will no longer be needed # Once GitHub Actions supports aarch64 without emulation, this will no longer needed as building will be faster if [[ "$PLAT" == "aarch64" ]]; then diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index 09646c4ad78..29796a74dcc 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -51,6 +51,7 @@ fi cmake -G Ninja -S . -B build \ -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DAVIF_LIBSHARPYUV=LOCAL \ -DAVIF_LIBYUV=LOCAL \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9758a804637..82488628c6f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -122,7 +122,6 @@ def cmd_msbuild( "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", - "MESON": "1.6.0", "LIBAVIF": "1.1.1", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") @@ -405,35 +404,20 @@ def cmd_msbuild( "dir": f"libavif-{V['LIBAVIF']}", "license": "LICENSE", "build": [ - cmd_mkdir("build.pillow"), - cmd_cd("build.pillow"), - " ".join( - [ - "{cmake}", - "-DCMAKE_BUILD_TYPE=Release", - "-DCMAKE_VERBOSE_MAKEFILE=ON", - "-DCMAKE_RULE_MESSAGES:BOOL=OFF", - "-DCMAKE_C_COMPILER=cl.exe", - "-DCMAKE_CXX_COMPILER=cl.exe", - "-DCMAKE_C_FLAGS=-nologo", - "-DCMAKE_CXX_FLAGS=-nologo", - "-DBUILD_SHARED_LIBS=OFF", - "-DAVIF_CODEC_AOM=LOCAL", - "-DAVIF_LIBYUV=LOCAL", - "-DAVIF_LIBSHARPYUV=LOCAL", - "-DAVIF_CODEC_RAV1E=LOCAL", - "-DCMAKE_MODULE_PATH={winbuild_dir_cmake}", - "-DAVIF_CODEC_DAV1D=LOCAL", - "-DAVIF_CODEC_SVT=LOCAL", - '-G "Ninja"', - "..", - ] + f"{sys.executable} -m pip install meson", + *cmds_cmake( + "avif_static", + "-DBUILD_SHARED_LIBS=OFF", + "-DAVIF_CODEC_AOM=LOCAL", + "-DAVIF_LIBYUV=LOCAL", + "-DAVIF_LIBSHARPYUV=LOCAL", + "-DAVIF_CODEC_RAV1E=LOCAL", + "-DAVIF_CODEC_DAV1D=LOCAL", + "-DAVIF_CODEC_SVT=LOCAL", ), - "ninja -v", - cmd_cd(".."), cmd_xcopy("include", "{inc_dir}"), ], - "libs": [r"build.pillow\avif.lib"], + "libs": ["avif.lib"], }, } @@ -663,19 +647,13 @@ def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> if dep_name in disabled: print(f"Skipping disabled dependency {dep_name}") continue - - scripts = [] - if dep_name == "libavif": - scripts.append("install_meson.cmd") - scripts.append(build_dep(dep_name, prefs, verbose)) - - for script in scripts: - if gha_groups: - lines.append(f"@echo ::group::Running {script}") - lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') - lines.append("if errorlevel 1 echo Build failed! && exit /B 1") - if gha_groups: - lines.append("@echo ::endgroup::") + script = build_dep(dep_name, prefs, verbose) + if gha_groups: + lines.append(f"@echo ::group::Running {script}") + lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') + lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + if gha_groups: + lines.append("@echo ::endgroup::") print() lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines, prefs, verbose) @@ -796,7 +774,6 @@ def main() -> None: **arch_prefs, # Pillow paths "winbuild_dir": winbuild_dir, - "winbuild_dir_cmake": winbuild_dir.replace("\\", "/"), # Build paths "bin_dir": bin_dir, "build_dir": args.build_dir, @@ -818,19 +795,6 @@ def main() -> None: print() write_script(".gitignore", ["*"], prefs, args.verbose) - if "libavif" not in disabled: - write_script( - "install_meson.cmd", - [ - r'call "{build_dir}\build_env.cmd"', - "@echo " + ("=" * 70), - f"@echo ==== {'Building meson':<60} ====", - "@echo " + ("=" * 70), - f"{sys.executable} -m pip install meson=={V['MESON']}", - ], - prefs, - args.verbose, - ) build_env(prefs, args.verbose) build_dep_all(disabled, prefs, args.verbose) From 8d77678d029d7fa9db6cfed3318152d7266bd513 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Fri, 13 Dec 2024 10:55:25 -0500 Subject: [PATCH 15/22] chore(docs): update quality and speed with correct defaults --- docs/handbook/image-file-formats.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 68999084055..1e6a832c8ea 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1345,7 +1345,7 @@ as 8-bit RGB(A). The :py:meth:`~PIL.Image.Image.save` method supports the following options: **quality** - Integer, 1-100, defaults to 90. 0 gives the smallest size and poorest + Integer, 1-100, defaults to 75. 0 gives the smallest size and poorest quality, 100 the largest and best quality. The value of this setting controls the ``qmin`` and ``qmax`` encoder options. @@ -1364,7 +1364,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: * ``4:4:4`` **speed** - Quality/speed trade-off (0=slower-better, 10=fastest). Defaults to 8. + Quality/speed trade-off (0=slower-better, 10=fastest). Defaults to 6. **range** YUV range, either "full" or "limited". Defaults to "full" From bdb24f9c842a592075621de9e6cea07efb7bb722 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 16 Dec 2024 06:48:00 +1100 Subject: [PATCH 16/22] Removed `_avif.HAVE_AVIF` and `_avif.VERSION` (#11) * Removed unused attributes * Decrement reference count --------- Co-authored-by: Andrew Murray --- src/_avif.c | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/_avif.c b/src/_avif.c index f8041b40107..45f014f86a4 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -1006,36 +1006,15 @@ static PyMethodDef avifMethods[] = { static int setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - - PyObject *v = PyUnicode_FromString(avifVersion()); - if (PyDict_SetItemString(d, "libavif_version", v) < 0) { - Py_DECREF(v); - return -1; - } - Py_DECREF(v); - - v = Py_True; - Py_INCREF(v); - if (PyDict_SetItemString(d, "HAVE_AVIF", v) < 0) { - Py_DECREF(v); + if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { return -1; } - Py_DECREF(v); - - v = Py_BuildValue( - "(iii)", AVIF_VERSION_MAJOR, AVIF_VERSION_MINOR, AVIF_VERSION_PATCH - ); - if (PyDict_SetItemString(d, "VERSION", v) < 0) { - Py_DECREF(v); - return -1; - } - Py_DECREF(v); + PyObject *d = PyModule_GetDict(m); + PyObject *v = PyUnicode_FromString(avifVersion()); + PyDict_SetItemString(d, "libavif_version", v ? v : Py_None); + Py_XDECREF(v); - if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { - return -1; - } return 0; } @@ -1052,6 +1031,7 @@ PyInit__avif(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } From ddc8e7e459749f22b6944f5bab6f79e12ff41826 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Sun, 15 Dec 2024 14:55:51 -0500 Subject: [PATCH 17/22] Use "rav1e" if available as default ("auto") avif encoder --- docs/handbook/image-file-formats.rst | 2 +- src/_avif.c | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1e6a832c8ea..28cf015fd84 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1370,7 +1370,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: YUV range, either "full" or "limited". Defaults to "full" **codec** - AV1 codec to use for encoding. Specific values are "aom", "rav1e", and + AV1 codec to use for encoding. Specific values are "rav1e", "aom", 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. diff --git a/src/_avif.c b/src/_avif.c index 45f014f86a4..5792d6d0676 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -3,6 +3,8 @@ #include #include "avif/avif.h" +static int have_rav1e = 0; + typedef struct { avifPixelFormat subsampling; int qmin; @@ -369,7 +371,11 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { enc_options.speed = speed; if (strcmp(codec, "auto") == 0) { - enc_options.codec = AVIF_CODEC_CHOICE_AUTO; + if (have_rav1e) { + enc_options.codec = AVIF_CODEC_CHOICE_RAV1E; + } else { + enc_options.codec = AVIF_CODEC_CHOICE_AUTO; + } } else { enc_options.codec = avifCodecChoiceFromName(codec); } @@ -1015,6 +1021,8 @@ setup_module(PyObject *m) { PyDict_SetItemString(d, "libavif_version", v ? v : Py_None); Py_XDECREF(v); + have_rav1e = _codec_available("rav1e", AVIF_CODEC_FLAG_CAN_ENCODE); + return 0; } From b585f9e560c9ed07ee0abd5ac064da9bf5551c7c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 16 Dec 2024 07:00:14 +1100 Subject: [PATCH 18/22] Simplified EXIF code (#12) * Use break in switch * Use walrus operator * Do not add irot and imir flags if orientation is default * Do not potentially call Exif tobytes() twice * Simplified code by only setting info["exif"] once --------- Co-authored-by: Andrew Murray --- src/PIL/AvifImagePlugin.py | 35 ++++++++++++++++------------------- src/_avif.c | 26 ++++++++------------------ 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 3b34e52e576..7bd1e84c58d 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -96,20 +96,21 @@ def _open(self) -> None: if icc: self.info["icc_profile"] = icc - if exif: - self.info["exif"] = exif if xmp: self.info["xmp"] = xmp - if exif_orientation != 1 or exif is not None: + if exif_orientation != 1 or exif: exif_data = Image.Exif() - orig_orientation = 1 - if exif is not None: + if exif: exif_data.load(exif) - orig_orientation = exif_data.get(ExifTags.Base.Orientation, 1) - if exif_orientation != orig_orientation: + 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 - self.info["exif"] = exif_data.tobytes() + exif = exif_data.tobytes() + if exif: + self.info["exif"] = exif def seek(self, frame: int) -> None: if not self._seek_check(frame): @@ -180,22 +181,18 @@ def _save( autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) icc_profile = info.get("icc_profile", im.info.get("icc_profile")) - exif = info.get("exif") - if exif: + exif_orientation = 1 + if exif := info.get("exif"): if isinstance(exif, Image.Exif): exif_data = exif - exif = exif.tobytes() else: exif_data = Image.Exif() exif_data.load(exif) - exif_orientation = exif_data.pop(ExifTags.Base.Orientation, 0) - if exif_orientation != 0: - if len(exif_data): - exif = exif_data.tobytes() - else: - exif = None - else: - exif_orientation = 0 + 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") diff --git a/src/_avif.c b/src/_avif.c index 5792d6d0676..7dd0f535429 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -126,16 +126,6 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { // 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 1: // The 0th row is at the visual top of the image, and the 0th column is - // the visual left-hand side. - image->transformFlags = otherFlags; - image->irot.angle = 0; // ignored -#if AVIF_VERSION_MAJOR >= 1 - image->imir.axis = 0; // ignored -#else - image->imir.mode = 0; // ignored -#endif - return; 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 = otherFlags | AVIF_TRANSFORM_IMIR; @@ -145,7 +135,7 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { #else image->imir.mode = 1; #endif - return; + 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 = otherFlags | AVIF_TRANSFORM_IROT; @@ -155,7 +145,7 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { #else image->imir.mode = 0; // ignored #endif - return; + 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 = otherFlags | AVIF_TRANSFORM_IMIR; @@ -165,7 +155,7 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { #else image->imir.mode = 0; #endif - return; + 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 = @@ -177,7 +167,7 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { #else image->imir.mode = 0; #endif - return; + 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 = otherFlags | AVIF_TRANSFORM_IROT; @@ -187,7 +177,7 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { #else image->imir.mode = 0; // ignored #endif - return; + 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 = @@ -199,7 +189,7 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { #else image->imir.mode = 0; #endif - return; + 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 = otherFlags | AVIF_TRANSFORM_IROT; @@ -209,7 +199,7 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { #else image->imir.mode = 0; // ignored #endif - return; + break; } } @@ -529,7 +519,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { return NULL; } } - if (exif_orientation > 0) { + if (exif_orientation > 1) { exif_orientation_to_irot_imir(image, exif_orientation); } From da2e18de3ebf429b1f4a8ad8e0f7134692961f99 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Tue, 17 Dec 2024 13:15:15 -0500 Subject: [PATCH 19/22] Revert "Use "rav1e" if available as default ("auto") avif encoder" This reverts commit ddc8e7e459749f22b6944f5bab6f79e12ff41826. --- docs/handbook/image-file-formats.rst | 2 +- src/_avif.c | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 28cf015fd84..1e6a832c8ea 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1370,7 +1370,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: YUV range, either "full" or "limited". Defaults to "full" **codec** - AV1 codec to use for encoding. Specific values are "rav1e", "aom", and + 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. diff --git a/src/_avif.c b/src/_avif.c index 7dd0f535429..9d83ec241cd 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -3,8 +3,6 @@ #include #include "avif/avif.h" -static int have_rav1e = 0; - typedef struct { avifPixelFormat subsampling; int qmin; @@ -361,11 +359,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { enc_options.speed = speed; if (strcmp(codec, "auto") == 0) { - if (have_rav1e) { - enc_options.codec = AVIF_CODEC_CHOICE_RAV1E; - } else { - enc_options.codec = AVIF_CODEC_CHOICE_AUTO; - } + enc_options.codec = AVIF_CODEC_CHOICE_AUTO; } else { enc_options.codec = avifCodecChoiceFromName(codec); } @@ -1011,8 +1005,6 @@ setup_module(PyObject *m) { PyDict_SetItemString(d, "libavif_version", v ? v : Py_None); Py_XDECREF(v); - have_rav1e = _codec_available("rav1e", AVIF_CODEC_FLAG_CAN_ENCODE); - return 0; } From 3a9a3ab9ccb23ec5c743aa7f84d2b04185f8d21c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 24 Dec 2024 16:12:36 +1100 Subject: [PATCH 20/22] Reduced epsilons (#13) * Derive dir from filename * Reduced epsilons * Simplified code * Do not shadow builtin * Test saving EXIF instance without orientation * More closely match wheels-dependencies --------- Co-authored-by: Andrew Murray --- Tests/test_file_avif.py | 35 ++++++++---- depends/install_libavif.sh | 15 ++--- src/_avif.c | 112 ++++++++++++------------------------- winbuild/build_prepare.py | 1 - 4 files changed, 68 insertions(+), 95 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 1bc7299b62e..7a92f781a9c 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -147,7 +147,7 @@ def test_read(self) -> None: # generated with: # avifdec hopper.avif hopper_avif_write.png assert_image_similar_tofile( - image, "Tests/images/avif/hopper_avif_write.png", 12.0 + image, "Tests/images/avif/hopper_avif_write.png", 11.5 ) def _roundtrip(self, tmp_path: Path, mode: str, epsilon: float) -> None: @@ -163,7 +163,7 @@ def _roundtrip(self, tmp_path: Path, mode: str, epsilon: float) -> None: if mode == "RGB": # avifdec hopper.avif avif/hopper_avif_write.png assert_image_similar_tofile( - image, "Tests/images/avif/hopper_avif_write.png", 12.0 + image, "Tests/images/avif/hopper_avif_write.png", 6.02 ) # This test asserts that the images are similar. If the average pixel @@ -181,7 +181,7 @@ def test_write_rgb(self, tmp_path: Path) -> None: Does it have the bits we expect? """ - self._roundtrip(tmp_path, "RGB", 12.5) + self._roundtrip(tmp_path, "RGB", 8.62) def test_AvifEncoder_with_invalid_args(self) -> None: """ @@ -329,11 +329,11 @@ def test_exif(self) -> None: exif = im.getexif() assert exif[274] == 3 - @pytest.mark.parametrize("bytes,orientation", [(True, 1), (False, 2)]) + @pytest.mark.parametrize("use_bytes, orientation", [(True, 1), (False, 2)]) def test_exif_save( self, tmp_path: Path, - bytes: bool, + use_bytes: bool, orientation: int, ) -> None: exif = Image.Exif() @@ -341,7 +341,7 @@ def test_exif_save( exif_data = exif.tobytes() with Image.open(TEST_AVIF_FILE) as im: test_file = str(tmp_path / "temp.avif") - im.save(test_file, exif=exif_data if bytes else exif) + im.save(test_file, exif=exif_data if use_bytes else exif) with Image.open(test_file) as reloaded: if orientation == 1: @@ -349,6 +349,17 @@ def test_exif_save( else: assert reloaded.info["exif"] == exif_data + def test_exif_without_orientation(self, tmp_path: Path): + exif = Image.Exif() + exif[272] = b"test" + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = str(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 = str(tmp_path / "temp.avif") @@ -356,7 +367,7 @@ def test_exif_invalid(self, tmp_path: Path) -> None: im.save(test_file, exif=b"invalid") @pytest.mark.parametrize( - "rot,mir,exif_orientation", + "rot, mir, exif_orientation", [ (0, 0, 4), (0, 1, 2), @@ -574,7 +585,7 @@ def test_p_mode_transparency(self) -> None: im_png.save(buf_out, format="AVIF", quality=100) with Image.open(buf_out) as expected: - assert_image_similar(im_png.convert("RGBA"), expected, 1) + 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 @@ -633,10 +644,10 @@ def test_write_animation_L(self, tmp_path: Path) -> None: assert im.n_frames == orig.n_frames # Compare first and second-to-last frames to the original animated GIF - assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) + assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 2.25) orig.seek(orig.n_frames - 2) im.seek(im.n_frames - 2) - assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 25.0) + assert_image_similar(im.convert("RGB"), orig.convert("RGB"), 2.54) def test_write_animation_RGB(self, tmp_path: Path) -> None: """ @@ -649,11 +660,11 @@ def check(temp_file: str) -> None: assert im.n_frames == 4 # Compare first frame to original - assert_image_similar(im, frame1.convert("RGBA"), 25.0) + assert_image_similar(im, frame1.convert("RGBA"), 2.7) # Compare second frame to original im.seek(1) - assert_image_similar(im, frame2.convert("RGBA"), 25.0) + assert_image_similar(im, frame2.convert("RGBA"), 4.1) with self.star_frames() as frames: frame1 = frames[0] diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index 29796a74dcc..ef4d30cbb39 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -7,7 +7,7 @@ version=1.1.1 pushd libavif-$version -if uname -s | grep -q Darwin; then +if [ $(uname) == "Darwin" ]; then PREFIX=$(brew --prefix) else PREFIX=/usr @@ -49,15 +49,16 @@ if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) fi -cmake -G Ninja -S . -B build \ +cmake \ -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DAVIF_LIBSHARPYUV=LOCAL \ - -DAVIF_LIBYUV=LOCAL \ - -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_MACOSX_RPATH=OFF \ - "${LIBAVIF_CMAKE_FLAGS[@]}" + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + "${LIBAVIF_CMAKE_FLAGS[@]}" \ + . -sudo ninja -C build install +sudo make install popd diff --git a/src/_avif.c b/src/_avif.c index 9d83ec241cd..d86ab340a42 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -78,34 +78,39 @@ exc_type_for_avif_result(avifResult result) { static uint8_t irot_imir_to_exif_orientation(const avifImage *image) { + uint8_t axis; #if AVIF_VERSION_MAJOR >= 1 - uint8_t axis = image->imir.axis; + axis = image->imir.axis; #else - uint8_t axis = image->imir.mode; + axis = image->imir.mode; #endif uint8_t angle = image->irot.angle; - int irot = !!(image->transformFlags & AVIF_TRANSFORM_IROT); - int imir = !!(image->transformFlags & AVIF_TRANSFORM_IMIR); - if (irot && 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. + int imir = image->transformFlags & AVIF_TRANSFORM_IMIR; + int irot = image->transformFlags & AVIF_TRANSFORM_IROT; + if (irot) { + 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. } - return 6; // 90 degrees anti-clockwise. - } - if (irot && 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. + 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. } - return 3; // 180 degrees anti-clockwise. - } - if (irot && 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. + 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. } - return 8; // 270 degrees anti-clockwise. } if (imir) { return axis ? 2 // Swap left and right. @@ -116,18 +121,13 @@ irot_imir_to_exif_orientation(const avifImage *image) { static void exif_orientation_to_irot_imir(avifImage *image, int orientation) { - const avifTransformFlags otherFlags = - image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR); - - // // 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 = otherFlags | AVIF_TRANSFORM_IMIR; - image->irot.angle = 0; // ignored + image->transformFlags |= AVIF_TRANSFORM_IMIR; #if AVIF_VERSION_MAJOR >= 1 image->imir.axis = 1; #else @@ -136,67 +136,34 @@ exif_orientation_to_irot_imir(avifImage *image, int orientation) { 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 = otherFlags | AVIF_TRANSFORM_IROT; + image->transformFlags |= AVIF_TRANSFORM_IROT; image->irot.angle = 2; -#if AVIF_VERSION_MAJOR >= 1 - image->imir.axis = 0; // ignored -#else - image->imir.mode = 0; // ignored -#endif 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 = otherFlags | AVIF_TRANSFORM_IMIR; - image->irot.angle = 0; // ignored -#if AVIF_VERSION_MAJOR >= 1 - image->imir.axis = 0; -#else - image->imir.mode = 0; -#endif + 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 = - otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + 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 -#if AVIF_VERSION_MAJOR >= 1 - image->imir.axis = 0; -#else - image->imir.mode = 0; -#endif 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 = otherFlags | AVIF_TRANSFORM_IROT; + image->transformFlags |= AVIF_TRANSFORM_IROT; image->irot.angle = 3; -#if AVIF_VERSION_MAJOR >= 1 - image->imir.axis = 0; // ignored -#else - image->imir.mode = 0; // ignored -#endif 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 = - otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + 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 -#if AVIF_VERSION_MAJOR >= 1 - image->imir.axis = 0; -#else - image->imir.mode = 0; -#endif 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 = otherFlags | AVIF_TRANSFORM_IROT; + image->transformFlags |= AVIF_TRANSFORM_IROT; image->irot.angle = 1; -#if AVIF_VERSION_MAJOR >= 1 - image->imir.axis = 0; // ignored -#else - image->imir.mode = 0; // ignored -#endif break; } } @@ -381,13 +348,8 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { enc_options.tile_rows_log2 = normalize_tiles_log2(tile_rows_log2); enc_options.tile_cols_log2 = normalize_tiles_log2(tile_cols_log2); - - if (alpha_premultiplied == Py_True) { - enc_options.alpha_premultiplied = AVIF_TRUE; - } else { - enc_options.alpha_premultiplied = AVIF_FALSE; - } - + enc_options.alpha_premultiplied = + (alpha_premultiplied == Py_True) ? AVIF_TRUE : AVIF_FALSE; enc_options.autotiling = (autotiling == Py_True) ? AVIF_TRUE : AVIF_FALSE; // Create a new animation encoder and picture frame @@ -573,9 +535,9 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { return NULL; } - is_first_frame = (self->frame_index == -1); + is_first_frame = self->frame_index == -1; - if ((image->width != width) || (image->height != height)) { + if (image->width != width || image->height != height) { PyErr_Format( PyExc_ValueError, "Image sequence dimensions mismatch, %ux%u != %ux%u", diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c84f1000b39..32dcdb3c86e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -391,7 +391,6 @@ def cmd_msbuild( "libavif": { "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", "filename": f"libavif-{V['LIBAVIF']}.zip", - "dir": f"libavif-{V['LIBAVIF']}", "license": "LICENSE", "build": [ f"{sys.executable} -m pip install meson", From 93289324c681463e75c1d25605a56e624f61c93f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 4 Jan 2025 02:56:02 +1100 Subject: [PATCH 21/22] Removed avifEncOptions (#14) * Fixed indentation * Removed avifEncOptions * Destroy encoder on failure * Destroy image on failure * Delete encoder object on failure * Corrected comment * Allow libavif to install rav1e on manylinux2014 --------- Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 6 +- src/_avif.c | 379 +++++++++++------------ 2 files changed, 183 insertions(+), 202 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3310902bec5..eb09e30d5f6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -106,8 +106,7 @@ function build_harfbuzz { function build_libavif { if [ -e libavif-stamp ]; then return; fi - if [[ "$MB_ML_VER" == 2014 ]] || [[ "$PLAT" == "aarch64" ]]; then - # Once Amazon 2 is EOL on 30 June 2025, manylinux2014 will no longer be needed + if [[ "$PLAT" == "aarch64" ]]; then # Once GitHub Actions supports aarch64 without emulation, this will no longer needed as building will be faster if [[ "$PLAT" == "aarch64" ]]; then suffix="aarch64" @@ -137,6 +136,9 @@ EOF if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum install -y perl + if [[ "$MB_ML_VER" == 2014 ]]; then + yum install -y perl-IPC-Cmd + fi fi rav1e=LOCAL diff --git a/src/_avif.c b/src/_avif.c index d86ab340a42..e1509a16134 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -3,20 +3,6 @@ #include #include "avif/avif.h" -typedef struct { - avifPixelFormat subsampling; - int qmin; - int qmax; - int quality; - int speed; - avifCodecChoice codec; - avifRange range; - avifBool alpha_premultiplied; - int tile_rows_log2; - int tile_cols_log2; - avifBool autotiling; -} avifEncOptions; - // Encoder type typedef struct { PyObject_HEAD avifEncoder *encoder; @@ -241,9 +227,8 @@ _add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { PyObject * AvifEncoderNew(PyObject *self_, PyObject *args) { unsigned int width, height; - avifEncOptions enc_options; AvifEncoderObject *self = NULL; - avifEncoder *encoder = NULL; + avifEncoder *encoder; char *subsampling; int qmin; @@ -291,201 +276,194 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { return NULL; } + // Create a new animation encoder and picture frame + avifImage *image = avifImageCreateEmpty(); + + // 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"); + return NULL; + } if (strcmp(subsampling, "4:0:0") == 0) { - enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV400; + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; } else if (strcmp(subsampling, "4:2:0") == 0) { - enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV420; + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420; } else if (strcmp(subsampling, "4:2:2") == 0) { - enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV422; + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422; } else if (strcmp(subsampling, "4:4:4") == 0) { - enc_options.subsampling = AVIF_PIXEL_FORMAT_YUV444; + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; } else { PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); return NULL; } - if (qmin == -1 || qmax == -1) { -#if AVIF_VERSION >= 1000000 - enc_options.qmin = -1; - enc_options.qmax = -1; -#else - enc_options.qmin = normalize_quantize_value(64 - quality); - enc_options.qmax = normalize_quantize_value(100 - quality); -#endif - } else { - enc_options.qmin = normalize_quantize_value(qmin); - enc_options.qmax = normalize_quantize_value(qmax); - } - enc_options.quality = quality; - - if (speed < AVIF_SPEED_SLOWEST) { - speed = AVIF_SPEED_SLOWEST; - } else if (speed > AVIF_SPEED_FASTEST) { - speed = AVIF_SPEED_FASTEST; - } - enc_options.speed = speed; - - if (strcmp(codec, "auto") == 0) { - enc_options.codec = AVIF_CODEC_CHOICE_AUTO; - } else { - enc_options.codec = avifCodecChoiceFromName(codec); - } - - if (strcmp(range, "full") == 0) { - enc_options.range = AVIF_RANGE_FULL; - } else if (strcmp(range, "limited") == 0) { - enc_options.range = AVIF_RANGE_LIMITED; - } else { - PyErr_SetString(PyExc_ValueError, "Invalid range"); - return NULL; - } + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; // Validate canvas dimensions if (width <= 0 || height <= 0) { PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); + avifImageDestroy(image); return NULL; } + image->width = width; + image->height = height; - enc_options.tile_rows_log2 = normalize_tiles_log2(tile_rows_log2); - enc_options.tile_cols_log2 = normalize_tiles_log2(tile_cols_log2); - enc_options.alpha_premultiplied = - (alpha_premultiplied == Py_True) ? AVIF_TRUE : AVIF_FALSE; - enc_options.autotiling = (autotiling == Py_True) ? AVIF_TRUE : AVIF_FALSE; - - // Create a new animation encoder and picture frame - self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); - if (self) { - self->icc_bytes = NULL; - self->exif_bytes = NULL; - self->xmp_bytes = NULL; + image->depth = 8; +#if AVIF_VERSION >= 90000 + image->alphaPremultiplied = alpha_premultiplied == Py_True ? AVIF_TRUE : AVIF_FALSE; +#endif - encoder = avifEncoderCreate(); + encoder = avifEncoderCreate(); - int is_aom_encode = strcmp(codec, "aom") == 0 || - (strcmp(codec, "auto") == 0 && - _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE)); + 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->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads; + if (qmin == -1 || qmax == -1) { #if AVIF_VERSION >= 1000000 - if (enc_options.qmin != -1 && enc_options.qmax != -1) { - encoder->minQuantizer = enc_options.qmin; - encoder->maxQuantizer = enc_options.qmax; - } else { - encoder->quality = enc_options.quality; - } + encoder->quality = quality; #else - encoder->minQuantizer = enc_options.qmin; - encoder->maxQuantizer = enc_options.qmax; + encoder->minQuantizer = normalize_quantize_value(64 - quality); + encoder->maxQuantizer = normalize_quantize_value(100 - quality); #endif - encoder->codecChoice = enc_options.codec; - encoder->speed = enc_options.speed; - encoder->timescale = (uint64_t)1000; - encoder->tileRowsLog2 = enc_options.tile_rows_log2; - encoder->tileColsLog2 = enc_options.tile_cols_log2; + } else { + encoder->minQuantizer = normalize_quantize_value(qmin); + encoder->maxQuantizer = normalize_quantize_value(qmax); + } + + 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->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2); + encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2); #if AVIF_VERSION >= 110000 - encoder->autoTiling = enc_options.autotiling; + encoder->autoTiling = autotiling == Py_True ? AVIF_TRUE : AVIF_FALSE; #endif - if (advanced != Py_None) { + if (advanced != Py_None) { #if AVIF_VERSION >= 80200 - if (_add_codec_specific_options(encoder, advanced)) { - return NULL; - } -#else - PyErr_SetString( - PyExc_ValueError, "Advanced codec options require libavif >= 0.8.2" - ); + if (_add_codec_specific_options(encoder, advanced)) { + avifImageDestroy(image); + avifEncoderDestroy(encoder); return NULL; -#endif } - - self->encoder = encoder; - - avifImage *image = avifImageCreateEmpty(); - // Set these in advance so any upcoming RGB -> YUV use the proper coefficients - image->yuvRange = enc_options.range; - image->yuvFormat = enc_options.subsampling; - image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; - image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; - image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; - image->width = width; - image->height = height; - image->depth = 8; -#if AVIF_VERSION >= 90000 - image->alphaPremultiplied = enc_options.alpha_premultiplied; +#else + PyErr_SetString( + PyExc_ValueError, "Advanced codec options require libavif >= 0.8.2" + ); + avifImageDestroy(image); + avifEncoderDestroy(encoder); + return NULL; #endif + } - avifResult result; - if (PyBytes_GET_SIZE(icc_bytes)) { - self->icc_bytes = icc_bytes; - Py_INCREF(icc_bytes); + self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); + avifImageDestroy(image); + avifEncoderDestroy(encoder); + return NULL; + } + self->frame_index = -1; + self->icc_bytes = NULL; + self->exif_bytes = NULL; + self->xmp_bytes = NULL; + + avifResult result; + if (PyBytes_GET_SIZE(icc_bytes)) { + self->icc_bytes = icc_bytes; + Py_INCREF(icc_bytes); - result = avifImageSetProfileICC( - image, - (uint8_t *)PyBytes_AS_STRING(icc_bytes), - PyBytes_GET_SIZE(icc_bytes) + result = avifImageSetProfileICC( + image, (uint8_t *)PyBytes_AS_STRING(icc_bytes), PyBytes_GET_SIZE(icc_bytes) + ); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting ICC profile failed: %s", + avifResultToString(result) ); - if (result != AVIF_RESULT_OK) { - PyErr_Format( - exc_type_for_avif_result(result), - "Setting ICC profile failed: %s", - avifResultToString(result) - ); - return NULL; - } - } else { - image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; - image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + avifImageDestroy(image); + avifEncoderDestroy(encoder); + Py_XDECREF(self->icc_bytes); + PyObject_Del(self); + return NULL; } + } else { + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + } - if (PyBytes_GET_SIZE(exif_bytes)) { - self->exif_bytes = exif_bytes; - Py_INCREF(exif_bytes); + if (PyBytes_GET_SIZE(exif_bytes)) { + self->exif_bytes = exif_bytes; + Py_INCREF(exif_bytes); - result = avifImageSetMetadataExif( - image, - (uint8_t *)PyBytes_AS_STRING(exif_bytes), - PyBytes_GET_SIZE(exif_bytes) + result = avifImageSetMetadataExif( + image, + (uint8_t *)PyBytes_AS_STRING(exif_bytes), + PyBytes_GET_SIZE(exif_bytes) + ); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting EXIF data failed: %s", + avifResultToString(result) ); - if (result != AVIF_RESULT_OK) { - PyErr_Format( - exc_type_for_avif_result(result), - "Setting EXIF data failed: %s", - avifResultToString(result) - ); - return NULL; - } + avifImageDestroy(image); + avifEncoderDestroy(encoder); + Py_XDECREF(self->icc_bytes); + Py_XDECREF(self->exif_bytes); + PyObject_Del(self); + return NULL; } - if (PyBytes_GET_SIZE(xmp_bytes)) { - self->xmp_bytes = xmp_bytes; - Py_INCREF(xmp_bytes); - - result = avifImageSetMetadataXMP( - image, - (uint8_t *)PyBytes_AS_STRING(xmp_bytes), - PyBytes_GET_SIZE(xmp_bytes) + } + if (PyBytes_GET_SIZE(xmp_bytes)) { + self->xmp_bytes = xmp_bytes; + Py_INCREF(xmp_bytes); + + result = avifImageSetMetadataXMP( + image, (uint8_t *)PyBytes_AS_STRING(xmp_bytes), PyBytes_GET_SIZE(xmp_bytes) + ); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting XMP data failed: %s", + avifResultToString(result) ); - if (result != AVIF_RESULT_OK) { - PyErr_Format( - exc_type_for_avif_result(result), - "Setting XMP data failed: %s", - avifResultToString(result) - ); - return NULL; - } - } - if (exif_orientation > 1) { - exif_orientation_to_irot_imir(image, exif_orientation); + avifImageDestroy(image); + avifEncoderDestroy(encoder); + Py_XDECREF(self->icc_bytes); + Py_XDECREF(self->exif_bytes); + Py_XDECREF(self->xmp_bytes); + PyObject_Del(self); + return NULL; } + } + if (exif_orientation > 1) { + exif_orientation_to_irot_imir(image, exif_orientation); + } - self->image = image; - self->frame_index = -1; + self->image = image; + self->encoder = encoder; - return (PyObject *)self; - } - PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); - return NULL; + return (PyObject *)self; } PyObject * @@ -606,10 +584,11 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { // rgb.pixels is safe for writes memcpy(rgb.pixels, rgb_bytes, size); - Py_BEGIN_ALLOW_THREADS result = avifImageRGBToYUV(frame, &rgb); - Py_END_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; + result = avifImageRGBToYUV(frame, &rgb); + Py_END_ALLOW_THREADS; - if (result != AVIF_RESULT_OK) { + if (result != AVIF_RESULT_OK) { PyErr_Format( exc_type_for_avif_result(result), "Conversion to YUV failed: %s", @@ -624,11 +603,11 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE; } - Py_BEGIN_ALLOW_THREADS result = - avifEncoderAddImage(encoder, frame, duration, addImageFlags); - Py_END_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderAddImage(encoder, frame, duration, addImageFlags); + Py_END_ALLOW_THREADS; - if (result != AVIF_RESULT_OK) { + if (result != AVIF_RESULT_OK) { PyErr_Format( exc_type_for_avif_result(result), "Failed to encode image: %s", @@ -660,10 +639,11 @@ _encoder_finish(AvifEncoderObject *self) { avifResult result; PyObject *ret = NULL; - Py_BEGIN_ALLOW_THREADS result = avifEncoderFinish(encoder, &raw); - Py_END_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderFinish(encoder, &raw); + Py_END_ALLOW_THREADS; - if (result != AVIF_RESULT_OK) { + if (result != AVIF_RESULT_OK) { PyErr_Format( exc_type_for_avif_result(result), "Failed to finish encoding: %s", @@ -685,6 +665,7 @@ PyObject * AvifDecoderNew(PyObject *self_, PyObject *args) { PyObject *avif_bytes; AvifDecoderObject *self = NULL; + avifDecoder *decoder; char *codec_str; avifCodecChoice codec; @@ -707,29 +688,26 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); return NULL; } - self->decoder = NULL; Py_INCREF(avif_bytes); self->data = avif_bytes; - self->decoder = avifDecoderCreate(); + decoder = avifDecoderCreate(); #if AVIF_VERSION >= 80400 - self->decoder->maxThreads = max_threads; + decoder->maxThreads = max_threads; #endif #if AVIF_VERSION >= 90200 // Turn off libavif's 'clap' (clean aperture) property validation. - self->decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; + decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; // Allow the PixelInformationProperty ('pixi') to be missing in AV1 image // items. libheif v1.11.0 and older does not add the 'pixi' item property to // AV1 image items. - self->decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; + decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; #endif - self->decoder->codecChoice = codec; + decoder->codecChoice = codec; result = avifDecoderSetIOMemory( - self->decoder, - (uint8_t *)PyBytes_AS_STRING(self->data), - PyBytes_GET_SIZE(self->data) + decoder, (uint8_t *)PyBytes_AS_STRING(self->data), PyBytes_GET_SIZE(self->data) ); if (result != AVIF_RESULT_OK) { PyErr_Format( @@ -737,31 +715,31 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { "Setting IO memory failed: %s", avifResultToString(result) ); - avifDecoderDestroy(self->decoder); - self->decoder = NULL; - Py_DECREF(self); + avifDecoderDestroy(decoder); + PyObject_Del(self); return NULL; } - result = avifDecoderParse(self->decoder); + result = avifDecoderParse(decoder); if (result != AVIF_RESULT_OK) { PyErr_Format( exc_type_for_avif_result(result), "Failed to decode image: %s", avifResultToString(result) ); - avifDecoderDestroy(self->decoder); - self->decoder = NULL; - Py_DECREF(self); + avifDecoderDestroy(decoder); + PyObject_Del(self); return NULL; } - if (self->decoder->alphaPresent) { + if (decoder->alphaPresent) { self->mode = "RGBA"; } else { self->mode = "RGB"; } + self->decoder = decoder; + return (PyObject *)self; } @@ -876,10 +854,11 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { return NULL; } - Py_BEGIN_ALLOW_THREADS result = avifImageYUVToRGB(image, &rgb); - Py_END_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; + result = avifImageYUVToRGB(image, &rgb); + Py_END_ALLOW_THREADS; - if (result != AVIF_RESULT_OK) { + if (result != AVIF_RESULT_OK) { PyErr_Format( exc_type_for_avif_result(result), "Conversion from YUV failed: %s", @@ -918,7 +897,7 @@ static struct PyMethodDef _encoder_methods[] = { {NULL, NULL} /* sentinel */ }; -// AvifDecoder type definition +// AvifEncoder type definition static PyTypeObject AvifEncoder_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder", .tp_basicsize = sizeof(AvifEncoderObject), From 4c63ea61864281571ca51b169d3026e3aed6064c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 16 Jan 2025 05:19:55 +1100 Subject: [PATCH 22/22] Fixed series of tuples as advanced argument (#15) * Removed check_avif_leaks.py * Removed _VALID_AVIF_MODES * Fixed series of tuples as advanced argument * Do not pass advanced values to C as bytes * Simplified code * Reuse size * Destroy image on failure * Rearranged image settings * Fixed typo * Test roundtrip colors from premultiplied alpha --------- Co-authored-by: Andrew Murray --- Tests/check_avif_leaks.py | 43 --------------------- Tests/test_file_avif.py | 78 ++++++++++++++------------------------ src/PIL/AvifImagePlugin.py | 22 ++++------- src/_avif.c | 40 +++++++++++-------- 4 files changed, 60 insertions(+), 123 deletions(-) delete mode 100644 Tests/check_avif_leaks.py diff --git a/Tests/check_avif_leaks.py b/Tests/check_avif_leaks.py deleted file mode 100644 index 343349dd6a1..00000000000 --- a/Tests/check_avif_leaks.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -from io import BytesIO - -import pytest - -from PIL import Image - -from .helper import is_win32, skip_unless_feature - -# Limits for testing the leak -mem_limit = 1024 * 1048576 -stack_size = 8 * 1048576 -iterations = int((mem_limit / stack_size) * 2) -test_file = "Tests/images/avif/hopper.avif" - -pytestmark = [ - pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), - skip_unless_feature("avif"), -] - - -def test_leak_load() -> None: - from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit - - setrlimit(RLIMIT_STACK, (stack_size, stack_size)) - setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) - for _ in range(iterations): - with Image.open(test_file) as im: - im.load() - - -def test_leak_save() -> None: - from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit - - setrlimit(RLIMIT_STACK, (stack_size, stack_size)) - setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) - for _ in range(iterations): - test_output = BytesIO() - with Image.open(test_file) as im: - im.save(test_output, "AVIF") - test_output.seek(0) - test_output.read() diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 7a92f781a9c..3f3e2ad0b1f 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -8,7 +8,6 @@ from contextlib import contextmanager from io import BytesIO from pathlib import Path -from struct import unpack from typing import Any import pytest @@ -75,39 +74,6 @@ def is_docker_qemu() -> bool: return "qemu" in init_proc_exe -def has_alpha_premultiplied(im_bytes: bytes) -> bool: - stream = BytesIO(im_bytes) - length = len(im_bytes) - while stream.tell() < length: - start = stream.tell() - size, boxtype = unpack(">L4s", stream.read(8)) - if not all(0x20 <= c <= 0x7E for c in boxtype): - # Not ascii - return False - if size == 1: # 64bit size - (size,) = unpack(">Q", stream.read(8)) - end = start + size - version, _ = unpack(">B3s", stream.read(4)) - if boxtype in (b"ftyp", b"hdlr", b"pitm", b"iloc", b"iinf"): - # Skip these boxes - stream.seek(end) - continue - elif boxtype == b"meta": - # Container box possibly including iref prem, continue to parse boxes - # inside it - continue - elif boxtype == b"iref": - while stream.tell() < end: - _, iref_type = unpack(">L4s", stream.read(8)) - version, _ = unpack(">B3s", stream.read(4)) - if iref_type == b"prem": - return True - stream.read(2 if version == 0 else 4) - else: - return False - return False - - class TestUnsupportedAvif: def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) @@ -170,10 +136,8 @@ def _roundtrip(self, tmp_path: Path, mode: str, epsilon: float) -> None: # 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. - target = hopper(mode) - if mode != "RGB": - target = target.convert("RGB") - assert_image_similar(image, target, epsilon) + expected = hopper() + assert_image_similar(image, expected, epsilon) def test_write_rgb(self, tmp_path: Path) -> None: """ @@ -479,7 +443,19 @@ def test_encoder_codec_cannot_encode(self, tmp_path: Path) -> None: @skip_unless_avif_encoder("aom") @skip_unless_feature("avif") - def test_encoder_advanced_codec_options(self) -> None: + @pytest.mark.parametrize( + "advanced", + [ + { + "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] | tuple[tuple[str, str], ...] + ) -> None: with Image.open(TEST_AVIF_FILE) as im: ctrl_buf = BytesIO() im.save(ctrl_buf, "AVIF", codec="aom") @@ -488,10 +464,7 @@ def test_encoder_advanced_codec_options(self) -> None: test_buf, "AVIF", codec="aom", - advanced={ - "aq-mode": "1", - "enable-chroma-deltaq": "1", - }, + advanced=advanced, ) assert ctrl_buf.getvalue() != test_buf.getvalue() @@ -699,13 +672,18 @@ def test_heif_raises_unidentified_image_error(self) -> None: with Image.open("Tests/images/avif/rgba10.heif"): pass - @pytest.mark.parametrize("alpha_premultipled", [False, True]) - def test_alpha_premultiplied_true(self, alpha_premultipled: bool) -> None: - im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) - im_buf = BytesIO() - im.save(im_buf, "AVIF", alpha_premultiplied=alpha_premultipled) - im_bytes = im_buf.getvalue() - assert has_alpha_premultiplied(im_bytes) is alpha_premultipled + @pytest.mark.parametrize("alpha_premultiplied", [False, True]) + def test_alpha_premultiplied( + self, tmp_path: Path, alpha_premultiplied: bool + ) -> None: + temp_file = str(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: """ diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index 7bd1e84c58d..2696599c56d 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -18,8 +18,6 @@ DECODE_CODEC_CHOICE = "auto" DEFAULT_MAX_THREADS = 0 -_VALID_AVIF_MODES = {"RGB", "RGBA"} - def _accept(prefix: bytes) -> bool | str: if prefix[4:8] != b"ftyp": @@ -41,8 +39,7 @@ def _accept(prefix: bytes) -> bool | str: ): if not SUPPORTED: return ( - "image file could not be identified because AVIF " - "support not installed" + "image file could not be identified because AVIF support not installed" ) return True return False @@ -63,9 +60,6 @@ class AvifImageFile(ImageFile.ImageFile): __loaded = -1 __frame = 0 - def load_seek(self, pos: int) -> None: - pass - def _open(self) -> None: if not SUPPORTED: msg = ( @@ -136,6 +130,9 @@ def load(self) -> Image.core.PixelAccess | None: return super().load() + def load_seek(self, pos: int) -> None: + pass + def tell(self) -> int: return self.__frame @@ -200,24 +197,21 @@ def _save( xmp = xmp.encode("utf-8") advanced = info.get("advanced") - if isinstance(advanced, dict): - advanced = tuple([k, v] for (k, v) in advanced.items()) if advanced is not None: + if isinstance(advanced, dict): + advanced = advanced.items() try: advanced = tuple(advanced) except TypeError: invalid = True else: - invalid = all(isinstance(v, tuple) and len(v) == 2 for v in advanced) + 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) - advanced = tuple( - (str(k).encode("utf-8"), str(v).encode("utf-8")) for k, v in advanced - ) # Setup the AVIF encoder enc = _avif.AvifEncoder( @@ -257,7 +251,7 @@ def _save( # Make sure image mode is supported frame = ims rawmode = ims.mode - if ims.mode not in _VALID_AVIF_MODES: + if ims.mode not in {"RGB", "RGBA"}: rawmode = "RGBA" if ims.has_transparency_data else "RGB" frame = ims.convert(rawmode) diff --git a/src/_avif.c b/src/_avif.c index e1509a16134..d2ec6438996 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -188,7 +188,6 @@ static int _add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { Py_ssize_t i, size; PyObject *keyval, *py_key, *py_val; - char *key, *val; if (!PyTuple_Check(opts)) { PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); return 1; @@ -203,12 +202,16 @@ _add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { } py_key = PyTuple_GetItem(keyval, 0); py_val = PyTuple_GetItem(keyval, 1); - if (!PyBytes_Check(py_key) || !PyBytes_Check(py_val)) { + 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; } - key = PyBytes_AsString(py_key); - val = PyBytes_AsString(py_val); avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val); if (result != AVIF_RESULT_OK) { @@ -286,6 +289,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { image->yuvRange = AVIF_RANGE_LIMITED; } else { PyErr_SetString(PyExc_ValueError, "Invalid range"); + avifImageDestroy(image); return NULL; } if (strcmp(subsampling, "4:0:0") == 0) { @@ -298,13 +302,10 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; } else { PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); + avifImageDestroy(image); return NULL; } - image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; - image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; - image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; - // Validate canvas dimensions if (width <= 0 || height <= 0) { PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); @@ -387,12 +388,13 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { self->xmp_bytes = NULL; avifResult result; - if (PyBytes_GET_SIZE(icc_bytes)) { + Py_ssize_t size = PyBytes_GET_SIZE(icc_bytes); + if (size) { self->icc_bytes = icc_bytes; Py_INCREF(icc_bytes); result = avifImageSetProfileICC( - image, (uint8_t *)PyBytes_AS_STRING(icc_bytes), PyBytes_GET_SIZE(icc_bytes) + image, (uint8_t *)PyBytes_AS_STRING(icc_bytes), size ); if (result != AVIF_RESULT_OK) { PyErr_Format( @@ -406,19 +408,23 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { PyObject_Del(self); return NULL; } + // 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 (PyBytes_GET_SIZE(exif_bytes)) { + size = PyBytes_GET_SIZE(exif_bytes); + if (size) { self->exif_bytes = exif_bytes; Py_INCREF(exif_bytes); result = avifImageSetMetadataExif( - image, - (uint8_t *)PyBytes_AS_STRING(exif_bytes), - PyBytes_GET_SIZE(exif_bytes) + image, (uint8_t *)PyBytes_AS_STRING(exif_bytes), size ); if (result != AVIF_RESULT_OK) { PyErr_Format( @@ -434,12 +440,14 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { return NULL; } } - if (PyBytes_GET_SIZE(xmp_bytes)) { + + size = PyBytes_GET_SIZE(xmp_bytes); + if (size) { self->xmp_bytes = xmp_bytes; Py_INCREF(xmp_bytes); result = avifImageSetMetadataXMP( - image, (uint8_t *)PyBytes_AS_STRING(xmp_bytes), PyBytes_GET_SIZE(xmp_bytes) + image, (uint8_t *)PyBytes_AS_STRING(xmp_bytes), size ); if (result != AVIF_RESULT_OK) { PyErr_Format(