From b9fe001232a3076ae470ab292bbdd4607ceb0000 Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:34:19 +0200 Subject: [PATCH 01/14] feat: AVIF image support --- app/Config/Mimes.php | 1 + app/Config/Publisher.php | 2 +- system/Config/Publisher.php | 2 +- system/Images/Handlers/BaseHandler.php | 3 +- system/Images/Handlers/GDHandler.php | 23 ++++++++++-- system/Images/Image.php | 1 + system/Language/en/Images.php | 1 + tests/system/Images/GDHandlerTest.php | 49 ++++++++++++++++++++------ 8 files changed, 66 insertions(+), 16 deletions(-) diff --git a/app/Config/Mimes.php b/app/Config/Mimes.php index c2db7340cd7e..c8f3297336a3 100644 --- a/app/Config/Mimes.php +++ b/app/Config/Mimes.php @@ -259,6 +259,7 @@ class Mimes 'image/x-png', ], 'webp' => 'image/webp', + 'avif' => 'image/avif', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'css' => [ diff --git a/app/Config/Publisher.php b/app/Config/Publisher.php index bf03be1db7e0..0cb6769972fc 100644 --- a/app/Config/Publisher.php +++ b/app/Config/Publisher.php @@ -23,6 +23,6 @@ class Publisher extends BasePublisher */ public $restrictions = [ ROOTPATH => '*', - FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|avif|bmp|ico|svg)$#i', ]; } diff --git a/system/Config/Publisher.php b/system/Config/Publisher.php index 7e1b0ffb54f2..3038644e4aeb 100644 --- a/system/Config/Publisher.php +++ b/system/Config/Publisher.php @@ -32,7 +32,7 @@ class Publisher extends BaseConfig */ public $restrictions = [ ROOTPATH => '*', - FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|avif|bmp|ico|svg)$#i', ]; /** diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index e38c5836ccca..442614b3d00f 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -115,6 +115,7 @@ abstract class BaseHandler implements ImageHandlerInterface protected $supportTransparency = [ IMAGETYPE_PNG, IMAGETYPE_WEBP, + IMAGETYPE_AVIF, ]; /** @@ -682,7 +683,7 @@ abstract public function getVersion(); * * @return bool */ - abstract public function save(?string $target = null, int $quality = 90); + abstract public function save(?string $target = null, int $quality = 90, int $speed = -1); /** * Does the driver-specific processing of the image. diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 01c05384c744..851a6d21c55a 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -178,7 +178,7 @@ protected function process(string $action) $dest = $create($this->width, $this->height); - // for png and webp we can actually preserve transparency + // for png, webp and avif we can actually preserve transparency if (in_array($this->image()->imageType, $this->supportTransparency, true)) { imagealphablending($dest, false); imagesavealpha($dest, true); @@ -202,7 +202,7 @@ protected function process(string $action) * * @param non-empty-string|null $target */ - public function save(?string $target = null, int $quality = 90): bool + public function save(?string $target = null, int $quality = 90, int $speed = -1): bool { $original = $target; $target = ($target === null || $target === '') ? $this->image()->getPathname() : $target; @@ -222,7 +222,7 @@ public function save(?string $target = null, int $quality = 90): bool $this->ensureResource(); - // for png and webp we can actually preserve transparency + // for png, webp and avif we can actually preserve transparency if (in_array($this->image()->imageType, $this->supportTransparency, true)) { imagepalettetotruecolor($this->resource); imagealphablending($this->resource, false); @@ -270,6 +270,16 @@ public function save(?string $target = null, int $quality = 90): bool } break; + case IMAGETYPE_AVIF: + if (! function_exists('imageavif')) { + throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported')); + } + + if (! @imageavif($this->resource, $target, $quality, $speed)) { + throw ImageException::forSaveFailed(); + } + break; + default: throw ImageException::forInvalidImageCreate(); } @@ -361,6 +371,13 @@ protected function getImageResource(string $path, int $imageType) return imagecreatefromwebp($path); + case IMAGETYPE_AVIF: + if (! function_exists('imagecreatefromavif')) { + throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported')); + } + + return imagecreatefromavif($path); + default: throw ImageException::forInvalidImageCreate('Ima'); } diff --git a/system/Images/Image.php b/system/Images/Image.php index 55754e339535..b69b78fa6b27 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -113,6 +113,7 @@ public function getProperties(bool $return = false) IMAGETYPE_JPEG => 'jpeg', IMAGETYPE_PNG => 'png', IMAGETYPE_WEBP => 'webp', + IMAGETYPE_AVIF => 'avif', ]; $mime = 'image/' . ($types[$vals[2]] ?? 'jpg'); diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php index f5dfc9435ea8..2302cfb89018 100644 --- a/system/Language/en/Images.php +++ b/system/Language/en/Images.php @@ -20,6 +20,7 @@ 'jpgNotSupported' => 'JPG images are not supported.', 'pngNotSupported' => 'PNG images are not supported.', 'webpNotSupported' => 'WEBP images are not supported.', + 'avifNotSupported' => 'AVIF images are not supported.', 'fileNotSupported' => 'The supplied file is not a supported image type.', 'unsupportedImageCreate' => 'Your server does not support the required functionality to process this type of image.', 'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.', diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php index d85d1a142ffd..c75fd3c47cf8 100644 --- a/tests/system/Images/GDHandlerTest.php +++ b/tests/system/Images/GDHandlerTest.php @@ -319,10 +319,14 @@ public function testMoreText(): void public function testImageCreation(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! function_exists('imagecreatefromavif')) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -334,10 +338,14 @@ public function testImageCreation(): void public function testImageCopy(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! function_exists('imagecreatefromavif')) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -353,9 +361,13 @@ public function testImageCopy(): void public function testImageCopyWithNoTargetAndMaxQuality(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { $this->handler->withFile($this->origin . 'ci-logo.' . $type); - $this->handler->save(null, 100); + if($type === 'avif') { + $this->handler->save(null, 100, 10); + } else { + $this->handler->save(null, 100); + } $this->assertFileExists($this->origin . 'ci-logo.' . $type); $this->assertSame( @@ -367,10 +379,14 @@ public function testImageCopyWithNoTargetAndMaxQuality(): void public function testImageCompressionGetResource(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! function_exists('imagecreatefromavif')) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -387,10 +403,14 @@ public function testImageCompressionGetResource(): void public function testImageCompressionWithResource(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! function_exists('imagecreatefromavif')) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type) @@ -423,6 +443,15 @@ public function testImageConvertPngToWebp(): void $this->assertSame(IMAGETYPE_WEBP, exif_imagetype($saved)); } + public function testImageConvertPngToAvif(): void + { + $this->handler->withFile($this->origin . 'rocket.png'); + $this->handler->convert(IMAGETYPE_AVIF); + $saved = $this->start . 'work/rocket.avif'; + $this->handler->save($saved); + $this->assertSame(IMAGETYPE_AVIF, exif_imagetype($saved)); + } + public function testImageReorientLandscape(): void { for ($i = 0; $i <= 8; $i++) { From e793e78377ead56c81678dc076c4bb2658092c4e Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:53:17 +0200 Subject: [PATCH 02/14] code formatting for PHP CS fixer --- tests/system/Images/GDHandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php index c75fd3c47cf8..87656a171415 100644 --- a/tests/system/Images/GDHandlerTest.php +++ b/tests/system/Images/GDHandlerTest.php @@ -363,7 +363,7 @@ public function testImageCopyWithNoTargetAndMaxQuality(): void { foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { $this->handler->withFile($this->origin . 'ci-logo.' . $type); - if($type === 'avif') { + if ($type === 'avif') { $this->handler->save(null, 100, 10); } else { $this->handler->save(null, 100); From fa4df35653b3c9772821add4e9dabf44b49746ec Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:54:14 +0200 Subject: [PATCH 03/14] also modify the ImageMagick save function to match the BaseHandler --- system/Images/Handlers/ImageMagickHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index f0071b7e0e6a..ffbdcfcacdf2 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -324,7 +324,7 @@ protected function supportedFormatCheck() * * @throws ImagickException */ - public function save(?string $target = null, int $quality = 90): bool + public function save(?string $target = null, int $quality = 90, int $speed = -1): bool { $original = $target; $target = ($target === null || $target === '') ? $this->image()->getPathname() : $target; From 10211ca68c0acf7c7f2ed7aaa268921a1dc100ea Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:08:41 +0200 Subject: [PATCH 04/14] add ci-logo.avif generated with GIMP 3.0.8, quality 52, speed balanced --- tests/_support/Images/ci-logo.avif | Bin 0 -> 2343 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/_support/Images/ci-logo.avif diff --git a/tests/_support/Images/ci-logo.avif b/tests/_support/Images/ci-logo.avif new file mode 100644 index 0000000000000000000000000000000000000000..9741b69ab0c95d48cddc85e337400dfbf7783632 GIT binary patch literal 2343 zcmaJ=c|6oxA3ih2dPmn*$jB(IlDO*4RJQaI*$G`_HvDRqUo&GZMTizPDp`_!NoX^< zx4PMvQr!b->ra?6|Y`qDlm?L522Pxx+pga@+nT-fa=fWf+ zHOypNEFeONFQ;=^Je0?U^Wt+^bT*%01&_%Q@Z9Ze$UeTi$@0TM2FL<6pbKQFbUteB z>besO=P8q&vpeoE!sePMX+Bz+!Q?|vaAcTZMMrtiLR=uk8^Z-ChSMR|podTy5SK%| zmIpToaW}@(X7LDygQZyNjRw!teH%QR2IR@X|G>0=V0s7--VELh<`^*895_GTXBIOs zd=SHIR;cv8AjWQs$mDo=U_7iOUne&z(^qZ-a!Vzr%kbN;T^Km%<@@D~<7S7Ck%bS^* z&WHCc2B4NRr|0IvCnSNqaE>#`0pQno0IC}2I9eJ2e{%C}2LMNi#mf)?o{%6T2R#Cd5gv2)Pr~(} znuXz*&Kcjo;-Jdn*#_)6}2N+}^yD zRa@6NK)RGh`sS~4a{I{1i*Ef9-;!v-;pvY~1zpW3w@}^s`BM8%ysSjQeIP85SJ@pZ zbXIAbB(15=jkj8!Yi-#2H*rwhu)R$-syiWrw@O`eGCb4aHP6}J#xzc~`(s1#yLgMv zJB7#E8*CRGjbsGDt;tEZ=4o23pxXEA7FSV<#+JR0#ir@O3Z;R* zGXhnu`c5U$s%M46e=chO)oj1#IgxI6-9=CDLi{)5rsyGC;u^7~iXly+ zx_Zh$CLHNqtQXqE>2MtW<<%8jvfsuD%9hC?xBgN!yN`>+ZSo_-GOl*!jlYz})_lsI zR+-v${du)R@c^&63cY*G(K;?eOSdmo33czgA=f2zl8BX>>+r40)g?Qs$O+~ibYPlm zynj`@M;U9A=c4$#34TW?PdW{F6EqV8PIJt-W1;^_^A+mX)CZpq*c(UN?yJ#uY)kPU zqJ7UPykmIDEAZ`m`@6~~U!~=emw8p)A635xlrje-?k%5%ri`KI&R$BO=f-Ry2D)O-W1_vz4^k_F_hn7_*Jx>%Of3(xmK;SCuAx2OOAKL83-=ar;&&B1$XQ*?_hl-lpDc^Mw7U#1j28uj_*s&i@3t@&tvB|r71vtyl)=69h3YWT$@CfX&2^TPI`{?!tO(S6Q3x00f|k406xd{sN% zdRjP~W;+XF4&53zAd(-S&U)^ekSF}=jy}lJUa+Nl*IL86D=nJVt(Pw=j&t-0=fg6N zint2r#$tPmJFG;+-OuzU>S@i>YOf{JhaOB`l#lc&6KV%$6I-d`{LO5mhXVIyt6TnU zcGOLh|L$KDeX|}t0}o&}n7Fl3DZcWK){aTts9n7Siunez-uXu7o$41+E4srT^tmjH zioX94pP#szh`6;)~u^y1lle z&bih~l3tQx`jV^<-7?u{D=lx97H5{H=6+gIfi{ObcO}%kZGR)WMfkg_ZTw7 zXQwFISjbjwXy2L`J>|0EQuUGHqBwLWoe`j8d*itF@QA1$IdMPFMN5sSFc_;d5g6DK ia?Eus&%Yx?!}mzYh`OXDhbYn+%5giK{W$r0%zpvwUnV*L literal 0 HcmV?d00001 From d9c219d9780c8c35cf843c93bc937a6e5d9b2151 Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:33:41 +0200 Subject: [PATCH 05/14] remove the speed parameter --- system/Images/Handlers/BaseHandler.php | 2 +- system/Images/Handlers/GDHandler.php | 4 ++-- system/Images/Handlers/ImageMagickHandler.php | 2 +- tests/system/Images/GDHandlerTest.php | 6 +----- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 442614b3d00f..7784663b0b7a 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -683,7 +683,7 @@ abstract public function getVersion(); * * @return bool */ - abstract public function save(?string $target = null, int $quality = 90, int $speed = -1); + abstract public function save(?string $target = null, int $quality = 90); /** * Does the driver-specific processing of the image. diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 851a6d21c55a..2e91ecac89a0 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -202,7 +202,7 @@ protected function process(string $action) * * @param non-empty-string|null $target */ - public function save(?string $target = null, int $quality = 90, int $speed = -1): bool + public function save(?string $target = null, int $quality = 90): bool { $original = $target; $target = ($target === null || $target === '') ? $this->image()->getPathname() : $target; @@ -275,7 +275,7 @@ public function save(?string $target = null, int $quality = 90, int $speed = -1) throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported')); } - if (! @imageavif($this->resource, $target, $quality, $speed)) { + if (! @imageavif($this->resource, $target, $quality)) { throw ImageException::forSaveFailed(); } break; diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index ffbdcfcacdf2..f0071b7e0e6a 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -324,7 +324,7 @@ protected function supportedFormatCheck() * * @throws ImagickException */ - public function save(?string $target = null, int $quality = 90, int $speed = -1): bool + public function save(?string $target = null, int $quality = 90): bool { $original = $target; $target = ($target === null || $target === '') ? $this->image()->getPathname() : $target; diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php index 87656a171415..9d92c618c366 100644 --- a/tests/system/Images/GDHandlerTest.php +++ b/tests/system/Images/GDHandlerTest.php @@ -363,11 +363,7 @@ public function testImageCopyWithNoTargetAndMaxQuality(): void { foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { $this->handler->withFile($this->origin . 'ci-logo.' . $type); - if ($type === 'avif') { - $this->handler->save(null, 100, 10); - } else { - $this->handler->save(null, 100); - } + $this->handler->save(null, 100); $this->assertFileExists($this->origin . 'ci-logo.' . $type); $this->assertSame( From 538713e81364029fe344b0b0a3694d61abd9a2bb Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:47:24 +0200 Subject: [PATCH 06/14] correct lang string reference --- system/Images/Handlers/ImageMagickHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index f0071b7e0e6a..2991d63cf4da 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -307,7 +307,7 @@ protected function supportedFormatCheck() } if ($this->image()->imageType === IMAGETYPE_WEBP && ! in_array('WEBP', Imagick::queryFormats(), true)) { - throw ImageException::forInvalidImageCreate(lang('images.webpNotSupported')); + throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported')); } } From 4a1fc1e777e405b061754e1833a857dd586bf2af Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:19:20 +0200 Subject: [PATCH 07/14] extend image type checks in ImageMagick to make those for GD --- system/Images/Handlers/ImageMagickHandler.php | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index 2991d63cf4da..80637951f29e 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -306,8 +306,38 @@ protected function supportedFormatCheck() return; } - if ($this->image()->imageType === IMAGETYPE_WEBP && ! in_array('WEBP', Imagick::queryFormats(), true)) { - throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported')); + $supported = Imagick::queryFormats(); + + switch ($this->image()->imageType) { + case IMAGETYPE_GIF: + if (! in_array('GIF', $supported, true)) { + throw ImageException::forInvalidImageCreate(lang('Images.gifNotSupported')); + } + break; + + case IMAGETYPE_JPEG: + if (! in_array('JPEG', $supported, true)) { + throw ImageException::forInvalidImageCreate(lang('Images.jpgNotSupported')); + } + break; + + case IMAGETYPE_PNG: + if (! in_array('PNG', $supported, true)) { + throw ImageException::forInvalidImageCreate(lang('Images.pngNotSupported')); + } + break; + + case IMAGETYPE_WEBP: + if (! in_array('WEBP', $supported, true)) { + throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported')); + } + break; + + case IMAGETYPE_AVIF: + if (! in_array('AVIF', $supported, true)) { + throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported')); + } + break; } } From f5bf1a7c540aadd3a0d7f959c9c84a55538f6c58 Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:41:19 +0200 Subject: [PATCH 08/14] expand tests for AVIF image format, add missing testImageConvertPngToWebp function --- .../system/Images/ImageMagickHandlerTest.php | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/tests/system/Images/ImageMagickHandlerTest.php b/tests/system/Images/ImageMagickHandlerTest.php index 9c322c8c520f..535ad5e39fd7 100644 --- a/tests/system/Images/ImageMagickHandlerTest.php +++ b/tests/system/Images/ImageMagickHandlerTest.php @@ -313,10 +313,14 @@ public function testMoreText(): void public function testImageCreation(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! in_array('WEBP', Imagick::queryFormats(), true)) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! in_array('AVIF', Imagick::queryFormats(), true)) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -328,10 +332,14 @@ public function testImageCreation(): void public function testImageCopy(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! in_array('WEBP', Imagick::queryFormats(), true)) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! in_array('AVIF', Imagick::queryFormats(), true)) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -347,7 +355,7 @@ public function testImageCopy(): void public function testImageCopyWithNoTargetAndMaxQuality(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { $this->handler->withFile($this->origin . 'ci-logo.' . $type); $this->handler->save(null, 100); $this->assertFileExists($this->origin . 'ci-logo.' . $type); @@ -361,10 +369,14 @@ public function testImageCopyWithNoTargetAndMaxQuality(): void public function testImageCompressionGetResource(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! in_array('WEBP', Imagick::queryFormats(), true)) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! in_array('AVIF', Imagick::queryFormats(), true)) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -381,10 +393,14 @@ public function testImageCompressionGetResource(): void public function testImageCompressionWithResource(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! in_array('WEBP', Imagick::queryFormats(), true)) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! in_array('AVIF', Imagick::queryFormats(), true)) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type) @@ -408,6 +424,22 @@ public function testImageConvert(): void $this->assertSame(IMAGETYPE_PNG, exif_imagetype($this->root . 'ci-logo.png')); } + public function testImageConvertPngToWebp(): void + { + $this->handler->withFile($this->origin . 'ci-logo.png'); + $this->handler->convert(IMAGETYPE_WEBP); + $this->handler->save($this->root . 'ci-logo.webp'); + $this->assertSame(IMAGETYPE_WEBP, exif_imagetype($this->root . 'ci-logo.webp')); + } + + public function testImageConvertPngToAvif(): void + { + $this->handler->withFile($this->origin . 'ci-logo.png'); + $this->handler->convert(IMAGETYPE_AVIF); + $this->handler->save($this->root . 'ci-logo.avif'); + $this->assertSame(IMAGETYPE_AVIF, exif_imagetype($this->root . 'ci-logo.avif')); + } + public function testImageReorientLandscape(): void { for ($i = 0; $i <= 8; $i++) { From f0bf795d89ff411e6e727cdbeb49fd49caa0935d Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Sat, 7 Mar 2026 12:06:59 +0200 Subject: [PATCH 09/14] add debug code to see why test fails --- tests/system/Images/ImageMagickHandlerTest.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/system/Images/ImageMagickHandlerTest.php b/tests/system/Images/ImageMagickHandlerTest.php index 535ad5e39fd7..d60a0ba8b70f 100644 --- a/tests/system/Images/ImageMagickHandlerTest.php +++ b/tests/system/Images/ImageMagickHandlerTest.php @@ -436,7 +436,22 @@ public function testImageConvertPngToAvif(): void { $this->handler->withFile($this->origin . 'ci-logo.png'); $this->handler->convert(IMAGETYPE_AVIF); - $this->handler->save($this->root . 'ci-logo.avif'); + $rc = $this->handler->save($this->root . 'ci-logo.avif'); + +echo "\n\n"; +var_dump($rc); +var_dump($this->root); +var_dump($this->root . 'ci-logo.avif'); +if(is_file($this->root . 'ci-logo.avif') { + echo "filesize: " . filesize($this->root . 'ci-logo.avif') . "\n"; +} else { + echo "is_file fail\n"; +} +var_dump(exif_imagetype($this->root . 'ci-logo.avif')); +var_dump( IMAGETYPE_AVIF ); +echo "\n\n"; + + $this->assertSame(IMAGETYPE_AVIF, exif_imagetype($this->root . 'ci-logo.avif')); } From ff45fa8238f5a91f65f2fe4b235da1c0351cc298 Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Sat, 7 Mar 2026 12:11:14 +0200 Subject: [PATCH 10/14] add debug code to see why test fails --- .../system/Images/ImageMagickHandlerTest.php | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/system/Images/ImageMagickHandlerTest.php b/tests/system/Images/ImageMagickHandlerTest.php index d60a0ba8b70f..cdd690b2363a 100644 --- a/tests/system/Images/ImageMagickHandlerTest.php +++ b/tests/system/Images/ImageMagickHandlerTest.php @@ -438,19 +438,18 @@ public function testImageConvertPngToAvif(): void $this->handler->convert(IMAGETYPE_AVIF); $rc = $this->handler->save($this->root . 'ci-logo.avif'); -echo "\n\n"; -var_dump($rc); -var_dump($this->root); -var_dump($this->root . 'ci-logo.avif'); -if(is_file($this->root . 'ci-logo.avif') { - echo "filesize: " . filesize($this->root . 'ci-logo.avif') . "\n"; -} else { - echo "is_file fail\n"; -} -var_dump(exif_imagetype($this->root . 'ci-logo.avif')); -var_dump( IMAGETYPE_AVIF ); -echo "\n\n"; - + echo "\n\n"; + var_dump($rc); + var_dump($this->root); + var_dump($this->root . 'ci-logo.avif'); + if (is_file($this->root . 'ci-logo.avif')) { + echo "filesize: " . filesize($this->root . 'ci-logo.avif') . "\n"; + } else { + echo "is_file fail\n"; + } + var_dump(exif_imagetype($this->root . 'ci-logo.avif')); + var_dump( IMAGETYPE_AVIF ); + echo "\n\n"; $this->assertSame(IMAGETYPE_AVIF, exif_imagetype($this->root . 'ci-logo.avif')); } From d12910d827378e55954e9ad0550bf521d6bdc6a4 Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Sat, 7 Mar 2026 12:39:22 +0200 Subject: [PATCH 11/14] add debug code to see why test fails --- system/Images/Handlers/ImageMagickHandler.php | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index 80637951f29e..0fa9132a5b28 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -356,43 +356,55 @@ protected function supportedFormatCheck() */ public function save(?string $target = null, int $quality = 90): bool { + echo "\n\nstart of save() with target: " . $target . " and quality: " . $quality . "\n"; $original = $target; $target = ($target === null || $target === '') ? $this->image()->getPathname() : $target; - + echo "target is now: " . $target . "\n"; // If no new resource has been created, then we're // simply copy the existing one. if (! $this->resource instanceof Imagick && $quality === 100) { + echo "instance not Imagick, quality: " . $quality . "\n"; if ($original === null) { + echo "original is null, return true\n"; return true; } $name = basename($target); $path = pathinfo($target, PATHINFO_DIRNAME); - + echo "return result from image()->copy()\nend of save()\n"; return $this->image()->copy($path, $name); } - + echo "ensureResource() call\n"; $this->ensureResource(); - + echo "setImageCompressionQuality() call\n"; $this->resource->setImageCompressionQuality($quality); - + echo "if target is not null\n"; + var_dump($target); if ($target !== null) { + echo "target is not null\n"; $extension = pathinfo($target, PATHINFO_EXTENSION); + echo "setImageFormat() call\n"; + var_dump($extension); $this->resource->setImageFormat($extension); } - + echo "try...\n"; try { + echo "writeImage() call\n"; $result = $this->resource->writeImage($target); + echo "result: \n"; + var_dump($result); chmod($target, $this->filePermissions); $this->resource->clear(); $this->resource = null; - + echo "return result\nend of save()\n"; return $result; } catch (ImagickException) { + echo "EXCEPTION\n"; throw ImageException::forSaveFailed(); } + echo "last end of save()\n"; } /** From 710a4d1368a5ff63b9f3adc7ed9efcae2dd69eac Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:15:28 +0200 Subject: [PATCH 12/14] remove debug code --- system/Images/Handlers/ImageMagickHandler.php | 26 +++++-------------- .../system/Images/ImageMagickHandlerTest.php | 16 +----------- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index 0fa9132a5b28..80637951f29e 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -356,55 +356,43 @@ protected function supportedFormatCheck() */ public function save(?string $target = null, int $quality = 90): bool { - echo "\n\nstart of save() with target: " . $target . " and quality: " . $quality . "\n"; $original = $target; $target = ($target === null || $target === '') ? $this->image()->getPathname() : $target; - echo "target is now: " . $target . "\n"; + // If no new resource has been created, then we're // simply copy the existing one. if (! $this->resource instanceof Imagick && $quality === 100) { - echo "instance not Imagick, quality: " . $quality . "\n"; if ($original === null) { - echo "original is null, return true\n"; return true; } $name = basename($target); $path = pathinfo($target, PATHINFO_DIRNAME); - echo "return result from image()->copy()\nend of save()\n"; + return $this->image()->copy($path, $name); } - echo "ensureResource() call\n"; + $this->ensureResource(); - echo "setImageCompressionQuality() call\n"; + $this->resource->setImageCompressionQuality($quality); - echo "if target is not null\n"; - var_dump($target); + if ($target !== null) { - echo "target is not null\n"; $extension = pathinfo($target, PATHINFO_EXTENSION); - echo "setImageFormat() call\n"; - var_dump($extension); $this->resource->setImageFormat($extension); } - echo "try...\n"; + try { - echo "writeImage() call\n"; $result = $this->resource->writeImage($target); - echo "result: \n"; - var_dump($result); chmod($target, $this->filePermissions); $this->resource->clear(); $this->resource = null; - echo "return result\nend of save()\n"; + return $result; } catch (ImagickException) { - echo "EXCEPTION\n"; throw ImageException::forSaveFailed(); } - echo "last end of save()\n"; } /** diff --git a/tests/system/Images/ImageMagickHandlerTest.php b/tests/system/Images/ImageMagickHandlerTest.php index cdd690b2363a..535ad5e39fd7 100644 --- a/tests/system/Images/ImageMagickHandlerTest.php +++ b/tests/system/Images/ImageMagickHandlerTest.php @@ -436,21 +436,7 @@ public function testImageConvertPngToAvif(): void { $this->handler->withFile($this->origin . 'ci-logo.png'); $this->handler->convert(IMAGETYPE_AVIF); - $rc = $this->handler->save($this->root . 'ci-logo.avif'); - - echo "\n\n"; - var_dump($rc); - var_dump($this->root); - var_dump($this->root . 'ci-logo.avif'); - if (is_file($this->root . 'ci-logo.avif')) { - echo "filesize: " . filesize($this->root . 'ci-logo.avif') . "\n"; - } else { - echo "is_file fail\n"; - } - var_dump(exif_imagetype($this->root . 'ci-logo.avif')); - var_dump( IMAGETYPE_AVIF ); - echo "\n\n"; - + $this->handler->save($this->root . 'ci-logo.avif'); $this->assertSame(IMAGETYPE_AVIF, exif_imagetype($this->root . 'ci-logo.avif')); } From bccf811b392f634bd9defc9e7cb6576528b29f8f Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:36:11 +0200 Subject: [PATCH 13/14] ImageMagick rw support for AVIF as suggested at #10025 --- .github/workflows/reusable-phpunit-test.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 7881d688bc70..872f5bf128a6 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -165,7 +165,16 @@ jobs: if: ${{ contains(inputs.extra-extensions, 'imagick') }} run: | sudo apt-get update - sudo apt-get install -y imagemagick libmagickwand-dev ghostscript poppler-data libjbig2dec0:amd64 libopenjp2-7:amd64 + sudo apt-get install -y ghostscript poppler-data libmagickwand-dev + + # Install ImageMagick 7 with AVIF rw+ support (vintagesucks/imagemagick-deb) + RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/vintagesucks/imagemagick-deb/releases/latest) + mkdir -p /tmp/imagemagick-debs + while IFS= read -r url; do + curl -fsSL "$url" -o "/tmp/imagemagick-debs/$(basename "$url")" + done < <(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | contains("noble_amd64")) | .browser_download_url') + sudo dpkg -i /tmp/imagemagick-debs/*.deb || true + sudo apt-get install -f -y - name: Checkout base branch for PR if: github.event_name == 'pull_request' From c3df1eba4594a818f29bb501e183db27b72773e0 Mon Sep 17 00:00:00 2001 From: Julian Atkins <156084296+JulianAtkins@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:20:43 +0200 Subject: [PATCH 14/14] update changelog --- user_guide_src/source/changelogs/v4.8.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index abc8265e0e63..0d42fd103740 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -174,6 +174,7 @@ Libraries ========= - **Context**: This new feature allows you to easily set and retrieve normal or hidden contextual data for the current request. See :ref:`Context ` for details. +- **Images:**: Added support for the AVIF file format. Helpers and Functions =====================