hayro_interpret/
context.rs

1use crate::cache::{Cache, CacheKey};
2use crate::color::ColorSpace;
3use crate::convert::convert_transform;
4use crate::font::Font;
5use crate::interpret::state::{ClipType, State};
6use crate::ocg::OcgState;
7use crate::util::Float64Ext;
8use crate::{ClipPath, Device, FillRule, InterpreterSettings, StrokeProps};
9use hayro_syntax::content::ops::Transform;
10use hayro_syntax::object::Dict;
11use hayro_syntax::object::Name;
12use hayro_syntax::page::Resources;
13use hayro_syntax::xref::XRef;
14use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape};
15use log::warn;
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    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    pub(crate) fn save_state(&mut self) {
80        let Some(cur) = self.states.last().cloned() else {
81            warn!("attempted to save state without existing state");
82            return;
83        };
84
85        self.states.push(cur);
86    }
87
88    pub(crate) fn bbox(&self) -> Rect {
89        self.bbox.last().copied().unwrap_or_else(|| {
90            warn!("failed to get a bbox");
91
92            Rect::new(0.0, 0.0, 1.0, 1.0)
93        })
94    }
95
96    fn push_bbox(&mut self, bbox: Rect) {
97        let new = self.bbox().intersect(bbox);
98        self.bbox.push(new);
99    }
100
101    pub(crate) fn push_clip_path(
102        &mut self,
103        clip_path: BezPath,
104        fill: FillRule,
105        device: &mut impl Device<'a>,
106    ) {
107        if let Some(clip_rect) = path_as_rect(&clip_path) {
108            let cur_bbox = self.bbox();
109
110            // If the clip path is a rect and completely covers the current bbox, don't emit it.
111            if cur_bbox
112                .min_x()
113                .is_nearly_greater_or_equal(clip_rect.min_x())
114                && cur_bbox
115                    .min_y()
116                    .is_nearly_greater_or_equal(clip_rect.min_y())
117                && cur_bbox.max_x().is_nearly_less_or_equal(clip_rect.max_x())
118                && cur_bbox.max_y().is_nearly_less_or_equal(clip_rect.max_y())
119            {
120                self.get_mut().clips.push(ClipType::Dummy);
121                return;
122            }
123        }
124
125        let bbox = clip_path.bounding_box();
126        device.push_clip_path(&ClipPath {
127            path: clip_path,
128            fill,
129        });
130        self.push_bbox(bbox);
131        self.get_mut().clips.push(ClipType::Real);
132    }
133
134    pub(crate) fn pop_clip_path(&mut self, device: &mut impl Device<'a>) {
135        if let Some(ClipType::Real) = self.get_mut().clips.pop() {
136            device.pop_clip_path();
137            self.pop_bbox();
138        }
139    }
140
141    fn pop_bbox(&mut self) {
142        self.bbox.pop();
143    }
144
145    pub(crate) fn push_root_transform(&mut self) {
146        self.root_transforms.push(self.get().ctm);
147    }
148
149    pub(crate) fn pop_root_transform(&mut self) {
150        self.root_transforms.pop();
151    }
152
153    pub(crate) fn root_transform(&self) -> Affine {
154        self.root_transforms
155            .last()
156            .copied()
157            .unwrap_or(Affine::IDENTITY)
158    }
159
160    pub(crate) fn restore_state(&mut self, device: &mut impl Device<'a>) {
161        let Some(target_clips) = self
162            .states
163            .get(self.states.len().saturating_sub(2))
164            .map(|s| s.clips.len())
165        else {
166            warn!("underflowed graphics state");
167            return;
168        };
169
170        while self.get().clips.len() > target_clips {
171            self.pop_clip_path(device);
172        }
173
174        // The first state should never be popped.
175        if self.states.len() > 1 {
176            self.states.pop();
177        }
178    }
179
180    pub(crate) fn path(&self) -> &BezPath {
181        &self.path
182    }
183
184    pub(crate) fn path_mut(&mut self) -> &mut BezPath {
185        &mut self.path
186    }
187
188    pub(crate) fn sub_path_start(&self) -> &Point {
189        &self.sub_path_start
190    }
191
192    pub(crate) fn sub_path_start_mut(&mut self) -> &mut Point {
193        &mut self.sub_path_start
194    }
195
196    pub(crate) fn last_point(&self) -> &Point {
197        &self.last_point
198    }
199
200    pub(crate) fn last_point_mut(&mut self) -> &mut Point {
201        &mut self.last_point
202    }
203
204    pub(crate) fn clip(&self) -> &Option<FillRule> {
205        &self.clip
206    }
207
208    pub(crate) fn clip_mut(&mut self) -> &mut Option<FillRule> {
209        &mut self.clip
210    }
211
212    pub(crate) fn get(&self) -> &State<'a> {
213        self.states.last().unwrap()
214    }
215
216    pub(crate) fn get_mut(&mut self) -> &mut State<'a> {
217        self.states.last_mut().unwrap()
218    }
219
220    pub(crate) fn pre_concat_transform(&mut self, transform: Transform) {
221        self.pre_concat_affine(convert_transform(transform));
222    }
223
224    pub(crate) fn pre_concat_affine(&mut self, transform: Affine) {
225        self.get_mut().ctm *= transform;
226    }
227
228    pub(crate) fn get_font(
229        &mut self,
230        resources: &Resources<'a>,
231        name: Name<'_>,
232    ) -> Option<Font<'a>> {
233        let font_dict = resources.get_font(name)?;
234        let cache_key = font_dict.cache_key();
235        self.font_cache
236            .entry(cache_key)
237            .or_insert_with(|| Font::new(&font_dict, &self.settings.font_resolver))
238            .clone()
239    }
240
241    pub(crate) fn get_color_space(
242        &mut self,
243        resources: &Resources<'_>,
244        name: Name<'_>,
245    ) -> Option<ColorSpace> {
246        let cs_object = resources.get_color_space(name)?;
247        self.object_cache
248            .get_or_insert_with(cs_object.cache_key(), || {
249                ColorSpace::new(cs_object.clone(), &self.object_cache)
250            })
251    }
252
253    pub(crate) fn stroke_props(&self) -> StrokeProps {
254        self.get().graphics_state.stroke_props.clone()
255    }
256
257    pub(crate) fn num_states(&self) -> usize {
258        self.states.len()
259    }
260}
261
262fn path_as_rect(path: &BezPath) -> Option<Rect> {
263    let bbox = path.bounding_box();
264    let (min_x, min_y, max_x, max_y) = (bbox.min_x(), bbox.min_y(), bbox.max_x(), bbox.max_y());
265    let mut touched = [false; 4];
266
267    // One MoveTo, three LineTo, one ClosePath
268    if path.elements().len() != 5 {
269        return None;
270    }
271
272    let mut check_point = |p: Point| {
273        touched[0] |= p.x.is_nearly_equal(min_x);
274        touched[1] |= p.y.is_nearly_equal(min_y);
275        touched[2] |= p.x.is_nearly_equal(max_x);
276        touched[3] |= p.y.is_nearly_equal(max_y);
277    };
278
279    for el in path.elements() {
280        match el {
281            PathEl::MoveTo(p) => check_point(*p),
282            PathEl::LineTo(l) => check_point(*l),
283            PathEl::QuadTo(_, _) => {
284                return None;
285            }
286            PathEl::CurveTo(_, _, _) => {
287                return None;
288            }
289            PathEl::ClosePath => {}
290        }
291    }
292
293    if touched[0] && touched[1] && touched[2] && touched[3] {
294        Some(bbox)
295    } else {
296        None
297    }
298}