Skip to main content

hayro_interpret/interpret/
mod.rs

1use crate::FillRule;
2use crate::color::ColorSpace;
3use crate::context::Context;
4use crate::convert::{convert_line_cap, convert_line_join};
5use crate::device::Device;
6use crate::font::{Font, FontData, FontQuery, StandardFont};
7use crate::interpret::path::{
8    close_path, fill_path, fill_path_impl, fill_stroke_path, stroke_path,
9};
10use crate::interpret::state::{TextStateFont, handle_gs};
11use crate::interpret::text::TextRenderingMode;
12use crate::pattern::{Pattern, ShadingPattern};
13use crate::shading::Shading;
14use crate::util::{OptionLog, RectExt};
15use crate::x_object::{
16    FormXObject, ImageXObject, XObject, draw_form_xobject, draw_image_xobject, draw_xobject,
17};
18use hayro_syntax::content::TypedIter;
19use hayro_syntax::content::ops::TypedInstruction;
20use hayro_syntax::object::dict::keys::{ANNOTS, AP, F, MCID, N, OC, RECT};
21use hayro_syntax::object::{Array, Dict, Object, Rect, Stream, dict_or_stream};
22use hayro_syntax::page::{Page, Resources};
23use kurbo::{Affine, Point, Shape};
24use smallvec::smallvec;
25use std::sync::Arc;
26
27pub(crate) mod path;
28pub(crate) mod state;
29pub(crate) mod text;
30
31pub use state::ActiveTransferFunction;
32
33/// A callback function for resolving font queries.
34///
35/// The first argument is the raw data, the second argument is the index in case the font
36/// is a TTC, otherwise it should be 0.
37pub type FontResolverFn = Arc<dyn Fn(&FontQuery) -> Option<(FontData, u32)> + Send + Sync>;
38/// A callback function for resolving cmap names to their files.
39pub type CMapResolverFn =
40    Arc<dyn Fn(hayro_cmap::CMapName<'_>) -> Option<&'static [u8]> + Send + Sync>;
41/// A callback function for resolving warnings during interpretation.
42pub type WarningSinkFn = Arc<dyn Fn(InterpreterWarning) + Send + Sync>;
43
44#[derive(Clone)]
45/// Settings that should be applied during the interpretation process.
46pub struct InterpreterSettings {
47    /// Nearly every PDF contains text. In most cases, PDF files embed the fonts they use, and
48    /// hayro can therefore read the font files and do all the processing needed. However, there
49    /// are two problems:
50    /// - Fonts don't _have_ to be embedded, it's possible that the PDF file only defines the basic
51    ///   metadata of the font, like its name, but relies on the PDF processor to find that font
52    ///   in its environment.
53    /// - The PDF specification requires a list of 14 fonts that should always be available to a
54    ///   PDF processor. These include:
55    ///   - Times New Roman (Normal, Bold, Italic, `BoldItalic`)
56    ///   - Courier (Normal, Bold, Italic, `BoldItalic`)
57    ///   - Helvetica (Normal, Bold, Italic, `BoldItalic`)
58    ///   - `ZapfDingBats`
59    ///   - Symbol
60    ///
61    /// Because of this, if any of the above situations occurs, this callback will be called, which
62    /// expects the data of an appropriate font to be returned, if available. If no such font is
63    /// provided, the text will most likely fail to render.
64    ///
65    /// For the font data, there are two different formats that are accepted:
66    /// - Any valid TTF/OTF font.
67    /// - A valid CFF font program.
68    ///
69    /// The following recommendations are given for the implementation of this callback function.
70    ///
71    /// For the standard fonts, in case the original fonts are available on the system, you should
72    /// just return those. Otherwise, for Helvetica, Courier and Times New Roman, the best alternative
73    /// are the corresponding fonts of the [Liberation font family](https://github.com/liberationfonts/liberation-fonts).
74    /// If you prefer smaller fonts, you can use the [Foxit CFF fonts](https://github.com/LaurenzV/hayro/tree/master/assets/standard_fonts),
75    /// which are much smaller but are missing glyphs for certain scripts.
76    ///
77    /// For the `Symbol` and `ZapfDingBats` fonts, you should also prefer the system fonts, and if
78    /// not available to you, you can, similarly to above, use the corresponding fonts from Foxit.
79    ///
80    /// If you don't want having to deal with this, you can just enable the `embed-fonts` feature
81    /// and use the default implementation of the callback.
82    pub font_resolver: FontResolverFn,
83    /// A callback for resolving cmaps that aren't embedded.
84    ///
85    /// When the PDF requires using a cmap that is not directly embedded in the PDF,
86    /// this callback will be called to attempt fetching the data of the file.
87    ///
88    /// When the `embed-cmaps` feature is enabled, this uses `load_embedded`
89    /// method from `hayro-cmap` by default, which embeds the cmap files for
90    /// all 61 predefined cmaps
91    /// that the PDF specification requires to be readily available on a system.
92    /// Otherwise, you can implement your custom logic for lazily fetching the
93    /// data. If you are fine not supporting such PDFs, you can simply pass a closure
94    /// that always returns `None`.
95    pub cmap_resolver: CMapResolverFn,
96    /// In certain cases, `hayro` will emit a warning in case an issue was encountered while interpreting
97    /// the PDF file. Providing a callback allows you to catch those warnings and handle them, if desired.
98    pub warning_sink: WarningSinkFn,
99    /// Whether annotations should be rendered as well.
100    ///
101    /// Note that this feature is currently not fully implemented yet, so some
102    /// annotations might be missing.
103    pub render_annotations: bool,
104}
105
106impl Default for InterpreterSettings {
107    fn default() -> Self {
108        Self {
109            #[cfg(not(feature = "embed-fonts"))]
110            font_resolver: Arc::new(|_| None),
111            #[cfg(feature = "embed-fonts")]
112            font_resolver: Arc::new(|query| match query {
113                FontQuery::Standard(s) => Some(s.get_font_data()),
114                FontQuery::Fallback(f) => Some(f.pick_standard_font().get_font_data()),
115            }),
116            #[cfg(feature = "embed-cmaps")]
117            cmap_resolver: Arc::new(hayro_cmap::load_embedded),
118            #[cfg(not(feature = "embed-cmaps"))]
119            cmap_resolver: Arc::new(|_| None),
120            warning_sink: Arc::new(|_| {}),
121            render_annotations: true,
122        }
123    }
124}
125
126#[derive(Copy, Clone, Debug)]
127/// Warnings that can occur while interpreting a PDF file.
128pub enum InterpreterWarning {
129    /// An unsupported font kind was encountered.
130    ///
131    /// Currently, only CID fonts with non-identity encoding are unsupported.
132    UnsupportedFont,
133    /// An image failed to decode.
134    ImageDecodeFailure,
135}
136
137/// interpret the contents of the page and render them into the device.
138pub fn interpret_page<'a>(
139    page: &Page<'a>,
140    context: &mut Context<'a>,
141    device: &mut impl Device<'a>,
142) {
143    let resources = page.resources();
144    interpret(page.typed_operations(), resources, context, device);
145
146    if context.settings.render_annotations
147        && let Some(annot_arr) = page.raw().get::<Array<'_>>(ANNOTS)
148    {
149        for annot in annot_arr.iter::<Dict<'_>>() {
150            let flags = annot.get::<u32>(F).unwrap_or(0);
151
152            // Annotation should be hidden.
153            if flags & 2 != 0 {
154                continue;
155            }
156
157            if let Some(apx) = annot
158                .get::<Dict<'_>>(AP)
159                .and_then(|ap| ap.get::<Stream<'_>>(N))
160                .and_then(|o| FormXObject::new(&o))
161            {
162                let Some(rect) = annot.get::<Rect>(RECT) else {
163                    continue;
164                };
165
166                let annot_rect = rect.to_kurbo();
167                // 12.5.5. Appearance streams
168                // "The algorithm outlined in this subclause shall be used
169                // to map from the coordinate system of the appearance XObject."
170
171                // 1) The appearance’s bounding box (specified by its BBox entry)
172                // shall be transformed, using Matrix, to produce a
173                // quadrilateral with arbitrary orientation. The transformed
174                // appearance box is the smallest upright rectangle that
175                // encompasses this quadrilateral.
176                let transformed_rect = (apx.matrix
177                    * kurbo::Rect::new(
178                        apx.bbox[0] as f64,
179                        apx.bbox[1] as f64,
180                        apx.bbox[2] as f64,
181                        apx.bbox[3] as f64,
182                    )
183                    .to_path(0.1))
184                .bounding_box();
185
186                // 2) A matrix A shall be computed that scales and translates
187                // the transformed appearance box to align with the edges
188                // of the annotation’s rectangle (specified by the Rect entry).
189                // A maps the lower-left corner (the corner with the smallest
190                // x and y coordinates) and the upper-right corner (the
191                // corner with the greatest x and y coordinates) of the
192                // transformed appearance box to the corresponding corners
193                // of the annotation’s rectangle.
194                let affine = Affine::new([
195                    annot_rect.width() / transformed_rect.width(),
196                    0.0,
197                    0.0,
198                    annot_rect.height() / transformed_rect.height(),
199                    annot_rect.x0 - transformed_rect.x0,
200                    annot_rect.y0 - transformed_rect.y0,
201                ]);
202
203                // 3) Matrix shall be concatenated with A to form a matrix
204                // AA that maps from the appearance’s coordinate system to
205                // the annotation’s rectangle in default user space.
206                context.save_state();
207                context.pre_concat_affine(affine);
208                context.push_root_transform();
209
210                draw_form_xobject(resources, &apx, context, device);
211                context.pop_root_transform();
212                context.restore_state(device);
213            }
214        }
215    }
216}
217
218/// Interpret the instructions from `ops` and render them into the device.
219pub fn interpret<'a>(
220    mut ops: TypedIter<'_>,
221    resources: &Resources<'a>,
222    context: &mut Context<'a>,
223    device: &mut impl Device<'a>,
224) {
225    let num_states = context.num_states();
226
227    context.save_state();
228
229    while let Some(op) = ops.next() {
230        match op {
231            TypedInstruction::SaveState(_) => context.save_state(),
232            TypedInstruction::StrokeColorDeviceRgb(s) => {
233                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
234                context.get_mut().graphics_state.stroke_color =
235                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
236                context.get_mut().graphics_state.stroke_pattern = None;
237            }
238            TypedInstruction::StrokeColorDeviceGray(s) => {
239                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
240                context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
241                context.get_mut().graphics_state.stroke_pattern = None;
242            }
243            TypedInstruction::StrokeColorCmyk(s) => {
244                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
245                context.get_mut().graphics_state.stroke_color =
246                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
247                context.get_mut().graphics_state.stroke_pattern = None;
248            }
249            TypedInstruction::LineWidth(w) => {
250                context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32();
251            }
252            TypedInstruction::LineCap(c) => {
253                context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c);
254            }
255            TypedInstruction::LineJoin(j) => {
256                context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j);
257            }
258            TypedInstruction::MiterLimit(l) => {
259                context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32();
260            }
261            TypedInstruction::Transform(t) => {
262                context.pre_concat_transform(t);
263            }
264            TypedInstruction::RectPath(r) => {
265                let rect = kurbo::Rect::new(
266                    r.0.as_f64(),
267                    r.1.as_f64(),
268                    r.0.as_f64() + r.2.as_f64(),
269                    r.1.as_f64() + r.3.as_f64(),
270                )
271                .to_path(0.1);
272                context.path_mut().extend(rect);
273            }
274            TypedInstruction::MoveTo(m) => {
275                let p = Point::new(m.0.as_f64(), m.1.as_f64());
276                *(context.last_point_mut()) = p;
277                *(context.sub_path_start_mut()) = p;
278                context.path_mut().move_to(p);
279            }
280            TypedInstruction::FillPathEvenOdd(_) => {
281                fill_path(context, device, FillRule::EvenOdd);
282            }
283            TypedInstruction::FillPathNonZero(_) => {
284                fill_path(context, device, FillRule::NonZero);
285            }
286            TypedInstruction::FillPathNonZeroCompatibility(_) => {
287                fill_path(context, device, FillRule::NonZero);
288            }
289            TypedInstruction::FillAndStrokeEvenOdd(_) => {
290                fill_stroke_path(context, device, FillRule::EvenOdd);
291            }
292            TypedInstruction::FillAndStrokeNonZero(_) => {
293                fill_stroke_path(context, device, FillRule::NonZero);
294            }
295            TypedInstruction::CloseAndStrokePath(_) => {
296                close_path(context);
297                stroke_path(context, device);
298            }
299            TypedInstruction::CloseFillAndStrokeEvenOdd(_) => {
300                close_path(context);
301                fill_stroke_path(context, device, FillRule::EvenOdd);
302            }
303            TypedInstruction::CloseFillAndStrokeNonZero(_) => {
304                close_path(context);
305                fill_stroke_path(context, device, FillRule::NonZero);
306            }
307            TypedInstruction::NonStrokeColorDeviceGray(s) => {
308                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
309                context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
310                context.get_mut().graphics_state.non_stroke_pattern = None;
311            }
312            TypedInstruction::NonStrokeColorDeviceRgb(s) => {
313                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
314                context.get_mut().graphics_state.non_stroke_color =
315                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
316                context.get_mut().graphics_state.non_stroke_pattern = None;
317            }
318            TypedInstruction::NonStrokeColorCmyk(s) => {
319                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
320                context.get_mut().graphics_state.non_stroke_color =
321                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
322                context.get_mut().graphics_state.non_stroke_pattern = None;
323            }
324            TypedInstruction::LineTo(m) => {
325                if !context.path().elements().is_empty() {
326                    let last_point = *context.last_point();
327                    let mut p = Point::new(m.0.as_f64(), m.1.as_f64());
328                    *(context.last_point_mut()) = p;
329                    if last_point == p {
330                        // Add a small delta so that zero width lines can still have a round stroke.
331                        p.x += 0.0001;
332                    }
333
334                    context.path_mut().line_to(p);
335                }
336            }
337            TypedInstruction::CubicTo(c) => {
338                if !context.path().elements().is_empty() {
339                    let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
340                    let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
341                    let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
342
343                    *(context.last_point_mut()) = p3;
344
345                    context.path_mut().curve_to(p1, p2, p3);
346                }
347            }
348            TypedInstruction::CubicStartTo(c) => {
349                if !context.path().elements().is_empty() {
350                    let p1 = *context.last_point();
351                    let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
352                    let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
353
354                    *(context.last_point_mut()) = p3;
355
356                    context.path_mut().curve_to(p1, p2, p3);
357                }
358            }
359            TypedInstruction::CubicEndTo(c) => {
360                if !context.path().elements().is_empty() {
361                    let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
362                    let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
363
364                    *(context.last_point_mut()) = p3;
365
366                    context.path_mut().curve_to(p2, p3, p3);
367                }
368            }
369            TypedInstruction::ClosePath(_) => {
370                close_path(context);
371            }
372            TypedInstruction::SetGraphicsState(gs) => {
373                if let Some(gs) = resources
374                    .get_ext_g_state(gs.0)
375                    .warn_none(&format!("failed to get extgstate {}", gs.0.as_str()))
376                {
377                    handle_gs(&gs, context, resources);
378                }
379            }
380            TypedInstruction::StrokePath(_) => {
381                stroke_path(context, device);
382            }
383            TypedInstruction::EndPath(_) => {
384                if let Some(clip) = *context.clip()
385                    && !context.path().elements().is_empty()
386                {
387                    let clip_path = context.get().ctm * context.path().clone();
388                    context.push_clip_path(clip_path, clip, device);
389
390                    *(context.clip_mut()) = None;
391                }
392
393                context.path_mut().truncate(0);
394            }
395            TypedInstruction::NonStrokeColor(c) => {
396                let gs = &mut context.get_mut().graphics_state;
397                gs.non_stroke_color = c.0.into_iter().map(|n| n.as_f32()).collect();
398                gs.non_stroke_pattern = None;
399            }
400            TypedInstruction::StrokeColor(c) => {
401                let gs = &mut context.get_mut().graphics_state;
402                gs.stroke_color = c.0.into_iter().map(|n| n.as_f32()).collect();
403                gs.stroke_pattern = None;
404            }
405            TypedInstruction::ClipNonZero(_) => {
406                *(context.clip_mut()) = Some(FillRule::NonZero);
407            }
408            TypedInstruction::ClipEvenOdd(_) => {
409                *(context.clip_mut()) = Some(FillRule::EvenOdd);
410            }
411            TypedInstruction::RestoreState(_) => context.restore_state(device),
412            TypedInstruction::FlatnessTolerance(_) => {
413                // Ignore for now.
414            }
415            TypedInstruction::ColorSpaceStroke(c) => {
416                let cs = if let Some(named) = ColorSpace::new_from_name(c.0) {
417                    named
418                } else {
419                    context
420                        .get_color_space(resources, c.0)
421                        .unwrap_or(ColorSpace::device_gray())
422                };
423
424                if !cs.is_pattern() {
425                    context.get_mut().graphics_state.stroke_pattern = None;
426                }
427                context.get_mut().graphics_state.stroke_color = cs.initial_color();
428                context.get_mut().graphics_state.stroke_cs = cs;
429            }
430            TypedInstruction::ColorSpaceNonStroke(c) => {
431                let cs = if let Some(named) = ColorSpace::new_from_name(c.0) {
432                    named
433                } else {
434                    context
435                        .get_color_space(resources, c.0)
436                        .unwrap_or(ColorSpace::device_gray())
437                };
438
439                if !cs.is_pattern() {
440                    context.get_mut().graphics_state.non_stroke_pattern = None;
441                }
442                context.get_mut().graphics_state.non_stroke_color = cs.initial_color();
443                context.get_mut().graphics_state.none_stroke_cs = cs;
444            }
445            TypedInstruction::DashPattern(p) => {
446                context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
447                // kurbo apparently cannot properly deal with offsets that are exactly 0.
448                context.get_mut().graphics_state.stroke_props.dash_array =
449                    p.0.iter::<f32>()
450                        .map(|n| if n == 0.0 { 0.01 } else { n })
451                        .collect();
452            }
453            TypedInstruction::RenderingIntent(_) => {
454                // Ignore for now.
455            }
456            TypedInstruction::NonStrokeColorNamed(n) => {
457                context.get_mut().graphics_state.non_stroke_color =
458                    n.0.into_iter().map(|n| n.as_f32()).collect();
459                context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
460                    resources
461                        .get_pattern(name)
462                        .and_then(|d| Pattern::new(d, context, resources))
463                });
464            }
465            TypedInstruction::StrokeColorNamed(n) => {
466                context.get_mut().graphics_state.stroke_color =
467                    n.0.into_iter().map(|n| n.as_f32()).collect();
468                context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
469                    resources
470                        .get_pattern(name)
471                        .and_then(|d| Pattern::new(d, context, resources))
472                });
473            }
474            TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
475                // Properties can be either:
476                // 1. A Name that references an entry in the Resources/Properties dictionary
477                // 2. An inline dictionary with an OC key
478
479                let mcid = dict_or_stream(bdc.1).and_then(|(props, _)| props.get::<i32>(MCID));
480
481                let oc = bdc
482                    .1
483                    .clone()
484                    .into_name()
485                    .and_then(|name| {
486                        let r = resources.properties.get_ref(name.as_ref())?;
487                        let d = resources
488                            .properties
489                            .get::<Dict<'_>>(name)
490                            .unwrap_or_default();
491                        Some((d, r))
492                    })
493                    .or_else(|| {
494                        let (props, _) = dict_or_stream(bdc.1)?;
495                        let r = props.get_ref(OC)?;
496                        let d = props.get::<Dict<'_>>(OC).unwrap_or_default();
497                        Some((d, r))
498                    });
499
500                if let Some((dict, oc_ref)) = oc {
501                    context.ocg_state.begin_ocg(&dict, oc_ref.into());
502                } else {
503                    context.ocg_state.begin_marked_content();
504                }
505
506                device.begin_marked_content(bdc.0, mcid);
507            }
508            TypedInstruction::MarkedContentPointWithProperties(_) => {}
509            TypedInstruction::EndMarkedContent(_) => {
510                context.ocg_state.end_marked_content();
511                device.end_marked_content();
512            }
513            TypedInstruction::MarkedContentPoint(_) => {}
514            TypedInstruction::BeginMarkedContent(bmc) => {
515                context.ocg_state.begin_marked_content();
516                device.begin_marked_content(bmc.0, None);
517            }
518            TypedInstruction::BeginText(_) => {
519                context.get_mut().text_state.text_matrix = Affine::IDENTITY;
520                context.get_mut().text_state.text_line_matrix = Affine::IDENTITY;
521            }
522            TypedInstruction::SetTextMatrix(m) => {
523                let m = Affine::new([
524                    m.0.as_f64(),
525                    m.1.as_f64(),
526                    m.2.as_f64(),
527                    m.3.as_f64(),
528                    m.4.as_f64(),
529                    m.5.as_f64(),
530                ]);
531                context.get_mut().text_state.text_line_matrix = m;
532                context.get_mut().text_state.text_matrix = m;
533            }
534            TypedInstruction::EndText(_) => {
535                let has_outline = context
536                    .get()
537                    .text_state
538                    .clip_paths
539                    .segments()
540                    .next()
541                    .is_some();
542
543                if has_outline {
544                    let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone();
545
546                    context.push_clip_path(clip_path, FillRule::NonZero, device);
547                }
548
549                context.get_mut().text_state.clip_paths.truncate(0);
550            }
551            TypedInstruction::TextFont(t) => {
552                let name = t.0;
553
554                // In case we are unable to resolve the font, two scenarios:
555                // 1) If the font doesn't exist in the first place in the resource dictionary,
556                // assume Helvetica (this seems to be what other PDF viewers do).
557                // 2) In case it's `None` because we were unable to resolve the font
558                // (for whatever reason), leave it as `None`. Better showing no
559                // text at all than garbage text.
560                let font = if let Some(font_dict) = resources.get_font(name) {
561                    context.resolve_font(&font_dict)
562                } else {
563                    Font::new_standard(StandardFont::Helvetica, &context.settings.font_resolver)
564                        .map(TextStateFont::Fallback)
565                };
566
567                context.get_mut().text_state.font_size = t.1.as_f32();
568                context.get_mut().text_state.font = font;
569            }
570            TypedInstruction::ShowText(s) => {
571                if context.get().text_state.font.is_none() {
572                    // Even if no explicit font was set, we try to assume Helvetica. Acrobat
573                    // seems to do the same.
574                    context.get_mut().text_state.font = Font::new_standard(
575                        StandardFont::Helvetica,
576                        &context.settings.font_resolver,
577                    )
578                    .map(TextStateFont::Fallback);
579                }
580
581                text::show_text_string(context, device, resources, s.0);
582            }
583            TypedInstruction::ShowTexts(s) => {
584                if context.get().text_state.font.is_none() {
585                    // Even if no explicit font was set, we try to assume Helvetica. Acrobat
586                    // seems to do the same.
587                    context.get_mut().text_state.font = Font::new_standard(
588                        StandardFont::Helvetica,
589                        &context.settings.font_resolver,
590                    )
591                    .map(TextStateFont::Fallback);
592                }
593
594                for obj in s.0.iter::<Object<'_>>() {
595                    match obj {
596                        Object::Number(num) => {
597                            context.get_mut().text_state.apply_adjustment(num.as_f32());
598                        }
599                        Object::String(text) => {
600                            text::show_text_string(context, device, resources, &text);
601                        }
602                        _ => {}
603                    }
604                }
605            }
606            TypedInstruction::HorizontalScaling(h) => {
607                context.get_mut().text_state.horizontal_scaling = h.0.as_f32();
608            }
609            TypedInstruction::TextLeading(tl) => {
610                context.get_mut().text_state.leading = tl.0.as_f32();
611            }
612            TypedInstruction::CharacterSpacing(c) => {
613                context.get_mut().text_state.char_space = c.0.as_f32();
614            }
615            TypedInstruction::WordSpacing(w) => {
616                context.get_mut().text_state.word_space = w.0.as_f32();
617            }
618            TypedInstruction::NextLine(n) => {
619                let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
620                text::next_line(context, tx, ty);
621            }
622            TypedInstruction::NextLineUsingLeading(_) => {
623                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
624            }
625            TypedInstruction::NextLineAndShowText(n) => {
626                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
627                text::show_text_string(context, device, resources, n.0);
628            }
629            TypedInstruction::TextRenderingMode(r) => {
630                let mode = match r.0.as_i64() {
631                    0 => TextRenderingMode::Fill,
632                    1 => TextRenderingMode::Stroke,
633                    2 => TextRenderingMode::FillStroke,
634                    3 => TextRenderingMode::Invisible,
635                    4 => TextRenderingMode::FillAndClip,
636                    5 => TextRenderingMode::StrokeAndClip,
637                    6 => TextRenderingMode::FillAndStrokeAndClip,
638                    7 => TextRenderingMode::Clip,
639                    _ => {
640                        warn!("unknown text rendering mode {}", r.0.as_i64());
641
642                        TextRenderingMode::Fill
643                    }
644                };
645
646                context.get_mut().text_state.render_mode = mode;
647            }
648            TypedInstruction::NextLineAndSetLeading(n) => {
649                let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
650                context.get_mut().text_state.leading = -ty as f32;
651                text::next_line(context, tx, ty);
652            }
653            TypedInstruction::ShapeGlyph(_) => {}
654            TypedInstruction::XObject(x) => {
655                let cache = context.interpreter_cache.object_cache.clone();
656                let transfer_function = context.get().graphics_state.transfer_function.clone();
657                if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| {
658                    XObject::new(
659                        &s,
660                        &context.settings.warning_sink,
661                        &cache,
662                        transfer_function.clone(),
663                    )
664                }) {
665                    draw_xobject(&x_object, resources, context, device);
666                }
667            }
668            TypedInstruction::InlineImage(i) => {
669                let warning_sink = context.settings.warning_sink.clone();
670                let transfer_function = context.get().graphics_state.transfer_function.clone();
671                let cache = context.interpreter_cache.object_cache.clone();
672                if let Some(x_object) = ImageXObject::new(
673                    i.0,
674                    |name| context.get_color_space(resources, name),
675                    &warning_sink,
676                    &cache,
677                    false,
678                    transfer_function,
679                ) {
680                    draw_image_xobject(&x_object, context, device);
681                }
682            }
683            TypedInstruction::TextRise(t) => {
684                context.get_mut().text_state.rise = t.0.as_f32();
685            }
686            TypedInstruction::Shading(s) => {
687                if !context.ocg_state.is_visible() {
688                    continue;
689                }
690
691                let transfer_function = context.get().graphics_state.transfer_function.clone();
692
693                if let Some(sp) = resources
694                    .get_shading(s.0)
695                    .and_then(|o| {
696                        let (dict, stream) = dict_or_stream(&o)?;
697                        Shading::new(dict, stream, &context.interpreter_cache.object_cache)
698                    })
699                    .map(|s| {
700                        Pattern::Shading(ShadingPattern {
701                            shading: Arc::new(s),
702                            matrix: Affine::IDENTITY,
703                            opacity: context.get().graphics_state.non_stroke_alpha,
704                            transfer_function: transfer_function.clone(),
705                        })
706                    })
707                {
708                    context.save_state();
709                    context.push_root_transform();
710                    let st = context.get_mut();
711                    st.graphics_state.non_stroke_pattern = Some(sp);
712                    st.graphics_state.none_stroke_cs = ColorSpace::pattern();
713
714                    device.set_soft_mask(st.graphics_state.soft_mask.clone());
715                    device.set_blend_mode(st.graphics_state.blend_mode);
716
717                    let bbox = context.bbox().to_path(0.1);
718                    let inverted_bbox = context.get().ctm.inverse() * bbox;
719                    fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox));
720
721                    context.pop_root_transform();
722                    context.restore_state(device);
723                } else {
724                    warn!("failed to process shading");
725                }
726            }
727            TypedInstruction::BeginCompatibility(_) => {}
728            TypedInstruction::EndCompatibility(_) => {}
729            TypedInstruction::ColorGlyph(_) => {}
730            TypedInstruction::ShowTextWithParameters(t) => {
731                context.get_mut().text_state.word_space = t.0.as_f32();
732                context.get_mut().text_state.char_space = t.1.as_f32();
733                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
734                text::show_text_string(context, device, resources, t.2);
735            }
736            _ => {
737                warn!("failed to read an operator");
738            }
739        }
740    }
741
742    while context.num_states() > num_states {
743        context.restore_state(device);
744    }
745}