1use crate::destination::{Action, Destination, parse_action, parse_destination};
30use crate::diagnostics::{LocationHint, ParsePhase, Severity, WarningSink};
31use crate::metadata::{PdfDate, pdf_string_to_rust_pub};
32use crate::objects::{PdfDict, PdfObj};
33use crate::page_tree::PageInfo;
34use crate::resolver::Resolver;
35
36#[derive(Debug, Clone)]
42pub struct Annotation {
43 pub kind: AnnotationKind,
45 pub rect: [f64; 4],
48 pub contents: Option<String>,
51 pub name: Option<String>,
53 pub modified: Option<AnnotationDate>,
55 pub title: Option<String>,
57 pub subject: Option<String>,
59 pub flags: AnnotationFlags,
61 pub color: Option<AnnotationColor>,
63 pub border: Option<Border>,
65 pub has_appearance: bool,
69 pub kind_data: AnnotationKindData,
71}
72
73#[derive(Debug, Clone, PartialEq)]
76#[non_exhaustive]
77pub enum AnnotationDate {
78 Date(PdfDate),
80 Raw(String),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86#[non_exhaustive]
87pub enum AnnotationKind {
88 Text,
89 Link,
90 FreeText,
91 Line,
92 Square,
93 Circle,
94 Polygon,
95 PolyLine,
96 Highlight,
97 Underline,
98 Squiggly,
99 StrikeOut,
100 Stamp,
101 Caret,
102 Ink,
103 Popup,
104 FileAttachment,
105 Widget,
106 Screen,
107 PrinterMark,
108 TrapNet,
109 Watermark,
110 Sound,
112 Movie,
114 ThreeD,
116 RichMedia,
118 Other(String),
120}
121
122impl AnnotationKind {
123 fn from_name(name: &[u8]) -> Self {
124 match name {
125 b"Text" => AnnotationKind::Text,
126 b"Link" => AnnotationKind::Link,
127 b"FreeText" => AnnotationKind::FreeText,
128 b"Line" => AnnotationKind::Line,
129 b"Square" => AnnotationKind::Square,
130 b"Circle" => AnnotationKind::Circle,
131 b"Polygon" => AnnotationKind::Polygon,
132 b"PolyLine" => AnnotationKind::PolyLine,
133 b"Highlight" => AnnotationKind::Highlight,
134 b"Underline" => AnnotationKind::Underline,
135 b"Squiggly" => AnnotationKind::Squiggly,
136 b"StrikeOut" => AnnotationKind::StrikeOut,
137 b"Stamp" => AnnotationKind::Stamp,
138 b"Caret" => AnnotationKind::Caret,
139 b"Ink" => AnnotationKind::Ink,
140 b"Popup" => AnnotationKind::Popup,
141 b"FileAttachment" => AnnotationKind::FileAttachment,
142 b"Widget" => AnnotationKind::Widget,
143 b"Screen" => AnnotationKind::Screen,
144 b"PrinterMark" => AnnotationKind::PrinterMark,
145 b"TrapNet" => AnnotationKind::TrapNet,
146 b"Watermark" => AnnotationKind::Watermark,
147 b"Sound" => AnnotationKind::Sound,
148 b"Movie" => AnnotationKind::Movie,
149 b"3D" => AnnotationKind::ThreeD,
150 b"RichMedia" => AnnotationKind::RichMedia,
151 other => AnnotationKind::Other(String::from_utf8_lossy(other).into_owned()),
152 }
153 }
154}
155
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
158pub struct AnnotationFlags {
159 pub invisible: bool,
160 pub hidden: bool,
161 pub print: bool,
162 pub no_zoom: bool,
163 pub no_rotate: bool,
164 pub no_view: bool,
165 pub read_only: bool,
166 pub locked: bool,
167 pub toggle_no_view: bool,
168 pub locked_contents: bool,
169}
170
171impl AnnotationFlags {
172 fn from_bits(bits: i64) -> Self {
173 Self {
174 invisible: bits & 0x0001 != 0,
175 hidden: bits & 0x0002 != 0,
176 print: bits & 0x0004 != 0,
177 no_zoom: bits & 0x0008 != 0,
178 no_rotate: bits & 0x0010 != 0,
179 no_view: bits & 0x0020 != 0,
180 read_only: bits & 0x0040 != 0,
181 locked: bits & 0x0080 != 0,
182 toggle_no_view: bits & 0x0100 != 0,
183 locked_contents: bits & 0x0200 != 0,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq)]
190#[non_exhaustive]
191pub enum AnnotationColor {
192 Transparent,
194 Gray(f32),
196 Rgb([f32; 3]),
198 Cmyk([f32; 4]),
200}
201
202impl AnnotationColor {
203 fn from_array(arr: &[PdfObj]) -> Option<Self> {
204 let n = |i: usize| arr.get(i).and_then(|o| o.as_f64()).map(|v| v as f32);
205 match arr.len() {
206 0 => Some(AnnotationColor::Transparent),
207 1 => Some(AnnotationColor::Gray(n(0)?)),
208 3 => Some(AnnotationColor::Rgb([n(0)?, n(1)?, n(2)?])),
209 4 => Some(AnnotationColor::Cmyk([n(0)?, n(1)?, n(2)?, n(3)?])),
210 _ => None,
211 }
212 }
213}
214
215#[derive(Debug, Clone, Default, PartialEq)]
217pub struct Border {
218 pub h_radius: f64,
219 pub v_radius: f64,
220 pub width: f64,
221 pub dash: Vec<f64>,
222}
223
224#[derive(Debug, Clone)]
226#[non_exhaustive]
227pub enum AnnotationKindData {
228 Link(LinkAnnotation),
229 Text(TextAnnotation),
230 FreeText(FreeTextAnnotation),
231 Markup(MarkupAnnotation),
233 Line(LineAnnotation),
234 Shape(ShapeAnnotation),
236 Polygon(PolygonAnnotation),
238 Ink(InkAnnotation),
239 Stamp(StampAnnotation),
240 Caret(CaretAnnotation),
241 FileAttachment(FileAttachmentAnnotation),
242 Popup(PopupAnnotation),
243 Minimal,
246}
247
248#[derive(Debug, Clone, Default)]
250pub struct LinkAnnotation {
251 pub action: Option<Action>,
253 pub destination: Option<Destination>,
255 pub highlight_mode: Option<String>,
258 pub quad_points: Vec<[f64; 8]>,
261}
262
263#[derive(Debug, Clone, Default)]
265pub struct TextAnnotation {
266 pub open: bool,
268 pub icon: Option<String>,
271 pub state: Option<String>,
273 pub state_model: Option<String>,
275}
276
277#[derive(Debug, Clone, Default)]
279pub struct FreeTextAnnotation {
280 pub default_appearance: Option<String>,
282 pub quadding: u8,
284 pub rich_content: Option<String>,
286 pub default_style: Option<String>,
288 pub callout_line: Option<Vec<f64>>,
290 pub intent: Option<String>,
292 pub rect_diff: Option<[f64; 4]>,
294 pub line_ending: Option<String>,
296}
297
298#[derive(Debug, Clone, Default)]
300pub struct MarkupAnnotation {
301 pub quad_points: Vec<[f64; 8]>,
305}
306
307#[derive(Debug, Clone, Default)]
309pub struct LineAnnotation {
310 pub endpoints: [f64; 4],
312 pub line_ending: Option<[String; 2]>,
314 pub interior_color: Option<AnnotationColor>,
316 pub leader_length: Option<f64>,
318 pub leader_extension: Option<f64>,
320 pub leader_offset: Option<f64>,
322 pub cap: Option<bool>,
324 pub cap_position: Option<String>,
326 pub intent: Option<String>,
328}
329
330#[derive(Debug, Clone, Default)]
332pub struct ShapeAnnotation {
333 pub interior_color: Option<AnnotationColor>,
335 pub rect_diff: Option<[f64; 4]>,
337}
338
339#[derive(Debug, Clone, Default)]
341pub struct PolygonAnnotation {
342 pub vertices: Vec<f64>,
344 pub line_ending: Option<[String; 2]>,
346 pub interior_color: Option<AnnotationColor>,
348 pub intent: Option<String>,
350}
351
352#[derive(Debug, Clone, Default)]
354pub struct InkAnnotation {
355 pub strokes: Vec<Vec<f64>>,
358}
359
360#[derive(Debug, Clone, Default)]
362pub struct StampAnnotation {
363 pub icon: Option<String>,
366 pub intent: Option<String>,
368}
369
370#[derive(Debug, Clone, Default)]
372pub struct CaretAnnotation {
373 pub rect_diff: Option<[f64; 4]>,
375 pub symbol: Option<String>,
377}
378
379#[derive(Debug, Clone, Default)]
381pub struct FileAttachmentAnnotation {
382 pub filename: Option<String>,
385 pub icon: Option<String>,
387}
388
389#[derive(Debug, Clone, Default)]
391pub struct PopupAnnotation {
392 pub open: bool,
394 pub parent_obj_num: Option<u32>,
397}
398
399pub fn parse_page_annotations(
407 resolver: &Resolver,
408 pages: &[PageInfo],
409 page_index: usize,
410 sink: &WarningSink,
411) -> Vec<Annotation> {
412 let Some(page) = pages.get(page_index) else {
413 return Vec::new();
414 };
415 let mut out = Vec::with_capacity(page.annots.len());
416 for &(num, gen_num) in &page.annots {
417 let Ok(obj) = resolver.resolve(num, gen_num) else {
418 sink.record(
419 ParsePhase::Annotations { page: page_index },
420 Some(LocationHint::Object {
421 obj_num: num,
422 gen_num,
423 }),
424 Severity::Warning,
425 "annotation object could not be resolved; skipped",
426 );
427 continue;
428 };
429 let Some(dict) = obj.as_dict() else {
430 sink.record(
431 ParsePhase::Annotations { page: page_index },
432 Some(LocationHint::Object {
433 obj_num: num,
434 gen_num,
435 }),
436 Severity::Warning,
437 "annotation object is not a dict; skipped",
438 );
439 continue;
440 };
441 match parse_annotation(resolver, pages, dict) {
442 Some(annot) => out.push(annot),
443 None => sink.record(
444 ParsePhase::Annotations { page: page_index },
445 Some(LocationHint::Object {
446 obj_num: num,
447 gen_num,
448 }),
449 Severity::Warning,
450 "annotation missing or malformed /Rect; skipped",
451 ),
452 }
453 }
454 out
455}
456
457pub fn parse_annotation(
462 resolver: &Resolver,
463 pages: &[PageInfo],
464 dict: &PdfDict,
465) -> Option<Annotation> {
466 let rect = dict.get_array(b"Rect").and_then(parse_rect)?;
467 let subtype_bytes = dict.get_name(b"Subtype").unwrap_or(b"");
468 let kind = AnnotationKind::from_name(subtype_bytes);
469
470 let contents = dict.get(b"Contents").and_then(pdf_string_to_rust_pub);
471 let name = dict.get(b"NM").and_then(pdf_string_to_rust_pub);
472 let modified = dict.get(b"M").and_then(parse_annotation_date);
473 let title = dict.get(b"T").and_then(pdf_string_to_rust_pub);
474 let subject = dict.get(b"Subj").and_then(pdf_string_to_rust_pub);
475 let flags = AnnotationFlags::from_bits(dict.get_int(b"F").unwrap_or(0));
476 let color = dict.get_array(b"C").and_then(AnnotationColor::from_array);
477 let border = parse_border(dict);
478 let has_appearance = dict.get(b"AP").is_some();
479
480 let kind_data = parse_kind_data(resolver, pages, &kind, dict);
481
482 Some(Annotation {
483 kind,
484 rect,
485 contents,
486 name,
487 modified,
488 title,
489 subject,
490 flags,
491 color,
492 border,
493 has_appearance,
494 kind_data,
495 })
496}
497
498fn parse_rect(arr: &[PdfObj]) -> Option<[f64; 4]> {
499 if arr.len() < 4 {
500 return None;
501 }
502 Some([
503 arr[0].as_f64()?,
504 arr[1].as_f64()?,
505 arr[2].as_f64()?,
506 arr[3].as_f64()?,
507 ])
508}
509
510fn parse_rect_diff(arr: &[PdfObj]) -> Option<[f64; 4]> {
511 if arr.len() < 4 {
512 return None;
513 }
514 Some([
515 arr[0].as_f64()?,
516 arr[1].as_f64()?,
517 arr[2].as_f64()?,
518 arr[3].as_f64()?,
519 ])
520}
521
522fn parse_border(dict: &PdfDict) -> Option<Border> {
523 let arr = dict.get_array(b"Border")?;
524 if arr.len() < 3 {
525 return None;
526 }
527 let mut border = Border {
528 h_radius: arr[0].as_f64().unwrap_or(0.0),
529 v_radius: arr[1].as_f64().unwrap_or(0.0),
530 width: arr[2].as_f64().unwrap_or(1.0),
531 dash: Vec::new(),
532 };
533 if let Some(dash_arr) = arr.get(3).and_then(|o| o.as_array()) {
534 border.dash = dash_arr.iter().filter_map(|o| o.as_f64()).collect();
535 }
536 Some(border)
537}
538
539fn parse_annotation_date(obj: &PdfObj) -> Option<AnnotationDate> {
540 let s = obj.as_str()?;
541 if let Some(date) = PdfDate::parse(s) {
542 Some(AnnotationDate::Date(date))
543 } else {
544 Some(AnnotationDate::Raw(String::from_utf8_lossy(s).into_owned()))
545 }
546}
547
548fn parse_quad_points(arr: &[PdfObj]) -> Vec<[f64; 8]> {
549 let mut out = Vec::new();
550 let coords: Vec<f64> = arr.iter().filter_map(|o| o.as_f64()).collect();
551 let mut i = 0;
552 while i + 8 <= coords.len() {
553 out.push([
554 coords[i],
555 coords[i + 1],
556 coords[i + 2],
557 coords[i + 3],
558 coords[i + 4],
559 coords[i + 5],
560 coords[i + 6],
561 coords[i + 7],
562 ]);
563 i += 8;
564 }
565 out
566}
567
568fn parse_line_ending_pair(arr: &[PdfObj]) -> Option<[String; 2]> {
569 if arr.len() < 2 {
570 return None;
571 }
572 let a = arr[0]
573 .as_name()
574 .map(|n| String::from_utf8_lossy(n).into_owned())?;
575 let b = arr[1]
576 .as_name()
577 .map(|n| String::from_utf8_lossy(n).into_owned())?;
578 Some([a, b])
579}
580
581fn parse_kind_data(
582 resolver: &Resolver,
583 pages: &[PageInfo],
584 kind: &AnnotationKind,
585 dict: &PdfDict,
586) -> AnnotationKindData {
587 match kind {
588 AnnotationKind::Link => {
589 let action = dict
590 .get(b"A")
591 .and_then(|o| parse_action(resolver, pages, o));
592 let destination = dict
593 .get(b"Dest")
594 .and_then(|o| parse_destination(resolver, pages, o));
595 let highlight_mode = dict
596 .get_name(b"H")
597 .map(|n| String::from_utf8_lossy(n).into_owned());
598 let quad_points = dict
599 .get_array(b"QuadPoints")
600 .map(parse_quad_points)
601 .unwrap_or_default();
602 AnnotationKindData::Link(LinkAnnotation {
603 action,
604 destination,
605 highlight_mode,
606 quad_points,
607 })
608 }
609 AnnotationKind::Text => AnnotationKindData::Text(TextAnnotation {
610 open: dict.get(b"Open").and_then(as_bool).unwrap_or(false),
611 icon: dict
612 .get_name(b"Name")
613 .map(|n| String::from_utf8_lossy(n).into_owned()),
614 state: dict.get(b"State").and_then(pdf_string_to_rust_pub),
615 state_model: dict.get(b"StateModel").and_then(pdf_string_to_rust_pub),
616 }),
617 AnnotationKind::FreeText => AnnotationKindData::FreeText(FreeTextAnnotation {
618 default_appearance: dict.get(b"DA").and_then(pdf_string_to_rust_pub),
619 quadding: dict.get_int(b"Q").unwrap_or(0).clamp(0, 2) as u8,
620 rich_content: dict.get(b"RC").and_then(pdf_string_to_rust_pub),
621 default_style: dict.get(b"DS").and_then(pdf_string_to_rust_pub),
622 callout_line: dict
623 .get_array(b"CL")
624 .map(|a| a.iter().filter_map(|o| o.as_f64()).collect()),
625 intent: dict
626 .get_name(b"IT")
627 .map(|n| String::from_utf8_lossy(n).into_owned()),
628 rect_diff: dict.get_array(b"RD").and_then(parse_rect_diff),
629 line_ending: dict
630 .get_name(b"LE")
631 .map(|n| String::from_utf8_lossy(n).into_owned()),
632 }),
633 AnnotationKind::Highlight
634 | AnnotationKind::Underline
635 | AnnotationKind::Squiggly
636 | AnnotationKind::StrikeOut => AnnotationKindData::Markup(MarkupAnnotation {
637 quad_points: dict
638 .get_array(b"QuadPoints")
639 .map(parse_quad_points)
640 .unwrap_or_default(),
641 }),
642 AnnotationKind::Line => {
643 let endpoints = dict
644 .get_array(b"L")
645 .and_then(parse_rect)
646 .unwrap_or_default();
647 AnnotationKindData::Line(LineAnnotation {
648 endpoints,
649 line_ending: dict.get_array(b"LE").and_then(parse_line_ending_pair),
650 interior_color: dict.get_array(b"IC").and_then(AnnotationColor::from_array),
651 leader_length: dict.get_f64(b"LL"),
652 leader_extension: dict.get_f64(b"LLE"),
653 leader_offset: dict.get_f64(b"LLO"),
654 cap: dict.get(b"Cap").and_then(as_bool),
655 cap_position: dict
656 .get_name(b"CP")
657 .map(|n| String::from_utf8_lossy(n).into_owned()),
658 intent: dict
659 .get_name(b"IT")
660 .map(|n| String::from_utf8_lossy(n).into_owned()),
661 })
662 }
663 AnnotationKind::Square | AnnotationKind::Circle => {
664 AnnotationKindData::Shape(ShapeAnnotation {
665 interior_color: dict.get_array(b"IC").and_then(AnnotationColor::from_array),
666 rect_diff: dict.get_array(b"RD").and_then(parse_rect_diff),
667 })
668 }
669 AnnotationKind::Polygon | AnnotationKind::PolyLine => {
670 AnnotationKindData::Polygon(PolygonAnnotation {
671 vertices: dict
672 .get_array(b"Vertices")
673 .map(|a| a.iter().filter_map(|o| o.as_f64()).collect())
674 .unwrap_or_default(),
675 line_ending: dict.get_array(b"LE").and_then(parse_line_ending_pair),
676 interior_color: dict.get_array(b"IC").and_then(AnnotationColor::from_array),
677 intent: dict
678 .get_name(b"IT")
679 .map(|n| String::from_utf8_lossy(n).into_owned()),
680 })
681 }
682 AnnotationKind::Ink => {
683 let strokes = dict
684 .get_array(b"InkList")
685 .map(|outer| {
686 outer
687 .iter()
688 .filter_map(|stroke| {
689 stroke
690 .as_array()
691 .map(|a| a.iter().filter_map(|o| o.as_f64()).collect::<Vec<_>>())
692 })
693 .collect()
694 })
695 .unwrap_or_default();
696 AnnotationKindData::Ink(InkAnnotation { strokes })
697 }
698 AnnotationKind::Stamp => AnnotationKindData::Stamp(StampAnnotation {
699 icon: dict
700 .get_name(b"Name")
701 .map(|n| String::from_utf8_lossy(n).into_owned()),
702 intent: dict
703 .get_name(b"IT")
704 .map(|n| String::from_utf8_lossy(n).into_owned()),
705 }),
706 AnnotationKind::Caret => AnnotationKindData::Caret(CaretAnnotation {
707 rect_diff: dict.get_array(b"RD").and_then(parse_rect_diff),
708 symbol: dict
709 .get_name(b"Sy")
710 .map(|n| String::from_utf8_lossy(n).into_owned()),
711 }),
712 AnnotationKind::FileAttachment => {
713 let filename = parse_file_spec_value(resolver, dict.get(b"FS"));
714 AnnotationKindData::FileAttachment(FileAttachmentAnnotation {
715 filename,
716 icon: dict
717 .get_name(b"Name")
718 .map(|n| String::from_utf8_lossy(n).into_owned()),
719 })
720 }
721 AnnotationKind::Popup => AnnotationKindData::Popup(PopupAnnotation {
722 open: dict.get(b"Open").and_then(as_bool).unwrap_or(false),
723 parent_obj_num: dict.get_ref(b"Parent").map(|(n, _)| n),
724 }),
725 _ => AnnotationKindData::Minimal,
727 }
728}
729
730fn as_bool(obj: &PdfObj) -> Option<bool> {
731 match obj {
732 PdfObj::Bool(b) => Some(*b),
733 _ => None,
734 }
735}
736
737fn parse_file_spec_value(resolver: &Resolver, obj: Option<&PdfObj>) -> Option<String> {
738 let obj = obj?;
739 let resolved = resolver.deref(obj).ok()?;
740 if let Some(s) = resolved.as_str() {
741 return Some(crate::metadata::decode_pdf_text_string_pub(s));
742 }
743 if let Some(d) = resolved.as_dict() {
744 if let Some(uf) = d.get(b"UF").and_then(pdf_string_to_rust_pub) {
745 return Some(uf);
746 }
747 if let Some(f) = d.get(b"F").and_then(pdf_string_to_rust_pub) {
748 return Some(f);
749 }
750 }
751 None
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 #[test]
759 fn flags_decode() {
760 let f = AnnotationFlags::from_bits(0x0085); assert!(f.invisible);
762 assert!(f.print);
763 assert!(f.locked);
764 assert!(!f.hidden);
765 assert!(!f.read_only);
766 }
767
768 #[test]
769 fn color_array_lengths() {
770 assert_eq!(
771 AnnotationColor::from_array(&[]),
772 Some(AnnotationColor::Transparent)
773 );
774 assert_eq!(
775 AnnotationColor::from_array(&[PdfObj::Real(0.5)]),
776 Some(AnnotationColor::Gray(0.5))
777 );
778 assert_eq!(
779 AnnotationColor::from_array(
780 &[PdfObj::Real(1.0), PdfObj::Real(0.0), PdfObj::Real(0.0),]
781 ),
782 Some(AnnotationColor::Rgb([1.0, 0.0, 0.0]))
783 );
784 assert_eq!(
785 AnnotationColor::from_array(&[
786 PdfObj::Real(0.0),
787 PdfObj::Real(0.0),
788 PdfObj::Real(0.0),
789 PdfObj::Real(1.0),
790 ]),
791 Some(AnnotationColor::Cmyk([0.0, 0.0, 0.0, 1.0]))
792 );
793 assert!(AnnotationColor::from_array(&[PdfObj::Real(0.5), PdfObj::Real(0.5)]).is_none());
795 }
796
797 #[test]
798 fn quad_points_split_into_quads() {
799 let coords: Vec<PdfObj> = (0..16).map(|i| PdfObj::Real(i as f64)).collect();
800 let quads = parse_quad_points(&coords);
801 assert_eq!(quads.len(), 2);
802 assert_eq!(quads[0], [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
803 assert_eq!(quads[1], [8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0]);
804 }
805
806 #[test]
807 fn quad_points_drops_partial_trailing() {
808 let coords: Vec<PdfObj> = (0..12).map(|i| PdfObj::Real(i as f64)).collect();
809 let quads = parse_quad_points(&coords);
810 assert_eq!(quads.len(), 1, "trailing 4 coords are dropped");
811 }
812
813 #[test]
814 fn subtype_known_and_unknown() {
815 assert_eq!(AnnotationKind::from_name(b"Link"), AnnotationKind::Link);
816 assert_eq!(
817 AnnotationKind::from_name(b"Highlight"),
818 AnnotationKind::Highlight
819 );
820 assert_eq!(
821 AnnotationKind::from_name(b"InventedSubtype"),
822 AnnotationKind::Other("InventedSubtype".to_string())
823 );
824 }
825
826 #[test]
827 fn border_default_when_short() {
828 let mut dict = PdfDict::new();
830 dict.insert(
831 b"Border".to_vec(),
832 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(0)]),
833 );
834 assert!(parse_border(&dict).is_none());
835 }
836
837 #[test]
838 fn border_with_dash_array() {
839 let mut dict = PdfDict::new();
840 dict.insert(
841 b"Border".to_vec(),
842 PdfObj::Array(vec![
843 PdfObj::Int(0),
844 PdfObj::Int(0),
845 PdfObj::Real(2.0),
846 PdfObj::Array(vec![PdfObj::Real(3.0), PdfObj::Real(2.0)]),
847 ]),
848 );
849 let b = parse_border(&dict).unwrap();
850 assert_eq!(b.width, 2.0);
851 assert_eq!(b.dash, vec![3.0, 2.0]);
852 }
853}