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, decode_text_string};
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::{
22 ACTUAL_TEXT, ANNOTS, AP, AS, BTN, CH, DV, F, FF, FT, KIDS, MCID, N, OC, OPT, PARENT, RECT,
23 SUBTYPE, TX, V, WIDGET,
24};
25use pdf_syntax::object::{
26 Array, Dict, Name, Object, Rect, Stream, String as PdfString, dict_or_stream,
27};
28use pdf_syntax::page::{Page, Resources};
29use smallvec::smallvec;
30use std::sync::{Arc, OnceLock};
31
32pub(crate) mod path;
33pub(crate) mod state;
34pub(crate) mod text;
35
36pub use state::ActiveTransferFunction;
37
38pub type FontResolverFn = Arc<dyn Fn(&FontQuery) -> Option<(FontData, u32)> + Send + Sync>;
43pub type CMapResolverFn =
45 Arc<dyn Fn(pdf_font::cmap::CMapName<'_>) -> Option<&'static [u8]> + Send + Sync>;
46pub type WarningSinkFn = Arc<dyn Fn(InterpreterWarning) + Send + Sync>;
48
49#[derive(Clone)]
50pub struct InterpreterSettings {
52 pub font_resolver: FontResolverFn,
88 pub cmap_resolver: CMapResolverFn,
101 pub warning_sink: WarningSinkFn,
104 pub render_annotations: bool,
109 pub skip_signature_widgets: bool,
114 pub max_operator_count: Option<u64>,
119 pub shared_cache: Option<crate::Cache>,
121}
122
123#[cfg(feature = "embed-fonts")]
126const CJK_FONT_CANDIDATE_PATHS: &[&str] = &[
127 "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
129 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
131 "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
132 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
133 "/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
134 "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
136 "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
137 "/usr/share/fonts/truetype/arphic/uming.ttc",
139 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
141];
142
143#[cfg(feature = "embed-fonts")]
145static SYSTEM_CJK_FONT: OnceLock<Option<Arc<Vec<u8>>>> = OnceLock::new();
146
147#[cfg(feature = "embed-fonts")]
149fn system_cjk_font() -> Option<FontData> {
150 SYSTEM_CJK_FONT
151 .get_or_init(|| {
152 for path in CJK_FONT_CANDIDATE_PATHS {
153 if let Ok(bytes) = std::fs::read(path) {
154 log::debug!("CJK fallback font loaded from {path}");
155 return Some(Arc::new(bytes));
156 }
157 }
158 log::warn!(
159 "no system CJK font found; non-embedded CJK fonts will render with a Latin fallback"
160 );
161 None
162 })
163 .as_ref()
164 .map(|data| -> FontData { data.clone() })
165}
166
167impl Default for InterpreterSettings {
168 fn default() -> Self {
169 Self {
170 #[cfg(not(feature = "embed-fonts"))]
171 font_resolver: Arc::new(|_| None),
172 #[cfg(feature = "embed-fonts")]
173 font_resolver: Arc::new(|query| match query {
174 FontQuery::Standard(s) => Some(s.get_font_data()),
175 FontQuery::Fallback(f) => {
176 if f.character_collection
181 .as_ref()
182 .is_some_and(|cc| cc.family.is_cjk())
183 && let Some(data) = system_cjk_font()
184 {
185 return Some((data, 0));
186 }
187 Some(f.pick_standard_font().get_font_data())
188 }
189 }),
190 #[cfg(feature = "embed-cmaps")]
191 cmap_resolver: Arc::new(pdf_font::cmap::load_embedded),
192 #[cfg(not(feature = "embed-cmaps"))]
193 cmap_resolver: Arc::new(|_| None),
194 warning_sink: Arc::new(|_| {}),
195 render_annotations: true,
196 skip_signature_widgets: true,
197 max_operator_count: None,
198 shared_cache: None,
199 }
200 }
201}
202
203#[derive(Copy, Clone, Debug)]
204pub enum InterpreterWarning {
206 UnsupportedFont,
210 ImageDecodeFailure,
212 StreamTooLarge {
218 observed: u64,
220 limit: u64,
222 },
223 NestingTooDeep {
232 limit: u32,
234 },
235}
236
237fn normal_appearance_stream<'a>(annot: &Dict<'a>) -> Option<Stream<'a>> {
259 let ap = annot.get::<Dict<'_>>(AP)?;
260
261 if let Some(stream) = ap.get::<Stream<'_>>(N) {
263 return Some(stream);
264 }
265
266 let states = ap.get::<Dict<'_>>(N)?;
268
269 if let Some(state) = annot.get::<Name>(AS) {
270 return states.get::<Stream<'_>>(state.as_ref());
273 }
274
275 let candidates = [
278 annot.get::<Name>(V),
279 annot.get::<Dict<'_>>(PARENT).and_then(|p| p.get::<Name>(V)),
280 ];
281
282 candidates
283 .into_iter()
284 .flatten()
285 .find_map(|state| states.get::<Stream<'_>>(state.as_ref()))
286}
287
288const ANNOT_FLAG_HIDDEN: u32 = 1 << 1;
290const ANNOT_FLAG_NOVIEW: u32 = 1 << 5;
292
293const FIELD_INHERITANCE_DEPTH: usize = 32;
299
300const FIELD_FLAG_PUSHBUTTON: u32 = 1 << 16;
302const FIELD_FLAG_PASSWORD: u32 = 1 << 13;
304
305fn inherited_field_entry<'a, T>(
308 annot: &Dict<'a>,
309 entry: impl Fn(&Dict<'a>) -> Option<T>,
310) -> Option<T> {
311 let mut node = annot.clone();
312
313 for _ in 0..FIELD_INHERITANCE_DEPTH {
314 if let Some(value) = entry(&node) {
315 return Some(value);
316 }
317
318 node = node.get::<Dict<'a>>(PARENT)?;
319 }
320
321 None
322}
323
324fn name_as_text(name: &Name) -> Option<String> {
331 std::str::from_utf8(name.as_ref()).ok().map(str::to_string)
332}
333
334fn choice_option_label(annot: &Dict<'_>, value: &str) -> String {
344 let Some(options) = inherited_field_entry(annot, |d| d.get::<Array<'_>>(OPT)) else {
345 return value.to_string();
346 };
347
348 for option in options.iter::<Array<'_>>() {
349 let mut pair = option.iter::<PdfString>();
350 let Some(export) = pair.next().and_then(|s| decode_text_string(s.as_bytes())) else {
351 continue;
352 };
353 let Some(display) = pair.next().and_then(|s| decode_text_string(s.as_bytes())) else {
354 continue;
355 };
356
357 if export == value {
358 return display;
359 }
360 }
361
362 value.to_string()
363}
364
365fn choice_value_text<'a>(annot: &Dict<'a>, key: &'static [u8]) -> Option<String> {
367 if let Some(value) = inherited_field_entry(annot, |d| d.get::<PdfString>(key)) {
368 let value = decode_text_string(value.as_bytes())?;
369 return Some(choice_option_label(annot, &value));
370 }
371
372 let options = inherited_field_entry(annot, |d| d.get::<Array<'a>>(key))?;
376 let lines: Vec<String> = options
377 .iter::<PdfString>()
378 .filter_map(|option| decode_text_string(option.as_bytes()))
379 .map(|value| choice_option_label(annot, &value))
380 .collect();
381
382 (!lines.is_empty()).then(|| lines.join("\n"))
383}
384
385fn is_the_only_widget_of_its_field(annot: &Dict<'_>) -> bool {
394 let Some(parent) = annot.get::<Dict<'_>>(PARENT) else {
395 return true;
396 };
397
398 match parent.get::<Array<'_>>(KIDS) {
399 Some(kids) => kids.raw_iter().take(2).count() < 2,
400 None => true,
401 }
402}
403
404fn field_value_text(annot: &Dict<'_>) -> Option<String> {
416 let field_type = inherited_field_entry(annot, |d| d.get::<Name>(FT))?;
417 let flags = inherited_field_entry(annot, |d| d.get::<u32>(FF)).unwrap_or(0);
418
419 match field_type.as_ref() {
420 BTN => {
421 if flags & FIELD_FLAG_PUSHBUTTON != 0 {
422 return None;
423 }
424
425 let state = annot.get::<Name>(AS).or_else(|| {
430 is_the_only_widget_of_its_field(annot)
431 .then(|| {
432 inherited_field_entry(annot, |d| d.get::<Name>(V))
433 .or_else(|| inherited_field_entry(annot, |d| d.get::<Name>(DV)))
434 })
435 .flatten()
436 })?;
437
438 if state.as_ref() == b"Off" {
442 return None;
443 }
444
445 name_as_text(&state)
446 }
447 TX => {
448 if flags & FIELD_FLAG_PASSWORD != 0 {
449 return None;
450 }
451
452 let value = inherited_field_entry(annot, |d| d.get::<PdfString>(V))
456 .or_else(|| inherited_field_entry(annot, |d| d.get::<PdfString>(DV)))?;
457 let text = decode_text_string(value.as_bytes())?;
458
459 (!text.trim().is_empty()).then_some(text)
460 }
461 CH => choice_value_text(annot, V).or_else(|| choice_value_text(annot, DV)),
462 _ => None,
463 }
464}
465
466pub fn interpret_page<'a>(
468 page: &Page<'a>,
469 context: &mut Context<'a>,
470 device: &mut impl Device<'a>,
471) {
472 let resources = page.resources();
473 interpret(page.typed_operations(), resources, context, device);
474
475 if context.settings.render_annotations
476 && let Some(annot_arr) = page.raw().get::<Array<'_>>(ANNOTS)
477 {
478 for annot in annot_arr.iter::<Dict<'_>>() {
479 let flags = annot.get::<u32>(F).unwrap_or(0);
480
481 if flags & ANNOT_FLAG_HIDDEN != 0 {
483 continue;
484 }
485
486 if context.settings.skip_signature_widgets
491 && annot
492 .get::<Name>(FT)
493 .as_deref()
494 .is_some_and(|n| n == b"Sig")
495 {
496 continue;
497 }
498
499 if let Some(apx) = normal_appearance_stream(&annot)
500 .and_then(|o| FormXObject::new(&o, &context.settings.warning_sink))
501 {
502 let Some(rect) = annot.get::<Rect>(RECT) else {
503 continue;
504 };
505
506 let annot_rect = rect.to_kurbo();
507 let transformed_rect = (apx.matrix
517 * kurbo::Rect::new(
518 apx.bbox[0] as f64,
519 apx.bbox[1] as f64,
520 apx.bbox[2] as f64,
521 apx.bbox[3] as f64,
522 )
523 .to_path(0.1))
524 .bounding_box();
525
526 let (tw, th) = (transformed_rect.width(), transformed_rect.height());
531 if !(tw.is_finite() && tw > 0.0 && th.is_finite() && th > 0.0) {
532 continue;
533 }
534
535 let affine = Affine::new([
544 annot_rect.width() / transformed_rect.width(),
545 0.0,
546 0.0,
547 annot_rect.height() / transformed_rect.height(),
548 annot_rect.x0 - transformed_rect.x0,
549 annot_rect.y0 - transformed_rect.y0,
550 ]);
551
552 context.save_state();
556 context.pre_concat_affine(affine);
557 context.push_root_transform();
558
559 draw_form_xobject(resources, &apx, context, device);
560 context.pop_root_transform();
561 context.restore_state(device);
562 } else if annot.get::<Name>(SUBTYPE).as_deref() == Some(WIDGET)
563 && flags & ANNOT_FLAG_NOVIEW == 0
564 && let Some(text) = field_value_text(&annot)
565 && let Some(rect) = annot.get::<Rect>(RECT)
566 {
567 device.draw_placed_text(
583 &text,
584 context.get().ctm.transform_rect_bbox(rect.to_kurbo()),
585 );
586 }
587 }
588 }
589}
590
591pub fn interpret<'a, 'b>(
593 ops: impl Iterator<Item = TypedInstruction<'b>>,
594 resources: &Resources<'a>,
595 context: &mut Context<'a>,
596 device: &mut impl Device<'a>,
597) {
598 if !context.begin_nested_interpretation() {
607 let limit = crate::context::MAX_NESTED_INTERPRETATION_DEPTH;
608 warn!("content stream nesting exceeds {limit}, stopping interpretation");
609 (context.settings.warning_sink)(InterpreterWarning::NestingTooDeep { limit });
612
613 return;
614 }
615
616 let num_states = context.num_states();
617 let max_operator_count = context.settings.max_operator_count.unwrap_or(u64::MAX);
618 let mut operator_count = 0_u64;
619
620 context.save_state();
621
622 for op in ops {
623 operator_count = operator_count.saturating_add(1);
624 if operator_count > max_operator_count {
625 warn!(
626 "content stream operator count exceeds {max_operator_count}, stopping interpretation"
627 );
628 break;
629 }
630
631 match op {
632 TypedInstruction::SaveState(_) => context.save_state(),
633 TypedInstruction::StrokeColorDeviceRgb(s) => {
634 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
635 context.get_mut().graphics_state.stroke_color =
636 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
637 }
638 TypedInstruction::StrokeColorDeviceGray(s) => {
639 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
640 context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
641 }
642 TypedInstruction::StrokeColorCmyk(s) => {
643 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
644 context.get_mut().graphics_state.stroke_color =
645 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
646 }
647 TypedInstruction::LineWidth(w) => {
648 context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32();
649 }
650 TypedInstruction::LineCap(c) => {
651 context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c);
652 }
653 TypedInstruction::LineJoin(j) => {
654 context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j);
655 }
656 TypedInstruction::MiterLimit(l) => {
657 context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32();
658 }
659 TypedInstruction::Transform(t) => {
660 context.pre_concat_transform(t);
661 }
662 TypedInstruction::RectPath(r) => {
663 let rect = kurbo::Rect::new(
664 r.0.as_f64(),
665 r.1.as_f64(),
666 r.0.as_f64() + r.2.as_f64(),
667 r.1.as_f64() + r.3.as_f64(),
668 )
669 .to_path(0.1);
670 context.path_mut().extend(rect);
671 }
672 TypedInstruction::MoveTo(m) => {
673 let p = Point::new(m.0.as_f64(), m.1.as_f64());
674 *(context.last_point_mut()) = p;
675 *(context.sub_path_start_mut()) = p;
676 context.path_mut().move_to(p);
677 }
678 TypedInstruction::FillPathEvenOdd(_) => {
679 fill_path(context, device, FillRule::EvenOdd);
680 }
681 TypedInstruction::FillPathNonZero(_) => {
682 fill_path(context, device, FillRule::NonZero);
683 }
684 TypedInstruction::FillPathNonZeroCompatibility(_) => {
685 fill_path(context, device, FillRule::NonZero);
686 }
687 TypedInstruction::FillAndStrokeEvenOdd(_) => {
688 fill_stroke_path(context, device, FillRule::EvenOdd);
689 }
690 TypedInstruction::FillAndStrokeNonZero(_) => {
691 fill_stroke_path(context, device, FillRule::NonZero);
692 }
693 TypedInstruction::CloseAndStrokePath(_) => {
694 close_path(context);
695 stroke_path(context, device);
696 }
697 TypedInstruction::CloseFillAndStrokeEvenOdd(_) => {
698 close_path(context);
699 fill_stroke_path(context, device, FillRule::EvenOdd);
700 }
701 TypedInstruction::CloseFillAndStrokeNonZero(_) => {
702 close_path(context);
703 fill_stroke_path(context, device, FillRule::NonZero);
704 }
705 TypedInstruction::NonStrokeColorDeviceGray(s) => {
706 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
707 context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
708 }
709 TypedInstruction::NonStrokeColorDeviceRgb(s) => {
710 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
711 context.get_mut().graphics_state.non_stroke_color =
712 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
713 }
714 TypedInstruction::NonStrokeColorCmyk(s) => {
715 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
716 context.get_mut().graphics_state.non_stroke_color =
717 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
718 }
719 TypedInstruction::LineTo(m) => {
720 if !context.path().elements().is_empty() {
721 let last_point = *context.last_point();
722 let mut p = Point::new(m.0.as_f64(), m.1.as_f64());
723 *(context.last_point_mut()) = p;
724 if last_point == p {
725 p.x += 0.0001;
727 }
728
729 context.path_mut().line_to(p);
730 }
731 }
732 TypedInstruction::CubicTo(c) => {
733 if !context.path().elements().is_empty() {
734 let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
735 let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
736 let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
737
738 *(context.last_point_mut()) = p3;
739
740 context.path_mut().curve_to(p1, p2, p3);
741 }
742 }
743 TypedInstruction::CubicStartTo(c) => {
744 if !context.path().elements().is_empty() {
745 let p1 = *context.last_point();
746 let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
747 let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
748
749 *(context.last_point_mut()) = p3;
750
751 context.path_mut().curve_to(p1, p2, p3);
752 }
753 }
754 TypedInstruction::CubicEndTo(c) => {
755 if !context.path().elements().is_empty() {
756 let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
757 let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
758
759 *(context.last_point_mut()) = p3;
760
761 context.path_mut().curve_to(p2, p3, p3);
762 }
763 }
764 TypedInstruction::ClosePath(_) => {
765 close_path(context);
766 }
767 TypedInstruction::SetGraphicsState(gs) => {
768 if let Some(gs) = resources
769 .get_ext_g_state(gs.0.clone())
770 .warn_none(&format!("failed to get extgstate {}", gs.0.as_str()))
771 {
772 handle_gs(&gs, context, resources);
773 }
774 }
775 TypedInstruction::StrokePath(_) => {
776 stroke_path(context, device);
777 }
778 TypedInstruction::EndPath(_) => {
779 if let Some(clip) = *context.clip()
780 && !context.path().elements().is_empty()
781 {
782 let clip_path = context.get().ctm * context.path().clone();
783 context.push_clip_path(clip_path, clip, device);
784
785 *(context.clip_mut()) = None;
786 }
787
788 context.path_mut().truncate(0);
789 }
790 TypedInstruction::NonStrokeColor(c) => {
791 let fill_c = &mut context.get_mut().graphics_state.non_stroke_color;
792 fill_c.truncate(0);
793
794 for e in c.0 {
795 fill_c.push(e.as_f32());
796 }
797 }
798 TypedInstruction::StrokeColor(c) => {
799 let stroke_c = &mut context.get_mut().graphics_state.stroke_color;
800 stroke_c.truncate(0);
801
802 for e in c.0 {
803 stroke_c.push(e.as_f32());
804 }
805 }
806 TypedInstruction::ClipNonZero(_) => {
807 *(context.clip_mut()) = Some(FillRule::NonZero);
808 }
809 TypedInstruction::ClipEvenOdd(_) => {
810 *(context.clip_mut()) = Some(FillRule::EvenOdd);
811 }
812 TypedInstruction::RestoreState(_) => context.restore_state(device),
813 TypedInstruction::FlatnessTolerance(_) => {
814 }
816 TypedInstruction::ColorSpaceStroke(c) => {
817 let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
818 named
819 } else {
820 context
821 .get_color_space(resources, c.0)
822 .unwrap_or(ColorSpace::device_gray())
823 };
824
825 context.get_mut().graphics_state.stroke_color = cs.initial_color();
826 context.get_mut().graphics_state.stroke_cs = cs;
827 }
828 TypedInstruction::ColorSpaceNonStroke(c) => {
829 let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
830 named
831 } else {
832 context
833 .get_color_space(resources, c.0)
834 .unwrap_or(ColorSpace::device_gray())
835 };
836
837 context.get_mut().graphics_state.non_stroke_color = cs.initial_color();
838 context.get_mut().graphics_state.none_stroke_cs = cs;
839 }
840 TypedInstruction::DashPattern(p) => {
841 context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
842 context.get_mut().graphics_state.stroke_props.dash_array =
844 p.0.iter::<f32>()
845 .map(|n| if n == 0.0 { 0.01 } else { n })
846 .collect();
847 }
848 TypedInstruction::RenderingIntent(_) => {
849 }
851 TypedInstruction::NonStrokeColorNamed(n) => {
852 context.get_mut().graphics_state.non_stroke_color =
853 n.0.into_iter().map(|n| n.as_f32()).collect();
854 context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
855 resources
856 .get_pattern(name)
857 .and_then(|d| Pattern::new(d, context, resources))
858 });
859 }
860 TypedInstruction::StrokeColorNamed(n) => {
861 context.get_mut().graphics_state.stroke_color =
862 n.0.into_iter().map(|n| n.as_f32()).collect();
863 context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
864 resources
865 .get_pattern(name)
866 .and_then(|d| Pattern::new(d, context, resources))
867 });
868 }
869 TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
870 let props = bdc
875 .1
876 .clone()
877 .into_name()
878 .and_then(|name| resources.properties.get::<Dict<'_>>(name))
879 .or_else(|| dict_or_stream(&bdc.1).map(|(props, _)| props));
880
881 let mcid = props.as_ref().and_then(|p| p.get::<i32>(MCID));
882
883 let actual_text = props
888 .as_ref()
889 .and_then(|p| p.get::<PdfString>(ACTUAL_TEXT))
890 .and_then(|s| decode_text_string(s.as_bytes()));
891
892 let oc = bdc
893 .1
894 .clone()
895 .into_name()
896 .and_then(|name| {
897 let r = resources.properties.get_ref(name.clone())?;
898 let d = resources
899 .properties
900 .get::<Dict<'_>>(name)
901 .unwrap_or_default();
902 Some((d, r))
903 })
904 .or_else(|| {
905 let (props, _) = dict_or_stream(&bdc.1)?;
906 let r = props.get_ref(OC)?;
907 let d = props.get::<Dict<'_>>(OC).unwrap_or_default();
908 Some((d, r))
909 });
910
911 if let Some((dict, oc_ref)) = oc {
912 context.ocg_state.begin_ocg(&dict, oc_ref.into());
913 } else {
914 context.ocg_state.begin_marked_content();
915 }
916
917 device.begin_marked_content(&bdc.0, mcid, actual_text.as_deref());
918 }
919 TypedInstruction::MarkedContentPointWithProperties(_) => {}
920 TypedInstruction::EndMarkedContent(_) => {
921 context.ocg_state.end_marked_content();
922 device.end_marked_content();
923 }
924 TypedInstruction::MarkedContentPoint(_) => {}
925 TypedInstruction::BeginMarkedContent(bmc) => {
926 context.ocg_state.begin_marked_content();
927 device.begin_marked_content(&bmc.0, None, None);
928 }
929 TypedInstruction::BeginText(_) => {
930 context.get_mut().text_state.text_matrix = Affine::IDENTITY;
931 context.get_mut().text_state.text_line_matrix = Affine::IDENTITY;
932 }
933 TypedInstruction::SetTextMatrix(m) => {
934 let m = Affine::new([
935 m.0.as_f64(),
936 m.1.as_f64(),
937 m.2.as_f64(),
938 m.3.as_f64(),
939 m.4.as_f64(),
940 m.5.as_f64(),
941 ]);
942 context.get_mut().text_state.text_line_matrix = m;
943 context.get_mut().text_state.text_matrix = m;
944 }
945 TypedInstruction::EndText(_) => {
946 let has_outline = context
947 .get()
948 .text_state
949 .clip_paths
950 .segments()
951 .next()
952 .is_some();
953
954 if has_outline {
955 let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone();
956
957 context.push_clip_path(clip_path, FillRule::NonZero, device);
958 }
959
960 context.get_mut().text_state.clip_paths.truncate(0);
961 }
962 TypedInstruction::TextFont(t) => {
963 let name = t.0;
964
965 let font = if let Some(font_dict) = resources.get_font(name.clone()) {
972 context.resolve_font(&font_dict)
973 } else {
974 Font::new_standard(StandardFont::Helvetica, &context.settings.font_resolver)
975 .map(TextStateFont::Fallback)
976 };
977
978 context.get_mut().text_state.font_size = t.1.as_f32();
979 context.get_mut().text_state.font = font;
980 }
981 TypedInstruction::ShowText(s) => {
982 if context.get().text_state.font.is_none() {
983 context.get_mut().text_state.font = Font::new_standard(
986 StandardFont::Helvetica,
987 &context.settings.font_resolver,
988 )
989 .map(TextStateFont::Fallback);
990 }
991
992 text::show_text_string(context, device, resources, s.0);
993 }
994 TypedInstruction::ShowTexts(s) => {
995 if context.get().text_state.font.is_none() {
996 context.get_mut().text_state.font = Font::new_standard(
999 StandardFont::Helvetica,
1000 &context.settings.font_resolver,
1001 )
1002 .map(TextStateFont::Fallback);
1003 }
1004
1005 for obj in s.0.iter::<Object<'_>>() {
1006 if let Some(adjustment) = obj.clone().into_f32() {
1007 device.text_adjustment(adjustment);
1012 context.get_mut().text_state.apply_adjustment(adjustment);
1013 } else if let Some(text) = obj.into_string() {
1014 text::show_text_string(context, device, resources, text);
1015 }
1016 }
1017 }
1018 TypedInstruction::HorizontalScaling(h) => {
1019 context.get_mut().text_state.horizontal_scaling = h.0.as_f32();
1020 }
1021 TypedInstruction::TextLeading(tl) => {
1022 context.get_mut().text_state.leading = tl.0.as_f32();
1023 }
1024 TypedInstruction::CharacterSpacing(c) => {
1025 context.get_mut().text_state.char_space = c.0.as_f32();
1026 }
1027 TypedInstruction::WordSpacing(w) => {
1028 context.get_mut().text_state.word_space = w.0.as_f32();
1029 }
1030 TypedInstruction::NextLine(n) => {
1031 let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
1032 text::next_line(context, tx, ty);
1033 }
1034 TypedInstruction::NextLineUsingLeading(_) => {
1035 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
1036 }
1037 TypedInstruction::NextLineAndShowText(n) => {
1038 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
1039 text::show_text_string(context, device, resources, n.0);
1040 }
1041 TypedInstruction::TextRenderingMode(r) => {
1042 let mode = match r.0.as_i64() {
1043 0 => TextRenderingMode::Fill,
1044 1 => TextRenderingMode::Stroke,
1045 2 => TextRenderingMode::FillStroke,
1046 3 => TextRenderingMode::Invisible,
1047 4 => TextRenderingMode::FillAndClip,
1048 5 => TextRenderingMode::StrokeAndClip,
1049 6 => TextRenderingMode::FillAndStrokeAndClip,
1050 7 => TextRenderingMode::Clip,
1051 _ => {
1052 warn!("unknown text rendering mode {}", r.0.as_i64());
1053
1054 TextRenderingMode::Fill
1055 }
1056 };
1057
1058 context.get_mut().text_state.render_mode = mode;
1059 }
1060 TypedInstruction::NextLineAndSetLeading(n) => {
1061 let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
1062 context.get_mut().text_state.leading = -ty as f32;
1063 text::next_line(context, tx, ty);
1064 }
1065 TypedInstruction::ShapeGlyph(_) => {}
1072 TypedInstruction::XObject(x) => {
1073 let cache = context.object_cache.clone();
1074 let transfer_function = context.get().graphics_state.transfer_function.clone();
1075 if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| {
1076 XObject::new(
1077 &s,
1078 &context.settings.warning_sink,
1079 &cache,
1080 transfer_function.clone(),
1081 )
1082 }) {
1083 draw_xobject(&x_object, resources, context, device);
1084 }
1085 }
1086 TypedInstruction::InlineImage(i) => {
1087 let warning_sink = context.settings.warning_sink.clone();
1088 let transfer_function = context.get().graphics_state.transfer_function.clone();
1089 let cache = context.object_cache.clone();
1090 if let Some(x_object) = ImageXObject::new(
1091 &i.0,
1092 |name| context.get_color_space(resources, name.clone()),
1093 &warning_sink,
1094 &cache,
1095 false,
1096 transfer_function,
1097 ) {
1098 draw_image_xobject(&x_object, context, device);
1099 }
1100 }
1101 TypedInstruction::TextRise(t) => {
1102 context.get_mut().text_state.rise = t.0.as_f32();
1103 }
1104 TypedInstruction::Shading(s) => {
1105 if !context.ocg_state.is_visible() {
1106 continue;
1107 }
1108
1109 let transfer_function = context.get().graphics_state.transfer_function.clone();
1110
1111 if let Some(sp) = resources
1112 .get_shading(s.0)
1113 .and_then(|o| dict_or_stream(&o))
1114 .and_then(|s| {
1115 Shading::new(
1116 &s.0,
1117 s.1.as_ref(),
1118 &context.object_cache,
1119 &context.settings.warning_sink,
1120 )
1121 })
1122 .map(|s| {
1123 Pattern::Shading(ShadingPattern {
1124 shading: Arc::new(s),
1125 matrix: Affine::IDENTITY,
1126 opacity: context.get().graphics_state.non_stroke_alpha,
1127 transfer_function: transfer_function.clone(),
1128 })
1129 })
1130 {
1131 context.save_state();
1132 context.push_root_transform();
1133 let st = context.get_mut();
1134 st.graphics_state.non_stroke_pattern = Some(sp);
1135 st.graphics_state.none_stroke_cs = ColorSpace::pattern();
1136
1137 device.set_soft_mask(st.graphics_state.soft_mask.clone());
1138 device.set_blend_mode(st.graphics_state.blend_mode);
1139
1140 let bbox = context.bbox().to_path(0.1);
1141 let inverted_bbox = context.get().ctm.inverse() * bbox;
1142 fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox));
1143
1144 context.pop_root_transform();
1145 context.restore_state(device);
1146 } else {
1147 warn!("failed to process shading");
1148 }
1149 }
1150 TypedInstruction::BeginCompatibility(_) => {}
1151 TypedInstruction::EndCompatibility(_) => {}
1152 TypedInstruction::ColorGlyph(_) => {}
1155 TypedInstruction::ShowTextWithParameters(t) => {
1156 context.get_mut().text_state.word_space = t.0.as_f32();
1157 context.get_mut().text_state.char_space = t.1.as_f32();
1158 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
1159 text::show_text_string(context, device, resources, t.2);
1160 }
1161 _ => {
1162 warn!("failed to read an operator");
1163 }
1164 }
1165 }
1166
1167 while context.num_states() > num_states {
1168 context.restore_state(device);
1169 }
1170
1171 context.end_nested_interpretation();
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use crate::device::Device;
1181 use crate::font::Glyph;
1182 use crate::soft_mask::SoftMask;
1183 use crate::util::PageExt;
1184 use crate::{
1185 BlendMode, ClipPath, Context, GlyphDrawMode, Image, InterpreterSettings, Paint,
1186 PathDrawMode, interpret_page,
1187 };
1188 use kurbo::{Affine, BezPath, Shape};
1189 use pdf_syntax::Pdf;
1190
1191 #[derive(Default)]
1195 struct CountingDevice {
1196 path_widths: Vec<f64>,
1197 }
1198
1199 impl Device<'_> for CountingDevice {
1200 fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
1201 fn set_blend_mode(&mut self, _: BlendMode) {}
1202 fn draw_path(&mut self, path: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {
1203 self.path_widths.push(path.bounding_box().width());
1204 }
1205 fn push_clip_path(&mut self, _: &ClipPath) {}
1206 fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
1207 fn draw_glyph(
1208 &mut self,
1209 _: &Glyph<'_>,
1210 _: Affine,
1211 _: Affine,
1212 _: &Paint<'_>,
1213 _: &GlyphDrawMode,
1214 ) {
1215 }
1216 fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {}
1217 fn pop_clip_path(&mut self) {}
1218 fn pop_transparency_group(&mut self) {}
1219 }
1220
1221 fn build_pdf(objects: &[Vec<u8>]) -> Vec<u8> {
1224 let mut out = b"%PDF-1.7\n".to_vec();
1225 let mut offsets = Vec::with_capacity(objects.len());
1226
1227 for (i, body) in objects.iter().enumerate() {
1228 offsets.push(out.len());
1229 out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
1230 out.extend_from_slice(body);
1231 out.extend_from_slice(b"\nendobj\n");
1232 }
1233
1234 let xref_pos = out.len();
1235 out.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
1236 out.extend_from_slice(b"0000000000 65535 f \n");
1237 for offset in offsets {
1238 out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
1239 }
1240 out.extend_from_slice(
1241 format!(
1242 "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",
1243 objects.len() + 1
1244 )
1245 .as_bytes(),
1246 );
1247
1248 out
1249 }
1250
1251 fn form_stream(bbox: &str, content: &str) -> Vec<u8> {
1253 format!(
1254 "<< /Type /XObject /Subtype /Form /BBox {bbox} /Length {} >>\nstream\n{content}\nendstream",
1255 content.len()
1256 )
1257 .into_bytes()
1258 }
1259
1260 fn checkbox_pdf(annot_body: &[u8], on_bbox: &str, extra_objects: &[Vec<u8>]) -> Vec<u8> {
1268 let mut objects = vec![
1269 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
1270 b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
1271 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
1272 /Annots [4 0 R] /Contents 7 0 R >>"
1273 .to_vec(),
1274 annot_body.to_vec(),
1275 form_stream(on_bbox, "0 0 10 10 re f\n12 12 4 4 re f"),
1276 form_stream("[0 0 20 20]", "0 0 7 7 re f"),
1277 b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
1278 ];
1279 objects.extend_from_slice(extra_objects);
1280 build_pdf(&objects)
1281 }
1282
1283 fn interpret_widths(pdf_bytes: Vec<u8>) -> Vec<f64> {
1286 let pdf = Pdf::new(pdf_bytes).expect("test PDF must parse");
1287 let pages = pdf.pages();
1288 let page = pages.first().expect("test PDF must have one page");
1289
1290 let settings = InterpreterSettings::default();
1291 let initial_transform = page.initial_transform(true);
1292 let bbox = kurbo::Rect::new(0.0, 0.0, 100.0, 100.0);
1293 let mut context = Context::new(initial_transform, bbox, page.xref(), settings);
1294 let mut device = CountingDevice::default();
1295
1296 interpret_page(page, &mut context, &mut device);
1297 device.path_widths
1298 }
1299
1300 fn assert_widths(widths: &[f64], expected: &[f64]) {
1301 assert_eq!(
1302 widths.len(),
1303 expected.len(),
1304 "expected {expected:?}, got {widths:?}"
1305 );
1306 for (got, want) in widths.iter().zip(expected) {
1307 assert!(
1308 (got - want).abs() < 1e-6,
1309 "expected {expected:?}, got {widths:?}"
1310 );
1311 }
1312 }
1313
1314 #[test]
1317 fn widget_substate_as_on_state() {
1318 let pdf = checkbox_pdf(
1319 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1320 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Yes >>",
1321 "[0 0 20 20]",
1322 &[],
1323 );
1324 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1325 }
1326
1327 #[test]
1330 fn widget_substate_as_off_state() {
1331 let pdf = checkbox_pdf(
1332 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1333 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Off >>",
1334 "[0 0 20 20]",
1335 &[],
1336 );
1337 assert_widths(&interpret_widths(pdf), &[7.0]);
1338 }
1339
1340 #[test]
1343 fn widget_substate_as_off_without_off_entry() {
1344 let pdf = checkbox_pdf(
1345 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1346 /AP << /N << /Yes 5 0 R >> >> /AS /Off >>",
1347 "[0 0 20 20]",
1348 &[],
1349 );
1350 assert_widths(&interpret_widths(pdf), &[]);
1351 }
1352
1353 #[test]
1356 fn widget_substate_v_fallback() {
1357 let pdf = checkbox_pdf(
1358 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1359 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /V /Yes >>",
1360 "[0 0 20 20]",
1361 &[],
1362 );
1363 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1364 }
1365
1366 #[test]
1370 fn widget_substate_parent_v_fallback() {
1371 let pdf = checkbox_pdf(
1372 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1373 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /Parent 8 0 R >>",
1374 "[0 0 20 20]",
1375 &[b"<< /FT /Btn /V /Yes >>".to_vec()],
1376 );
1377 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1378 }
1379
1380 #[test]
1384 fn widget_substate_non_ascii_state_name() {
1385 let annot = b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1386 /AP << /N << /Stra\xf6m 5 0 R /Off 6 0 R >> >> /AS /Stra#F6m >>";
1387 assert!(annot.contains(&0xf6));
1389 let pdf = checkbox_pdf(annot, "[0 0 20 20]", &[]);
1390 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1391 }
1392
1393 #[test]
1396 fn widget_degenerate_bbox_skipped() {
1397 let pdf = checkbox_pdf(
1398 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1399 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Yes >>",
1400 "[0 0 0 20]",
1401 &[],
1402 );
1403 assert_widths(&interpret_widths(pdf), &[]);
1404 }
1405
1406 #[test]
1408 fn widget_direct_stream_still_renders() {
1409 let pdf = checkbox_pdf(
1410 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1411 /AP << /N 6 0 R >> >>",
1412 "[0 0 20 20]",
1413 &[],
1414 );
1415 assert_widths(&interpret_widths(pdf), &[7.0]);
1416 }
1417}