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/// A context for interpreting PDF files.
19pub struct Context<'a> {
20    states: Vec<State<'a>>,
21    path: BezPath,
22    sub_path_start: Point,
23    last_point: Point,
24    clip: Option<FillRule>,
25    pub(crate) font_cache: HashMap<u128, Option<Font<'a>>>,
26    root_transforms: Vec<Affine>,
27    bbox: Vec<Rect>,
28    pub(crate) settings: InterpreterSettings,
29    pub(crate) object_cache: Cache,
30    pub(crate) xref: &'a XRef,
31    pub(crate) ocg_state: OcgState,
32}
33
34impl<'a> Context<'a> {
35    /// Create a new context.
36    pub fn new(
37        initial_transform: Affine,
38        bbox: Rect,
39        xref: &'a XRef,
40        settings: InterpreterSettings,
41    ) -> Self {
42        let cache = Cache::new();
43        let state = State::new(initial_transform);
44
45        Self::new_with(initial_transform, bbox, cache, xref, settings, state)
46    }
47
48    pub(crate) fn new_with(
49        initial_transform: Affine,
50        bbox: Rect,
51        cache: Cache,
52        xref: &'a XRef,
53        settings: InterpreterSettings,
54        state: State<'a>,
55    ) -> Self {
56        let ocg_state = {
57            let root_ref = xref.root_id();
58            xref.get::<Dict<'_>>(root_ref)
59                .map(|catalog| OcgState::from_catalog(&catalog))
60                .unwrap_or_default()
61        };
62
63        Self {
64            states: vec![state],
65            settings,
66            xref,
67            root_transforms: vec![initial_transform],
68            last_point: Point::default(),
69            sub_path_start: Point::default(),
70            clip: None,
71            bbox: vec![bbox],
72            path: BezPath::new(),
73            font_cache: HashMap::new(),
74            object_cache: cache,
75            ocg_state,
76        }
77    }
78
79    /// Return the interpreter settings owned by this context.
80    pub fn into_settings(self) -> InterpreterSettings {
81        self.settings
82    }
83
84    pub(crate) fn save_state(&mut self) {
85        let Some(cur) = self.states.last().cloned() else {
86            warn!("attempted to save state without existing state");
87            return;
88        };
89
90        self.states.push(cur);
91    }
92
93    pub(crate) fn bbox(&self) -> Rect {
94        self.bbox.last().copied().unwrap_or_else(|| {
95            warn!("failed to get a bbox");
96
97            Rect::new(0.0, 0.0, 1.0, 1.0)
98        })
99    }
100
101    fn push_bbox(&mut self, bbox: Rect) {
102        let new = self.bbox().intersect(bbox);
103        self.bbox.push(new);
104    }
105
106    pub(crate) fn push_clip_path(
107        &mut self,
108        clip_path: BezPath,
109        fill: FillRule,
110        device: &mut impl Device<'a>,
111    ) {
112        if let Some(clip_rect) = path_as_rect(&clip_path) {
113            let cur_bbox = self.bbox();
114
115            // If the clip path is a rect and completely covers the current bbox, don't emit it.
116            if cur_bbox
117                .min_x()
118                .is_nearly_greater_or_equal(clip_rect.min_x())
119                && cur_bbox
120                    .min_y()
121                    .is_nearly_greater_or_equal(clip_rect.min_y())
122                && cur_bbox.max_x().is_nearly_less_or_equal(clip_rect.max_x())
123                && cur_bbox.max_y().is_nearly_less_or_equal(clip_rect.max_y())
124            {
125                self.get_mut().clips.push(ClipType::Dummy);
126                return;
127            }
128        }
129
130        let bbox = clip_path.bounding_box();
131        device.push_clip_path(&ClipPath {
132            path: clip_path,
133            fill,
134        });
135        self.push_bbox(bbox);
136        self.get_mut().clips.push(ClipType::Real);
137    }
138
139    pub(crate) fn pop_clip_path(&mut self, device: &mut impl Device<'a>) {
140        if let Some(ClipType::Real) = self.get_mut().clips.pop() {
141            device.pop_clip_path();
142            self.pop_bbox();
143        }
144    }
145
146    fn pop_bbox(&mut self) {
147        self.bbox.pop();
148    }
149
150    pub(crate) fn push_root_transform(&mut self) {
151        self.root_transforms.push(self.get().ctm);
152    }
153
154    pub(crate) fn pop_root_transform(&mut self) {
155        self.root_transforms.pop();
156    }
157
158    pub(crate) fn root_transform(&self) -> Affine {
159        self.root_transforms
160            .last()
161            .copied()
162            .unwrap_or(Affine::IDENTITY)
163    }
164
165    pub(crate) fn restore_state(&mut self, device: &mut impl Device<'a>) {
166        let Some(target_clips) = self
167            .states
168            .get(self.states.len().saturating_sub(2))
169            .map(|s| s.clips.len())
170        else {
171            warn!("underflowed graphics state");
172            return;
173        };
174
175        while self.get().clips.len() > target_clips {
176            self.pop_clip_path(device);
177        }
178
179        // The first state should never be popped.
180        if self.states.len() > 1 {
181            self.states.pop();
182        }
183
184        device.set_soft_mask(
185            self.states
186                .last()
187                .and_then(|l| l.graphics_state.soft_mask.clone()),
188        );
189    }
190
191    pub(crate) fn path(&self) -> &BezPath {
192        &self.path
193    }
194
195    pub(crate) fn path_mut(&mut self) -> &mut BezPath {
196        &mut self.path
197    }
198
199    pub(crate) fn sub_path_start(&self) -> &Point {
200        &self.sub_path_start
201    }
202
203    pub(crate) fn sub_path_start_mut(&mut self) -> &mut Point {
204        &mut self.sub_path_start
205    }
206
207    pub(crate) fn last_point(&self) -> &Point {
208        &self.last_point
209    }
210
211    pub(crate) fn last_point_mut(&mut self) -> &mut Point {
212        &mut self.last_point
213    }
214
215    pub(crate) fn clip(&self) -> &Option<FillRule> {
216        &self.clip
217    }
218
219    pub(crate) fn clip_mut(&mut self) -> &mut Option<FillRule> {
220        &mut self.clip
221    }
222
223    pub(crate) fn get(&self) -> &State<'a> {
224        self.states.last().unwrap()
225    }
226
227    pub(crate) fn get_mut(&mut self) -> &mut State<'a> {
228        self.states.last_mut().unwrap()
229    }
230
231    pub(crate) fn pre_concat_transform(&mut self, transform: Transform) {
232        self.pre_concat_affine(convert_transform(transform));
233    }
234
235    pub(crate) fn pre_concat_affine(&mut self, transform: Affine) {
236        self.get_mut().ctm *= transform;
237    }
238
239    pub(crate) fn get_color_space(
240        &mut self,
241        resources: &Resources<'_>,
242        name: Name,
243    ) -> Option<ColorSpace> {
244        let cs_object = resources.get_color_space(name)?;
245        self.object_cache
246            .get_or_insert_with(cs_object.cache_key(), || {
247                ColorSpace::new(cs_object.clone(), &self.object_cache)
248            })
249    }
250
251    pub(crate) fn stroke_props(&self) -> StrokeProps {
252        self.get().graphics_state.stroke_props.clone()
253    }
254
255    pub(crate) fn num_states(&self) -> usize {
256        self.states.len()
257    }
258
259    pub(crate) fn resolve_font(&mut self, font_dict: &Dict<'a>) -> Option<TextStateFont<'a>> {
260        let cache_key = font_dict.cache_key();
261
262        if let Some(resolved) = self
263            .font_cache
264            .entry(cache_key)
265            .or_insert_with(|| {
266                Font::new(
267                    font_dict,
268                    &self.settings.font_resolver,
269                    &self.settings.cmap_resolver,
270                )
271            })
272            .clone()
273        {
274            Some(TextStateFont::Font(resolved))
275        } else {
276            Font::new_standard(StandardFont::Helvetica, &self.settings.font_resolver)
277                .map(TextStateFont::Fallback)
278        }
279    }
280}
281
282pub(crate) fn path_as_rect(path: &BezPath) -> Option<Rect> {
283    // One MoveTo, three LineTo, one ClosePath
284    if path.elements().len() != 5 {
285        return None;
286    }
287
288    let bbox = path.fast_bounding_box();
289    let (min_x, min_y, max_x, max_y) = (bbox.min_x(), bbox.min_y(), bbox.max_x(), bbox.max_y());
290    let mut corners = [false; 4];
291
292    let mut check_point = |p: Point| {
293        corners[0] |= p.x.is_nearly_equal(min_x) && p.y.is_nearly_equal(min_y);
294        corners[1] |= p.x.is_nearly_equal(min_x) && p.y.is_nearly_equal(max_y);
295        corners[2] |= p.x.is_nearly_equal(max_x) && p.y.is_nearly_equal(min_y);
296        corners[3] |= p.x.is_nearly_equal(max_x) && p.y.is_nearly_equal(max_y);
297    };
298
299    for (idx, el) in path.elements().iter().enumerate() {
300        match el {
301            PathEl::MoveTo(p) => {
302                if idx != 0 {
303                    return None;
304                }
305
306                check_point(*p);
307            }
308            PathEl::LineTo(l) => check_point(*l),
309            PathEl::QuadTo(_, _) => return None,
310            PathEl::CurveTo(_, _, _) => return None,
311            PathEl::ClosePath => {}
312        }
313    }
314
315    if corners[0] && corners[1] && corners[2] && corners[3] {
316        Some(bbox)
317    } else {
318        None
319    }
320}