ImagickException "image profile does not exist" when regenerating thumbnails

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 → getImageProfiles still 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

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 than process(), so save() is still called and the thumbnail is actually generated
  • The Darkroom::$types registry is the intended extension point → no core files modified
  • The catch is deliberately narrow: only the specific message is silenced, all other ImagickException errors are re-thrown

Hope this helps someone. Happy to open a GitHub issue if the Kirby team confirms this should be fixed upstream.