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