Files
4062d40070 Color: Dot Gain 20%-compatible default working gray, as in Photoshop (#4610)
* feat(color): Dot Gain 20%-compatible default working gray, as in Photoshop

Photoshop's default working gray is Dot Gain 20%, and its RGB -> Grayscale
measured in Photoshop 27.11 (default Color Settings) is D50 luminance
encoded with gamma 1.732: sRGB 64 / 128 / 192 grey and pure red become
46 / 105 / 177 / 107. PhotoCraft defaulted to sGray, which kept the sRGB
value (64 / 128 / 192 / 130), up to 24 levels off.

Adds the built-in "Dot Gain 20%-compatible (gamma 1.732, Photocraft)"
gray profile (id dot-gain-20-compat; our own fitted curve, not Adobe's
profile, the same gamma PhotoCraft already uses for gray adjustments) and
makes it the default working gray and the fallback for untagged gray
documents. sGray and Gray Gamma 2.2 stay selectable.

This changes a default: untagged gray documents now convert and display
as Photoshop shows them (midtones lighter than under sGray); documents with
an embedded profile are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Status bar test: the default working gray is now Dot Gain 20%-compatible

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* .atn test: #202020 in the Dot Gain 20%-compatible working gray

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 02:45:09 -04:00
..
2026-10-01 08:25:06 -04:00
2026-09-30 11:36:20 -04:00

photocraft-cms

Pure-Rust ICC colour management for Photocraft. No workspace dependencies (L0, publishable), no C, no unsafe. 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.

API

use photocraft_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
  • 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.
  • Profile::to_bytes(): original bytes of parsed profiles (byte-exact), or an ICC v4.3 encoding.
  • 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 with CmsError::Invalid); BPC uses neutral black points (Y only) mapped linearly in XYZ.

Built-in profiles (all CC0-1.0, generated by this crate)

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

The CMYK profile

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 Photocraft generates its own (src/synth.rs): 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 %), GCR black generation (starts at 20 % gray component, K max 95 %), total ink limit 300 %, perceptual tables with L* black-point scaling and soft chroma compression against a sampled gamut boundary. profiles/photocraft-coated-cmyk.icc (237 KB, AToB 11⁴ lut16, BToA 21³ lut16) is that output, dedicated to the public domain (CC0-1.0). Regenerate with

PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test regen

It is a plausible coated-offset profile, not a measured characterisation: use your printer's profile for production print.

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).