Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions root-files/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ build_get_build_packages() {
re2c
unzip

libavif-dev
libcurl4-openssl-dev
libfreetype-dev
libgmp-dev
Expand Down Expand Up @@ -77,6 +78,7 @@ build_get_build_packages() {
#
build_get_runtime_packages() {
local packages="
libavif16
libcurl4t64
libicu76
libonig5
Expand All @@ -88,6 +90,8 @@ build_get_runtime_packages() {
libbz2-1.0
libncurses6
libsqlite3-0
libheif-plugin-aomenc
libheif-plugin-x265
"

echo $packages
Expand Down Expand Up @@ -161,6 +165,7 @@ build_compile_php() {
--enable-pcntl \
--enable-soap \
--enable-sockets \
--with-avif \
--with-curl \
--with-freetype \
--with-gmp \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ extensions_imagick_build_packages() {
extensions_imagick_runtime_packages() {
local packages="
libmagickwand-7.q16
libmagickcore-7.q16-10-extra
"
echo $packages
}
Expand Down
37 changes: 28 additions & 9 deletions tests/image-formats.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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]');
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -478,6 +493,7 @@ final class VipsLibrary implements ImageLibrary
'gif' => ['VipsForeignSaveCgifFile', 'VipsForeignSaveGifFile'],
'webp' => ['VipsForeignSaveWebpFile'],
'avif' => ['VipsForeignSaveHeifFile', 'VipsForeignSaveAvifFile'],
'heic' => ['VipsForeignSaveHeifFile'],
];

public function name(): string
Expand Down Expand Up @@ -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))) {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;
}

Expand Down
Loading