zenpixels_convert/cms.rs
1//! Color Management System (CMS) traits.
2//!
3//! Defines the interface for ICC profile-based color transforms. When a CMS
4//! feature is enabled (e.g., `cms-moxcms`, `cms-lcms2`), the implementation
5//! provides ICC-to-ICC transforms. Named profile conversions (sRGB, P3,
6//! BT.2020) use hardcoded matrices and don't require a CMS.
7//!
8//! # When codecs need a CMS
9//!
10//! Most codecs don't need to interact with the CMS directly.
11//! [`finalize_for_output`](super::finalize_for_output) handles CMS transforms
12//! internally when the [`OutputProfile`](super::OutputProfile) requires one.
13//!
14//! A codec needs CMS awareness only when:
15//!
16//! - **Decoding** an image with an embedded ICC profile that doesn't match
17//! any known CICP combination. The decoder extracts the ICC bytes and
18//! stores them on [`ColorContext`](crate::ColorContext). The CMS is used
19//! later (at encode or processing time), not during decode.
20//!
21//! - **Encoding** with `OutputProfile::Icc(custom_profile)`. The CMS builds
22//! a source→destination transform, which `finalize_for_output` applies
23//! row-by-row via [`RowTransform`].
24//!
25//! # Implementing a CMS backend
26//!
27//! To add a new CMS backend (e.g., wrapping Little CMS 2):
28//!
29//! 1. Implement [`ColorManagement`] on your backend struct.
30//! 2. `build_transform` should parse both ICC profiles, create an internal
31//! transform object, and return it as `Box<dyn RowTransform>`.
32//! 3. `identify_profile` should check if an ICC profile matches a known
33//! standard (sRGB, Display P3, etc.) and return the corresponding
34//! [`Cicp`](crate::Cicp). This enables the fast path: if both source
35//! and destination are known profiles, hardcoded matrices are used
36//! instead of the CMS.
37//! 4. Feature-gate your implementation behind a cargo feature
38//! (e.g., `cms-lcms2`).
39//!
40//! ```rust,ignore
41//! struct MyLcms2;
42//!
43//! impl ColorManagement for MyLcms2 {
44//! type Error = lcms2::Error;
45//!
46//! fn build_transform(
47//! &self,
48//! src_icc: &[u8],
49//! dst_icc: &[u8],
50//! ) -> Result<Box<dyn RowTransform>, Self::Error> {
51//! let src = lcms2::Profile::new_icc(src_icc)?;
52//! let dst = lcms2::Profile::new_icc(dst_icc)?;
53//! let xform = lcms2::Transform::new(&src, &dst, ...)?;
54//! Ok(Box::new(Lcms2RowTransform(xform)))
55//! }
56//!
57//! fn identify_profile(&self, icc: &[u8]) -> Option<Cicp> {
58//! // Fast: check MD5 hash against known profiles
59//! // Slow: parse TRC+matrix, compare within tolerance
60//! None
61//! }
62//! }
63//! ```
64//!
65//! # No-op CMS
66//!
67//! Codecs that don't need ICC support can provide a no-op CMS whose
68//! `build_transform` always returns an error. This satisfies the type
69//! system while making it clear that ICC transforms are unsupported.
70
71use crate::PixelFormat;
72use alloc::boxed::Box;
73
74/// Row-level color transform produced by a [`ColorManagement`] implementation.
75///
76/// Applies an ICC-to-ICC color conversion to a row of pixel data.
77pub trait RowTransform: Send {
78 /// Transform one row of pixels from source to destination color space.
79 ///
80 /// `src` and `dst` may be different lengths if the transform changes
81 /// the pixel format (e.g., CMYK to RGB). `width` is the number of
82 /// pixels, not bytes.
83 fn transform_row(&self, src: &[u8], dst: &mut [u8], width: u32);
84}
85
86/// Color management system interface.
87///
88/// Abstracts over CMS backends (moxcms, lcms2, etc.) to provide
89/// ICC profile transforms and profile identification.
90///
91/// # Feature-gated
92///
93/// The trait is always available for trait bounds and generic code.
94/// Concrete implementations are provided by feature-gated modules
95/// (e.g., `cms-moxcms`).
96pub trait ColorManagement {
97 /// Error type for CMS operations.
98 type Error: core::fmt::Debug;
99
100 /// Build a row-level transform between two ICC profiles.
101 ///
102 /// Returns a [`RowTransform`] that converts pixel rows from the
103 /// source profile's color space to the destination profile's.
104 ///
105 /// This method assumes u8 RGB pixel data. For format-aware transforms
106 /// that match the actual source/destination bit depth and layout, use
107 /// [`build_transform_for_format`](Self::build_transform_for_format).
108 fn build_transform(
109 &self,
110 src_icc: &[u8],
111 dst_icc: &[u8],
112 ) -> Result<Box<dyn RowTransform>, Self::Error>;
113
114 /// Build a format-aware row-level transform between two ICC profiles.
115 ///
116 /// Like [`build_transform`](Self::build_transform), but the CMS backend
117 /// can use the pixel format information to create a transform at the
118 /// native bit depth (u8, u16, or f32) and layout (RGB, RGBA, Gray, etc.),
119 /// avoiding unnecessary depth conversions.
120 ///
121 /// The default implementation ignores the format parameters and delegates
122 /// to [`build_transform`](Self::build_transform).
123 fn build_transform_for_format(
124 &self,
125 src_icc: &[u8],
126 dst_icc: &[u8],
127 src_format: PixelFormat,
128 dst_format: PixelFormat,
129 ) -> Result<Box<dyn RowTransform>, Self::Error> {
130 let _ = (src_format, dst_format);
131 self.build_transform(src_icc, dst_icc)
132 }
133
134 /// Identify whether an ICC profile matches a known CICP combination.
135 ///
136 /// Two-tier matching:
137 /// 1. Hash table of known ICC byte sequences for instant lookup.
138 /// 2. Semantic comparison: parse matrix + TRC, compare against known
139 /// values within tolerance.
140 ///
141 /// Returns `Some(cicp)` if the profile matches a standard combination,
142 /// `None` if the profile is custom.
143 fn identify_profile(&self, icc: &[u8]) -> Option<crate::Cicp>;
144}