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
- Take a JPEG that has an embedded Display P3 or Adobe RGB profile (e.g. straight from an iPhone).
- Add it to an image field and let ProcessWire generate a variation (any
->size() / ->width() call), using the ImageMagick engine.
- 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
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
->size()/->width()call), using the ImageMagick engine.Expected: the variation looks like the original (colours preserved).
Actual: the variation is visibly desaturated/dull.
Evidence
Original vs. stripped variation (ImageMagick
identify):Mean HSL-saturation of the sRGB-encoded pixels for one test image (
spa_service.jpg, Display P3):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 theiccprofile (the "keep embedded icc profiles" line is commented out), andsetColorspace()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):
$config->imageSizerOptions['sRGB'] = true;. Unknown option keys already flow into$this->optionsviaImageSizerEngine::setOptions(), so no base-class plumbing is needed.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).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 smallsRGB.iccwould ship beside the module. To avoid any licensing question, a CC0 / public-domain profile is appropriate — e.g.sRGB-elle-V2-srgbtrc.iccfrom Elle Stone'selles_icc_profiles(CC0), or the ICC's own freely-redistributable sRGB2014 profile from color.org.Notes
sRGBfor wide-gamut sources" would be a reasonable stance.Environment