Skip to main content

appthere_color/
policy.rs

1//! Document-level color management policy.
2//!
3//! Provides [`ColorPolicy`] and [`OutputIntent`] which represent format-agnostic
4//! document-level color management configuration. This crate provides the data
5//! model only — format-specific serialization belongs in the consuming crate.
6
7extern crate alloc;
8use alloc::collections::BTreeMap;
9use alloc::string::String;
10
11use crate::profile::IccProfile;
12use crate::rendering_intent::RenderingIntent;
13
14/// Describes the intended output condition for a document.
15///
16/// Maps conceptually to PDF/X `/OutputIntents`, ODF `draw:color-profile`, or
17/// embedded ICC profiles in EPUB image resources.
18///
19/// # Examples
20///
21/// ```
22/// use appthere_color::{OutputIntent, IccProfile};
23///
24/// let intent = OutputIntent::new(
25///     "FOGRA39".to_string(),
26///     IccProfile::new_srgb(),
27/// );
28/// assert_eq!(intent.name(), "FOGRA39");
29/// ```
30#[derive(Debug, Clone)]
31pub struct OutputIntent {
32    name: String,
33    profile: IccProfile,
34    condition: Option<String>,
35}
36
37impl OutputIntent {
38    /// Creates a new output intent with the given name and profile.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use appthere_color::{OutputIntent, IccProfile};
44    ///
45    /// let oi = OutputIntent::new("ISOcoated_v2".to_string(), IccProfile::new_srgb());
46    /// ```
47    pub fn new(name: String, profile: IccProfile) -> Self {
48        Self {
49            name,
50            profile,
51            condition: None,
52        }
53    }
54
55    /// Sets the output condition identifier (e.g. a registered condition name).
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use appthere_color::{OutputIntent, IccProfile};
61    ///
62    /// let oi = OutputIntent::new("FOGRA39".to_string(), IccProfile::new_srgb())
63    ///     .with_condition("FOGRA39L".to_string());
64    /// assert_eq!(oi.condition(), Some("FOGRA39L"));
65    /// ```
66    pub fn with_condition(mut self, condition: String) -> Self {
67        self.condition = Some(condition);
68        self
69    }
70
71    /// Returns the output intent name.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use appthere_color::{OutputIntent, IccProfile};
77    ///
78    /// let oi = OutputIntent::new("test".to_string(), IccProfile::new_srgb());
79    /// assert_eq!(oi.name(), "test");
80    /// ```
81    pub fn name(&self) -> &str {
82        &self.name
83    }
84
85    /// Returns a reference to the output intent's ICC profile.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use appthere_color::{OutputIntent, IccProfile};
91    ///
92    /// let oi = OutputIntent::new("sRGB".to_string(), IccProfile::new_srgb());
93    /// let _profile = oi.profile();
94    /// ```
95    pub fn profile(&self) -> &IccProfile {
96        &self.profile
97    }
98
99    /// Returns the output condition string, if set.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use appthere_color::{OutputIntent, IccProfile};
105    ///
106    /// let oi = OutputIntent::new("test".to_string(), IccProfile::new_srgb());
107    /// assert_eq!(oi.condition(), None);
108    /// ```
109    pub fn condition(&self) -> Option<&str> {
110        self.condition.as_deref()
111    }
112}
113
114/// Document-level color management policy.
115///
116/// Captures the output intent, fallback profile, default rendering intent,
117/// and a registry of named embedded profiles. This is a format-agnostic
118/// representation — serialization to PDF/X, ODF, or EPUB is handled by the
119/// consuming crate.
120///
121/// # Examples
122///
123/// ```
124/// use appthere_color::{ColorPolicy, OutputIntent, IccProfile, RenderingIntent};
125///
126/// let policy = ColorPolicy::builder()
127///     .output_intent(OutputIntent::new(
128///         "sRGB".to_string(),
129///         IccProfile::new_srgb(),
130///     ))
131///     .default_intent(RenderingIntent::RelativeColorimetric)
132///     .build();
133/// ```
134#[derive(Debug, Clone)]
135pub struct ColorPolicy {
136    output_intent: Option<OutputIntent>,
137    fallback_profile: Option<IccProfile>,
138    default_intent: RenderingIntent,
139    embedded_profiles: BTreeMap<String, IccProfile>,
140}
141
142impl ColorPolicy {
143    /// Creates a new [`ColorPolicyBuilder`].
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use appthere_color::ColorPolicy;
149    ///
150    /// let builder = ColorPolicy::builder();
151    /// ```
152    pub fn builder() -> ColorPolicyBuilder {
153        ColorPolicyBuilder {
154            output_intent: None,
155            fallback_profile: None,
156            default_intent: RenderingIntent::Perceptual,
157            embedded_profiles: BTreeMap::new(),
158        }
159    }
160
161    /// Returns the output intent, if set.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use appthere_color::ColorPolicy;
167    ///
168    /// let policy = ColorPolicy::builder().build();
169    /// assert!(policy.output_intent().is_none());
170    /// ```
171    pub fn output_intent(&self) -> Option<&OutputIntent> {
172        self.output_intent.as_ref()
173    }
174
175    /// Returns the fallback profile, if set.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use appthere_color::ColorPolicy;
181    ///
182    /// let policy = ColorPolicy::builder().build();
183    /// assert!(policy.fallback_profile().is_none());
184    /// ```
185    pub fn fallback_profile(&self) -> Option<&IccProfile> {
186        self.fallback_profile.as_ref()
187    }
188
189    /// Returns the default rendering intent for this policy.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use appthere_color::{ColorPolicy, RenderingIntent};
195    ///
196    /// let policy = ColorPolicy::builder().build();
197    /// assert_eq!(policy.default_intent(), RenderingIntent::Perceptual);
198    /// ```
199    pub fn default_intent(&self) -> RenderingIntent {
200        self.default_intent
201    }
202
203    /// Looks up a named embedded profile in the registry.
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use appthere_color::{ColorPolicy, IccProfile};
209    ///
210    /// let policy = ColorPolicy::builder()
211    ///     .embed_profile("sRGB".to_string(), IccProfile::new_srgb())
212    ///     .build();
213    /// assert!(policy.get_profile("sRGB").is_some());
214    /// assert!(policy.get_profile("AdobeRGB").is_none());
215    /// ```
216    pub fn get_profile(&self, name: &str) -> Option<&IccProfile> {
217        self.embedded_profiles.get(name)
218    }
219}
220
221/// Builder for [`ColorPolicy`].
222///
223/// Created via [`ColorPolicy::builder()`].
224#[derive(Debug)]
225pub struct ColorPolicyBuilder {
226    output_intent: Option<OutputIntent>,
227    fallback_profile: Option<IccProfile>,
228    default_intent: RenderingIntent,
229    embedded_profiles: BTreeMap<String, IccProfile>,
230}
231
232impl ColorPolicyBuilder {
233    /// Sets the output intent.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use appthere_color::{ColorPolicy, OutputIntent, IccProfile};
239    ///
240    /// let builder = ColorPolicy::builder()
241    ///     .output_intent(OutputIntent::new("sRGB".into(), IccProfile::new_srgb()));
242    /// ```
243    pub fn output_intent(mut self, intent: OutputIntent) -> Self {
244        self.output_intent = Some(intent);
245        self
246    }
247
248    /// Sets the fallback profile.
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// use appthere_color::{ColorPolicy, IccProfile};
254    ///
255    /// let builder = ColorPolicy::builder()
256    ///     .fallback_profile(IccProfile::new_srgb());
257    /// ```
258    pub fn fallback_profile(mut self, profile: IccProfile) -> Self {
259        self.fallback_profile = Some(profile);
260        self
261    }
262
263    /// Sets the default rendering intent.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use appthere_color::{ColorPolicy, RenderingIntent};
269    ///
270    /// let builder = ColorPolicy::builder()
271    ///     .default_intent(RenderingIntent::Saturation);
272    /// ```
273    pub fn default_intent(mut self, intent: RenderingIntent) -> Self {
274        self.default_intent = intent;
275        self
276    }
277
278    /// Registers a named embedded profile.
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use appthere_color::{ColorPolicy, IccProfile};
284    ///
285    /// let builder = ColorPolicy::builder()
286    ///     .embed_profile("logo".to_string(), IccProfile::new_srgb());
287    /// ```
288    pub fn embed_profile(mut self, name: String, profile: IccProfile) -> Self {
289        self.embedded_profiles.insert(name, profile);
290        self
291    }
292
293    /// Builds the [`ColorPolicy`].
294    ///
295    /// # Examples
296    ///
297    /// ```
298    /// use appthere_color::ColorPolicy;
299    ///
300    /// let policy = ColorPolicy::builder().build();
301    /// ```
302    pub fn build(self) -> ColorPolicy {
303        ColorPolicy {
304            output_intent: self.output_intent,
305            fallback_profile: self.fallback_profile,
306            default_intent: self.default_intent,
307            embedded_profiles: self.embedded_profiles,
308        }
309    }
310}