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>,
82
83 pub field_name: Option<String>,
86
87 pub field_value: Option<String>,
89
90 pub default_value: Option<String>,
92
93 pub field_flags: Option<u32>,
95
96 pub options: Option<Vec<String>>,
98
99 pub appearance_state: Option<String>,
101
102 pub raw_dict: Option<std::collections::HashMap<String, crate::object::Object>>,
108}
109
110#[derive(Debug, Clone, PartialEq)]
114pub enum LinkDestination {
115 Named(String),
117 Explicit {
119 page: u32,
121 fit_type: String,
123 params: Vec<f32>,
125 },
126}
127
128#[derive(Debug, Clone, PartialEq)]
132pub enum LinkAction {
133 Uri(String),
135 GoTo(LinkDestination),
137 GoToRemote {
139 file: String,
141 destination: Option<LinkDestination>,
143 },
144 Other {
146 action_type: String,
148 },
149}
150
151struct FlatAnnotationFields {
152 annotation_type: String,
153 subtype: Option<String>,
154 subtype_enum: AnnotationSubtype,
155 contents: Option<String>,
156 rect: Option<[f64; 4]>,
157 author: Option<String>,
158 creation_date: Option<String>,
159 modification_date: Option<String>,
160 subject: Option<String>,
161 flags: AnnotationFlags,
162 color: Option<Vec<f64>>,
163 opacity: Option<f64>,
164 border: Option<[f64; 3]>,
165 interior_color: Option<Vec<f64>>,
166}
167
168impl PdfDocument {
169 pub fn get_annotations(&self, page_index: usize) -> Result<Vec<Annotation>> {
199 let page_ref = self.get_page_ref(page_index)?;
200 let page_obj = self.load_object(page_ref)?;
201
202 let annots = match page_obj.as_dict() {
203 Some(dict) => match dict.get("Annots") {
204 Some(Object::Array(arr)) => arr.clone(),
205 Some(Object::Reference(annot_ref)) => match self.load_object(*annot_ref)? {
206 Object::Array(arr) => arr,
207 _ => return Ok(Vec::new()),
208 },
209 _ => return Ok(Vec::new()),
210 },
211 None => return Ok(Vec::new()),
212 };
213
214 let mut result = Vec::new();
215
216 for annot_obj in annots {
217 let annot_ref = match annot_obj {
218 Object::Reference(r) => r,
219 _ => continue,
220 };
221
222 if let Ok(annotation) = self.parse_annotation(annot_ref) {
223 result.push(annotation);
224 }
225 }
226
227 Ok(result)
228 }
229
230 fn parse_annotation(&self, annot_ref: crate::object::ObjectRef) -> Result<Annotation> {
232 let annot_obj = self.load_object(annot_ref)?;
233
234 let dict = annot_obj
235 .as_dict()
236 .ok_or_else(|| crate::error::Error::InvalidPdf("Annotation is not a dictionary".to_string()))?;
237
238 let f = Self::parse_flat_annotation_fields(dict);
239 let quad_points = if f.subtype_enum.is_text_markup() {
240 Self::parse_quad_points(dict.get("QuadPoints"))
241 } else {
242 None
243 };
244 let (destination, action) = self.parse_link_destination_and_action(dict, f.subtype_enum);
245 let (field_type, field_name, field_value, default_value, field_flags, options, appearance_state) =
246 if f.subtype_enum == AnnotationSubtype::Widget {
247 self.parse_widget_fields(dict)
248 } else {
249 (None, None, None, None, None, None, None)
250 };
251
252 Ok(Annotation {
253 annotation_type: f.annotation_type,
254 subtype: f.subtype,
255 subtype_enum: f.subtype_enum,
256 contents: f.contents,
257 rect: f.rect,
258 author: f.author,
259 creation_date: f.creation_date,
260 modification_date: f.modification_date,
261 subject: f.subject,
262 destination,
263 action,
264 quad_points,
265 color: f.color,
266 opacity: f.opacity,
267 flags: f.flags,
268 border: f.border,
269 interior_color: f.interior_color,
270 field_type,
271 field_name,
272 field_value,
273 default_value,
274 field_flags,
275 options,
276 appearance_state,
277 raw_dict: Some(dict.clone()),
278 })
279 }
280
281 fn parse_flat_annotation_fields(dict: &std::collections::HashMap<String, Object>) -> FlatAnnotationFields {
282 let annotation_type = dict
283 .get("Type")
284 .and_then(|t| t.as_name())
285 .unwrap_or("Unknown")
286 .to_string();
287 let subtype = dict.get("Subtype").and_then(|s| s.as_name()).map(|s| s.to_string());
288 let subtype_enum = subtype
289 .as_deref()
290 .map(AnnotationSubtype::from_pdf_name)
291 .unwrap_or(AnnotationSubtype::Unknown);
292 let contents = Self::pdf_string_field(dict, "Contents");
293 let rect = dict.get("Rect").and_then(|r| match r {
294 Object::Array(arr) if arr.len() == 4 => {
295 let mut rect_arr = [0.0; 4];
296 for (i, obj) in arr.iter().enumerate() {
297 rect_arr[i] = match obj {
298 Object::Integer(n) => *n as f64,
299 Object::Real(f) => *f,
300 _ => 0.0,
301 };
302 }
303 Some(rect_arr)
304 }
305 _ => None,
306 });
307 let author = Self::pdf_string_field(dict, "T");
308 let creation_date = Self::pdf_string_field(dict, "CreationDate");
309 let modification_date = Self::pdf_string_field(dict, "M");
310 let subject = Self::pdf_string_field(dict, "Subj");
311 let flags = dict
312 .get("F")
313 .and_then(|f| match f {
314 Object::Integer(n) => Some(AnnotationFlags::new(*n as u32)),
315 _ => None,
316 })
317 .unwrap_or_default();
318 let color = Self::parse_number_array(dict.get("C"));
319 let opacity = dict.get("CA").and_then(|o| match o {
320 Object::Real(f) => Some(*f),
321 Object::Integer(n) => Some(*n as f64),
322 _ => None,
323 });
324 let border = dict.get("Border").and_then(|b| match b {
325 Object::Array(arr) if arr.len() >= 3 => {
326 let mut border_arr = [0.0; 3];
327 for (i, obj) in arr.iter().take(3).enumerate() {
328 border_arr[i] = match obj {
329 Object::Integer(n) => *n as f64,
330 Object::Real(f) => *f,
331 _ => 0.0,
332 };
333 }
334 Some(border_arr)
335 }
336 _ => None,
337 });
338 let interior_color = Self::parse_number_array(dict.get("IC"));
339
340 FlatAnnotationFields {
341 annotation_type,
342 subtype,
343 subtype_enum,
344 contents,
345 rect,
346 author,
347 creation_date,
348 modification_date,
349 subject,
350 flags,
351 color,
352 opacity,
353 border,
354 interior_color,
355 }
356 }
357
358 fn parse_link_destination_and_action(
359 &self,
360 dict: &std::collections::HashMap<String, Object>,
361 subtype_enum: AnnotationSubtype,
362 ) -> (Option<LinkDestination>, Option<LinkAction>) {
363 if subtype_enum != AnnotationSubtype::Link {
364 return (None, None);
365 }
366 let dest = dict.get("Dest").and_then(|d| self.parse_destination(d).ok());
367 let act = dict.get("A").and_then(|a| self.parse_action(a).ok());
368 (dest, act)
369 }
370
371 fn parse_widget_fields(
375 &self,
376 dict: &std::collections::HashMap<String, Object>,
377 ) -> (
378 Option<WidgetFieldType>,
379 Option<String>,
380 Option<String>,
381 Option<String>,
382 Option<u32>,
383 Option<Vec<String>>,
384 Option<String>,
385 ) {
386 let mut ft = dict.get("FT").and_then(|f| f.as_name()).map(|s| s.to_string());
387 let mut field_flags = dict.get("Ff").and_then(|f| match f {
388 Object::Integer(n) => Some(*n as u32),
389 _ => None,
390 });
391 let mut field_value = Self::parse_string_value(dict.get("V"));
392 let mut default_value = Self::parse_string_value(dict.get("DV"));
393
394 self.inherit_widget_parent_fields(dict, &mut ft, &mut field_flags, &mut field_value, &mut default_value);
395
396 let field_name = Self::pdf_string_field(dict, "T");
397 let appearance_state = dict.get("AS").and_then(|a| a.as_name()).map(|s| s.to_string());
398 let options = Self::parse_options_array(dict.get("Opt"));
399
400 let field_type =
401 Self::resolve_widget_field_type(ft.as_deref(), field_flags, &appearance_state, &field_value, &options);
402
403 (
404 field_type,
405 field_name,
406 field_value,
407 default_value,
408 field_flags,
409 options,
410 appearance_state,
411 )
412 }
413
414 fn inherit_widget_parent_fields(
416 &self,
417 dict: &std::collections::HashMap<String, Object>,
418 ft: &mut Option<String>,
419 field_flags: &mut Option<u32>,
420 field_value: &mut Option<String>,
421 default_value: &mut Option<String>,
422 ) {
423 if ft.is_some() && field_flags.is_some() && field_value.is_some() && default_value.is_some() {
424 return;
425 }
426 let mut parent_ref = dict
427 .get("Parent")
428 .and_then(|p| if let Object::Reference(r) = p { Some(*r) } else { None });
429 let mut depth = 0;
430 while let Some(pref) = parent_ref {
431 if depth >= 10 {
432 break;
433 }
434 depth += 1;
435 let Ok(parent_obj) = self.load_object(pref) else {
436 break;
437 };
438 let Some(parent_dict) = parent_obj.as_dict() else {
439 break;
440 };
441 if ft.is_none() {
442 *ft = parent_dict.get("FT").and_then(|f| f.as_name()).map(|s| s.to_string());
443 }
444 if field_flags.is_none() {
445 *field_flags = parent_dict.get("Ff").and_then(|f| match f {
446 Object::Integer(n) => Some(*n as u32),
447 _ => None,
448 });
449 }
450 if field_value.is_none() {
451 *field_value = Self::parse_string_value(parent_dict.get("V"));
452 }
453 if default_value.is_none() {
454 *default_value = Self::parse_string_value(parent_dict.get("DV"));
455 }
456 parent_ref = parent_dict
457 .get("Parent")
458 .and_then(|p| if let Object::Reference(r) = p { Some(*r) } else { None });
459 }
460 }
461
462 fn resolve_widget_field_type(
463 ft: Option<&str>,
464 field_flags: Option<u32>,
465 appearance_state: &Option<String>,
466 field_value: &Option<String>,
467 options: &Option<Vec<String>>,
468 ) -> Option<WidgetFieldType> {
469 match ft {
470 Some("Tx") => Some(WidgetFieldType::Text),
471 Some("Btn") => {
472 let ff = field_flags.unwrap_or(0);
473 if ff & 0x10000 != 0 {
476 Some(WidgetFieldType::Radio {
477 selected: appearance_state.clone(),
478 })
479 } else if ff & 0x8000 != 0 {
480 Some(WidgetFieldType::Button)
481 } else {
482 let checked = appearance_state
483 .as_deref()
484 .map(|s| s != "Off" && !s.is_empty())
485 .unwrap_or(false);
486 Some(WidgetFieldType::Checkbox { checked })
487 }
488 }
489 Some("Ch") => Some(WidgetFieldType::Choice {
490 options: options.clone().unwrap_or_default(),
491 selected: field_value.clone(),
492 }),
493 Some("Sig") => Some(WidgetFieldType::Signature),
494 _ => None,
495 }
496 }
497
498 fn pdf_string_field(dict: &std::collections::HashMap<String, Object>, key: &str) -> Option<String> {
499 dict.get(key).and_then(|v| match v {
500 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
501 _ => None,
502 })
503 }
504
505 fn parse_string_value(obj: Option<&Object>) -> Option<String> {
507 match obj {
508 Some(Object::String(s)) => Some(String::from_utf8_lossy(s).to_string()),
509 Some(Object::Name(n)) => Some(n.clone()),
510 Some(Object::Integer(i)) => Some(i.to_string()),
511 Some(Object::Real(f)) => Some(f.to_string()),
512 _ => None,
513 }
514 }
515
516 fn parse_options_array(obj: Option<&Object>) -> Option<Vec<String>> {
518 match obj {
519 Some(Object::Array(arr)) if !arr.is_empty() => {
520 let opts: Vec<String> = arr
521 .iter()
522 .filter_map(|o| match o {
523 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
524 Object::Name(n) => Some(n.clone()),
525 Object::Array(inner) if !inner.is_empty() => {
526 inner.first().and_then(|first| match first {
528 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
529 _ => None,
530 })
531 }
532 _ => None,
533 })
534 .collect();
535 if opts.is_empty() { None } else { Some(opts) }
536 }
537 _ => None,
538 }
539 }
540
541 fn parse_number_array(obj: Option<&Object>) -> Option<Vec<f64>> {
543 match obj {
544 Some(Object::Array(arr)) if !arr.is_empty() => {
545 let nums: Vec<f64> = arr
546 .iter()
547 .filter_map(|o| match o {
548 Object::Integer(n) => Some(*n as f64),
549 Object::Real(f) => Some(*f),
550 _ => None,
551 })
552 .collect();
553 if nums.is_empty() { None } else { Some(nums) }
554 }
555 _ => None,
556 }
557 }
558
559 fn parse_quad_points(obj: Option<&Object>) -> Option<Vec<[f64; 8]>> {
563 match obj {
564 Some(Object::Array(arr)) if arr.len() >= 8 => {
565 let nums: Vec<f64> = arr
566 .iter()
567 .filter_map(|o| match o {
568 Object::Integer(n) => Some(*n as f64),
569 Object::Real(f) => Some(*f),
570 _ => None,
571 })
572 .collect();
573
574 let quads: Vec<[f64; 8]> = nums
575 .as_chunks::<8>()
576 .0
577 .iter()
578 .map(|chunk| {
579 let mut quad = [0.0; 8];
580 quad.copy_from_slice(chunk);
581 quad
582 })
583 .collect();
584
585 if quads.is_empty() { None } else { Some(quads) }
586 }
587 _ => None,
588 }
589 }
590
591 fn parse_destination(&self, dest_obj: &Object) -> Result<LinkDestination> {
595 match dest_obj {
596 Object::String(s) => Ok(LinkDestination::Named(String::from_utf8_lossy(s).to_string())),
597 Object::Name(n) => Ok(LinkDestination::Named(n.clone())),
598 Object::Array(arr) if !arr.is_empty() => {
599 let page = match &arr[0] {
600 Object::Integer(n) => *n as u32,
601 Object::Reference(r) => {
602 self.page_index_of_ref(*r)? as u32
606 }
607 _ => 0,
608 };
609
610 let fit_type = if arr.len() > 1 {
611 arr[1].as_name().unwrap_or("Fit").to_string()
612 } else {
613 "Fit".to_string()
614 };
615
616 let params: Vec<f32> = arr
617 .iter()
618 .skip(2)
619 .filter_map(|obj| match obj {
620 Object::Integer(i) => Some(*i as f32),
621 Object::Real(r) => Some(*r as f32),
622 _ => None,
623 })
624 .collect();
625
626 Ok(LinkDestination::Explicit { page, fit_type, params })
627 }
628 Object::Reference(r) => {
629 let dest_loaded = self.load_object(*r)?;
630 self.parse_destination(&dest_loaded)
631 }
632 _ => Err(crate::error::Error::InvalidPdf(
633 "Invalid destination format".to_string(),
634 )),
635 }
636 }
637
638 fn parse_action(&self, action_obj: &Object) -> Result<LinkAction> {
642 let action = if let Object::Reference(r) = action_obj {
643 self.load_object(*r)?
644 } else {
645 action_obj.clone()
646 };
647
648 let dict = action
649 .as_dict()
650 .ok_or_else(|| crate::error::Error::InvalidPdf("Action is not a dictionary".to_string()))?;
651
652 let action_type = dict
653 .get("S")
654 .and_then(|s| s.as_name())
655 .ok_or_else(|| crate::error::Error::InvalidPdf("Action missing /S field".to_string()))?;
656
657 match action_type {
658 "URI" => {
659 let uri = dict
660 .get("URI")
661 .and_then(|u| match u {
662 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
663 _ => None,
664 })
665 .ok_or_else(|| crate::error::Error::InvalidPdf("URI action missing /URI field".to_string()))?;
666
667 Ok(LinkAction::Uri(uri))
668 }
669 "GoTo" => {
670 let dest_obj = dict
671 .get("D")
672 .ok_or_else(|| crate::error::Error::InvalidPdf("GoTo action missing /D field".to_string()))?;
673
674 match self.parse_destination(dest_obj) {
679 Ok(destination) => Ok(LinkAction::GoTo(destination)),
680 Err(error) => {
681 tracing::warn!(
682 operation = "resolve_goto_destination",
683 error_code = error.telemetry_code(),
684 error_offset = ?error.telemetry_offset(),
685 "GoTo destination unresolvable"
686 );
687 Ok(LinkAction::Other {
688 action_type: "GoTo".to_string(),
689 })
690 }
691 }
692 }
693 "GoToR" => {
694 let file = dict
695 .get("F")
696 .and_then(|f| match f {
697 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
698 Object::Dictionary(d) => d.get("F").and_then(|f| match f {
699 Object::String(s) => Some(String::from_utf8_lossy(s).to_string()),
700 _ => None,
701 }),
702 _ => None,
703 })
704 .ok_or_else(|| crate::error::Error::InvalidPdf("GoToR action missing /F field".to_string()))?;
705
706 let destination = dict.get("D").and_then(|d| self.parse_destination(d).ok());
707
708 Ok(LinkAction::GoToRemote { file, destination })
709 }
710 other => Ok(LinkAction::Other {
711 action_type: other.to_string(),
712 }),
713 }
714 }
715}
716
717#[cfg(test)]
718mod tests {
719 use super::*;
720
721 #[test]
722 fn test_annotation_creation() {
723 let annot = Annotation {
724 annotation_type: "Annot".to_string(),
725 subtype: Some("Text".to_string()),
726 subtype_enum: AnnotationSubtype::Text,
727 contents: Some("This is a comment".to_string()),
728 rect: Some([100.0, 200.0, 150.0, 250.0]),
729 author: Some("John Doe".to_string()),
730 creation_date: Some("D:20231030120000".to_string()),
731 modification_date: None,
732 subject: Some("Review".to_string()),
733 destination: None,
734 action: None,
735 quad_points: None,
736 color: None,
737 opacity: None,
738 flags: AnnotationFlags::empty(),
739 border: None,
740 interior_color: None,
741 field_type: None,
742 field_name: None,
743 field_value: None,
744 default_value: None,
745 field_flags: None,
746 options: None,
747 appearance_state: None,
748 raw_dict: None,
749 };
750
751 assert_eq!(annot.annotation_type, "Annot");
752 assert_eq!(annot.subtype, Some("Text".to_string()));
753 assert_eq!(annot.subtype_enum, AnnotationSubtype::Text);
754 assert_eq!(annot.contents, Some("This is a comment".to_string()));
755 assert!(annot.rect.is_some());
756 }
757
758 #[test]
759 fn test_highlight_annotation() {
760 let annot = Annotation {
761 annotation_type: "Annot".to_string(),
762 subtype: Some("Highlight".to_string()),
763 subtype_enum: AnnotationSubtype::Highlight,
764 contents: Some("Highlighted text".to_string()),
765 rect: Some([100.0, 700.0, 200.0, 720.0]),
766 author: Some("Reviewer".to_string()),
767 creation_date: None,
768 modification_date: None,
769 subject: None,
770 destination: None,
771 action: None,
772 quad_points: Some(vec![[100.0, 700.0, 200.0, 700.0, 200.0, 720.0, 100.0, 720.0]]),
773 color: Some(vec![1.0, 1.0, 0.0]),
774 opacity: Some(0.5),
775 flags: AnnotationFlags::printable(),
776 border: None,
777 interior_color: None,
778 field_type: None,
779 field_name: None,
780 field_value: None,
781 default_value: None,
782 field_flags: None,
783 options: None,
784 appearance_state: None,
785 raw_dict: None,
786 };
787
788 assert!(annot.subtype_enum.is_text_markup());
789 assert!(annot.quad_points.is_some());
790 assert_eq!(annot.quad_points.as_ref().unwrap().len(), 1);
791 assert_eq!(annot.color, Some(vec![1.0, 1.0, 0.0]));
792 assert_eq!(annot.opacity, Some(0.5));
793 assert!(annot.flags.is_printable());
794 }
795
796 #[test]
797 fn test_parse_number_array() {
798 use crate::object::Object;
799
800 let arr = vec![Object::Real(1.0), Object::Real(0.5), Object::Real(0.0)];
801 let result = PdfDocument::parse_number_array(Some(&Object::Array(arr)));
802 assert_eq!(result, Some(vec![1.0, 0.5, 0.0]));
803
804 let arr2 = vec![Object::Integer(1), Object::Real(0.5)];
805 let result2 = PdfDocument::parse_number_array(Some(&Object::Array(arr2)));
806 assert_eq!(result2, Some(vec![1.0, 0.5]));
807
808 let result3 = PdfDocument::parse_number_array(None);
809 assert!(result3.is_none());
810 }
811
812 #[test]
813 fn test_parse_quad_points() {
814 use crate::object::Object;
815
816 let arr: Vec<Object> = vec![
817 Object::Real(100.0),
818 Object::Real(700.0),
819 Object::Real(200.0),
820 Object::Real(700.0),
821 Object::Real(200.0),
822 Object::Real(720.0),
823 Object::Real(100.0),
824 Object::Real(720.0),
825 ];
826 let result = PdfDocument::parse_quad_points(Some(&Object::Array(arr)));
827 assert!(result.is_some());
828 let quads = result.unwrap();
829 assert_eq!(quads.len(), 1);
830 assert_eq!(quads[0][0], 100.0);
831 assert_eq!(quads[0][6], 100.0);
832 }
833
834 #[test]
835 fn test_widget_text_field_annotation() {
836 let annot = Annotation {
837 annotation_type: "Annot".to_string(),
838 subtype: Some("Widget".to_string()),
839 subtype_enum: AnnotationSubtype::Widget,
840 contents: None,
841 rect: Some([100.0, 700.0, 300.0, 720.0]),
842 author: None,
843 creation_date: None,
844 modification_date: None,
845 subject: None,
846 destination: None,
847 action: None,
848 quad_points: None,
849 color: None,
850 opacity: None,
851 flags: AnnotationFlags::empty(),
852 border: None,
853 interior_color: None,
854 field_type: Some(WidgetFieldType::Text),
855 field_name: Some("FirstName".to_string()),
856 field_value: Some("John".to_string()),
857 default_value: None,
858 field_flags: None,
859 options: None,
860 appearance_state: None,
861 raw_dict: None,
862 };
863
864 assert_eq!(annot.subtype_enum, AnnotationSubtype::Widget);
865 assert_eq!(annot.field_type, Some(WidgetFieldType::Text));
866 assert_eq!(annot.field_name, Some("FirstName".to_string()));
867 assert_eq!(annot.field_value, Some("John".to_string()));
868 }
869
870 #[test]
871 fn test_widget_checkbox_annotation() {
872 let annot = Annotation {
873 annotation_type: "Annot".to_string(),
874 subtype: Some("Widget".to_string()),
875 subtype_enum: AnnotationSubtype::Widget,
876 contents: None,
877 rect: Some([100.0, 600.0, 120.0, 620.0]),
878 author: None,
879 creation_date: None,
880 modification_date: None,
881 subject: None,
882 destination: None,
883 action: None,
884 quad_points: None,
885 color: None,
886 opacity: None,
887 flags: AnnotationFlags::empty(),
888 border: None,
889 interior_color: None,
890 field_type: Some(WidgetFieldType::Checkbox { checked: true }),
891 field_name: Some("AcceptTerms".to_string()),
892 field_value: Some("Yes".to_string()),
893 default_value: None,
894 field_flags: None,
895 options: None,
896 appearance_state: Some("Yes".to_string()),
897 raw_dict: None,
898 };
899
900 assert_eq!(annot.subtype_enum, AnnotationSubtype::Widget);
901 match &annot.field_type {
902 Some(WidgetFieldType::Checkbox { checked }) => assert!(*checked),
903 _ => panic!("Expected Checkbox field type"),
904 }
905 assert_eq!(annot.appearance_state, Some("Yes".to_string()));
906 }
907
908 #[test]
909 fn test_widget_choice_annotation() {
910 let annot = Annotation {
911 annotation_type: "Annot".to_string(),
912 subtype: Some("Widget".to_string()),
913 subtype_enum: AnnotationSubtype::Widget,
914 contents: None,
915 rect: Some([100.0, 500.0, 250.0, 520.0]),
916 author: None,
917 creation_date: None,
918 modification_date: None,
919 subject: None,
920 destination: None,
921 action: None,
922 quad_points: None,
923 color: None,
924 opacity: None,
925 flags: AnnotationFlags::empty(),
926 border: None,
927 interior_color: None,
928 field_type: Some(WidgetFieldType::Choice {
929 options: vec!["Option A".to_string(), "Option B".to_string(), "Option C".to_string()],
930 selected: Some("Option B".to_string()),
931 }),
932 field_name: Some("Selection".to_string()),
933 field_value: Some("Option B".to_string()),
934 default_value: Some("Option A".to_string()),
935 field_flags: None,
936 options: Some(vec![
937 "Option A".to_string(),
938 "Option B".to_string(),
939 "Option C".to_string(),
940 ]),
941 appearance_state: None,
942 raw_dict: None,
943 };
944
945 assert_eq!(annot.subtype_enum, AnnotationSubtype::Widget);
946 match &annot.field_type {
947 Some(WidgetFieldType::Choice { options, selected }) => {
948 assert_eq!(options.len(), 3);
949 assert_eq!(selected, &Some("Option B".to_string()));
950 }
951 _ => panic!("Expected Choice field type"),
952 }
953 assert_eq!(annot.options.as_ref().unwrap().len(), 3);
954 }
955
956 #[test]
957 fn test_widget_field_type_default() {
958 assert_eq!(WidgetFieldType::default(), WidgetFieldType::Text);
959 }
960
961 #[test]
962 fn test_parse_string_value() {
963 assert_eq!(
964 PdfDocument::parse_string_value(Some(&Object::String(b"Hello".to_vec()))),
965 Some("Hello".to_string())
966 );
967 assert_eq!(
968 PdfDocument::parse_string_value(Some(&Object::Name("MyName".to_string()))),
969 Some("MyName".to_string())
970 );
971 assert_eq!(
972 PdfDocument::parse_string_value(Some(&Object::Integer(42))),
973 Some("42".to_string())
974 );
975 assert_eq!(PdfDocument::parse_string_value(None), None);
976 }
977
978 #[test]
979 fn test_parse_options_array() {
980 let arr = vec![
981 Object::String(b"Option 1".to_vec()),
982 Object::String(b"Option 2".to_vec()),
983 ];
984 let result = PdfDocument::parse_options_array(Some(&Object::Array(arr)));
985 assert!(result.is_some());
986 let opts = result.unwrap();
987 assert_eq!(opts.len(), 2);
988 assert_eq!(opts[0], "Option 1");
989 assert_eq!(opts[1], "Option 2");
990
991 let empty: Vec<Object> = vec![];
992 assert!(PdfDocument::parse_options_array(Some(&Object::Array(empty))).is_none());
993
994 assert!(PdfDocument::parse_options_array(None).is_none());
995 }
996}