Colour policy: no derivative is ever delivered in CMYK¶
The contract for iiiris's colour-space handling in derivatives —
independent of, but derived by default from, the metadata-privacy
policy. This spec replaces docs/briefs/COLOR_POLICY.md now that
the decoupling has shipped.
The operator-facing reference (YAML shape, env var, worked
examples) lives in ../configuration.md
"Colour policy". This spec captures what the implementation
guarantees; that file captures how to configure it.
What it does¶
Every derivative's colour space is normalized by one of two
policies before encode. srgb converts everything — including
wide-gamut RGB — to sRGB. respect-icc leaves an RGB source's
embedded profile untouched but still converts CMYK, because no
mainstream browser renders a CMYK JPEG correctly. The policy is
independent of image.metadata (which governs EXIF/XMP/IPTC and
orientation), so an operator can strip identifying metadata while
still passing through a wide-gamut colour profile — a combination
that was inexpressible before this shipped, because colour
normalization used to run only in metadata: strip mode.
Surface¶
- Config —
image.color(internal/config:ImageConfig.Color, string), validated byImageConfig.Validate, env-overridable asIIIRIS_IMAGE_COLOR. Values:""(unset, derive fromimage.metadata),"srgb","respect-icc". Any other value is a startup error. Full table indocs/configuration.md. - Derivation accessor —
ImageConfig.ColorPolicy() string(config.ColorSRGB/config.ColorRespectICC) is the single place that resolves an unsetColorfield againstMetadata. Every consumer (pipeline wiring, cache-salt wiring) calls this instead of re-deriving the rule. - Pipeline options —
image.Options.ColorPolicy(string,colorSRGB/colorRespectICC; empty derives fromOptions.StripMetadata, mirroring the config-layer rule so a directly-constructedPipelinebehaves the same as one wired from config). Internal helpersnormalizeColor(srgb path) andnormalizeColorRespectICC(respect-icc path) ininternal/image/color.go. - Cache salt —
auth.RenderQualitySalt(sharpen, sigma, x1, supersample, colorPolicy string) stringtakes the effective colour policy as its fifth argument. Seerender-quality.mdandauth.md"Cache contract" for the key shape this feeds.
Config surface¶
image:
metadata: strip # strip | preserve (EXIF/XMP/IPTC policy; unchanged)
color: # srgb | respect-icc; unset = derive from metadata
Derivation (unset color)¶
image.metadata |
effective color |
Result |
|---|---|---|
strip (default) |
srgb |
Today's behaviour, unchanged |
preserve |
respect-icc |
Today's behaviour, except CMYK now converts |
Setting color explicitly overrides the derivation in either
direction — including metadata: strip + color: respect-icc, the
combination that motivated the decoupling: strip GPS/EXIF, keep a
wide-gamut profile.
Effective-policy matrix¶
metadata: strip |
metadata: preserve |
|
|---|---|---|
color: srgb |
sRGB output, no EXIF/XMP/IPTC | sRGB output, EXIF/XMP/IPTC kept |
color: respect-icc |
source RGB profile kept, no EXIF/XMP/IPTC (headline new combination) | source RGB profile kept, EXIF/XMP/IPTC kept |
In every cell, a CMYK source is converted to sRGB and a grayscale source stays sGray.
Contracts¶
- CMYK is always converted to sRGB, in every mode. Not configurable, not overridable. A CMYK JPEG is not reliably renderable in a browser; this is the one non-negotiable step in either policy.
srgb+metadata: stripis byte-identical to pre-feature output. This is the default combination and it renders exactly the pixels iiiris rendered before colour policy existed.respect-iccleaves an RGB or grayscale source's embedded ICC profile — and its pixels — untouched. Only CMYK pixels are transformed underrespect-icc; everything else passes through verbatim, profile included.- Grayscale sources are never promoted to RGB, in either policy. A 1-band scan stays 1-band; promoting it would triple served bytes for no visual gain.
- An embedded ICC profile survives a metadata strip byte-identical.
metadata: stripremoves EXIF/XMP/IPTC but is orthogonal to colour handling; combined withcolor: respect-iccthe source's wide-gamut profile (ProPhoto, AdobeRGB, Display-P3, etc.) is preserved exactly while GPS/camera/serial metadata is dropped. quality=grayandquality=bitonalare unaffected. Those are IIIF client-requested output qualities, not properties of the source, and are orthogonal toimage.color.- The effective colour policy folds into the render-quality cache
salt (
auth.RenderQualitySalt).srgbcontributes nothing — the salt fragment is byte-identical to the pre-colour-policy salt, so a deployment already runningsrgb(the default, or an explicit setting) rekeys nothing on upgrade.respect-iccappends;co=respect-iccto the salt fragment before hashing, making the salt non-empty (and non-colliding with a sharpen/supersample-only salt) whenever the effective policy isrespect-icc— including apreserve-mode deployment that never touchedimage.colorexplicitly. Seerender-quality.mdandauth.md"Cache contract". - Colour policy is restart-only. Resolved once at
Pipelineconstruction and folded into the cache salt at wiring time (server.Deps.RenderQuality); not hot-reloadable, not exposed in the admin config editor's live fields. - Changing the effective colour policy rekeys the render cache; it
never serves stale renders. Because the salt is keyed off the
effective policy (not the raw config value), a
preserve-mode deployment upgrading onto this feature rekeys automatically — the stale CMYK-passthrough entries a pre-upgrade cache holds are abandoned and re-rendered correctly under the new salt. No cache flush is required.
Out of scope¶
- Literal always-3-band RGB output. Would break IIIF-required
quality=gray/bitonaland triple the size of grayscale scans for no visual benefit. - Promoting grayscale sources to RGB. Same cost, no benefit; the sGray path exists precisely to avoid it.
- Defaulting every deployment to
color: srgbunconditionally (dropping the derivation table). The cleaner mental model, but it would silently convertpreserve-mode deployments' wide-gamut output on upgrade — declined in favour of metadata-derived defaults that preserve every existing deployment's behaviour except the CMYK fix. - Folding colour into
image.metadataas a third value. The two concerns are orthogonal (privacy vs. colour fidelity); a combined enum can't express "preserve EXIF, force sRGB" or "strip EXIF, keep ICC" without an awkward fourth/fifth value. - Output ICC profile selection (e.g. converting to Display-P3).
Only
srgband passthrough are offered; no third target profile.
Test coverage¶
internal/image/color_test.goandinternal/image/color_policy_test.go— the four cells of the effective-policy matrix, plus thepreserve-mode CMYK overlay path (the pre-overlay CMYK normalization keys off the colour policy, notstripMetadata, so apreserve-mode CMYK source composited with an overlay does not take a muddy-watermark path).internal/config—image.colorvalidation (unsupported values are a startup error) and the derivation table.internal/auth/cachekey_test.go—RenderQualitySalt'srespect-iccfragment, its append order relative tosh=/sg=/x1=/ss=, and thatsrgbreproduces the pre-colour-policy salt exactly.