jsColorEngine samples

CMYK → RGB via LUT — emulation mode

Real-world use case: your app has CMYK images (proofs, prepress previews, archival scans) and needs to display them as RGB on screen. In production you don’t want to ship ICC profiles or run a full ICC pipeline per pixel. You want one pre-baked LUT and a fast kernel.

This page builds two CMYK→RGB LUTs at load time, serialises them to JSON (the portable handshake format), then rebuilds Transforms from the JSON and converts a CMYK master image three ways. The jsCE LUT column shows what production looks like — no ICC profile loaded, just a JSON file and the engine. The lcms LUT column shows emulation: jsCE’s WASM-SIMD kernels with lcms’s colour math sampled into the grid.

Important: Once you build and save the LUT from Lcms or jsCE, you no longer need to load lcms or the profile, only the LUT and jsCE’s Transform.fromJSON() to load it.

Profile: GRACoL2006_Coated1v2.icc · Intent: relative colorimetric + BPC (lcms’s perceptual disables BPC) · Grid: 174 (4D) · Source: view source · API guide: lutbuilder.md

What you should see in practice: the two LUTs (jsCE-built and lcms-built) agree on raw u16 grid data to ~0.1 ΔP per channel — effectively the same colour math. Pixel ΔP between live (no LUT) and the LUT path is < 1 on average, with occasional pixels at K-generation transitions or gamut boundaries where 17-pt grid interpolation diverges from the f64 pipeline. jsCE LUT and lcms LUT are interchangeable as runtime artefacts.

LUT build — one-time, at page load

Idle.

jsCE LUT
build (engine + buildLut)
jsCE JSON size
lcms LUT
build (sample lcms grid)
lcms JSON size
fromJSON cost
parse + setLut + intLut
LUT delta (jsCE vs lcms)
u16 grid bytes, mean ΔP

CMYK image → three RGB conversions

(a) jsCE live transform

CMYK→RGB through the full ICC pipeline. Ground truth, built at runtime (just now). No LUT.

(b) jsCE LUT — from JSON

No ICC profile at runtime. JSON parsed, setLut, kernel. Production deployment path: ship JSON, no profiles.

(c) lcms LUT — emulation

lcms colour math, jsCE kernel speed. If you need exact LittleCMS output at WASM-SIMD speed, this is the path.
pairmean ΔPp95 ΔPmax ΔPnote
Run a conversion to populate.

ΔP = per-pixel Euclidean distance on RGB in 0–255 scale (per-channel difference, not perceptual ΔE). ΔP < 1 means “sub-LSB at 8-bit, indistinguishable on screen”. Mean and p95 are the meaningful numbers — the typical pixel. Max is usually a single pixel at a profile “kink”: K-generation transitions, gamut-boundary crossings, or TAC clamp regions where the CMYK profile is non-smooth and 17-pt linear interpolation diverges from the f64 curve. Going to a 33-pt grid (4× the cells, 4× the JSON size) shrinks the max substantially. For most production work the 17-pt budget is fine.

What’s actually in the JSON?

The portable handshake format. CLUT bytes are elided ("<data>") so you can see the chain, grid shape, and signature. This is what JSON.stringify(transform) produces and what Transform.fromJSON() consumes.

Build LUTs first.