Skip to main content

pdfrum_page/color/
value.rs

1//! A colour as the graphics state holds it: a space plus its components, or
2//! a pattern (ISO 32000-1 §8.6.8).
3//!
4//! The rule that surprises people is in [`ColorValue::set_components`]:
5//! **too few operands change nothing at all.** `1 2 sc` in a four-component
6//! space leaves the previous colour standing rather than filling in zeros.
7//! Combined with the fact that installing a colorspace *resets* the colour to
8//! that space's default, this is why `cs` followed by an under-specified `sc`
9//! paints the space's default rather than anything the operands suggested.
10
11use super::{ColorSpace, MAX_PATTERN_COMPONENTS, Rgb};
12use pdfrum_object::Name;
13use smallvec::SmallVec;
14use std::sync::Arc;
15
16/// A colour in some space.
17///
18/// Cheap to clone — the space is shared, the components are inline.
19#[derive(Debug, Clone, PartialEq)]
20pub struct ColorValue {
21    /// The space the components live in. `None` before any colour operator
22    /// has run, which reads as `DeviceGray` on first use.
23    pub space: Option<Arc<ColorSpace>>,
24    /// The components, whose length always matches the space when set.
25    pub components: SmallVec<[f32; 4]>,
26    /// The pattern, when the space is `/Pattern`.
27    pub pattern: Option<Box<PatternValue>>,
28}
29
30impl Default for ColorValue {
31    /// Opaque black in `DeviceGray`, which is a page's initial fill and
32    /// stroke colour.
33    fn default() -> Self {
34        Self {
35            space: None,
36            components: SmallVec::from_slice(&[0.0]),
37            pattern: None,
38        }
39    }
40}
41
42/// A pattern colour: which pattern, and the components an uncoloured one
43/// paints with.
44#[derive(Debug, Clone, PartialEq)]
45pub struct PatternValue {
46    /// The pattern's name in the `/Pattern` resource dictionary.
47    pub name: Name,
48    /// The colour operands, capped at sixteen. Meaningless for a coloured
49    /// (`/PaintType 1`) pattern, which supplies its own.
50    pub components: SmallVec<[f32; 4]>,
51    /// The pattern the name resolved to, when it resolved to one.
52    ///
53    /// Loading it here rather than at paint time is what lets the renderer
54    /// stay free of the resolver: a tiling cell's page objects and a shading
55    /// pattern's ramp are both already in hand by the time an object carrying
56    /// this colour reaches the walk. `None` only where `scn` named a pattern
57    /// the resources do not define, which is a no-op operator.
58    pub loaded: Option<Arc<crate::pattern::Pattern>>,
59}
60
61/// Why [`ColorValue::set_components`] refused the values.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
63pub enum SetComponentsError {
64    /// The slice was shorter than the space's component count.
65    #[error("need {needed} colour components, got {got}")]
66    TooFew {
67        /// Components the space requires.
68        needed: usize,
69        /// Components the caller supplied.
70        got: usize,
71    },
72    /// The current space is a pattern; only `scn` with a name changes it.
73    #[error("a pattern colour is not set by components")]
74    PatternSpace,
75}
76
77impl ColorValue {
78    /// Install a colorspace, **resetting** the components to its default
79    /// colour.
80    ///
81    /// This is what makes `cs` discard whatever colour was current: the
82    /// operands that follow are the only ones that count.
83    pub fn set_space(&mut self, space: Arc<ColorSpace>) {
84        self.components = space.default_color().into();
85        self.pattern = None;
86        self.space = Some(space);
87    }
88
89    /// Set the components without changing the space.
90    ///
91    /// **Too few values change nothing**; surplus values are kept, since
92    /// PDFium stores the vector verbatim. With no space installed,
93    /// `DeviceGray` is installed first.
94    ///
95    /// # Errors
96    ///
97    /// [`SetComponentsError::TooFew`] when `values` is shorter than the space
98    /// requires. [`SetComponentsError::PatternSpace`] when the current space
99    /// is a pattern — only `scn` with a name changes a pattern colour.
100    pub fn set_components(&mut self, values: &[f32]) -> Result<(), SetComponentsError> {
101        let space = self
102            .space
103            .get_or_insert_with(|| Arc::new(ColorSpace::DeviceGray));
104        if space.n_components() > values.len() {
105            return Err(SetComponentsError::TooFew {
106                needed: space.n_components(),
107                got: values.len(),
108            });
109        }
110        if matches!(**space, ColorSpace::Pattern(_)) {
111            // A pattern space keeps its pattern; only `scn` with a name
112            // changes it.
113            return Err(SetComponentsError::PatternSpace);
114        }
115        self.components = SmallVec::from_slice(values);
116        Ok(())
117    }
118
119    /// Set both the space and the components in one step, as `g`, `rg` and
120    /// `k` do.
121    pub fn set_stock(&mut self, space: ColorSpace, values: &[f32]) {
122        let space = Arc::new(space);
123        self.set_space(Arc::clone(&space));
124        if space.n_components() <= values.len() {
125            self.components = SmallVec::from_slice(values);
126        }
127    }
128
129    /// Install a pattern, with the operands an uncoloured one paints with.
130    ///
131    /// More than sixteen operands is refused outright: nothing is written, so
132    /// the colour keeps whatever pattern it had, or none. Otherwise the space
133    /// becomes `/Pattern` if it was not one already, so a `/P1 scn` with no
134    /// preceding `/Pattern cs` still paints the pattern rather than resolving
135    /// its operands through whatever space was current.
136    // Leaving the space alone instead would resolve the operands through the
137    // current space — black in the default `DeviceGray` — and paint the object
138    // solid, which on a page-sized rectangle is the whole page.
139    pub fn set_pattern(
140        &mut self,
141        name: Name,
142        values: &[f32],
143        loaded: Option<Arc<crate::pattern::Pattern>>,
144    ) {
145        if values.len() > MAX_PATTERN_COMPONENTS {
146            return;
147        }
148        if !self.is_pattern() {
149            self.space = Some(Arc::new(ColorSpace::Pattern(Box::default())));
150        }
151        self.pattern = Some(Box::new(PatternValue {
152            name,
153            components: SmallVec::from_slice(values),
154            loaded,
155        }));
156    }
157
158    /// The colour to paint with, or `None` when the space cannot produce one
159    /// — a `/None` separation, an out-of-range palette index, or a pattern.
160    #[must_use]
161    pub fn to_rgb(&self) -> Option<Rgb> {
162        let space = self.space.as_ref()?;
163        if let ColorSpace::Pattern(pattern_space) = &**space {
164            let value = self.pattern.as_ref()?;
165            return pattern_space.to_rgb(&value.components);
166        }
167        space.try_to_rgb(&self.components)
168    }
169
170    /// Whether this colour comes from a pattern.
171    #[must_use]
172    pub fn is_pattern(&self) -> bool {
173        self.space
174            .as_ref()
175            .is_some_and(|s| matches!(**s, ColorSpace::Pattern(_)))
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    // Test fixtures quote the oracle's own vectors, compare floats exactly
182    // where the behaviour being pinned is exact, and index arrays whose
183    // length the fixture itself fixes.
184    #![allow(
185        clippy::unreadable_literal,
186        clippy::float_cmp,
187        clippy::indexing_slicing,
188        clippy::cast_precision_loss,
189        clippy::cast_possible_truncation,
190        reason = "test fixtures quote oracle vectors verbatim and compare exactly"
191    )]
192
193    use super::{ColorSpace, ColorValue, SetComponentsError};
194    use pdfrum_object::Name;
195    use smallvec::SmallVec;
196    use std::sync::Arc;
197
198    #[test]
199    fn the_initial_colour_is_black_in_device_gray() {
200        let c = ColorValue::default();
201        assert_eq!(&c.components[..], &[0.0]);
202        assert!(c.space.is_none());
203    }
204
205    #[test]
206    fn installing_a_space_resets_the_components() {
207        let mut c = ColorValue::default();
208        c.set_stock(ColorSpace::DeviceRgb, &[1.0, 0.5, 0.25]);
209        assert_eq!(&c.components[..], &[1.0, 0.5, 0.25]);
210        // `cs` back to gray discards the RGB colour entirely.
211        c.set_space(Arc::new(ColorSpace::DeviceGray));
212        assert_eq!(&c.components[..], &[0.0]);
213    }
214
215    #[test]
216    fn a_pattern_space_refuses_component_operands() {
217        let mut c = ColorValue::default();
218        c.set_space(Arc::new(ColorSpace::Pattern(Box::default())));
219        let before = c.components.clone();
220        assert_eq!(
221            c.set_components(&[0.5]),
222            Err(SetComponentsError::PatternSpace)
223        );
224        assert_eq!(c.components, before);
225    }
226
227    #[test]
228    fn too_few_components_change_nothing_at_all() {
229        let mut c = ColorValue::default();
230        c.set_stock(ColorSpace::DeviceCmyk, &[0.1, 0.2, 0.3, 0.4]);
231        assert_eq!(
232            c.set_components(&[0.9, 0.9]),
233            Err(SetComponentsError::TooFew { needed: 4, got: 2 })
234        );
235        assert_eq!(&c.components[..], &[0.1, 0.2, 0.3, 0.4]);
236        // Exactly enough does change it.
237        assert!(c.set_components(&[0.5, 0.5, 0.5, 0.5]).is_ok());
238        assert_eq!(&c.components[..], &[0.5, 0.5, 0.5, 0.5]);
239    }
240
241    #[test]
242    fn components_with_no_space_install_device_gray() {
243        let mut c = ColorValue {
244            space: None,
245            components: SmallVec::new(),
246            pattern: None,
247        };
248        assert!(c.set_components(&[0.75]).is_ok());
249        assert_eq!(
250            c.space.as_deref(),
251            Some(&ColorSpace::DeviceGray),
252            "an unset space becomes DeviceGray"
253        );
254    }
255
256    #[test]
257    fn a_pattern_operand_vector_over_sixteen_is_refused_outright() {
258        // `SetValueForPattern` returns *before* touching anything, so the
259        // name is not replaced either — not merely the components.
260        let mut c = ColorValue::default();
261        c.set_space(Arc::new(ColorSpace::Pattern(Box::default())));
262        c.set_pattern(Name::from("P0"), &[0.5, 0.25], None);
263        c.set_pattern(Name::from("P1"), &[1.0; 17], None);
264        let p = c.pattern.as_ref().expect("pattern");
265        assert_eq!(p.name.as_bytes(), b"P0", "the whole call is refused");
266        assert_eq!(&p.components[..], &[0.5, 0.25]);
267    }
268
269    #[test]
270    fn installing_a_pattern_installs_the_pattern_space() {
271        // A `/P1 scn` with no preceding `/Pattern cs` is still a pattern
272        // colour, because `SetValueForPattern` installs the stock space when
273        // the current one is not already a pattern space. Leaving the space
274        // alone resolves the operands through `DeviceGray` and paints the
275        // object solid black — which on a page-sized rectangle is the whole
276        // page, and is what four of the corpus's fuzz files exercise.
277        let mut c = ColorValue::default();
278        assert!(!c.is_pattern());
279        c.set_pattern(Name::from("P1"), &[], None);
280        assert!(c.is_pattern(), "the stock /Pattern space is installed");
281        // And a pattern space that already has a base keeps it: the C++ only
282        // installs the stock space when the current one is not a pattern.
283        let mut with_base = ColorValue::default();
284        with_base.set_space(Arc::new(ColorSpace::Pattern(Box::new(
285            super::super::PatternSpace {
286                base: Some(Box::new(ColorSpace::DeviceRgb)),
287            },
288        ))));
289        with_base.set_pattern(Name::from("P1"), &[1.0, 0.0, 0.0], None);
290        assert_eq!(
291            with_base.to_rgb().map(super::Rgb::to_bytes),
292            Some([255, 0, 0])
293        );
294    }
295
296    #[test]
297    fn a_pattern_colour_resolves_through_its_base_space() {
298        let mut c = ColorValue::default();
299        c.set_space(Arc::new(ColorSpace::Pattern(Box::new(
300            super::super::PatternSpace {
301                base: Some(Box::new(ColorSpace::DeviceGray)),
302            },
303        ))));
304        assert!(c.is_pattern());
305        // No pattern installed yet: no colour.
306        assert!(c.to_rgb().is_none());
307        c.set_pattern(Name::from("P0"), &[0.5], None);
308        let rgb = c.to_rgb().expect("colour");
309        assert!((rgb.r - 0.5).abs() < 1e-6);
310    }
311}