Builtin::from_profile_name matches only a built-in's own description. Builtin::stand_in_for adds the FOGRA39 approximations (Coated FOGRA39, ISO Coated v2) for documents that name a profile without carrying its bytes. ISO Coated v2 300% (ECI) has no stand-in: the built-in separates to up to 330 % total ink. The README example parses the document's ICC bytes first and handles a missing stand-in explicitly.
11 KiB
Craft CMS
craft-cms is a pure-Rust ICC colour management module for the Craft suite: profile parsing
and writing, transforms between profiles, rendering intents, black point compensation, soft
proofing, gamut checks, 3D LUTs and a set of built-in profiles. No C, no unsafe, and no
dependency on other craft-libs crates. Implemented from the public ICC.1:2010 (v4.3) and
ICC.1:2001-04 (v2) specifications, CIE 15, and Adobe's published black point compensation paper.
Extracted from PhotoCraft's crates/cms at
b3222ee
(refs #36).
API
use craft_cms::{Builtin, Intent, Profile, Transform};
let src = Profile::parse(&icc_bytes)?; // v2/v4: matrix/TRC, gray TRC, mft1/mft2/mAB/mBA
let dst = Builtin::CoatedCmyk.profile();
let t = Transform::new(&src, dst, Intent::RelativeColorimetric, /*bpc*/ true)?;
t.convert_u8(&rgba8, 4, &mut cmyka8, 5, /*copy alpha*/ true)?; // also convert_u16 / convert_f32 / apply (in place)
let mut out = [0.0; 4];
t.eval(&[1.0, 0.0, 0.0], &mut out)?; // exact single colour
Calls never panic on caller input: strides below the channel count, buffers that are not
whole pixels, a destination with fewer pixels than the source, slices shorter than the
channel count and out-of-range table or LUT dimensions return CmsError::InvalidArgument.
Buffers are checked once per call, before any pixel is written.
Transform::proof(src, proof, display, intent, bpc, simulate_paper): soft-proof chain.GamutCheck::new(src, proof, ΔE): out-of-gamut test (gamut warning).Lut3d::from_transform(&t, 33)?.to_rgba16f_bytes(): display transform as a 3D texture.transform::cached(src, dst, opts): process-wide transform cache.lutfile::parse(name, bytes)/lutfile::write_cube(&lut):.cube,.3dland.lookLUT files;lutfile::builtin(id)returns generated looks (lutfile::BUILTIN).Profile::to_bytes(): original bytes of parsed profiles (byte-exact), or an ICC v4.3 encoding.Builtin::from_profile_name(desc): the built-in with that description (exact match);Builtin::stand_in_for(desc): the built-in that approximates a profile a document names when its ICC bytes are not available (see Selecting by profile name).Profile::same_colors(&other): the same colour space whatever the encoding (every sample of a device grid within ΔE 0.5 through both), e.g. Photoshop's sRGB IEC61966-2.1 and the built-in sRGB.content_hash()compares bytes and suits cache keys only.
Evaluation
- Exact path: a float stage pipeline (curves, matrices, CLUTs, Lab↔XYZ, PCS encodings, BPC /
absolute scaling). Float buffers use it by default (
TransformOptions::precise_float). - Integer buffers: matrix/TRC chains use per-code input tables + matrix + output tables indexed
by
x^(1/4)(accurate near black even for pure gamma curves); chains containing a CLUT are sampled into a device link (33³ for 3 inputs, 17⁴ for 4, 4096 for 1) evaluated with tetrahedral interpolation (4D: linear on the first input between two tetrahedra); purely analytic chains (e.g. Lab ↔ RGB) are evaluated exactly. Buffers ≥ 16k pixels run on rayon (native only). - Rendering intents: AToB0/1/2 and BToA0/1/2 by intent with AToB0/BToA0 fallback; absolute
colorimetric scales by media white (
wtpt; v2 display profiles treated as D50; a zero, negative or non-finite white is refused withCmsError::Invalid); BPC uses neutral black points (Y only) mapped linearly in XYZ. - White: except in absolute colorimetric (and soft proofs that simulate paper), the source's
device white comes out as exactly the destination's (no ink, or 1.0) on every path. LUT
profiles rarely put white on a grid node, so the device link's white node is set to it and
the exact and float paths compare each pixel with white; all other colours are left as the
profiles give them. With a profile whose white falls between its nodes (
coated-cmyk: 0.4 % C), the last step to white is that much larger than its neighbours.
Built-in profiles (all CC0-1.0, generated by this crate)
The profile descriptions and copyright text keep the PhotoCraft names they were first published with, so profiles embedded in existing files still match byte for byte.
| id | description |
|---|---|
srgb |
sRGB IEC61966-2.1 (Rec. 709 primaries, D65 Bradford-adapted, sRGB TRC) |
display-p3 |
Display P3 |
adobe-rgb-compat |
Adobe RGB (1998)-compatible (published primaries, gamma 563/256) |
prophoto-compat |
ProPhoto-compatible (ROMM primaries, D50, gamma 1.8) |
linear-srgb |
sRGB primaries, linear |
rec2020 |
Rec. 2020 primaries, Rec. 709 OETF |
gray-gamma-2.2, sgray |
gray gamma 2.2; gray with the sRGB curve (default gray working space) |
lab-d50 |
Lab identity (v4 encoding) |
coated-cmyk |
Photocraft Coated CMYK (synthetic, 300% TAC, medium GCR): default CMYK |
coated-fogra39 |
Coated FOGRA39 (fitted, craft-cms): fitted to Fogra's FOGRA39 data, 330% TAC, medium GCR |
The CMYK profiles
Adobe's CMYK profiles (U.S. Web Coated SWOP, Coated FOGRA39) are proprietary, and the freely
downloadable characterisation-based profiles (ECI/FOGRA, colord, Ghostscript) carry
redistribution conditions or unclear licences, so this crate generates its own. Both share the
separation code in src/synth.rs: GCR black generation (starts at 20 % gray component, K max
95 %), a bounded least-squares CMY solve under the total ink limit, and perceptual tables with
L* black-point scaling and soft chroma compression against a sampled gamut boundary.
coated-cmyk (profiles/photocraft-coated-cmyk.icc, 237 KB, AToB 11⁴ lut16, BToA 21³
lut16) comes from a Yule–Nielsen modified Neugebauer model (n = 2) over the 16 overprints with
ISO 12647-2 coated-paper Lab aims for paper/C/M/Y/RGB overprints, parabolic dot gain (14 % CMY,
17 % K at 50 %) and a 300 % ink limit. It is a plausible coated-offset profile, not a measured
characterisation: its midtones are darker than FOGRA39 (mean ΔE00 6.7 against the FOGRA39
patches, oranges such as 0/50/100/0 about 5 ΔE00 off).
coated-fogra39 (profiles/craft-coated-fogra39.icc, 713 KB, AToB 11⁴ lut16, BToA 33³
lut16, 330 % ink limit) reproduces the colours of documents set up for Coated FOGRA39 or ISO
Coated v2. It is fitted to Fogra's FOGRA39 characterisation data (tests/data/FOGRA39.txt,
1617 IT8.7/4 patches, CMYK with D50 XYZ and Lab; ISO 12647-2:2004 Amd 1 offset on paper type
1/2): a Yule–Nielsen modified Neugebauer model with free overprints, per-ink tone curves and a
fitted n (1.45) is solved by least squares against all patches (mean ΔE00 1.1 on its own), then
a Gaussian-kernel ridge regression of the remaining Lab errors over CMYK (σ = 0.2, λ = 0.01)
corrects it before the AToB table is sampled, with no ink mapped to exactly L* 100,
a* = b* = 0. The BToA tables invert that AToB table; their input curves put that white on a
grid node holding no ink (CmykParams::white_on_node), so the profile separates PCS white to
within 2·10⁻⁵ ink on its own.
| Through the AToB, relative colorimetric, against the 1617 FOGRA39 patches | mean ΔE00 | p95 | max |
|---|---|---|---|
coated-fogra39 |
0.07 | 0.18 | 0.53 |
coated-fogra39 fit, against the 10 % of patches held out of it |
0.11 | 0.32 | 0.79 |
coated-cmyk |
6.67 | 13.4 | 16.1 |
Lab → CMYK → Lab through the profile (relative colorimetric) returns colours inside the gamut
within mean ΔE00 0.24 (p95 1.07, max 2.55). Converted to sRGB (relative colorimetric with black
point compensation), process solids, overprints and oranges land within 0.8 ΔE00 of reference
values for Coated FOGRA39 (tests/fogra39.rs).
"FOGRA39" identifies the reference data the profile is fitted to; it does not imply
certification, approval or endorsement by Fogra. The data file is Fogra's, distributed
unmodified with Fogra identified as the source as Fogra's terms require (see NOTICE); it is
left out of the published crate, as only the tests read it.
Regenerate the profiles with
CRAFT_CMS_REGEN_PROFILES=1 cargo test -p craft-cms --release --test regen --test fogra39
Use your printer's profile for production print.
Selecting by profile name
Use the ICC bytes a document embeds or a user loads whenever there are any. When a document
only names its profile (its desc tag), two functions map the name to a built-in, ignoring
case and whitespace:
Builtin::from_profile_name: exact matches only, a built-in's own description.Builtin::stand_in_for: the exact match, else a built-in that approximates the named profile.
| Name in the document | from_profile_name |
stand_in_for |
|---|---|---|
| each built-in's own description (e.g. sRGB IEC61966-2.1) | that built-in | that built-in |
| Coated FOGRA39 (ISO 12647-2:2004), Coated FOGRA39 | None |
coated-fogra39 |
| ISO Coated v2 (ECI) | None |
coated-fogra39 |
| ISO Coated v2 300% (ECI) | None |
None |
| anything else | None |
None |
The stand-ins are made from the same characterisation data as the named profiles: ECI's ISO Coated v2 is made from FOGRA39L, the data of Adobe's Coated FOGRA39. They describe the same printed colours. The built-in separates to at most 330 % total ink, the limit of ISO Coated v2 (Adobe doesn't publish Coated FOGRA39's). Black generation and ink distribution differ, so separations through a stand-in are not the named profile's. ISO Coated v2 300% (ECI) has no stand-in: the built-in separates to up to 330 % total ink.
Display transforms
Page-layout and image editors usually convert CMYK for display with relative colorimetric and
black point compensation. To match them, keep BPC on (the TransformOptions default):
use craft_cms::{Builtin, Intent, Profile, Transform};
// The document's own profile first; a stand-in only when it carries no ICC bytes.
let parsed;
let cmyk: &Profile = match doc_icc_bytes {
Some(bytes) => {
parsed = Profile::parse(bytes)?;
&parsed
}
None => match Builtin::stand_in_for(&doc_profile_name) {
Some(b) => b.profile(),
None => return Err(UnknownProfile(doc_profile_name)), // e.g. ask the user for the profile
},
};
let t = Transform::new(cmyk, Builtin::Srgb.profile(), Intent::RelativeColorimetric, /*bpc*/ true)?;
// or: transform::cached(src, dst, TransformOptions::default())
Not implemented / approximate
- v4.4
mpet(multiProcessElements, D2Bx/B2Dx) tags, named colour, device link and abstract profiles as transform endpoints,chad-based adaptation of v2 absolute colorimetry. - BPC destination black uses the BToA/AToB round trip directly (Adobe's curve fit for ill-behaved LUT profiles is not implemented); v2/v4 perceptual reference black mixing is not modelled beyond the PRMG black for v4 LUT profiles.
- Profile ID (MD5) is not computed when writing (left zero, which the spec allows).