From cc3fdbcf2d0b95d1b103e631b3f3d1db36307ea9 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 7 Oct 2026 13:12:35 +0200 Subject: [PATCH 1/7] Test HEIC writing in image format tests Move HEIC from the read-only formats to the writable ones, to see if h265 encoding is enabled. The vips library writes HEIC through the HEIF saver, and written files are detected by their "heic" or "heix" brand. The report no longer hardcodes the number of read-only formats, and the README reflects the new set of written formats. --- README.md | 4 ++-- tests/image-formats.php | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3a17f09..b2cd179 100644 --- a/README.md +++ b/README.md @@ -144,8 +144,8 @@ Check the latest stable release on [php.net](https://www.php.net). The workflow `.github/workflows/docker.test.yaml` builds the image for every supported PHP version on each push and pull request and runs `tests/image-formats.php` inside it. The script checks that GD, Imagick -and vips can read, manipulate and write JPEG, PNG, GIF, WebP and AVIF -images, can read SVG, HEIC, TIFF and the first page of a PDF, keep +and vips can read, manipulate and write JPEG, PNG, GIF, WebP, AVIF and +HEIC images, can read SVG, TIFF and the first page of a PDF, keep transparency when writing PNG, GIF, WebP and AVIF, and keep the frames of an animated GIF when writing GIF and WebP. A library without any API for a format, such as GD built without AVIF support, is reported as skipped. diff --git a/tests/image-formats.php b/tests/image-formats.php index abefb32..c0f65a7 100755 --- a/tests/image-formats.php +++ b/tests/image-formats.php @@ -10,7 +10,7 @@ * runs it through resize, rotate, flip and crop, writes it in each format and * reads the result back. The fixtures in tests/fixtures were produced by an * independent toolchain and cover reading, including formats which are only - * ever read on a web server, such as SVG, HEIC, TIFF and PDF. Files written + * ever read on a web server, such as SVG, TIFF and PDF. Files written * by one library are read by the others as well. Separate checks make sure * that transparency and animation frames survive a resize and a write. * @@ -23,8 +23,8 @@ * least one library. */ -const WRITABLE_FORMATS = ['jpeg', 'png', 'gif', 'webp', 'avif']; -const READ_ONLY_FORMATS = ['svg', 'heic', 'tiff', 'pdf']; +const WRITABLE_FORMATS = ['jpeg', 'png', 'gif', 'webp', 'avif', 'heic']; +const READ_ONLY_FORMATS = ['svg', 'tiff', 'pdf']; const ALPHA_FORMATS = ['png', 'gif', 'webp', 'avif']; const ANIMATION_FORMATS = ['gif', 'webp']; @@ -478,6 +478,7 @@ final class VipsLibrary implements ImageLibrary 'gif' => ['VipsForeignSaveCgifFile', 'VipsForeignSaveGifFile'], 'webp' => ['VipsForeignSaveWebpFile'], 'avif' => ['VipsForeignSaveHeifFile', 'VipsForeignSaveAvifFile'], + 'heic' => ['VipsForeignSaveHeifFile'], ]; public function name(): string @@ -540,7 +541,7 @@ public function write(mixed $image, string $format, string $path): void $this->requireOperation(self::SAVERS[$format], sprintf('this libvips build has no %s saver', $format)); $options = match ($format) { 'jpeg', 'webp' => ['Q' => 90], - 'avif' => ['Q' => 60], + 'avif', 'heic' => ['Q' => 60], default => [], }; if (!is_array(vips_image_write_to_file($image, $path, $options))) { @@ -772,7 +773,7 @@ public function markdown(array $libraries, array $versions): string $lines[] = ''; $lines[] = implode(', ', $versions); $lines[] = ''; - $lines[] = 'Read and write per library (read / write, read only for the last four):'; + $lines[] = sprintf('Read and write per library (read / write, read only for the last %d):', count(READ_ONLY_FORMATS)); $lines[] = ''; $lines[] = '| Library | Manipulate | ' . implode(' | ', [...WRITABLE_FORMATS, ...READ_ONLY_FORMATS]) . ' |'; $lines[] = '|---|---|' . str_repeat('---|', count(WRITABLE_FORMATS) + count(READ_ONLY_FORMATS)); @@ -972,6 +973,9 @@ function detectFormat(string $path): ?string if (substr($bytes, 4, 4) === 'ftyp' && in_array(substr($bytes, 8, 4), ['avif', 'avis'], true)) { return 'avif'; } + if (substr($bytes, 4, 4) === 'ftyp' && in_array(substr($bytes, 8, 4), ['heic', 'heix'], true)) { + return 'heic'; + } return null; } From c761126af3ccc8af7ace5472e9aca5c4f5c79487 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 7 Oct 2026 13:42:15 +0200 Subject: [PATCH 2/7] Test HEIC transparency in image format tests Add HEIC to the formats which must keep their alpha channel when written, and mention it in the README accordingly. --- README.md | 2 +- tests/image-formats.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b2cd179..240e0b2 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ every supported PHP version on each push and pull request and runs `tests/image-formats.php` inside it. The script checks that GD, Imagick and vips can read, manipulate and write JPEG, PNG, GIF, WebP, AVIF and HEIC images, can read SVG, TIFF and the first page of a PDF, keep -transparency when writing PNG, GIF, WebP and AVIF, and keep the frames of +transparency when writing PNG, GIF, WebP, AVIF and HEIC, and keep the frames of an animated GIF when writing GIF and WebP. A library without any API for a format, such as GD built without AVIF support, is reported as skipped. Any other failure, or a format which no library can read or write, fails diff --git a/tests/image-formats.php b/tests/image-formats.php index c0f65a7..8901fb9 100755 --- a/tests/image-formats.php +++ b/tests/image-formats.php @@ -25,7 +25,7 @@ const WRITABLE_FORMATS = ['jpeg', 'png', 'gif', 'webp', 'avif', 'heic']; const READ_ONLY_FORMATS = ['svg', 'tiff', 'pdf']; -const ALPHA_FORMATS = ['png', 'gif', 'webp', 'avif']; +const ALPHA_FORMATS = ['png', 'gif', 'webp', 'avif', 'heic']; const ANIMATION_FORMATS = ['gif', 'webp']; // Quadrant colours of the test pattern: top left, top right, bottom left, bottom right From a6151570261759dd7d01c9abdeccc592f8464067 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 7 Oct 2026 13:06:14 +0200 Subject: [PATCH 3/7] Add AV1 encoding library --- root-files/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/root-files/build.sh b/root-files/build.sh index e0ed7c4..0283458 100755 --- a/root-files/build.sh +++ b/root-files/build.sh @@ -88,6 +88,7 @@ build_get_runtime_packages() { libbz2-1.0 libncurses6 libsqlite3-0 + libheif-plugin-aomenc " echo $packages From 400c2a6c5b059f341d5bc4495ebefc6ebfb5bb17 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 7 Oct 2026 12:52:37 +0200 Subject: [PATCH 4/7] Enable AV1 for GD --- root-files/build.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/root-files/build.sh b/root-files/build.sh index 0283458..6eec504 100755 --- a/root-files/build.sh +++ b/root-files/build.sh @@ -47,6 +47,7 @@ build_get_build_packages() { re2c unzip + libavif-dev libcurl4-openssl-dev libfreetype-dev libgmp-dev @@ -77,6 +78,7 @@ build_get_build_packages() { # build_get_runtime_packages() { local packages=" + libavif16 libcurl4t64 libicu76 libonig5 @@ -162,6 +164,7 @@ build_compile_php() { --enable-pcntl \ --enable-soap \ --enable-sockets \ + --with-avif \ --with-curl \ --with-freetype \ --with-gmp \ From 583750859148888fe66a9ed9c18b7742c295e5c2 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 7 Oct 2026 12:53:14 +0200 Subject: [PATCH 5/7] Add h265 encoding library --- root-files/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/root-files/build.sh b/root-files/build.sh index 6eec504..9e329af 100755 --- a/root-files/build.sh +++ b/root-files/build.sh @@ -91,6 +91,7 @@ build_get_runtime_packages() { libncurses6 libsqlite3-0 libheif-plugin-aomenc + libheif-plugin-x265 " echo $packages From 8e9b816b3c285aee5498fbbfdba8aad7bd6741c1 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 7 Oct 2026 14:19:39 +0200 Subject: [PATCH 6/7] Add SVG support to Imagick via extra coders Install libmagickcore-7.q16-10-extra as a runtime package of the imagick extension. The package provides the SVG coder, so Imagick can now read SVG files. Debian builds that coder without librsvg, so rendering is done by ImageMagick's internal renderer. The README is updated to reflect this. --- README.md | 4 ++-- .../opt/flownative/php/build/extensions/imagick/imagick.sh | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 240e0b2..ffa7785 100644 --- a/README.md +++ b/README.md @@ -174,8 +174,8 @@ As of October 2026 the test reports these gaps: `libheif-plugin-aomenc` as a runtime package of the imagick or vips extension gives both an AV1 encoder. GD would additionally need `libavif-dev` at build time and `--with-avif` in the configure call. -- Imagick cannot read SVG. ImageMagick delegates SVG rendering to an - external program which is not installed. libvips renders SVG through +- Imagick reads SVG through the coder from `libmagickcore-7.q16-10-extra`, + which Debian builds without librsvg. libvips renders SVG through librsvg. - Imagick cannot read PDF because Ghostscript is not installed, although the ImageMagick policy explicitly allows the PDF coder. libvips renders diff --git a/root-files/opt/flownative/php/build/extensions/imagick/imagick.sh b/root-files/opt/flownative/php/build/extensions/imagick/imagick.sh index fea483d..f236671 100644 --- a/root-files/opt/flownative/php/build/extensions/imagick/imagick.sh +++ b/root-files/opt/flownative/php/build/extensions/imagick/imagick.sh @@ -32,6 +32,7 @@ extensions_imagick_build_packages() { extensions_imagick_runtime_packages() { local packages=" libmagickwand-7.q16 + libmagickcore-7.q16-10-extra " echo $packages } From 62328d27e4c5ad4bb420b488b67775378cdf9fe8 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 7 Oct 2026 14:29:17 +0200 Subject: [PATCH 7/7] Skip Imagick PDF test without Ghostscript ImageMagick hands PDF rendering to Ghostscript, which this image does not ship. The check therefore always failed, although the missing delegate is intentional: images built on top of this one, such as docker-beach-php, install Ghostscript when they need it. The Imagick PDF check now looks for a gs binary in the PATH and is reported as skipped if none is found. The README explains this. --- README.md | 6 ++++-- tests/image-formats.php | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ffa7785..e10383d 100644 --- a/README.md +++ b/README.md @@ -178,8 +178,10 @@ As of October 2026 the test reports these gaps: which Debian builds without librsvg. libvips renders SVG through librsvg. - Imagick cannot read PDF because Ghostscript is not installed, although - the ImageMagick policy explicitly allows the PDF coder. libvips renders - PDF pages through poppler. + the ImageMagick policy explicitly allows the PDF coder. The check is + reported as skipped in that case; images built on top of this one, such + as docker-beach-php, install Ghostscript. libvips renders PDF pages + through poppler. - GD has no API for SVG, HEIC, TIFF or PDF and reads only the first frame of an animated GIF. Imagick and libvips handle all of these, including writing animated GIF and WebP. diff --git a/tests/image-formats.php b/tests/image-formats.php index 8901fb9..91b27c3 100755 --- a/tests/image-formats.php +++ b/tests/image-formats.php @@ -285,9 +285,11 @@ private function keepAlpha(mixed $image): mixed } /** - * Imagick is never skipped: ImageMagick supports every format checked here, - * so a failure means a missing delegate or policy restriction in the image, - * which is exactly what the check is meant to reveal. + * Imagick is not skipped for a missing coder: ImageMagick supports every + * format checked here, so a failure means a missing delegate or policy + * restriction in the image, which is exactly what the check is meant to + * reveal. The one exception is PDF, which ImageMagick hands to Ghostscript. + * This image ships without it; images built on top install it when needed. */ final class ImagickLibrary implements ImageLibrary { @@ -374,6 +376,9 @@ public function read(string $path, string $format): mixed { $image = new Imagick(); if ($format === 'pdf') { + if (!$this->hasGhostscript()) { + throw new SkippedException('Ghostscript is not installed'); + } // The resolution decides the raster size of a page. At 72 dpi one point is one pixel. $image->setResolution(72, 72); $image->readImage($path . '[0]'); @@ -447,6 +452,16 @@ public function framePixel(mixed $frames, int $frame, int $x, int $y): array $frames->setIteratorIndex($frame); return $this->pixel($frames, $x, $y); } + + private function hasGhostscript(): bool + { + foreach (explode(PATH_SEPARATOR, (string)getenv('PATH')) as $directory) { + if (is_executable($directory . '/gs')) { + return true; + } + } + return false; + } } /**