Skip to main content

pdfrum_page/pattern/
mod.rs

1//! Patterns, tiling and shading (ISO 32000-1 §8.7.3).
2//!
3//! # The matrix rule
4//!
5//! A pattern's own `/Matrix` composes with the **parent matrix** — the
6//! form's or page's coordinate system — not with the current transformation
7//! matrix. Patterns are anchored to the space they were declared in, so
8//! translating the CTM before filling does not slide the tiling. This is a
9//! frequent source of bugs and is worth restating whenever it comes up.
10//!
11//! `kurbo::Affine` multiplies right-to-left, so the code writes
12//! `parent * pattern` — the pattern's matrix applied first, then the parent's.
13//!
14//! One asymmetry to know: a **bare shading** reached through the `sh`
15//! operator ignores any `/Matrix` on its dictionary entirely. The matrix is
16//! composed for a pattern, never for a shading object reached directly.
17
18mod tiling;
19
20pub use tiling::{TileRange, TilingPattern};
21
22use crate::color::ColorSpace;
23use crate::function::FunctionCache;
24use crate::names;
25use crate::shading::{Shading, ShadingSource};
26use kurbo::Affine;
27use pdfrum_common::{Diagnostics, Limits};
28use pdfrum_object::{Dict, Object, Resolve};
29use std::sync::Arc;
30
31/// A loaded pattern.
32#[derive(Debug, Clone, PartialEq)]
33#[non_exhaustive]
34pub enum Pattern {
35    /// `/PatternType 1`: a content stream tiled across the fill.
36    Tiling(Box<TilingPattern>),
37    /// `/PatternType 2`: a shading painted across the fill.
38    Shading(Box<ShadingPattern>),
39}
40
41/// A `/PatternType 2` pattern: a shading plus the matrix placing it.
42#[derive(Debug, Clone, PartialEq)]
43pub struct ShadingPattern {
44    /// The shading itself.
45    pub shading: Arc<Shading>,
46    /// The pattern's own space composed with the parent matrix.
47    pub matrix: Affine,
48    /// The pattern's `/ExtGState`, which a shading pattern may carry.
49    pub ext_g_state: Option<Dict>,
50}
51
52impl Pattern {
53    /// The matrix taking the pattern's own space to the parent's.
54    #[must_use]
55    pub fn matrix(&self) -> Affine {
56        match self {
57            Self::Tiling(p) => p.matrix,
58            Self::Shading(p) => p.matrix,
59        }
60    }
61
62    /// Load a pattern from the object a `/Pattern` resource names.
63    ///
64    /// `parent_matrix` is the form's or page's coordinate system, **not** the
65    /// current transformation matrix. Dispatch is on `/PatternType`: 1 is
66    /// tiling, 2 is shading, and anything else — including a missing key —
67    /// yields no pattern at all.
68    ///
69    /// **There is no pattern cache.** A pattern is loaded afresh at each use,
70    /// so the `parent_matrix` a use is given is the one it gets.
71    //
72    // [oracle-bug] cpdf_docpagedata.cpp:388 looks a pattern up in
73    // `pattern_map_` keyed on **the pattern object alone**, and :406 stores it
74    // there. The `matrix` argument is a *construction* parameter (:396, :400),
75    // so it is baked into whichever instance was built first and every later
76    // user of the same object silently inherits it. Two forms at different
77    // nesting levels naming one pattern therefore paint it in one form's
78    // coordinate system — §8.7.3 anchors a pattern in the space of the
79    // content stream in which it is *used*, which is what makes the shared
80    // matrix wrong rather than merely surprising. Worse, `GetShading`
81    // (:410-424) consults and writes **the same map** while constructing with
82    // `bShading = true` where `GetPattern` passes `false` (:400), so an object
83    // reached once through `sh` and once through `scn` returns whichever was
84    // built first — with the wrong `/Background` handling for the other. There
85    // is no independent implementation to weigh: pdf.js is canvas-backed and
86    // has no pattern cache at all. We keep no cache either, which cannot
87    // alias; if one is ever added it must be keyed on
88    // `(ObjRef, parent_matrix, is_shading)`.
89    #[must_use]
90    pub fn load<R: Resolve>(
91        obj: &Object,
92        parent_matrix: Affine,
93        resources: Option<&Dict>,
94        r: &R,
95        functions: &mut FunctionCache,
96        limits: &Limits,
97        diags: &mut Diagnostics,
98    ) -> Option<Self> {
99        let resolved = obj.resolve(r).ok()?;
100        let dict = match &*resolved {
101            Object::Dict(d) => d.clone(),
102            Object::Stream(s) => s.dict.clone(),
103            _ => return None,
104        };
105        // `/Matrix` must have exactly six elements or it reads as identity.
106        let own = dict.matrix(names::MATRIX, r);
107        let matrix = parent_matrix * own;
108
109        match dict.int(names::PATTERN_TYPE, r)? {
110            1 => {
111                let stream = resolved.as_stream()?;
112                Some(Self::Tiling(Box::new(TilingPattern::load(
113                    stream, matrix, r, limits, diags,
114                ))))
115            }
116            2 => {
117                let shading_obj = dict.raw(names::SHADING)?;
118                let shading = Shading::load(
119                    shading_obj,
120                    resources,
121                    ShadingSource::Pattern,
122                    r,
123                    functions,
124                    limits,
125                    diags,
126                )?;
127                Some(Self::Shading(Box::new(ShadingPattern {
128                    shading: Arc::new(shading),
129                    matrix,
130                    ext_g_state: dict.dict(names::EXT_G_STATE, r),
131                })))
132            }
133            _ => None,
134        }
135    }
136}
137
138/// The colour an uncoloured pattern paints with, and the fallbacks when none
139/// resolves.
140///
141/// PDFium's two sentinels are worth naming: a **coloured** tiling pattern
142/// whose colour will not resolve falls back to mid grey (`0xBFBFBF`), while
143/// everything else falls back to white. The grey is what makes an unpainted
144/// coloured tile visible rather than invisible.
145#[must_use]
146pub fn uncolored_pattern_rgb(
147    space: &ColorSpace,
148    components: &[f32],
149    colored_tiling: bool,
150) -> crate::color::Rgb {
151    if let ColorSpace::Pattern(p) = space
152        && let Some(rgb) = p.to_rgb(components)
153    {
154        return rgb;
155    }
156    if colored_tiling {
157        // Mid grey, `0x00BFBFBF`.
158        crate::color::Rgb {
159            r: 191.0 / 255.0,
160            g: 191.0 / 255.0,
161            b: 191.0 / 255.0,
162        }
163    } else {
164        crate::color::Rgb {
165            r: 1.0,
166            g: 1.0,
167            b: 1.0,
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    // Test fixtures quote the oracle's own vectors, compare floats exactly
175    // where the behaviour being pinned is exact, and index arrays whose
176    // length the fixture itself fixes.
177    #![allow(
178        clippy::unreadable_literal,
179        clippy::float_cmp,
180        clippy::indexing_slicing,
181        clippy::cast_precision_loss,
182        clippy::cast_possible_truncation,
183        reason = "test fixtures quote oracle vectors verbatim and compare exactly"
184    )]
185
186    use super::{Pattern, uncolored_pattern_rgb};
187    use crate::color::{ColorSpace, PatternSpace};
188    use crate::function::FunctionCache;
189    use kurbo::Affine;
190    use pdfrum_common::{Diagnostics, Limits};
191    use pdfrum_object::{Dict, Name, NoResolve, Object};
192
193    fn load(dict: Dict, parent: Affine) -> Option<Pattern> {
194        let mut funcs = FunctionCache::new();
195        let mut diags = Diagnostics::default();
196        Pattern::load(
197            &Object::Dict(dict),
198            parent,
199            None,
200            &NoResolve,
201            &mut funcs,
202            &Limits::default(),
203            &mut diags,
204        )
205    }
206
207    #[test]
208    fn an_unknown_pattern_type_yields_no_pattern() {
209        for kind in [0i64, 3, -1] {
210            let dict = Dict::from_pairs([(Name::from("PatternType"), Object::Int(kind))]);
211            assert!(load(dict, Affine::IDENTITY).is_none(), "type {kind}");
212        }
213        // A missing `/PatternType` likewise.
214        assert!(load(Dict::new(), Affine::IDENTITY).is_none());
215    }
216
217    #[test]
218    fn a_tiling_pattern_must_be_a_stream() {
219        // A plain dictionary with `/PatternType 1` is not enough.
220        let dict = Dict::from_pairs([(Name::from("PatternType"), Object::Int(1))]);
221        assert!(load(dict, Affine::IDENTITY).is_none());
222    }
223
224    #[test]
225    fn uncolored_fallbacks_differ_by_paint_type() {
226        let no_base = ColorSpace::Pattern(Box::default());
227        let grey = uncolored_pattern_rgb(&no_base, &[0.5], true);
228        assert!((grey.r - 191.0 / 255.0).abs() < 1e-5);
229        let white = uncolored_pattern_rgb(&no_base, &[0.5], false);
230        assert!((white.r - 1.0).abs() < 1e-6);
231
232        // With a base the operands resolve normally and no fallback applies.
233        let with_base = ColorSpace::Pattern(Box::new(PatternSpace {
234            base: Some(Box::new(ColorSpace::DeviceGray)),
235        }));
236        let resolved = uncolored_pattern_rgb(&with_base, &[0.25], true);
237        assert!((resolved.r - 0.25).abs() < 1e-6);
238    }
239}