Color Pipeline
How OpenGPEX manages color spaces across the entire image lifecycle — from import through compositing to export — using a centralized strategy matrix that ensures zero color drift and full ICC profile round-trip fidelity.
Architecture: Centralized Strategy Routing
All color-space decisions are concentrated in a single ColorPipeline.ts strategy matrix. Format handlers, compositors, and renderers query this matrix and execute instructions — they never make autonomous color decisions.
Source File → [FORMAT_COLOR_STRATEGY] → detectedCS → [IMPORT_PIPELINE] → Frame.colorSpace
│
┌─────────┴─────────┐
▼ ▼
[WORKING_PIPELINE] sourceBlob?
composite/display │
/export ▼
canUseFastExport()
YES → direct return
NO → strategy encode
Core Design Principles
- Single source of truth — Modify one strategy entry to change behavior globally
- Only two runtime values —
Frame.colorSpaceis always'srgb'or'display-p3' - Handlers are pure executors — They switch/case on strategy instructions, zero if/else judgment
- Renderer-agnostic — Color management is pipeline logic, not tied to Canvas 2D / WebGL / WebGPU
Two-Layer Strategy System
Layer 1: Format Strategy (by file format)
Decides how to read color metadata and whether to retain the original source blob:
| Format | Reads ICC | Supports ICC Embed | Source Blob Retention |
|---|---|---|---|
| PNG | ✅ | ✅ | On fidelity loss |
| JPEG | ✅ | ✅ | On fidelity loss |
| WebP | ✅ | ✅ | On fidelity loss |
| AVIF | ✅ | ✅ | On fidelity loss |
| TIFF | ✅ | ✅ | On fidelity loss |
| RAW | ✅ | ❌ | Always |
| HEIC | ✅ | ❌ (decode only) | Never |
| BMP/GIF/SVG | ❌ | ❌ | Never (fixed sRGB) |
Layer 2: Color Space Strategy (by detected color space)
Decides what happens to pixels at each pipeline stage:
| Source ColorSpace | Frame.colorSpace | Conversion | Notes |
|---|---|---|---|
| sRGB | srgb |
None | Zero-cost passthrough |
| Display P3 | display-p3 |
None | Native wide-gamut |
| Adobe RGB | display-p3 |
3×3 Matrix | P3 covers ~98% of AdobeRGB |
| ProPhoto RGB | display-p3 |
3×3 Matrix | ~30% super-gamut clamped |
| CMYK | srgb |
ICC Engine (vips) | Full Little CMS transform |
| Custom ICC | srgb |
ICC Engine (vips) | sourceBlob preserves original |
Wide-Gamut (Display P3) Support
OpenGPEX natively supports Display P3 as a working color space:
- Import: P3 source files set
Frame.colorSpace = 'display-p3'directly; AdobeRGB/ProPhoto are matrix-converted to P3 at import time (one-shot, ~4ms/1080p) - Composite: Canvas 2D context created with
{ colorSpace: 'display-p3' }— zero per-layer conversion overhead - Display: Auto-detects P3 hardware via
color-gamut: p3media query; falls back to sRGB + matrix clamp on non-P3 displays - Export: P3 pixels encoded directly + ICC Profile embedded (PNG/JPEG/WebP/AVIF/TIFF)
Import-Time Conversion (Choice A)
AdobeRGB/ProPhoto pixels are converted to P3 once during decode(), not at composite time. This means:
- Zero composite overhead — No per-layer, per-frame conversion during editing
- Full export fidelity —
sourceBlobpreserves the original file for lossless round-trip - Simple architecture — Compositing only ever sees sRGB or P3 pixels
Export Color Management
Fast-Path Export
When conditions are met (single layer + unedited + same format), the original sourceBlob is returned directly — preserving original ICC, bit depth, and color encoding with zero computation.
Normal Export Path
Three decisions are made at export time:
| Decision | Function | Purpose |
|---|---|---|
| ICC Embed | shouldEmbedIcc(format, frameCS, userOverride?) |
3-layer decision: format capability × strategy default × user choice |
| Pixel Conversion | resolveExportPixelConversion(frameCS, meta, embedIcc, format?) |
Determines if pixels need transformation |
| Format Downgrade | getExportStrategy(frameCS, targetFormat?) |
Auto-downgrades P3→sRGB for formats that don't support P3 (BMP/GIF) |
ICC Embed Three-Layer Model
Layer 1: Format capability (BMP/GIF → never; PNG/JPEG/WebP/AVIF/TIFF → allowed)
Layer 2: Strategy default (sRGB: true; P3: true — matches Photoshop/Lightroom)
Layer 3: User override (export UI checkbox, if provided)
Color Pipeline Infrastructure
Matrix Conversion Engine
Pure TypeScript 3×3 linear matrix transforms (zero external dependencies):
| Transform | Use Case | Performance (1080p) |
|---|---|---|
| sRGB ↔ P3 | Most common | ~3ms |
| AdobeRGB → P3 | Import | ~3ms |
| ProPhoto → P3 | Import (with Bradford D50→D65) | ~4ms |
TRC (Transfer Curve) Handling
- sRGB ↔ linear conversion via pre-computed LUT (~2ms/4K)
- Linear-light compositing for physically-correct blend modes
- Filter TRC auto-insertion (blur executes in linear, auto-converts back)
ICC Profile Support
| Capability | Technology | Location |
|---|---|---|
| ICC v2/v4 parsing | Pure TypeScript | core/files/icc.ts |
| Full ICC transform | wasm-vips + Little CMS (Worker) | file-io.ts |
| ICC injection (PNG) | iCCP chunk builder | handlers/png/writers.ts |
| ICC injection (JPEG) | APP2 marker injection | handlers/jpeg/jfif.ts |
| ICC injection (WebP) | RIFF ICCP chunk (custom) | handlers/webp/riff.ts |
| ICC injection (TIFF/AVIF) | vips iccTransform | Worker (vips) |
Format × Color Space Matrix (Summary)
| Source | Import Action | Frame.colorSpace | Export Behavior |
|---|---|---|---|
| sRGB anything | Passthrough | srgb |
sRGB direct output |
| P3 anything | Passthrough | display-p3 |
P3 + ICC embed |
| AdobeRGB | Matrix → P3 | display-p3 |
P3 + ICC (or sourceBlob fast-path) |
| ProPhoto | Matrix → P3 | display-p3 |
P3 + ICC (or sourceBlob fast-path) |
| CMYK | ICC → sRGB | srgb |
sRGB + optional ICC restore |
| Custom ICC | ICC → sRGB | srgb |
sRGB + optional ICC restore |
Comparison with Desktop Applications
| Aspect | Photoshop / GIMP | OpenGPEX |
|---|---|---|
| Working space | User-configured (any ICC) | Auto-detected (sRGB or P3) |
| Color engine | Little CMS (compiled in) | Pluggable: BuiltinEngine (matrices) + VipsEngine (Little CMS) |
| Per-pixel tagging | Yes (babl in GIMP) | No — entire Frame shares one space |
| Conversion trigger | Every op declares format → auto-insert | Import-time one-shot + strategy matrix |
| Export ICC | Optional, user-configured | Three-layer auto-decision |
| Wide-gamut preview | Requires calibrated workflow | Auto-detects P3 hardware |
Known Limitations & Future
| Limitation | Cause | Resolution |
|---|---|---|
| ProPhoto ~30% super-gamut clamped | Canvas 2D only supports P3/sRGB | WebGPU (float16 textures) |
| 8-bit canvas precision | OffscreenCanvas limitation | WebGPU (float16/float32) |
| No soft-proofing | No display transform layer yet | Phase D: DisplayStrategy extension |
| No user-initiated space conversion | Frame.colorSpace currently immutable | Planned as undoable command |
Last updated: 2026-08-08