Skip to main content

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}