Skip to main content

rpdfium_doc/
interactive_form.rs

1// Derived from PDFium's cpdf_interactiveform.cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! Interactive forms (AcroForm) — read-only field parsing and value extraction.
7//!
8//! Parses the `/AcroForm` dictionary from the document catalog into a flat
9//! and hierarchical view of form fields (ISO 32000-2 section 12.7).
10
11use 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
20/// Maximum number of fields to parse (security limit).
21const MAX_FIELDS: usize = 10_000;
22
23/// A parsed interactive form (AcroForm).
24#[derive(Debug, Clone)]
25pub struct InteractiveForm {
26    /// Top-level form fields (may contain children for hierarchical fields).
27    pub fields: Vec<FormField>,
28    /// Calculation order — field names in the order they should be calculated.
29    pub calculation_order: Vec<String>,
30    /// Default appearance string (`/DA`) from the AcroForm dictionary.
31    pub default_appearance: Option<String>,
32    /// Default quadding/alignment (`/Q`) — 0=Left, 1=Center, 2=Right.
33    pub default_alignment: Alignment,
34}
35
36impl InteractiveForm {
37    /// Parse the interactive form from the document catalog.
38    ///
39    /// Returns `None` if no `/AcroForm` dictionary is present.
40    /// Field trees are traversed **iteratively** for WASM safety.
41    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        // Parse /CO (calculation order)
64        let calculation_order = parse_calculation_order(acroform_dict, store);
65
66        // Parse /DA (default appearance)
67        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        // Parse /Q (default quadding/alignment)
73        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        // Iterative tree traversal using a Vec stack.
107        // Each stack item records: (dict, parent_ft, parent_name, depth_index path).
108        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 of indices for inserting into the result tree.
116            path: Vec<usize>,
117        }
118
119        let mut stack: Vec<StackItem> = Vec::new();
120
121        // Push top-level fields in reverse order so we process them in forward order
122        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            // Determine the /FT for this node (for inheritance to children)
143            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            // Determine the partial name for this node
151            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            // Check for /Kids — if present, this is a non-terminal node
163            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                // Try to parse as a field first (some fields have both /FT and /Kids)
171                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                    // This is a field with children — insert it and
180                    // push kids to populate its children
181                    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                    // Non-terminal node without /FT — just pass name/FT down to kids
203                    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                // Leaf field
220                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    /// Returns the calculation order field names.
242    pub fn calculation_order(&self) -> &[String] {
243        &self.calculation_order
244    }
245
246    /// Return a flat iterator over all fields (depth-first).
247    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    /// Find a field by its fully qualified name.
260    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    /// Find a field by its fully qualified name (mutable).
274    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    /// Collect all field names in depth-first order.
288    ///
289    /// This can be combined with [`Self::field_by_name_mut`] to iterate mutably.
290    pub fn all_field_names(&self) -> Vec<String> {
291        self.all_fields().iter().map(|f| f.name.clone()).collect()
292    }
293
294    /// Return the default appearance string (`/DA`) from the AcroForm dictionary.
295    pub fn default_appearance(&self) -> Option<&str> {
296        self.default_appearance.as_deref()
297    }
298
299    /// ADR-019 alias for [`default_appearance()`](Self::default_appearance).
300    ///
301    /// Corresponds to `CPDF_InteractiveForm::GetDefaultAppearance()` in PDFium.
302    #[inline]
303    pub fn get_default_appearance(&self) -> Option<&str> {
304        self.default_appearance()
305    }
306
307    /// Return the default alignment (`/Q`) from the AcroForm dictionary.
308    pub fn default_alignment(&self) -> Alignment {
309        self.default_alignment
310    }
311
312    /// ADR-019 alias for [`default_alignment()`](Self::default_alignment).
313    ///
314    /// Corresponds to `CPDF_InteractiveForm::GetFormAlignment()` in PDFium.
315    #[inline]
316    pub fn get_form_alignment(&self) -> Alignment {
317        self.default_alignment()
318    }
319
320    /// Check which required fields have empty values.
321    ///
322    /// Returns the names of fields that have the Required flag (bit 2) set
323    /// but have no value.
324    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    /// Find a form control (widget) at the given point.
336    ///
337    /// Returns the field name and control index if a control's rect contains (x, y).
338    ///
339    /// Corresponds to `CPDF_InteractiveForm::GetControlAtPoint()` in PDFium.
340    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    /// ADR-019 alias for [`control_at_point()`](Self::control_at_point).
354    ///
355    /// Corresponds to `CPDF_InteractiveForm::GetControlAtPoint()` in PDFium.
356    #[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
362/// Parse the `/CO` (calculation order) array from an AcroForm dictionary.
363///
364/// Each element in the array is a reference to a field dict. We resolve each
365/// reference and extract its `/T` (partial name) to build the order list.
366fn 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
402/// Insert a field at the given path in the tree, returning its index in the parent container.
403fn 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    // Take ownership by swapping with an empty placeholder
407    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
432/// Navigate to the children vec at the given index path (iterative).
433fn 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        // Parent node with /FT Tx and /T "address", /Kids with two children
538        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        // The children should inherit /FT from parent
571        let all = form.all_fields();
572        // There should be 2 leaf fields with inherited Tx type
573        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        // Check field_by_name
580        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        // Parent with one child
625        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        // Another standalone field
635        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        // parent + child + btn1 = 3
655        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        // Build a minimal PDF with an AcroForm baked in
683        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        // Get catalog from store via trailer root
700        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        // CO array references field dicts directly
726        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        // No /CO key
758
759        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)); // Required flag
835
836        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)); // Required flag
860        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    /// Upstream: TEST_F(CPDFInteractiveFormTest, LoadFieldsWithReferencedNames)
879    ///
880    /// The upstream test creates fields where /T is an indirect reference to:
881    /// 1. A CPDF_String ("good_string") — should resolve as a valid field name
882    /// 2. A CPDF_Name ("bad_name") — should NOT resolve as a string, so no name
883    /// 3. A CPDF_Stream ("bad_stream") — should NOT resolve as a string, so no name
884    ///
885    /// In rpdfium, from_catalog resolves /T via deep_resolve + as_string().
886    /// A Name object returns None from as_string(), and a Stream also returns None.
887    /// So only the String-based /T produces a valid partial name.
888    #[test]
889    fn test_cpdf_interactive_form_load_fields_with_referenced_names() {
890        let store = build_store();
891
892        // Field 1: /T is a PDF String → should parse as field name "good_string"
893        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        // Field 2: /T is a Name object → as_string() returns None, no partial name
898        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        // Field 3: /T is a Stream object → as_string() returns None, no partial name
903        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        // The good_string field should be found by name
934        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        // The bad_name and bad_stream fields have no valid /T string,
941        // so their partial name is empty. They still exist as fields
942        // but with an empty name (inherited from empty parent_name).
943        let all = form.all_fields();
944
945        // We expect all 3 fields to be parsed (they all have /FT)
946        assert_eq!(all.len(), 3);
947
948        // The first field should have name "good_string"
949        let good_field = all.iter().find(|f| f.name == "good_string");
950        assert!(good_field.is_some());
951
952        // The other two fields should have empty names (Name and Stream
953        // don't produce valid partial names via as_string())
954        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}