Skip to content

ImageSizerEngineIMagick desaturates wide-gamut (Display P3 / Adobe RGB) images because it strips the ICC profile without converting to sRGB #2265

@elabx

Description

@elabx

Short description

When a source image carries a wide-gamut ICC profile (Display P3 from Apple devices, Adobe RGB from editing tools), the generated variations look noticeably duller / desaturated than the original.

The cause is in ImageSizerEngineIMagick: it removes the embedded ICC profile but never converts the pixels to sRGB. The wide-gamut pixel values are then interpreted by browsers as sRGB, which collapses the saturation. (The GD engine has the same end result, since it also can't colour-manage.)

Steps to reproduce

  1. Take a JPEG that has an embedded Display P3 or Adobe RGB profile (e.g. straight from an iPhone).
  2. Add it to an image field and let ProcessWire generate a variation (any ->size() / ->width() call), using the ImageMagick engine.
  3. Compare the variation to the original in a colour-managed browser.

Expected: the variation looks like the original (colours preserved).
Actual: the variation is visibly desaturated/dull.

Evidence

Original vs. stripped variation (ImageMagick identify):

ORIGINAL:    Colorspace: sRGB   Profile-icc: 548 bytes   icc:description: Display P3
VARIATION:   Colorspace: sRGB   (no Profile-icc)         icc:description: (none)

Mean HSL-saturation of the sRGB-encoded pixels for one test image (spa_service.jpg, Display P3):

version saturation
strip profile only (current behaviour) 0.420
convert P3→sRGB, then strip (proposed) 0.485–0.491

The numbers are kept ~identical by the resize, but dropping the "this is P3" tag is what desaturates the result. Converting to sRGB first bakes the wide-gamut colours into sRGB-space pixel values, so they survive the profile being stripped.

Root cause

In wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module, the metadata-stripping loop removes the icc profile (the "keep embedded icc profiles" line is commented out), and setColorspace() only relabels the colorspace — it does not remap pixels. So a Display P3 image loses its profile but keeps P3-encoded pixels → shown as sRGB → dull.

Proposed fix (opt-in, LCMS-aware)

Before the strip loop, if the image has an embedded ICC profile and the site has opted in, convert to sRGB with profileImage() (which performs a proper perceptual transform while the source profile is still attached). The now-sRGB profile is then stripped by the existing loop.

Design choices, following the discussion in #1055 (where collaborators suggested metadata behaviour should be optional):

  • Off by default, enabled with $config->imageSizerOptions['sRGB'] = true;. Unknown option keys already flow into $this->options via ImageSizerEngine::setOptions(), so no base-class plumbing is needed.
  • LCMS-aware. ICC transforms require ImageMagick built with the Little CMS delegate. Per the php.net note on Imagick::profileImage() (comment #114781), without LCMS the call may either throw or silently do nothing. The patch wraps it in try/catch, logs once, and disables itself for the rest of the request on failure — so on a server without LCMS it is a safe no-op (output identical to today, never broken).
// in processResize(), immediately before the "remove not wanted / needed Metadata" loop:
if($this->hasICC && !empty($this->options['sRGB'])) {
    static $srgbBlob = null;   // loaded once per process
    static $disabled = false;  // set if the transform proves unavailable
    if($srgbBlob === null) {
        $srgbProfilePath = __DIR__ . '/sRGB.icc';
        $srgbBlob = is_file($srgbProfilePath) ? file_get_contents($srgbProfilePath) : '';
        if($srgbBlob === '') $disabled = true;
    }
    if(!$disabled) {
        try {
            $this->im->setImageRenderingIntent(\Imagick::RENDERINGINTENT_PERCEPTUAL);
            $this->im->profileImage('icc', $srgbBlob);
        } catch(\Exception $e) {
            $disabled = true;
            $this->wire('log')->save('imagesizer',
                'ImageSizerEngineIMagick: sRGB conversion unavailable (LCMS delegate missing?) - ' . $e->getMessage());
        }
    }
}

Bundled sRGB profile + licensing

The transform needs a target sRGB profile, and PHP's Imagick can't generate one at runtime (lcms2's cmsCreate_sRGBProfile() isn't exposed). So a small sRGB.icc would ship beside the module. To avoid any licensing question, a CC0 / public-domain profile is appropriate — e.g. sRGB-elle-V2-srgbtrc.icc from Elle Stone's elles_icc_profiles (CC0), or the ICC's own freely-redistributable sRGB2014 profile from color.org.

Notes

  • The GD engine has the same visual outcome and can't be fixed the same way (no colour management); documenting "use ImageMagick + enable sRGB for wide-gamut sources" would be a reasonable stance.
  • Tested on ProcessWire 3.0.256, PHP 8.4, ImageMagick 7.1.1-43 Q16 (with LCMS).

Environment

  • ProcessWire: 3.0.256
  • PHP: 8.4
  • ImageMagick (Imagick): 7.1.1-43 Q16, LCMS delegate present

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions