Hi everyone,
I encountered a crash when generating thumbnails on-demand (page load) for certain images. I investigated the root cause with the help of AI (Claude), which helped trace the exact code path and identify the PHP Imagick extension as the source of the exception. Sharing this in case others hit the same issue … and to ask whether this should be fixed in Kirby core.
The problem
When Kirby’s Imagick darkroom processes an image, strip() iterates over profiles returned by getImageProfiles('*', false) and removes those not in the ['icc', 'icm'] keep-list:
// vendor/getkirby/cms/src/Image/Darkroom/Imagick.php
$profiles = $image->getImageProfiles('*', false);
foreach ($profiles as $profile) {
if (in_array($profile, $options['profiles'] ?? [], true) === false) {
$image->removeImageProfile($profile); // no try/catch
}
}
On certain images (in our case .jpeg files from an iOS app), getImageProfiles('*', false) returns ['8bim', 'exif', 'iptc']. Removing 8bim and exif succeeds, but removing iptc throws:
ImagickException: The image profile does not exist
Root cause
The exception comes from the PHP Imagick extension (imagick_class.c), not from the ImageMagick library itself:
profile = MagickRemoveImageProfile(intern->magick_wand, name, &profile_len);
if (!profile) {
php_imagick_throw_exception(IMAGICK_CLASS, "The image profile does not exist");
}
The PHP extension unconditionally throws whenever MagickRemoveImageProfile returns NULL — i.e. whenever the named profile does not exist in the image’s internal splay tree.
In JPEG images that carry Photoshop metadata, IPTC data is embedded inside the 8BIM block (this is standard — 8BIM is the Photoshop wrapper for IPTC in JPEGs). When getImageProfiles lists 'iptc' as a profile name, it may be because the library parsed a reference to it — but if the IPTC data was never extracted as a standalone splay-tree entry, calling removeImageProfile('iptc') finds nothing and the PHP extension throws.
This appears as an ImageMagick 6 vs 7 issue in practice:
- ImageMagick 7 more aggressively extracts IPTC from the 8BIM block on image load, giving it a standalone entry →
removeImageProfile('iptc')succeeds - ImageMagick 6 may leave IPTC embedded inside 8BIM →
getImageProfilesstill lists'iptc', but it has no standalone entry →removeImageProfile('iptc')throws
The PHP Imagick extension behaviour (throw instead of return false) has never changed across versions — this is the same code in current master.
Reproducible directly on ImageMagick 6:
$img = new Imagick('affected-image.jpeg');
// getImageProfiles returns ['8bim', 'exif', 'iptc']
$img->removeImageProfile('8bim'); // OK
$img->removeImageProfile('exif'); // OK
$img->removeImageProfile('iptc'); // ImagickException: The image profile does not exist
Environment
| Local | Production | |
|---|---|---|
| Kirby | 5.4.3 | 5.4.3 |
| PHP Imagick extension | 3.8.1 | 3.8.1 |
| ImageMagick | 7.1.2-21 | 6.9.11-60 |
Related references
- ImageMagick forum 2011 — same scenario with IPTC embedded in 8BIM: https://www.imagemagick.org/discourse-server/viewtopic.php?t=19634
- ImageMagick GitHub issue #7040 — 8BIM/IPTC parsing confusion in APP13 segment: Parsing IPTC data in JPEG's APP13 segment - possible 8BIM collision · Issue #7040 · ImageMagick/ImageMagick · GitHub
- PHP Imagick extension source — the exact throw site: imagick/imagick_class.c at master · Imagick/imagick · GitHub (line ~5330)
The open question
Should Kirby\Image\Darkroom\Imagick::strip() guard against this? Something like:
foreach ($profiles as $profile) {
if (in_array($profile, $options['profiles'] ?? [], true) === false) {
try {
$image->removeImageProfile($profile);
} catch (\ImagickException $e) {
if (str_contains($e->getMessage(), 'image profile does not exist')) {
continue; // profile listed but not a standalone entry — safe to skip
}
throw $e;
}
}
}
This would make Kirby resilient to both ImageMagick 6 behaviour and any future edge cases where a profile is listed but not independently removable.
I also noticed Kirby issue #8066 which touches on imagick profile handling but not sure if this is related.
Workaround
Until this is addressed in core, a small plugin fixes it by overriding strip() in a SafeImagick subclass:
site/plugins/imagick-fix/index.php
<?php
use Kirby\Image\Darkroom;
use Kirby\Image\Darkroom\Imagick;
/**
* On ImageMagick 6, removeImageProfile() throws ImagickException when the
* profile is listed by getImageProfiles() but its data is embedded inside
* another profile (e.g. iptc inside 8bim). Override strip() to catch and
* skip these, so save() is still called and the thumbnail is generated.
*/
class SafeImagick extends Imagick
{
protected function strip(\Imagick $image, array $options): \Imagick
{
$profiles = $image->getImageProfiles('*', false);
foreach ($profiles as $profile) {
if (in_array($profile, $options['profiles'] ?? [], true) === false) {
try {
$image->removeImageProfile($profile);
} catch (\ImagickException $e) {
if (!str_contains($e->getMessage(), 'image profile does not exist')) {
throw $e;
}
}
}
}
$properties = $image->getImageProperties('*', false);
foreach ($properties as $property) {
$image->deleteImageProperty($property);
}
return $image;
}
}
Darkroom::$types['imagick'] = SafeImagick::class;
Key points:
- Overrides
strip()directly rather thanprocess(), sosave()is still called and the thumbnail is actually generated - The
Darkroom::$typesregistry is the intended extension point → no core files modified - The catch is deliberately narrow: only the specific message is silenced, all other
ImagickExceptionerrors are re-thrown
Hope this helps someone. Happy to open a GitHub issue if the Kirby team confirms this should be fixed upstream.