Skip to main content

pdf_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 kurbo::{Affine, Point, Shape};
19use log::warn;
20use pdf_syntax::content::ops::TypedInstruction;
21use pdf_syntax::object::dict::keys::{ANNOTS, AP, F, FT, MCID, N, OC, RECT};
22use pdf_syntax::object::{Array, Dict, Name, Object, Rect, Stream, dict_or_stream};
23use pdf_syntax::page::{Page, Resources};
24use smallvec::smallvec;
25use std::sync::{Arc, OnceLock};
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(pdf_font::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    /// pdf-interpret 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/pdf-interpret/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 `pdf-interpret-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, `pdf-interpret` 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    /// Whether to skip `/FT /Sig` (signature widget) appearance streams.
105    ///
106    /// Rendering sets this to `true` to match MuPDF behaviour, but text
107    /// extraction should set it to `false` so that signature text is included.
108    pub skip_signature_widgets: bool,
109    /// Maximum number of content-stream operators to interpret.
110    ///
111    /// `None` preserves the historical unlimited behavior for callers that do
112    /// not configure processing limits.
113    pub max_operator_count: Option<u64>,
114}
115
116/// Known paths for CJK fonts, ordered by preference.
117/// Covers macOS, Ubuntu/Debian, Fedora/RHEL, and Alpine Linux.
118#[cfg(feature = "embed-fonts")]
119const CJK_FONT_CANDIDATE_PATHS: &[&str] = &[
120    // macOS — ships with every installation
121    "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
122    // Noto CJK — most common on Linux
123    "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
124    "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
125    "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
126    "/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
127    // WenQuanYi — fallback on older Ubuntu/Debian systems
128    "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
129    "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
130    // Arphic (traditional)
131    "/usr/share/fonts/truetype/arphic/uming.ttc",
132    // Alpine Linux
133    "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
134];
135
136/// Lazily loaded CJK system font bytes.  `None` means no CJK font was found.
137#[cfg(feature = "embed-fonts")]
138static SYSTEM_CJK_FONT: OnceLock<Option<Arc<Vec<u8>>>> = OnceLock::new();
139
140/// Try to load a CJK font from the host system, returning its raw bytes.
141#[cfg(feature = "embed-fonts")]
142fn system_cjk_font() -> Option<FontData> {
143    SYSTEM_CJK_FONT
144        .get_or_init(|| {
145            for path in CJK_FONT_CANDIDATE_PATHS {
146                if let Ok(bytes) = std::fs::read(path) {
147                    log::debug!("CJK fallback font loaded from {path}");
148                    return Some(Arc::new(bytes));
149                }
150            }
151            log::warn!(
152                "no system CJK font found; non-embedded CJK fonts will render with a Latin fallback"
153            );
154            None
155        })
156        .as_ref()
157        .map(|data| -> FontData { data.clone() })
158}
159
160impl Default for InterpreterSettings {
161    fn default() -> Self {
162        Self {
163            #[cfg(not(feature = "embed-fonts"))]
164            font_resolver: Arc::new(|_| None),
165            #[cfg(feature = "embed-fonts")]
166            font_resolver: Arc::new(|query| match query {
167                FontQuery::Standard(s) => Some(s.get_font_data()),
168                FontQuery::Fallback(f) => {
169                    // For non-embedded CJK fonts (Adobe-GB1, CNS1, Japan1, Korea1)
170                    // try a system CJK font first so characters render correctly.
171                    // This avoids the situation where a Latin fallback font is used
172                    // and Chinese/Japanese/Korean glyphs appear as "d", "a", etc.
173                    if f.character_collection
174                        .as_ref()
175                        .is_some_and(|cc| cc.family.is_cjk())
176                        && let Some(data) = system_cjk_font()
177                    {
178                        return Some((data, 0));
179                    }
180                    Some(f.pick_standard_font().get_font_data())
181                }
182            }),
183            #[cfg(feature = "embed-cmaps")]
184            cmap_resolver: Arc::new(pdf_font::cmap::load_embedded),
185            #[cfg(not(feature = "embed-cmaps"))]
186            cmap_resolver: Arc::new(|_| None),
187            warning_sink: Arc::new(|_| {}),
188            render_annotations: true,
189            skip_signature_widgets: true,
190            max_operator_count: None,
191        }
192    }
193}
194
195#[derive(Copy, Clone, Debug)]
196/// Warnings that can occur while interpreting a PDF file.
197pub enum InterpreterWarning {
198    /// An unsupported font kind was encountered.
199    ///
200    /// Currently, only CID fonts with non-identity encoding are unsupported.
201    UnsupportedFont,
202    /// An image failed to decode.
203    ImageDecodeFailure,
204    /// A stream exceeded the configured `max_stream_bytes` cap during
205    /// image decode.  Must not be silently discarded — propagate as
206    /// `LimitError::StreamTooLarge` / `Error::ResourceLimitExceeded`.
207    ///
208    /// Both fields are `u64` so the variant remains `Copy`.
209    StreamTooLarge {
210        /// Observed decompressed size in bytes.
211        observed: u64,
212        /// Configured limit in bytes.
213        limit: u64,
214    },
215}
216
217/// interpret the contents of the page and render them into the device.
218pub fn interpret_page<'a>(
219    page: &Page<'a>,
220    context: &mut Context<'a>,
221    device: &mut impl Device<'a>,
222) {
223    let resources = page.resources();
224    interpret(page.typed_operations(), resources, context, device);
225
226    if context.settings.render_annotations
227        && let Some(annot_arr) = page.raw().get::<Array<'_>>(ANNOTS)
228    {
229        for annot in annot_arr.iter::<Dict<'_>>() {
230            let flags = annot.get::<u32>(F).unwrap_or(0);
231
232            // Annotation should be hidden.
233            if flags & 2 != 0 {
234                continue;
235            }
236
237            // MuPDF renders signature widgets (/FT /Sig) with its own built-in
238            // "SIGN here" indicator and ignores the custom /AP/N stream, so we
239            // skip AP rendering for these annotations to match MuPDF output.
240            // Text extraction disables this skip so signature text is included.
241            if context.settings.skip_signature_widgets
242                && annot
243                    .get::<Name>(FT)
244                    .as_deref()
245                    .is_some_and(|n| n == b"Sig")
246            {
247                continue;
248            }
249
250            if let Some(apx) = annot
251                .get::<Dict<'_>>(AP)
252                .and_then(|ap| ap.get::<Stream<'_>>(N))
253                .and_then(|o| FormXObject::new(&o, &context.settings.warning_sink))
254            {
255                let Some(rect) = annot.get::<Rect>(RECT) else {
256                    continue;
257                };
258
259                let annot_rect = rect.to_kurbo();
260                // 12.5.5. Appearance streams
261                // "The algorithm outlined in this subclause shall be used
262                // to map from the coordinate system of the appearance XObject."
263
264                // 1) The appearance’s bounding box (specified by its BBox entry)
265                // shall be transformed, using Matrix, to produce a
266                // quadrilateral with arbitrary orientation. The transformed
267                // appearance box is the smallest upright rectangle that
268                // encompasses this quadrilateral.
269                let transformed_rect = (apx.matrix
270                    * kurbo::Rect::new(
271                        apx.bbox[0] as f64,
272                        apx.bbox[1] as f64,
273                        apx.bbox[2] as f64,
274                        apx.bbox[3] as f64,
275                    )
276                    .to_path(0.1))
277                .bounding_box();
278
279                // 2) A matrix A shall be computed that scales and translates
280                // the transformed appearance box to align with the edges
281                // of the annotation’s rectangle (specified by the Rect entry).
282                // A maps the lower-left corner (the corner with the smallest
283                // x and y coordinates) and the upper-right corner (the
284                // corner with the greatest x and y coordinates) of the
285                // transformed appearance box to the corresponding corners
286                // of the annotation’s rectangle.
287                let affine = Affine::new([
288                    annot_rect.width() / transformed_rect.width(),
289                    0.0,
290                    0.0,
291                    annot_rect.height() / transformed_rect.height(),
292                    annot_rect.x0 - transformed_rect.x0,
293                    annot_rect.y0 - transformed_rect.y0,
294                ]);
295
296                // 3) Matrix shall be concatenated with A to form a matrix
297                // AA that maps from the appearance’s coordinate system to
298                // the annotation’s rectangle in default user space.
299                context.save_state();
300                context.pre_concat_affine(affine);
301                context.push_root_transform();
302
303                draw_form_xobject(resources, &apx, context, device);
304                context.pop_root_transform();
305                context.restore_state(device);
306            }
307        }
308    }
309}
310
311/// Interpret the instructions from `ops` and render them into the device.
312pub fn interpret<'a, 'b>(
313    ops: impl Iterator<Item = TypedInstruction<'b>>,
314    resources: &Resources<'a>,
315    context: &mut Context<'a>,
316    device: &mut impl Device<'a>,
317) {
318    let num_states = context.num_states();
319    let max_operator_count = context.settings.max_operator_count.unwrap_or(u64::MAX);
320    let mut operator_count = 0_u64;
321
322    context.save_state();
323
324    for op in ops {
325        operator_count = operator_count.saturating_add(1);
326        if operator_count > max_operator_count {
327            warn!(
328                "content stream operator count exceeds {max_operator_count}, stopping interpretation"
329            );
330            break;
331        }
332
333        match op {
334            TypedInstruction::SaveState(_) => context.save_state(),
335            TypedInstruction::StrokeColorDeviceRgb(s) => {
336                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
337                context.get_mut().graphics_state.stroke_color =
338                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
339            }
340            TypedInstruction::StrokeColorDeviceGray(s) => {
341                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
342                context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
343            }
344            TypedInstruction::StrokeColorCmyk(s) => {
345                context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
346                context.get_mut().graphics_state.stroke_color =
347                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
348            }
349            TypedInstruction::LineWidth(w) => {
350                context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32();
351            }
352            TypedInstruction::LineCap(c) => {
353                context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c);
354            }
355            TypedInstruction::LineJoin(j) => {
356                context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j);
357            }
358            TypedInstruction::MiterLimit(l) => {
359                context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32();
360            }
361            TypedInstruction::Transform(t) => {
362                context.pre_concat_transform(t);
363            }
364            TypedInstruction::RectPath(r) => {
365                let rect = kurbo::Rect::new(
366                    r.0.as_f64(),
367                    r.1.as_f64(),
368                    r.0.as_f64() + r.2.as_f64(),
369                    r.1.as_f64() + r.3.as_f64(),
370                )
371                .to_path(0.1);
372                context.path_mut().extend(rect);
373            }
374            TypedInstruction::MoveTo(m) => {
375                let p = Point::new(m.0.as_f64(), m.1.as_f64());
376                *(context.last_point_mut()) = p;
377                *(context.sub_path_start_mut()) = p;
378                context.path_mut().move_to(p);
379            }
380            TypedInstruction::FillPathEvenOdd(_) => {
381                fill_path(context, device, FillRule::EvenOdd);
382            }
383            TypedInstruction::FillPathNonZero(_) => {
384                fill_path(context, device, FillRule::NonZero);
385            }
386            TypedInstruction::FillPathNonZeroCompatibility(_) => {
387                fill_path(context, device, FillRule::NonZero);
388            }
389            TypedInstruction::FillAndStrokeEvenOdd(_) => {
390                fill_stroke_path(context, device, FillRule::EvenOdd);
391            }
392            TypedInstruction::FillAndStrokeNonZero(_) => {
393                fill_stroke_path(context, device, FillRule::NonZero);
394            }
395            TypedInstruction::CloseAndStrokePath(_) => {
396                close_path(context);
397                stroke_path(context, device);
398            }
399            TypedInstruction::CloseFillAndStrokeEvenOdd(_) => {
400                close_path(context);
401                fill_stroke_path(context, device, FillRule::EvenOdd);
402            }
403            TypedInstruction::CloseFillAndStrokeNonZero(_) => {
404                close_path(context);
405                fill_stroke_path(context, device, FillRule::NonZero);
406            }
407            TypedInstruction::NonStrokeColorDeviceGray(s) => {
408                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
409                context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
410            }
411            TypedInstruction::NonStrokeColorDeviceRgb(s) => {
412                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
413                context.get_mut().graphics_state.non_stroke_color =
414                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
415            }
416            TypedInstruction::NonStrokeColorCmyk(s) => {
417                context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
418                context.get_mut().graphics_state.non_stroke_color =
419                    smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
420            }
421            TypedInstruction::LineTo(m) => {
422                if !context.path().elements().is_empty() {
423                    let last_point = *context.last_point();
424                    let mut p = Point::new(m.0.as_f64(), m.1.as_f64());
425                    *(context.last_point_mut()) = p;
426                    if last_point == p {
427                        // Add a small delta so that zero width lines can still have a round stroke.
428                        p.x += 0.0001;
429                    }
430
431                    context.path_mut().line_to(p);
432                }
433            }
434            TypedInstruction::CubicTo(c) => {
435                if !context.path().elements().is_empty() {
436                    let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
437                    let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
438                    let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
439
440                    *(context.last_point_mut()) = p3;
441
442                    context.path_mut().curve_to(p1, p2, p3);
443                }
444            }
445            TypedInstruction::CubicStartTo(c) => {
446                if !context.path().elements().is_empty() {
447                    let p1 = *context.last_point();
448                    let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
449                    let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
450
451                    *(context.last_point_mut()) = p3;
452
453                    context.path_mut().curve_to(p1, p2, p3);
454                }
455            }
456            TypedInstruction::CubicEndTo(c) => {
457                if !context.path().elements().is_empty() {
458                    let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
459                    let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
460
461                    *(context.last_point_mut()) = p3;
462
463                    context.path_mut().curve_to(p2, p3, p3);
464                }
465            }
466            TypedInstruction::ClosePath(_) => {
467                close_path(context);
468            }
469            TypedInstruction::SetGraphicsState(gs) => {
470                if let Some(gs) = resources
471                    .get_ext_g_state(gs.0.clone())
472                    .warn_none(&format!("failed to get extgstate {}", gs.0.as_str()))
473                {
474                    handle_gs(&gs, context, resources);
475                }
476            }
477            TypedInstruction::StrokePath(_) => {
478                stroke_path(context, device);
479            }
480            TypedInstruction::EndPath(_) => {
481                if let Some(clip) = *context.clip()
482                    && !context.path().elements().is_empty()
483                {
484                    let clip_path = context.get().ctm * context.path().clone();
485                    context.push_clip_path(clip_path, clip, device);
486
487                    *(context.clip_mut()) = None;
488                }
489
490                context.path_mut().truncate(0);
491            }
492            TypedInstruction::NonStrokeColor(c) => {
493                let fill_c = &mut context.get_mut().graphics_state.non_stroke_color;
494                fill_c.truncate(0);
495
496                for e in c.0 {
497                    fill_c.push(e.as_f32());
498                }
499            }
500            TypedInstruction::StrokeColor(c) => {
501                let stroke_c = &mut context.get_mut().graphics_state.stroke_color;
502                stroke_c.truncate(0);
503
504                for e in c.0 {
505                    stroke_c.push(e.as_f32());
506                }
507            }
508            TypedInstruction::ClipNonZero(_) => {
509                *(context.clip_mut()) = Some(FillRule::NonZero);
510            }
511            TypedInstruction::ClipEvenOdd(_) => {
512                *(context.clip_mut()) = Some(FillRule::EvenOdd);
513            }
514            TypedInstruction::RestoreState(_) => context.restore_state(device),
515            TypedInstruction::FlatnessTolerance(_) => {
516                // Ignore for now.
517            }
518            TypedInstruction::ColorSpaceStroke(c) => {
519                let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
520                    named
521                } else {
522                    context
523                        .get_color_space(resources, c.0)
524                        .unwrap_or(ColorSpace::device_gray())
525                };
526
527                context.get_mut().graphics_state.stroke_color = cs.initial_color();
528                context.get_mut().graphics_state.stroke_cs = cs;
529            }
530            TypedInstruction::ColorSpaceNonStroke(c) => {
531                let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
532                    named
533                } else {
534                    context
535                        .get_color_space(resources, c.0)
536                        .unwrap_or(ColorSpace::device_gray())
537                };
538
539                context.get_mut().graphics_state.non_stroke_color = cs.initial_color();
540                context.get_mut().graphics_state.none_stroke_cs = cs;
541            }
542            TypedInstruction::DashPattern(p) => {
543                context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
544                // kurbo apparently cannot properly deal with offsets that are exactly 0.
545                context.get_mut().graphics_state.stroke_props.dash_array =
546                    p.0.iter::<f32>()
547                        .map(|n| if n == 0.0 { 0.01 } else { n })
548                        .collect();
549            }
550            TypedInstruction::RenderingIntent(_) => {
551                // Ignore for now.
552            }
553            TypedInstruction::NonStrokeColorNamed(n) => {
554                context.get_mut().graphics_state.non_stroke_color =
555                    n.0.into_iter().map(|n| n.as_f32()).collect();
556                context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
557                    resources
558                        .get_pattern(name)
559                        .and_then(|d| Pattern::new(d, context, resources))
560                });
561            }
562            TypedInstruction::StrokeColorNamed(n) => {
563                context.get_mut().graphics_state.stroke_color =
564                    n.0.into_iter().map(|n| n.as_f32()).collect();
565                context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
566                    resources
567                        .get_pattern(name)
568                        .and_then(|d| Pattern::new(d, context, resources))
569                });
570            }
571            TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
572                // Properties can be either:
573                // 1. A Name that references an entry in the Resources/Properties dictionary
574                // 2. An inline dictionary with an OC key
575
576                let mcid = dict_or_stream(&bdc.1).and_then(|(props, _)| props.get::<i32>(MCID));
577
578                let oc = bdc
579                    .1
580                    .clone()
581                    .into_name()
582                    .and_then(|name| {
583                        let r = resources.properties.get_ref(name.clone())?;
584                        let d = resources
585                            .properties
586                            .get::<Dict<'_>>(name)
587                            .unwrap_or_default();
588                        Some((d, r))
589                    })
590                    .or_else(|| {
591                        let (props, _) = dict_or_stream(&bdc.1)?;
592                        let r = props.get_ref(OC)?;
593                        let d = props.get::<Dict<'_>>(OC).unwrap_or_default();
594                        Some((d, r))
595                    });
596
597                if let Some((dict, oc_ref)) = oc {
598                    context.ocg_state.begin_ocg(&dict, oc_ref.into());
599                } else {
600                    context.ocg_state.begin_marked_content();
601                }
602
603                device.begin_marked_content(&bdc.0, mcid);
604            }
605            TypedInstruction::MarkedContentPointWithProperties(_) => {}
606            TypedInstruction::EndMarkedContent(_) => {
607                context.ocg_state.end_marked_content();
608                device.end_marked_content();
609            }
610            TypedInstruction::MarkedContentPoint(_) => {}
611            TypedInstruction::BeginMarkedContent(bmc) => {
612                context.ocg_state.begin_marked_content();
613                device.begin_marked_content(&bmc.0, None);
614            }
615            TypedInstruction::BeginText(_) => {
616                context.get_mut().text_state.text_matrix = Affine::IDENTITY;
617                context.get_mut().text_state.text_line_matrix = Affine::IDENTITY;
618            }
619            TypedInstruction::SetTextMatrix(m) => {
620                let m = Affine::new([
621                    m.0.as_f64(),
622                    m.1.as_f64(),
623                    m.2.as_f64(),
624                    m.3.as_f64(),
625                    m.4.as_f64(),
626                    m.5.as_f64(),
627                ]);
628                context.get_mut().text_state.text_line_matrix = m;
629                context.get_mut().text_state.text_matrix = m;
630            }
631            TypedInstruction::EndText(_) => {
632                let has_outline = context
633                    .get()
634                    .text_state
635                    .clip_paths
636                    .segments()
637                    .next()
638                    .is_some();
639
640                if has_outline {
641                    let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone();
642
643                    context.push_clip_path(clip_path, FillRule::NonZero, device);
644                }
645
646                context.get_mut().text_state.clip_paths.truncate(0);
647            }
648            TypedInstruction::TextFont(t) => {
649                let name = t.0;
650
651                // In case we are unable to resolve the font, two scenarios:
652                // 1) If the font doesn't exist in the first place in the resource dictionary,
653                // assume Helvetica (this seems to be what other PDF viewers do).
654                // 2) In case it's `None` because we were unable to resolve the font
655                // (for whatever reason), leave it as `None`. Better showing no
656                // text at all than garbage text.
657                let font = if let Some(font_dict) = resources.get_font(name.clone()) {
658                    context.resolve_font(&font_dict)
659                } else {
660                    Font::new_standard(StandardFont::Helvetica, &context.settings.font_resolver)
661                        .map(TextStateFont::Fallback)
662                };
663
664                context.get_mut().text_state.font_size = t.1.as_f32();
665                context.get_mut().text_state.font = font;
666            }
667            TypedInstruction::ShowText(s) => {
668                if context.get().text_state.font.is_none() {
669                    // Even if no explicit font was set, we try to assume Helvetica. Acrobat
670                    // seems to do the same.
671                    context.get_mut().text_state.font = Font::new_standard(
672                        StandardFont::Helvetica,
673                        &context.settings.font_resolver,
674                    )
675                    .map(TextStateFont::Fallback);
676                }
677
678                text::show_text_string(context, device, resources, s.0);
679            }
680            TypedInstruction::ShowTexts(s) => {
681                if context.get().text_state.font.is_none() {
682                    // Even if no explicit font was set, we try to assume Helvetica. Acrobat
683                    // seems to do the same.
684                    context.get_mut().text_state.font = Font::new_standard(
685                        StandardFont::Helvetica,
686                        &context.settings.font_resolver,
687                    )
688                    .map(TextStateFont::Fallback);
689                }
690
691                for obj in s.0.iter::<Object<'_>>() {
692                    if let Some(adjustment) = obj.clone().into_f32() {
693                        // ANN[r17/TEX1] Surface TJ adjustment to the Device
694                        // before mutating the text matrix so extractors can
695                        // record the word-boundary signal alongside the
696                        // spatial gap they'd otherwise have to infer.
697                        device.text_adjustment(adjustment);
698                        context.get_mut().text_state.apply_adjustment(adjustment);
699                    } else if let Some(text) = obj.into_string() {
700                        text::show_text_string(context, device, resources, text);
701                    }
702                }
703            }
704            TypedInstruction::HorizontalScaling(h) => {
705                context.get_mut().text_state.horizontal_scaling = h.0.as_f32();
706            }
707            TypedInstruction::TextLeading(tl) => {
708                context.get_mut().text_state.leading = tl.0.as_f32();
709            }
710            TypedInstruction::CharacterSpacing(c) => {
711                context.get_mut().text_state.char_space = c.0.as_f32();
712            }
713            TypedInstruction::WordSpacing(w) => {
714                context.get_mut().text_state.word_space = w.0.as_f32();
715            }
716            TypedInstruction::NextLine(n) => {
717                let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
718                text::next_line(context, tx, ty);
719            }
720            TypedInstruction::NextLineUsingLeading(_) => {
721                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
722            }
723            TypedInstruction::NextLineAndShowText(n) => {
724                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
725                text::show_text_string(context, device, resources, n.0);
726            }
727            TypedInstruction::TextRenderingMode(r) => {
728                let mode = match r.0.as_i64() {
729                    0 => TextRenderingMode::Fill,
730                    1 => TextRenderingMode::Stroke,
731                    2 => TextRenderingMode::FillStroke,
732                    3 => TextRenderingMode::Invisible,
733                    4 => TextRenderingMode::FillAndClip,
734                    5 => TextRenderingMode::StrokeAndClip,
735                    6 => TextRenderingMode::FillAndStrokeAndClip,
736                    7 => TextRenderingMode::Clip,
737                    _ => {
738                        warn!("unknown text rendering mode {}", r.0.as_i64());
739
740                        TextRenderingMode::Fill
741                    }
742                };
743
744                context.get_mut().text_state.render_mode = mode;
745            }
746            TypedInstruction::NextLineAndSetLeading(n) => {
747                let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
748                context.get_mut().text_state.leading = -ty as f32;
749                text::next_line(context, tx, ty);
750            }
751            // d1: uncolored (shape) glyph header.  The advance width (wx) and
752            // bounding-box arguments are intentionally ignored here: the glyph
753            // advance is taken from the Type3 font's /Widths array (via
754            // Font::code_advance), and the is_shape_glyph flag is determined
755            // by the pre-scan in Type3::render_glyph before the stream is
756            // interpreted.
757            TypedInstruction::ShapeGlyph(_) => {}
758            TypedInstruction::XObject(x) => {
759                let cache = context.object_cache.clone();
760                let transfer_function = context.get().graphics_state.transfer_function.clone();
761                if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| {
762                    XObject::new(
763                        &s,
764                        &context.settings.warning_sink,
765                        &cache,
766                        transfer_function.clone(),
767                    )
768                }) {
769                    draw_xobject(&x_object, resources, context, device);
770                }
771            }
772            TypedInstruction::InlineImage(i) => {
773                let warning_sink = context.settings.warning_sink.clone();
774                let transfer_function = context.get().graphics_state.transfer_function.clone();
775                let cache = context.object_cache.clone();
776                if let Some(x_object) = ImageXObject::new(
777                    &i.0,
778                    |name| context.get_color_space(resources, name.clone()),
779                    &warning_sink,
780                    &cache,
781                    false,
782                    transfer_function,
783                ) {
784                    draw_image_xobject(&x_object, context, device);
785                }
786            }
787            TypedInstruction::TextRise(t) => {
788                context.get_mut().text_state.rise = t.0.as_f32();
789            }
790            TypedInstruction::Shading(s) => {
791                if !context.ocg_state.is_visible() {
792                    continue;
793                }
794
795                let transfer_function = context.get().graphics_state.transfer_function.clone();
796
797                if let Some(sp) = resources
798                    .get_shading(s.0)
799                    .and_then(|o| dict_or_stream(&o))
800                    .and_then(|s| {
801                        Shading::new(
802                            &s.0,
803                            s.1.as_ref(),
804                            &context.object_cache,
805                            &context.settings.warning_sink,
806                        )
807                    })
808                    .map(|s| {
809                        Pattern::Shading(ShadingPattern {
810                            shading: Arc::new(s),
811                            matrix: Affine::IDENTITY,
812                            opacity: context.get().graphics_state.non_stroke_alpha,
813                            transfer_function: transfer_function.clone(),
814                        })
815                    })
816                {
817                    context.save_state();
818                    context.push_root_transform();
819                    let st = context.get_mut();
820                    st.graphics_state.non_stroke_pattern = Some(sp);
821                    st.graphics_state.none_stroke_cs = ColorSpace::pattern();
822
823                    device.set_soft_mask(st.graphics_state.soft_mask.clone());
824                    device.set_blend_mode(st.graphics_state.blend_mode);
825
826                    let bbox = context.bbox().to_path(0.1);
827                    let inverted_bbox = context.get().ctm.inverse() * bbox;
828                    fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox));
829
830                    context.pop_root_transform();
831                    context.restore_state(device);
832                } else {
833                    warn!("failed to process shading");
834                }
835            }
836            TypedInstruction::BeginCompatibility(_) => {}
837            TypedInstruction::EndCompatibility(_) => {}
838            // d0: colored glyph header.  The advance width (wx) argument is
839            // intentionally ignored here for the same reason as d1 above.
840            TypedInstruction::ColorGlyph(_) => {}
841            TypedInstruction::ShowTextWithParameters(t) => {
842                context.get_mut().text_state.word_space = t.0.as_f32();
843                context.get_mut().text_state.char_space = t.1.as_f32();
844                text::next_line(context, 0.0, -context.get().text_state.leading as f64);
845                text::show_text_string(context, device, resources, t.2);
846            }
847            _ => {
848                warn!("failed to read an operator");
849            }
850        }
851    }
852
853    while context.num_states() > num_states {
854        context.restore_state(device);
855    }
856}