From 6dae23a32467c0ff9c20d1b1e2f3d8b71a64383e Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Sun, 3 Jan 2021 15:12:01 -0500 Subject: [PATCH] Add AVIF plugin (using libavif) --- .ci/install.sh | 6 +- .github/workflows/macos-install.sh | 5 +- .github/workflows/test-cygwin.yml | 1 + .github/workflows/test-mingw.yml | 1 + .github/workflows/test-windows.yml | 6 + Tests/check_avif_leaks.py | 42 + Tests/helper.py | 2 + 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 | 782 +++++++++++++++++ depends/install_libavif.sh | 83 ++ depends/libavif-1.0.1-local-static.patch | 148 ++++ docs/handbook/image-file-formats.rst | 75 +- docs/installation.rst | 30 +- docs/reference/features.rst | 1 + docs/reference/plugins.rst | 8 + setup.py | 16 + src/PIL/AvifImagePlugin.py | 260 ++++++ src/PIL/Image.py | 4 +- src/PIL/__init__.py | 1 + src/PIL/features.py | 2 + src/_avif.c | 908 ++++++++++++++++++++ winbuild/build.rst | 1 + winbuild/build_prepare.py | 45 + 36 files changed, 2419 insertions(+), 8 deletions(-) create mode 100644 Tests/check_avif_leaks.py 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 depends/libavif-1.0.1-local-static.patch create mode 100644 src/PIL/AvifImagePlugin.py create mode 100644 src/_avif.c diff --git a/.ci/install.sh b/.ci/install.sh index 4748feb3d49..a013e177746 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 libffi-dev 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 @@ -55,6 +56,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 a20838a1507..374bbdc7edf 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,7 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas libraqm dav1d aom rav1e export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" PYTHONOPTIMIZE=0 python3 -m pip install cffi @@ -16,5 +16,8 @@ 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-cygwin.yml b/.github/workflows/test-cygwin.yml index 5071e5bd442..7ce88771afc 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -54,6 +54,7 @@ jobs: liblcms2-devel libopenjp2-devel libraqm-devel + libavif-devel libtiff-devel libwebp-devel libxcb-devel diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 08dfb9a2da0..8b657019ea7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -50,6 +50,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 ae3cc6127da..2213c03769e 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -66,6 +66,8 @@ jobs: 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH + python -m pip install meson + choco install ghostscript --version=10.0.0.20230317 echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH @@ -149,6 +151,10 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_fribidi.cmd" + - name: Build dependencies / libavif + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libavif.cmd" + # trim ~150MB for each job - name: Optimize build cache if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/Tests/check_avif_leaks.py b/Tests/check_avif_leaks.py new file mode 100644 index 00000000000..57818efcbee --- /dev/null +++ b/Tests/check_avif_leaks.py @@ -0,0 +1,42 @@ +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/helper.py b/Tests/helper.py index 69246bfcf45..6b18c74c62c 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -2,6 +2,7 @@ Helper functions. """ +import gc import logging import os import shutil @@ -218,6 +219,7 @@ def _test_leak(self, core): start_mem = self._get_mem_usage() for cycle in range(self.iterations): core() + gc.collect() mem = self._get_mem_usage() - start_mem msg = f"memory usage limit exceeded in iteration {cycle}" assert mem < self.mem_limit, msg 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 zcmX}Sb8u(R^F18fwzIKq+qU_NZ9Cc6wr$(CZEkFBo_&A5^?OcL&zbJice>~PF;#Qx z0s#RLnY(y87`Xw=f&Sq?wgs3o*#eBr<%F1ofq;NnY|UJZ{?q<}($dt%>HnrcKn?&C zm;X2ak8J?P|8E%t2Y`$1|1{u#BQ3zj-sC?|6bJ|e=)cH61NH|3;#2tN|DUJxkIDX- z5CCBJzfS(A;QVvU{!97q2_rW~CJ}oZ`~Nk7{r@ZfWHAqb`G59bA`ZaG?Ee-30zxn` zbvF55$^VL40UVqh{;>?e!NmR_qXC>9%>MB|{8!?CWsn>I9)SNMpiody{|L^=jY%XB z81BCis)@aglbwl;$G>Lafgl3@g)jjQwnqPH{{RdG31=dueU!mndS%A{l!iwY^{X3TeCBOwOQz{~Ikf#LsZ(1sO9itM;45v933?*se zd02#uN(%){V{#_&@$9&-AaRK1sggjHFJ&r0fbb{|O95<%x=k<=lRubd6o^psb#*A$ zO(Ji(H6s0_HMd+gd&qB9jA?mazH;wtQ_oJX=af8cNL1-7UrmtStxSX6vkwk0`~rE| zJh~RbeAr(4p9E;%Ow6xN{+r4NG5O_T(}2{~kyb5#j_j*E(g@V2vLu3`V&Hff?VQBU zp@eV6AvvI|Cyl6IA_lIBZ@XsvM>AM&K(wM2iJoRoq&tV;j!B^l6Bbt=YXq!?6`otO zL9g8quZ*?~z=A=QZBvovEpz*n)}I^FV=m}FhCpz6Vyn5ZiQfsbxU3<5_mog!lXUlg zBqjL)H(N7Gv68n~e*P$|8^N=R>3z$>gOs1@ay10_TrAq+jzBH97JI7m`hoO;R38ix zPucuCIp##=LjrS*$HAv}pd4aPT;YA@>=ztBVO%zlo#=^sn43Wr>eyA}EK`Z5=swT0 z303&uu?Igyu^R9g5=n;l69-WoXT$Z0u?oCMO@4LC-}eM{EZC_>lpH#zFXBPwTlPhG zclbo=il{G73E43-+_=x54Q?kfejxDrLK@GSRcQ0_rFTpZ>AnY_*RjGoSQ$`+#{{`9 z2wV?x1`^FPST99bx{nRC*)JBnGU=IW-VNvdKVIrl3n3cAGT)^1< zM@336XD@t)5Soc)qxdpRu$)=9Twi#?9BrwW0* zSGcp&*M4Ml0XN=00bZENjy4aZ0`uE(jYUX$-upV2l7t%2<^s=#xqO^shX&*(N}O7W z{Fq1I8M>UfM{Ag%pxBb**Ba!e%ScGN2_*!w%}4_8IMdt2^Gl>`NW#&uLn* znOTA}!ZFGK+ZKWb*!Y<~S26JY;$!v(A#4y?_JKoH4 zvX&-!%j^1j6^_sF}19<(+So z=~f=IFKSN!>?hF5>;nXub@2>OQS;bgywkd4q!}*G{PT2MmPgir1)?>`n6Wd#EGRyu!Bq>Qy_gzK$yAYm$OA=P zr{gob3{+rD5t6H5w{$LQcU*Fb95J46^(EvWZcfmzX25_cdZf_P<_j>dMcBb1LQXQ$ zsKDkj&1*$*$M>yM5;oX!jp~rlIO(`mxm)~9!fSfdB&F@9y|XvGZ+JE4V2tjT5XddG zgR82Wl%ifgXm)o#?hu&)PgAdq+2igIr~OmiI*WF9CBggD`6Z$X=X14R%mG*JEi;Gw z9rulE$@RD5H?FEzMm{Fj#j_pzm9~b*5Ogkw06CWjjF9wamKzq_rmP`xD*ux#)6u{Y za^jkQy|VKh;O`z)ZrKx`PP9LcS@jC(N}LSiI;=;^>c*>zmx$2AJgJh;&r3b!N76>; zH-7UFz_`USgvMZj3~VT&%y=G!LSR$atW^&*navha(#c^u z!buuN>8|3$@-&(zDr%2U0jh?=D*C=Dan6w{Aih(qw7xNo6cnn;*Ek?(*fb z#N}mdG7_P@Azp1WJfnnZ@I4d%DAihkc>YmeIdn;$j_}bE2+Dm$Os;J70asCc>dLHs$>ng-1Gy-O~7RTJPTNSeHVi;_BRzlhVw^Le<&g6&r0w8<8;A5&Wqyp@ACJlD)wqkmWAx?1;u16bwXSkb9g zN#c3Dxw+=O-wAS%0UpVqcTv(1-;0Mt-AZ5OoJYT)?geesBPaPyFj%_$YVV}N4@SG6 z2(tFqtdDCiK_EldI<(HiI`xacqq5BdXDA$WhoxN&0>tRnwo?*uOM1dWNFtOhkCI#i zoOg5)M)2qWGU{tYEb zMt+VRzGxZd8M+5jC)!#g^eaUisye+K)Lh&9*o$wFn0u5Il3Nukfs6RLD;6YO2}T&I zSUsHU+m;m%f}4z0AcbcmW%MKR1fP6?r{4l}^DGRo897EZx2`#?=xU0DydiPCKqtP* zASvSck&-OXXlx0*DEg_ZAzfTlHq`W-=n`=N;~84Xt69VjNDg&yQ;=WbS-vo~1>8$* zO(4>o3(ep@Z`c%{YR?5(bkBHuOO+Yr>R)>$GU>Y>3`_}c6ic)CV>bPXpfvA8Dm$A_ zK%qV4YS7U{bexUI5Lj%oVhSZH9+$cGAvQwPX~dh;Z%$c)(B*>0>K7#DzS4N772VmY zLJ-p2Aos8pk?U(lBpQ=G{E>BZy8baH=8>d*j&^1bf?Tc^s-g z7{z6t`wT&?gAR?K7vc2is%Ju0+OL*c1O{XPPP8lt90XTPj~ZfsQY970F16`E zkA+Yqdt^;GyW$ZiUV#Zhsz&RntbX_9G=MtXaF%~Y5Y{F;wwvQkD;U3W`7&;3jAc~( zev%2)i{T`%DqHNl4~p^eHh@QNR?%8j_tJ~T8dpS?V~aj~zpR8KTwf}Kuq)LUE|&UM zZqaJ3+cTWNLL$Cek2FO0Q0PMbfo0+2DrfWD_NJnKXI(z}-qLjYfYXKM@qgIv+ik3g ztE$ExG+&UnDbRGLCz#Ebr5RjuB(^xpgp>lPTY8UD-et>VCFRgh>T+=xWu$HJGT^3a zeH0GpQe2kr3pY+dV#)pWP*4nAQ7KE1*e1ja0w_ri%RUGV?eDPWDGmE~gJ2^I?MnDa zACiI0ZsjOFsnQ4u+k>KTd17VNmjU({c_B#l2-P)Vt<8Vj$zk}~$h3%IDj8P5hw6ao zY3KLbE75UF(30vZu_l1%j5P{QVgsGoUGH9cnuN9{WN%|YBf}>aj=8I8dL5z6<}#G8 zQj%^-N|#*&IeYoPqhlz5aZFq}fTBVj;2Ejw*rvP^+F+c2;9ruUcc=A_>D99UwNL^M zM&O>``^q`u6|z=n;^70D!KzGp3{!F!Hn{uFjwMe4>Tr<;JOZ$98FH3#xp7!YdX$$P zUb_3m!MXdj0e>_(k{d<=$4u$L;5hTr&41Z&$MFj5aq-F6R&8Gk_)CIaYpkgy_W+>n zweg#$Ci~%I)g}sk8^e-2Rpn^!uFU|+s3Mb<8(;LT(>QX_PGAOdQ+PD0O5AY1c2&dfzhv0|}3#MK+1_R{~Q)+lQpp{43 z>Zfewan(YzxjrMnE&D^N3oN5jwhQv_O~R#M z@crhF>;CLnmnO<_pPpZJC+Tm58aW;7yOBb4He}jLPf!@g=}+cHfQLxj1KFPrCJmjm zVW+AWK8_wQtE2wBATHLDE5f?B6=Pg9om+fg98&}ks?hlaM0mR8?eQBTPGJSel6qY; znj4+ay?+|RbCl#Me&;_FkKSrfM;%_0(cm<}bljh6FY7wA5-xF}G4DptK5n zttA*l!SDo@^RM#4I!mnEM)EoOR)L0^^arUvZ(T^YWU|TUjA06b$Z5SPbT-XkAl-Js z1GUYn7GPrAui{5>J>U@*z3fGYfc*q|{^WEBoGz)>3iYD5amio^toj9kI!F#isX zOUhaDt4O}T6oEn1oUD%iX+gj)|95sWRH-)1-jKGQjXiYRpk%FUPK-y4xQSJ$qlzHl z1^tHmE=pCuN9&EYl3KIj9Na#nrPX68)#;_US>#SPVb~nL9yj_}l}>Q}q&{-cm)H&y zizc1dU_gzTi*^NO5J8}Nsi!xbr|4@~;AD+FWIS_9jDt@odn-M1YL+pJw>^P%N*0O^ z#7?bgsOzrNEMVvV8H61i6|W3Ge2y!%GR}icZy{<_xEDrHzKd#ii*wW5ATa{Fso6L{w>n*1*fpgMq0VcHY0_=} z47AS@=F(@JnvBvM_<&KCY%E4GPTI@dx3!-#JD=Q5fa{tRfg?gWdJLDW1>FzSa`0Q9 z7YZbH*igDD!_PzUmKSwiF$jO(XKhO}0BWLA;gi(xEWZvAi1sstLJaRJGV2$cE15`$ z2w^`x`J9?96|~6%kN7US7p_R|iPZdJ_9urBn8~z;!fT3l5~IsjbB>C$&S{F+SNE z(cWx1J;-AcziN{>CITbehXZ1`?G#wlftF&d9X+>%AYA+>o25x(&W63fF#Xg1Rj|14 z6-s>A`)Vx@^bak=GZF2jqaewXb~?*?S$z?d4>>+0z^ zg|l-F*SX0ria9!+>sdAr4xDVbNtn(~gEY|_XONV>4C&2-K<2QInqAqu=F3Ytxd(R6 zY6MTyNC^!PnI0(OA(*xyX6o~?*V39)tkT)ACCk6bn-sM6m6GgzCmWI@Oy&{tG0`9Uvnju`@gS`??6yKo>y%+bnKnH8OA`q- zQyp-NX=JK4w4j&n2I|7-QD};(gZ*J}_^v(suL*I_o6I4kBjR3X5$k_r;dH$@*dq-v zOES%7(x^Rg;z^~AbJV4&e;UKhp>>BK7fr&l;VO0+Jka-wB_JlTMIN!Hkb+=h88fCi zrR3-5zY>Ia#C?chnz0X7W_g)N%gRcMlq%qu#(MWZQpPr|O1N9CX$>aPTeVrn?(N#C z1t~%bNG`ct2!BaoPC??BevoM4UTYCWOd*@Rpr*H6T38Rq`mQF7E~z8K0~viu2f8#n_iE zm7qYWSlB1Ezy`pjf(00bNB#`X^f$5WhF+-GJb2}TD!ppLU32u^nBwT=Q8pW7>$ADkw0kUaDi{#w_0AE7+hxo$`r1`?-)E zN$>ZTga^H?>1DQ1dH!Y!v@8?YZ`qq5rRu^b-X^OTJ!WNp2+a7TPx{MKHpRqw^I@=~ zE`c#IrZA(y$gXkO-nNv@U1kdULX1|4_wx1(eZ3(HqF{r!-d^bkZUaZmy!}6fb`J%K?I=EK7DUe|_GekVyLNm%J zJK%q&b8dw0gEQsiEt&M`bALz>=S$U?iSm0fW~>UF2;#9OEQLk0iy3+0nel$CSDr|Q ztd=n%&P#bemWdmk)$p+`Z72V=^*C+%;Szyy(_Am^cffpotW7A76EzCXFMJ2X{9j6$ zI`od~c4n@naBUpEslz(`9<_D5=tDJls}=lbBA8n^1z0jKCdmLJcyp>H3QW+cpyOZl zEwg)L&g0a#!IhykS(Nkc04QUBJ{KMDOlXo$xvIa73F{fQ=T@PLDm=C;^(&tQ#ZDhP z<9ftmQKFDOVaFPsMrzyq)bVDV!sOz04?R*dpM}ATlo?E`t4S!*Cb=~3CL=2qT&I23 z+T9o_=%K{5Dj)}NPEEX*raMg(l5jq zph2bl=#sL$Ix1cx=xn=_gQAbU@_{9JPlB~|$Gb9@HC5XSz1r!?dZ40D!dyBQYDOw4 zyXtNX8ZSGebJ1_O5hUQyw=^@liA64^O)K!?o7u#d-2$3DXs7)WCDBL{9L|-eREf^8 zH(N$P0QSf3hczd$!D7p zW+DFuX)Z9D)E%FN)%#+MlvkI&X-o_M1|B)&NmSh6Xg!*5B@OgY=aijTbdIJC$xIQ7 zD)lX+@6YPmH!X&4JcvNN`OvD6G9_D%#Lt& zb7#H;b(jD%i-w=BJ3xt>rXT+cMUN)G-OZViwlj|z9=delD!_UF*|KBkU)f|~ zfxqP_Jv-v88(lgQ7VVQrtbBOY+6@#v(w(JYuDEO+RD-@(iY863fG@T$P7ZS^0Q3c( zg%=CSbw9tTU!D4A{#-Wu|!HRYEyRXYcyY}GD8;w?2IB-niY0qO*Ekr<< zh5Y!mt2%>=7pM<9NGW>V(!`AME`)JUJUtecRvG){Sz7fMrj7pk*^BHp5PH(=^od)Y(@q?7KSAu@kycqB$%_NBVuHu5>VH$nCvT ze>>!-wNl~l?tSyj*ts1%%h)I3;Us3FoJ|wLwYTmURF`Ne>?^I(Oa8TWjIFY z-kUkB_lf-(Nrq$*D36HxAXbLgTzfpueR^jh-Hw|6cBf}(uJoqR;kB8xe;R`6moNW| zD$ZfC`T*uPnAzp9SD!JC!GQD%cT6Sf>SM1!xk@7~Q0N7fmhZ6W`|BDIyfwhwhULx= zX@K|E2^ZI?&+qJ&{!YUu8zVZg<~Qgzc(I))HW$a!!;0J!1%4HZpEA@=cY>u(I;QGq z2MkuL-O{rSPd1J4QjYn-#})bV0Io~MHb{%s{6+aadVrlDBAu9kM8wBzlQRD^SJr71 zC!@04Znqo%5@e9h#xD87R~i5!eIwh25`GTy8TChW%NCef34~O#0$QO->IhyPBHG^;}f zG@M4)jd9vJ&^Ph(vE?V41Z7ZT@y#9*C)?Q$%?-W#z>@(7Q`kOw^I5WL#-!zUi>z@% zqU!U?E-=;$9G|86npOU@SC(Q@1{G=cR6fOC)2i_aiYN>yD++2+rFphH+fQskaOR&J zIGC(pq|Q@)K(Jk*ku;lqSzZBu*|mf+3kd1YEm3pAasp{CjEg`*qIdne_MTOZOvP5I zqMIAXx@XuZ?$XzG=mF$;zJ-8u+j5S*sH82yUAHixN~rYlt?ab~!ptkAz>5oeSoTt^G0f+Yjjtcq#ew z26OCFHNoFllhU_B9`5QNPhO=-4_!sSmb4Om)pbzbq;N`t{6}46LV?arfPKpP(yss) zVW+TJ(!;&5x&BkIrzPUe4ddFb&7dZxiXOjX?IM@WilH+iEx70eW$bRM!_nbNy~J$? z2`?=F=B`S*wygZ4JMYc7=V=CtQ-=qdHl7KbaNq;ii;<00uq;O5 zIEe}}2%ke;Gt89N*!^FOt$!t$4>@0!eUA#A1V#CM`=|<*8s_ah9eJ=96J&pdE^j^Y zc=cpOU)OAGDl&VWe1&F?yUEDk%yic>}c1gOu`b~!b34^t;rIz((7=Y+koAL||F za8Lrir#bt$a9vuh{Bx4Tw$|;Rk7^9Q%khizsWaKkxpc)Ayq9=V=A}q?zUrTG;FCA8 z<aPnAe6$hmjdp4Znda6-8v6?l1MmI@|dSjZkU}{r*U*@`NO9T z71}(U5Wod;*=0{cftjTk!d%Q=e|h$<^e`biSNrGcEx7>1*UHrDxZymH1{wy!35ML9 zD-GWt)6d6kJ7!2iM6$iXT%h|%yi$OU66r&+h%Ep6op4>xGe$-|W_}>MR?^Yl`EK>^ zCQ-7;&X=P^CMVs^1`1LnNsfqiO6lACy2}t5S-u=5lc?=Efe-tiFRo(D1F`gWP$Vyd1^`)B(rZkAsbICU!x`gmIZb7`wc0f}QFr_@iKFm|J?tzl1M+}ZkKKgb$M zu9ZTo<#=6NlN0I4xHhK0_ec{Iy3C10VO(s0&&i8O;LSa#L-SO%$T6gV!?bq@FL3g- zATu8Z)i~XHkFylGR?)=dOFdN91AeE2XWPs9?~XrSZD{G^TL+gj7TaU=ifZb3s+B(# zLv8aOsu+NGcW#8L!Mr;w|a^^_BefJ=hJgvMV0kC_R39MtY;uULIhMm=PR(u{x)z~x>4SVtL3=2f!0Oh~!;Cr!E z)5%@d?jtdGhI^WvG?hue1_)d@5qjW8${tgQFZDQ*^e4tUUejP^I23-x_887YPJ{Vk z27zDCfMS^2X#I^B>l@Rt2Zkil+tLnqIx9w34Y0uNBQ`~(E~MoF_K1HLCNcMlaBvE! z`qiSTcteQ^KQdQ=$9FnkFe)#Bzn9Umho>tR2F@s8d`slj6}S+4BN<7nvc=Y%3x~We zDLFr8o98Lu$YvNi!eb9!j7=^cv@<=hcasTc)}k)Iyr}0lHCQYSRLU$*vP1xf%W0$x zJQ+wUg%x)}>S8)hvi&Q1+7ZO;^l_%$q*nin*uMLg$a~d|t1g8!7h)~b=`l1QZ-Z;} zuP!~*T`BV#(ck-t;0VW1QSJ;|j4Ydlb{*!IsV0^ct-9)ylF8;vLq8^4^6P^Xg z{sdq1Yk-^yo%oAnRlY@pbcwavsz-8H8py1NXc7YZ1hIX?UzLS7=?Jrs7ZJsQ|KgU648{>TwkRYx0gq_ ziMn#b^$V(g?~W=OEKz3T7hV-_@i1cI8lua4{JU0SXf#nUYlO$z?Grav(Dah48nGpH z2lBm$E937#KPhm2V|-eWw4NMW*^p)D;cDwJxSdUPp}M{aO>`UcVFAuboSmTcj1;Mc zovA#4RZ!Qz!4$`lbL(a`DT}gMBQNb8~UTlfe$wOG8CQ`$?C5A9bUcP+2@Uzt;#SoW- z@DXPOKm_&03kZa0`MhMdYTnl&A+(dzhMH$Ul7hbk-bJ*&LXqrXK2WeviMc>H(N^4U zLp>=VbAS7`?ZASWGbGlGPeSPD!R)C8{V!&Zhm3s+M9!l;kKR}n4Ft4{>`?r0qgrEYr-qQzR2Nn z;;I;y=v+HL8*jdT8oGN9EQ&m?<;qGpq7Q>+S`kA`5Xj682L`mtL`$DtLSWf4kJs)S z?~#p7R+k=R==%8sG_7d4En5suT}E@U8e@ zv&TvBUxql9-t9%wc=B~qbBH0~9d&c=?{@x%-7Rmw|G@6il_r$ZsWw?R7BYTxWa+Px zXD;=NvA4F@1@Y-+qNt zo-p@LsS%BrSjv*X4OBSKsOgs$q*g&9I8qyiBs4~AUZ?>Q!2W7cXYGu4mh!YR#^%7e zx1F3FVh&HfptYN(SK&Mq8|RA<4E%v_^9T)pN%IfE$re!%F7mGL<55`E=+H>9qD?$m z%`VXnp^#8aA>@t`WawGVn#pAvCHpPT4Y=4+U zKO&QAYu?~-aX#QU{c1^}-@N{~XduJ~*C7I|U(>8wqfEcRl&h{ z)2`i6-)h2ZuJ*Vq><5aIpOsYmNk`~+7>Z)6C!Ow1evn>4ysm_Q;o9_TLhYnVceM1i z3jBiOGxz=rd@iVfN}G^Jq&e3-Jfz! z#je3NWaFl4Jy^|1M-g3K5bi${= zgEdS!Wjwg7;U1dzniN8@NO1rDU!1h;y>2Es^WUsWsu+`(UWXc^_XNqPFNf+gXp09s z=PI1FIhLUMFuU)WtNgaIc5SJmoJ<@<#2NMg3t6ZG!&=jglpdUIhWicqj$)o#WgR4^ zU$8n2vB1WvkuG?A3^8~PvR@_xf1$`Pelm3&!4FZbjcYsYDym;30pt}&WlpXH8loJF zwfiKlx~gIJ%7wW=@t0`Z|E#?zzqrX3NsPnkK@TBR4q-xTYo|gH{`#6d4!=YY!xW+p z+rV$nk-z&RcQ&D4-0@>qo+p9hx|6FWsOG&|&2o4y4JcLaPgr(r%q)Tk~_w+1Q9*|3R~>^&M~gEN462mZreKAY>JH9 zw|mvu83w+#oChiterC|MO1x26-i{SqI>2c@pkt-so!q3rp)(0r4IbjI^D@#drfy~R z&^GL=4Iz^uH%59>V)_8#R>m!HM=mZMDouh;pnN9w`oS&-n+HMXy=LLPl(PE3+n=Ye zv(bFnqC5My`C&C}fgn%DPA=yI|NAyj&!pAmj8AR$`g4d}l3OAh5t&z(fdwAghIE_K zxvbbK((WET$XildHov%!KnMFGV*j}T1ijOx1fjRFNH;=0AZ)4?G0qUL-!I^8bNMZ{ z4Ux{3oIvq+;V79qLtMP~j=P-{{g-2iFNsVsqmNj!d7ku`0$+!2%D{^r3@^9cI`xuO0$t0i zC8Is+b3!r@fsVB{zvv{7QNE(p(WdWNX5|`f>eMt znpEL*b<-5rM&+rkqkRBhG(!QIi{QtosYNO=og0@cHZhMoZ2fQ zCjlUnJ)uea>CjDK#ePLDh%9y$sXO6)cqK_5qTIkKKlbINO9oqc36E!F5 z3EaQg!ec5E|6W{xDyr%^DM)De^l>UTk?j~8Js66E`Bneo3J_IFkbcV0DaUo1>XJ)X zu?IR1a~V-WAbgK(7Mtqlbn_(Vg2#yNdj^!KC7;L@u469jgYwrIEb7fUnQnMs-k3)V z=(sMY*J*zjE2r$!`a=DgBG8i*XEf^D#ilY#q-gf{Z; ze0Mshe&N7nB=&fv4wNx8hrk)vO}ACLWK{wB2(z_#v4c;F_aPbtS{<5XblV=h3tev^ z@S?mdfOeK3icE^v3gw@A-Kbe)=<%9RFS_4pU0eCp-c=Rg8X^_xsI@Tt9dk=70QdyO zAi=iT$I*HIn5s#`S&w@`HGBI_!JNDlWWF&pB6Ig>Ld=!ZiI))i3z3Cz*>xzD)2&^= zOvJ`*N}H0+~t)0OjfK zc^2YR=XM)6-a7HFys;YWEL1b^n62+$EA9CkOMV_vH1pm_I`)!qr|&qki$FX0ud%)| z3Wn|2A~curuMF^#YAPws1i9Z;5JR)R_rQmTp-HtY!6`s@#{Da`F|ngg(M{mL6P|WL zAXW`?<{u-1=AVru*!$cRi>md zaIpnxsnRCGFcjwkR6eH&-4rUZ_&~*U*(=$r2zv;ft}E<{!NdU<6$(1;y2p0easv!u zdj&cJ0Xg5`!!-Lre>;b^ZIpzX7Pv`?a4%1*C~OvO*8t$REPaxJ*3qEa6VNH4QK zN=zVuuKc4_loft*s3}3=CYFKQw9cH=tYAwCL(jtw@c5yVkRc-`8nKd43<=px^9r|| zSMW~4!oX=aFTvKU=iUs_rq`!lPd<=wz>eEi&;)QBmdQB8kD@Z+k3-H>AJH+R#MxUR z-1k3(0f!@-&~AsZ0=B+ZohQ`dZ2nWdUBMKz1U=gP8Ms9c@T1MT_h*ZV*h0Ex+;5_1 z5-_x0K1Cpdf_#OuCXzg-7J@D`8b2a;=L^8dU*3uI^kkEH2{`=N$&d^T;&hug`h|GaaPnwqC;O&5Q!)4V3nBwg{ntmtc5? zFDFwhS=^XytAm3py1TVO8oxo4fVCi=`tc+Lei??(Eir4Ee2LV)lYHo#MVQTiM{uyH9_ zbNpGEfYXAX<6BFUoqRlqo}g`$>R6O=JjGQ;dp~VXhTrNy8Eu>$>WG16n(us?6WFdd zNB$M$ShyZPNvi|w(V=u0=xCYpszihS@P%s_qE~XJrqOT&w)VL^oOnT72@DxmYxy`FFQ$8CQpBYKh?|K>quHK` zSkH4|$;2GWfgQ33@XV6F_A8ECi!n|xx$e#%w+%hhpL1O-;LW(r0p z85*{3NZd^gz>a_OniC1_ts}(7Ajf6F>S?_JlQek??V!(*xtttLY_RF>rr}eJYC@(hx8H*0gwK!76jbQUm2}QQ&`B-;vBMLA0Z#~;OE^NwkhgzjN~ZM zDy^zS$@j6+=T2p*2^g@swyp<$_%i5gL>8YlAdMmCmx3QERgAwY4LIbt;>+bugltSS z)2-j&Z8@|XvI8DsUQUZ zZRM&+@*CIE53V9N&)yQ;A8a`mJB5vgDivTkhM(QAjXuusbevS^HEIV?F{kffZr8B{ z9PQH`Mct}pivrC_?4zYps~%M7tJ)^kFqWM+K=;K+#BXIW@*7$#Kom{dtU zVwcn8N!eT-_;Nm$sW&=zvF<&X}k>vox{5v<_%TZTl8>DN~JdDQvt#7anU>mUpkneTLaoo_Gsb$2nCr>dmsmva) z6}`@UrvEMEjIkK)^0K-XMOW1JfAO~Yn0H|ogie6EPN^e5An~km=;6YwFqK>^5eiM! zg)|@uu@^XcE0P(4vR85BN~JQnRzZ`F;$d3^U?6J$E#C$K7Lt7a-Ir3L2}Lzc%LaI*Pgq7}%kjkM~6f5v2_la47VZ7`2##3kfZv;b1u z$n_$U+cSB%cHexUqABb7vUb=ICqRg$u^6em_&o91SKQrahO`VK!qKQm_e2LfsfmgF z+XkEci8415!Uc)$VBTR4HYP<_0*N~cL25LwbNs!^aaeSNMV?~|KZ2Y#UVtIJRB?EK zX3q5~GzKqc3QD(n@1gEOE-(XwJE@m!SeHpEk|Huu*ZMbd(lCdb`*%}*I;x?EfH`(U_Qg4t3C1iEc{Mb6W34D$AcDmKlAjixm+07D!LT_nBUw`r zalT7Qo3RWZWVfh+cuk9|DE$^n{q<>gXrgm@f1{3)C_R%oG|MqEataX#=-|z*2!2d5 z=goV7B`cqh8Zq*#_qzl(gM@yL;s&ohkm#zk?4iyC7df>4kHn|rVm&<*#?J@ns)iPPNJNWaVy0UpTD$Y zh=UtgWq3C=7!rcfG+TM0}}4EXaqCoQH)?s0369)pUan{RzBk8ckN@&*b>B3*SX&f~(+W zhqXj(wHQi$2HDo)!@f=J+qn_QB_{J<9e}6wqjs9JRLWk*nWk*okXE*|XOkv>uWkF; zf@&ZGPl<-Ip&2Gn)mz$%D?>g9w?$D%n|&3uD{0C`vsziPU7wJuyqZirmP@LN9jFiC zjx}}k4prrmqX%_fGp@hEMybJhQl-B<02P~L5`@A|JaT{vPB=93m;_b09R@4MQ8u8m zE3w*l&<+Rb*eCS#pp;v+%2gm3s4o1h(NUCpAyUD8U}}+sYw(_BLCbB+hsbuDq*TrH zSsYw5gm#_=2niDwoc4=P{}NidA6(!e3NZ&?@v75+P3pSTT|0a( z?7+naoykQ zTv%%9Y^R_kmiHVFgW)Y=w#{6K 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 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/star.png b/Tests/images/avif/star.png new file mode 100644 index 0000000000000000000000000000000000000000..468dcde005da39fc6807cfda46036dc78a1efe1c GIT binary patch literal 3844 zcmV+f5BuIr?fN3wSlaXSgJjniUG{R$dEc}9{yuv5Jn!GH{`;Kw{(_?-RM&gj-~BPw z^`2=ham7_!rSva;1avcn#a}TkC9a5R0dYk0xeuYd1SEkW;7Unnw;7fdJ6z_g1Z4vw z#sg>o{@S3L*y1u@nP@(DBg%{CeBiNCnzAoMuPDB_%vT1gD(E;_PI**S-H*8pocYS3 zzo#9zZp_b=YbbQz8Lgmr<9?|nqMA#P=QZHc3BL9QDVB<4i&5!{GcNO$N}f(8HXgtt z`n4?zIkkW@UkQx1<<^z-Ws$#dZNJiR<|~Cl_wB&a62E;p`P{8x%BdBc`AR`~xV#7C ztKkT#Eu8r#K?~h0f$PhB>^jYF`kgTJ)Dq5olTp+^ujo0I%ATtuT*G(*^kD89JpQxd z0G@ZXG_`LDP*P3dGT%fRCw9f0OEdphZ0YQfcaXJ1AYX zgv)&6QPlgXzJ3z;-r6AK)FLkPje#D?H8aEx;Jg6B4U!yQAdPn)3?Nob;WFP?oX~p- z#&`hJDJH%cP^_B5Wxlh}J)5U9?bvppIk=$gXON5KqU^E2V%7jI^PSDK<9|Sq#sd&8 zUpfJ%=^zLXxSeii;ws~G3YXo9urQR^$_FVdx+9d>F~ynhbW|S< zH@-X&YW$et%r}bmbzcvBB;;al(7ughFT<}xocT_n(PSmhyFZe0Vt^ZJ0kn{NKR#~( zi3o~wQ*NWU%m?7h14LsyfCR7iVjB0GT@5f(d0GC5xwKhYOqnbzSl zpD$k`TH^ssXSn!LK zSHDwkb(!)tzwvU^`xUnQ|Ah}rD*HdCWkgMVWE!WQEJsG4ni8p57GKtyQY(-$Je zUPg|ywS^(Hc!^|IziV1Zl+<75^M==Am+=4^5buvoi-?-K%6y|!Y1@HWrnQX05xk)V z()7u^X(3TjUzu;1`>@k^0JG3!U1h(z$$Wa?pJtJGZwHVvtz`oGDNemunm>NTw2&yM zpUjsy{w3@-9zYuJMBOYnsEf>}@BIBVX3yxw)I4r^>|=n|df4MzADORl=DozjcmQ*l zDQirNh=Q8We0unY8HR^{4WvzLDT6+|p@p^H^Q~F)sph88?_YuEeGTPi(|Ri4In;lI zwD+<|_JnCMVGwKcsph6|*jvDGu@&(yLHt&HX+^jMScH9p$(1k!yn*r?M0!DcQSBv} z=%vx$5ocpsYd+Q76b=^8ry((yqO{^^3o3IFZAEEeqz$)#=r;b?N*>sYum`0V)xC)9 z!Pj0Sv6rF2?K0=n1Exieg%R_qJ~Nd=$L8Vt^MLulJn%0BnU69LVIDBYl)f56fqe+O zQ1*cC1aB9x8`z2G?Pm6QyG3q$FKS_-{BgDLWC|Ku@wEkHE=a4(Sk`l921R=jZ!blE z56NUNv(r08RtyJOU`6ITWv-&MB7Q6CwczPolvZFd!5ysVu!kYwU4-6qUhm0duQavw zmK$(F^XdNFOcMSQ#9Ick40IW=1p8+S9MO{pwt;K|-HQ5KDSEHU1=*wHJ#dcswC|~l zkmS9fzt6}X2iNX$REh$x0zXC*e=C``U8jGTJm>3P@=jFCylc$i7!zUS!o;OWaX0X- z(?2|SdabGd+rTd)k>;omg#RKn^SfvN^Eu`dSu=zdyMP^$2y@gr+wl`^V-4kuxv)So ztKY@*R|0!Nm*%KtcH{Xso!!52;?IB&l>4ls zX4#JC-&F1)=VkBVkW||ahFK15EB9GPP4WglH&k-Ee#P^)dMKC1=U-5+sWgTo_Pk63 zzbCV@2P%2E;@xX9JKGNpw*dcMX$(iKc@aIaqEh2Qm0KqzbytDZ%$pIOt|pEncKkB~ z+$NcO4p$Q-kmXXUxda9MHY%SFBA&x$^a)ZId@hJp&4I1eQ&l0~^C-CGST&2v-;i!! zD4DH|aFvz2( zb<`<}>enRGx!#lj<9~F`Z~6=>-(#$!I~-wAMD>f3={)BYfhaL>jZw&NvMR&s1@*jSbMPU5y_2F9Bs z+ZydN9$1?BPNGM0%?$B8Bd1$B%3~|Z;g!;O_d(M_#>2|YcM_SEJ-{%lfZv$b5-XIq z7#dz>v+=<4%y$wkY`g;B`)|`aVuR;jS;{Fg3vVN#V^_i*Uj~>OX_bZ1XQp~jd7IOPD-=U*RaKh4`;rL5bNvB>JG=q9$yXvQ%!4$A;(GO8%5R( znHDl0c4xjm|8ku1DrFjl%o5WgCcc?s)Z9af`i^)(fCo6mJNszdCqKfBE*uCq}U zmUq^y?QrP++)R=bfP`r+F{a3gvch+-bAdeAwMsjtv%fI;`w)=%HMiA_uWlU(8SvoIi3)s9o5Z z`G)msobiG}7}6_Dix>+#&F9(MNRB0z)~B83v$&BQODwHVJIx1Inifz~SUOW=o%!^@ zQ!NmiP2xq<0!rjU-JhFdTEy8{XFie%>sR0+{1jysDW;=x3(E7RB~40FtgBc1Hs<(p z6~SC&9R|MueOOYRzc}sRo&%oK{@e|~2H<)#(k5YKS^bYp%QzkTGM}9@8*ieLCDpN9 z(q|a&6q3&VT+-R!#@CyHtr6>+91-i<(e|0ouFl48RQ`|@OC;0LEi!pZmh(G*MymNr zRKEc1i9lniSohksY9LqhPhE^A-Z8BM7zFty#VKEx=8qq#G>GomJe_HSe**HCz^nlE zRVXNNk!0F-nU--9yE30|ytAPQ-AH1wq&wDCV>|$9xqVR5og4631bhdG{C=}D@Ps4ss7B?iw-q;~oRM%%^-f!qNsoHZaU$NoRjhN@pSnL_{Us@gxIW z3c3!c>eXH*R%JeGIvm=cn-2u~x*M-3gdRxzSpp6(ddUTO^%b4tfU=-f)v(&4wa-%!h_?6xvZlzRCa}lvKwPQMK!;LDHR@8Q?NR z{|X3U%cDJ~yxP>}dyC*M`WtE2DGy3IyECAek#kym@N)#Wjr0vkXBP()t17H8pYD5N z22-XTCYVh8BI4aIsrFw55X%ZiZ(w>HxG{imMUJ1>C@r@S1`w+%?0D@qOj|~^T9mgC zy-U*B6}4nM0BP!aS<>0xMWhXQyUK70rW~{4^eQ{dhgNfbZ~=53>6uF<)%lNsNwN@0 zb^MT2ei6c2pipt3g#1=;sw&40^GQWF44pvv2Fc_HB;C0|mM|2==PWU_0e@e_Tasj?Vpy!)V}JHHGpq})Xd;>`E| zD5X_~@;n|_ODda{rmkw+J~e@)vpY`>QY$Z%_>?o>DP&Ufy^cy>ED?DeLat9Rq1< z-y)gp%F&h7lXbY9`2e)=WC}=)^YJ#2)udiuDw&S%D0Rh^k(Jc1Uj=$M@ai~UYu3K+ zH%6&zER2uyjk9|B6+|AV>9y~RtT(#=GsdW@kk5S#k*}bfc|NlRtD4R4Uh^rA{sesB z-%uWtO!ir`qH2TD>BP;zX6^6374#uS2C4-uT0hko37XH{HM*qA5i;7Jo3HuYU8Z$7 z91e%W;cz${4u`|xa5x+ehr{7;I2;a#!{Kl^91e%$4E_%R{vO%sunm9!0000 zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*rb{x5qh5us}vjn(+Sq|(uZwIsd`EFKGl+=`3 z-EYJr7FkRKgt;cN-1*=CT=zfxvzm&_R$J+%c>d*{dmQ}G{OjNAHTe8|fByCR{#*F* zefRATk+%}B>Gf+l@8bvW+vf>2zu%ufzVGUKU#Gn<^!~%g1(WU^`Qd$BdtWHU*ZU!V ztmu8-$lq<}{r_W!p?|;A&);(;7~8mBiY}fMlIMHVUDE6S;D3I;b7THq`8lQbe2-tp z&VBpwr{I12>8IHF@qR!37z*dh`+)LOjNb1D`#N?{guY)<_`LHEf3O(LfB)~h*xkF^ zz2~*NkqeP3?!Bpxtvo+*;3SmkIj!(j`EUGOpRdkWV~dN7ZL&G}S}ybuiS`XS>@dO& z=XqUWvBVrtth~m!VtU?dsm2~xQVO!Z!i_ccv>myKX_2+W+wt34!gJsL+;4@(op<2K z7`RyAjK6%juipGOU*9ftuSz!r-@e6)bwyPT%TVU@ouf!dxbK+S6W`~{eOEXByTm3k zXiu0M8ytSVE-@{9)>eA*oH)*WcK+2Gq3-tu2od)-784Q~@FgS*CHNX+4S_f|@-tX@ zOgT<65X|BZ?vk673dw0v_vV}$o@1lM*V{k~iLg{?l}3gJ$;#QNpPCyrG%Q(mW>&0P zvu>lLl8cp6Y7s__nrg0AORcrlUPntUH*2NU)>?0)$DV*N>!nxMTkm}gZaTR1;OxN> zW6U_y%+qF>b+*~(Sd`Do%T`%+wbj?yai>lD@3O1gZTCG+IF!=K$4)u*wA0VH)Y?rq zU%Tbj+it((Th`uL{V{9dr_B96YvG+WWh_6()z7T)cC8aIv>U$V?lW`0&6~6QkMb6Ol{u%>{eNW6DRuA6 z{WWi2WNnX!QPPtjb)jPF(*@W#;It9j5GVcXSNGpu;FlM=wcXusOgo0LuTd9wroM@{ z>Z#v-Y7Z?h>UHz4-Bsoc*OBKH#`TjdlylOg0MqFUXiT}4bqm1QOX|^P^=VzOsP%DZ zFp?9vWmb_4SN51A9V;WwId9k%+}(B;g$VB2K5_P)`k^d18Jx(gq;z8=ZDI9rdZ#Xj zqx|NejN96F;-Y%SUazeV!BRG{``^Dx*3^1@xy|-iwe1q`EUVl=`SjJJH!73j_p7lG zdW+n%mNVO7M}<;fp{DFuLaH=Pv|!gV1)PrGwXwOhYrfU5p4j)vW9CwBDujh{U5A2a zZ%@qpI4)giyte!a>F=CGx1j+ZGpn4&&-9^^;H2O zQf;k2Eu3pbMKbTnR0qqNh=|p-ya7mzJFLu;=cT1b+uKM@ZnRU}!*1N%z3sVHViWTy zWDsUMsS|8vHqOK9E}SsNws>l`$hC4Dp%rMB>ReVkrM48n(#UUU!)D{`M7>@^*7P`I zZ;LW0-DdYCuLkjSb!)QI!5NYwP)c)ANjuirZ(!8`K}driV!X#iYR7 zzJY54vZ#w5v}O?F&K_?BIi~r^-r4C1y!t+n$L(aYvQm^-2)DBZU)!0-17Ehlmwl-G z04RDYE&9?96;)!QCsOx`<=1ewBiFQ+on~Qys)Nd*gW$$l(y2nb3oubfpq}5i&#<-f z!*X(LB?Se&A!}H>LA}!wF#&|1nm0=5sfqS_1q4y2VQ1hGm_(N;}ml5(HiCaEigX4}u@5-F!ppp8n?l>s%zQc?_yJP~Q&rxGcpWS$o1Mp%-V zCdo@g^5pD(Q+I2Wb2)Vh0=-pW>CQ}m77Caiy~RrlUxf>1x_JP7o!%|i!lWD@#01!o zL2J!Z=q9MEwp>UMESXNBMu3U9>R=Z@3I%Fs*lL{a`+Cw#Yy~L+eZ+6_IKO|jNqUSP z*PQ%H z>=yHknG=8u6jX=itYK<8c(M2vPTd5RRC6u_LnclG`rDM=8#Dpur;DN5yqCZeLU)lv z5+yktM{ru17$Rd8`R0n<1V%t*%9XIGdCFW+LqP#i;4$BnU_k8m8Ea8p{t_lv2V#t+ zcw%t}$>k`T75UkB90Ua^I9rGbj67VPGFfC3n5GtG%4j%)3!rR?z6$*j@_`@-0j%-; z={J+n>+$uiT_e^9M!<3v#9PXHrd1~ubG-wj=5BzrdS>JdPtQW&0R)um6xbkmI|iyr z>@KR8#)oq#6)u&i_>8>GZX$%ltOo6vjc>jRUvFwYK3VWx9t0K_5g3DoD@=5@G?S=g5LoP=h8 zLFQbTF4Gr_hNd*(IRsBFJ>2|&Gev|#ji0#nhrssnT6?}!4jidf(n%_JqInjIO=t!s zjddgsm&(fxzlT7;mCoAtVO3Q@|*#v$c3vW)q1b zILM2EnUn*tV{OKPWFtcep&5^wdAPx&`Tjr}(y7jyoGZ%e-6TTB!c3wxpGO z2D!c$5`XjRzP-^L zS;tf|=xYe6By=ge#Xn97OTf^^!Gm0^g9)4?J`Z&O4h#b207mr+8oIt#iv0Q0s(R8G z=?h$kPGh0RrS_;Q){DJ^Rrwiv7*zUZ*u36NC>lu3n-`H#+^s568XBs zV~CFPX!oH#5Hpl8(qA@AE@))%4*^jq+boj-o#0SMh7jG(uNAx4c>=y1yoH`eBSB%+2)GA5Fz*oL zAiH!stbvQ=aQLFeUm>0Q@(=V$=Rva* zA8$9%GzoKvQBn>w* z#2T%zcd(MA0%q_tx(TUaVh9n0`WsZ%4T+hJ|O`1MY z8F&{c9}nFRR3pZbds+7Ioa;XHsxc81F$To;x5p3U`LVG zMY(l9Va^P%QD43yW3kXsegc&z@DBCk^U`i>RKzT0&_NFHM0MN%7qFQ8CfvJ`qy&s? zX#P<>hc#YYK*!vnU@@cS$pbi?0f^rk?>RE*@2FL@0frtN8HJKUHK2_mwk59&Ty>v? zk`T+@H+fA7R4MsYc-LRn=ulq70SXRSIrtW`eTp}+umdzuGVkIYoCH*%G0=GEOiw|g zRHeVCY?hx9;AjY5+n_+`um$oGPNx>VtQ_8#vyMk&P5J69P-M>w`=EjPhQqO0xnbu| z`r%aIf7hNsb6h8JF6l1?82k7Gg8ajRWJwJKe)j0-H2957QV^_!Kze-fzR9%m04DS@G!Rr7}j9)8r-zW?pi<02hL1x`Aw!^VwnO;rtAg{Pm1nR=|ELE zmNB*nZMKGP0=~uGY#-l)RiiDz;^qQTPiWe9fG74!gCV_;HJT}%Ig7N&kfBu&lcq&! zdjRA}nc+OKpYYCbu)(NO`-l%L7>p*bQg=pWXZn_2cpVOecbsv3#tWEod;q}br5DD8 z`(HW9sRTN&J=OazZ8Zp?`UP(<~+U0Yh3ToHL(PY}yo(piz%o)`mU2T@g6 zLnnunctVl^%cw04&_!lbiVaM_+IQ6(w(9Enj$wMA9(uBgq6s*()k%M;v2wu)sc%*R zQ5AYUEmrNwWueiqjR@9gQM02&lkWf{9Av>WM}h=d0s<&V_HS%1?2j$IeHpTMBmH{O z__v>a_@hz%hK%XbL|YH_F8L%1w0QtmSc#9P(yQ>Trb}M^oD@sPxk=O0Q7|m%a()Zw zT#n3ov|S<{x>R5II64)oz`?NEWfxH}@S!NSfT`K%&Z~Kizmfetk6E#H6tKpSc$zM| z%(@NDslf!o7f>+Gh=Jv%A(2uFWs`sCV<*iMy#}zt(HzmbsKFi$DH%XwJ&9Ne$@TKT zp!qX9Vi^sh)RUTNXe^8(AVYWJq$j(ORc1ZMBVA}3Ju^hRJOe4b{3H@}QEf_87%=j1 zh-D~5)B&;0M_pIMdSGWFQ;Y~SSY?_a(KXo2C99h9&%5V%=Gh0q;mEG0t!TJsZo>b9 z=!>-o=m5+<+Vn8|Lx=g-a@763UPThIT*PSEXs)&)O2k5f>tG?&fIVYux9(mZl1^+ZW!UQiR@Ddju$;Gm*&W(Q&E2oVd1gJ$>$$z>@V%o=03KWeU{wgJnh2!Y9%`kN zskHeTB5>;u!{a_H28)n8z71!=En#oeKDUDyDzYh{_k*~4z~^1_Jk9mV9bzBGX%Hl- z#+sERH+@eldFP4*y!sRi;x$w~0BIx@_i1_}rYX8#s1MRcF=ftM)#c~WM?j*>cc%g9 z>k;@VU_ah?dOdZ-H0DXoa)pRexG*suQ9Ko6f!fg84!5p~VM{oorQ2(ovdTqKZYtma zDS0Cfm!4bTy1-yCvWsp3Wja!G92}$x@et*U%EfzLJpe>*Ck;+1!8Aa2aXDha=mD%= zQHgV~c!ov5HK81gpJfgCCR`3{ZH2u(?iSsCHQ+7feA&^MLOn5S`gl+3ScovxA=qBj zQIDPSYjJhO5Dp1kG~Bn&L<0nkrIN-tShK2x%nTv6Jse9+tfdnTZqEL=#_QoQSx3yL zqcMk(R9T3CjiE$A{G6rg4;>3_Tct5{T6@rjEGcEuw;)(ZfX}e7ugLZ}d%Zt$qEt=%2niCGSi< zUA;$7sUqp4l&~|P&<0@fd6<)`XNzMLec)!-gz)itOs63#ySRy9qedaX2smPP=72Eu zkjM)30nQ@ktwa>Z(c>qt=?P+zeCQGO=?J}0ABo`lrhEV&0wKZwphF9vlOK~p&=3Pc z^QfG3G^}RyqtZf{>!aF%vz$r=dvo@ihgyKc9;J>NW2Ixj5+u z8Wc?3diHt*e?{{$s(Rpzal3bf@W?3K%i>hHv#vM+hrvtK8F{z~PUB_vdO#GH0Bx)gW+-M2(KECG9&N@jyPH zf_MGlju(v@_0a($AGirj=Zf`!I30nejTjeNPko>##}V0c+bQRORr5>?82=!GxHT6@ zc}Ch(A6G$-IWnXHN~yx;iyhKOO*UwoK_My4vtiUGhbz0M20j-(jp>EX!C62Q@8}&U zKPME6G@SEeLi|Zrf1Eq_$0u0P^T&(;h?wPkC7KgB1!N@H#!+MvI$E8pB@Iygf>EnMwIFi#3w@uA}ksGND z=?Q&=2o=G~aqam2+VntR>yZN$Q>ctK@s!+NpCDjuWG$NJZ7`G`E0X2daG>ncg2z~B zPU$q5JtKg`dr-c5yaVt+(1_s0uf6X26fJJ}__qK`OI=`0P1&(_;!$V2~Oij@`3r`7GEC(H;+Zr<@7Qn8+DAyp=P2 z(B~uQ8VN1H*9!-ZJ~6}HoDwWf9(Z~^4M%UiGkp6;is{{2K$xXrUTLU55R=9xzkwnF zwEc8&06;Fb0s0`!I0gM-I-bPK|*%#efq~Pq$X^3X7=vPJ9D4kpSv@2 z-+A8Oect!E@4WYdVHk#C7=~dOhG7_nVHk#C7=~dOhG7_nVHk$t5?b)D)Vx2-B2j@@ z=lxk)@UL{CEmHMh^!hD$yaFh&4&)X|&--uFB;q*d{69sw5x4~K&|sD{UA?=SZAw;O z|AavB=814A=%rflpF#B|Qknl05jAzn6iyd;^v(V&RKE(G{|Eo{NLc2}-8Tst_u4T3 z-U@m%sg8D$MWN|bf^>!18PI^YlAn8oH+HLh5iP^3Z{{I0yS*y()IZt)SUk)qW%ct zfsPpOILW8Q1w;4*VfeJ>{iRkNzbReY*;?>@LY_sqtjIW;e8(avyGm^H^}tTex8JGz z@1GJ&>GnU^q&a^Hp|>4nUWrqvmrTC5S|R0}iQ_=N%s_Id7W^Ajmo~(sm{@g+=GzxC zFz^z>4TEtg`9nHQzBdWBgzsAsSYhdX!ozK+r&_O5^;E^&{Q#51`@gz@-sGJ+8gj-SOM%Y&`Njmdx8+QLN)SDvwumo^DiNp>qw0~1~`{Qd$ zy50VGEo8p|Y%8Of7%AO6@5=XdNIuy#1|K>Bm49Ud+ce+)IrobR6)j}1Wdb`;{WdUh z4C1`m!{#KNapt1Dhg&>X!DF2cvX}|HsQK&yRjtU0sH&oQ{~D5P2VGb}@ti6)+>=jr zLlurX6ZAd`*)g9FYQFs<`m#?0zZg7jrQ<8lnS8^yrkcUI=&ku+uir8z^EzaGW#)BG zo!m7dUzptBKs>I;qeJuU3$^Q4qZ$&mXXWXd&n`xT?U7%vE9=_@PRZv~!7)^HbOUEN z(wr_-7m=QNkI2Udicc_pg1lTe^rOxeN4`ssq%%`3^PL=*&d%wGETho#oc6X~UQ{uW z%g&>~Rz!c~ka(3Y=4wG1QDSl?kGiq9v<~>4OwQy@zS8P&8+xO`**H2ZpA{X^47y_S zl^!%JI-=>}(Bvzbqi?*x&|M>qvI*#yjV52Iqwe62`fcXVsQ9f+%^WoOtmvr1)Wz-i zxh0yiF!w*@^G(`(aFPqfyWEy)7b{ zqbA?zqGWnX8#R*of=y;pIr8adlW){Tht~o0n@uG#z~qTrO}7oKDCMR5|pdjC5Jyf!nclzPK!*M0sO zCf{hp+kgKQ8j_D7yxWW_iFc9Y5k25HyWB`|R=i%n1&>E@@m_be!xjekluXU+HTjAa zuWv;wVXy{xr};B#UP2O=OX>?R+ps&5cs*_B5C&^ayauLE3q6m=GCqi+Ri8R)|+@W!+X%+3C%5^UDNVuK6^3hJq}E>KCLNcAmKXgUH->4 zDc|5#bp189YEi2+;e~6p;9pTw@@X#ncOd@(jJH0lX~v^0*Mk4oH6fqovo|8T3|qCR z7aCCR)q;P6v(>Ju3i;(r5dDhvSG^+NB{lc!QLTnZG~%sIQrPzskZY}j>fJy6F^%#i zkxWo^`E<8Go(Z%8S6I))ML<^2IB~&fZ$>mK`80RmBxKx5;8N?6*s&gIxI$7__ElUy zJ>WOf&%?k4)(^4GfY0FZXXP%(l_lTMw#L=K`>Y3IPubfuOO|hFo%T^|cbRdFvbJZI zB%kJ%&ql)4*m9Y1BCil|xin{9EcOjWUu4#T|3?r$X5tycFoOhZOZh=UQSxbT+lNse zLzrqYHcrAcBz!`9{f|U4>}p@z1%%!j><0oy@#Mk#xTNQ9E?Yj$`R5`$4m4XljS|^Q z5MCn9^LLai-%wS%4rsAB8fDUj7ygMf&fitEe41M^0}0k+ms5<<^D-J-EKOJM9#D0>k0fc_r5SAadh@A14n zOdh|d8jnbI_fZIYe>Q=J)82^&)81xyw+uR5#uDu&lr9q5Nq^AIltgc(?><%Y@)egs z{c-H~&SW5HMWhv^71dURcQN=w*Ls+H_5&R#yYRFV^*YgT7s*5?lfsu>6|3&$8&S-B zcmd*l3;0wG_RMO+@Lx!K8*5pxSju;-GXF)2uwf_AR71_qlT4l?ldj6ubKiLNE}t|m zC?IlEZJjZ4bG?ff3;6&zs7rypHPYH{_8QvaO&N0>5|yUC(WNffansplOErf=FcB6`RdZZfx7@#JNE!d?ub)e%w z^-2Aqn~l?DmAVVO$yc?ZExe1tRRfpkCrmDhTUW$RzQMf&`ha82%1YB0UNQM%VW51^ zsWL;tTjJOav6N3znHLaNJJOuTU3&Vr$yW(Nz0Hx9+!og^i0zLxRaGctH#4|Ns#O`eG?|1OC%G^fY&R0)I)fBciclCd--J2RXJ3CT;ZeM8@Hu2lW#Z@!JQ251giuA zp5A6bH~9whP~-ev2oF}M!d)qincrE|O}=A<;q4Vt-Ljs^cbpPU^Pd8q7~{h{DNXZ! zW6?JG-p0T&o{LGX>?1MxDoyS zR76p|xyY9*QY@manpk_jwYHs>3gxnE!Q zsbQYP{uHTs7ut|(nQ!doe!|y>IgP(&;+cG7D_~>hF<{kke||(#nRON&ldlqdjbrz9 z9za-VAu;(Xll{_}EeM0pb5m}XROSVXO6epm1ZC2={U!>tH zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*rlH5A7h5us}vjn(=Sq^N(?BFebzQd}P?JINR zbxW#J84^fyOp@;W-+!`;`#|qEJ`R|4=E#%x=i2)~DZXA0`C~=z z?~VM;cHVzKb{P7%JN^3qTnWZDj+df~Cxztsx9KeD^>^?$KmT%K-mg4Qsqt@4{qxwl zFHe66-j`=T#LkcR>*@PYIA7i$C_lvL{d%y^WA{Yp+ZBbcXa4RD>%siDKi|ae-rep! zuicGYh*WX!O?_Tl$1D}k6 zg9X0v)35vN#eehrbpS+Q!( zx{Z=bE>=pZMHn?|s<~P%wboX99WAxotd&+EO_VZx22( z#*8z~JZ+X)XPbSFMft3}Y?W14TYZfkciOc7F1xzjcHiTKLn)nn?37baJN=AHt=)9< zwOekz?e;sqWbK{R@3R(u$lR~97T#G?#`5#I`Za63TU8tD)bOAO#aN3A%h?9Q(IH`NZY4m*;&k8B%<*@VM!m>bfgi_M> zH7#SSt$|QB&LtRowq0hvmA^9|IEMvW*?ZQ|?;QE&)$TfO4pyDgh%1amt;AVVTAdEv zt)x_YIG`gZCFty^hn@Pk!`SflgGl8`S8^ay)pDJ~cV0iaP$@O66c6D|oT~;~-DXH< zoib7ZDrx)MyRF+gF*!4>x({?T*Y)Yl822dUrm{|ri|r(0en(pcT&7ybscmWRHCEdx zkoU-9!kDFj~LA*X+{9Y+V{*8tu2smBO`H`ORx*aF%vW zwB|XZPIwPy?BV8dd!7oex4A!JdulqaSYOBb*rl$!FMz;T2CPW7ZfHc9WV4zyZEt_n zAvW^5qnwe{`}>%!aDm#idwX(y4*?-Nr>6^kuf$GUFJ!orVLO4oU!k7Z`puoMV-SST8%$nXIU%zT0_eVYUyT?1GL!B(;hOX$OcvS0G z|7Rh)R_eFb73YSES=*)Rhgmyd;1MRoeyH4B?snOiZoAb}d`q`&(nmDRY6Y`+l@6=K z?%5lNN41n2=ngV?D1p*W0fEfBXnWIM>^N{lH!bG z?vkku+Z4kqI*5z$)EGMl^xO!dq zfV*IVRuM<(mgVYL%lBq+f!Avd50YeZKm6W})Qk#5gED6Tpb*_ue;?|JVj^Qiv9-Mo zg3L#i2yn~;DBonap_2V7P+!v?E`IbnCV=xECMfKy=K zIeDsjSG`JQ zR?U-F;sH$nHQ$DSKeOoqK2lE<_-)HgV^1ujZcal=5IE~V@^p>-Rsi7=G?w$gbR>2` zBG0P+;$dejPbLNCZu&j&B*_Kh5}iaY zZf&#PQ_&>6HuWm2zcTI;6%LYEc2st|iTS9e6f@+gMiO#d5QZ1d?y?Pr?SgU<5&QN% z=j46f8VIe*@kTv>a-$K1gU1)T=pt*&yuE5$W^xyZMmlRYfbkx5+?k-~G%;0iqytJO z6BQqmsiYS(z<%lXfN}(MtKQYKD=c3--Ie~l2^=T)e!?xKq4d~A@MrH~o#=w-Axu;Q zxVq`^MJsl8PunI=pE588w@&UyWri*^8ab3JZfwPeHvAIgaG zr#O&$Dku<3C%y@EL$L)?jetnhRGXJ7fAsdj1kiYx<257pmxBW_G%%r)hQI3EL0x(R zNO!=Cd?HL=vIwaJFbF4NQH#ueMfW2KcXERzTp(NMLP`M?Tfw~$&5A#u9%(ATW;8N~ zeTmZVD1Yo_r;SkEec>enU`k3syG}C9_X#d4&^N$^RTYg@@!&s}0e?_gYKh5F8gLk9 zhR%WNPxBYh)t0K~IKjZW=pV?Ur6;DOzS55vej&PMNEY13lkXlWfv)=9%@F2@)&nJk z0%V~WW@3W_KCalg2<_1HI7oVHAg956BF>Uk-w<<1(@*4(j~BBrNt{z=-WVAG$a^Z zTnz9mox4HX9(}1EPcy^8gPg^;=LMe&5yNhR))=I@@E}lAWF5H-Br;^)9R!7%E$|#) zK<0ufSU$8KnkP&W!v3jH1i=0P1|a8WAdnaZ&+Cy(I-)6QB&D4N0riv}W+xE;#z7%8 zwtg^MBut#{2ebeIfsoh=TdS6h7rqfUv2vpn1AuJ^@HxA`C4Z>WESh5NrXKAx=0#P6C}ki?AyeFwU0iAbJ!o5rlI%0KzwZOibDv zYnC6Mpt$=(-MH{-U@sQQC+c-TQRsHi4$Fq_D(0lukuySUh;eKTAInAG-gaZ+bC2F= z(pjMhIq7#K=)j&?&k1^fQyD5Iqw?wA^M8QnjAme(7{xGOTzrUX6>B%QHA7B<38@;S z0If+S*TtxQ;*@-GON#v|*3|fetw&XQd<`-~0~yx20FQT#fz^f`FDy2qQ{OHwr&ss? ztr7X;F?IJyr2*uhbOCX)f2U?;xx=ZDaRaP+%cW$XP!lMPUKSljk6!Gf1ZkaXy7mmc z`hfUq_h=Cz?1a9rtZO1~;?2z2TwN3;lyf*TAGuJh17 zQf1W$S|CO#;!e_od(=jPzu^zcta=+5RRBvPFQQOH(+h}99$`w6)EN*B9W2M`$YcW& zXnU7x2kSy;k~c{FLv+xKCxRyl^g0^%BV_2e1&a?3t}9wvK4{mi?7Q+U(*?W?Rtth6 zR$j|TN$Cn1Vnm}uDP38dEjxUsy7l6+r^YX=hBiVbwXz*MEFkScy&=;Hg~9FVwhk8| zP%ts+Yt{XtnB3n~9TjuY^pyAn*%74y&IC|Tq#)@_`sucSpF2Gk=yqy$iVekV;@Qw0 zG+?E%q_F(6bmBDV?VbUIpsR%VeuT&Nw6%tNP!e1K)d%^b15WnYk*bu?2>`I0;2<#mqFtH?Y`f4dVREG--9~-pdlc)CVC7S!RJfxHOi!ykEXPTG zT?WH~JKg95c)bey=WwWhaEridxExgWxGwB~5Pxs_6RV&TgR=M;D5pG^v+|)#@*QAU ze}=Fw>4I6oufp?jXgE;!dOY0TN0O3O9j8ef;=jWp6F>gN9K>$G887%M;-vDJ zejf@(G6|JMG0VL@gO+9_d|GIT5Y_t4gdn25J1YVhE^wpC2)qF(q>8ZEnt`cUC!!JS z6zI%~5nT1FVmTgkzN;xV{0@KMB)^xgH*pY;58fIw5{YHz+vK?TKparibTv_< zFE)X@>R%|fA0Q5kei8;OCw7gw&DRASqb_G4XEm)!ibc~M0fwhgd*e#xsnpEu@fIZD zmF?mWT3P51fInWv{Xe_GHxVZZgJB-(r9~Z&;qnRfd#=BA1oue*sT==fxw0aW4 z-2l{H&G@i6Q}cZ&QCEZjkM)YcY#+0U-%Pcgo_vfRNGyo<8a)(i&}^zZgemZ&Bv};D zs{Xw_Z$Ncdqis+hD1Ha?Jj0tauO+OCCKFIC3WR8mbXIyNC#JD}HL6FN+D6q(NkyuA zq+9$pq)jN2yYwE!Nik9j0F4QHS8a&v)^E^-d83ebc3I;tH0PO#E<+NBAg)M(6o`jB zojB$#qz{X9C_k8Y(OmEYiHpD(ZJM_u0s&{XFx$F|rF>%DLA21ZN~=Fp?U|0QY6Xo7 z;dtb^^o!eI_j+y~m?|T*gl2|UP>GJffFRsQ9jFZWBkvV6!dO>}g$^cZc`o|>I{W~d z&nOrYk9iXaoeAI&k(6Rzx_1I=77B{(kcW$b2%H96w)07FcS561m*xuyEXUtB$fUoJ zEtN2yX$4x?8kj-=!)5txs>UNpK&1@1X;S@IiVk1RB+M+}R2URmD^?a>9K4{WdykrZ zBYo1NY9kGSw}WgD)mZBG4`XrvWl9|q9@V2eAiO<5knE=Fb#|}OAPo}hxw@-h^=C|- z^mNAyQ~|tQhGA^FZx)iC!PJUgWR+<;eHl1jm1@%*r1)!mmse=jDwQU_)l*6)(iapS zUqIop02%5#@L?OBUC^We8v(=fXo5eYCp29H8wE#C#X`vNfEthahhv1tpA8P=IVonZ zZOv?_MkQ3NM(w8gZB#PW(fu$(ItL4Ln2R$Io9O@p(mbAIt}9hv0fB= zt3yZe)M(8D0O*;xgvW8WxWQziLd-OZEbi*6)7(HG<`cn0(|hBH)QQHv)ZanUApLY* zSeC3v_@I9eW_e}INkj*P1T{Y-&S>9>JAOY1C~QtwotfpZ(W6Ev78IXbAc*Iks#rc*93!Zb;yYhuH#YQ(1U)# z*0;2&24!hK1L0QiRebr9umcuf6mq^05&{A5YuRRSkD13mq4ypYeILw8kI#X&Hg*K< z0^0FD8Vgs$L()LI2y2ceJGFIst>9fWhusLAJV_I^zi>A-3bC(lFF1)x-nueDfoiTR zW8?dZk#fqKgpcCR`KwX*hnbjit!xApKOZImMyO;Ia?s@xyK8FHEVMi7myTVgF&$q8f>_xOdwl)caJXMqT3Jt` z=oZmSEnpMUdR>#Rn5h#V<%d2;#Nh2fG8l^zIM3vjnCDK@lHyWna99{0gdC5Nzf9 zTOHg__d0acd8srq`@N>?4OnFy4CpapnG?vx2l@pvWTflDv`Cb?U1iOnPG~C26@!u* zTH$Wia~)i_E#SPUR$A}K3DHqgG%vAmJg&V{o~}?0x^DB_S78uaw4fWEfa8Pgz@NXy z&@%4NeHD*E-Kv7At0^@c+kJUwsKMeF-b!r3$!^5#tShKgIE-R7T?{0HnjRe83R0e{ zYd8{B1YL>@!}taM-&D>A^{X3~s%{uq8fimGsXj?WJYc_ZdGF3FKUyF6dvEk-k6#Z2 zp56sf7i4?T^?jR|3ymqENRoymU>TQ&L`#Dq{d5+r3#HV3Ep9!gM|Tj(c;~KBCmccP z>((LNleu2e7FnJsB0vD^W{aa|sb}rq>qZ(Q<}?ri$2y<>+8QKVEC@Y(B=Ebb zC6rdwWsx~+ExItNS_&6)T+t4Y_ROTjp1YI`D28|AbJ+nv#V-J~nxBEkB&h*aNM*J< z5&jYc9N9bAmkm7kJ#CSAUb+>4FKWIs=BL+*K6`Y(`vA>DQ}Mz70WO}@-gV0$&j0`b z24YJ`L;wH)0002_L%V+f000SaNLh0L00IC200IC3ety&A00007bV*G`2jm0;2qrN) zCn&rC01e+sL_t(|+U=cta9q`W$3MTj>-Pig%5t!=X^?=iE!#4dL@3ypnqYzx9s%h} zK%0`5LIX}PIAcsoItnv1I88fEGHuCBGE6DK@|8^jo;VOf2_=D+H0cBr0vR5~#=>B< zyM7_Z-o3y6p~Vo8^}KiY?$z%*;~9IryXTzy``q8-+;h$algVTsn16_6$bHM3 zN|OB_7~gJ`RZ{IfZ)P{o7oHOS!hUY7lMsHYxz43#c7rcOf9@vc5%wd?1C-Bigg2-ViYOg4UuzKOxLS{70<{Hu!?` zdAY5%Kx&8|pM~%IMBnyKG87EH(JP;?9p*Paio>INz^gGF48Bp0NNPv$t@#|~VePqZ zx*=ikjUw*q>=Ax*DKh>+-Q`U(EDXMpNo3Y2zI`53=u~|?X=oUHBY=$CJR^U_7XPK{H+J4H1KHXxf)K85kEJ&Ua}~ z*R_U;!8f>uKgTzC1}S>CW;S1Js2F?$E8)xz<5xj8Yo=?t;bQO&5GDOtA^q|s(1$eJ z*Re8-EDwITj^73%!R79tpY$IH-aLvCRfzVnDZ2R8Wn3A@ZLK`V6}VY}{qaGK#` z@cp4@DvgJ2!ukCVRK4+rkikcBb9ky`4GJsCa*t|%lhS6Au z&-cr<8hMTOZ0<0W#&*tka`~!k`CIMT{8wWE811=ly7p|H6U5eoIIUYN=ROQUy$eamJbbM~v<{>Wr~_)C00NrzE|--1eS;5wPMJ^tNFdy&nXWe_ z-TGw3fVOw*ESy3E!V=I1kOt^I9Y~;ByrlhJS4H`ydA>g9b8-YaF#+@;&3NZZ+T9Vo zkhygZPQD4BMnsx`Mkv(0-+>Q9Pe|I`Y4G)3x7lbUdQt`aSoe0Wkec@2l?}AFyc0=E z6Y4i1(g-v`;Y3sd3J~(O(VHwTiEzg2rc{&6L(@*2#Ggpj6|aOo_I+NOqq-EwX-2gf zp&6JH;*I`{q}-1ge1Pt`cM9Vt9*E$0{tusJGQ-^+>{ur(L5iR$%?PKKcI}T?u8*?F-bnvT>bIMN`mvaJKAjz5 z&-ZSEa~i($OD6V$?|^9KU1wj{i*)#b`HXl>v&bw=A>ZKqhs zZ@=?L21sbiku2O1){j>{eEEVE8{rjl9V2-%Gj`w;G5EsK%T#6mCCmn5P;eN0A>GvL zMXo<;@C{sX7<|F&2}!j+5OyOm2)mqWtOTk8j(kx&)S4!1b#lok#@1+puE5=KZDU5EEnk&0Y)m+6bsnT8EB&N#s_k$1SswWyNfnVX9WNuq;X=}ww2(Ox4gUVVUZIFbb zpGn#+`I*y^^?cg1X)fZd!I#egb`WED@^PG-%h7P;;M1(P1l5~CuCz+FBnoA%Ud6zt zszSzFg(K@wE{M@y_NePXDW_HnJ3?Kt6vhFc~!h$|t!^=C<~hwFjlCTszOg zac)7mobn#^VCvJ)NwqEy>g0jQc|O$+hur*2P+12oGcOkz`8dwna)TENe7eh<#1#IF z9P3a{H(N(eWt&Q2|vt-5TS<*A_iCU**VCcLA8NC9LF9G(J?Mjue7X*F0S!nR7s*vlt z4CPL2gHEOEDM`EMM6qMZ4;K>NN^)IaMLE;#77N`P)s7v5Pcz;k;4XYto4w*vWbl|& zyFZI8e0`6>7Tk_wgoYMDv;KnM!ACyso!izxdX_Xy<(*V z8CFEvFCR+}vfdJujle3iTU??pk4nm26?ypH2jL2oyTB%sV^Vc2lXTn9qZxKTNMp)< zf>i6-sJOt3X1t1be;ZX*Dgr(L5mBjb?IJ}za5b>Qj99Tchs>6iiU_;?I_|zF9v7qB z3Y=~BtT??uiiIL>A@ZKDUr(5FACk0t4vu^r zz!amPth!p#?)_s1zGD#X^V0Zo1FAP*@5(Kgc9Ca+%ybWqDfr&Y3%5@obMQ|QZU*X& zhmxCrqoi9mj7j+3gHVNR*J@xLa2|FvX{l7!sh3DRnGg|`wEIIzyDJg@4B&p?sId{c zCPU%x$F%3`ze3(R2PeN4mFuw6=tI5Ks4SD})~5`<{)&@4Hz9l;NEs`kP*9esmx{@) zbnk=HNF2Ta|D(Y-q!H*V#!KKjPqUjZ zGx!Gee2&2vhJ5*&>h=i+-+=J>7Iwo4zNe8ru-4!kNXdkGzVHa#p!*-FHu#RG>Hcd# z!9WQ`RUFNKIXveYhuMLQ_cBmxtc0Q?IQ{~uY7acUiOWd?Zx|z?X*}wD)xz%a{dc!9 z5@u+jMRQ)G!PoD_5i2^vzP$@&RWB?52<6Cz=pxLbMU(3}B=S2OgKSyVBqTG_(1eAfGRDH+oTjrBt{7R|Va)CxbR1 zP6O!TB9OHVJ`Lvsr;KK^QIgomc^%4n)?psBt&+e4D8jQ7*mexaJ@-yw+{DG8^AV{9 zPDQChsN=m;BOHXr;&;b7e3WylMG?JLQf=Gfa3STy&m01tF8=w$f#$xKLY))vDMy*j!$=4;_wh_}9hVO()yD9mKX4yol zWxZiE#^DoL{d=I4Cu}}UO0OyRiS7npbX2OPRZnx|a7*#T1e3u>sa7<l3+03>GKP+Mz7NtD=5qQT)cHhIUYcj{MGwBPa=u=C=aW+1@*6|O z;5*J}LRn6K6ro*awEVZ>V(<;bNzh@}NBB!gxepmC2H(KNZr`Jy;10AFZjqGx?}mxN zH&i_z5P2IV<$l8uG5Ch#twtYuMZZI;og=dg)mlPK$m<+xVC^4d^96NBhNK&8984?EHC^YQ3 zzL&_eN=|U?d2PvS!x&04UWTE)9`$u3vCN*=n&%5r&kf--Z&2Xl2A;tev_pS(kPqZg z=Yn#cC}4^$d^!+(8GJ6Z6)NRdGM?=D&m#TaS9`#@P-e6|Yo<5P7mfDf#45|e|ZWmEDz%)CML`Ij+6K0{~0H_ to2ty+BG=p7$W11b$z(E_Or|pE{{gTEK{{DD$vOZ4002ovPDHLkV1h*o;ZXnp 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..070d862a495 --- /dev/null +++ b/Tests/test_file_avif.py @@ -0,0 +1,782 @@ +import os +import re +import xml.etree.ElementTree +from contextlib import contextmanager +from io import BytesIO +from struct import unpack +from unittest import mock + +import pytest + +from PIL import AvifImagePlugin, Image, UnidentifiedImageError, features + +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +try: + from PIL import _avif +except ImportError: + _avif = None + + +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 _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 _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 skip_unless_avif_version_gte(version): + if not _avif: + reason = "AVIF unavailable" + should_skip = True + else: + version_str = ".".join([str(v) for v in version]) + reason = "%s < %s" % (_avif.libavif_version, version_str) + should_skip = _avif.VERSION < version + return pytest.mark.skipif(should_skip, reason=reason) + + +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): + if features.check("avif"): + AvifImagePlugin.SUPPORTED = False + + try: + file_path = "Tests/images/avif/hopper.avif" + pytest.warns( + UserWarning, + lambda: pytest.raises(UnidentifiedImageError, Image.open, file_path), + ) + finally: + AvifImagePlugin.SUPPORTED = features.check("avif") + + +@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_no_resource_warning(self, tmp_path): + with Image.open(TEST_AVIF_FILE) as image: + temp_file = str(tmp_path / "temp.avif") + pytest.warns(None, 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_avif_version_gte((0, 8, 2)) + @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_avif_version_gte((0, 8, 2)) + @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 + + @pytest.mark.parametrize( + "quality,expected_qminmax", + [ + [0, (63, 63)], + [100, (0, 0)], + [90, (0, 10)], + [None, (0, 25)], # default + [50, (14, 50)], + ], + ) + def test_encoder_quality_qmin_qmax_map(self, tmp_path, quality, expected_qminmax): + MockEncoder = mock.Mock(wraps=_avif.AvifEncoder) + with mock.patch.object(_avif, "AvifEncoder", new=MockEncoder) as mock_encoder: + with Image.open("Tests/images/avif/hopper.avif") as im: + test_file = str(tmp_path / "temp.avif") + if quality is None: + im.save(test_file) + else: + im.save(test_file, quality=quality) + assert mock_encoder.call_args[0][3:5] == expected_qminmax + + 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" + + +@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 + + @skip_unless_avif_version_gte((0, 9, 0)) + @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() + + +@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() + + self._test_leak(core) diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh new file mode 100755 index 00000000000..1e414668804 --- /dev/null +++ b/depends/install_libavif.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -eo pipefail + +LIBAVIF_VERSION=${LIBAVIF_VERSION:-1.0.1} + +LIBAVIF_CMAKE_FLAGS=() + +if uname -s | grep -q Darwin; then + PREFIX=/usr/local +else + PREFIX=/usr +fi + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +PKGCONFIG=${PKGCONFIG:-pkg-config} + +export CFLAGS="-fPIC -O3 $CFLAGS" +export CXXFLAGS="-fPIC -O3 $CXXFLAGS" + +mkdir -p libavif-$LIBAVIF_VERSION +curl -sLo - \ + https://github.com/AOMediaCodec/libavif/archive/v$LIBAVIF_VERSION.tar.gz \ + | tar --strip-components=1 -C libavif-$LIBAVIF_VERSION -zxf - +pushd libavif-$LIBAVIF_VERSION + +if [ "$LIBAVIF_VERSION" == "1.0.1" ]; then + patch -p1 < "${SCRIPT_DIR}/libavif-1.0.1-local-static.patch" +fi + +HAS_DECODER=0 +HAS_ENCODER=0 + +if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=ON) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=ON) + HAS_ENCODER=1 +fi + +if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=ON) + HAS_ENCODER=1 +fi + +if $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=ON) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=ON) + HAS_ENCODER=1 + HAS_DECODER=1 +fi + +if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + pushd ext > /dev/null + bash aom.cmd + popd > /dev/null + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_AOM=ON) +fi + +if uname -s | grep -q Darwin; then + # Prevent cmake from using @rpath in install id, so that delocate can + # find and bundle the libavif dylib + LIBAVIF_CMAKE_FLAGS+=("-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib" -DCMAKE_MACOSX_RPATH=OFF) +fi + +mkdir build +pushd build +cmake .. \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DCMAKE_BUILD_TYPE=Release \ + "${LIBAVIF_CMAKE_FLAGS[@]}" +make +sudo make install +popd + +popd diff --git a/depends/libavif-1.0.1-local-static.patch b/depends/libavif-1.0.1-local-static.patch new file mode 100644 index 00000000000..e7faeed0f8a --- /dev/null +++ b/depends/libavif-1.0.1-local-static.patch @@ -0,0 +1,148 @@ +From f8f4ed7ecec80a596f60a4a7e1392c09cedbf7ed Mon Sep 17 00:00:00 2001 +From: Frankie Dintino +Date: Tue, 12 Sep 2023 05:47:43 -0400 +Subject: [PATCH] ci: link shared library build against static local + +--- + CMakeLists.txt | 33 +++++++++++++-------------------- + ext/libyuv.cmd | 2 +- + 2 files changed, 14 insertions(+), 21 deletions(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 1f0cde1..521560e 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -78,10 +78,10 @@ endif() + + if(BUILD_SHARED_LIBS) + set(AVIF_LIBRARY_PREFIX "${CMAKE_SHARED_LIBRARY_PREFIX}") +- set(AVIF_LIBRARY_SUFFIX "${CMAKE_SHARED_LIBRARY_SUFFIX}") + else() + set(AVIF_LIBRARY_PREFIX "${CMAKE_STATIC_LIBRARY_PREFIX}") +- set(AVIF_LIBRARY_SUFFIX "${CMAKE_STATIC_LIBRARY_SUFFIX}") ++ # This is needed to get shared libraries (e.g. pixbufloader-avif) to compile against a static libavif. ++ set(CMAKE_POSITION_INDEPENDENT_CODE ON) + endif() + + set(AVIF_PLATFORM_DEFINITIONS) +@@ -112,7 +112,7 @@ if(AVIF_LOCAL_ZLIBPNG) + set(PREV_ANDROID ${ANDROID}) + set(ANDROID TRUE) + set(PNG_BUILD_ZLIB "${CMAKE_CURRENT_SOURCE_DIR}/ext/zlib" CACHE STRING "" FORCE) +- set(PNG_SHARED ${BUILD_SHARED_LIBS} CACHE BOOL "") ++ set(PNG_SHARED OFF CACHE BOOL "") + set(PNG_TESTS OFF CACHE BOOL "") + add_subdirectory(ext/libpng) + set(PNG_PNG_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/libpng") +@@ -135,7 +135,7 @@ if(AVIF_LOCAL_JPEG) + endif() + option(AVIF_LOCAL_LIBYUV "Build libyuv by providing your own copy inside the ext subdir." OFF) + if(AVIF_LOCAL_LIBYUV) +- set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/libyuv/build/${AVIF_LIBRARY_PREFIX}yuv${AVIF_LIBRARY_SUFFIX}") ++ set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/libyuv/build/${AVIF_LIBRARY_PREFIX}yuv${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(NOT EXISTS "${LIB_FILENAME}") + message(FATAL_ERROR "libavif(AVIF_LOCAL_LIBYUV): ${LIB_FILENAME} is missing, bailing out") + endif() +@@ -146,13 +146,6 @@ if(AVIF_LOCAL_LIBYUV) + set(LIBYUV_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/libyuv/include" PARENT_SCOPE) + set(LIBYUV_LIBRARY ${LIB_FILENAME} PARENT_SCOPE) + endif() +- if(BUILD_SHARED_LIBS) +- # Fix "libyuv.so: undefined reference to `jpeg_read_raw_data'" errors. +- if(NOT AVIF_LOCAL_JPEG) +- find_package(JPEG REQUIRED) +- endif() +- set(LIBYUV_LIBRARY ${LIBYUV_LIBRARY} ${JPEG_LIBRARY}) +- endif() + set(libyuv_FOUND TRUE) + message(STATUS "libavif: local libyuv found; libyuv-based fast paths enabled.") + else(AVIF_LOCAL_LIBYUV) +@@ -184,7 +177,7 @@ if(libyuv_FOUND) + endif(libyuv_FOUND) + option(AVIF_LOCAL_LIBSHARPYUV "Build libsharpyuv by providing your own copy inside the ext subdir." OFF) + if(AVIF_LOCAL_LIBSHARPYUV) +- set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/libwebp/build/libsharpyuv${AVIF_LIBRARY_SUFFIX}") ++ set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/libwebp/build/libsharpyuv${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(NOT EXISTS "${LIB_FILENAME}") + message(FATAL_ERROR "libavif(AVIF_LOCAL_LIBSHARPYUV): ${LIB_FILENAME} is missing, bailing out") + endif() +@@ -309,16 +302,16 @@ if(AVIF_CODEC_DAV1D) + if(DEFINED ANDROID_ABI) + set(AVIF_DAV1D_BUILD_DIR "${AVIF_DAV1D_BUILD_DIR}/${ANDROID_ABI}") + endif() +- set(LIB_FILENAME "${AVIF_DAV1D_BUILD_DIR}/src/libdav1d${AVIF_LIBRARY_SUFFIX}") ++ set(LIB_FILENAME "${AVIF_DAV1D_BUILD_DIR}/src/libdav1d${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(NOT EXISTS "${LIB_FILENAME}") +- if("${AVIF_LIBRARY_SUFFIX}" STREQUAL ".a") ++ if("${CMAKE_STATIC_LIBRARY_SUFFIX}" STREQUAL ".a") + message(FATAL_ERROR "libavif: ${LIB_FILENAME} is missing, bailing out") + else() + # On windows, meson will produce a libdav1d.a instead of the expected libdav1d.dll/.lib. + # See https://github.com/mesonbuild/meson/issues/8153. + set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/dav1d/build/src/libdav1d.a") + if(NOT EXISTS "${LIB_FILENAME}") +- message(FATAL_ERROR "libavif: ${LIB_FILENAME} (or libdav1d${AVIF_LIBRARY_SUFFIX}) is missing, bailing out") ++ message(FATAL_ERROR "libavif: ${LIB_FILENAME} (or libdav1d${CMAKE_STATIC_LIBRARY_SUFFIX}) is missing, bailing out") + endif() + endif() + endif() +@@ -353,7 +346,7 @@ if(AVIF_CODEC_LIBGAV1) + if(DEFINED ANDROID_ABI) + set(AVIF_LIBGAV1_BUILD_DIR "${AVIF_LIBGAV1_BUILD_DIR}/${ANDROID_ABI}") + endif() +- set(LIB_FILENAME "${AVIF_LIBGAV1_BUILD_DIR}/libgav1${AVIF_LIBRARY_SUFFIX}") ++ set(LIB_FILENAME "${AVIF_LIBGAV1_BUILD_DIR}/libgav1${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(NOT EXISTS "${LIB_FILENAME}") + message(FATAL_ERROR "libavif: ${LIB_FILENAME} is missing, bailing out") + endif() +@@ -378,7 +371,7 @@ if(AVIF_CODEC_RAV1E) + + if(AVIF_LOCAL_RAV1E) + set(LIB_FILENAME +- "${CMAKE_CURRENT_SOURCE_DIR}/ext/rav1e/build.libavif/usr/lib/${AVIF_LIBRARY_PREFIX}rav1e${AVIF_LIBRARY_SUFFIX}" ++ "${CMAKE_CURRENT_SOURCE_DIR}/ext/rav1e/build.libavif/usr/lib/${AVIF_LIBRARY_PREFIX}rav1e${CMAKE_STATIC_LIBRARY_SUFFIX}" + ) + if(NOT EXISTS "${LIB_FILENAME}") + message(FATAL_ERROR "libavif: compiled rav1e library is missing (in ext/rav1e/build.libavif/usr/lib), bailing out") +@@ -411,7 +404,7 @@ if(AVIF_CODEC_SVT) + + if(AVIF_LOCAL_SVT) + set(LIB_FILENAME +- "${CMAKE_CURRENT_SOURCE_DIR}/ext/SVT-AV1/Bin/Release/${AVIF_LIBRARY_PREFIX}SvtAv1Enc${AVIF_LIBRARY_SUFFIX}" ++ "${CMAKE_CURRENT_SOURCE_DIR}/ext/SVT-AV1/Bin/Release/${AVIF_LIBRARY_PREFIX}SvtAv1Enc${CMAKE_STATIC_LIBRARY_SUFFIX}" + ) + if(NOT EXISTS "${LIB_FILENAME}") + message(FATAL_ERROR "libavif: compiled svt library is missing (in ext/SVT-AV1/Bin/Release), bailing out") +@@ -450,7 +443,7 @@ if(AVIF_CODEC_AOM) + endif() + set(AVIF_SRCS ${AVIF_SRCS} src/codec_aom.c) + if(AVIF_LOCAL_AOM) +- set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/aom/build.libavif/${AVIF_LIBRARY_PREFIX}aom${AVIF_LIBRARY_SUFFIX}") ++ set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/aom/build.libavif/${AVIF_LIBRARY_PREFIX}aom${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(NOT EXISTS "${LIB_FILENAME}") + message(FATAL_ERROR "libavif: ${LIB_FILENAME} is missing, bailing out") + endif() +@@ -482,7 +475,7 @@ if(AVIF_CODEC_AVM) + set(AVIF_SRCS ${AVIF_SRCS} src/codec_avm.c) + if(AVIF_LOCAL_AVM) + # Building the avm repository generates files such as "libaom.a" because it is a fork of aom. +- set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/avm/build.libavif/${AVIF_LIBRARY_PREFIX}aom${AVIF_LIBRARY_SUFFIX}") ++ set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/avm/build.libavif/${AVIF_LIBRARY_PREFIX}aom${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(NOT EXISTS "${LIB_FILENAME}") + message(FATAL_ERROR "libavif: ${LIB_FILENAME} (from avm) is missing, bailing out") + endif() +diff --git a/ext/libyuv.cmd b/ext/libyuv.cmd +index c959777..1186156 100755 +--- a/ext/libyuv.cmd ++++ b/ext/libyuv.cmd +@@ -22,6 +22,6 @@ git checkout 464c51a0 + mkdir build + cd build + +-cmake -G Ninja -DCMAKE_BUILD_TYPE=Release .. ++cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF .. + ninja yuv + cd ../.. +-- +2.30.0 + diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 2a42bdacba7..98f30ba3679 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -203,7 +203,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. @@ -1220,6 +1220,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.rst b/docs/installation.rst index ca78b29989d..bc38ec08823 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -203,6 +203,15 @@ Many of Pillow's features require external libraries: See `Build Options`_ to see how to build this version. * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. +* **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. + * **libxcb** provides X11 screengrab support. .. tab:: Linux @@ -233,6 +242,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 \ @@ -272,6 +287,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. @@ -309,7 +330,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:: @@ -324,11 +346,11 @@ Many of Pillow's features require external libraries: sudo pkg install python3 - Prerequisites are installed on **FreeBSD 10 or 11** with:: + Prerequisites are installed on **FreeBSD 11 or 12** 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. .. tab:: Android diff --git a/docs/reference/features.rst b/docs/reference/features.rst index c6619306186..3f7f73de0f8 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 fcf4514a84d..af5ac7c5f2e 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 93516671670..19856ed259e 100755 --- a/setup.py +++ b/setup.py @@ -295,6 +295,7 @@ class feature: "jpeg2000", "imagequant", "xcb", + "avif", ] required = {"jpeg", "zlib"} @@ -805,6 +806,12 @@ def build_extensions(self): if _find_library_file(self, "xcb"): feature.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.avif = "avif" + for f in feature: if not getattr(feature, f) and feature.require(f): if f in ("jpeg", "zlib"): @@ -898,6 +905,13 @@ def build_extensions(self): else: self._remove_extension("PIL._webp") + if feature.avif: + libs = [feature.avif] + defs = [] + self._update_extension("PIL._avif", libs, defs) + else: + self._remove_extension("PIL._avif") + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] self._update_extension("PIL._imagingtk", tk_libs) @@ -937,6 +951,7 @@ def summary_report(self, feature): (feature.webp, "WEBP"), (feature.webpmux, "WEBPMUX"), (feature.xcb, "XCB (X protocol)"), + (feature.avif, "LIBAVIF"), ] all = 1 @@ -979,6 +994,7 @@ def debug_build(): 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..6a110282786 --- /dev/null +++ b/src/PIL/AvifImagePlugin.py @@ -0,0 +1,260 @@ +from io import BytesIO + +from . import 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" + +_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 _open(self): + self._decoder = _avif.AvifDecoder( + self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING + ) + + # 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") + qmax = info.get("qmax") + + if qmin is None and qmax is None: + # The min and max quantizer settings in libavif range from 0 (best quality) + # to 63 (worst quality). If neither are explicitly specified, we use a 0-100 + # quality scale (default 75) and calculate the qmin and qmax from that. + # + # - qmin is 0 for quality >= 64. Below that, qmin has an inverse linear + # relation to quality (i.e., quality 63 = qmin 1, quality 0 => qmin 63) + # - qmax is 0 for quality=100, then qmax increases linearly relative to + # quality decreasing, until it flattens out at quality=37. + quality = info.get("quality", 75) + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + qmin = max(0, min(64 - quality, 63)) + qmax = max(0, min(100 - quality, 63)) + + duration = info.get("duration", 0) + subsampling = info.get("subsampling", "4:2:0") + speed = info.get("speed", 6) + 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() + 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, + codec, + range_, + tile_rows_log2, + tile_cols_log2, + alpha_premultiplied, + autotiling, + icc_profile or b"", + exif or b"", + 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()) + ) + 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 2a6b4646bbd..40ef526dc0d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1442,7 +1442,9 @@ def getexif(self): # 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 2bb8f6d7f10..a13ec83f967 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -23,6 +23,7 @@ _plugins = [ + "AvifImagePlugin", "BlpImagePlugin", "BmpImagePlugin", "BufrStubImagePlugin", diff --git a/src/PIL/features.py b/src/PIL/features.py index f14e60cf5d4..af5a09dc58f 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -13,6 +13,7 @@ "freetype2": ("PIL._imagingft", "freetype2_version"), "littlecms2": ("PIL._imagingcms", "littlecms_version"), "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), } @@ -263,6 +264,7 @@ def pilinfo(out=None, supported_formats=True): ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), + ("avif", "AVIF"), ("transp_webp", "WEBP Transparency"), ("webp_mux", "WEBPMUX"), ("webp_anim", "WEBP Animation"), diff --git a/src/_avif.c b/src/_avif.c new file mode 100644 index 00000000000..55a592c1a14 --- /dev/null +++ b/src/_avif.c @@ -0,0 +1,908 @@ +#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 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; + } + + 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 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; + 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, + "IIsiiiissiiOOSSSO", + &width, + &height, + &subsampling, + &qmin, + &qmax, + &quality, + &speed, + &codec, + &range, + &tile_rows_log2, + &tile_cols_log2, + &alpha_premultiplied, + &autotiling, + &icc_bytes, + &exif_bytes, + &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; + } + + 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) { + init_max_threads(); + } + + encoder->maxThreads = max_threads; +#if AVIF_VERSION >= 1000000 + 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)); + } + + 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; + + avifResult result; + + if (!PyArg_ParseTuple(args, "Sss", &avif_bytes, &codec_str, &upsampling_str)) { + 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) { + init_max_threads(); + } + self->decoder->maxThreads = max_threads; +#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_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/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6cc..ae746fbc3eb 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 d9122e680a2..95670eb04e7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -36,6 +36,11 @@ def cmd_rmdir(path): return f'rmdir /S /Q "{path}"' +def cmd_lib_combine(outfile, *libfiles): + params = " ".join(['"%s"' % f for f in libfiles]) + return f"LIB.EXE /OUT:{outfile} {params}" + + def cmd_nmake(makefile=None, target="", params=None): if params is None: params = "" @@ -361,6 +366,39 @@ def cmd_msbuild( ], "bins": [r"*.dll"], }, + "libavif": { + "url": "https://github.com/AOMediaCodec/libavif/archive/v1.0.1.zip", + "filename": "libavif-1.0.1.zip", + "dir": "libavif-1.0.1", + "license": "LICENSE", + "build": [ + cmd_cd("ext"), + cmd_rmdir("aom"), + 'cmd.exe /c "aom.cmd"', + cmd_rmdir("dav1d"), + 'cmd.exe /c "dav1d.cmd"', + cmd_cd(".."), + *cmds_cmake( + "avif", + "-DBUILD_SHARED_LIBS=OFF", + "-DAVIF_CODEC_AOM=ON", + "-DAVIF_LOCAL_AOM=ON", + "-DAVIF_CODEC_DAV1D=ON", + "-DAVIF_LOCAL_DAV1D=ON", + ), + cmd_nmake(), + cmd_cd(".."), + cmd_lib_combine( + r"avif_combined.lib", + r"avif.lib", + r"ext\aom\build.libavif\aom.lib", + r"ext\dav1d\build\src\libdav1d.a", + ), + cmd_mkdir(r"{inc_dir}\avif"), + cmd_copy(r"include\avif\avif.h", r"{inc_dir}\avif"), + ], + "libs": [r"avif_combined.lib"], + }, } @@ -632,6 +670,11 @@ def build_dep_all(): 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] @@ -672,6 +715,8 @@ def build_dep_all(): disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] + if args.no_avif: + disabled += ["libavif"] prefs = { "architecture": args.architecture,