diff --git a/README.md b/README.md index 3a17f09..e10383d 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,9 @@ 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 -transparency when writing PNG, GIF, WebP and AVIF, and keep the frames of +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, 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 @@ -174,12 +174,14 @@ 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 - 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/root-files/build.sh b/root-files/build.sh index e0ed7c4..9e329af 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 @@ -88,6 +90,8 @@ build_get_runtime_packages() { libbz2-1.0 libncurses6 libsqlite3-0 + libheif-plugin-aomenc + libheif-plugin-x265 " echo $packages @@ -161,6 +165,7 @@ build_compile_php() { --enable-pcntl \ --enable-soap \ --enable-sockets \ + --with-avif \ --with-curl \ --with-freetype \ --with-gmp \ 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 } diff --git a/tests/image-formats.php b/tests/image-formats.php index abefb32..91b27c3 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,9 +23,9 @@ * least one library. */ -const WRITABLE_FORMATS = ['jpeg', 'png', 'gif', 'webp', 'avif']; -const READ_ONLY_FORMATS = ['svg', 'heic', 'tiff', 'pdf']; -const ALPHA_FORMATS = ['png', 'gif', 'webp', 'avif']; +const WRITABLE_FORMATS = ['jpeg', 'png', 'gif', 'webp', 'avif', 'heic']; +const READ_ONLY_FORMATS = ['svg', 'tiff', 'pdf']; +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 @@ -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; + } } /** @@ -478,6 +493,7 @@ final class VipsLibrary implements ImageLibrary 'gif' => ['VipsForeignSaveCgifFile', 'VipsForeignSaveGifFile'], 'webp' => ['VipsForeignSaveWebpFile'], 'avif' => ['VipsForeignSaveHeifFile', 'VipsForeignSaveAvifFile'], + 'heic' => ['VipsForeignSaveHeifFile'], ]; public function name(): string @@ -540,7 +556,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 +788,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 +988,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; }