Skip to content

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

  • Configimage.color (internal/config: ImageConfig.Color, string), validated by ImageConfig.Validate, env-overridable as IIIRIS_IMAGE_COLOR. Values: "" (unset, derive from image.metadata), "srgb", "respect-icc". Any other value is a startup error. Full table in docs/configuration.md.
  • Derivation accessorImageConfig.ColorPolicy() string (config.ColorSRGB / config.ColorRespectICC) is the single place that resolves an unset Color field against Metadata. Every consumer (pipeline wiring, cache-salt wiring) calls this instead of re-deriving the rule.
  • Pipeline optionsimage.Options.ColorPolicy (string, colorSRGB / colorRespectICC; empty derives from Options.StripMetadata, mirroring the config-layer rule so a directly-constructed Pipeline behaves the same as one wired from config). Internal helpers normalizeColor (srgb path) and normalizeColorRespectICC (respect-icc path) in internal/image/color.go.
  • Cache saltauth.RenderQualitySalt(sharpen, sigma, x1, supersample, colorPolicy string) string takes the effective colour policy as its fifth argument. See render-quality.md and auth.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: strip is byte-identical to pre-feature output. This is the default combination and it renders exactly the pixels iiiris rendered before colour policy existed.
  • respect-icc leaves an RGB or grayscale source's embedded ICC profile — and its pixels — untouched. Only CMYK pixels are transformed under respect-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: strip removes EXIF/XMP/IPTC but is orthogonal to colour handling; combined with color: respect-icc the source's wide-gamut profile (ProPhoto, AdobeRGB, Display-P3, etc.) is preserved exactly while GPS/camera/serial metadata is dropped.
  • quality=gray and quality=bitonal are unaffected. Those are IIIF client-requested output qualities, not properties of the source, and are orthogonal to image.color.
  • The effective colour policy folds into the render-quality cache salt (auth.RenderQualitySalt). srgb contributes nothing — the salt fragment is byte-identical to the pre-colour-policy salt, so a deployment already running srgb (the default, or an explicit setting) rekeys nothing on upgrade. respect-icc appends ;co=respect-icc to the salt fragment before hashing, making the salt non-empty (and non-colliding with a sharpen/supersample-only salt) whenever the effective policy is respect-icc — including a preserve-mode deployment that never touched image.color explicitly. See render-quality.md and auth.md "Cache contract".
  • Colour policy is restart-only. Resolved once at Pipeline construction 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/bitonal and 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: srgb unconditionally (dropping the derivation table). The cleaner mental model, but it would silently convert preserve-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.metadata as 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 srgb and passthrough are offered; no third target profile.

Test coverage

  • internal/image/color_test.go and internal/image/color_policy_test.go — the four cells of the effective-policy matrix, plus the preserve-mode CMYK overlay path (the pre-overlay CMYK normalization keys off the colour policy, not stripMetadata, so a preserve-mode CMYK source composited with an overlay does not take a muddy-watermark path).
  • internal/configimage.color validation (unsupported values are a startup error) and the derivation table.
  • internal/auth/cachekey_test.goRenderQualitySalt's respect-icc fragment, its append order relative to sh=/sg=/x1=/ss=, and that srgb reproduces the pre-colour-policy salt exactly.