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 UnresolvedAnnotationAppearance,
238}
239
240fn normal_appearance_stream<'a>(annot: &Dict<'a>) -> Option<Stream<'a>> {
262 let ap = annot.get::<Dict<'_>>(AP)?;
263
264 if let Some(stream) = ap.get::<Stream<'_>>(N) {
266 return Some(stream);
267 }
268
269 let states = ap.get::<Dict<'_>>(N)?;
271
272 if let Some(state) = annot.get::<Name>(AS) {
273 return states.get::<Stream<'_>>(state.as_ref());
276 }
277
278 let candidates = [
281 annot.get::<Name>(V),
282 annot.get::<Dict<'_>>(PARENT).and_then(|p| p.get::<Name>(V)),
283 ];
284
285 candidates
286 .into_iter()
287 .flatten()
288 .find_map(|state| states.get::<Stream<'_>>(state.as_ref()))
289}
290
291const ANNOT_FLAG_HIDDEN: u32 = 1 << 1;
293const ANNOT_FLAG_NOVIEW: u32 = 1 << 5;
295
296const FIELD_INHERITANCE_DEPTH: usize = 32;
302
303const FIELD_FLAG_PUSHBUTTON: u32 = 1 << 16;
305const FIELD_FLAG_PASSWORD: u32 = 1 << 13;
307
308fn inherited_field_entry<'a, T>(
311 annot: &Dict<'a>,
312 entry: impl Fn(&Dict<'a>) -> Option<T>,
313) -> Option<T> {
314 let mut node = annot.clone();
315
316 for _ in 0..FIELD_INHERITANCE_DEPTH {
317 if let Some(value) = entry(&node) {
318 return Some(value);
319 }
320
321 node = node.get::<Dict<'a>>(PARENT)?;
322 }
323
324 None
325}
326
327fn name_as_text(name: &Name) -> Option<String> {
334 std::str::from_utf8(name.as_ref()).ok().map(str::to_string)
335}
336
337fn choice_option_label(annot: &Dict<'_>, value: &str) -> String {
347 let Some(options) = inherited_field_entry(annot, |d| d.get::<Array<'_>>(OPT)) else {
348 return value.to_string();
349 };
350
351 for option in options.iter::<Array<'_>>() {
352 let mut pair = option.iter::<PdfString>();
353 let Some(export) = pair.next().and_then(|s| decode_text_string(s.as_bytes())) else {
354 continue;
355 };
356 let Some(display) = pair.next().and_then(|s| decode_text_string(s.as_bytes())) else {
357 continue;
358 };
359
360 if export == value {
361 return display;
362 }
363 }
364
365 value.to_string()
366}
367
368fn choice_value_text<'a>(annot: &Dict<'a>, key: &'static [u8]) -> Option<String> {
370 if let Some(value) = inherited_field_entry(annot, |d| d.get::<PdfString>(key)) {
371 let value = decode_text_string(value.as_bytes())?;
372 return Some(choice_option_label(annot, &value));
373 }
374
375 let options = inherited_field_entry(annot, |d| d.get::<Array<'a>>(key))?;
379 let lines: Vec<String> = options
380 .iter::<PdfString>()
381 .filter_map(|option| decode_text_string(option.as_bytes()))
382 .map(|value| choice_option_label(annot, &value))
383 .collect();
384
385 (!lines.is_empty()).then(|| lines.join("\n"))
386}
387
388fn is_the_only_widget_of_its_field(annot: &Dict<'_>) -> bool {
397 let Some(parent) = annot.get::<Dict<'_>>(PARENT) else {
398 return true;
399 };
400
401 match parent.get::<Array<'_>>(KIDS) {
402 Some(kids) => kids.raw_iter().take(2).count() < 2,
403 None => true,
404 }
405}
406
407fn field_value_text(annot: &Dict<'_>) -> Option<String> {
419 let field_type = inherited_field_entry(annot, |d| d.get::<Name>(FT))?;
420 let flags = inherited_field_entry(annot, |d| d.get::<u32>(FF)).unwrap_or(0);
421
422 match field_type.as_ref() {
423 BTN => {
424 if flags & FIELD_FLAG_PUSHBUTTON != 0 {
425 return None;
426 }
427
428 let state = annot.get::<Name>(AS).or_else(|| {
433 is_the_only_widget_of_its_field(annot)
434 .then(|| {
435 inherited_field_entry(annot, |d| d.get::<Name>(V))
436 .or_else(|| inherited_field_entry(annot, |d| d.get::<Name>(DV)))
437 })
438 .flatten()
439 })?;
440
441 if state.as_ref() == b"Off" {
445 return None;
446 }
447
448 name_as_text(&state)
449 }
450 TX => {
451 if flags & FIELD_FLAG_PASSWORD != 0 {
452 return None;
453 }
454
455 let value = inherited_field_entry(annot, |d| d.get::<PdfString>(V))
459 .or_else(|| inherited_field_entry(annot, |d| d.get::<PdfString>(DV)))?;
460 let text = decode_text_string(value.as_bytes())?;
461
462 (!text.trim().is_empty()).then_some(text)
463 }
464 CH => choice_value_text(annot, V).or_else(|| choice_value_text(annot, DV)),
465 _ => None,
466 }
467}
468
469pub fn interpret_page<'a>(
471 page: &Page<'a>,
472 context: &mut Context<'a>,
473 device: &mut impl Device<'a>,
474) {
475 let resources = page.resources();
476 interpret(page.typed_operations(), resources, context, device);
477
478 if context.settings.render_annotations
479 && let Some(annot_arr) = page.raw().get::<Array<'_>>(ANNOTS)
480 {
481 for annot in annot_arr.iter::<Dict<'_>>() {
482 let flags = annot.get::<u32>(F).unwrap_or(0);
483
484 if flags & ANNOT_FLAG_HIDDEN != 0 {
486 continue;
487 }
488
489 if context.settings.skip_signature_widgets
494 && annot
495 .get::<Name>(FT)
496 .as_deref()
497 .is_some_and(|n| n == b"Sig")
498 {
499 continue;
500 }
501
502 if let Some(apx) = normal_appearance_stream(&annot)
503 .and_then(|o| FormXObject::new(&o, &context.settings.warning_sink))
504 {
505 let Some(rect) = annot.get::<Rect>(RECT) else {
506 continue;
507 };
508
509 let annot_rect = rect.to_kurbo();
510 let transformed_rect = (apx.matrix
520 * kurbo::Rect::new(
521 apx.bbox[0] as f64,
522 apx.bbox[1] as f64,
523 apx.bbox[2] as f64,
524 apx.bbox[3] as f64,
525 )
526 .to_path(0.1))
527 .bounding_box();
528
529 let (tw, th) = (transformed_rect.width(), transformed_rect.height());
534 if !(tw.is_finite() && tw > 0.0 && th.is_finite() && th > 0.0) {
535 continue;
536 }
537
538 let affine = Affine::new([
547 annot_rect.width() / transformed_rect.width(),
548 0.0,
549 0.0,
550 annot_rect.height() / transformed_rect.height(),
551 annot_rect.x0 - transformed_rect.x0,
552 annot_rect.y0 - transformed_rect.y0,
553 ]);
554
555 context.save_state();
559 context.pre_concat_affine(affine);
560 context.push_root_transform();
561
562 device
568 .begin_annotation_appearance(context.get().ctm.transform_rect_bbox(annot_rect));
569 draw_form_xobject(resources, &apx, context, device);
570 device.end_annotation_appearance();
571 context.pop_root_transform();
572 context.restore_state(device);
573 } else if annot.get::<Name>(SUBTYPE).as_deref() == Some(WIDGET)
574 && flags & ANNOT_FLAG_NOVIEW == 0
575 && let Some(text) = field_value_text(&annot)
576 && let Some(rect) = annot.get::<Rect>(RECT)
577 {
578 device.draw_placed_text(
594 &text,
595 context.get().ctm.transform_rect_bbox(rect.to_kurbo()),
596 );
597 } else if annot.get::<Dict<'_>>(AP).is_some() {
598 (context.settings.warning_sink)(InterpreterWarning::UnresolvedAnnotationAppearance);
602 }
603 }
604 }
605}
606
607pub fn interpret<'a, 'b>(
609 ops: impl Iterator<Item = TypedInstruction<'b>>,
610 resources: &Resources<'a>,
611 context: &mut Context<'a>,
612 device: &mut impl Device<'a>,
613) {
614 if !context.begin_nested_interpretation() {
623 let limit = crate::context::MAX_NESTED_INTERPRETATION_DEPTH;
624 warn!("content stream nesting exceeds {limit}, stopping interpretation");
625 (context.settings.warning_sink)(InterpreterWarning::NestingTooDeep { limit });
628
629 return;
630 }
631
632 let num_states = context.num_states();
633 let max_operator_count = context.settings.max_operator_count.unwrap_or(u64::MAX);
634 let mut operator_count = 0_u64;
635
636 context.save_state();
637
638 for op in ops {
639 operator_count = operator_count.saturating_add(1);
640 if operator_count > max_operator_count {
641 warn!(
642 "content stream operator count exceeds {max_operator_count}, stopping interpretation"
643 );
644 break;
645 }
646
647 match op {
648 TypedInstruction::SaveState(_) => context.save_state(),
649 TypedInstruction::StrokeColorDeviceRgb(s) => {
650 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
651 context.get_mut().graphics_state.stroke_color =
652 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
653 }
654 TypedInstruction::StrokeColorDeviceGray(s) => {
655 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
656 context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
657 }
658 TypedInstruction::StrokeColorCmyk(s) => {
659 context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
660 context.get_mut().graphics_state.stroke_color =
661 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
662 }
663 TypedInstruction::LineWidth(w) => {
664 context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32();
665 }
666 TypedInstruction::LineCap(c) => {
667 context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c);
668 }
669 TypedInstruction::LineJoin(j) => {
670 context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j);
671 }
672 TypedInstruction::MiterLimit(l) => {
673 context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32();
674 }
675 TypedInstruction::Transform(t) => {
676 context.pre_concat_transform(t);
677 }
678 TypedInstruction::RectPath(r) => {
679 let rect = kurbo::Rect::new(
680 r.0.as_f64(),
681 r.1.as_f64(),
682 r.0.as_f64() + r.2.as_f64(),
683 r.1.as_f64() + r.3.as_f64(),
684 )
685 .to_path(0.1);
686 context.path_mut().extend(rect);
687 }
688 TypedInstruction::MoveTo(m) => {
689 let p = Point::new(m.0.as_f64(), m.1.as_f64());
690 *(context.last_point_mut()) = p;
691 *(context.sub_path_start_mut()) = p;
692 context.path_mut().move_to(p);
693 }
694 TypedInstruction::FillPathEvenOdd(_) => {
695 fill_path(context, device, FillRule::EvenOdd);
696 }
697 TypedInstruction::FillPathNonZero(_) => {
698 fill_path(context, device, FillRule::NonZero);
699 }
700 TypedInstruction::FillPathNonZeroCompatibility(_) => {
701 fill_path(context, device, FillRule::NonZero);
702 }
703 TypedInstruction::FillAndStrokeEvenOdd(_) => {
704 fill_stroke_path(context, device, FillRule::EvenOdd);
705 }
706 TypedInstruction::FillAndStrokeNonZero(_) => {
707 fill_stroke_path(context, device, FillRule::NonZero);
708 }
709 TypedInstruction::CloseAndStrokePath(_) => {
710 close_path(context);
711 stroke_path(context, device);
712 }
713 TypedInstruction::CloseFillAndStrokeEvenOdd(_) => {
714 close_path(context);
715 fill_stroke_path(context, device, FillRule::EvenOdd);
716 }
717 TypedInstruction::CloseFillAndStrokeNonZero(_) => {
718 close_path(context);
719 fill_stroke_path(context, device, FillRule::NonZero);
720 }
721 TypedInstruction::NonStrokeColorDeviceGray(s) => {
722 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
723 context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
724 }
725 TypedInstruction::NonStrokeColorDeviceRgb(s) => {
726 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
727 context.get_mut().graphics_state.non_stroke_color =
728 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
729 }
730 TypedInstruction::NonStrokeColorCmyk(s) => {
731 context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
732 context.get_mut().graphics_state.non_stroke_color =
733 smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
734 }
735 TypedInstruction::LineTo(m) => {
736 if !context.path().elements().is_empty() {
737 let last_point = *context.last_point();
738 let mut p = Point::new(m.0.as_f64(), m.1.as_f64());
739 *(context.last_point_mut()) = p;
740 if last_point == p {
741 p.x += 0.0001;
743 }
744
745 context.path_mut().line_to(p);
746 }
747 }
748 TypedInstruction::CubicTo(c) => {
749 if !context.path().elements().is_empty() {
750 let p1 = Point::new(c.0.as_f64(), c.1.as_f64());
751 let p2 = Point::new(c.2.as_f64(), c.3.as_f64());
752 let p3 = Point::new(c.4.as_f64(), c.5.as_f64());
753
754 *(context.last_point_mut()) = p3;
755
756 context.path_mut().curve_to(p1, p2, p3);
757 }
758 }
759 TypedInstruction::CubicStartTo(c) => {
760 if !context.path().elements().is_empty() {
761 let p1 = *context.last_point();
762 let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
763 let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
764
765 *(context.last_point_mut()) = p3;
766
767 context.path_mut().curve_to(p1, p2, p3);
768 }
769 }
770 TypedInstruction::CubicEndTo(c) => {
771 if !context.path().elements().is_empty() {
772 let p2 = Point::new(c.0.as_f64(), c.1.as_f64());
773 let p3 = Point::new(c.2.as_f64(), c.3.as_f64());
774
775 *(context.last_point_mut()) = p3;
776
777 context.path_mut().curve_to(p2, p3, p3);
778 }
779 }
780 TypedInstruction::ClosePath(_) => {
781 close_path(context);
782 }
783 TypedInstruction::SetGraphicsState(gs) => {
784 if let Some(gs) = resources
785 .get_ext_g_state(gs.0.clone())
786 .warn_none(&format!("failed to get extgstate {}", gs.0.as_str()))
787 {
788 handle_gs(&gs, context, resources);
789 }
790 }
791 TypedInstruction::StrokePath(_) => {
792 stroke_path(context, device);
793 }
794 TypedInstruction::EndPath(_) => {
795 if let Some(clip) = *context.clip()
796 && !context.path().elements().is_empty()
797 {
798 let clip_path = context.get().ctm * context.path().clone();
799 context.push_clip_path(clip_path, clip, device);
800
801 *(context.clip_mut()) = None;
802 }
803
804 context.path_mut().truncate(0);
805 }
806 TypedInstruction::NonStrokeColor(c) => {
807 let fill_c = &mut context.get_mut().graphics_state.non_stroke_color;
808 fill_c.truncate(0);
809
810 for e in c.0 {
811 fill_c.push(e.as_f32());
812 }
813 }
814 TypedInstruction::StrokeColor(c) => {
815 let stroke_c = &mut context.get_mut().graphics_state.stroke_color;
816 stroke_c.truncate(0);
817
818 for e in c.0 {
819 stroke_c.push(e.as_f32());
820 }
821 }
822 TypedInstruction::ClipNonZero(_) => {
823 *(context.clip_mut()) = Some(FillRule::NonZero);
824 }
825 TypedInstruction::ClipEvenOdd(_) => {
826 *(context.clip_mut()) = Some(FillRule::EvenOdd);
827 }
828 TypedInstruction::RestoreState(_) => context.restore_state(device),
829 TypedInstruction::FlatnessTolerance(_) => {
830 }
832 TypedInstruction::ColorSpaceStroke(c) => {
833 let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
834 named
835 } else {
836 context
837 .get_color_space(resources, c.0)
838 .unwrap_or(ColorSpace::device_gray())
839 };
840
841 context.get_mut().graphics_state.stroke_color = cs.initial_color();
842 context.get_mut().graphics_state.stroke_cs = cs;
843 }
844 TypedInstruction::ColorSpaceNonStroke(c) => {
845 let cs = if let Some(named) = ColorSpace::new_from_name(c.0.clone()) {
846 named
847 } else {
848 context
849 .get_color_space(resources, c.0)
850 .unwrap_or(ColorSpace::device_gray())
851 };
852
853 context.get_mut().graphics_state.non_stroke_color = cs.initial_color();
854 context.get_mut().graphics_state.none_stroke_cs = cs;
855 }
856 TypedInstruction::DashPattern(p) => {
857 context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32();
858 context.get_mut().graphics_state.stroke_props.dash_array =
860 p.0.iter::<f32>()
861 .map(|n| if n == 0.0 { 0.01 } else { n })
862 .collect();
863 }
864 TypedInstruction::RenderingIntent(_) => {
865 }
867 TypedInstruction::NonStrokeColorNamed(n) => {
868 context.get_mut().graphics_state.non_stroke_color =
869 n.0.into_iter().map(|n| n.as_f32()).collect();
870 context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
871 resources
872 .get_pattern(name)
873 .and_then(|d| Pattern::new(d, context))
874 });
875 }
876 TypedInstruction::StrokeColorNamed(n) => {
877 context.get_mut().graphics_state.stroke_color =
878 n.0.into_iter().map(|n| n.as_f32()).collect();
879 context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
880 resources
881 .get_pattern(name)
882 .and_then(|d| Pattern::new(d, context))
883 });
884 }
885 TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
886 let oc = bdc
891 .1
892 .clone()
893 .into_name()
894 .and_then(|name| {
895 let r = resources.properties.get_ref(name.clone())?;
896 let d = resources
897 .properties
898 .get::<Dict<'_>>(name)
899 .unwrap_or_default();
900 Some((d, r))
901 })
902 .or_else(|| {
903 let (props, _) = dict_or_stream(&bdc.1)?;
904 let r = props.get_ref(OC)?;
905 let d = props.get::<Dict<'_>>(OC).unwrap_or_default();
906 Some((d, r))
907 });
908
909 if let Some((dict, oc_ref)) = oc {
910 context.ocg_state.begin_ocg(&dict, oc_ref.into());
911 } else {
912 context.ocg_state.begin_marked_content();
913 }
914 let props = bdc
915 .1
916 .clone()
917 .into_name()
918 .and_then(|name| resources.properties.get::<Dict<'_>>(name))
919 .or_else(|| dict_or_stream(&bdc.1).map(|(props, _)| props));
920 let mcid = props.as_ref().and_then(|p| p.get::<i32>(MCID));
921 let actual_text = props
922 .as_ref()
923 .and_then(|p| p.get::<PdfString>(ACTUAL_TEXT))
924 .and_then(|s| decode_text_string(s.as_bytes()));
925 device.begin_marked_content(&bdc.0, mcid, actual_text.as_deref());
926 }
927 TypedInstruction::MarkedContentPointWithProperties(_) => {}
928 TypedInstruction::EndMarkedContent(_) => {
929 context.ocg_state.end_marked_content();
930 device.end_marked_content();
931 }
932 TypedInstruction::MarkedContentPoint(_) => {}
933 TypedInstruction::BeginMarkedContent(bmc) => {
934 context.ocg_state.begin_marked_content();
935 device.begin_marked_content(&bmc.0, None, None);
936 }
937 TypedInstruction::BeginText(_) => {
938 context.get_mut().text_state.text_matrix = Affine::IDENTITY;
939 context.get_mut().text_state.text_line_matrix = Affine::IDENTITY;
940 }
941 TypedInstruction::SetTextMatrix(m) => {
942 let m = Affine::new([
943 m.0.as_f64(),
944 m.1.as_f64(),
945 m.2.as_f64(),
946 m.3.as_f64(),
947 m.4.as_f64(),
948 m.5.as_f64(),
949 ]);
950 context.get_mut().text_state.text_line_matrix = m;
951 context.get_mut().text_state.text_matrix = m;
952 }
953 TypedInstruction::EndText(_) => {
954 let has_outline = context
955 .get()
956 .text_state
957 .clip_paths
958 .segments()
959 .next()
960 .is_some();
961
962 if has_outline {
963 let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone();
964
965 context.push_clip_path(clip_path, FillRule::NonZero, device);
966 }
967
968 context.get_mut().text_state.clip_paths.truncate(0);
969 }
970 TypedInstruction::TextFont(t) => {
971 let name = t.0;
972
973 let mut font = if let Some(font_dict) = resources.get_font(name.clone()) {
980 context.resolve_font(&font_dict)
981 } else {
982 Font::new_standard(
983 StandardFont::Helvetica,
984 &context.settings.font_resolver,
985 &context.object_cache,
986 )
987 .map(TextStateFont::Fallback)
988 };
989 if let Some(TextStateFont::Font(resolved)) = font.as_mut() {
994 resolved.set_resource_name(name.as_str());
995 }
996
997 context.get_mut().text_state.font_size = t.1.as_f32();
998 context.get_mut().text_state.font = font;
999 }
1000 TypedInstruction::ShowText(s) => {
1001 if context.get().text_state.font.is_none() {
1002 context.get_mut().text_state.font = Font::new_standard(
1005 StandardFont::Helvetica,
1006 &context.settings.font_resolver,
1007 &context.object_cache,
1008 )
1009 .map(TextStateFont::Fallback);
1010 }
1011
1012 text::show_text_string(context, device, resources, s.0);
1013 }
1014 TypedInstruction::ShowTexts(s) => {
1015 if context.get().text_state.font.is_none() {
1016 context.get_mut().text_state.font = Font::new_standard(
1019 StandardFont::Helvetica,
1020 &context.settings.font_resolver,
1021 &context.object_cache,
1022 )
1023 .map(TextStateFont::Fallback);
1024 }
1025
1026 for obj in s.0.iter::<Object<'_>>() {
1027 if let Some(adjustment) = obj.clone().into_f32() {
1028 device.text_adjustment(adjustment);
1033 context.get_mut().text_state.apply_adjustment(adjustment);
1034 } else if let Some(text) = obj.into_string() {
1035 text::show_text_string(context, device, resources, text);
1036 }
1037 }
1038 }
1039 TypedInstruction::HorizontalScaling(h) => {
1040 context.get_mut().text_state.horizontal_scaling = h.0.as_f32();
1041 }
1042 TypedInstruction::TextLeading(tl) => {
1043 context.get_mut().text_state.leading = tl.0.as_f32();
1044 }
1045 TypedInstruction::CharacterSpacing(c) => {
1046 context.get_mut().text_state.char_space = c.0.as_f32();
1047 }
1048 TypedInstruction::WordSpacing(w) => {
1049 context.get_mut().text_state.word_space = w.0.as_f32();
1050 }
1051 TypedInstruction::NextLine(n) => {
1052 let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
1053 text::next_line(context, tx, ty);
1054 }
1055 TypedInstruction::NextLineUsingLeading(_) => {
1056 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
1057 }
1058 TypedInstruction::NextLineAndShowText(n) => {
1059 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
1060 text::show_text_string(context, device, resources, n.0);
1061 }
1062 TypedInstruction::TextRenderingMode(r) => {
1063 let mode = match r.0.as_i64() {
1064 0 => TextRenderingMode::Fill,
1065 1 => TextRenderingMode::Stroke,
1066 2 => TextRenderingMode::FillStroke,
1067 3 => TextRenderingMode::Invisible,
1068 4 => TextRenderingMode::FillAndClip,
1069 5 => TextRenderingMode::StrokeAndClip,
1070 6 => TextRenderingMode::FillAndStrokeAndClip,
1071 7 => TextRenderingMode::Clip,
1072 _ => {
1073 warn!("unknown text rendering mode {}", r.0.as_i64());
1074
1075 TextRenderingMode::Fill
1076 }
1077 };
1078
1079 context.get_mut().text_state.render_mode = mode;
1080 }
1081 TypedInstruction::NextLineAndSetLeading(n) => {
1082 let (tx, ty) = (n.0.as_f64(), n.1.as_f64());
1083 context.get_mut().text_state.leading = -ty as f32;
1084 text::next_line(context, tx, ty);
1085 }
1086 TypedInstruction::ShapeGlyph(_) => {}
1093 TypedInstruction::XObject(x) => {
1094 let cache = context.object_cache.clone();
1095 let warning_sink = context.settings.warning_sink.clone();
1096 let transfer_function = context.get().graphics_state.transfer_function.clone();
1097 if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| {
1098 XObject::new(
1099 &s,
1100 |name| context.get_color_space(resources, name.clone()),
1101 &warning_sink,
1102 &cache,
1103 transfer_function.clone(),
1104 )
1105 }) {
1106 draw_xobject(&x_object, resources, context, device);
1107 }
1108 }
1109 TypedInstruction::InlineImage(i) => {
1110 let warning_sink = context.settings.warning_sink.clone();
1111 let transfer_function = context.get().graphics_state.transfer_function.clone();
1112 let cache = context.object_cache.clone();
1113 if let Some(x_object) = ImageXObject::new(
1114 &i.0,
1115 |name| context.get_color_space(resources, name.clone()),
1116 &warning_sink,
1117 &cache,
1118 false,
1119 transfer_function,
1120 ) {
1121 draw_image_xobject(&x_object, context, device);
1122 }
1123 }
1124 TypedInstruction::TextRise(t) => {
1125 context.get_mut().text_state.rise = t.0.as_f32();
1126 }
1127 TypedInstruction::Shading(s) => {
1128 if !context.ocg_state.is_visible() {
1129 continue;
1130 }
1131
1132 let transfer_function = context.get().graphics_state.transfer_function.clone();
1133
1134 if let Some(sp) = resources
1135 .get_shading(s.0)
1136 .and_then(|o| dict_or_stream(&o))
1137 .and_then(|s| {
1138 Shading::new(
1139 &s.0,
1140 s.1.as_ref(),
1141 &context.object_cache,
1142 &context.settings.warning_sink,
1143 )
1144 })
1145 .map(|s| {
1146 Pattern::Shading(ShadingPattern {
1147 shading: Arc::new(s),
1148 matrix: Affine::IDENTITY,
1149 opacity: context.get().graphics_state.non_stroke_alpha,
1150 transfer_function: transfer_function.clone(),
1151 })
1152 })
1153 {
1154 context.save_state();
1155 context.push_root_transform();
1156 let st = context.get_mut();
1157 st.graphics_state.non_stroke_pattern = Some(sp);
1158 st.graphics_state.none_stroke_cs = ColorSpace::pattern();
1159
1160 device.set_soft_mask(st.graphics_state.soft_mask.clone());
1161 device.set_blend_mode(st.graphics_state.blend_mode);
1162
1163 let bbox = context.bbox().to_path(0.1);
1164 let inverted_bbox = context.get().ctm.inverse() * bbox;
1165 fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox));
1166
1167 context.pop_root_transform();
1168 context.restore_state(device);
1169 } else {
1170 warn!("failed to process shading");
1171 }
1172 }
1173 TypedInstruction::BeginCompatibility(_) => {}
1174 TypedInstruction::EndCompatibility(_) => {}
1175 TypedInstruction::ColorGlyph(_) => {}
1178 TypedInstruction::ShowTextWithParameters(t) => {
1179 context.get_mut().text_state.word_space = t.0.as_f32();
1180 context.get_mut().text_state.char_space = t.1.as_f32();
1181 text::next_line(context, 0.0, -context.get().text_state.leading as f64);
1182 text::show_text_string(context, device, resources, t.2);
1183 }
1184 _ => {
1185 warn!("failed to read an operator");
1186 }
1187 }
1188 }
1189
1190 while context.num_states() > num_states {
1191 context.restore_state(device);
1192 }
1193
1194 context.end_nested_interpretation();
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use crate::device::Device;
1204 use crate::font::Glyph;
1205 use crate::soft_mask::SoftMask;
1206 use crate::util::PageExt;
1207 use crate::{
1208 BlendMode, ClipPath, Context, GlyphDrawMode, Image, InterpreterSettings,
1209 InterpreterWarning, Paint, PathDrawMode, interpret_page,
1210 };
1211 use kurbo::{Affine, BezPath, Shape};
1212 use pdf_syntax::Pdf;
1213
1214 #[derive(Default)]
1218 struct CountingDevice {
1219 path_widths: Vec<f64>,
1220 }
1221
1222 impl Device<'_> for CountingDevice {
1223 fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
1224 fn set_blend_mode(&mut self, _: BlendMode) {}
1225 fn draw_path(&mut self, path: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {
1226 self.path_widths.push(path.bounding_box().width());
1227 }
1228 fn push_clip_path(&mut self, _: &ClipPath) {}
1229 fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
1230 fn draw_glyph(
1231 &mut self,
1232 _: &Glyph<'_>,
1233 _: Affine,
1234 _: Affine,
1235 _: &Paint<'_>,
1236 _: &GlyphDrawMode,
1237 ) {
1238 }
1239 fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {}
1240 fn pop_clip_path(&mut self) {}
1241 fn pop_transparency_group(&mut self) {}
1242 }
1243
1244 fn build_pdf(objects: &[Vec<u8>]) -> Vec<u8> {
1247 let mut out = b"%PDF-1.7\n".to_vec();
1248 let mut offsets = Vec::with_capacity(objects.len());
1249
1250 for (i, body) in objects.iter().enumerate() {
1251 offsets.push(out.len());
1252 out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
1253 out.extend_from_slice(body);
1254 out.extend_from_slice(b"\nendobj\n");
1255 }
1256
1257 let xref_pos = out.len();
1258 out.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
1259 out.extend_from_slice(b"0000000000 65535 f \n");
1260 for offset in offsets {
1261 out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
1262 }
1263 out.extend_from_slice(
1264 format!(
1265 "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",
1266 objects.len() + 1
1267 )
1268 .as_bytes(),
1269 );
1270
1271 out
1272 }
1273
1274 fn form_stream(bbox: &str, content: &str) -> Vec<u8> {
1276 format!(
1277 "<< /Type /XObject /Subtype /Form /BBox {bbox} /Length {} >>\nstream\n{content}\nendstream",
1278 content.len()
1279 )
1280 .into_bytes()
1281 }
1282
1283 fn checkbox_pdf(annot_body: &[u8], on_bbox: &str, extra_objects: &[Vec<u8>]) -> Vec<u8> {
1291 let mut objects = vec![
1292 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
1293 b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
1294 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
1295 /Annots [4 0 R] /Contents 7 0 R >>"
1296 .to_vec(),
1297 annot_body.to_vec(),
1298 form_stream(on_bbox, "0 0 10 10 re f\n12 12 4 4 re f"),
1299 form_stream("[0 0 20 20]", "0 0 7 7 re f"),
1300 b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
1301 ];
1302 objects.extend_from_slice(extra_objects);
1303 build_pdf(&objects)
1304 }
1305
1306 fn interpret_widths(pdf_bytes: Vec<u8>) -> Vec<f64> {
1309 interpret_widths_and_warnings(pdf_bytes).1
1310 }
1311
1312 fn interpret_widths_and_warnings(pdf_bytes: Vec<u8>) -> (Vec<InterpreterWarning>, Vec<f64>) {
1315 use std::sync::{Arc, Mutex};
1316
1317 let pdf = Pdf::new(pdf_bytes).expect("test PDF must parse");
1318 let pages = pdf.pages();
1319 let page = pages.first().expect("test PDF must have one page");
1320
1321 let warnings = Arc::new(Mutex::new(Vec::new()));
1322 let warnings_sink = warnings.clone();
1323 let settings = InterpreterSettings {
1324 warning_sink: Arc::new(move |w| warnings_sink.lock().expect("test lock").push(w)),
1325 ..Default::default()
1326 };
1327 let initial_transform = page.initial_transform(true);
1328 let bbox = kurbo::Rect::new(0.0, 0.0, 100.0, 100.0);
1329 let mut context = Context::new(initial_transform, bbox, page.xref(), settings);
1330 let mut device = CountingDevice::default();
1331
1332 interpret_page(page, &mut context, &mut device);
1333 (
1334 warnings.lock().expect("test lock").clone(),
1335 device.path_widths,
1336 )
1337 }
1338
1339 fn assert_widths(widths: &[f64], expected: &[f64]) {
1340 assert_eq!(
1341 widths.len(),
1342 expected.len(),
1343 "expected {expected:?}, got {widths:?}"
1344 );
1345 for (got, want) in widths.iter().zip(expected) {
1346 assert!(
1347 (got - want).abs() < 1e-6,
1348 "expected {expected:?}, got {widths:?}"
1349 );
1350 }
1351 }
1352
1353 #[test]
1356 fn widget_substate_as_on_state() {
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 >> >> /AS /Yes >>",
1360 "[0 0 20 20]",
1361 &[],
1362 );
1363 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1364 }
1365
1366 #[test]
1369 fn widget_substate_as_off_state() {
1370 let pdf = checkbox_pdf(
1371 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1372 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Off >>",
1373 "[0 0 20 20]",
1374 &[],
1375 );
1376 assert_widths(&interpret_widths(pdf), &[7.0]);
1377 }
1378
1379 #[test]
1382 fn widget_substate_as_off_without_off_entry() {
1383 let pdf = checkbox_pdf(
1384 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1385 /AP << /N << /Yes 5 0 R >> >> /AS /Off >>",
1386 "[0 0 20 20]",
1387 &[],
1388 );
1389 assert_widths(&interpret_widths(pdf), &[]);
1390 }
1391
1392 #[test]
1395 fn widget_substate_v_fallback() {
1396 let pdf = checkbox_pdf(
1397 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1398 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /V /Yes >>",
1399 "[0 0 20 20]",
1400 &[],
1401 );
1402 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1403 }
1404
1405 #[test]
1409 fn widget_substate_parent_v_fallback() {
1410 let pdf = checkbox_pdf(
1411 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1412 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /Parent 8 0 R >>",
1413 "[0 0 20 20]",
1414 &[b"<< /FT /Btn /V /Yes >>".to_vec()],
1415 );
1416 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1417 }
1418
1419 #[test]
1423 fn widget_substate_non_ascii_state_name() {
1424 let annot = b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1425 /AP << /N << /Stra\xf6m 5 0 R /Off 6 0 R >> >> /AS /Stra#F6m >>";
1426 assert!(annot.contains(&0xf6));
1428 let pdf = checkbox_pdf(annot, "[0 0 20 20]", &[]);
1429 assert_widths(&interpret_widths(pdf), &[10.0, 4.0]);
1430 }
1431
1432 #[test]
1435 fn widget_degenerate_bbox_skipped() {
1436 let pdf = checkbox_pdf(
1437 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1438 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Yes >>",
1439 "[0 0 0 20]",
1440 &[],
1441 );
1442 assert_widths(&interpret_widths(pdf), &[]);
1443 }
1444
1445 #[test]
1447 fn widget_direct_stream_still_renders() {
1448 let pdf = checkbox_pdf(
1449 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1450 /AP << /N 6 0 R >> >>",
1451 "[0 0 20 20]",
1452 &[],
1453 );
1454 assert_widths(&interpret_widths(pdf), &[7.0]);
1455 }
1456
1457 #[test]
1462 fn widget_unresolved_appearance_warns() {
1463 let pdf = checkbox_pdf(
1464 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1465 /AP << /N << /Yes 5 0 R >> >> /AS /Off >>",
1466 "[0 0 20 20]",
1467 &[],
1468 );
1469 let (warnings, widths) = interpret_widths_and_warnings(pdf);
1470 assert_widths(&widths, &[]);
1471 assert_eq!(warnings.len(), 1, "expected one warning, got {warnings:?}");
1472 assert!(
1473 matches!(
1474 warnings[0],
1475 InterpreterWarning::UnresolvedAnnotationAppearance
1476 ),
1477 "expected UnresolvedAnnotationAppearance, got {:?}",
1478 warnings[0]
1479 );
1480 }
1481
1482 #[test]
1485 fn widget_resolved_appearance_warns_nothing() {
1486 let pdf = checkbox_pdf(
1487 b"<< /Type /Annot /Subtype /Widget /FT /Btn /Rect [10 10 30 30] \
1488 /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> /AS /Yes >>",
1489 "[0 0 20 20]",
1490 &[],
1491 );
1492 let (warnings, widths) = interpret_widths_and_warnings(pdf);
1493 assert_widths(&widths, &[10.0, 4.0]);
1494 assert!(
1495 warnings.is_empty(),
1496 "expected no warnings, got {warnings:?}"
1497 );
1498 }
1499
1500 #[derive(Default)]
1503 struct FirstPixelDevice {
1504 first_pixels: Vec<[u8; 3]>,
1505 }
1506
1507 impl Device<'_> for FirstPixelDevice {
1508 fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
1509 fn set_blend_mode(&mut self, _: BlendMode) {}
1510 fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {}
1511 fn push_clip_path(&mut self, _: &ClipPath) {}
1512 fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
1513 fn draw_glyph(
1514 &mut self,
1515 _: &Glyph<'_>,
1516 _: Affine,
1517 _: Affine,
1518 _: &Paint<'_>,
1519 _: &GlyphDrawMode,
1520 ) {
1521 }
1522 fn draw_image(&mut self, image: Image<'_, '_>, _: Affine) {
1523 if let Image::Raster(raster) = image {
1524 raster.with_rgba(
1525 |rgb, _| {
1526 if rgb.data.len() >= 3 {
1527 self.first_pixels
1528 .push([rgb.data[0], rgb.data[1], rgb.data[2]]);
1529 }
1530 },
1531 None,
1532 );
1533 }
1534 }
1535 fn pop_clip_path(&mut self) {}
1536 fn pop_transparency_group(&mut self) {}
1537 }
1538
1539 fn interpret_first_pixels(pdf_bytes: Vec<u8>) -> Vec<[u8; 3]> {
1542 let pdf = Pdf::new(pdf_bytes).expect("test PDF must parse");
1543 let pages = pdf.pages();
1544 let page = pages.first().expect("test PDF must have one page");
1545
1546 let settings = InterpreterSettings::default();
1547 let initial_transform = page.initial_transform(true);
1548 let bbox = kurbo::Rect::new(0.0, 0.0, 200.0, 200.0);
1549 let mut context = Context::new(initial_transform, bbox, page.xref(), settings);
1550 let mut device = FirstPixelDevice::default();
1551
1552 interpret_page(page, &mut context, &mut device);
1553 device.first_pixels
1554 }
1555
1556 #[test]
1562 fn image_xobject_with_bare_name_colorspace_renders() {
1563 let mut pixels = Vec::with_capacity(64);
1564 for _ in 0..8 {
1565 pixels.extend_from_slice(&[0, 1, 2, 3, 4, 5, 6, 7]);
1566 }
1567 let mut image = format!(
1568 "<< /Type /XObject /Subtype /Image /Width 8 /Height 8 /BitsPerComponent 8 \
1569 /ColorSpace /CS0 /Length {} >>\nstream\n",
1570 pixels.len()
1571 )
1572 .into_bytes();
1573 image.extend_from_slice(&pixels);
1574 image.extend_from_slice(b"\nendstream");
1575
1576 let content = b"q\n200 0 0 200 0 0 cm\n/Im0 Do\nQ";
1577 let mut content_stream = format!("<< /Length {} >>\nstream\n", content.len()).into_bytes();
1578 content_stream.extend_from_slice(content);
1579 content_stream.extend_from_slice(b"\nendstream");
1580
1581 let pdf = build_pdf(&[
1582 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
1583 b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
1584 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1585 /Resources << /XObject << /Im0 4 0 R >> /ColorSpace << /CS0 6 0 R >> >> \
1586 /Contents 5 0 R >>"
1587 .to_vec(),
1588 image,
1589 content_stream,
1590 b"[/Indexed /DeviceRGB 7 <ff0000ff8000ffff0000ff0000ffff0000ffff00ff000000>]".to_vec(),
1591 ]);
1592
1593 assert_eq!(interpret_first_pixels(pdf), [[255, 0, 0]]);
1596 }
1597
1598 #[derive(Default)]
1601 struct TextDevice {
1602 text: String,
1603 }
1604
1605 impl Device<'_> for TextDevice {
1606 fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
1607 fn set_blend_mode(&mut self, _: BlendMode) {}
1608 fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {}
1609 fn push_clip_path(&mut self, _: &ClipPath) {}
1610 fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
1611 fn draw_glyph(
1612 &mut self,
1613 glyph: &Glyph<'_>,
1614 _: Affine,
1615 _: Affine,
1616 _: &Paint<'_>,
1617 _: &GlyphDrawMode,
1618 ) {
1619 match glyph.as_unicode() {
1620 Some(pdf_font::cmap::BfString::Char(c)) => self.text.push(c),
1621 Some(pdf_font::cmap::BfString::String(s)) => self.text.push_str(&s),
1622 None => {}
1623 }
1624 }
1625 fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {}
1626 fn pop_clip_path(&mut self) {}
1627 fn pop_transparency_group(&mut self) {}
1628 }
1629
1630 fn interpret_text(pdf_bytes: Vec<u8>) -> String {
1632 let pdf = Pdf::new(pdf_bytes).expect("test PDF must parse");
1633 let pages = pdf.pages();
1634 let page = pages.first().expect("test PDF must have one page");
1635
1636 let settings = InterpreterSettings::default();
1637 let initial_transform = page.initial_transform(true);
1638 let bbox = kurbo::Rect::new(0.0, 0.0, 100.0, 100.0);
1639 let mut context = Context::new(initial_transform, bbox, page.xref(), settings);
1640 let mut device = TextDevice::default();
1641
1642 interpret_page(page, &mut context, &mut device);
1643 device.text
1644 }
1645
1646 fn differences_font() -> Vec<u8> {
1649 b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding << \
1650 /Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [83 /X] >> >>"
1651 .to_vec()
1652 }
1653
1654 #[test]
1661 fn page_resources_shadow_pages_resources() {
1662 let content = b"BT /F1 12 Tf 72 700 Td (Shadowed) Tj ET";
1663 let content_stream = format!("<< /Length {} >>\nstream\n", content.len()).into_bytes();
1664 let mut content_stream_body = content_stream;
1665 content_stream_body.extend_from_slice(content);
1666 content_stream_body.extend_from_slice(b"\nendstream");
1667
1668 let pdf = build_pdf(&[
1669 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
1670 b"<< /Type /Pages /Kids [3 0 R] /Count 1 \
1671 /Resources << /Font << /F1 5 0 R >> >> >>"
1672 .to_vec(),
1673 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
1674 /Resources << /Font << /F2 6 0 R >> >> /Contents 4 0 R >>"
1675 .to_vec(),
1676 content_stream_body,
1677 differences_font(),
1678 b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(),
1679 ]);
1680
1681 assert_eq!(interpret_text(pdf), "Shadowed");
1682 }
1683
1684 #[test]
1690 fn form_resources_shadow_page_resources() {
1691 let form_content = b"BT /F1 12 Tf 10 10 Td (Shadowed) Tj ET";
1692 let mut form = format!(
1693 "<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] \
1694 /Resources << /Font << /F2 7 0 R >> >> /Length {} >>\nstream\n",
1695 form_content.len()
1696 )
1697 .into_bytes();
1698 form.extend_from_slice(form_content);
1699 form.extend_from_slice(b"\nendstream");
1700
1701 let page_content = b"q 1 0 0 1 0 0 cm /Fm0 Do Q";
1702 let mut page_stream =
1703 format!("<< /Length {} >>\nstream\n", page_content.len()).into_bytes();
1704 page_stream.extend_from_slice(page_content);
1705 page_stream.extend_from_slice(b"\nendstream");
1706
1707 let pdf = build_pdf(&[
1708 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
1709 b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
1710 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
1711 /Resources << /Font << /F1 6 0 R >> /XObject << /Fm0 4 0 R >> >> \
1712 /Contents 5 0 R >>"
1713 .to_vec(),
1714 form,
1715 page_stream,
1716 differences_font(),
1717 b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(),
1718 ]);
1719
1720 assert_eq!(interpret_text(pdf), "Shadowed");
1721 }
1722}