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, MarkPlacement, 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    /// Where the combining marks the AMS name rows give are placed on this
78    /// page (spec `specs/text/circlecopyrt.md`, C4 and C5a).
79    pub(crate) marks: MarkPlacement,
80}
81
82impl<'a> Context<'a> {
83    /// Create a new context.
84    pub fn new(
85        initial_transform: Affine,
86        bbox: Rect,
87        xref: &'a XRef,
88        settings: InterpreterSettings,
89    ) -> Self {
90        let cache = settings.shared_cache.clone().unwrap_or_default();
91        let state = State::new(initial_transform);
92
93        Self::new_with(initial_transform, bbox, cache, xref, settings, state, 0)
94    }
95
96    pub(crate) fn new_with(
97        initial_transform: Affine,
98        bbox: Rect,
99        cache: Cache,
100        xref: &'a XRef,
101        settings: InterpreterSettings,
102        state: State<'a>,
103        nesting_depth: u32,
104    ) -> Self {
105        let ocg_state = {
106            let root_ref = xref.root_id();
107            xref.get::<Dict<'_>>(root_ref)
108                .map(|catalog| OcgState::from_catalog(&catalog))
109                .unwrap_or_default()
110        };
111
112        Self {
113            nesting_depth,
114            states: vec![state],
115            settings,
116            xref,
117            root_transforms: vec![initial_transform],
118            last_point: Point::default(),
119            sub_path_start: Point::default(),
120            clip: None,
121            bbox: vec![bbox],
122            path: BezPath::new(),
123            font_cache: HashMap::new(),
124            object_cache: cache,
125            ocg_state,
126            marks: MarkPlacement::default(),
127        }
128    }
129
130    /// Return the interpreter settings owned by this context.
131    pub fn into_settings(self) -> InterpreterSettings {
132        self.settings
133    }
134
135    pub(crate) fn save_state(&mut self) {
136        let Some(cur) = self.states.last().cloned() else {
137            warn!("attempted to save state without existing state");
138            return;
139        };
140
141        self.states.push(cur);
142    }
143
144    pub(crate) fn bbox(&self) -> Rect {
145        self.bbox.last().copied().unwrap_or_else(|| {
146            warn!("failed to get a bbox");
147
148            Rect::new(0.0, 0.0, 1.0, 1.0)
149        })
150    }
151
152    fn push_bbox(&mut self, bbox: Rect) {
153        let new = self.bbox().intersect(bbox);
154        self.bbox.push(new);
155    }
156
157    pub(crate) fn push_clip_path(
158        &mut self,
159        clip_path: BezPath,
160        fill: FillRule,
161        device: &mut impl Device<'a>,
162    ) {
163        if let Some(clip_rect) = path_as_rect(&clip_path) {
164            let cur_bbox = self.bbox();
165
166            // If the clip path is a rect and completely covers the current bbox, don't emit it.
167            if cur_bbox
168                .min_x()
169                .is_nearly_greater_or_equal(clip_rect.min_x())
170                && cur_bbox
171                    .min_y()
172                    .is_nearly_greater_or_equal(clip_rect.min_y())
173                && cur_bbox.max_x().is_nearly_less_or_equal(clip_rect.max_x())
174                && cur_bbox.max_y().is_nearly_less_or_equal(clip_rect.max_y())
175            {
176                self.get_mut().clips.push(ClipType::Dummy);
177                return;
178            }
179        }
180
181        let bbox = clip_path.bounding_box();
182        device.push_clip_path(&ClipPath {
183            path: clip_path,
184            fill,
185        });
186        self.push_bbox(bbox);
187        self.get_mut().clips.push(ClipType::Real);
188    }
189
190    pub(crate) fn pop_clip_path(&mut self, device: &mut impl Device<'a>) {
191        if let Some(ClipType::Real) = self.get_mut().clips.pop() {
192            device.pop_clip_path();
193            self.pop_bbox();
194        }
195    }
196
197    fn pop_bbox(&mut self) {
198        self.bbox.pop();
199    }
200
201    pub(crate) fn push_root_transform(&mut self) {
202        self.root_transforms.push(self.get().ctm);
203    }
204
205    pub(crate) fn pop_root_transform(&mut self) {
206        self.root_transforms.pop();
207    }
208
209    pub(crate) fn root_transform(&self) -> Affine {
210        self.root_transforms
211            .last()
212            .copied()
213            .unwrap_or(Affine::IDENTITY)
214    }
215
216    pub(crate) fn restore_state(&mut self, device: &mut impl Device<'a>) {
217        let Some(target_clips) = self
218            .states
219            .get(self.states.len().saturating_sub(2))
220            .map(|s| s.clips.len())
221        else {
222            warn!("underflowed graphics state");
223            return;
224        };
225
226        while self.get().clips.len() > target_clips {
227            self.pop_clip_path(device);
228        }
229
230        // The first state should never be popped.
231        if self.states.len() > 1 {
232            self.states.pop();
233        }
234
235        device.set_soft_mask(
236            self.states
237                .last()
238                .and_then(|l| l.graphics_state.soft_mask.clone()),
239        );
240    }
241
242    pub(crate) fn path(&self) -> &BezPath {
243        &self.path
244    }
245
246    pub(crate) fn path_mut(&mut self) -> &mut BezPath {
247        &mut self.path
248    }
249
250    pub(crate) fn sub_path_start(&self) -> &Point {
251        &self.sub_path_start
252    }
253
254    pub(crate) fn sub_path_start_mut(&mut self) -> &mut Point {
255        &mut self.sub_path_start
256    }
257
258    pub(crate) fn last_point(&self) -> &Point {
259        &self.last_point
260    }
261
262    pub(crate) fn last_point_mut(&mut self) -> &mut Point {
263        &mut self.last_point
264    }
265
266    pub(crate) fn clip(&self) -> &Option<FillRule> {
267        &self.clip
268    }
269
270    pub(crate) fn clip_mut(&mut self) -> &mut Option<FillRule> {
271        &mut self.clip
272    }
273
274    /// The depth this interpretation sits at.
275    ///
276    /// Read by the constructs that build a FRESH `Context` — tiling patterns,
277    /// soft masks, Type 3 glyphs — so the child starts where the parent left off
278    /// instead of at zero. Without that, a cycle alternating between two of them
279    /// never reaches the limit: measured on a 1082-byte file whose pattern cell
280    /// sets a soft mask whose group paints with that pattern, rc=134 (#318).
281    pub(crate) fn nesting_depth(&self) -> u32 {
282        self.nesting_depth
283    }
284
285    /// Claim one level of nesting, or refuse.
286    ///
287    /// Returns false at the limit; the caller must then not interpret. Pair with
288    /// `end_nested_interpretation`, which is why this is not a plain `+= 1`: a
289    /// page draws many XObjects in sequence, and a counter that only rises would
290    /// refuse the fifty-first sibling rather than the fifty-first ancestor.
291    pub(crate) fn begin_nested_interpretation(&mut self) -> bool {
292        if self.nesting_depth >= MAX_NESTED_INTERPRETATION_DEPTH {
293            return false;
294        }
295
296        self.nesting_depth += 1;
297
298        true
299    }
300
301    pub(crate) fn end_nested_interpretation(&mut self) {
302        self.nesting_depth = self.nesting_depth.saturating_sub(1);
303    }
304
305    pub(crate) fn get(&self) -> &State<'a> {
306        self.states.last().unwrap()
307    }
308
309    pub(crate) fn get_mut(&mut self) -> &mut State<'a> {
310        self.states.last_mut().unwrap()
311    }
312
313    pub(crate) fn pre_concat_transform(&mut self, transform: Transform) {
314        self.pre_concat_affine(convert_transform(transform));
315    }
316
317    pub(crate) fn pre_concat_affine(&mut self, transform: Affine) {
318        self.get_mut().ctm *= transform;
319    }
320
321    pub(crate) fn get_color_space(
322        &mut self,
323        resources: &Resources<'_>,
324        name: Name,
325    ) -> Option<ColorSpace> {
326        let cs_object = resources.get_color_space(name)?;
327        self.object_cache
328            .get_or_insert_with(cs_object.cache_key(), || {
329                ColorSpace::new(
330                    cs_object.clone(),
331                    &self.object_cache,
332                    &self.settings.warning_sink,
333                )
334            })
335    }
336
337    pub(crate) fn stroke_props(&self) -> StrokeProps {
338        self.get().graphics_state.stroke_props.clone()
339    }
340
341    pub(crate) fn num_states(&self) -> usize {
342        self.states.len()
343    }
344
345    pub(crate) fn resolve_font(&mut self, font_dict: &Dict<'a>) -> Option<TextStateFont<'a>> {
346        let cache_key = font_dict.cache_key();
347
348        if let Some(resolved) = self
349            .font_cache
350            .entry(cache_key)
351            .or_insert_with(|| {
352                Font::new(
353                    font_dict,
354                    &self.settings.font_resolver,
355                    &self.settings.cmap_resolver,
356                    &self.settings.warning_sink,
357                )
358            })
359            .clone()
360        {
361            Some(TextStateFont::Font(resolved))
362        } else {
363            Font::new_standard(StandardFont::Helvetica, &self.settings.font_resolver)
364                .map(TextStateFont::Fallback)
365        }
366    }
367}
368
369pub(crate) fn path_as_rect(path: &BezPath) -> Option<Rect> {
370    let points = match path.elements() {
371        [
372            PathEl::MoveTo(first),
373            PathEl::LineTo(second),
374            PathEl::LineTo(third),
375            PathEl::LineTo(fourth),
376            PathEl::ClosePath,
377        ] => [*first, *second, *third, *fourth, *first],
378        [
379            PathEl::MoveTo(first),
380            PathEl::LineTo(second),
381            PathEl::LineTo(third),
382            PathEl::LineTo(fourth),
383            PathEl::LineTo(last),
384        ] if first.x.is_nearly_equal(last.x) && first.y.is_nearly_equal(last.y) => {
385            [*first, *second, *third, *fourth, *last]
386        }
387        _ => return None,
388    };
389
390    let mut previous_axis = None;
391    for edge in points.windows(2) {
392        let same_x = edge[0].x.is_nearly_equal(edge[1].x);
393        let same_y = edge[0].y.is_nearly_equal(edge[1].y);
394        if same_x == same_y || previous_axis == Some(same_x) {
395            return None;
396        }
397        previous_axis = Some(same_x);
398    }
399
400    Some(path.fast_bounding_box())
401}
402
403#[cfg(test)]
404mod clip_path_shape {
405    use super::*;
406
407    /// A bowtie is not a rectangle, however many of its corners are.
408    ///
409    /// The version this replaces decided by collecting which corners of the
410    /// bounding box the path touches. A self-intersecting quadrilateral touches
411    /// all four, so it was accepted and a clip shaped like an hourglass became
412    /// its bounding box -- content drawn where the document said to hide it.
413    /// Ported from LaurenzV/hayro#1338; measured here before and after, the same
414    /// path answered `true` and now answers `false`.
415    #[test]
416    fn a_bowtie_is_not_a_rect() {
417        let mut bowtie = BezPath::new();
418        bowtie.move_to((0.0, 0.0));
419        bowtie.line_to((10.0, 10.0));
420        bowtie.line_to((10.0, 0.0));
421        bowtie.line_to((0.0, 10.0));
422        bowtie.close_path();
423
424        assert!(
425            path_as_rect(&bowtie).is_none(),
426            "a self-intersecting quadrilateral was accepted as a rectangle, so a \
427             clip would cover its bounding box instead of its actual shape"
428        );
429    }
430
431    /// The other half: the fix must not refuse ordinary rectangles.
432    ///
433    /// Without this, returning `None` unconditionally would pass the test above
434    /// and turn every rectangular clip into a general path -- slower, and a
435    /// silent behaviour change nobody asked for.
436    #[test]
437    fn an_ordinary_rectangle_still_is_one() {
438        let mut rect = BezPath::new();
439        rect.move_to((0.0, 0.0));
440        rect.line_to((10.0, 0.0));
441        rect.line_to((10.0, 10.0));
442        rect.line_to((0.0, 10.0));
443        rect.close_path();
444
445        assert!(
446            path_as_rect(&rect).is_some(),
447            "a plain rectangle stopped being recognised as one"
448        );
449    }
450
451    /// The unclosed arm: `MoveTo + 4 LineTo` is accepted only when the last
452    /// point returns to the first.
453    ///
454    /// This is a separate acceptance path from the `ClosePath` form, and it is
455    /// also a tightening -- the version it replaces took that shape whether or
456    /// not it closed. Without a test, deleting the arm's guard or inverting it
457    /// changes nothing that goes red, and it is the arm most likely to be
458    /// "simplified" later by someone who sees two arms doing the same thing.
459    #[test]
460    fn four_lines_that_do_not_return_to_the_start_are_not_a_rect() {
461        let mut open = BezPath::new();
462        open.move_to((0.0, 0.0));
463        open.line_to((10.0, 0.0));
464        open.line_to((10.0, 10.0));
465        open.line_to((0.0, 10.0));
466        open.line_to((0.0, 5.0)); // back down the left edge, but not to the start
467
468        assert!(
469            path_as_rect(&open).is_none(),
470            "an unclosed four-line path was accepted as a rectangle, so a clip \
471             would cover the closed shape the path never drew"
472        );
473    }
474
475    /// And the same arm must still accept the shape it exists for.
476    ///
477    /// Paired with the test above so that neither "always None" nor "always
478    /// Some" passes both.
479    #[test]
480    fn four_lines_that_do_return_to_the_start_are_a_rect() {
481        let mut closed = BezPath::new();
482        closed.move_to((0.0, 0.0));
483        closed.line_to((10.0, 0.0));
484        closed.line_to((10.0, 10.0));
485        closed.line_to((0.0, 10.0));
486        closed.line_to((0.0, 0.0));
487
488        assert!(
489            path_as_rect(&closed).is_some(),
490            "a four-line rectangle that returns to its start stopped being one"
491        );
492    }
493}