Commit Graph
12 Commits
Author SHA1 Message Date
Dylan M. Taylor bc8fe0eb53 cms: keep NaN propagation in multilinear interpolation
A NaN input gives a NaN fraction, which the active-dimension check dropped, returning a
finite node value. Keep any nonzero fraction, NaN included (CodeRabbit review).
2026-10-22 22:22:39 -04:00
Dylan M. Taylor c900c69553 security: bound cms CLUT interpolation and inverse-curve table construction
- Clut::multilinear enumerates corners only for dimensions between two grid points, so
  singleton dimensions no longer multiply per-pixel work (CodeRabbit CWE-407, clut.rs:238)
- Transform::from_pipeline finds a sampled curve's flat ends once per curve via
  Curve::inverse64_many instead of rescanning the table for each of the 4096 output entries
  (CodeRabbit CWE-407, curve.rs:276)
2026-10-22 21:39:20 -04:00
midasmatterhorn 8b71728482 cms: the mluc record is read as one checked 12-byte slice
The eager tuple computed o + 8 even when the first read had failed,
overflowing a 32-bit usize at offsets near u32::MAX (n = 2 with
size = u32::MAX - 20 reaches it: o fits, o + 8 does not). The record is
now bounds-checked in one slice and the fields read inside it, the same
complete-record pattern the profile reader uses elsewhere.
2026-10-16 14:46:23 +03:00
midasmatterhorn 1db56c92d2 cms: bound the ICC curve count before it is multiplied, and read mluc record offsets checked
parse_curve computed 12 + n * 2 with n straight from the file and only
checked the 1 << 20 bound afterwards, inside the table arm — on a 32-bit
(wasm32) target, which craft-cms ships branches for, a trC curve count of
0xFFFFFFFF overflows with overflow checks on and panics before the guard
could reject it. The mluc reader computed 16 + k * size unchecked, where
wrapping reads another record's bytes as the language, length and offset
(wrong text, no error) instead of stopping.

Hoist the curve-count guard above the arithmetic and compute the record
offset with checked_mul/checked_add, breaking out when it does not fit.
This ports the app-side fix accepted as PhotoCraft #4976 / PR #5227,
which was never carried into craft-cms. On a 64-bit host both sites are
benign (the arithmetic fits), so the two new tests pass before and after
there; they bite on a 32-bit/overflow-check build, which deserves a
wasm32 CI job — craft-cms has none yet.
2026-10-15 17:58:13 +03:00
skylartaylor 8b8576f0b0 Use fixed-size chunks in the CMS total-ink regression 2026-10-14 06:27:12 +02:00
aiongg 9e2b2b03d8 craft-cms: separate exact profile-name matches from stand-ins
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.
2026-10-13 03:22:39 +00:00
aiongg d282bd06c6 craft-cms: reference-value regression cases, including the reported DesignCraft orange
The reference value for CMYK 0/41/90/4 in Coated FOGRA39 is 239,161,35.
The fitted profile gives 239,161,33 (ΔE00 0.16); the uncalibrated formula
DesignCraft used gives 245,144,25 (ΔE00 6.36). Six more reference values
(K50 grey, rich black, light tint, skin tone, C+Y green, near-neutral)
are within 0.7 ΔE00.
2026-10-13 03:17:20 +00:00
aiongg 7f05840bc0 craft-cms: portable moxcms CMYK oracle with the built-in Coated FOGRA39
The Generic CMYK oracle only runs where macOS ships that profile. The
same comparison (CMYK -> sRGB, 8-bit, relative colorimetric without
BPC, same bytes for both engines) with the built-in Coated FOGRA39
runs everywhere. Measured: max 10, p99 5, mean 0.89 codes; the worst
pixel matches an independent float computation, not moxcms.
2026-10-13 01:50:14 +00:00
aiongg 1f0d815477 craft-cms: map device white to device white on every path
With a LUT destination, sRGB white came out as 0.3-0.5 % C/M/Y through
Transform::eval and precise float conversions: the legacy Lab encoding
puts L* 100 at 0.996 and a* = 0 at 0.50001, between the BToA grid
nodes, and fix_white only patched the device-link node, so only the
table paths were right.

Transform: for every intent but absolute colorimetric (and proofs
without paper simulation), a pixel equal to the source's device white
gives exactly the destination's device white. fix_white still sets the
device-link node; the paths that do not read that node exactly (the
exact pipeline, float interpolation, and 8-bit lookups when white is no
node) compare each pixel with white once. No other colour changes.
device_white(Lab) had a* = b* = 0.5; Lab white is 128/255 in the v4
encoding, which is no node of the 33-point grid, so Lab sources take
the per-pixel check there too.

Profile: synth::CmykParams::white_on_node maps no ink to exactly
L* 100, a* = b* = 0 in the AToB and gives the BToA tables input curves
that put that white on a node holding no ink. The Coated FOGRA39
profile is regenerated with it and separates PCS white to 2e-5 ink
(was 0.5 %), so the step into white is as smooth as its neighbours.
The default parameters and the Photocraft profile are unchanged; its
white still falls between nodes, so its last grey step into white is
up to about 0.4 % C larger than the one before, as with lcms's
white-node fix, and the test allows that for this profile.

FOGRA39 accuracy against 16a66e1: AToB against the patches unchanged
(mean 0.07, p95 0.18, max 0.53 ΔE00), reference sRGB values unchanged
(max 0.76), Lab -> CMYK -> Lab p95 1.04 -> 1.07, max 2.54 -> 2.55
(mean 0.24).
2026-10-13 01:50:14 +00:00
aiongg d0107d1130 craft-cms: return errors instead of panicking on caller input
The crate denies panics, but asserts and slice indexing were reachable
from arguments: pixel strides, buffers that are not whole pixels or a
destination shorter than the source, single-colour slices shorter than
the channel count, CLUT and pipeline dimensions, and LUT sizes.

Every such check now returns CmsError::InvalidArgument. Buffer
conversions validate once per call, before any pixel is written, so the
per-pixel loops carry no extra checks; internal callers that already
guarantee lengths use crate-private unchecked evaluation.

Pipeline, Clut and Lut3d keep their fields private (read through
accessors) so a validated value cannot be changed into one that panics.
Pipeline::new checks matrix shapes and channel counts; Clut::new and
Clut::sample check grid, outputs and the table size limit.

API changes (unreleased crate):
- Transform::eval, eval_fast, convert_u8/u16/f32, apply,
  convert_bytes_many and from_pipeline return Result.
- Pipeline::new, extend and eval, Clut::new, sample, eval,
  eval_trilinear and eval_multilinear return Result.
- GamutCheck::delta_e and out_of_gamut, Lut3d::from_transform,
  LutFile::identity and from_fn, synth::cmyk_profile and coated_cmyk
  return Result.
- Pipeline, Clut and Lut3d fields are read through methods.
2026-10-13 01:33:07 +00:00
aiongg f97ed953f4 craft-cms: built-in Coated FOGRA39 profile fitted to Fogra's data
Add Builtin::CoatedFogra39 ("Coated FOGRA39 (fitted, craft-cms)",
profiles/craft-coated-fogra39.icc: AToB 11^4, BToA 33^3, 330% TAC,
medium GCR). It is fitted to Fogra's FOGRA39 characterisation data
(tests/data/FOGRA39.txt, unmodified, 1617 patches): a Yule-Nielsen
Neugebauer model with free overprints, per-ink tone curves and n, plus
a Gaussian-kernel ridge correction of the residuals. Against the
patches, relative colorimetric through the AToB: mean dE00 0.07,
p95 0.18, max 0.53. The BToA tables invert the AToB table
(synth::cmyk_profile_from_model); the Photocraft coated profile and its
synth defaults are unchanged.

Builtin::from_profile_name maps the names documents carry for
FOGRA39-based profiles (Coated FOGRA39, ISO Coated v2 (ECI)) to the new
profile and every built-in by its description. Adds math::delta_e2000.

Refs #36
2026-10-13 01:23:45 +00:00
+2 dd9e3f1bc3 Add craft-cms: ICC colour management extracted from PhotoCraft
Move PhotoCraft's crates/cms (storytold/photocraft@b3222ee8e7)
into crates/cms as craft-cms: ICC v2/v4 profile parsing and writing, transforms, rendering
intents, black point compensation, soft proofing, gamut checks, synthetic CMYK, 3D LUTs, LUT
files and built-in profiles, with all tests.

The Bradford inverse and the built-in RGB colorant matrices are computed in constants, so the
build rejects a singular matrix and the two expect calls are gone; the values are bit-identical
to the runtime results. Built-in profile descriptions and copyright text keep their PhotoCraft
wording so the profile bytes do not change. rayon joins the workspace dependencies.

Refs #36

Co-authored-by: Alex Gorevski <agorevski@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Elvis33LE <67311580+Elvis33LE@users.noreply.github.com>
Co-authored-by: F4LCON <107556180+itsF4LCON@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: HEOJUNFO <100739943+HEOJUNFO@users.noreply.github.com>
Co-authored-by: Shehan Kavishka <kavishkashehan726@gmail.com>
Co-authored-by: Valentin Klein <vangel1976@googlemail.com>
Co-authored-by: ljkx <177671075+ljkx@users.noreply.github.com>
Co-authored-by: storybored379 <storybored379@gmail.com>
Co-authored-by: wstock <w.stock@yahoo.com>
2026-10-13 01:23:45 +00:00