feat(icc): built-in profile constructors and CICP → profile - #542
Open
justin13888 wants to merge 26 commits into
Open
justin13888 wants to merge 26 commits into
justin13888 wants to merge 26 commits into
Conversation
Add `IccProfile::builtin`, `gray_with_gamma`, `from_cicp` and `from_source_profile`: spec-valid v4 matrix/TRC display profiles for the colour spaces gamut-color already carries colorimetry for, and for the H.273 code-point triple AVIF/HEIC/JXL usually signal instead of embedding a profile. gamut-icc gains a normal dependency on gamut-color. The constructors need gamut-color's colorimetry and gamut-icc's serializer, and only one of the two can own that edge: gamut-color is the primitive (fan-in 8) with no need of a serializer, so pointing it the other way would invert the layering. Primaries, white point and transfer are read from gamut-color rather than restated, so the buildable set is exactly what it can express on the two CICP axes. Adobe RGB and ProPhoto RGB have no code point on either axis and their chromaticities are private to gamut-color, so they are declined rather than approximated.
Bradford-adapting to gamut-color's CIE D50 chromaticity while writing ICC's mandated D50 encoding as the mediaWhitePointTag left the colorants disagreeing with the white point they sum to by 2e-4 in Z, and put the disagreement with lcms2's own colorants at 1.8e-4. Deriving the adaptation target from `XyzNumber::D50` instead brings both inside four s15Fixed16 quanta.
…hoices STATUS gains the dependency direction and the evidence for it, the buildable set and why Adobe RGB / ProPhoto are declined, the parametric-vs-sampled tone-curve choice per transfer with the clause that decides it, the PCS white point, and the lcms2 acceptance criteria.
The explicit `Bt709 | Hlg | Unspecified => None` arm in `Trc::from_cicp` sat in front of a `_ => None` wildcard that `#[non_exhaustive]` makes mandatory, so deleting it changed no behaviour: an equivalent mutant no test can kill. Fold it into the wildcard's comment, which still names the three code points and why gamut-color supplies no curve for them. `unrepresentable_signalling_is_rejected` already pins BT.709 and HLG to `None` through the public entry point, so the behaviour stays covered.
`from_cicp` gave H.273 transfer code point 14 the PQ curve. Table 3 defines 14 as the BT.709 curve — its own remark calls it "functionally the same as the values 1, 6 and 15" — so a BT.2020 10-bit signal produced an `rTRC` reading 0.009224 at signal 0.5 where the spec gives 0.259719, a factor of 28. The same match rejected code point 1 outright while accepting 14. Codes 1, 6, 14 and 15 are one curve, and its inverse is exactly `parametricCurveType` function type 3 (ICC.1:2022 §10.18), so all four are now encoded from Table 3's own α = 1 + 5.5β and β = 0.018053968510807. The transfer axis is keyed on the raw code point rather than on `gamut_color::cicp::TransferCharacteristics`, because what an ICC tag can encode is not what gamut-color can evaluate: codes 6 and 15 have no variant there and none of the four has an EOTF, yet all four are exactly encodable. Three further conformance fixes on the same surface: * §10.3 requires `MatrixCoefficients` to be 0 when the data colour space is RGB, so the caller's value — 1, 5, 6 or 9 in a typical AVIF or HEIC `nclx` box — is no longer written into the `cicpType` tag, and `VideoFullRangeFlag` is normalized to 1 to match the full-scale RGB the profile's matrix and curves are defined over. Nothing is lost: both describe an encoding the caller de-matrixes before the profile applies, and both stay in the container signalling a decoder reads them from. * `gray_with_gamma` declines a gamma no `kTRC` can carry — non-finite, non-positive, or at or above the 32 768 `s15Fixed16` cannot hold, which `S15Fixed16::from_f64` would otherwise saturate silently. * `colorants_d50` returns the option instead of falling back to the identity, so primaries with no chromaticities are declined structurally rather than by naming `Unspecified`, and no profile can claim the PCS axes as its colorants. `constructors_are_byte_deterministic` compared `first.ok() == second.ok()`, so two serialization failures compared equal and it passed vacuously. BREAKING CHANGE: `IccProfile::builtin` and `IccProfile::gray_with_gamma` now return `Option<IccProfile>`. `IccProfile::from_cicp` no longer records the caller's `MatrixCoefficients` or `VideoFullRangeFlag`, and builds a BT.709 tone curve — not a PQ one — for transfer code point 14.
…imit STATUS.md's tone-curve table codified the defect the previous commit fixed, listing a sampled PQ `curveType` as the encoding for transfer code points "16, 14". It now names the BT.709 family separately, states which code points are declined and why, and records that `from_cicp` builds from two of the four H.273 fields because §10.3 fixes the other two. The dependency paragraph claimed gamut-color has fan-in 8. It is 10 before this crate's edge and 11 with it: gamut, av1, av2, avif, cmm, dng, heic, jpeg, vvc, webp. The frozen record elsewhere stands as written — this is new prose, and new prose must not restate a figure known to be wrong. Also records, as a known limit, that the BT.2100 PQ profile is peak-referred: its curve is normalized to ST 2084's own 10 000 cd/m² peak, so a diffuse-white signal evaluates to roughly 0.02 media-relative and a CMM renders such content through it near black. That is correct for a peak-referred profile and may still surprise a caller embedding it, so the alternative normalization is tracked rather than taken silently. `AGENTS.md`'s architecture table listed gamut-icc with no dependency edge. This branch adds `gamut-icc ← color`.
`gray_with_gamma` is public but `GAMMA_ENCODING_LIMIT` is not, so rustdoc reported "public documentation for `gray_with_gamma` links to private item GAMMA_ENCODING_LIMIT" and rendered no link at all. The magnitude is short enough to state.
This was referenced Sep 10, 2026
`gray_with_gamma` refused a gamma at or above 32 768 because `S15Fixed16::from_f64` saturates there, but the same conversion degenerates at the low end too: it rounds, so any gamma below 0.5 / 65 536 = 7.62939453125e-6 is written as raw 0. `Y = X^0` maps every input, black included, to white — exactly the profile the doc comment gives as the reason for refusing a literal 0.0 — and the constructor accepted it, serialized it, and `validate` reported it clean. Verified by re-opening the written bytes: gamma 1e-6 gave a `kTRC` of raw 0 and a transfer that is identically white. The guard now tests the encoding rather than the value, at both ends: `(gamma * 65 536).round()` must land in `1 ..= i32::MAX`, which is the same arithmetic `from_f64` performs, so the two cannot disagree about where rounding stops carrying the caller's number. That also corrects where the top bound sits. Saturation does not begin at 32 768 but at (2^31 − 0.5) / 65 536 = 32 767.999992370605468750, half a quantum above the largest representable value; gammas in between were accepted and silently written as 32 767.99998474121. The boundary test now reads each accepted gamma back out of the tag it was written into, so a value that survives the guard but not the encoding fails it.
`Trc::for_code_point` maps seven H.273 code points to a tone curve and declines the rest, but every test of it started from a code point it already believed in. That catches a member sent to the wrong curve; it cannot catch a non-member let in. Adding an eighth arm — one H.273 Table 3 gives a different curve, with its own toe and its own constants — left all 161 tests passing, and no mutation of a match expression produces an extra arm, so the mutation gate is blind to it as well. The new sweep asserts `None` for every byte in 0..=255 outside the accepted set, with that set restated rather than read back from the function under test.
ICC.1:2022 §10.3 carries one `shall` about a `cicpType` tag in an RGB profile: `MatrixCoefficients` shall be 0. It says nothing of the kind about `VideoFullRangeFlag` — only that it "is often 1" for RGB — and its own RGB examples include 1-1-0-0 and 9-16-0-0 with the flag at zero. This module wrote both fields and documented both as §10.3 requirements, which is true of one and false of the other. Fixing the flag at 1 is a deliberate normalisation: the colorants, the `chad` and the tone curves written alongside it are all defined over full-scale RGB, so a profile of this shape signalling narrow range would describe a scaling it does not perform. That is now documented as this crate's choice, together with its consequence — the caller's flag is discarded, not preserved, and a caller that needs the original value must read it from the container signalling it came from. Also repairs a doc link left behind by an earlier rename. Both items are private, so `cargo doc` without `--document-private-items` never resolved it and reported the crate clean.
H.273 Table 3 defines transfer code points 1, 6, 14 and 15 as an opto-electronic function, and an ICC tone curve encodes signal to light, so this module writes the exact inverse. That literal reading stays: it is what the reference implementations write, and changing it would make gamut-icc disagree with every other tool that reads the same signalling. It is not the only reading H.273 sanctions. §8.2 NOTE 1 points at BT.1886-0 as the corresponding electro-optical function for flat-panel displays, which at reference black zero is a pure gamma of 2.4 — and every profile built here is a display-class profile, so that reading has a claim. The two are far apart, and the docs now say so with numbers: at mid-grey the literal reading gives Y = 0.259719 against BT.1886's 0.189465, a factor of 1.371. The same table records that two code points this module already encodes, 1 and 13, differ by 0.0457 at mid-grey — 21 % of the sRGB value — so a caller treating "BT.709 primaries" and "sRGB" as interchangeable sees that much shift from the transfer alone. Offering the BT.1886 reading as an option is filed separately.
`from_cicp` rewrote `VideoFullRangeFlag` to 1 and called the loss harmless, on the reading that the flag — like `MatrixCoefficients` beside it — describes a luma-chroma encoding the caller de-matrixes before an RGB profile applies. ICC.1:2022 §10.3's own RGB examples refute that. `1-1-0-0` is listed as "RGB narrow range representation specified in Recommendation ITU-R BT.709-6, Item 3.4", and its `MatrixCoefficients` is already zero: the narrow range is on the RGB samples themselves, and no de-matrixing removes it. So a caller holding genuine narrow-range signalling was not merely losing a piece of metadata; it was handed a profile whose colorants, `chad` and tone curves are all defined over full-scale RGB, and its colour rendered wrongly. The flag is now a precondition rather than a rewrite: anything but 1 is declined, which is what this module already does for primaries it has no chromaticities for and for a transfer with no ICC tone curve. Narrow-range callers scale to full range and pass 1. `normalized_cicp` becomes `rgb_conforming_cicp` — it now carries only the §10.3 `shall`, which is the one field that is still rewritten — and the accepted flag is stated once as `FULL_RANGE`, read by both the tag `cicp_of` writes and the precondition `from_cicp` enforces, so the two cannot drift. The complement is swept over all 256 flag bytes rather than sampled at 0: the guard is a comparison, and an ordering mutation of it leaves one side of 1 admitted. BREAKING CHANGE: `IccProfile::from_cicp` returns `None` for a `Cicp` whose `video_full_range_flag` is not 1, where it previously built a full-range profile from it.
`gray_with_gamma` wrote the `profileDescriptionTag` from the value the caller passed and the `kTRC` from that value encoded as `s15Fixed16`, so the two disagreed by up to half a quantum — and at the smallest accepted gamma, `0.5 / 65 536`, the description read one number while the tag carried exactly twice it. A profile contradicting its own tag is the same class of defect this branch has been closing: one fact written down in two places. The guard now returns the encoded parameter instead of a bool, and `gray_with_gamma` shadows its argument with it. The requested value is out of scope from that line on, so nothing below can be written from it — the description names what the tag holds because there is nothing else left to name. `gray_with_gamma(2.2)` is therefore described as `Grey gamma 2.1999969482421875`, which is the parameter a reader inspecting the tag will find. Two statements of the encoding bound are corrected while they are in hand. The saturating parameter's value was written truncated (`32 767.999 984 741 21`) beside a neighbour written exact, and is now exact: `32 767.999 984 741 210 937 5`. And the two ends of the domain are named for what they are — the bottom refuses a degenerate all-white curve, while the top is fidelity only, since the first rejected gamma and the last accepted one are one f64 ulp apart (2^-38) and evaluate identically. It is kept for the symmetry of a closed domain, and now says so instead of implying both ends refuse harm. STATUS.md carried the superseded bound — "non-finite, non-positive, or ≥ 32 768" — which was wrong in both directions once the guard moved onto the encoding: it refused values the constructor accepts and admitted the whole low interval the constructor refuses.
…lues Two documentation statements in this module were about something other than what they named. `cicp_byte` cited ICC.1:2022 §10.7 for the four-byte `cicpType` layout. §10.7 is `dataType`; `cicpType` is §10.3 — the clause this branch spent a round reading correctly for `MatrixCoefficients` and `VideoFullRangeFlag`. Every other citation in the module checks out. The mid-grey comparison table labelled a row "what this module writes" but carried the value of the exact closed form, which is not what the tag holds: the `parametricCurveType` parameters are rounded to `s15Fixed16` first. Both rows this module actually writes are now quoted as the tag evaluates — 0.259721 for the BT.709-family inverse OETF and 0.214045 for sRGB, against 0.259719 and 0.214041 before rounding — and the BT.1886 row is labelled as the closed form, because no tag here holds it. The divergence figures the paragraph draws from the table (1.371x, 0.0703, 0.0457, 21 %) are unchanged at their stated precision.
`§9.2.10` is BToD1Tag and `§9.2.35` is metadataTag in the vendored ICC.1:2022; the MatrixColumn tags are `§9.2.46`/`§9.2.31`/`§9.2.4` and chromaticAdaptationTag is `§9.2.15`. Every `§`-citation in the crate was resolved against a clause index extracted from the vendored PDF rather than read by hand, and these two were the only members that resolved false. Names the numbering used, since the standard's own §8.4.3 cross-references disagree with its headings for two of the four.
The module docs and STATUS.md both said `gamut_color::transfer::eotf_for` supplies no EOTF for any of H.273's four BT.709-family code points. It supplies one for code point 14: `bt2020_pq_to_sdr`, a PQ EOTF plus a tone map, which at `V = 0.5` evaluates 20.3 % above the curve Table 3 gives that code point. The sentence was the stated justification for keying transfers on the raw code point, so a reader who believed it would key on `TransferCharacteristics` and write a tone map into an ICC tag. Both statements now say what the crate returns, and the claim itself is a doctest beside them, so it cannot drift from the crate it describes. That the two crates read code point 14 differently is gamut-color's question and is filed as #605, not fixed here. Refs #605
…oids `pcs_d50_chromaticity` gave the CIE-published D50's Z as 0.82521. Derived from `gamut_color::matrix::D50` — the chromaticity the sentence names — it is 0.825105, against ICC's 0.824905. The gap the paragraph is about is unchanged at 2.0e-4; only the number naming the other crate's constant was wrong, and it is a third statement of an external value this crate restated without checking.
`SAMPLED_TRC_POINTS` claims 1024 points hold the linear-interpolation error below one `uInt16` quantum, and the guarding test admitted two — so the claim was stated twice and gated nowhere. Its 101-point sweep also missed the peak, which sits mid-interval at `V` about 0.9956: it measured 0.962 quanta where the true worst case is 0.986. The sweep now visits every interval at both ends and at its midpoint, and the tolerance is one quantum. Measured 0.9854 on that grid, 0.9861 over a 200 001-point sweep; both figures are published on the constant.
`from_source_profile(SourceProfile::BT2020)` builds a peak-referred ST 2084 profile, while the bundle's own `eotf` is that curve plus a Reinhard tone map to SDR: measured up to 0.735 absolute apart over the signal domain, 52x at V = 0.1, against 4.2e-6 for the sRGB bundle. The reasoning for building it anyway was on the pull request and nowhere a consumer reads, so it is now on the constructor, in STATUS.md and beside the test that asserts the mapping, with the measured divergence and the distinction that carries it: a sample range is a property of the samples, a tone map is a rendering choice.
Both the module docs and STATUS.md said the colorimetry is never written down twice. Three published constants are written here: H.273's beta, the IEC 61966-2-1 parametric set, and the PCS D50. All three are correctly gated, so the handling was right and only the blanket sentence was wrong - but a sentence that overstates is what four rounds of review have been about. Each restatement is now named with why it cannot be borrowed and which test pins it.
The record justified declining a narrow-range triple by argument: the tag would contradict the colorants beside it. ICC.1:2022 §9.2.17 settles it outright - the CICP tag content "shall be equivalent to the data colour space encoding represented by this ICC profile" - so carrying the flag through is non-conforming, not merely inconsistent. The same clause is why `gray_with_gamma` writes no `cicpType`: it is permitted only for RGB, YCbCr and XYZ, which read as an omission until it is cited.
`Perceptual` is `ProfileHeader::new`'s default and a surprising one on a colorimetrically exact matrix/TRC profile, where a caller most likely means media-relative colorimetric. It costs one section to say so, and a doctest to keep the statement true; the field is a preference a CMM may override, so nothing about the colorimetry turns on it.
Both the crate docs and the README enumerated the dependency list and both omitted `thiserror`, which `error.rs` derives its error type from. Found by the same sweep that derived the cross-crate claims, and false for the same reason: a list that says "only" is a completeness claim.
The derivation of every doc sentence asserting what a function outside this crate returns left two members ungated: the 20.3 % by which `gamut_color::transfer::eotf_for`'s curve for code point 14 exceeds the one this module writes, and the tristimuli separating `gamut_color::matrix::D50` from the PCS D50. Both resolve true — 20.298 % and a 1.99e-4 gap — and both were stated twice with nothing to hold them. The first joins the doctest that already pins the sentences beside it; the second is a drift guard on the two constants, since the arithmetic it describes is what decides the adaptation target. STATUS.md's claim that the doctest pins "every clause" of that paragraph was itself an ungated completeness claim, and is replaced by the list of what it asserts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gamut-iccgains built-in profile constructors and a CICP → profile path — pieces 1 and 2 ofissue #424.
IccProfile::builtin(BuiltinProfile)emits a spec-valid v4 three-component matrix/TRC displayprofile (ICC.1:2022 §8.4) for sRGB, linear sRGB, Display P3 and BT.2100 PQ.
IccProfile::gray_with_gamma(f64)emits the monochrome equivalent.IccProfile::from_cicp(Cicp)builds one from the H.273 code-point triple AVIF, HEIC and JXLusually signal instead of embedding a profile, and records it in a
cicpTypetag.IccProfile::from_source_profile(SourceProfile)builds one fromgamut-color's bundle.gamut-iccgains a normal dependency ongamut-color. The colorimetry is never restated in thiscrate: primaries and white point come from
ColourPrimaries::chromaticities, the RGB→XYZconstruction and Bradford adaptation from
gamut_color::matrix, and the ST 2084 curve fromgamut_color::transfer. That is what makes the buildable set exactly whatgamut-colorcan expresson the two CICP axes — and why Adobe RGB and ProPhoto RGB are declined (
None) rather thanapproximated: both return
Nonefromcolour_primaries()andtransfer_characteristics(), andtheir chromaticities are private to
gamut-color. Filed as #537.Tone curves follow the record: a
parametricCurveType(§10.18) where H.273 gives the transfer aclosed form ICC also defines (linear → type 0, sRGB → type 3, grey gamma → type 0), a sampled
curveType(§10.6) of 1024uInt16points for PQ, which §10.18 has no form for. The per-spacechoice and its deciding clause are tabulated in
crates/gamut-icc/STATUS.md.One defect was found and fixed by the oracle while building this: adapting colorants to
gamut_color::matrix::D50(the CIE chromaticity) while writingXyzNumber::D50(ICC's roundedtristimulus, §7.2.16) as the
mediaWhitePointTagleft the colorants disagreeing with the whitepoint they are supposed to sum to by 2e-4 in Z, and put the disagreement with lcms2's own colorants
at 1.8e-4. Deriving the adaptation target from
XyzNumber::D50brings both inside fours15Fixed16quanta. See commit 2 and the newcolorants_sum_to_the_declared_media_white_point.Commit 4 is a no-behaviour follow-up:
Trc::from_cicpcarried an explicitBt709 | Hlg | Unspecified => Nonearm in front of the_ => Nonethat#[non_exhaustive]makesmandatory, so deleting it changed nothing and no test could kill the mutant. The arm is folded into
the wildcard's comment, which still names the three code points.
No human approved this plan. This is an unattended automated run; the record below is what a
human reads afterwards.
Validation
Every command below was run in this worktree against the head SHA
9b12bba1, and every one of themcompleted in that run. Each was wrapped in the mandated memory-capped scope
(
systemd-run --user --scope --slice=agents.slice -p MemoryMax=16G -p MemorySwapMax=0 -- env CARGO_BUILD_JOBS=2 CMAKE_BUILD_PARALLEL_LEVEL=2 sh -c 'ulimit -v 12000000; exec <command>').mise run fmt-checkmise run check-testsmise run check-commitsconvco check: "no errors in 3 commits"mise run check-release-depsmise run check-ffi-featuresmise run check-ffi-headercbindgenreproduces the committed header unchangedcargo test -p gamut-icc --all-featurestests/oracle.rs+ 7tests/roundtrip.rs+ 6 doctests, 0 failed (15 of the 156 lib tests are new; master has 141)mise run linttooling/codemise run testmise run mutants-diffThe
fmt/fmt-checktasks were invoked with a__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel)prefix. That is the documented nested-worktree artefact: in a nested tree cargo
otherwise walks past the worktree root to the primary checkout's
Cargo.tomlwhen loading thetooling/*manifests, and the task exits 101 on an untouched tree. No manifest was changed to workaround it.
Acceptance against the oracle
Per the record, no assertion in this change compares against golden bytes this crate produced.
tooling/lcms2-oracle(vendored Little-CMS) is the authority, matchingdocs/testing.md's tablefor
gamut-icc(Little-CMS, differential):oracle_colorants_match_lcms_for_the_same_primaries— lcms2 re-opens the built-in profiles forthe three distinct primary sets (sRGB/BT.709, Display P3, BT.2020) and reports the same
colorants it derives from the same chromaticities itself, within four
s15Fixed16quanta. LinearsRGB is not in that loop because colorants are a function of the primaries alone and it shares
sRGB's; it is covered by the two whole-set tests below. The tolerance is the tag encoding, not the
derivation: largest observed disagreement 2.4e-5, under two quanta.
oracle_srgb_tone_curve_matches_lcms— lcms2 evaluates our sRGB TRC to the same values as thesRGB profile it synthesizes itself. Two independent constructions of IEC 61966-2-1, read by one
reference CMM.
oracle_transform_through_our_srgb_is_the_identity— a media-relative colorimetric transform fromour sRGB into lcms2's own sRGB returns all 256 ramp values within one 8-bit code. End-to-end:
colorants, white point, adaptation and TRC all have to be right together.
oracle_gray_gamma_matches_lcms— lcms2'sestimate_gammaonkTRCreturns the gamma asked for.colorants_sum_to_the_declared_media_white_point— over all four spaces: the colorants sum to themediaWhitePointTagthe profile itself declares. This is the assertion the D50 defect abovetripped.
every_constructor_satisfies_the_section_8_display_model— over all four spaces plus the greyconstructor: gamut's own
validate()accepts each profile against the §8 Display required-tagset.
The one self-referential assertion is
constructors_are_byte_deterministic, which compares twocalls of the same constructor to each other. It deliberately proves nothing colorimetric; it
guards the "no timestamp, no profile ID, no entropy" property the module documents, which a later
DateTime::now()would silently break.Tests are inline in
src/builtin.rs, nottests/: a dev-dependency oracle is explicitly not areason to move up (
docs/testing.md), several assertions read non-pubitems (colorants_d50,cicp_byte,Trc,BuiltinProfile::parts), and inline is the only placement from which a mutantmasked at the public boundary is killable.
Risks and rollout
gamut-icc → gamut-color. No cycle:gamut-colordoes not andwill not depend on
gamut-icc.check-release-depsconfirms release-plz can still order thegraph. It widens
gamut-icc's dependency footprint fromgamut-core+md-5;gamut-color'sown dependencies are
gamut-coreplus an optionalserdethis edge does not select, so nothird-party dependency is added (the
Cargo.lockchange is a single line).and validation are untouched.
BuiltinProfileis#[non_exhaustive],#[repr(u8)], withpermanent append-only discriminants, so gamut-color/gamut-icc: expose Gamut chromaticities so Adobe RGB and ProPhoto get built-in profiles #537 can add variants without a breaking change.
back from
pq_eotf(1.0)rather than restated. A caller wanting a different peak needs a curvethis crate does not offer;
SourceProfile::BT2020's encoder-exact transfer (PQ + Reinhard@203tone map to SDR) is deliberately not what the profile carries — that is a tone map, not a
display TRC, and embedding it would misdescribe the signal.
cicpTypein every RGB profile built here. Consistent with the matrix/TRC pipeline beside itby construction. Harmless to a CMM that ignores it; lcms2 transforms through these profiles
correctly (test above).
crates/gamut-icc/src/builtin.rs, itslib.rswiring and thegamut-colordependency line (plus the README/STATUS paragraphs that describe them).
Issue
Refs #424 — pieces 1 and 2 land here. The issue does not fully close:
accessor in
gamut-color, filed as gamut-color/gamut-icc: expose Gamut chromaticities so Adobe RGB and ProPhoto get built-in profiles #537 — gamut-color/gamut-icc: expose Gamut chromaticitiesso Adobe RGB and ProPhoto get built-in profiles;
to take it "only with a named consumer"; ICtCp belongs to the HDR milestone.
Decisions taken
Appended by this lane
Unresolved review notes
None.
Round 2 — review repair (branch
feat/424-icc-builtin-profiles, issue #424)A review of this pull request found eight items on the same surface. All eight are repaired here, in
commits
6592e65(fix(icc)!) andd250db5(docs(icc)). Everything above this line is theround-1 record and is reproduced unchanged.
What changed
The headline defect: transfer code point 14 was given the PQ curve. ITU-T H.273 (07/2024)
Table 3 defines code point 14 as the BT.709 curve — its own informative remark calls it
"functionally the same as the values 1, 6 and 15" — so
from_cicpproduced anrTRCreading0.009224 at signal 0.5 where the spec gives 0.259719, a factor of 28, and Little-CMS read the
same wrong value back from the serialized bytes. The same match rejected code point 1 outright
while accepting 14.
Table 3's four members (1, 6, 14, 15) share one pair of constants — §8.2 states them exactly:
β = 0.018053968510807…andα = 1 + 5.5 β = 1.099296826809442…— and the inverse of thatopto-electronic function is
parametricCurveTypefunction type 3 (ICC.1:2022 §10.18) exactly:so the parametric branch of the record's decision 1 is the one taken; nothing had to be declined
for want of a closed form. Quantizing
(g, a, b, c, d)tos15Fixed16costs at most 1.4e-6across the whole domain, measured, which is what sets the test tolerance.
§10.3 conformance. "When the data colour space in the profile header is RGB or XYZ,
MatrixCoefficients shall be 0 (zero)."
from_cicpwrote the caller's value verbatim, so thecommonest real input there is — an AVIF or HEIC
nclxbox carrying 1, 5, 6 or 9 — produced aspec-violating profile. It now writes 0, and normalizes
VideoFullRangeFlagto 1 to match thefull-scale RGB the profile's own matrix and curves are defined over (§10.3's RGB examples note the
flag "is often 1"). This is conformance, not information loss: both fields describe a luma–chroma
encoding the caller de-matrixes before this profile applies, and both remain in the container
signalling a decoder actually reads them from.
Fallible where the input is open.
gray_with_gammaaccepted0.0,-1.0,NaN,∞and40000.0, emitting descriptions like"Grey gamma NaN"; it now declines any gamma akTRCcannotcarry.
colorants_d50returned the identity matrix for primaries with no chromaticities — aprofile silently claiming the PCS axes as its colorants — and now returns the option, which
from_cicpconsumes with?in place of a guard that namedUnspecifiedby hand.constructors_are_byte_deterministiccomparedfirst.ok() == second.ok(), so two serializationfailures compared equal and the test passed vacuously. It now serializes twice and compares the
bytes, failing if either serialization fails.
Validation
Every command below completed in this run, in this worktree, at head
d250db5. Each workspace-wideone was wrapped in the mandated memory-capped scope (
systemd-run --user --scope --slice=agents.slice -p MemoryMax=16G -p MemorySwapMax=0 -- env CARGO_BUILD_JOBS=2 CMAKE_BUILD_PARALLEL_LEVEL=2 TMPDIR=… sh -c 'ulimit -v 12000000; exec <command>').cargo test -p gamut-icc --all-featurestests/oracle.rs+ 7tests/roundtrip.rs+ 6 doctests, 0 failedcargo clippy -p gamut-icc --all-targets --all-features -- -D warnings__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkmise run check-testsmise run check-commitsconvco check: "no errors in 6 commits"mise run check-release-depsmise run check-ffi-featuresmise run lintmise run testtest result: oklines, 0 failedmise run mutants-diffA green mutation gate does not by itself pin an or-pattern's alternatives — cargo-mutants does not
mutate them individually — so
every_bt709_family_code_point_builds_the_same_curveasserts each of1, 6, 14 and 15 separately rather than sweeping them as a set.
New and repaired tests, each naming one thing:
bt709_curve_inverts_the_h273_transfer— the written curve is the inverse of Table 3's forwardfunction, transcribed independently in the test with its own literals (sharing
BT709_ALPHAwould share any mistyped digit), swept over both segments and across the β knee.
every_bt709_family_code_point_builds_the_same_curve— the F1 regression pin, and thealternative-by-alternative pin of the or-pattern.
oracle_bt709_tone_curve_matches_the_h273_transfer— Little-CMS re-opens our serialized bytesand evaluates the
rTRCback to the light Table 3's forward function started from. This is whatsays the function type, parameter order and
s15Fixed16encoding are right, not merely that ourown evaluator agrees with itself.
from_cicp_normalizes_the_matrix_coefficients_and_range_flag— over matrix coefficients 1, 5, 6and 9 × both range flags.
an_unencodable_grey_gamma_is_declined— asserts the encoding limit from both sides, becauseonly a value exactly at it separates
>=from>.every_builtin_space_is_buildable— what makesbuiltin'sOptiona type-level guard ratherthan a new burden.
unrepresentable_signalling_is_rejected— BT.709 leaves the rejection sweep (it now builds) andUnspecified (2) joins it, so the loop pins both code points the round-1 decision 10 claimed and
the third one it had left unpinned.
Issues filed
tags. The half of issue gamut-icc/gamut-color: built-in profile constructors + CICP -> profile #424's piece 2 this pull request declines. The round-1 body said
"pieces 1 and 2 land here" without naming it; the corrected statement is below.
near black. Recorded as a known limit in
STATUS.mdrather than changed silently.Issue (corrected)
Refs #424. Piece 1 lands except for Adobe RGB and ProPhoto RGB (blocked on #537). Piece 2lands only its constructor half —
from_cicp/from_source_profile; its transform-sideoption is #555. Piece 3 is declined per the issue's own instruction. The issue does not close.
Decisions appended in round 2
Unresolved review notes (round 2)
None. All eight review findings are repaired; the two pieces of work this pull request declines are
filed as #555 and #557, and #537 (round 1) remains open and unchanged.
No human approved this repair. This remains an unattended automated run; the record above is
what a human reads afterwards.
Addendum — head
f868da15A third commit followed the validation table above:
gray_with_gammais public andGAMMA_ENCODING_LIMITis not, socargo doc -p gamut-iccreported "public documentation forgray_with_gammalinks to private item GAMMA_ENCODING_LIMIT" and rendered no link at all. Themagnitude is now stated inline. No repository gate covers rustdoc warnings, so this was found by
running
cargo docdirectly against the diff.Every gate in the table was then re-run at
f868da15and passed again, unchanged in outcome:check-commits("no errors in 7 commits"),check-tests,check-release-deps,check-ffi-features,mise run lint(exit 0),mise run test(exit 0, 202test result: oklines),
mise run mutants-diff(83 mutants — 69 caught, 14 unviable, 0 missed), pluscargo doc -p gamut-icc --no-deps --all-featureswith 0 warnings andcargo test -p gamut-icc --all-features(161 + 19 + 7 + 6, 0 failed).Round 3 — repair of the round-2 review (branch
feat/424-icc-builtin-profiles, issue #424)Four commits at head
3d56d3d5, on top off868da15. Scope:crates/gamut-icc/src/builtin.rsand this body.
Corrections to entries above (the entries themselves are left verbatim)
gray_with_gammaalso declines a gammas15Fixed16cannothold". That entry bounded the value at 32 768 and called it the encoding limit. It was wrong
in both directions.
S15Fixed16::from_f64rounds before it clamps, so it also degenerates at thelow end: any gamma below
0.5 / 65 536 = 7.62939453125e-6is written as raw0, andY = X^0maps every input — black included — to white. Executed against the shipped code, gamma1e-6was accepted, serialized, and reported clean byvalidate(), with akTRCofParametricCurve { function_type: 0, params: [S15Fixed16(0)] }— precisely the profile thatentry gives as the reason for refusing a literal
0.0. The top bound was also in the wrongplace: saturation begins at
(2^31 − 0.5) / 65 536 = 32 767.999992370605468750, half a quantumabove the largest representable value, so gammas between that point and 32 768 were accepted and
silently written as
32 767.99998474121.carried". That entry documented both fields as ICC.1:2022 §10.3 conformance. §10.3 carries
one
shall—MatrixCoefficientsshall be 0 in an RGB or XYZ profile — and no requirement atall about
VideoFullRangeFlag: it says only that the flag "is often 1" for RGB, and its own RGBexamples list
1-1-0-0and9-16-0-0with the flag at zero. Writing 1 unconditionally isthis crate's normalisation, not conformance, and its consequence — the caller's flag is
discarded, not preserved — was not stated.
f868da15. That addendum reportedcargo doc -p gamut-icc --no-deps --all-featureswith 0 warnings. The rename it made in the same breath left a secondstale intra-doc link to
Trc::from_cicpon a private item. Both ends of that link are private, sorustdoc without
--document-private-itemsdoes not resolve it and reports the crate clean —demonstrated: with the stale link restored, the private-items run fails with
"error: unresolved link to
Trc::from_cicp" while the default run emits zero diagnostics.Decisions taken (round 3) — the record this lane was given, verbatim
Decisions appended by this lane
What each repair pins that nothing else did
HALF_QUANTUM.next_down()with the accepted profile'skTRCprinted asS15Fixed16(0)— 161other tests pass. The test reads each accepted gamma back out of the tag it was written into, so
a value that survives the guard but not the encoding cannot pass.
Trc::for_code_point(code point 4 →Trc::Gamma(2.2), acurve H.273 Table 3 defines differently), exactly one test fails —
no_unlisted_transfer_code_point_is_encodable— and the other 161 pass. No mutation of a matchexpression produces an extra arm, so
mutants-diffcannot generate that defect either.not.
32_767.999_992_370_605(accepted at.next_down(), refused at the value itself).Validation (round 3, at head
3d56d3d5)cargo test -p gamut-icc --all-features__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkmise run check-testsmise run check-commitsmise run lintclippy::excessive_precisionon the fully written saturation literal under-D warnings; repaired by decision 22 and re-run to exit 0mise run testtest result: FAILED, noerrorlinesmise run mutants-diffRUSTDOCFLAGS="-D warnings" cargo doc -p gamut-icc --all-features --no-deps --document-private-itemsmise run lint,mise run testandmise run mutants-diffeach ran inside aMemoryMax=16G, MemorySwapMax=0scope withCARGO_BUILD_JOBS=2.check-release-deps,check-ffi-featuresandcheck-ffi-headerwere not re-run: noCargo.tomland no publicC-surface type changed this round. Coverage was not re-run: no new module was added.
Issues filed (round 3)
alongside the literal inverse-OETF one
validate()does not check ICC.1:2022 §10.3's MatrixCoefficientsconstraint on a parsed
cicpTypetag2.8) as
parametricCurveTypetype 0gray_with_gammashould bound gamma colorimetricallyrather than by the
s15Fixed16encodingUnresolved review notes (round 3)
None. All four round-2 findings are repaired and both design questions are answered in the
documentation; the four questions the record defers are filed as #586–#589. #537 and #555 remain
open and unchanged.
No human approved this repair. This remains an unattended automated run; the record above is
what a human reads afterwards.
Round 4 — repair of the round-3 review (branch
feat/424-icc-builtin-profiles, issue #424)Three commits at head
1b45aa46, on top of3d56d3d5. Scope:crates/gamut-icc/in full —source,
STATUS.mdandREADME.mdalike — and this body.The lesson of this round, stated before the findings. Rounds 1–3 chased one class of defect:
a second statement of a fact, drifting from the arithmetic that owns it. Round 1 restated the
fact as a constant; round 2 removed the constant from one place and restated it in a doc comment;
round 3 deleted the constant and put the guard on the arithmetic, which genuinely eliminated the
class inside
builtin.rs. Both superseded statements survived one file away, inSTATUS.md,because the scope line named the file the arithmetic is in. A repair of this class is complete
only when it covers every statement of the fact wherever it lives. Every restatement found in
this crate is tabulated below with what was done to it.
Corrections to entries above (the entries themselves are left verbatim)
conformance", and decision 13 (round 2), "MatrixCoefficients and VideoFullRangeFlag are
normalized, not carried". Both rested on the premise that the two fields describe a
luma–chroma encoding the caller de-matrixes before the profile applies, so normalising the range
flag loses metadata but not colour. ICC.1:2022 §10.3's own RGB examples refute the premise for
the range flag:
1-1-0-0is listed as "RGB narrow range representation specified inRecommendation ITU-R BT.709-6, Item 3.4" with
MatrixCoefficientsalready zero — a narrowrange on the RGB samples themselves, which no de-matrixing removes. A caller holding genuine
narrow-range signalling was therefore handed a profile that renders its colour wrongly, not
one that merely dropped metadata.
from_cicpnow declines anyvideo_full_range_flagotherthan
1.crates/gamut-icc/STATUS.md.Round 3 moved the guard onto the encoding but left
STATUS.mdstating the superseded valuedomain — "non-finite, non-positive, or ≥ 32 768", citing the fixed-point width as the deciding
clause. Executed, that sentence was wrong in both directions: a value that is finite,
positive and below 32 768 is refused (the whole interval under
0.5 / 65 536), and the entirelow interval the guard refuses was admitted by it.
this module writes" but carried the value of the exact closed form, which is not what the
tag holds — the
parametricCurveTypeparameters are rounded tos15Fixed16first.Findings repaired, with evidence
STATUS.mdstated the superseded grey-gamma domain (value-bounded at 32 768, citing the fixed-point width). Wrong in both directions against the shipped guard.STATUS.mdnow states the encoding domain the arithmetic owns: accepted from0.5 / 65 536 = 7.629 394 531 25e-6up to but not including(2^31 − 0.5) / 65 536 = 32 767.999 992 370 605 468 75, with the reason each end is refused.STATUS.mdsaid of the two rewritten fields "Neither is a loss of information: both describe a luma–chroma encoding the caller de-matrixes before this profile applies", while the round-3 source said the flag is discarded, not preserved — two documents in one PR contradicting each other. The status claim is also false on §10.3's own terms.STATUS.md, the module docs,from_cicp's doc andREADME.md, and replaced by the §10.31-1-0-0reading in each.builtin.rs:244cited ICC.1:2022 §10.7 for the four-bytecicpTypelayout. §10.7 isdataType;cicpTypeis §10.3 — the very clause this round's subject was a misreading of.references/icc/icc.1-2022-05.pdftable of contents (§10.3cicpType, §10.7dataType). Every other citation in the module checks out.profileDescriptionTagwas written from the requested gamma while thekTRCwas written from the encoded one. At the smallest accepted gamma (0.5 / 65 536) the description read one number and the tag carried twice it (1 / 65 536).S15Fixed16;gray_with_gammashadows its argument with it, so the requested value is out of scope for the rest of the function and nothing below can be written from it.gray_with_gamma(2.2)is now described asGrey gamma 2.1999969482421875— the parameter a reader inspecting the tag finds.32 767.999 984 741 21) where its neighbour was written exact.32 767.999 984 741 210 937 5. The paragraph's divergence figures (1.371×, 0.0703, 0.0457, 21 %) are unchanged at their stated precision — recomputed from the rounded parameters.Every place each fact was restated, and what was done with it
src/builtin.rsmodule docs (# What a CICP triple contributes)1-1-0-0reading is givensrc/builtin.rsnormalized_cicpdoc + bodyrgb_conforming_cicp; it now carries only the §10.3shallsrc/builtin.rsIccProfile::from_cicpdoc + doctestsrc/builtin.rstestfrom_cicp_normalizes_the_matrix_coefficients_and_range_flagfrom_cicp_zeroes_the_matrix_coefficients; the range loop is gone, replaced bya_cicp_triple_that_is_not_full_range_is_declinedcrates/gamut-icc/STATUS.md"CICP fields the profile does not carry"crates/gamut-icc/README.mdexample + prose1itselfcicp_of(tag) andfrom_cicp(precondition)const FULL_RANGE: u8 = 1, read by both, so the tag and the precondition cannot driftsrc/builtin.rsgamma_is_encodabledocencodable_gamma; bound written exact; the two ends named for what they aresrc/builtin.rsgray_with_gammapublic docsrc/builtin.rstesta_grey_gamma_the_ktrc_cannot_carry_is_declineddoccrates/gamut-icc/STATUS.md"Grey gamma domain"crates/gamut-icc/STATUS.md"Built-in profiles" openingSearched crate-wide (
crates/gamut-icc/) pluscrates/gamut/src,crates/gamut-cli/src,docs/and the root
README.mdfor32 768/32 767/s15Fixed16/full[-_ ]range/gray_with_gamma/thethree table values/
§10.7. Nothing outsidecrates/gamut-icc/restates any of these facts;crates/gamut-icc/src/lib.rs's one-line summary ("Each returnsNonefor signalling nomatrix/TRC profile can describe") is more accurate after this round, not less, and was left
alone.
Behaviour change and how it is versioned
IccProfile::from_cicpnow returnsNonefor aCicpwhosevideo_full_range_flagis not1,where it previously built a full-range profile from it. That is a behaviour change a caller can
observe, and it is published as
fix(icc)!: …with aBREAKING CHANGE:footer naming exactlythat, so release-plz treats it as a major-level change rather than a patch. Two facts a reader
should have alongside it: the released
gamut-iccv1.0.0 contains none of these constructors(
builtin,gray_with_gamma,from_cicp,from_source_profileare all introduced by this pullrequest), so the surface being narrowed is the one this branch itself adds; and the branch already
carries a
!commit for this same API (6592e655), so the bump this footer implies is not a newone. It is written as breaking regardless, because under-signalling a narrowed domain is the
failure that costs a consumer, and over-signalling costs a version number.
Decisions taken (round 4) — the record this lane was given, verbatim
Decisions appended by this lane
Validation (round 4, at head
1b45aa46)cargo test -p gamut-icc --all-features__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmtthen… mise run fmt-checkmise run check-testsmise run check-commitsmise run lintcargo clippy --workspace --all-targets --all-features -- -D warnings, 0 diagnosticsmise run testtest result: oklines, noFAILEDand noerrorlinesmise run mutants-diffRUSTDOCFLAGS="-D warnings" cargo doc -p gamut-icc --all-features --no-deps --document-private-itemsmise run lint,mise run testandmise run mutants-diffeach ran inside aMemoryMax=16G, MemorySwapMax=0scope withCARGO_BUILD_JOBS=2.check-release-deps,check-ffi-featuresandcheck-ffi-headerwere not re-run: noCargo.tomland no publicC-surface type changed this round. Coverage was not re-run: no new module was added.
Issues filed (round 4)
None. Decisions 5 and 6 of the record direct this round to reference existing issues rather than
file: #549 (no gate compiles a README block or fails on a broken rustdoc link — the gate that
would have caught F-A and F-B), #561 (repository-wide authority order for a documented claim)
and #590 (crate doc summary lines their own source contradicts). None of the three was edited,
commented on or labelled.
Unresolved review notes (round 4)
None. All five round-3 findings (F-A, F-B, F-C, F-E, F-F) are repaired, and all six decisions in
the record are carried out. One factual note for the reviewer, offered rather than acted on: the
record describes the narrow-range decline as "a behaviour change on a published crate". The
published
gamut-iccv1.0.0 does not containfrom_cicp— it is introduced by this pull request— so the narrowed surface is this branch's own. The change is published as breaking anyway, for
the reason given under "Behaviour change and how it is versioned". #537 and #555 remain open and
unchanged.
No human approved this repair. This remains an unattended automated run; the record above is
what a human reads afterwards.
Round 5 — repair of the round-4 review (branch
feat/424-icc-builtin-profiles, issue #424)Ten commits on top of
1b45aa46. Scope:crates/gamut-icc/(source,STATUS.md,README.md),this body, and two new issues. Everything above this line is reproduced unchanged.
This round's deliverable is the enumeration, not the two corrections. Rounds 1–4 each closed the
class of "a fact restated somewhere that drifts from the thing that owns it" one step from where the
last round searched — round 3 closed it inside one file, round 4 across the crate's files, and the
round-4 review then found the drift one fact away, including inside the sentence round 4 wrote to
close it ("Every other citation in the module checks out" — itself an ungated second statement, and
false). So the unit of repair here is the fact, and the deliverable is the derived set of
restated facts, mechanically, in the two shapes that have produced defects: every
§X.Ycitation,and every doc sentence asserting what a function outside this crate returns or does. Both sets are
below with the commands that derive them, every member resolved, and the members that resolved
false named.
Shape 1 — every
§X.Ycitation, resolved against a clause index extracted from the vendored PDFResolution of all 63:
§10.3 cicpType,§7.2.16 PCS illuminant field,§9.2.17 cicpTag, …).§4Basic number types,§7Profile requirements,§8Required tags,§9Tag definitions,§10Tag type definitions — each matches its citing file's subject.ICC.1:2001-04 §6.5.17(src/mluc.rs), resolved against the vendored 2001 PDF:textDescriptionType. Correct, and the line names the edition.H.273 §8.2; every one of the six occurrences names H.273 on its own line, so none is ambiguous with ICC's§8.2 Common requirements.Members that resolved false — two, both in
src/builtin.rs, both corrected in5a9cc0c5:§9.2.10forrXYZ/gXYZ/bXYZBToD1Tag§9.2.46/§9.2.31/§9.2.4(red/green/blueMatrixColumnTag)§9.2.35forchadmetadataTag§9.2.15(chromaticAdaptationTag)Neither number exists for those tags in any edition — the 2001 edition was checked too. Which
numbering: the citations use the standard's heading numbering. ICC.1:2022 numbers two of the
same clauses differently where §8.4.3 cross-references them (9.2.44 for
redMatrixColumnTag, 9.2.30for
greenMatrixColumnTag) — an erratum in the published document, not two editions. That is statedat the citation site, so a reader who follows the cross-reference is not misled.
No gate exists for this, and none is wired here.
pdftotextis not provisioned bymise.toml(
grep -n 'pdftotext\|poppler' mise.toml→ no match), so a gate would add an unprovisioned systemdependency from a single-crate entry. Filed as #606 — No gate checks a §-citation against the
clause the vendored specification gives it, referencing #549, and cited in
builtin.rsbeside thederivation. Residual:
Table Nreferences (39 occurrences) are a sibling citation shape thisderivation does not cover; they are named in "Residual risk" below rather than silently included.
Shape 2 — every doc sentence asserting what a function outside this crate returns or does
67 documentation lines name a crate outside this one. Twelve of them assert what an external item
returns or does — the shape that can be false — and each is resolved below. (The rest state
provenance or architecture: "the primaries come from
ColourPrimaries::chromaticities", which thecompiler checks by the call itself.)
gamut-colorsupplies no EOTF for any of the four BT.709-family code pointseotf_for(Bt2020_10)isSome(bt2020_pq_to_sdr)a5c88739)V = 0.5a34093d1) — it was stated twice and gated nowhereTransferCharacteristicsmodels only two of the four (1 and 14)gamut-colorhas no BT.709-family curve at alltransfer.rsexposes linear/sRGB/Adobe/ProPhoto/PQ curves and no BT.709 oneeotf_for(Bt709).is_none()in the doctestgamut_color::transfer::srgb_eotfis a function, so its five parameters must be restatedsrgb_parametric_curve_matches_gamut_colorSourceProfile::{ADOBE_RGB, PROPHOTO_RGB}returnNonefrom both CICP accessorssource_profiles_map_onto_the_builtin_spacesgamut-colorgamut_chromaticitiesis private)gamut_color::matrix::D50is the CIE-published chromaticity,Z = 0.825105atY = 10.82521)d0e32113) and now pinned bythe_two_d50_tristimuli_the_doc_names_are_what_the_constants_hold(a34093d1)XyzNumber::D50by 2.0e-4 inZColourPrimaries::Unspecifiednames no chromaticitiescolorants_d50's degenerate-arm testgamut-color'spq_eotfsampled_pq_curve_matches_gamut_colorgamut-core,gamut-color,md-5thiserrorwas omitted in bothlib.rsandREADME.md4379973e); no gate — a completeness claim about a manifestOne further member resolved false in this round's own prose:
STATUS.mdclaimed the doctest "pinsevery clause of this paragraph that is a claim about gamut-color", while claim 2 was not in it. That
is the round-4 mistake exactly — an ungated completeness claim written while repairing ungated
completeness claims. It is replaced by the list of what the doctest actually asserts (
a34093d1).The round-4 review's findings, and what each got
builtin.rsandSTATUS.mdboth sayeotf_forsupplies no EOTF for the four codes; it supplies one for code 14eotf_for) is pre-existing ingamut-color, out of this manifest, and filed as #605, cited from the corrected doc comment.from_source_profile(BT2020)diverges from the bundle it names by up to 0.729, and nothing in the crate says soV ≈ 0.773, 52.3× atV = 0.1, against 4.2e-6 for the sRGB control (whose transfer is its code point, so only thes15Fixed16rounding separates them). The review's 0.729 / 4.2e-9 are the same comparison taken against the unquantized curve. Kept building it, and the reasoning is now infrom_source_profile's doc, inSTATUS.mdand beside the test that asserts the mapping, with the measured divergence.builtin.rsis the new moduleSTATUS.mdsays the colorimetry "is never restated here"; three published constants arebfc41a36).V ≈ 0.9956). The sweep now visits every interval at both ends and its midpoint, the tolerance is one quantum, and the measured 0.9854 (grid) / 0.9861 (200 001-point sweep) are published on the constant (ce65dd7c).Two answers the record directs rather than leaves open:
§9.2.17is now cited for the rule thatdecides the range flag — the CICP tag content "shall be equivalent to the data colour space encoding
represented by this ICC profile" makes the rejected option non-conforming outright rather than
inconsistent by argument — and the same clause is why
gray_with_gammawrites nocicpTypeat all(
288b2a74); and every constructor'sPerceptualrendering intent is disclosed, with a doctest(
9baf3997).Correction to the round-4 validation entry (decision 6 of this round's record)
The round-4 entry read "Coverage was not re-run: no new module was added." That is wrong on its own
terms:
crates/gamut-icc/src/builtin.rsis the module this pull request adds. The true statementis that coverage had not been run locally for it in any round. It is run in this round and reported
in the table below.
The breaking markers: the cost, recorded rather than rewritten (decision 9)
Two commits on this branch carry
!and aBREAKING CHANGE:footer (6592e655,5fb03d31). Bothdescribe churn internal to this unreleased branch:
crates/gamut-icc/src/builtin.rsdoes notexist on
origin/masterand no commit there touches it, sobuiltin,gray_with_gamma,from_cicpandfrom_source_profileare all introduced here and the publishedgamut-icc1.0.0exposes none of them. The markers are therefore conservative rather than required, and their cost is
a major bump:
gamut-icc1.0.0 → 2.0.0, which widens the requirement ingamut,gamut-cmmandgamut-metadata(the three workspace dependents) and for any external consumer pinned to1. Thisrun does not rewrite published history under any authority, so the footers stand as written; whether
to collapse them at merge is a human's decision, stated here so it can be taken with the cost in
view.
Validation (round 5, at head
a34093d1)cargo test -p gamut-icc --all-featurestests/oracle.rs+ 7tests/roundtrip.rs+ 8 doctests, 0 failed (163 lib before this round's last commit)__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkmise run check-testsmise run check-commitsconvco check: "no errors in 26 commits"mise run linttooling/,-D warningsmise run testtest result: oklines, noFAILEDand no error linemise run mutants-difforigin/master, 82 mutants: 67 caught, 15 unviable, 0 missedmise run coveragecrates/gamut-icc/src/builtin.rs, the module this pull request adds, is 98.43 % regions / 98.39 % lines / 94.23 % functions. First local coverage run for this branch in any roundRUSTDOCFLAGS="-D warnings" cargo doc -p gamut-icc --all-features --no-deps --document-private-itemslint,test,mutants-diffandcoverageeach ran inside aMemoryMax=16G, MemorySwapMax=0scope with
CARGO_BUILD_JOBS=2. The mutation base isorigin/master: this branch is not stacked onanother unmerged head.
check-release-deps,check-ffi-featuresandcheck-ffi-headerwere notre-run this round — no
Cargo.tomland no public C-surface type changed since round 4, where theylast passed.
Decisions appended by this lane (round 5)
Issues filed (round 5)
eotf_formaps H.273 transfer code point 14 to a PQ tone map, not theBT.709 curve Table 3 gives it. Carries the quoted Table 3 row and the executed divergence.
crates/gamut-coloris outside this lane's manifest and under other lanes' feet, so it is filedand cited from the corrected doc comment rather than fixed here.
References No gate compiles a README code block or fails on a broken rustdoc link #549. Cited beside the derivation in
builtin.rs.No existing issue was edited, commented on, labelled or closed. #537, #555, #557 and #586–#589
remain open and unchanged.
Unresolved review notes (round 5)
Residual risk, stated rather than closed.
Table Nreferences (39 occurrences in this crate) area citation shape the shape-1 derivation does not resolve — the index built here is of clause
headings, not table captions. Members 7 and 12 of the shape-2 set are resolved but ungated: a
visibility fact and a manifest completeness claim, neither of which has anything in this crate to
assert against. Both are named so the next reader starts from the set rather than from the last
defect.
No human approved this repair. This remains an unattended automated run; the record above is what
a human reads afterwards.