hayro_interpret/interpret/
mod.rs

1use crate::ClipPath;
2use crate::FillRule;
3use crate::color::ColorSpace;
4use crate::context::Context;
5use crate::convert::{convert_line_cap, convert_line_join};
6use crate::device::Device;
7use crate::font::{FontData, FontQuery};
8use crate::interpret::path::{
9    close_path, fill_path, fill_path_impl, fill_stroke_path, stroke_path,
10};
11use crate::interpret::state::{handle_gs, restore_state, save_sate};
12use crate::interpret::text::TextRenderingMode;
13use crate::pattern::{Pattern, ShadingPattern};
14use crate::shading::Shading;
15use crate::util::OptionLog;
16use crate::x_object::{ImageXObject, XObject, draw_image_xobject, draw_xobject};
17use hayro_syntax::content::ops::TypedInstruction;
18use hayro_syntax::object::dict::keys::OC;
19use hayro_syntax::object::{Dict, Object, dict_or_stream};
20use hayro_syntax::page::{Page, Resources};
21use kurbo::{Affine, Point, Shape};
22use log::warn;
23use smallvec::smallvec;
24use std::sync::Arc;
25
26pub(crate) mod path;
27pub(crate) mod state;
28pub(crate) mod text;
29
30/// A callback function for resolving font queries.
31///
32/// The first argument is the raw data, the second argument is the index in case the font
33/// is a TTC, otherwise it should be 0.
34pub type FontResolverFn = Arc<dyn Fn(&FontQuery) -> Option<(FontData, u32)> + Send + Sync>;
35/// A callback function for resolving warnings during interpretation.
36pub type WarningSinkFn = Arc<dyn Fn(InterpreterWarning) + Send + Sync>;
37
38#[derive(Clone)]
39/// Settings that should be applied during the interpretation process.
40pub struct InterpreterSettings {
41    /// Nearly every PDF contains text. In most cases, PDF files embed the fonts they use, and
42    /// hayro can therefore read the font files and do all the processing needed. However, there
43    /// are two problems:
44    /// - Fonts don't _have_ to be embedded, it's possible that the PDF file only defines the basic
45    ///   metadata of the font, like its name, but relies on the PDF processor to find that font
46    ///   in its environment.
47    /// - The PDF specification requires a list of 14 fonts that should always be available to a
48    ///   PDF processor. These include:
49    ///   - Times New Roman (Normal, Bold, Italic, BoldItalic)
50    ///   - Courier (Normal, Bold, Italic, BoldItalic)
51    ///   - Helvetica (Normal, Bold, Italic, BoldItalic)
52    ///   - ZapfDingBats
53    ///   - Symbol
54    ///
55    /// Because of this, if any of the above situations occurs, this callback will be called, which
56    /// expects the data of an appropriate font to be returned, if available. If no such font is
57    /// provided, the text will most likely fail to render.
58    ///
59    /// For the font data, there are two different formats that are accepted:
60    /// - Any valid TTF/OTF font.
61    /// - A valid CFF font program.
62    ///
63    /// The following recommendations are given for the implementation of this callback function.
64    ///
65    /// For the standard fonts, in case the original fonts are available on the system, you should
66    /// just return those. Otherwise, for Helvetica, Courier and Times New Roman, the best alternative
67    /// are the corresponding fonts of the [Liberation font family](https://github.com/liberationfonts/liberation-fonts).
68    /// If you prefer smaller fonts, you can use the [Foxit CFF fonts](https://github.com/LaurenzV/hayro/tree/master/assets/standard_fonts),
69    /// which are much smaller but are missing glyphs for certain scripts.
70    ///
71    /// For the `Symbol` and `ZapfDingBats` fonts, you should also prefer the system fonts, and if
72    /// not available to you, you can, similarly to above, use the corresponding fonts from Foxit.
73    ///
74    /// If you don't want having to deal with this, you can just enable the `embed-fonts` feature
75    /// and use the default implementation of the callback.
76    pub font_resolver: FontResolverFn,
77
78    /// In certain cases, `hayro` will emit a warning in case an issue was encountered while interpreting
79    /// the PDF file. Providing a callback allows you to catch those warnings and handle them, if desired.
80    pub warning_sink: WarningSinkFn,
81}
82
83impl Default for InterpreterSettings {
84    fn default() -> Self {
85        Self {
86            #[cfg(not(feature = "embed-fonts"))]
87            font_resolver: Arc::new(|_| None),
88            #[cfg(feature = "embed-fonts")]
89            font_resolver: Arc::new(|query| match query {
90                FontQuery::Standard(s) => Some(s.get_font_data()),
91                FontQuery::Fallback(f) => Some(f.pick_standard_font().get_font_data()),
92            }),
93            warning_sink: Arc::new(|_| {}),
94        }
95    }
96}
97
98#[derive(Copy, Clone, Debug)]
99/// Warnings that can occur while interpreting a PDF file.
100pub enum InterpreterWarning {
101    /// A JPX image was encountered, even though the `jpeg2000` feature is not enabled.
102    JpxImage,
103    /// An unsupported font kind was encountered.
104    ///
105    /// Currently, only CID fonts with non-identity encoding are unsupported.
106    UnsupportedFont,
107    /// An image failed to decode.
108    ImageDecodeFailure,
109}
110
111/// interpret the contents of the page and render them into the device.
112pub fn interpret_page<'a>(
113    page: &Page<'a>,
114    context: &mut Context<'a>,
115    device: &mut impl Device<'a>,
116) {
117    let resources = page.resources();
118    interpret(page.typed_operations(), resources, context, device)
119}
120
121/// Interpret the instructions from `ops` and render them into the device.
122pub fn interpret<'a, 'b>(
123    ops: impl Iterator<Item = TypedInstruction<'b>>,
124    resources: &Resources<'a>,
125    context: &mut Context<'a>,
126    device: &mut impl Device<'a>,
127) {
128    let num_states = context.num_states();
129    let n_clips = context.get().n_clips;
130
131    save_sate(context);
132
133    for op in ops {
134        match op {
135            TypedInstruction::SaveState(_) => save_sate(context),
136            TypedInstruction::StrokeColorDeviceRgb(s) => {
137                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
138                context.get_mut().graphics_state.stroke_color =
139                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
140            }
141            TypedInstruction::StrokeColorDeviceGray(s) => {
142                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
143                context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
144            }
145            TypedInstruction::StrokeColorCmyk(s) => {
146                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
147                context.get_mut().graphics_state.stroke_color =
148                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
149            }
150            TypedInstruction::LineWidth(w) => {
151                context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32();
152            }
153            TypedInstruction::LineCap(c) => {
154                context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c);
155            }
156            TypedInstruction::LineJoin(j) => {
157                context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j);
158            }
159            TypedInstruction::MiterLimit(l) => {
160                context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32();
161            }
162            TypedInstruction::Transform(t) => {
163                context.pre_concat_transform(t);
164            }
165            TypedInstruction::RectPath(r) => {
166                let rect = kurbo::Rect::new(
167                    r.0.as_f64(),
168                    r.1.as_f64(),
169                    r.0.as_f64() + r.2.as_f64(),
170                    r.1.as_f64() + r.3.as_f64(),
171                )
172                .to_path(0.1);
173                context.path_mut().extend(rect);
174            }
175            TypedInstruction::MoveTo(m) => {
176                let p = Point::new(m.0.as_f64(), m.1.as_f64());
177                *(context.last_point_mut()) = p;
178                *(context.sub_path_start_mut()) = p;
179                context.path_mut().move_to(p);
180            }
181            TypedInstruction::FillPathEvenOdd(_) => {
182                fill_path(context, device, FillRule::EvenOdd);
183            }
184            TypedInstruction::FillPathNonZero(_) => {
185                fill_path(context, device, FillRule::NonZero);
186            }
187            TypedInstruction::FillPathNonZeroCompatibility(_) => {
188                fill_path(context, device, FillRule::NonZero);
189            }
190            TypedInstruction::FillAndStrokeEvenOdd(_) => {
191                fill_stroke_path(context, device, FillRule::EvenOdd);
192            }
193            TypedInstruction::FillAndStrokeNonZero(_) => {
194                fill_stroke_path(context, device, FillRule::NonZero);
195            }
196            TypedInstruction::CloseAndStrokePath(_) => {
197                close_path(context);
198                stroke_path(context, device);
199            }
200            TypedInstruction::CloseFillAndStrokeEvenOdd(_) => {
201                close_path(context);
202                fill_stroke_path(context, device, FillRule::EvenOdd);
203            }
204            TypedInstruction::CloseFillAndStrokeNonZero(_) => {
205                close_path(context);
206                fill_stroke_path(context, device, FillRule::NonZero);
207            }
208            TypedInstruction::NonStrokeColorDeviceGray(s) => {
209                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
210                context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
211            }
212            TypedInstruction::NonStrokeColorDeviceRgb(s) => {
213                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
214                context.get_mut().graphics_state.non_stroke_color =
215                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
216            }
217            TypedInstruction::NonStrokeColorCmyk(s) => {
218                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
219                context.get_mut().graphics_state.non_stroke_color =
220                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
221            }
222            TypedInstruction::LineTo(m) => {
223                if !context.path().elements().is_empty() {
224                    let last_point = *context.last_point();
225                    let mut p = Point::new(m.0.as_f64(), m.1.as_f64());
226                    *(context.last_point_mut()) = p;
227                    if last_point == p {
228                        // Add a small delta so that zero width lines can still have a round stroke.
229                        p.x += 0.0001;
230                    }
231
232                    context.path_mut().line_to(p);
233                }
234            }
235            TypedInstruction::CubicTo(c) => {
236                if !context.path().elements().is_empty() {
237                    let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
238                    let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
239                    let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
240
241                    *(context.last_point_mut()) = p3;
242
243                    context.path_mut().curve_to(p1, p2, p3)
244                }
245            }
246            TypedInstruction::CubicStartTo(c) => {
247                if !context.path().elements().is_empty() {
248                    let p1 = *context.last_point();
249                    let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
250                    let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
251
252                    *(context.last_point_mut()) = p3;
253
254                    context.path_mut().curve_to(p1, p2, p3)
255                }
256            }
257            TypedInstruction::CubicEndTo(c) => {
258                if !context.path().elements().is_empty() {
259                    let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
260                    let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
261
262                    *(context.last_point_mut()) = p3;
263
264                    context.path_mut().curve_to(p2, p3, p3)
265                }
266            }
267            TypedInstruction::ClosePath(_) => {
268                close_path(context);
269            }
270            TypedInstruction::SetGraphicsState(gs) => {
271                if let Some(gs) = resources
272                    .get_ext_g_state::<Dict>(gs.0.clone(), Box::new(|_| None), Box::new(Some))
273                    .warn_none(&format!("failed to get extgstate {}", gs.0.as_str()))
274                {
275                    handle_gs(&gs, context, resources);
276                }
277            }
278            TypedInstruction::StrokePath(_) => {
279                stroke_path(context, device);
280            }
281            TypedInstruction::EndPath(_) => {
282                if let Some(clip) = *context.clip()
283                    && !context.path().elements().is_empty()
284                {
285                    let clip_path = context.get().ctm * context.path().clone();
286                    context.push_bbox(clip_path.bounding_box());
287
288                    device.push_clip_path(&ClipPath {
289                        path: clip_path,
290                        fill: clip,
291                    });
292
293                    context.get_mut().n_clips += 1;
294
295                    *(context.clip_mut()) = None;
296                }
297
298                context.path_mut().truncate(0);
299            }
300            TypedInstruction::NonStrokeColor(c) => {
301                let fill_c = &mut context.get_mut().graphics_state.non_stroke_color;
302                fill_c.truncate(0);
303
304                for e in c.0 {
305                    fill_c.push(e.as_f32());
306                }
307            }
308            TypedInstruction::StrokeColor(c) => {
309                let stroke_c = &mut context.get_mut().graphics_state.stroke_color;
310                stroke_c.truncate(0);
311
312                for e in c.0 {
313                    stroke_c.push(e.as_f32());
314                }
315            }
316            TypedInstruction::ClipNonZero(_) => {
317                *(context.clip_mut()) = Some(FillRule::NonZero);
318            }
319            TypedInstruction::ClipEvenOdd(_) => {
320                *(context.clip_mut()) = Some(FillRule::EvenOdd);
321            }
322            TypedInstruction::RestoreState(_) => restore_state(context, device),
323            TypedInstruction::FlatnessTolerance(_) => {
324                // Ignore for now.
325            }
326            TypedInstruction::ColorSpaceStroke(c) => {
327                let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
328                    named
329                } else {
330                    context
331                        .get_color_space(resources, c.0)
332                        .unwrap_or(ColorSpace::device_gray())
333                };
334
335                context.get_mut().graphics_state.stroke_color = cs.initial_color();
336                context.get_mut().graphics_state.stroke_cs = cs;
337            }
338            TypedInstruction::ColorSpaceNonStroke(c) => {
339                let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
340                    named
341                } else {
342                    context
343                        .get_color_space(resources, c.0)
344                        .unwrap_or(ColorSpace::device_gray())
345                };
346
347                context.get_mut().graphics_state.non_stroke_color = cs.initial_color();
348                context.get_mut().graphics_state.none_stroke_cs = cs;
349            }
350            TypedInstruction::DashPattern(p) => {
351                context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
352                // kurbo apparently cannot properly deal with offsets that are exactly 0.
353                context.get_mut().graphics_state.stroke_props.dash_array =
354                    p.0.iter::<f32>()
355                        .map(|n| if n == 0.0 { 0.01 } else { n })
356                        .collect();
357            }
358            TypedInstruction::RenderingIntent(_) => {
359                // Ignore for now.
360            }
361            TypedInstruction::NonStrokeColorNamed(n) => {
362                context.get_mut().graphics_state.non_stroke_color =
363                    n.0.into_iter().map(|n| n.as_f32()).collect();
364                context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
365                    resources.get_pattern(
366                        name,
367                        Box::new(|_| None),
368                        Box::new(|d| Pattern::new(d, context, resources)),
369                    )
370                });
371            }
372            TypedInstruction::StrokeColorNamed(n) => {
373                context.get_mut().graphics_state.stroke_color =
374                    n.0.into_iter().map(|n| n.as_f32()).collect();
375                context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
376                    resources.get_pattern(
377                        name,
378                        Box::new(|_| None),
379                        Box::new(|d| Pattern::new(d, context, resources)),
380                    )
381                });
382            }
383            TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
384                // Properties can be either:
385                // 1. A Name that references an entry in the Resources/Properties dictionary
386                // 2. An inline dictionary with an OC key
387
388                if let Some(name) = bdc.1.clone().into_name()
389                    && let Some(ocg_ref) = resources.properties.get_ref(name.clone())
390                {
391                    context.ocg_state.begin_ocg(ocg_ref.into());
392                } else if let Some((props, _)) = dict_or_stream(&bdc.1)
393                    && let Some(oc_ref) = props.get_ref(OC)
394                {
395                    context.ocg_state.begin_ocg(oc_ref.into());
396                } else {
397                    context.ocg_state.begin_marked_content();
398                }
399            }
400            TypedInstruction::MarkedContentPointWithProperties(_) => {}
401            TypedInstruction::EndMarkedContent(_) => {
402                context.ocg_state.end_marked_content();
403            }
404            TypedInstruction::MarkedContentPoint(_) => {}
405            TypedInstruction::BeginMarkedContent(_) => {
406                context.ocg_state.begin_marked_content();
407            }
408            TypedInstruction::BeginText(_) => {
409                context.get_mut().text_state.text_matrix = Affine::IDENTITY;
410                context.get_mut().text_state.text_line_matrix = Affine::IDENTITY;
411            }
412            TypedInstruction::SetTextMatrix(m) => {
413                let m = Affine::new([
414                    m.0.as_f64(),
415                    m.1.as_f64(),
416                    m.2.as_f64(),
417                    m.3.as_f64(),
418                    m.4.as_f64(),
419                    m.5.as_f64(),
420                ]);
421                context.get_mut().text_state.text_line_matrix = m;
422                context.get_mut().text_state.text_matrix = m;
423            }
424            TypedInstruction::EndText(_) => {
425                let has_outline = context
426                    .get()
427                    .text_state
428                    .clip_paths
429                    .segments()
430                    .next()
431                    .is_some();
432
433                if has_outline {
434                    let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone();
435
436                    context.push_bbox(clip_path.bounding_box());
437
438                    device.push_clip_path(&ClipPath {
439                        path: clip_path,
440                        fill: FillRule::NonZero,
441                    });
442                    context.get_mut().n_clips += 1;
443                }
444
445                context.get_mut().text_state.clip_paths.truncate(0);
446            }
447            TypedInstruction::TextFont(t) => {
448                let font = context.get_font(resources, t.0);
449                context.get_mut().text_state.font_size = t.1.as_f32();
450                context.get_mut().text_state.font = font;
451            }
452            TypedInstruction::ShowText(s) => {
453                text::show_text_string(context, device, resources, s.0);
454            }
455            TypedInstruction::ShowTexts(s) => {
456                for obj in s.0.iter::<Object>() {
457                    if let Some(adjustment) = obj.clone().into_f32() {
458                        context.get_mut().text_state.apply_adjustment(adjustment);
459                    } else if let Some(text) = obj.into_string() {
460                        text::show_text_string(context, device, resources, text);
461                    }
462                }
463            }
464            TypedInstruction::HorizontalScaling(h) => {
465                context.get_mut().text_state.horizontal_scaling = h.0.as_f32();
466            }
467            TypedInstruction::TextLeading(tl) => {
468                context.get_mut().text_state.leading = tl.0.as_f32();
469            }
470            TypedInstruction::CharacterSpacing(c) => {
471                context.get_mut().text_state.char_space = c.0.as_f32()
472            }
473            TypedInstruction::WordSpacing(w) => {
474                context.get_mut().text_state.word_space = w.0.as_f32();
475            }
476            TypedInstruction::NextLine(n) => {
477                let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
478                text::next_line(context, tx, ty)
479            }
480            TypedInstruction::NextLineUsingLeading(_) => {
481                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
482            }
483            TypedInstruction::NextLineAndShowText(n) => {
484                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
485                text::show_text_string(context, device, resources, n.0)
486            }
487            TypedInstruction::TextRenderingMode(r) => {
488                let mode = match r.0.as_i64() {
489                    0 => TextRenderingMode::Fill,
490                    1 => TextRenderingMode::Stroke,
491                    2 => TextRenderingMode::FillStroke,
492                    3 => TextRenderingMode::Invisible,
493                    4 => TextRenderingMode::FillAndClip,
494                    5 => TextRenderingMode::StrokeAndClip,
495                    6 => TextRenderingMode::FillAndStrokeAndClip,
496                    7 => TextRenderingMode::Clip,
497                    _ => {
498                        warn!("unknown text rendering mode {}", r.0.as_i64());
499
500                        TextRenderingMode::Fill
501                    }
502                };
503
504                context.get_mut().text_state.render_mode = mode;
505            }
506            TypedInstruction::NextLineAndSetLeading(n) => {
507                let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
508                context.get_mut().text_state.leading = -ty as f32;
509                text::next_line(context, tx, ty)
510            }
511            TypedInstruction::ShapeGlyph(_) => {}
512            TypedInstruction::XObject(x) => {
513                let cache = context.object_cache.clone();
514                let transfer_function = context.get().graphics_state.transfer_function.clone();
515                if let Some(x_object) = resources.get_x_object(
516                    x.0,
517                    Box::new(|_| None),
518                    Box::new(|s| {
519                        XObject::new(
520                            &s,
521                            &context.settings.warning_sink,
522                            &cache,
523                            transfer_function.clone(),
524                        )
525                    }),
526                ) {
527                    draw_xobject(&x_object, resources, context, device);
528                }
529            }
530            TypedInstruction::InlineImage(i) => {
531                let warning_sink = context.settings.warning_sink.clone();
532                let transfer_function = context.get().graphics_state.transfer_function.clone();
533                let cache = context.object_cache.clone();
534                if let Some(x_object) = ImageXObject::new(
535                    &i.0,
536                    |name| context.get_color_space(resources, name.clone()),
537                    &warning_sink,
538                    &cache,
539                    false,
540                    transfer_function,
541                ) {
542                    draw_image_xobject(&x_object, context, device);
543                }
544            }
545            TypedInstruction::TextRise(t) => {
546                context.get_mut().text_state.rise = t.0.as_f32();
547            }
548            TypedInstruction::Shading(s) => {
549                if !context.ocg_state.is_visible() {
550                    continue;
551                }
552
553                if let Some(sp) = resources
554                    .get_shading(s.0, Box::new(|_| None), Box::new(Some))
555                    .and_then(|o| dict_or_stream(&o))
556                    .and_then(|s| Shading::new(&s.0, s.1.as_ref(), &context.object_cache))
557                    .map(|s| {
558                        Pattern::Shading(ShadingPattern {
559                            shading: Arc::new(s),
560                            matrix: Affine::IDENTITY,
561                        })
562                    })
563                {
564                    context.save_state();
565                    context.push_root_transform();
566                    let st = context.get_mut();
567                    st.graphics_state.non_stroke_pattern = Some(sp);
568                    st.graphics_state.none_stroke_cs = ColorSpace::pattern();
569
570                    device.set_soft_mask(st.graphics_state.soft_mask.clone());
571                    device.push_transparency_group(st.graphics_state.non_stroke_alpha, None);
572
573                    let bbox = context.bbox().to_path(0.1);
574                    let inverted_bbox = context.get().ctm.inverse() * bbox;
575                    fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox));
576
577                    device.pop_transparency_group();
578
579                    context.pop_root_transform();
580                    context.restore_state();
581                } else {
582                    warn!("failed to process shading");
583                }
584            }
585            TypedInstruction::BeginCompatibility(_) => {}
586            TypedInstruction::EndCompatibility(_) => {}
587            TypedInstruction::ColorGlyph(_) => {}
588            TypedInstruction::ShowTextWithParameters(t) => {
589                context.get_mut().text_state.word_space = t.0.as_f32();
590                context.get_mut().text_state.char_space = t.1.as_f32();
591                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
592                text::show_text_string(context, device, resources, t.2)
593            }
594            _ => {
595                warn!("failed to read an operator");
596            }
597        }
598    }
599
600    while context.num_states() > num_states {
601        restore_state(context, device);
602    }
603
604    // Invalid files may still have pending clip paths.
605    while context.get().n_clips > n_clips {
606        device.pop_clip_path();
607        context.pop_bbox();
608        context.get_mut().n_clips -= 1;
609    }
610}