CICP↔ICC bridge: general Cicp::resolve_matrix (MC=2/12/13) + Cicp↔moxcms CicpProfile wiring + cicp-tag round-trip
- Dominant language
- Rust
- Stars
- 1
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Split out of #34 (the HDR-IQA arc) as the **genuinely-general, non-IQA** piece. A 4-reviewer adversarial pass on the deferred PR #35 concluded the IQA-specific items (PU encoding, peak/reference luminance) belong in **zensim**, not the decade-frozen zenpixels surface — see #34. What's left that's general color-signaling, and worth building *when a real codec consumer exists* (YAGNI — do not add to frozen zenpixels API speculatively), is the H.273 matrix resolution + the `Cicp`↔ICC bridge.
Research memory grounding this: `reference_cicp_mc2_icc_bridge` (authoritative sources: ITU-T H.273 V4, ICC.1:2022 + ICC.2 cicp amendment, W3C PNG 3e, libavif, av1-avif#84).
## Spec corrections vs #34 / PR #35
- **The claim "NEVER auto-derives BT.2020-NCL from CP=9 (the silent-failure mode H.273 warns against)" is INVERTED.** CP=9 + MC=9 (BT.2020 primaries + BT.2020-NCL) is the *canonical correct* pair. The real constraints are: (a) MC=2 / reserved (3, 15–255) are **Unspecified** → need external info; (b) **MC=12/13 are chromaticity-derived from `color_primaries`** → cannot be copied across a different CP.
- MC=0 = **Identity** (planes already RGB; nothing to resolve).
## Trickle-down to ICC (why this is pre-ICC, not an ICC transform)
Decode order: `YCbCr → [MC] → RGB → [CP+TC | ICC] → PCS`. **MC is applied first; ICC profiles are RGB→PCS and have no YCbCr-matrix equivalent.** Precedence (libavif, de-facto): when both ICC + CICP present, **ICC overrides CP+TC but NOT MC** — so MC=2 must still be resolved even with an ICC profile present. ICC.1:2022's `cicp` tag (bytes 8/9/10/11 = CP/TC/MC/VideoFullRangeFlag) carries the 4 bytes **verbatim as metadata**; it does not make the ICC transform consume MC.
## The CMS side already exists — gap is the zen-side wiring
moxcms already reads **and writes** the ICC `cicp` tag: `CicpProfile` (`profile.rs:791`), `ColorProfile::new_from_cicp` (`defaults.rs:248`), `update_rgb_colorimetry_from_cicp` (`profile.rs:1572`), cicp-tag writer (`writer.rs:280`) + parser (`tag.rs`, `read_cicp_tag`). So this is **wiring zenpixels `Cicp` ↔ moxcms `CicpProfile`**, not building ICC cicp support from scratch.
## Hardened design (when a codec consumer lands, same PR)
```rust
// zenpixels Cicp — first-class (NOT #[doc(hidden)]), de-IQA'd docs
pub const fn resolve_matrix(self, hint_mc: Option) -> Result;
// MC==0 (identity) or a valid self-contained recipe (1/5/6/9/…) -> Ok(self)
// MC==12|13 -> derive from self.color_primaries (Err if CP unspecified); never blind-copy
// MC==2 or reserved -> apply a valid hint, else Err
#[non_exhaustive] // not Copy/Hash/serde
pub struct UnspecifiedMatrixError { /* private */ }
impl UnspecifiedMatrixError { pub fn color_primaries(&self)->u8; pub fn transfer_characteristics(&self)->u8; }
```
(`Option` hint, not `Option` — only the MC byte is consumed; leaves room for a future `resolve_unspecified(container)` that also handles CP=2/TC=2.)
## Checklist
- [ ] `Cicp::resolve_matrix` (hardened above) — land **only** alongside a real codec consumer (e.g. wire zenavif/zenjxl/zenpng decode, which today resolve MC ad-hoc: zenavif `_ => Bt601`).
- [ ] `Cicp` ↔ moxcms `CicpProfile` conversion (likely in `zencodec::icc`, which already deps moxcms) — round-trip via `new_from_cicp` + cicp tag; ICC→CICP cannot recover MC (emit MC=0 identity or MC=2 unspecified).
- [ ] Decode precedence honored: apply MC before ICC; ICC overrides CP+TC not MC (cross-ref #11-style decoder audits).
- [ ] Conformance tests: PNG cICP ⇒ MC must=0; ST 2086 max_luminance /10000 descale invariant; Cicp↔ICC cicp-tag round-trip.
No PU / luminance here — those move to zensim per #34 rescope.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.