Skip to main content

pdf_interpret/
context.rs

1use crate::cache::{Cache, CacheKey};
2use crate::color::ColorSpace;
3use crate::convert::convert_transform;
4use crate::font::{Font, StandardFont};
5use crate::interpret::state::{ClipType, State, TextStateFont};
6use crate::ocg::OcgState;
7use crate::util::{BezPathExt, Float64Ext};
8use crate::{ClipPath, Device, FillRule, InterpreterSettings, StrokeProps};
9use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape};
10use log::warn;
11use pdf_syntax::content::ops::Transform;
12use pdf_syntax::object::Dict;
13use pdf_syntax::object::Name;
14use pdf_syntax::page::Resources;
15use pdf_syntax::xref::XRef;
16use std::collections::HashMap;
17
18/// How deep a document may nest one interpretation inside another.
19///
20/// A Form XObject may draw another XObject, a pattern may paint with a pattern,
21/// a Type 3 glyph is a content stream of its own. Nothing in a PDF stops any of
22/// those from referring to itself, and the interpreter recurses on the Rust
23/// stack -- so a self-referencing XObject exhausts it. Reproduced on a 639-byte
24/// file: `q /X1 Do Q` inside the XObject named `/X1`, rc=134, "has overflowed
25/// its stack". No XFA and no script; it reaches every binding that exports
26/// `render_page`.
27///
28/// 50 because upstream chose 50 (LaurenzV/hayro#1152) and because real
29/// documents do not come close. The limit exists to convert a crash into a
30/// warning and a missing paint, not to judge how baroque a document is allowed
31/// to be.
32///
33/// WHAT "DO NOT COME CLOSE" IS MEASURED AGAINST
34///
35/// This counter changed meaning in #318, so an older figure would describe a
36/// quantity that no longer exists. Before, each construct that builds a fresh
37/// `Context` restarted at zero, and a chain XObject -> pattern -> soft mask ->
38/// glyph counted as four independent depths of 1. It now counts as one depth
39/// of 4. The number has to be re-measured under the new accounting to mean
40/// anything, and this is the change that moved it.
41///
42/// Re-measured on every PDF in this repository (164 documents, deepest page of
43/// each), instrumented at this function:
44///
45///   depth 2   one real government form, the deepest of the 164
46///   depth 1   every other real document
47///   depth 49-50  the three cycle fixtures, which recurse on purpose
48///
49/// Which document that was is not written here: it is one of the corpus files
50/// this repository holds and may not redistribute, and a name is a description
51/// of the corpus even where the corpus is absent (docs/PUBLIC_TREE.toml).
52///
53/// So 50 leaves a factor of 25 over the deepest real document reachable here.
54/// That is a floor, not the corpus maximum: the 181K-document corpus is not on
55/// this machine, and nothing here re-measures it. If that matters later, the
56/// instrument is four lines at the increment above -- a static max plus an
57/// eprintln -- and the scan takes a minute.
58pub(crate) const MAX_NESTED_INTERPRETATION_DEPTH: u32 = 50;
59
60/// A context for interpreting PDF files.
61pub struct Context<'a> {
62    states: Vec<State<'a>>,
63    path: BezPath,
64    sub_path_start: Point,
65    last_point: Point,
66    clip: Option<FillRule>,
67    pub(crate) font_cache: HashMap<u128, Option<Font<'a>>>,
68    root_transforms: Vec<Affine>,
69    bbox: Vec<Rect>,
70    pub(crate) settings: InterpreterSettings,
71    pub(crate) object_cache: Cache,
72    pub(crate) xref: &'a XRef,
73    pub(crate) ocg_state: OcgState,
74    /// How deep this interpretation already sits inside XObjects, patterns,
75    /// soft masks or Type 3 glyph procedures. See MAX_NESTED_INTERPRETATION_DEPTH.
76    nesting_depth: u32,
77}
78
79impl<'a> Context<'a> {
80    /// Create a new context.
81    pub fn new(
82        initial_transform: Affine,
83        bbox: Rect,
84        xref: &'a XRef,
85        settings: InterpreterSettings,
86    ) -> Self {
87        let cache = settings.shared_cache.clone().unwrap_or_default();
88        let state = State::new(initial_transform);
89
90        Self::new_with(initial_transform, bbox, cache, xref, settings, state, 0)
91    }
92
93    pub(crate) fn new_with(
94        initial_transform: Affine,
95        bbox: Rect,
96        cache: Cache,
97        xref: &'a XRef,
98        settings: InterpreterSettings,
99        state: State<'a>,
100        nesting_depth: u32,
101    ) -> Self {
102        let ocg_state = {
103            let root_ref = xref.root_id();
104            xref.get::<Dict<'_>>(root_ref)
105                .map(|catalog| OcgState::from_catalog(&catalog))
106                .unwrap_or_default()
107        };
108
109        Self {
110            nesting_depth,
111            states: vec![state],
112            settings,
113            xref,
114            root_transforms: vec![initial_transform],
115            last_point: Point::default(),
116            sub_path_start: Point::default(),
117            clip: None,
118            bbox: vec![bbox],
119            path: BezPath::new(),
120            font_cache: HashMap::new(),
121            object_cache: cache,
122            ocg_state,
123        }
124    }
125
126    /// Return the interpreter settings owned by this context.
127    pub fn into_settings(self) -> InterpreterSettings {
128        self.settings
129    }
130
131    pub(crate) fn save_state(&mut self) {
132        let Some(cur) = self.states.last().cloned() else {
133            warn!("attempted to save state without existing state");
134            return;
135        };
136
137        self.states.push(cur);
138    }
139
140    pub(crate) fn bbox(&self) -> Rect {
141        self.bbox.last().copied().unwrap_or_else(|| {
142            warn!("failed to get a bbox");
143
144            Rect::new(0.0, 0.0, 1.0, 1.0)
145        })
146    }
147
148    fn push_bbox(&mut self, bbox: Rect) {
149        let new = self.bbox().intersect(bbox);
150        self.bbox.push(new);
151    }
152
153    pub(crate) fn push_clip_path(
154        &mut self,
155        clip_path: BezPath,
156        fill: FillRule,
157        device: &mut impl Device<'a>,
158    ) {
159        if let Some(clip_rect) = path_as_rect(&clip_path) {
160            let cur_bbox = self.bbox();
161
162            // If the clip path is a rect and completely covers the current bbox, don't emit it.
163            if cur_bbox
164                .min_x()
165                .is_nearly_greater_or_equal(clip_rect.min_x())
166                && cur_bbox
167                    .min_y()
168                    .is_nearly_greater_or_equal(clip_rect.min_y())
169                && cur_bbox.max_x().is_nearly_less_or_equal(clip_rect.max_x())
170                && cur_bbox.max_y().is_nearly_less_or_equal(clip_rect.max_y())
171            {
172                self.get_mut().clips.push(ClipType::Dummy);
173                return;
174            }
175        }
176
177        let bbox = clip_path.bounding_box();
178        device.push_clip_path(&ClipPath {
179            path: clip_path,
180            fill,
181        });
182        self.push_bbox(bbox);
183        self.get_mut().clips.push(ClipType::Real);
184    }
185
186    pub(crate) fn pop_clip_path(&mut self, device: &mut impl Device<'a>) {
187        if let Some(ClipType::Real) = self.get_mut().clips.pop() {
188            device.pop_clip_path();
189            self.pop_bbox();
190        }
191    }
192
193    fn pop_bbox(&mut self) {
194        self.bbox.pop();
195    }
196
197    pub(crate) fn push_root_transform(&mut self) {
198        self.root_transforms.push(self.get().ctm);
199    }
200
201    pub(crate) fn pop_root_transform(&mut self) {
202        self.root_transforms.pop();
203    }
204
205    pub(crate) fn root_transform(&self) -> Affine {
206        self.root_transforms
207            .last()
208            .copied()
209            .unwrap_or(Affine::IDENTITY)
210    }
211
212    pub(crate) fn restore_state(&mut self, device: &mut impl Device<'a>) {
213        let Some(target_clips) = self
214            .states
215            .get(self.states.len().saturating_sub(2))
216            .map(|s| s.clips.len())
217        else {
218            warn!("underflowed graphics state");
219            return;
220        };
221
222        while self.get().clips.len() > target_clips {
223            self.pop_clip_path(device);
224        }
225
226        // The first state should never be popped.
227        if self.states.len() > 1 {
228            self.states.pop();
229        }
230
231        device.set_soft_mask(
232            self.states
233                .last()
234                .and_then(|l| l.graphics_state.soft_mask.clone()),
235        );
236    }
237
238    pub(crate) fn path(&self) -> &BezPath {
239        &self.path
240    }
241
242    pub(crate) fn path_mut(&mut self) -> &mut BezPath {
243        &mut self.path
244    }
245
246    pub(crate) fn sub_path_start(&self) -> &Point {
247        &self.sub_path_start
248    }
249
250    pub(crate) fn sub_path_start_mut(&mut self) -> &mut Point {
251        &mut self.sub_path_start
252    }
253
254    pub(crate) fn last_point(&self) -> &Point {
255        &self.last_point
256    }
257
258    pub(crate) fn last_point_mut(&mut self) -> &mut Point {
259        &mut self.last_point
260    }
261
262    pub(crate) fn clip(&self) -> &Option<FillRule> {
263        &self.clip
264    }
265
266    pub(crate) fn clip_mut(&mut self) -> &mut Option<FillRule> {
267        &mut self.clip
268    }
269
270    /// The depth this interpretation sits at.
271    ///
272    /// Read by the constructs that build a FRESH `Context` — tiling patterns,
273    /// soft masks, Type 3 glyphs — so the child starts where the parent left off
274    /// instead of at zero. Without that, a cycle alternating between two of them
275    /// never reaches the limit: measured on a 1082-byte file whose pattern cell
276    /// sets a soft mask whose group paints with that pattern, rc=134 (#318).
277    pub(crate) fn nesting_depth(&self) -> u32 {
278        self.nesting_depth
279    }
280
281    /// Claim one level of nesting, or refuse.
282    ///
283    /// Returns false at the limit; the caller must then not interpret. Pair with
284    /// `end_nested_interpretation`, which is why this is not a plain `+= 1`: a
285    /// page draws many XObjects in sequence, and a counter that only rises would
286    /// refuse the fifty-first sibling rather than the fifty-first ancestor.
287    pub(crate) fn begin_nested_interpretation(&mut self) -> bool {
288        if self.nesting_depth >= MAX_NESTED_INTERPRETATION_DEPTH {
289            return false;
290        }
291
292        self.nesting_depth += 1;
293
294        true
295    }
296
297    pub(crate) fn end_nested_interpretation(&mut self) {
298        self.nesting_depth = self.nesting_depth.saturating_sub(1);
299    }
300
301    pub(crate) fn get(&self) -> &State<'a> {
302        self.states.last().unwrap()
303    }
304
305    pub(crate) fn get_mut(&mut self) -> &mut State<'a> {
306        self.states.last_mut().unwrap()
307    }
308
309    pub(crate) fn pre_concat_transform(&mut self, transform: Transform) {
310        self.pre_concat_affine(convert_transform(transform));
311    }
312
313    pub(crate) fn pre_concat_affine(&mut self, transform: Affine) {
314        self.get_mut().ctm *= transform;
315    }
316
317    pub(crate) fn get_color_space(
318        &mut self,
319        resources: &Resources<'_>,
320        name: Name,
321    ) -> Option<ColorSpace> {
322        let cs_object = resources.get_color_space(name)?;
323        self.object_cache
324            .get_or_insert_with(cs_object.cache_key(), || {
325                ColorSpace::new(
326                    cs_object.clone(),
327                    &self.object_cache,
328                    &self.settings.warning_sink,
329                )
330            })
331    }
332
333    pub(crate) fn stroke_props(&self) -> StrokeProps {
334        self.get().graphics_state.stroke_props.clone()
335    }
336
337    pub(crate) fn num_states(&self) -> usize {
338        self.states.len()
339    }
340
341    pub(crate) fn resolve_font(&mut self, font_dict: &Dict<'a>) -> Option<TextStateFont<'a>> {
342        let cache_key = font_dict.cache_key();
343
344        if let Some(resolved) = self
345            .font_cache
346            .entry(cache_key)
347            .or_insert_with(|| {
348                Font::new(
349                    font_dict,
350                    &self.settings.font_resolver,
351                    &self.settings.cmap_resolver,
352                    &self.settings.warning_sink,
353                )
354            })
355            .clone()
356        {
357            Some(TextStateFont::Font(resolved))
358        } else {
359            Font::new_standard(StandardFont::Helvetica, &self.settings.font_resolver)
360                .map(TextStateFont::Fallback)
361        }
362    }
363}
364
365pub(crate) fn path_as_rect(path: &BezPath) -> Option<Rect> {
366    let points = match path.elements() {
367        [
368            PathEl::MoveTo(first),
369            PathEl::LineTo(second),
370            PathEl::LineTo(third),
371            PathEl::LineTo(fourth),
372            PathEl::ClosePath,
373        ] => [*first, *second, *third, *fourth, *first],
374        [
375            PathEl::MoveTo(first),
376            PathEl::LineTo(second),
377            PathEl::LineTo(third),
378            PathEl::LineTo(fourth),
379            PathEl::LineTo(last),
380        ] if first.x.is_nearly_equal(last.x) && first.y.is_nearly_equal(last.y) => {
381            [*first, *second, *third, *fourth, *last]
382        }
383        _ => return None,
384    };
385
386    let mut previous_axis = None;
387    for edge in points.windows(2) {
388        let same_x = edge[0].x.is_nearly_equal(edge[1].x);
389        let same_y = edge[0].y.is_nearly_equal(edge[1].y);
390        if same_x == same_y || previous_axis == Some(same_x) {
391            return None;
392        }
393        previous_axis = Some(same_x);
394    }
395
396    Some(path.fast_bounding_box())
397}
398
399#[cfg(test)]
400mod clip_path_shape {
401    use super::*;
402
403    /// A bowtie is not a rectangle, however many of its corners are.
404    ///
405    /// The version this replaces decided by collecting which corners of the
406    /// bounding box the path touches. A self-intersecting quadrilateral touches
407    /// all four, so it was accepted and a clip shaped like an hourglass became
408    /// its bounding box -- content drawn where the document said to hide it.
409    /// Ported from LaurenzV/hayro#1338; measured here before and after, the same
410    /// path answered `true` and now answers `false`.
411    #[test]
412    fn a_bowtie_is_not_a_rect() {
413        let mut bowtie = BezPath::new();
414        bowtie.move_to((0.0, 0.0));
415        bowtie.line_to((10.0, 10.0));
416        bowtie.line_to((10.0, 0.0));
417        bowtie.line_to((0.0, 10.0));
418        bowtie.close_path();
419
420        assert!(
421            path_as_rect(&bowtie).is_none(),
422            "a self-intersecting quadrilateral was accepted as a rectangle, so a \
423             clip would cover its bounding box instead of its actual shape"
424        );
425    }
426
427    /// The other half: the fix must not refuse ordinary rectangles.
428    ///
429    /// Without this, returning `None` unconditionally would pass the test above
430    /// and turn every rectangular clip into a general path -- slower, and a
431    /// silent behaviour change nobody asked for.
432    #[test]
433    fn an_ordinary_rectangle_still_is_one() {
434        let mut rect = BezPath::new();
435        rect.move_to((0.0, 0.0));
436        rect.line_to((10.0, 0.0));
437        rect.line_to((10.0, 10.0));
438        rect.line_to((0.0, 10.0));
439        rect.close_path();
440
441        assert!(
442            path_as_rect(&rect).is_some(),
443            "a plain rectangle stopped being recognised as one"
444        );
445    }
446
447    /// The unclosed arm: `MoveTo + 4 LineTo` is accepted only when the last
448    /// point returns to the first.
449    ///
450    /// This is a separate acceptance path from the `ClosePath` form, and it is
451    /// also a tightening -- the version it replaces took that shape whether or
452    /// not it closed. Without a test, deleting the arm's guard or inverting it
453    /// changes nothing that goes red, and it is the arm most likely to be
454    /// "simplified" later by someone who sees two arms doing the same thing.
455    #[test]
456    fn four_lines_that_do_not_return_to_the_start_are_not_a_rect() {
457        let mut open = BezPath::new();
458        open.move_to((0.0, 0.0));
459        open.line_to((10.0, 0.0));
460        open.line_to((10.0, 10.0));
461        open.line_to((0.0, 10.0));
462        open.line_to((0.0, 5.0)); // back down the left edge, but not to the start
463
464        assert!(
465            path_as_rect(&open).is_none(),
466            "an unclosed four-line path was accepted as a rectangle, so a clip \
467             would cover the closed shape the path never drew"
468        );
469    }
470
471    /// And the same arm must still accept the shape it exists for.
472    ///
473    /// Paired with the test above so that neither "always None" nor "always
474    /// Some" passes both.
475    #[test]
476    fn four_lines_that_do_return_to_the_start_are_a_rect() {
477        let mut closed = BezPath::new();
478        closed.move_to((0.0, 0.0));
479        closed.line_to((10.0, 0.0));
480        closed.line_to((10.0, 10.0));
481        closed.line_to((0.0, 10.0));
482        closed.line_to((0.0, 0.0));
483
484        assert!(
485            path_as_rect(&closed).is_some(),
486            "a four-line rectangle that returns to its start stopped being one"
487        );
488    }
489}