Skip to main content

pdf_interpret/
pattern.rs

1//! PDF patterns.
2
3use crate::WarningSinkFn;
4use crate::cache::Cache;
5use crate::color::{Color, ColorSpace};
6use crate::context::Context;
7use crate::device::Device;
8use crate::font::Glyph;
9use crate::interpret::state::{ActiveTransferFunction, State};
10use crate::shading::Shading;
11use crate::soft_mask::SoftMask;
12use crate::util::{Float32Ext, RectExt, decode_or_warn, hash128};
13use crate::{BlendMode, CacheKey, ClipPath, GlyphDrawMode, Image, PathDrawMode};
14use crate::{FillRule, InterpreterSettings, Paint, interpret};
15use kurbo::{Affine, BezPath, Rect, Shape};
16use log::warn;
17use pdf_syntax::content::TypedIter;
18use pdf_syntax::object::Dict;
19use pdf_syntax::object::Stream;
20use pdf_syntax::object::dict::keys::{
21    BBOX, EXT_G_STATE, MATRIX, PAINT_TYPE, RESOURCES, SHADING, X_STEP, Y_STEP,
22};
23use pdf_syntax::object::{Object, dict_or_stream};
24use pdf_syntax::page::Resources;
25use pdf_syntax::xref::XRef;
26use std::fmt::{Debug, Formatter};
27use std::sync::Arc;
28
29/// A PDF pattern.
30#[derive(Debug, Clone)]
31pub enum Pattern<'a> {
32    /// A shading pattern.
33    Shading(ShadingPattern),
34    /// A tiling pattern.
35    Tiling(Box<TilingPattern<'a>>),
36}
37
38impl<'a> Pattern<'a> {
39    pub(crate) fn new(object: Object<'a>, ctx: &Context<'a>) -> Option<Self> {
40        if let Some(dict) = object.clone().into_dict() {
41            Some(Self::Shading(ShadingPattern::new(
42                &dict,
43                &ctx.object_cache,
44                ctx.get().graphics_state.non_stroke_alpha,
45                &ctx.settings.warning_sink,
46            )?))
47        } else if let Some(stream) = object.clone().into_stream() {
48            Some(Self::Tiling(Box::new(TilingPattern::new(stream, ctx)?)))
49        } else {
50            None
51        }
52    }
53
54    pub(crate) fn pre_concat_transform(&mut self, transform: Affine) {
55        match self {
56            Self::Shading(p) => {
57                p.matrix = transform * p.matrix;
58                let transformed_clip_path = p.shading.clip_path.clone().map(|r| p.matrix * r);
59                Arc::make_mut(&mut p.shading).clip_path = transformed_clip_path;
60            }
61            Self::Tiling(p) => p.matrix = transform * p.matrix,
62        }
63    }
64
65    pub(crate) fn set_transfer_function(&mut self, tf: ActiveTransferFunction) {
66        if let Self::Shading(p) = self {
67            p.transfer_function = Some(tf);
68        }
69    }
70}
71
72impl CacheKey for Pattern<'_> {
73    fn cache_key(&self) -> u128 {
74        match self {
75            Self::Shading(p) => p.cache_key(),
76            Self::Tiling(p) => p.cache_key(),
77        }
78    }
79}
80
81/// A shading pattern.
82#[derive(Clone, Debug)]
83pub struct ShadingPattern {
84    /// The underlying shading of the pattern.
85    pub shading: Arc<Shading>,
86    /// A transformation matrix to apply prior to rendering.
87    pub matrix: Affine,
88    /// An additional opacity to apply to the shading pattern.
89    pub opacity: f32,
90    /// An optional transfer function to apply to the shading's output colors.
91    pub transfer_function: Option<ActiveTransferFunction>,
92}
93
94impl ShadingPattern {
95    pub(crate) fn new(
96        dict: &Dict<'_>,
97        cache: &Cache,
98        opacity: f32,
99        warning_sink: &WarningSinkFn,
100    ) -> Option<Self> {
101        let shading = dict.get::<Object<'_>>(SHADING).and_then(|o| {
102            let (dict, stream) = dict_or_stream(&o)?;
103
104            Shading::new(&dict, stream.as_ref(), cache, warning_sink)
105        })?;
106        let matrix = dict
107            .get::<[f64; 6]>(MATRIX)
108            .map(Affine::new)
109            .unwrap_or_default();
110
111        if dict.contains_key(EXT_G_STATE) {
112            warn!("shading patterns with ext_g_state are not supported yet");
113        }
114
115        Some(Self {
116            shading: Arc::new(shading),
117            opacity,
118            matrix,
119            transfer_function: None,
120        })
121    }
122}
123
124impl CacheKey for ShadingPattern {
125    fn cache_key(&self) -> u128 {
126        hash128(&(self.shading.cache_key(), self.matrix.cache_key()))
127    }
128}
129
130/// A tiling pattern.
131#[derive(Clone)]
132pub struct TilingPattern<'a> {
133    /// The depth the parent interpretation sat at (#318): a fresh Context
134    /// would otherwise restart the bound at zero, and a cycle alternating
135    /// between constructs would never reach it.
136    nesting_depth: u32,
137    cache_key: u128,
138    ctx_bbox: Rect,
139    /// The bbox of the tiling pattern.
140    pub bbox: Rect,
141    /// The step in the x direction.
142    pub x_step: f32,
143    /// The step in the y direction.
144    pub y_step: f32,
145    /// A transformation to apply prior to rendering.
146    pub matrix: Affine,
147    stream: Stream<'a>,
148    /// Whether this is a colored (`PaintType 1`) tiling pattern.
149    pub is_color: bool,
150    /// For colored tiling patterns: the non-stroking opacity from the parent
151    /// graphics state at the time the pattern was set. PDF ยง8.7.3.1 states
152    /// that the current graphics state (including opacity) applies when a
153    /// tiling pattern is painted, so we capture it here for later use.
154    pub opacity: f32,
155    pub(crate) stroke_paint: Color,
156    pub(crate) non_stroking_paint: Color,
157    resources: Resources<'a>,
158    pub(crate) cache: Cache,
159    pub(crate) settings: InterpreterSettings,
160    pub(crate) xref: &'a XRef,
161}
162
163impl Debug for TilingPattern<'_> {
164    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
165        f.write_str("TilingPattern")
166    }
167}
168
169impl<'a> TilingPattern<'a> {
170    pub(crate) fn new(stream: Stream<'a>, ctx: &Context<'a>) -> Option<Self> {
171        let cache_key = stream.cache_key();
172        let dict = stream.dict();
173
174        let bbox = dict.get::<pdf_syntax::object::Rect>(BBOX)?.to_kurbo();
175        let x_step = dict.get::<f32>(X_STEP)?;
176        let y_step = dict.get::<f32>(Y_STEP)?;
177
178        if x_step.is_nearly_zero() || y_step.is_nearly_zero() || bbox.is_zero_area() {
179            return None;
180        }
181
182        let is_color = dict.get::<u8>(PAINT_TYPE)? == 1;
183        let matrix = dict
184            .get::<[f64; 6]>(MATRIX)
185            .map(Affine::new)
186            .unwrap_or_default();
187
188        let state = ctx.get().clone();
189        let ctx_bbox = ctx.bbox();
190
191        let fill_cs = state
192            .graphics_state
193            .none_stroke_cs
194            .pattern_cs()
195            .unwrap_or(ColorSpace::device_gray());
196        let stroke_cs = state
197            .graphics_state
198            .stroke_cs
199            .pattern_cs()
200            .unwrap_or(ColorSpace::device_gray());
201
202        let non_stroke_alpha = state.graphics_state.non_stroke_alpha;
203        let stroke_alpha = state.graphics_state.stroke_alpha;
204
205        let non_stroking_paint = Color::new(
206            fill_cs,
207            state.graphics_state.non_stroke_color.clone(),
208            non_stroke_alpha,
209        );
210        let stroke_paint = Color::new(
211            stroke_cs,
212            state.graphics_state.stroke_color.clone(),
213            stroke_alpha,
214        );
215
216        // For colored tiling patterns the fill opacity controls transparency
217        // of the whole tile; for uncolored patterns it is already embedded in
218        // the paint colors above, so we record it but use it conditionally.
219        let opacity = non_stroke_alpha;
220        let resources = Resources::new(dict.get(RESOURCES).unwrap_or_default());
221
222        Some(Self {
223            nesting_depth: ctx.nesting_depth(),
224            cache_key,
225            bbox,
226            x_step,
227            y_step,
228            matrix,
229            ctx_bbox,
230            is_color,
231            stream,
232            opacity,
233            stroke_paint,
234            non_stroking_paint,
235            settings: ctx.settings.clone(),
236            resources,
237            cache: ctx.object_cache.clone(),
238            xref: ctx.xref,
239        })
240    }
241
242    /// Interpret the contents of the pattern into the given device.
243    pub fn interpret(
244        &self,
245        device: &mut impl Device<'a>,
246        initial_transform: Affine,
247        is_stroke: bool,
248    ) -> Option<()> {
249        let state = State::new(initial_transform);
250
251        let mut context = Context::new_with(
252            state.ctm,
253            // TODO: bbox?
254            (initial_transform * self.ctx_bbox.to_path(0.1)).bounding_box(),
255            self.cache.clone(),
256            self.xref,
257            self.settings.clone(),
258            state,
259            // The parent's depth, not zero: a fresh Context would restart the
260            // bound and a cycle alternating between constructs would never reach
261            // it. Measured before this: rc=134 on a 1082-byte pattern/soft-mask
262            // cycle (#318).
263            self.nesting_depth + 1,
264        );
265
266        let decoded = decode_or_warn(&self.stream, &self.settings.warning_sink)?;
267        let iter = TypedIter::new(decoded.as_ref());
268
269        let clip_path = ClipPath {
270            path: initial_transform * self.bbox.to_path(0.1),
271            fill: FillRule::NonZero,
272        };
273        device.push_clip_path(&clip_path);
274
275        if self.is_color {
276            interpret(iter, &self.resources, &mut context, device);
277        } else {
278            let paint = if !is_stroke {
279                Paint::Color(self.non_stroking_paint.clone())
280            } else {
281                Paint::Color(self.stroke_paint.clone())
282            };
283
284            let mut device = StencilPatternDevice::new(device, paint.clone());
285            interpret(iter, &self.resources, &mut context, &mut device);
286        }
287
288        device.pop_clip_path();
289
290        Some(())
291    }
292}
293
294impl CacheKey for TilingPattern<'_> {
295    fn cache_key(&self) -> u128 {
296        self.cache_key
297    }
298}
299
300struct StencilPatternDevice<'a, 'b, T: Device<'a>> {
301    inner: &'b mut T,
302    paint: Paint<'a>,
303}
304
305impl<'a, 'b, T: Device<'a>> StencilPatternDevice<'a, 'b, T> {
306    pub(crate) fn new(device: &'b mut T, paint: Paint<'a>) -> Self {
307        Self {
308            inner: device,
309            paint,
310        }
311    }
312}
313
314// Only filling, stroking of paths and stencil masks are allowed.
315impl<'a, T: Device<'a>> Device<'a> for StencilPatternDevice<'a, '_, T> {
316    fn draw_path(
317        &mut self,
318        path: &BezPath,
319        transform: Affine,
320        _: &Paint<'_>,
321        draw_mode: &PathDrawMode,
322    ) {
323        self.inner
324            .draw_path(path, transform, &self.paint, draw_mode);
325    }
326
327    fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
328
329    fn push_clip_path(&mut self, clip_path: &ClipPath) {
330        self.inner.push_clip_path(clip_path);
331    }
332
333    fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
334
335    fn draw_glyph(
336        &mut self,
337        g: &Glyph<'a>,
338        transform: Affine,
339        glyph_transform: Affine,
340        p: &Paint<'a>,
341        draw_mode: &GlyphDrawMode,
342    ) {
343        self.inner
344            .draw_glyph(g, transform, glyph_transform, p, draw_mode);
345    }
346
347    fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
348        if let Image::Stencil(mut s) = image {
349            s.paint = self.paint.clone();
350            self.inner.draw_image(Image::Stencil(s), transform);
351        }
352    }
353
354    fn pop_clip_path(&mut self) {
355        self.inner.pop_clip_path();
356    }
357
358    fn pop_transparency_group(&mut self) {}
359
360    fn set_blend_mode(&mut self, _: BlendMode) {}
361}