1use std::collections::HashMap;
12
13use rpdfium_core::{Name, PdfSource};
14use rpdfium_parser::{Object, ObjectStore};
15
16use crate::error::{DocError, DocResult};
17use crate::form_field::{FormField, FormFieldFlags, FormFieldType};
18use crate::variable_text::Alignment;
19
20const MAX_FIELDS: usize = 10_000;
22
23#[derive(Debug, Clone)]
25pub struct InteractiveForm {
26 pub fields: Vec<FormField>,
28 pub calculation_order: Vec<String>,
30 pub default_appearance: Option<String>,
32 pub default_alignment: Alignment,
34}
35
36impl InteractiveForm {
37 pub fn from_catalog<S: PdfSource>(
42 catalog: &Object,
43 store: &ObjectStore<S>,
44 ) -> DocResult<Option<Self>> {
45 let catalog_dict = store
46 .deep_resolve(catalog)
47 .map_err(|e| DocError::Parser(e.to_string()))?
48 .as_dict()
49 .ok_or(DocError::UnexpectedType)?;
50
51 let acroform_obj = match catalog_dict.get(&Name::acro_form()) {
52 Some(obj) => store
53 .deep_resolve(obj)
54 .map_err(|e| DocError::Parser(e.to_string()))?,
55 None => return Ok(None),
56 };
57
58 let acroform_dict = match acroform_obj.as_dict() {
59 Some(d) => d,
60 None => return Ok(None),
61 };
62
63 let calculation_order = parse_calculation_order(acroform_dict, store);
65
66 let default_appearance = acroform_dict
68 .get(&Name::da())
69 .and_then(|o| store.deep_resolve(o).ok())
70 .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
71
72 let default_alignment = acroform_dict
74 .get(&Name::q())
75 .and_then(|o| store.deep_resolve(o).ok())
76 .and_then(|o| o.as_i64())
77 .map(|v| Alignment::from_value(v as i32))
78 .unwrap_or(Alignment::Left);
79
80 let fields_obj = match acroform_dict.get(&Name::fields()) {
81 Some(obj) => store
82 .deep_resolve(obj)
83 .map_err(|e| DocError::Parser(e.to_string()))?,
84 None => {
85 return Ok(Some(InteractiveForm {
86 fields: Vec::new(),
87 calculation_order,
88 default_appearance: default_appearance.clone(),
89 default_alignment,
90 }));
91 }
92 };
93
94 let fields_arr = match fields_obj.as_array() {
95 Some(a) => a,
96 None => {
97 return Ok(Some(InteractiveForm {
98 fields: Vec::new(),
99 calculation_order,
100 default_appearance: default_appearance.clone(),
101 default_alignment,
102 }));
103 }
104 };
105
106 let mut result_fields: Vec<FormField> = Vec::new();
109 let mut total_count: usize = 0;
110
111 struct StackItem {
112 dict: HashMap<Name, Object>,
113 parent_ft: Option<String>,
114 parent_name: String,
115 path: Vec<usize>,
117 }
118
119 let mut stack: Vec<StackItem> = Vec::new();
120
121 for field_obj in fields_arr.iter().rev() {
123 let resolved = match store.deep_resolve(field_obj).ok() {
124 Some(o) => o,
125 None => continue,
126 };
127 if let Some(d) = resolved.as_dict() {
128 stack.push(StackItem {
129 dict: d.clone(),
130 parent_ft: None,
131 parent_name: String::new(),
132 path: Vec::new(),
133 });
134 }
135 }
136
137 while let Some(item) = stack.pop() {
138 if total_count >= MAX_FIELDS {
139 break;
140 }
141
142 let ft_str = item
144 .dict
145 .get(&Name::ft())
146 .and_then(|o| store.deep_resolve(o).ok())
147 .and_then(|o| o.as_name().map(|n| n.as_str().into_owned()));
148 let effective_ft = ft_str.as_deref().or(item.parent_ft.as_deref());
149
150 let partial_name = item
152 .dict
153 .get(&Name::t())
154 .and_then(|o| store.deep_resolve(o).ok())
155 .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
156 let current_name = match (&partial_name, item.parent_name.is_empty()) {
157 (Some(pn), true) => pn.clone(),
158 (Some(pn), false) => format!("{}.{pn}", item.parent_name),
159 (None, _) => item.parent_name.clone(),
160 };
161
162 let kids = item
164 .dict
165 .get(&Name::kids())
166 .and_then(|o| store.deep_resolve(o).ok())
167 .and_then(|o| o.as_array().map(|a| a.to_vec()));
168
169 if let Some(kids_arr) = kids {
170 let field = FormField::from_dict(
172 &item.dict,
173 store,
174 item.parent_ft.as_deref(),
175 &item.parent_name,
176 );
177
178 if let Some(mut f) = field {
179 let idx = insert_at_path(&mut result_fields, &item.path, &mut f);
182 total_count += 1;
183
184 let mut child_path = item.path.clone();
185 child_path.push(idx);
186
187 for kid_obj in kids_arr.iter().rev() {
188 let resolved = match store.deep_resolve(kid_obj).ok() {
189 Some(o) => o,
190 None => continue,
191 };
192 if let Some(d) = resolved.as_dict() {
193 stack.push(StackItem {
194 dict: d.clone(),
195 parent_ft: effective_ft.map(|s| s.to_string()),
196 parent_name: current_name.clone(),
197 path: child_path.clone(),
198 });
199 }
200 }
201 } else {
202 for kid_obj in kids_arr.iter().rev() {
204 let resolved = match store.deep_resolve(kid_obj).ok() {
205 Some(o) => o,
206 None => continue,
207 };
208 if let Some(d) = resolved.as_dict() {
209 stack.push(StackItem {
210 dict: d.clone(),
211 parent_ft: effective_ft.map(|s| s.to_string()),
212 parent_name: current_name.clone(),
213 path: item.path.clone(),
214 });
215 }
216 }
217 }
218 } else {
219 let field = FormField::from_dict(
221 &item.dict,
222 store,
223 item.parent_ft.as_deref(),
224 &item.parent_name,
225 );
226 if let Some(mut f) = field {
227 insert_at_path(&mut result_fields, &item.path, &mut f);
228 total_count += 1;
229 }
230 }
231 }
232
233 Ok(Some(InteractiveForm {
234 fields: result_fields,
235 calculation_order,
236 default_appearance,
237 default_alignment,
238 }))
239 }
240
241 pub fn calculation_order(&self) -> &[String] {
243 &self.calculation_order
244 }
245
246 pub fn all_fields(&self) -> Vec<&FormField> {
248 let mut result = Vec::new();
249 let mut stack: Vec<&FormField> = self.fields.iter().rev().collect();
250 while let Some(field) = stack.pop() {
251 result.push(field);
252 for child in field.children.iter().rev() {
253 stack.push(child);
254 }
255 }
256 result
257 }
258
259 pub fn field_by_name(&self, name: &str) -> Option<&FormField> {
261 let mut stack: Vec<&FormField> = self.fields.iter().rev().collect();
262 while let Some(field) = stack.pop() {
263 if field.name == name {
264 return Some(field);
265 }
266 for child in field.children.iter().rev() {
267 stack.push(child);
268 }
269 }
270 None
271 }
272
273 pub fn field_by_name_mut(&mut self, name: &str) -> Option<&mut FormField> {
275 let mut stack: Vec<&mut FormField> = self.fields.iter_mut().rev().collect();
276 while let Some(field) = stack.pop() {
277 if field.name == name {
278 return Some(field);
279 }
280 for child in field.children.iter_mut().rev() {
281 stack.push(child);
282 }
283 }
284 None
285 }
286
287 pub fn all_field_names(&self) -> Vec<String> {
291 self.all_fields().iter().map(|f| f.name.clone()).collect()
292 }
293
294 pub fn default_appearance(&self) -> Option<&str> {
296 self.default_appearance.as_deref()
297 }
298
299 #[inline]
303 pub fn get_default_appearance(&self) -> Option<&str> {
304 self.default_appearance()
305 }
306
307 pub fn default_alignment(&self) -> Alignment {
309 self.default_alignment
310 }
311
312 #[inline]
316 pub fn get_form_alignment(&self) -> Alignment {
317 self.default_alignment()
318 }
319
320 pub fn check_required_fields(&self) -> Vec<&str> {
325 let mut missing = Vec::new();
326 let all = self.all_fields();
327 for field in &all {
328 if field.flags.is_required() && field.value.is_none() {
329 missing.push(field.name.as_str());
330 }
331 }
332 missing
333 }
334
335 pub fn control_at_point(&self, x: f64, y: f64) -> Option<(&str, usize)> {
341 let all = self.all_fields();
342 for field in &all {
343 for (i, control) in field.controls.iter().enumerate() {
344 let r = &control.rect;
345 if x >= r.left && x <= r.right && y >= r.bottom && y <= r.top {
346 return Some((&field.name, i));
347 }
348 }
349 }
350 None
351 }
352
353 #[inline]
357 pub fn get_control_at_point(&self, x: f64, y: f64) -> Option<(&str, usize)> {
358 self.control_at_point(x, y)
359 }
360}
361
362fn parse_calculation_order<S: PdfSource>(
367 acroform_dict: &HashMap<Name, Object>,
368 store: &ObjectStore<S>,
369) -> Vec<String> {
370 let co_obj = match acroform_dict.get(&Name::co()) {
371 Some(o) => o,
372 None => return Vec::new(),
373 };
374 let resolved = match store.deep_resolve(co_obj).ok() {
375 Some(o) => o,
376 None => return Vec::new(),
377 };
378 let arr = match resolved.as_array() {
379 Some(a) => a,
380 None => return Vec::new(),
381 };
382
383 let mut order = Vec::new();
384 for item in arr {
385 let field_obj = match store.deep_resolve(item).ok() {
386 Some(o) => o,
387 None => continue,
388 };
389 if let Some(dict) = field_obj.as_dict() {
390 if let Some(t_obj) = dict.get(&Name::t()) {
391 if let Ok(resolved_t) = store.deep_resolve(t_obj) {
392 if let Some(s) = resolved_t.as_string() {
393 order.push(s.to_string_lossy());
394 }
395 }
396 }
397 }
398 }
399 order
400}
401
402fn insert_at_path(root: &mut Vec<FormField>, path: &[usize], field: &mut FormField) -> usize {
404 let container = get_children_at_path(root, path);
405 let idx = container.len();
406 let owned = std::mem::replace(
408 field,
409 FormField {
410 name: String::new(),
411 field_type: FormFieldType::Text,
412 value: None,
413 default_value: None,
414 flags: FormFieldFlags::from_bits(0),
415 tooltip: None,
416 alternate_name: None,
417 mapping_name: None,
418 max_len: None,
419 options: Vec::new(),
420 appearance_state: None,
421 children: Vec::new(),
422 controls: Vec::new(),
423 dirty: false,
424 selected_indices: Vec::new(),
425 additional_actions: None,
426 },
427 );
428 container.push(owned);
429 idx
430}
431
432fn get_children_at_path<'a>(
434 root: &'a mut Vec<FormField>,
435 path: &[usize],
436) -> &'a mut Vec<FormField> {
437 let mut current = root;
438 for &idx in path {
439 current = &mut current[idx].children;
440 }
441 current
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use rpdfium_core::PdfString;
448
449 fn build_store() -> ObjectStore<Vec<u8>> {
450 let pdf = build_minimal_pdf();
451 ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
452 }
453
454 fn build_minimal_pdf() -> Vec<u8> {
455 let mut pdf = Vec::new();
456 pdf.extend_from_slice(b"%PDF-1.4\n");
457 let obj1_offset = pdf.len();
458 pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
459 let obj2_offset = pdf.len();
460 pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
461 let xref_offset = pdf.len();
462 pdf.extend_from_slice(b"xref\n0 3\n");
463 pdf.extend_from_slice(b"0000000000 65535 f \r\n");
464 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
465 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
466 pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
467 pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
468 pdf
469 }
470
471 fn str_obj(s: &str) -> Object {
472 Object::String(PdfString::from_bytes(s.as_bytes().to_vec()))
473 }
474
475 fn make_field_dict(ft: &str, name: &str) -> HashMap<Name, Object> {
476 let mut dict = HashMap::new();
477 dict.insert(Name::ft(), Object::Name(Name::from(ft)));
478 dict.insert(Name::t(), str_obj(name));
479 dict
480 }
481
482 #[test]
483 fn test_no_acroform_returns_none() {
484 let store = build_store();
485 let mut catalog = HashMap::new();
486 catalog.insert(Name::r#type(), Object::Name(Name::from("Catalog")));
487 let obj = Object::Dictionary(catalog);
488 let result = InteractiveForm::from_catalog(&obj, &store).unwrap();
489 assert!(result.is_none());
490 }
491
492 #[test]
493 fn test_empty_fields_array() {
494 let store = build_store();
495 let mut acroform = HashMap::new();
496 acroform.insert(Name::fields(), Object::Array(Vec::new()));
497
498 let mut catalog = HashMap::new();
499 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
500 let obj = Object::Dictionary(catalog);
501
502 let form = InteractiveForm::from_catalog(&obj, &store)
503 .unwrap()
504 .unwrap();
505 assert!(form.fields.is_empty());
506 }
507
508 #[test]
509 fn test_single_text_field() {
510 let store = build_store();
511
512 let mut field_dict = make_field_dict("Tx", "username");
513 field_dict.insert(Name::v(), str_obj("alice"));
514
515 let mut acroform = HashMap::new();
516 acroform.insert(
517 Name::fields(),
518 Object::Array(vec![Object::Dictionary(field_dict)]),
519 );
520
521 let mut catalog = HashMap::new();
522 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
523 let obj = Object::Dictionary(catalog);
524
525 let form = InteractiveForm::from_catalog(&obj, &store)
526 .unwrap()
527 .unwrap();
528 assert_eq!(form.fields.len(), 1);
529 assert_eq!(form.fields[0].name, "username");
530 assert_eq!(form.fields[0].value.as_deref(), Some("alice"));
531 }
532
533 #[test]
534 fn test_hierarchical_fields_with_inherited_ft() {
535 let store = build_store();
536
537 let child1 = {
539 let mut d = HashMap::new();
540 d.insert(Name::t(), str_obj("city"));
541 d.insert(Name::v(), str_obj("Tokyo"));
542 Object::Dictionary(d)
543 };
544 let child2 = {
545 let mut d = HashMap::new();
546 d.insert(Name::t(), str_obj("zip"));
547 d.insert(Name::v(), str_obj("100-0001"));
548 Object::Dictionary(d)
549 };
550
551 let mut parent = HashMap::new();
552 parent.insert(Name::ft(), Object::Name(Name::from("Tx")));
553 parent.insert(Name::t(), str_obj("address"));
554 parent.insert(Name::kids(), Object::Array(vec![child1, child2]));
555
556 let mut acroform = HashMap::new();
557 acroform.insert(
558 Name::fields(),
559 Object::Array(vec![Object::Dictionary(parent)]),
560 );
561
562 let mut catalog = HashMap::new();
563 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
564 let obj = Object::Dictionary(catalog);
565
566 let form = InteractiveForm::from_catalog(&obj, &store)
567 .unwrap()
568 .unwrap();
569
570 let all = form.all_fields();
572 let leaf_fields: Vec<_> = all
574 .iter()
575 .filter(|f| f.field_type == FormFieldType::Text && f.value.is_some())
576 .collect();
577 assert_eq!(leaf_fields.len(), 2);
578
579 let city = form.field_by_name("address.city");
581 assert!(city.is_some());
582 assert_eq!(city.unwrap().value.as_deref(), Some("Tokyo"));
583
584 let zip = form.field_by_name("address.zip");
585 assert!(zip.is_some());
586 assert_eq!(zip.unwrap().value.as_deref(), Some("100-0001"));
587 }
588
589 #[test]
590 fn test_multiple_top_level_fields() {
591 let store = build_store();
592
593 let field1 = make_field_dict("Tx", "name");
594 let field2 = make_field_dict("Btn", "submit");
595 let field3 = make_field_dict("Ch", "color");
596
597 let mut acroform = HashMap::new();
598 acroform.insert(
599 Name::fields(),
600 Object::Array(vec![
601 Object::Dictionary(field1),
602 Object::Dictionary(field2),
603 Object::Dictionary(field3),
604 ]),
605 );
606
607 let mut catalog = HashMap::new();
608 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
609 let obj = Object::Dictionary(catalog);
610
611 let form = InteractiveForm::from_catalog(&obj, &store)
612 .unwrap()
613 .unwrap();
614 assert_eq!(form.fields.len(), 3);
615 assert_eq!(form.fields[0].field_type, FormFieldType::Text);
616 assert_eq!(form.fields[1].field_type, FormFieldType::Button);
617 assert_eq!(form.fields[2].field_type, FormFieldType::Choice);
618 }
619
620 #[test]
621 fn test_all_fields_flat() {
622 let store = build_store();
623
624 let child = {
626 let mut d = HashMap::new();
627 d.insert(Name::ft(), Object::Name(Name::from("Tx")));
628 d.insert(Name::t(), str_obj("child"));
629 Object::Dictionary(d)
630 };
631 let mut parent = make_field_dict("Tx", "parent");
632 parent.insert(Name::kids(), Object::Array(vec![child]));
633
634 let standalone = make_field_dict("Btn", "btn1");
636
637 let mut acroform = HashMap::new();
638 acroform.insert(
639 Name::fields(),
640 Object::Array(vec![
641 Object::Dictionary(parent),
642 Object::Dictionary(standalone),
643 ]),
644 );
645
646 let mut catalog = HashMap::new();
647 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
648 let obj = Object::Dictionary(catalog);
649
650 let form = InteractiveForm::from_catalog(&obj, &store)
651 .unwrap()
652 .unwrap();
653 let all = form.all_fields();
654 assert_eq!(all.len(), 3);
656 }
657
658 #[test]
659 fn test_field_by_name_not_found() {
660 let store = build_store();
661
662 let field = make_field_dict("Tx", "exists");
663
664 let mut acroform = HashMap::new();
665 acroform.insert(
666 Name::fields(),
667 Object::Array(vec![Object::Dictionary(field)]),
668 );
669
670 let mut catalog = HashMap::new();
671 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
672 let obj = Object::Dictionary(catalog);
673
674 let form = InteractiveForm::from_catalog(&obj, &store)
675 .unwrap()
676 .unwrap();
677 assert!(form.field_by_name("nonexistent").is_none());
678 }
679
680 #[test]
681 fn test_from_catalog_with_programmatic_pdf() {
682 let mut pdf = Vec::new();
684 pdf.extend_from_slice(b"%PDF-1.4\n");
685 let obj1_offset = pdf.len();
686 pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [ << /FT /Tx /T (email) /V (test@example.com) >> ] >> >>\nendobj\n");
687 let obj2_offset = pdf.len();
688 pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
689 let xref_offset = pdf.len();
690 pdf.extend_from_slice(b"xref\n0 3\n");
691 pdf.extend_from_slice(b"0000000000 65535 f \r\n");
692 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
693 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
694 pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
695 pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
696
697 let store = ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap();
698
699 let root_id = store.trailer().root;
701 let catalog = store.resolve(root_id).unwrap();
702 let form = InteractiveForm::from_catalog(catalog, &store)
703 .unwrap()
704 .unwrap();
705 assert_eq!(form.fields.len(), 1);
706 assert_eq!(form.fields[0].name, "email");
707 assert_eq!(form.fields[0].value.as_deref(), Some("test@example.com"));
708 }
709
710 #[test]
711 fn test_calculation_order_parsed() {
712 let store = build_store();
713
714 let field_a = {
715 let mut d = make_field_dict("Tx", "total");
716 d.insert(Name::v(), str_obj("100"));
717 Object::Dictionary(d)
718 };
719 let field_b = {
720 let mut d = make_field_dict("Tx", "tax");
721 d.insert(Name::v(), str_obj("10"));
722 Object::Dictionary(d)
723 };
724
725 let co_a = {
727 let mut d = HashMap::new();
728 d.insert(Name::t(), str_obj("tax"));
729 Object::Dictionary(d)
730 };
731 let co_b = {
732 let mut d = HashMap::new();
733 d.insert(Name::t(), str_obj("total"));
734 Object::Dictionary(d)
735 };
736
737 let mut acroform = HashMap::new();
738 acroform.insert(Name::fields(), Object::Array(vec![field_a, field_b]));
739 acroform.insert(Name::co(), Object::Array(vec![co_a, co_b]));
740
741 let mut catalog = HashMap::new();
742 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
743 let obj = Object::Dictionary(catalog);
744
745 let form = InteractiveForm::from_catalog(&obj, &store)
746 .unwrap()
747 .unwrap();
748 assert_eq!(form.calculation_order(), &["tax", "total"]);
749 }
750
751 #[test]
752 fn test_calculation_order_empty_when_absent() {
753 let store = build_store();
754
755 let mut acroform = HashMap::new();
756 acroform.insert(Name::fields(), Object::Array(Vec::new()));
757 let mut catalog = HashMap::new();
760 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
761 let obj = Object::Dictionary(catalog);
762
763 let form = InteractiveForm::from_catalog(&obj, &store)
764 .unwrap()
765 .unwrap();
766 assert!(form.calculation_order().is_empty());
767 }
768
769 #[test]
770 fn test_default_appearance_parsed() {
771 let store = build_store();
772
773 let mut acroform = HashMap::new();
774 acroform.insert(Name::fields(), Object::Array(Vec::new()));
775 acroform.insert(Name::da(), str_obj("0 g /Helv 12 Tf"));
776 acroform.insert(Name::q(), Object::Integer(1));
777
778 let mut catalog = HashMap::new();
779 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
780 let obj = Object::Dictionary(catalog);
781
782 let form = InteractiveForm::from_catalog(&obj, &store)
783 .unwrap()
784 .unwrap();
785 assert_eq!(form.default_appearance(), Some("0 g /Helv 12 Tf"));
786 assert_eq!(form.default_alignment(), Alignment::Center);
787 }
788
789 #[test]
790 fn test_default_alignment_defaults_to_left() {
791 let store = build_store();
792
793 let mut acroform = HashMap::new();
794 acroform.insert(Name::fields(), Object::Array(Vec::new()));
795
796 let mut catalog = HashMap::new();
797 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
798 let obj = Object::Dictionary(catalog);
799
800 let form = InteractiveForm::from_catalog(&obj, &store)
801 .unwrap()
802 .unwrap();
803 assert!(form.default_appearance().is_none());
804 assert_eq!(form.default_alignment(), Alignment::Left);
805 }
806
807 #[test]
808 fn test_check_required_fields_empty() {
809 let store = build_store();
810
811 let field = make_field_dict("Tx", "name");
812
813 let mut acroform = HashMap::new();
814 acroform.insert(
815 Name::fields(),
816 Object::Array(vec![Object::Dictionary(field)]),
817 );
818
819 let mut catalog = HashMap::new();
820 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
821 let obj = Object::Dictionary(catalog);
822
823 let form = InteractiveForm::from_catalog(&obj, &store)
824 .unwrap()
825 .unwrap();
826 assert!(form.check_required_fields().is_empty());
827 }
828
829 #[test]
830 fn test_check_required_fields_finds_missing() {
831 let store = build_store();
832
833 let mut field = make_field_dict("Tx", "required_field");
834 field.insert(Name::ff(), Object::Integer(2)); let mut acroform = HashMap::new();
837 acroform.insert(
838 Name::fields(),
839 Object::Array(vec![Object::Dictionary(field)]),
840 );
841
842 let mut catalog = HashMap::new();
843 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
844 let obj = Object::Dictionary(catalog);
845
846 let form = InteractiveForm::from_catalog(&obj, &store)
847 .unwrap()
848 .unwrap();
849 let missing = form.check_required_fields();
850 assert_eq!(missing.len(), 1);
851 assert_eq!(missing[0], "required_field");
852 }
853
854 #[test]
855 fn test_check_required_fields_skips_filled() {
856 let store = build_store();
857
858 let mut field = make_field_dict("Tx", "required_field");
859 field.insert(Name::ff(), Object::Integer(2)); field.insert(Name::v(), str_obj("filled"));
861
862 let mut acroform = HashMap::new();
863 acroform.insert(
864 Name::fields(),
865 Object::Array(vec![Object::Dictionary(field)]),
866 );
867
868 let mut catalog = HashMap::new();
869 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
870 let obj = Object::Dictionary(catalog);
871
872 let form = InteractiveForm::from_catalog(&obj, &store)
873 .unwrap()
874 .unwrap();
875 assert!(form.check_required_fields().is_empty());
876 }
877
878 #[test]
889 fn test_cpdf_interactive_form_load_fields_with_referenced_names() {
890 let store = build_store();
891
892 let mut field1 = HashMap::new();
894 field1.insert(Name::ft(), Object::Name(Name::from("Btn")));
895 field1.insert(Name::t(), str_obj("good_string"));
896
897 let mut field2 = HashMap::new();
899 field2.insert(Name::ft(), Object::Name(Name::from("Btn")));
900 field2.insert(Name::t(), Object::Name(Name::from("bad_name")));
901
902 let mut field3 = HashMap::new();
904 field3.insert(Name::ft(), Object::Name(Name::from("Btn")));
905 field3.insert(
906 Name::t(),
907 Object::Stream {
908 dict: HashMap::new(),
909 data: rpdfium_parser::StreamData::Decoded {
910 data: b"bad_stream".to_vec(),
911 },
912 },
913 );
914
915 let mut acroform = HashMap::new();
916 acroform.insert(
917 Name::fields(),
918 Object::Array(vec![
919 Object::Dictionary(field1),
920 Object::Dictionary(field2),
921 Object::Dictionary(field3),
922 ]),
923 );
924
925 let mut catalog = HashMap::new();
926 catalog.insert(Name::acro_form(), Object::Dictionary(acroform));
927 let obj = Object::Dictionary(catalog);
928
929 let form = InteractiveForm::from_catalog(&obj, &store)
930 .unwrap()
931 .unwrap();
932
933 let good = form.field_by_name("good_string");
935 assert!(
936 good.is_some(),
937 "String-based /T should produce a valid field name"
938 );
939
940 let all = form.all_fields();
944
945 assert_eq!(all.len(), 3);
947
948 let good_field = all.iter().find(|f| f.name == "good_string");
950 assert!(good_field.is_some());
951
952 let empty_name_fields: Vec<_> = all.iter().filter(|f| f.name.is_empty()).collect();
955 assert_eq!(
956 empty_name_fields.len(),
957 2,
958 "Name and Stream /T values should result in empty field names"
959 );
960 }
961}