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}