appthere_color/proofing.rs
1//! Soft proofing configuration and chained transform execution.
2//!
3//! Simulates how a document will appear when printed on a specific output
4//! device by chaining two transforms through an intermediate buffer.
5
6extern crate alloc;
7
8use alloc::boxed::Box;
9use alloc::vec;
10
11use crate::error::{ColorError, ColorResult};
12use crate::proofing_builder::ProofingConfigBuilder;
13use crate::rendering_intent::RenderingIntent;
14
15/// Gamut warning indicator for out-of-gamut pixels.
16///
17/// When soft proofing, pixels that fall outside the output device's gamut
18/// can be flagged with a warning color.
19///
20/// # Examples
21///
22/// ```
23/// use appthere_color::{GamutWarning, RgbColor};
24///
25/// let warning = GamutWarning::Replace(RgbColor::new(1.0, 0.0, 1.0));
26/// ```
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum GamutWarning {
29 /// No gamut warning — out-of-gamut colors pass through unchanged.
30 None,
31 /// Replace out-of-gamut pixels with the specified RGB warning color.
32 Replace(crate::color_value::RgbColor),
33}
34
35impl Default for GamutWarning {
36 /// Returns [`GamutWarning::None`] as the default.
37 fn default() -> Self {
38 GamutWarning::None
39 }
40}
41
42/// Configuration for soft proofing transforms.
43///
44/// Built via [`ProofingConfig::builder()`], this struct captures the profiles,
45/// intents, and optional gamut warning needed for a two-stage proofing pipeline.
46///
47/// # Examples
48///
49/// ```
50/// use appthere_color::{ProofingConfig, IccProfile, RenderingIntent};
51///
52/// let config = ProofingConfig::builder()
53/// .source(&IccProfile::new_srgb())
54/// .simulation(&IccProfile::new_srgb())
55/// .display(&IccProfile::new_srgb())
56/// .simulation_intent(RenderingIntent::Perceptual)
57/// .build()
58/// .unwrap();
59/// ```
60pub struct ProofingConfig {
61 sim_executor: Box<moxcms::TransformF32BitExecutor>,
62 disp_executor: Box<moxcms::TransformF32BitExecutor>,
63 gamut_warning: GamutWarning,
64 sim_channels: usize,
65 src_channels: usize,
66 dst_channels: usize,
67}
68
69impl ProofingConfig {
70 /// Creates a new [`ProofingConfigBuilder`].
71 ///
72 /// # Examples
73 ///
74 /// ```
75 /// use appthere_color::ProofingConfig;
76 ///
77 /// let builder = ProofingConfig::builder();
78 /// ```
79 pub fn builder() -> ProofingConfigBuilder {
80 ProofingConfigBuilder {
81 source: None,
82 simulation: None,
83 display: None,
84 simulation_intent: RenderingIntent::Perceptual,
85 display_intent: RenderingIntent::RelativeColorimetric,
86 gamut_warning: GamutWarning::None,
87 }
88 }
89
90 /// Internal constructor used by the builder.
91 pub(crate) fn new(
92 sim_executor: Box<moxcms::TransformF32BitExecutor>,
93 disp_executor: Box<moxcms::TransformF32BitExecutor>,
94 gamut_warning: GamutWarning,
95 sim_channels: usize,
96 src_channels: usize,
97 dst_channels: usize,
98 ) -> ColorResult<Self> {
99 Ok(Self {
100 sim_executor,
101 disp_executor,
102 gamut_warning,
103 sim_channels,
104 src_channels,
105 dst_channels,
106 })
107 }
108
109 /// Executes the two-stage proofing transform on pixel data.
110 ///
111 /// Transform 1: source → simulation profile (simulation_intent).
112 /// Transform 2: simulation → display profile (display_intent).
113 ///
114 /// # Errors
115 ///
116 /// Returns [`ColorError::TransformExecution`] if either stage fails.
117 ///
118 /// # Examples
119 ///
120 /// ```
121 /// use appthere_color::{ProofingConfig, IccProfile, RenderingIntent};
122 ///
123 /// let config = ProofingConfig::builder()
124 /// .source(&IccProfile::new_srgb())
125 /// .simulation(&IccProfile::new_srgb())
126 /// .display(&IccProfile::new_srgb())
127 /// .build()
128 /// .unwrap();
129 ///
130 /// let input = [0.5_f32, 0.3, 0.8];
131 /// let mut output = [0.0_f32; 3];
132 /// config.proof(&input, &mut output).unwrap();
133 /// ```
134 pub fn proof(&self, src: &[f32], dst: &mut [f32]) -> ColorResult<()> {
135 let mut intermediate = vec![0.0_f32; self.sim_channels];
136
137 self.sim_executor
138 .transform(src, &mut intermediate)
139 .map_err(|e| {
140 ColorError::TransformExecution(alloc::format!("{:?}", e))
141 })?;
142
143 self.disp_executor
144 .transform(&intermediate, dst)
145 .map_err(|e| {
146 ColorError::TransformExecution(alloc::format!("{:?}", e))
147 })?;
148
149 Ok(())
150 }
151
152 /// Returns the gamut warning mode configured for this proofing config.
153 ///
154 /// # Examples
155 ///
156 /// ```
157 /// use appthere_color::{ProofingConfig, IccProfile, GamutWarning};
158 ///
159 /// let config = ProofingConfig::builder()
160 /// .source(&IccProfile::new_srgb())
161 /// .simulation(&IccProfile::new_srgb())
162 /// .display(&IccProfile::new_srgb())
163 /// .build()
164 /// .unwrap();
165 /// assert_eq!(config.gamut_warning(), GamutWarning::None);
166 /// ```
167 pub fn gamut_warning(&self) -> GamutWarning {
168 self.gamut_warning
169 }
170
171 /// Returns the number of channels in the source color space.
172 ///
173 /// # Examples
174 ///
175 /// ```
176 /// use appthere_color::{ProofingConfig, IccProfile};
177 ///
178 /// let config = ProofingConfig::builder()
179 /// .source(&IccProfile::new_srgb())
180 /// .simulation(&IccProfile::new_srgb())
181 /// .display(&IccProfile::new_srgb())
182 /// .build()
183 /// .unwrap();
184 /// assert_eq!(config.source_channels(), 3);
185 /// ```
186 pub fn source_channels(&self) -> usize {
187 self.src_channels
188 }
189
190 /// Returns the number of channels in the display color space.
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// use appthere_color::{ProofingConfig, IccProfile};
196 ///
197 /// let config = ProofingConfig::builder()
198 /// .source(&IccProfile::new_srgb())
199 /// .simulation(&IccProfile::new_srgb())
200 /// .display(&IccProfile::new_srgb())
201 /// .build()
202 /// .unwrap();
203 /// assert_eq!(config.destination_channels(), 3);
204 /// ```
205 pub fn destination_channels(&self) -> usize {
206 self.dst_channels
207 }
208}