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, AS, F, FT, MCID, N, OC, PARENT, RECT, V};
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
33pub type FontResolverFn = Arc<dyn Fn(&FontQuery) -> Option<(FontData, u32)> + Send + Sync>;
38pub type CMapResolverFn =
40 Arc<dyn Fn(pdf_font::cmap::CMapName<'_>) -> Option<&'static [u8]> + Send + Sync>;
41pub type WarningSinkFn = Arc<dyn Fn(InterpreterWarning) + Send + Sync>;
43
44#[derive(Clone)]
45pub struct InterpreterSettings {
47 pub font_resolver: FontResolverFn,
83 pub cmap_resolver: CMapResolverFn,
96 pub warning_sink: WarningSinkFn,
99 pub render_annotations: bool,
104 pub skip_signature_widgets: bool,
109 pub max_operator_count: Option<u64>,
114 pub shared_cache: Option<crate::Cache>,
116}
117
118#[cfg(feature = "embed-fonts")]
121const CJK_FONT_CANDIDATE_PATHS: &[&str] = &[
122 "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
124 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
126 "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
127 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
128 "/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
129 "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
131 "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
132 "/usr/share/fonts/truetype/arphic/uming.ttc",
134 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
136];
137
138#[cfg(feature = "embed-fonts")]
140static SYSTEM_CJK_FONT: OnceLock<Option<Arc<Vec<u8>>>> = OnceLock::new();
141
142#[cfg(feature = "embed-fonts")]
144fn system_cjk_font() -> Option<FontData> {
145 SYSTEM_CJK_FONT
146 .get_or_init(|| {
147 for path in CJK_FONT_CANDIDATE_PATHS {
148 if let Ok(bytes) = std::fs::read(path) {
149 log::debug!("CJK fallback font loaded from {path}");
150 return Some(Arc::new(bytes));
151 }
152 }
153 log::warn!(
154 "no system CJK font found; non-embedded CJK fonts will render with a Latin fallback"
155 );
156 None
157 })
158 .as_ref()
159 .map(|data| -> FontData { data.clone() })
160}
161
162impl Default for InterpreterSettings {
163 fn default() -> Self {
164 Self {
165 #[cfg(not(feature = "embed-fonts"))]
166 font_resolver: Arc::new(|_| None),
167 #[cfg(feature = "embed-fonts")]
168 font_resolver: Arc::new(|query| match query {
169 FontQuery::Standard(s) => Some(s.get_font_data()),
170 FontQuery::Fallback(f) => {
171 if f.character_collection
176 .as_ref()
177 .is_some_and(|cc| cc.family.is_cjk())
178 && let Some(data) = system_cjk_font()
179 {
180 return Some((data, 0));
181 }
182 Some(f.pick_standard_font().get_font_data())
183 }
184 }),
185 #[cfg(feature = "embed-cmaps")]
186 cmap_resolver: Arc::new(pdf_font::cmap::load_embedded),
187 #[cfg(not(feature = "embed-cmaps"))]
188 cmap_resolver: Arc::new(|_| None),
189 warning_sink: Arc::new(|_| {}),
190 render_annotations: true,
191 skip_signature_widgets: true,
192 max_operator_count: None,
193 shared_cache: None,
194 }
195 }
196}
197
198#[derive(Copy, Clone, Debug)]
199pub enum InterpreterWarning {
201 UnsupportedFont,
205 ImageDecodeFailure,
207 StreamTooLarge {
213 observed: u64,
215 limit: u64,
217 },
218}
219
220fn normal_appearance_stream<'a>(annot: &Dict<'a>) -> Option<Stream<'a>> {
242 let ap = annot.get::<Dict<'_>>(AP)?;
243
244 if let Some(stream) = ap.get::<Stream<'_>>(N) {
246 return Some(stream);
247 }
248
249 let states = ap.get::<Dict<'_>>(N)?;
251
252 if let Some(state) = annot.get::<Name>(AS) {
253 return states.get::<Stream<'_>>(state.as_ref());
256 }
257
258 let candidates = [
261 annot.get::<Name>(V),
262 annot.get::<Dict<'_>>(PARENT).and_then(|p| p.get::<Name>(V)),
263 ];
264
265 candidates
266 .into_iter()
267 .flatten()
268 .find_map(|state| states.get::<Stream<'_>>(state.as_ref()))
269}
270
271pub fn interpret_page<'a>(
273 page: &Page<'a>,
274 context: &mut Context<'a>,
275 device: &mut impl Device<'a>,
276) {
277 let resources = page.resources();
278 interpret(page.typed_operations(), resources, context, device);
279
280 if context.settings.render_annotations
281 && let Some(annot_arr) = page.raw().get::<Array<'_>>(ANNOTS)
282 {
283 for annot in annot_arr.iter::<Dict<'_>>() {
284 let flags = annot.get::<u32>(F).unwrap_or(0);
285
286 if flags & 2 != 0 {
288 continue;
289 }
290
291 if context.settings.skip_signature_widgets
296 && annot
297 .get::<Name>(FT)
298 .as_deref()
299 .is_some_and(|n| n == b"Sig")
300 {
301 continue;
302 }
303
304 if let Some(apx) = normal_appearance_stream(&annot)
305 .and_then(|o| FormXObject::new(&o, &context.settings.warning_sink))
306 {
307 let Some(rect) = annot.get::<Rect>(RECT) else {
308 continue;
309 };
310
311 let annot_rect = rect.to_kurbo();
312 let transformed_rect = (apx.matrix
322 * kurbo::Rect::new(
323 apx.bbox[0] as f64,
324 apx.bbox[1] as f64,
325 apx.bbox[2] as f64,
326 apx.bbox[3] as f64,
327 )
328 .to_path(0.1))
329 .bounding_box();
330
331 let (tw, th) = (transformed_rect.width(), transformed_rect.height());
336 if !(tw.is_finite() && tw > 0.0 && th.is_finite() && th > 0.0) {
337 continue;
338 }
339
340 let affine = Affine::new([
349 annot_rect.width() / transformed_rect.width(),
350 0.0,
351 0.0,
352 annot_rect.height() / transformed_rect.height(),
353 annot_rect.x0 - transformed_rect.x0,
354 annot_rect.y0 - transformed_rect.y0,
355 ]);
356
357 context.save_state();
361 context.pre_concat_affine(affine);
362 context.push_root_transform();
363
364 draw_form_xobject(resources, &apx, context, device);
365 context.pop_root_transform();
366 context.restore_state(device);
367 }
368 }
369 }
370}
371
372pub fn interpret<'a, 'b>(
374 ops: impl Iterator<Item = TypedInstruction<'b>>,
375 resources: &Resources<'a>,
376 context: &mut Context<'a>,
377 device: &mut impl Device<'a>,
378) {
379 let num_states = context.num_states();
380 let max_operator_count = context.settings.max_operator_count.unwrap_or(u64::MAX);
381 let mut operator_count = 0_u64;
382
383 context.save_state();
384
385 for op in ops {
386 operator_count = operator_count.saturating_add(1);
387 if operator_count > max_operator_count {
388 warn!(
389 "content stream operator count exceeds {max_operator_count}, stopping interpretation"
390 );
391 break;
392 }
393
394 match op {
395 TypedInstruction::SaveState(_) => context.save_state(),
396 TypedInstruction::StrokeColorDeviceRgb(s) => {
397 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
398 context.get_mut().graphics_state.stroke_color =
399 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
400 }
401 TypedInstruction::StrokeColorDeviceGray(s) => {
402 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
403 context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
404 }
405 TypedInstruction::StrokeColorCmyk(s) => {
406 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
407 context.get_mut().graphics_state.stroke_color =
408 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
409 }
410 TypedInstruction::LineWidth(w) => {
411 context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32();
412 }
413 TypedInstruction::LineCap(c) => {
414 context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c);
415 }
416 TypedInstruction::LineJoin(j) => {
417 context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j);
418 }
419 TypedInstruction::MiterLimit(l) => {
420 context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32();
421 }
422 TypedInstruction::Transform(t) => {
423 context.pre_concat_transform(t);
424 }
425 TypedInstruction::RectPath(r) => {
426 let rect = kurbo::Rect::new(
427 r.0.as_f64(),
428 r.1.as_f64(),
429 r.0.as_f64() + r.2.as_f64(),
430 r.1.as_f64() + r.3.as_f64(),
431 )
432 .to_path(0.1);
433 context.path_mut().extend(rect);
434 }
435 TypedInstruction::MoveTo(m) => {
436 let p = Point::new(m.0.as_f64(), m.1.as_f64());
437 *(context.last_point_mut()) = p;
438 *(context.sub_path_start_mut()) = p;
439 context.path_mut().move_to(p);
440 }
441 TypedInstruction::FillPathEvenOdd(_) => {
442 fill_path(context, device, FillRule::EvenOdd);
443 }
444 TypedInstruction::FillPathNonZero(_) => {
445 fill_path(context, device, FillRule::NonZero);
446 }
447 TypedInstruction::FillPathNonZeroCompatibility(_) => {
448 fill_path(context, device, FillRule::NonZero);
449 }
450 TypedInstruction::FillAndStrokeEvenOdd(_) => {
451 fill_stroke_path(context, device, FillRule::EvenOdd);
452 }
453 TypedInstruction::FillAndStrokeNonZero(_) => {
454 fill_stroke_path(context, device, FillRule::NonZero);
455 }
456 TypedInstruction::CloseAndStrokePath(_) => {
457 close_path(context);
458 stroke_path(context, device);
459 }
460 TypedInstruction::CloseFillAndStrokeEvenOdd(_) => {
461 close_path(context);
462 fill_stroke_path(context, device, FillRule::EvenOdd);
463 }
464 TypedInstruction::CloseFillAndStrokeNonZero(_) => {
465 close_path(context);
466 fill_stroke_path(context, device, FillRule::NonZero);
467 }
468 TypedInstruction::NonStrokeColorDeviceGray(s) => {
469 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
470 context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
471 }
472 TypedInstruction::NonStrokeColorDeviceRgb(s) => {
473 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
474 context.get_mut().graphics_state.non_stroke_color =
475 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
476 }
477 TypedInstruction::NonStrokeColorCmyk(s) => {
478 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
479 context.get_mut().graphics_state.non_stroke_color =
480 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
481 }
482 TypedInstruction::LineTo(m) => {
483 if !context.path().elements().is_empty() {
484 let last_point = *context.last_point();
485 let mut p = Point::new(m.0.as_f64(), m.1.as_f64());
486 *(context.last_point_mut()) = p;
487 if last_point == p {
488 p.x += 0.0001;
490 }
491
492 context.path_mut().line_to(p);
493 }
494 }
495 TypedInstruction::CubicTo(c) => {
496 if !context.path().elements().is_empty() {
497 let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
498 let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
499 let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
500
501 *(context.last_point_mut()) = p3;
502
503 context.path_mut().curve_to(p1, p2, p3);
504 }
505 }
506 TypedInstruction::CubicStartTo(c) => {
507 if !context.path().elements().is_empty() {
508 let p1 = *context.last_point();
509 let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
510 let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
511
512 *(context.last_point_mut()) = p3;
513
514 context.path_mut().curve_to(p1, p2, p3);
515 }
516 }
517 TypedInstruction::CubicEndTo(c) => {
518 if !context.path().elements().is_empty() {
519 let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
520 let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
521
522 *(context.last_point_mut()) = p3;
523
524 context.path_mut().curve_to(p2, p3, p3);
525 }
526 }
527 TypedInstruction::ClosePath(_) => {
528 close_path(context);
529 }
530 TypedInstruction::SetGraphicsState(gs) => {
531 if let Some(gs) = resources
532 .get_ext_g_state(gs.0.clone())
533 .warn_none(&format!("failed to get extgstate {}", gs.0.as_str()))
534 {
535 handle_gs(&gs, context, resources);
536 }
537 }
538 TypedInstruction::StrokePath(_) => {
539 stroke_path(context, device);
540 }
541 TypedInstruction::EndPath(_) => {
542 if let Some(clip) = *context.clip()
543 && !context.path().elements().is_empty()
544 {
545 let clip_path = context.get().ctm * context.path().clone();
546 context.push_clip_path(clip_path, clip, device);
547
548 *(context.clip_mut()) = None;
549 }
550
551 context.path_mut().truncate(0);
552 }
553 TypedInstruction::NonStrokeColor(c) => {
554 let fill_c = &mut context.get_mut().graphics_state.non_stroke_color;
555 fill_c.truncate(0);
556
557 for e in c.0 {
558 fill_c.push(e.as_f32());
559 }
560 }
561 TypedInstruction::StrokeColor(c) => {
562 let stroke_c = &mut context.get_mut().graphics_state.stroke_color;
563 stroke_c.truncate(0);
564
565 for e in c.0 {
566 stroke_c.push(e.as_f32());
567 }
568 }
569 TypedInstruction::ClipNonZero(_) => {
570 *(context.clip_mut()) = Some(FillRule::NonZero);
571 }
572 TypedInstruction::ClipEvenOdd(_) => {
573 *(context.clip_mut()) = Some(FillRule::EvenOdd);
574 }
575 TypedInstruction::RestoreState(_) => context.restore_state(device),
576 TypedInstruction::FlatnessTolerance(_) => {
577 }
579 TypedInstruction::ColorSpaceStroke(c) => {
580 let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
581 named
582 } else {
583 context
584 .get_color_space(resources, c.0)
585 .unwrap_or(ColorSpace::device_gray())
586 };
587
588 context.get_mut().graphics_state.stroke_color = cs.initial_color();
589 context.get_mut().graphics_state.stroke_cs = cs;
590 }
591 TypedInstruction::ColorSpaceNonStroke(c) => {
592 let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
593 named
594 } else {
595 context
596 .get_color_space(resources, c.0)
597 .unwrap_or(ColorSpace::device_gray())
598 };
599
600 context.get_mut().graphics_state.non_stroke_color = cs.initial_color();
601 context.get_mut().graphics_state.none_stroke_cs = cs;
602 }
603 TypedInstruction::DashPattern(p) => {
604 context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
605 context.get_mut().graphics_state.stroke_props.dash_array =
607 p.0.iter::<f32>()
608 .map(|n| if n == 0.0 { 0.01 } else { n })
609 .collect();
610 }
611 TypedInstruction::RenderingIntent(_) => {
612 }
614 TypedInstruction::NonStrokeColorNamed(n) => {
615 context.get_mut().graphics_state.non_stroke_color =
616 n.0.into_iter().map(|n| n.as_f32()).collect();
617 context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
618 resources
619 .get_pattern(name)
620 .and_then(|d| Pattern::new(d, context, resources))
621 });
622 }
623 TypedInstruction::StrokeColorNamed(n) => {
624 context.get_mut().graphics_state.stroke_color =
625 n.0.into_iter().map(|n| n.as_f32()).collect();
626 context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
627 resources
628 .get_pattern(name)
629 .and_then(|d| Pattern::new(d, context, resources))
630 });
631 }
632 TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
633 let mcid = dict_or_stream(&bdc.1).and_then(|(props, _)| props.get::<i32>(MCID));
638
639 let oc = bdc
640 .1
641 .clone()
642 .into_name()
643 .and_then(|name| {
644 let r = resources.properties.get_ref(name.clone())?;
645 let d = resources
646 .properties
647 .get::<Dict<'_>>(name)
648 .unwrap_or_default();
649 Some((d, r))
650 })
651 .or_else(|| {
652 let (props, _) = dict_or_stream(&bdc.1)?;
653 let r = props.get_ref(OC)?;
654 let d = props.get::<Dict<'_>>(OC).unwrap_or_default();
655 Some((d, r))
656 });
657
658 if let Some((dict, oc_ref)) = oc {
659 context.ocg_state.begin_ocg(&dict, oc_ref.into());
660 } else {
661 context.ocg_state.begin_marked_content();
662 }
663
664 device.begin_marked_content(&bdc.0, mcid);
665 }
666 TypedInstruction::MarkedContentPointWithProperties(_) => {}
667 TypedInstruction::EndMarkedContent(_) => {
668 context.ocg_state.end_marked_content();
669 device.end_marked_content();
670 }
671 TypedInstruction::MarkedContentPoint(_) => {}
672 TypedInstruction::BeginMarkedContent(bmc) => {
673 context.ocg_state.begin_marked_content();
674 device.begin_marked_content(&bmc.0, None);
675 }
676 TypedInstruction::BeginText(_) => {
677 context.get_mut().text_state.text_matrix = Affine::IDENTITY;
678 context.get_mut().text_state.text_line_matrix = Affine::IDENTITY;
679 }
680 TypedInstruction::SetTextMatrix(m) => {
681 let m = Affine::new([
682 m.0.as_f64(),
683 m.1.as_f64(),
684 m.2.as_f64(),
685 m.3.as_f64(),
686 m.4.as_f64(),
687 m.5.as_f64(),
688 ]);
689 context.get_mut().text_state.text_line_matrix = m;
690 context.get_mut().text_state.text_matrix = m;
691 }
692 TypedInstruction::EndText(_) => {
693 let has_outline = context
694 .get()
695 .text_state
696 .clip_paths
697 .segments()
698 .next()
699 .is_some();
700
701 if has_outline {
702 let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone();
703
704 context.push_clip_path(clip_path, FillRule::NonZero, device);
705 }
706
707 context.get_mut().text_state.clip_paths.truncate(0);
708 }
709 TypedInstruction::TextFont(t) => {
710 let name = t.0;
711
712 let font = if let Some(font_dict) = resources.get_font(name.clone()) {
719 context.resolve_font(&font_dict)
720 } else {
721 Font::new_standard(StandardFont::Helvetica, &context.settings.font_resolver)
722 .map(TextStateFont::Fallback)
723 };
724
725 context.get_mut().text_state.font_size = t.1.as_f32();
726 context.get_mut().text_state.font = font;
727 }
728 TypedInstruction::ShowText(s) => {
729 if context.get().text_state.font.is_none() {
730 context.get_mut().text_state.font = Font::new_standard(
733 StandardFont::Helvetica,
734 &context.settings.font_resolver,
735 )
736 .map(TextStateFont::Fallback);
737 }
738
739 text::show_text_string(context, device, resources, s.0);
740 }
741 TypedInstruction::ShowTexts(s) => {
742 if context.get().text_state.font.is_none() {
743 context.get_mut().text_state.font = Font::new_standard(
746 StandardFont::Helvetica,
747 &context.settings.font_resolver,
748 )
749 .map(TextStateFont::Fallback);
750 }
751
752 for obj in s.0.iter::<Object<'_>>() {
753 if let Some(adjustment) = obj.clone().into_f32() {
754 device.text_adjustment(adjustment);
759 context.get_mut().text_state.apply_adjustment(adjustment);
760 } else if let Some(text) = obj.into_string() {
761 text::show_text_string(context, device, resources, text);
762 }
763 }
764 }
765 TypedInstruction::HorizontalScaling(h) => {
766 context.get_mut().text_state.horizontal_scaling = h.0.as_f32();
767 }
768 TypedInstruction::TextLeading(tl) => {
769 context.get_mut().text_state.leading = tl.0.as_f32();
770 }
771 TypedInstruction::CharacterSpacing(c) => {
772 context.get_mut().text_state.char_space = c.0.as_f32();
773 }
774 TypedInstruction::WordSpacing(w) => {
775 context.get_mut().text_state.word_space = w.0.as_f32();
776 }
777 TypedInstruction::NextLine(n) => {
778 let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
779 text::next_line(context, tx, ty);
780 }
781 TypedInstruction::NextLineUsingLeading(_) => {
782 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
783 }
784 TypedInstruction::NextLineAndShowText(n) => {
785 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
786 text::show_text_string(context, device, resources, n.0);
787 }
788 TypedInstruction::TextRenderingMode(r) => {
789 let mode = match r.0.as_i64() {
790 0 => TextRenderingMode::Fill,
791 1 => TextRenderingMode::Stroke,
792 2 => TextRenderingMode::FillStroke,
793 3 => TextRenderingMode::Invisible,
794 4 => TextRenderingMode::FillAndClip,
795 5 => TextRenderingMode::StrokeAndClip,
796 6 => TextRenderingMode::FillAndStrokeAndClip,
797 7 => TextRenderingMode::Clip,
798 _ => {
799 warn!("unknown text rendering mode {}", r.0.as_i64());
800
801 TextRenderingMode::Fill
802 }
803 };
804
805 context.get_mut().text_state.render_mode = mode;
806 }
807 TypedInstruction::NextLineAndSetLeading(n) => {
808 let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
809 context.get_mut().text_state.leading = -ty as f32;
810 text::next_line(context, tx, ty);
811 }
812 TypedInstruction::ShapeGlyph(_) => {}
819 TypedInstruction::XObject(x) => {
820 let cache = context.object_cache.clone();
821 let transfer_function = context.get().graphics_state.transfer_function.clone();
822 if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| {
823 XObject::new(
824 &s,
825 &context.settings.warning_sink,
826 &cache,
827 transfer_function.clone(),
828 )
829 }) {
830 draw_xobject(&x_object, resources, context, device);
831 }
832 }
833 TypedInstruction::InlineImage(i) => {
834 let warning_sink = context.settings.warning_sink.clone();
835 let transfer_function = context.get().graphics_state.transfer_function.clone();
836 let cache = context.object_cache.clone();
837 if let Some(x_object) = ImageXObject::new(
838 &i.0,
839 |name| context.get_color_space(resources, name.clone()),
840 &warning_sink,
841 &cache,
842 false,
843 transfer_function,
844 ) {
845 draw_image_xobject(&x_object, context, device);
846 }
847 }
848 TypedInstruction::TextRise(t) => {
849 context.get_mut().text_state.rise = t.0.as_f32();
850 }
851 TypedInstruction::Shading(s) => {
852 if !context.ocg_state.is_visible() {
853 continue;
854 }
855
856 let transfer_function = context.get().graphics_state.transfer_function.clone();
857
858 if let Some(sp) = resources
859 .get_shading(s.0)
860 .and_then(|o| dict_or_stream(&o))
861 .and_then(|s| {
862 Shading::new(
863 &s.0,
864 s.1.as_ref(),
865 &context.object_cache,
866 &context.settings.warning_sink,
867 )
868 })
869 .map(|s| {
870 Pattern::Shading(ShadingPattern {
871 shading: Arc::new(s),
872 matrix: Affine::IDENTITY,
873 opacity: context.get().graphics_state.non_stroke_alpha,
874 transfer_function: transfer_function.clone(),
875 })
876 })
877 {
878 context.save_state();
879 context.push_root_transform();
880 let st = context.get_mut();
881 st.graphics_state.non_stroke_pattern = Some(sp);
882 st.graphics_state.none_stroke_cs = ColorSpace::pattern();
883
884 device.set_soft_mask(st.graphics_state.soft_mask.clone());
885 device.set_blend_mode(st.graphics_state.blend_mode);
886
887 let bbox = context.bbox().to_path(0.1);
888 let inverted_bbox = context.get().ctm.inverse() * bbox;
889 fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox));
890
891 context.pop_root_transform();
892 context.restore_state(device);
893 } else {
894 warn!("failed to process shading");
895 }
896 }
897 TypedInstruction::BeginCompatibility(_) => {}
898 TypedInstruction::EndCompatibility(_) => {}
899 TypedInstruction::ColorGlyph(_) => {}
902 TypedInstruction::ShowTextWithParameters(t) => {
903 context.get_mut().text_state.word_space = t.0.as_f32();
904 context.get_mut().text_state.char_space = t.1.as_f32();
905 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
906 text::show_text_string(context, device, resources, t.2);
907 }
908 _ => {
909 warn!("failed to read an operator");
910 }
911 }
912 }
913
914 while context.num_states() > num_states {
915 context.restore_state(device);
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use crate::device::Device;
922 use crate::font::Glyph;
923 use crate::soft_mask::SoftMask;
924 use crate::util::PageExt;
925 use crate::{
926 BlendMode, ClipPath, Context, GlyphDrawMode, Image, InterpreterSettings, Paint,
927 PathDrawMode, interpret_page,
928 };
929 use kurbo::{Affine, BezPath, Shape};
930 use pdf_syntax::Pdf;
931
932 #[derive(Default)]
936 struct CountingDevice {
937 path_widths: Vec<f64>,
938 }
939
940 impl Device<'_> for CountingDevice {
941 fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
942 fn set_blend_mode(&mut self, _: BlendMode) {}
943 fn draw_path(&mut self, path: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {
944 self.path_widths.push(path.bounding_box().width());
945 }
946 fn push_clip_path(&mut self, _: &ClipPath) {}
947 fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
948 fn draw_glyph(
949 &mut self,
950 _: &Glyph<'_>,
951 _: Affine,
952 _: Affine,
953 _: &Paint<'_>,
954 _: &GlyphDrawMode,
955 ) {
956 }
957 fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {}
958 fn pop_clip_path(&mut self) {}
959 fn pop_transparency_group(&mut self) {}
960 }
961
962 fn build_pdf(objects: &[Vec<u8>]) -> Vec<u8> {
965 let mut out = b"%PDF-1.7\n".to_vec();
966 let mut offsets = Vec::with_capacity(objects.len());
967
968 for (i, body) in objects.iter().enumerate() {
969 offsets.push(out.len());
970 out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
971 out.extend_from_slice(body);
972 out.extend_from_slice(b"\nendobj\n");
973 }
974
975 let xref_pos = out.len();
976 out.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
977 out.extend_from_slice(b"0000000000 65535 f \n");
978 for offset in offsets {
979 out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
980 }
981 out.extend_from_slice(
982 format!(
983 "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",
984 objects.len() + 1
985 )
986 .as_bytes(),
987 );
988
989 out
990 }
991
992 fn form_stream(bbox: &str, content: &str) -> Vec<u8> {
994 format!(
995 "<< /Type /XObject /Subtype /Form /BBox {bbox} /Length {} >>\nstream\n{content}\nendstream",
996 content.len()
997 )
998 .into_bytes()
999 }
1000
1001 fn checkbox_pdf(annot_body: &[u8], on_bbox: &str, extra_objects: &[Vec<u8>]) -> Vec<u8> {
1009 let mut objects = vec![
1010 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
1011 b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
1012 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
1013 /Annots [4 0 R] /Contents 7 0 R >>"
1014 .to_vec(),
1015 annot_body.to_vec(),
1016 form_stream(on_bbox, "0 0 10 10 re f\n12 12 4 4 re f"),
1017 form_stream("[0 0 20 20]", "0 0 7 7 re f"),
1018 b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
1019 ];
1020 objects.extend_from_slice(extra_objects);
1021 build_pdf(&objects)
1022 }
1023
1024 fn interpret_widths(pdf_bytes: Vec<u8>) -> Vec<f64> {
1027 let pdf = Pdf::new(pdf_bytes).expect("test PDF must parse");
1028 let pages = pdf.pages();
1029 let page = pages.first().expect("test PDF must have one page");
1030
1031 let settings = InterpreterSettings::default();
1032 let initial_transform = page.initial_transform(true);
1033 let bbox = kurbo::Rect::new(0.0, 0.0, 100.0, 100.0);
1034 let mut context = Context::new(initial_transform, bbox, page.xref(), settings);
1035 let mut device = CountingDevice::default();
1036
1037 interpret_page(page, &mut context, &mut device);
1038 device.path_widths
1039 }
1040
1041 fn assert_widths(widths: &[f64], expected: &[f64]) {
1042 assert_eq!(
1043 widths.len(),
1044 expected.len(),
1045 "expected {expected:?}, got {widths:?}"
1046 );
1047 for (got, want) in widths.iter().zip(expected) {
1048 assert!(
1049 (got - want).abs() < 1e-6,
1050 "expected {expected:?}, got {widths:?}"
1051 );
1052 }
1053 }
1054
1055 #[test]
1058 fn widget_substate_as_on_state() {
1059 let pdf = checkbox_pdf(
1060 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1061 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Yes >>",
1062 "[0 0 20 20]",
1063 &[],
1064 );
1065 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1066 }
1067
1068 #[test]
1071 fn widget_substate_as_off_state() {
1072 let pdf = checkbox_pdf(
1073 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1074 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Off >>",
1075 "[0 0 20 20]",
1076 &[],
1077 );
1078 assert_widths(&interpret_widths(pdf), &[7.0]);
1079 }
1080
1081 #[test]
1084 fn widget_substate_as_off_without_off_entry() {
1085 let pdf = checkbox_pdf(
1086 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1087 /AP << /N << /Yes 5 0 R >> >> /AS /Off >>",
1088 "[0 0 20 20]",
1089 &[],
1090 );
1091 assert_widths(&interpret_widths(pdf), &[]);
1092 }
1093
1094 #[test]
1097 fn widget_substate_v_fallback() {
1098 let pdf = checkbox_pdf(
1099 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1100 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /V /Yes >>",
1101 "[0 0 20 20]",
1102 &[],
1103 );
1104 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1105 }
1106
1107 #[test]
1111 fn widget_substate_parent_v_fallback() {
1112 let pdf = checkbox_pdf(
1113 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1114 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /Parent 8 0 R >>",
1115 "[0 0 20 20]",
1116 &[b"<< /FT /Btn /V /Yes >>".to_vec()],
1117 );
1118 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1119 }
1120
1121 #[test]
1125 fn widget_substate_non_ascii_state_name() {
1126 let annot = b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1127 /AP << /N << /Stra\xf6m 5 0 R /Off 6 0 R >> >> /AS /Stra#F6m >>";
1128 assert!(annot.contains(&0xf6));
1130 let pdf = checkbox_pdf(annot, "[0 0 20 20]", &[]);
1131 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1132 }
1133
1134 #[test]
1137 fn widget_degenerate_bbox_skipped() {
1138 let pdf = checkbox_pdf(
1139 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1140 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Yes >>",
1141 "[0 0 0 20]",
1142 &[],
1143 );
1144 assert_widths(&interpret_widths(pdf), &[]);
1145 }
1146
1147 #[test]
1149 fn widget_direct_stream_still_renders() {
1150 let pdf = checkbox_pdf(
1151 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1152 /AP << /N 6 0 R >> >>",
1153 "[0 0 20 20]",
1154 &[],
1155 );
1156 assert_widths(&interpret_widths(pdf), &[7.0]);
1157 }
1158}