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 alloc::boxed::Box;
72
73/// Row-level color transform produced by a [`ColorManagement`] implementation.
74///
75/// Applies an ICC-to-ICC color conversion to a row of pixel data.
76pub trait RowTransform {
77    /// Transform one row of pixels from source to destination color space.
78    ///
79    /// `src` and `dst` may be different lengths if the transform changes
80    /// the pixel format (e.g., CMYK to RGB). `width` is the number of
81    /// pixels, not bytes.
82    fn transform_row(&self, src: &[u8], dst: &mut [u8], width: u32);
83}
84
85/// Color management system interface.
86///
87/// Abstracts over CMS backends (moxcms, lcms2, etc.) to provide
88/// ICC profile transforms and profile identification.
89///
90/// # Feature-gated
91///
92/// The trait is always available for trait bounds and generic code.
93/// Concrete implementations are provided by feature-gated modules
94/// (e.g., `cms-moxcms`).
95pub trait ColorManagement {
96    /// Error type for CMS operations.
97    type Error: core::fmt::Debug;
98
99    /// Build a row-level transform between two ICC profiles.
100    ///
101    /// Returns a [`RowTransform`] that converts pixel rows from the
102    /// source profile's color space to the destination profile's.
103    fn build_transform(
104        &self,
105        src_icc: &[u8],
106        dst_icc: &[u8],
107    ) -> Result<Box<dyn RowTransform>, Self::Error>;
108
109    /// Identify whether an ICC profile matches a known CICP combination.
110    ///
111    /// Two-tier matching:
112    /// 1. Hash table of known ICC byte sequences for instant lookup.
113    /// 2. Semantic comparison: parse matrix + TRC, compare against known
114    ///    values within tolerance.
115    ///
116    /// Returns `Some(cicp)` if the profile matches a standard combination,
117    /// `None` if the profile is custom.
118    fn identify_profile(&self, icc: &[u8]) -> Option<crate::Cicp>;
119}