1use crate::annotation_types::{AnnotationFlags, AnnotationSubtype, WidgetFieldType};
16use crate::document::PdfDocument;
17use crate::error::Result;
18use crate::object::Object;
19
20#[derive(Debug, Clone)]
24pub struct Annotation {
25 pub annotation_type: String,
27
28 pub subtype: Option<String>,
30
31 pub subtype_enum: AnnotationSubtype,
33
34 pub contents: Option<String>,
36
37 pub rect: Option<[f64; 4]>,
39
40 pub author: Option<String>,
42
43 pub creation_date: Option<String>,
45
46 pub modification_date: Option<String>,
48
49 pub subject: Option<String>,
51
52 pub destination: Option<LinkDestination>,
55
56 pub action: Option<LinkAction>,
59
60 pub quad_points: Option<Vec<[f64; 8]>>,
64
65 pub color: Option<Vec<f64>>,
67
68 pub opacity: Option<f64>,
70
71 pub flags: AnnotationFlags,
73
74 pub border: Option<[f64; 3]>,
76
77 pub interior_color: Option<Vec<f64>>,
79
80 pub field_type: Option<WidgetFieldType>,
83
84 pub field_name: Option<String>,
87
88 pub field_value: Option<String>,
90
91 pub default_value: Option<String>,
93
94 pub field_flags: Option<u32>,
96
97 pub options: Option<Vec<String>>,
99
100 pub appearance_state: Option<String>,
102
103 pub raw_dict: Option<std::collections::HashMap<String, crate::object::Object>>,
110}
111
112#[derive(Debug, Clone, PartialEq)]
116pub enum LinkDestination {
117 Named(String),
119 Explicit {
121 page: u32,
123 fit_type: String,
125 params: Vec<f32>,
127 },
128}
129
130#[derive(Debug, Clone, PartialEq)]
134pub enum LinkAction {
135 Uri(String),
137 GoTo(LinkDestination),
139 GoToRemote {
141 file: String,
143 destination: Option<LinkDestination>,
145 },
146 Other {
148 action_type: String,
150 },
151}
152
153impl PdfDocument {
154 pub fn get_annotations(&self, page_index: usize) -> Result<Vec<Annotation>> {
184 let page_ref = self.get_page_ref(page_index)?;
186 let page_obj = self.load_object(page_ref)?;
187
188 let annots = match page_obj.as_dict() {
190 Some(dict) => match dict.get("Annots") {
191 Some(Object::Array(arr)) => arr.clone(),
192 Some(Object::Reference(annot_ref)) => {
193 match self.load_object(*annot_ref)? {
195 Object::Array(arr) => arr,
196 _ => return Ok(Vec::new()),
197 }
198 },
199 _ => return Ok(Vec::new()), },
201 None => return Ok(Vec::new()),
202 };
203
204 let mut result = Vec::new();
205
206 for annot_obj in annots {
208 let annot_ref = match annot_obj {
209 Object::Reference(r) => r,
210 _ => continue, };
212
213 if let Ok(annotation) = self.parse_annotation(annot_ref) {
214 result.push(annotation);
215 }
216 }
217
218 Ok(result)
219 }
220
221 fn parse_annotation(&self, annot_ref: crate::object::ObjectRef) -> Result<Annotation> {
223 let annot_obj = self.load_object(annot_ref)?;
224
225 let dict = annot_obj.as_dict().ok_or_else(|| {
226 crate::error::Error::InvalidPdf("Annotation is not a dictionary".to_string())
227 })?;
228
229 let annotation_type = dict
231 .get("Type")
232 .and_then(|t| t.as_name())
233 .unwrap_or("Unknown")
234 .to_string();
235
236 let subtype = dict
237 .get("Subtype")
238 .and_then(|s| s.as_name())
239 .map(|s| s.to_string());
240
241 let subtype_enum = subtype
243 .as_deref()
244 .map(AnnotationSubtype::from_pdf_name)
245 .unwrap_or(AnnotationSubtype::Unknown);
246
247 let contents = dict.get("Contents").and_then(|c| match c {
249 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
250 _ => None,
251 });
252
253 let rect = dict.get("Rect").and_then(|r| match r {
255 Object::Array(arr) if arr.len() == 4 => {
256 let mut rect_arr = [0.0; 4];
257 for (i, obj) in arr.iter().enumerate() {
258 rect_arr[i] = match obj {
259 Object::Integer(n) => *n as f64,
260 Object::Real(f) => *f,
261 _ => 0.0,
262 };
263 }
264 Some(rect_arr)
265 },
266 _ => None,
267 });
268
269 let author = dict.get("T").and_then(|t| match t {
271 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
272 _ => None,
273 });
274
275 let creation_date = dict.get("CreationDate").and_then(|d| match d {
277 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
278 _ => None,
279 });
280
281 let modification_date = dict.get("M").and_then(|d| match d {
283 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
284 _ => None,
285 });
286
287 let subject = dict.get("Subj").and_then(|s| match s {
289 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
290 _ => None,
291 });
292
293 let flags = dict
295 .get("F")
296 .and_then(|f| match f {
297 Object::Integer(n) => Some(AnnotationFlags::new(*n as u32)),
298 _ => None,
299 })
300 .unwrap_or_default();
301
302 let color = Self::parse_number_array(dict.get("C"));
304
305 let opacity = dict.get("CA").and_then(|o| match o {
307 Object::Real(f) => Some(*f),
308 Object::Integer(n) => Some(*n as f64),
309 _ => None,
310 });
311
312 let border = dict.get("Border").and_then(|b| match b {
314 Object::Array(arr) if arr.len() >= 3 => {
315 let mut border_arr = [0.0; 3];
316 for (i, obj) in arr.iter().take(3).enumerate() {
317 border_arr[i] = match obj {
318 Object::Integer(n) => *n as f64,
319 Object::Real(f) => *f,
320 _ => 0.0,
321 };
322 }
323 Some(border_arr)
324 },
325 _ => None,
326 });
327
328 let interior_color = Self::parse_number_array(dict.get("IC"));
330
331 let quad_points = if subtype_enum.is_text_markup() {
333 Self::parse_quad_points(dict.get("QuadPoints"))
334 } else {
335 None
336 };
337
338 let (destination, action) = if subtype_enum == AnnotationSubtype::Link {
340 let dest = dict
341 .get("Dest")
342 .and_then(|d| self.parse_destination(d).ok());
343 let act = dict.get("A").and_then(|a| self.parse_action(a).ok());
344 (dest, act)
345 } else {
346 (None, None)
347 };
348
349 let (
351 field_type,
352 field_name,
353 field_value,
354 default_value,
355 field_flags,
356 options,
357 appearance_state,
358 ) = if subtype_enum == AnnotationSubtype::Widget {
359 self.parse_widget_fields(dict)
360 } else {
361 (None, None, None, None, None, None, None)
362 };
363
364 Ok(Annotation {
365 annotation_type,
366 subtype,
367 subtype_enum,
368 contents,
369 rect,
370 author,
371 creation_date,
372 modification_date,
373 subject,
374 destination,
375 action,
376 quad_points,
377 color,
378 opacity,
379 flags,
380 border,
381 interior_color,
382 field_type,
383 field_name,
384 field_value,
385 default_value,
386 field_flags,
387 options,
388 appearance_state,
389 raw_dict: Some(dict.clone()),
390 })
391 }
392
393 fn parse_widget_fields(
397 &self,
398 dict: &std::collections::HashMap<String, Object>,
399 ) -> (
400 Option<WidgetFieldType>,
401 Option<String>,
402 Option<String>,
403 Option<String>,
404 Option<u32>,
405 Option<Vec<String>>,
406 Option<String>,
407 ) {
408 let mut ft = dict
410 .get("FT")
411 .and_then(|f| f.as_name())
412 .map(|s| s.to_string());
413
414 let mut field_flags = dict.get("Ff").and_then(|f| match f {
416 Object::Integer(n) => Some(*n as u32),
417 _ => None,
418 });
419
420 let mut field_value = Self::parse_string_value(dict.get("V"));
422
423 let mut default_value = Self::parse_string_value(dict.get("DV"));
425
426 if ft.is_none() || field_flags.is_none() || field_value.is_none() || default_value.is_none()
428 {
429 let mut parent_ref = dict.get("Parent").and_then(|p| {
430 if let Object::Reference(r) = p {
431 Some(*r)
432 } else {
433 None
434 }
435 });
436 let mut depth = 0;
437 while let Some(pref) = parent_ref {
438 if depth >= 10 {
439 break;
440 }
441 depth += 1;
442 if let Ok(parent_obj) = self.load_object(pref) {
443 if let Some(parent_dict) = parent_obj.as_dict() {
444 if ft.is_none() {
445 ft = parent_dict
446 .get("FT")
447 .and_then(|f| f.as_name())
448 .map(|s| s.to_string());
449 }
450 if field_flags.is_none() {
451 field_flags = parent_dict.get("Ff").and_then(|f| match f {
452 Object::Integer(n) => Some(*n as u32),
453 _ => None,
454 });
455 }
456 if field_value.is_none() {
457 field_value = Self::parse_string_value(parent_dict.get("V"));
458 }
459 if default_value.is_none() {
460 default_value = Self::parse_string_value(parent_dict.get("DV"));
461 }
462 parent_ref = parent_dict.get("Parent").and_then(|p| {
463 if let Object::Reference(r) = p {
464 Some(*r)
465 } else {
466 None
467 }
468 });
469 } else {
470 break;
471 }
472 } else {
473 break;
474 }
475 }
476 }
477
478 let field_name = dict.get("T").and_then(|t| match t {
480 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
481 _ => None,
482 });
483
484 let appearance_state = dict
486 .get("AS")
487 .and_then(|a| a.as_name())
488 .map(|s| s.to_string());
489
490 let options = Self::parse_options_array(dict.get("Opt"));
492
493 let field_type = match ft.as_deref() {
495 Some("Tx") => Some(WidgetFieldType::Text),
496 Some("Btn") => {
497 let ff = field_flags.unwrap_or(0);
499 if ff & 0x10000 != 0 {
502 Some(WidgetFieldType::Radio {
504 selected: appearance_state.clone(),
505 })
506 } else if ff & 0x8000 != 0 {
507 Some(WidgetFieldType::Button)
509 } else {
510 let checked = appearance_state
512 .as_deref()
513 .map(|s| s != "Off" && !s.is_empty())
514 .unwrap_or(false);
515 Some(WidgetFieldType::Checkbox { checked })
516 }
517 },
518 Some("Ch") => {
519 Some(WidgetFieldType::Choice {
521 options: options.clone().unwrap_or_default(),
522 selected: field_value.clone(),
523 })
524 },
525 Some("Sig") => Some(WidgetFieldType::Signature),
526 _ => None,
527 };
528
529 (
530 field_type,
531 field_name,
532 field_value,
533 default_value,
534 field_flags,
535 options,
536 appearance_state,
537 )
538 }
539
540 fn parse_string_value(obj: Option<&Object>) -> Option<String> {
542 match obj {
543 Some(Object::String(s)) => Some(String::from_utf8_lossy(s).to_string()),
544 Some(Object::Name(n)) => Some(n.clone()),
545 Some(Object::Integer(i)) => Some(i.to_string()),
546 Some(Object::Real(f)) => Some(f.to_string()),
547 _ => None,
548 }
549 }
550
551 fn parse_options_array(obj: Option<&Object>) -> Option<Vec<String>> {
553 match obj {
554 Some(Object::Array(arr)) if !arr.is_empty() => {
555 let opts: Vec<String> = arr
556 .iter()
557 .filter_map(|o| match o {
558 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
559 Object::Name(n) => Some(n.clone()),
560 Object::Array(inner) if !inner.is_empty() => {
561 inner.first().and_then(|first| match first {
563 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
564 _ => None,
565 })
566 },
567 _ => None,
568 })
569 .collect();
570 if opts.is_empty() {
571 None
572 } else {
573 Some(opts)
574 }
575 },
576 _ => None,
577 }
578 }
579
580 fn parse_number_array(obj: Option<&Object>) -> Option<Vec<f64>> {
582 match obj {
583 Some(Object::Array(arr)) if !arr.is_empty() => {
584 let nums: Vec<f64> = arr
585 .iter()
586 .filter_map(|o| match o {
587 Object::Integer(n) => Some(*n as f64),
588 Object::Real(f) => Some(*f),
589 _ => None,
590 })
591 .collect();
592 if nums.is_empty() {
593 None
594 } else {
595 Some(nums)
596 }
597 },
598 _ => None,
599 }
600 }
601
602 fn parse_quad_points(obj: Option<&Object>) -> Option<Vec<[f64; 8]>> {
606 match obj {
607 Some(Object::Array(arr)) if arr.len() >= 8 => {
608 let nums: Vec<f64> = arr
609 .iter()
610 .filter_map(|o| match o {
611 Object::Integer(n) => Some(*n as f64),
612 Object::Real(f) => Some(*f),
613 _ => None,
614 })
615 .collect();
616
617 let quads: Vec<[f64; 8]> = nums
619 .chunks_exact(8)
620 .map(|chunk| {
621 let mut quad = [0.0; 8];
622 quad.copy_from_slice(chunk);
623 quad
624 })
625 .collect();
626
627 if quads.is_empty() {
628 None
629 } else {
630 Some(quads)
631 }
632 },
633 _ => None,
634 }
635 }
636
637 fn parse_destination(&self, dest_obj: &Object) -> Result<LinkDestination> {
641 match dest_obj {
642 Object::String(s) => Ok(LinkDestination::Named(String::from_utf8_lossy(s).to_string())),
644 Object::Name(n) => Ok(LinkDestination::Named(n.clone())),
645 Object::Array(arr) if !arr.is_empty() => {
647 let page = match &arr[0] {
649 Object::Integer(n) => *n as u32,
650 Object::Reference(r) => {
651 r.id
654 },
655 _ => 0,
656 };
657
658 let fit_type = if arr.len() > 1 {
660 arr[1].as_name().unwrap_or("Fit").to_string()
661 } else {
662 "Fit".to_string()
663 };
664
665 let params: Vec<f32> = arr
667 .iter()
668 .skip(2)
669 .filter_map(|obj| match obj {
670 Object::Integer(i) => Some(*i as f32),
671 Object::Real(r) => Some(*r as f32),
672 _ => None,
673 })
674 .collect();
675
676 Ok(LinkDestination::Explicit {
677 page,
678 fit_type,
679 params,
680 })
681 },
682 Object::Reference(r) => {
683 let dest_loaded = self.load_object(*r)?;
685 self.parse_destination(&dest_loaded)
686 },
687 _ => Err(crate::error::Error::InvalidPdf("Invalid destination format".to_string())),
688 }
689 }
690
691 fn parse_action(&self, action_obj: &Object) -> Result<LinkAction> {
695 let action = if let Object::Reference(r) = action_obj {
697 self.load_object(*r)?
698 } else {
699 action_obj.clone()
700 };
701
702 let dict = action.as_dict().ok_or_else(|| {
703 crate::error::Error::InvalidPdf("Action is not a dictionary".to_string())
704 })?;
705
706 let action_type = dict.get("S").and_then(|s| s.as_name()).ok_or_else(|| {
708 crate::error::Error::InvalidPdf("Action missing /S field".to_string())
709 })?;
710
711 match action_type {
712 "URI" => {
713 let uri = dict
715 .get("URI")
716 .and_then(|u| match u {
717 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
718 _ => None,
719 })
720 .ok_or_else(|| {
721 crate::error::Error::InvalidPdf("URI action missing /URI field".to_string())
722 })?;
723
724 Ok(LinkAction::Uri(uri))
725 },
726 "GoTo" => {
727 let dest_obj = dict.get("D").ok_or_else(|| {
729 crate::error::Error::InvalidPdf("GoTo action missing /D field".to_string())
730 })?;
731
732 let destination = self.parse_destination(dest_obj)?;
733 Ok(LinkAction::GoTo(destination))
734 },
735 "GoToR" => {
736 let file = dict
738 .get("F")
739 .and_then(|f| match f {
740 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
741 Object::Dictionary(d) => {
742 d.get("F").and_then(|f| match f {
744 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
745 _ => None,
746 })
747 },
748 _ => None,
749 })
750 .ok_or_else(|| {
751 crate::error::Error::InvalidPdf("GoToR action missing /F field".to_string())
752 })?;
753
754 let destination = dict.get("D").and_then(|d| self.parse_destination(d).ok());
755
756 Ok(LinkAction::GoToRemote { file, destination })
757 },
758 other => {
759 Ok(LinkAction::Other {
761 action_type: other.to_string(),
762 })
763 },
764 }
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771
772 #[test]
773 fn test_annotation_creation() {
774 let annot = Annotation {
775 annotation_type: "Annot".to_string(),
776 subtype: Some("Text".to_string()),
777 subtype_enum: AnnotationSubtype::Text,
778 contents: Some("This is a comment".to_string()),
779 rect: Some([100.0, 200.0, 150.0, 250.0]),
780 author: Some("John Doe".to_string()),
781 creation_date: Some("D:20231030120000".to_string()),
782 modification_date: None,
783 subject: Some("Review".to_string()),
784 destination: None,
785 action: None,
786 quad_points: None,
787 color: None,
788 opacity: None,
789 flags: AnnotationFlags::empty(),
790 border: None,
791 interior_color: None,
792 field_type: None,
793 field_name: None,
794 field_value: None,
795 default_value: None,
796 field_flags: None,
797 options: None,
798 appearance_state: None,
799 raw_dict: None,
800 };
801
802 assert_eq!(annot.annotation_type, "Annot");
803 assert_eq!(annot.subtype, Some("Text".to_string()));
804 assert_eq!(annot.subtype_enum, AnnotationSubtype::Text);
805 assert_eq!(annot.contents, Some("This is a comment".to_string()));
806 assert!(annot.rect.is_some());
807 }
808
809 #[test]
810 fn test_highlight_annotation() {
811 let annot = Annotation {
812 annotation_type: "Annot".to_string(),
813 subtype: Some("Highlight".to_string()),
814 subtype_enum: AnnotationSubtype::Highlight,
815 contents: Some("Highlighted text".to_string()),
816 rect: Some([100.0, 700.0, 200.0, 720.0]),
817 author: Some("Reviewer".to_string()),
818 creation_date: None,
819 modification_date: None,
820 subject: None,
821 destination: None,
822 action: None,
823 quad_points: Some(vec![[100.0, 700.0, 200.0, 700.0, 200.0, 720.0, 100.0, 720.0]]),
824 color: Some(vec![1.0, 1.0, 0.0]), opacity: Some(0.5),
826 flags: AnnotationFlags::printable(),
827 border: None,
828 interior_color: None,
829 field_type: None,
830 field_name: None,
831 field_value: None,
832 default_value: None,
833 field_flags: None,
834 options: None,
835 appearance_state: None,
836 raw_dict: None,
837 };
838
839 assert!(annot.subtype_enum.is_text_markup());
840 assert!(annot.quad_points.is_some());
841 assert_eq!(annot.quad_points.as_ref().unwrap().len(), 1);
842 assert_eq!(annot.color, Some(vec![1.0, 1.0, 0.0]));
843 assert_eq!(annot.opacity, Some(0.5));
844 assert!(annot.flags.is_printable());
845 }
846
847 #[test]
848 fn test_parse_number_array() {
849 use crate::object::Object;
850
851 let arr = vec![Object::Real(1.0), Object::Real(0.5), Object::Real(0.0)];
853 let result = PdfDocument::parse_number_array(Some(&Object::Array(arr)));
854 assert_eq!(result, Some(vec![1.0, 0.5, 0.0]));
855
856 let arr2 = vec![Object::Integer(1), Object::Real(0.5)];
858 let result2 = PdfDocument::parse_number_array(Some(&Object::Array(arr2)));
859 assert_eq!(result2, Some(vec![1.0, 0.5]));
860
861 let result3 = PdfDocument::parse_number_array(None);
863 assert!(result3.is_none());
864 }
865
866 #[test]
867 fn test_parse_quad_points() {
868 use crate::object::Object;
869
870 let arr: Vec<Object> = vec![
872 Object::Real(100.0),
873 Object::Real(700.0),
874 Object::Real(200.0),
875 Object::Real(700.0),
876 Object::Real(200.0),
877 Object::Real(720.0),
878 Object::Real(100.0),
879 Object::Real(720.0),
880 ];
881 let result = PdfDocument::parse_quad_points(Some(&Object::Array(arr)));
882 assert!(result.is_some());
883 let quads = result.unwrap();
884 assert_eq!(quads.len(), 1);
885 assert_eq!(quads[0][0], 100.0);
886 assert_eq!(quads[0][6], 100.0);
887 }
888
889 #[test]
890 fn test_widget_text_field_annotation() {
891 let annot = Annotation {
892 annotation_type: "Annot".to_string(),
893 subtype: Some("Widget".to_string()),
894 subtype_enum: AnnotationSubtype::Widget,
895 contents: None,
896 rect: Some([100.0, 700.0, 300.0, 720.0]),
897 author: None,
898 creation_date: None,
899 modification_date: None,
900 subject: None,
901 destination: None,
902 action: None,
903 quad_points: None,
904 color: None,
905 opacity: None,
906 flags: AnnotationFlags::empty(),
907 border: None,
908 interior_color: None,
909 field_type: Some(WidgetFieldType::Text),
910 field_name: Some("FirstName".to_string()),
911 field_value: Some("John".to_string()),
912 default_value: None,
913 field_flags: None,
914 options: None,
915 appearance_state: None,
916 raw_dict: None,
917 };
918
919 assert_eq!(annot.subtype_enum, AnnotationSubtype::Widget);
920 assert_eq!(annot.field_type, Some(WidgetFieldType::Text));
921 assert_eq!(annot.field_name, Some("FirstName".to_string()));
922 assert_eq!(annot.field_value, Some("John".to_string()));
923 }
924
925 #[test]
926 fn test_widget_checkbox_annotation() {
927 let annot = Annotation {
928 annotation_type: "Annot".to_string(),
929 subtype: Some("Widget".to_string()),
930 subtype_enum: AnnotationSubtype::Widget,
931 contents: None,
932 rect: Some([100.0, 600.0, 120.0, 620.0]),
933 author: None,
934 creation_date: None,
935 modification_date: None,
936 subject: None,
937 destination: None,
938 action: None,
939 quad_points: None,
940 color: None,
941 opacity: None,
942 flags: AnnotationFlags::empty(),
943 border: None,
944 interior_color: None,
945 field_type: Some(WidgetFieldType::Checkbox { checked: true }),
946 field_name: Some("AcceptTerms".to_string()),
947 field_value: Some("Yes".to_string()),
948 default_value: None,
949 field_flags: None,
950 options: None,
951 appearance_state: Some("Yes".to_string()),
952 raw_dict: None,
953 };
954
955 assert_eq!(annot.subtype_enum, AnnotationSubtype::Widget);
956 match &annot.field_type {
957 Some(WidgetFieldType::Checkbox { checked }) => assert!(*checked),
958 _ => panic!("Expected Checkbox field type"),
959 }
960 assert_eq!(annot.appearance_state, Some("Yes".to_string()));
961 }
962
963 #[test]
964 fn test_widget_choice_annotation() {
965 let annot = Annotation {
966 annotation_type: "Annot".to_string(),
967 subtype: Some("Widget".to_string()),
968 subtype_enum: AnnotationSubtype::Widget,
969 contents: None,
970 rect: Some([100.0, 500.0, 250.0, 520.0]),
971 author: None,
972 creation_date: None,
973 modification_date: None,
974 subject: None,
975 destination: None,
976 action: None,
977 quad_points: None,
978 color: None,
979 opacity: None,
980 flags: AnnotationFlags::empty(),
981 border: None,
982 interior_color: None,
983 field_type: Some(WidgetFieldType::Choice {
984 options: vec![
985 "Option A".to_string(),
986 "Option B".to_string(),
987 "Option C".to_string(),
988 ],
989 selected: Some("Option B".to_string()),
990 }),
991 field_name: Some("Selection".to_string()),
992 field_value: Some("Option B".to_string()),
993 default_value: Some("Option A".to_string()),
994 field_flags: None,
995 options: Some(vec![
996 "Option A".to_string(),
997 "Option B".to_string(),
998 "Option C".to_string(),
999 ]),
1000 appearance_state: None,
1001 raw_dict: None,
1002 };
1003
1004 assert_eq!(annot.subtype_enum, AnnotationSubtype::Widget);
1005 match &annot.field_type {
1006 Some(WidgetFieldType::Choice { options, selected }) => {
1007 assert_eq!(options.len(), 3);
1008 assert_eq!(selected, &Some("Option B".to_string()));
1009 },
1010 _ => panic!("Expected Choice field type"),
1011 }
1012 assert_eq!(annot.options.as_ref().unwrap().len(), 3);
1013 }
1014
1015 #[test]
1016 fn test_widget_field_type_default() {
1017 assert_eq!(WidgetFieldType::default(), WidgetFieldType::Text);
1018 }
1019
1020 #[test]
1021 fn test_parse_string_value() {
1022 assert_eq!(
1023 PdfDocument::parse_string_value(Some(&Object::String(b"Hello".to_vec()))),
1024 Some("Hello".to_string())
1025 );
1026 assert_eq!(
1027 PdfDocument::parse_string_value(Some(&Object::Name("MyName".to_string()))),
1028 Some("MyName".to_string())
1029 );
1030 assert_eq!(
1031 PdfDocument::parse_string_value(Some(&Object::Integer(42))),
1032 Some("42".to_string())
1033 );
1034 assert_eq!(PdfDocument::parse_string_value(None), None);
1035 }
1036
1037 #[test]
1038 fn test_parse_options_array() {
1039 let arr = vec![
1040 Object::String(b"Option 1".to_vec()),
1041 Object::String(b"Option 2".to_vec()),
1042 ];
1043 let result = PdfDocument::parse_options_array(Some(&Object::Array(arr)));
1044 assert!(result.is_some());
1045 let opts = result.unwrap();
1046 assert_eq!(opts.len(), 2);
1047 assert_eq!(opts[0], "Option 1");
1048 assert_eq!(opts[1], "Option 2");
1049
1050 let empty: Vec<Object> = vec![];
1052 assert!(PdfDocument::parse_options_array(Some(&Object::Array(empty))).is_none());
1053
1054 assert!(PdfDocument::parse_options_array(None).is_none());
1056 }
1057}