Skip to main content

pdfrum_page/color/
special.rs

1//! `Separation`, `DeviceN` and `Pattern` — the three families whose
2//! components are not colour (ISO 32000-1 §8.6.6.4, §8.6.6.5, §8.7.3).
3//!
4//! `Separation` and `DeviceN` differ in strictness in a way worth stating
5//! once, because it looks like an inconsistency and is not:
6//!
7//! | | `Separation` | `DeviceN` |
8//! |---|---|---|
9//! | tint transform | optional; a failed load leaves the space usable | **mandatory**; a failed load fails the space |
10//! | too few outputs | drops the function, keeps the space | fails the space |
11//! | components | always **1** | `len(/Names)`, uncapped |
12//! | no function | broadcasts the tint into every alternate component | unreachable |
13//!
14//! `/None` short-circuits `Separation` before any of that and paints nothing.
15//! `/All` gets **no** special handling at all — the string never appears in
16//! the C++ — so it loads as an ordinary named colorant, diverging from
17//! ISO 32000-1 on purpose.
18
19use super::{ColorSpace, Rgb};
20use crate::function::Function;
21use std::sync::Arc;
22
23/// The scratch floor both families size their output buffer to, because an
24/// alternate space may read past the tint transform's declared output count.
25const SCRATCH_FLOOR: usize = 16;
26
27/// The largest number of components a pattern colour may carry.
28pub const MAX_PATTERN_COMPONENTS: usize = 16;
29
30/// A `Separation` colorspace: one tint driving an alternate space.
31#[derive(Debug, Clone, PartialEq)]
32pub struct Separation {
33    /// Set when the colorant is `/None`, which paints nothing at all.
34    pub none: bool,
35    /// The alternate space. Absent only for `/None`.
36    pub alternate: Option<Box<ColorSpace>>,
37    /// The tint transform, shared because one function may back several
38    /// spaces. Absent when it failed to load or produced too few outputs —
39    /// which is not fatal here.
40    pub tint: Option<Arc<Function>>,
41}
42
43impl Separation {
44    /// Convert one tint value.
45    #[must_use]
46    pub fn to_rgb(&self, comps: &[f32]) -> Option<Rgb> {
47        if self.none {
48            return None;
49        }
50        let alternate = self.alternate.as_ref()?;
51        let tint = comps.first().copied().unwrap_or(0.0);
52        let Some(func) = &self.tint else {
53            // With no transform the single tint is broadcast into every
54            // alternate component — a crude but deliberate fallback.
55            let broadcast = vec![tint; alternate.n_components()];
56            return Some(alternate.to_rgb(&broadcast));
57        };
58        let mut results = vec![0.0f32; func.output_count().max(SCRATCH_FLOOR)];
59        let produced = func.eval_into(&[tint], &mut results);
60        if produced == 0 {
61            return None;
62        }
63        Some(alternate.to_rgb(&results))
64    }
65}
66
67/// A `DeviceN` colorspace: several colorants driving an alternate space.
68#[derive(Debug, Clone, PartialEq)]
69pub struct DeviceN {
70    /// The colorant names. Its length is the component count.
71    pub names: Box<[pdfrum_object::Name]>,
72    /// The alternate space, which is mandatory here.
73    pub alternate: Box<ColorSpace>,
74    /// The tint transform, mandatory here.
75    pub tint: Arc<Function>,
76}
77
78impl DeviceN {
79    /// Convert a colorant vector.
80    #[must_use]
81    pub fn to_rgb(&self, comps: &[f32]) -> Option<Rgb> {
82        let n = self.names.len();
83        let inputs: Vec<f32> = (0..n)
84            .map(|i| comps.get(i).copied().unwrap_or(0.0))
85            .collect();
86        let mut results = vec![0.0f32; self.tint.output_count().max(SCRATCH_FLOOR)];
87        let produced = self.tint.eval_into(&inputs, &mut results);
88        if produced == 0 {
89            return None;
90        }
91        Some(self.alternate.to_rgb(&results))
92    }
93}
94
95/// A `Pattern` colorspace: a marker with, for uncoloured patterns, the space
96/// its `scn` operands live in.
97///
98/// The base is genuinely optional: a `[/Pattern <unloadable>]` array loads
99/// **successfully** as a one-component pattern space with no base, which is
100/// the uncoloured-pattern case.
101#[derive(Debug, Clone, PartialEq, Default)]
102pub struct PatternSpace {
103    /// The underlying space for an uncoloured pattern's colour operands.
104    pub base: Option<Box<ColorSpace>>,
105}
106
107impl PatternSpace {
108    /// The component count: one for a bare `/Pattern`, one more than the
109    /// base's otherwise.
110    #[must_use]
111    pub fn n_components(&self) -> usize {
112        self.base.as_ref().map_or(1, |b| b.n_components() + 1)
113    }
114
115    /// Resolve an uncoloured pattern's colour from its operands.
116    ///
117    /// The base sees the **whole** operand array regardless of its own
118    /// component count, matching `GetPatternRGB`.
119    #[must_use]
120    pub fn to_rgb(&self, comps: &[f32]) -> Option<Rgb> {
121        let base = self.base.as_ref()?;
122        Some(base.to_rgb(comps))
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    // Test fixtures quote the oracle's own vectors, compare floats exactly
129    // where the behaviour being pinned is exact, and index arrays whose
130    // length the fixture itself fixes.
131    #![allow(
132        clippy::unreadable_literal,
133        clippy::float_cmp,
134        clippy::indexing_slicing,
135        clippy::cast_precision_loss,
136        clippy::cast_possible_truncation,
137        reason = "test fixtures quote oracle vectors verbatim and compare exactly"
138    )]
139
140    use super::{DeviceN, PatternSpace, Separation};
141    use crate::color::ColorSpace;
142    use crate::function::{Exponential, Function};
143    use std::sync::Arc;
144
145    /// A tint transform mapping one input to three outputs, `t -> (t, t, t)`.
146    fn ramp3() -> Arc<Function> {
147        Arc::new(Function::Exponential(Exponential {
148            domain: Box::from(&[0.0f32, 1.0][..]),
149            range: Box::from(&[0.0f32, 1.0, 0.0, 1.0, 0.0, 1.0][..]),
150            c0: Box::from(&[0.0f32, 0.0, 0.0][..]),
151            c1: Box::from(&[1.0f32, 1.0, 1.0][..]),
152            exponent: 1.0,
153            orig_outputs: 3,
154            outputs: 3,
155        }))
156    }
157
158    #[test]
159    fn none_separations_paint_nothing() {
160        let sep = Separation {
161            none: true,
162            alternate: None,
163            tint: None,
164        };
165        assert!(sep.to_rgb(&[1.0]).is_none());
166    }
167
168    #[test]
169    fn a_separation_without_a_transform_broadcasts_its_tint() {
170        let sep = Separation {
171            none: false,
172            alternate: Some(Box::new(ColorSpace::DeviceRgb)),
173            tint: None,
174        };
175        let rgb = sep.to_rgb(&[0.25]).expect("colour");
176        assert!((rgb.r - 0.25).abs() < 1e-6);
177        assert!((rgb.g - 0.25).abs() < 1e-6);
178        assert!((rgb.b - 0.25).abs() < 1e-6);
179    }
180
181    #[test]
182    fn a_separation_with_a_transform_uses_it() {
183        let sep = Separation {
184            none: false,
185            alternate: Some(Box::new(ColorSpace::DeviceRgb)),
186            tint: Some(ramp3()),
187        };
188        let rgb = sep.to_rgb(&[0.5]).expect("colour");
189        assert!((rgb.r - 0.5).abs() < 1e-5);
190    }
191
192    #[test]
193    fn device_n_reads_exactly_its_name_count() {
194        let cs = DeviceN {
195            names: Box::from(&[pdfrum_object::Name::from("A")][..]),
196            alternate: Box::new(ColorSpace::DeviceRgb),
197            tint: ramp3(),
198        };
199        assert_eq!(cs.names.len(), 1);
200        // Surplus operands are ignored, missing ones read as zero.
201        let a = cs.to_rgb(&[0.5, 9.0, 9.0]).expect("colour");
202        let b = cs.to_rgb(&[0.5]).expect("colour");
203        assert!((a.r - b.r).abs() < 1e-6);
204    }
205
206    #[test]
207    fn a_pattern_space_without_a_base_has_one_component_and_no_colour() {
208        let cs = PatternSpace::default();
209        assert_eq!(cs.n_components(), 1);
210        assert!(cs.to_rgb(&[0.5]).is_none());
211    }
212
213    #[test]
214    fn a_pattern_space_with_a_base_adds_one_component() {
215        let cs = PatternSpace {
216            base: Some(Box::new(ColorSpace::DeviceCmyk)),
217        };
218        assert_eq!(cs.n_components(), 5);
219        assert!(cs.to_rgb(&[0.0, 0.0, 0.0, 0.0]).is_some());
220    }
221}