Skip to main content

rpdfium_doc/
form_field.rs

1// Derived from PDFium's cpdf_formfield.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//! Form field types — Text, Button, Choice, Signature field parsing.
7//!
8//! Parses AcroForm field dictionaries (ISO 32000-2 section 12.7) into
9//! typed structs for read-only inspection.
10
11use std::collections::HashMap;
12
13use rpdfium_core::{Name, PdfSource};
14use rpdfium_parser::{Object, ObjectId, ObjectStore};
15
16use crate::aaction::{AdditionalActions, parse_additional_actions};
17use crate::error::DocError;
18use crate::form_control::{FormControl, HighlightingMode, TextPosition};
19use crate::icon_fit::{IconFit, parse_icon_fit};
20
21/// The type of an interactive form field.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum FormFieldType {
24    /// A text input field (`/FT Tx`).
25    Text,
26    /// A button field — checkbox, radio, or push button (`/FT Btn`).
27    Button,
28    /// A choice field — combo box or list box (`/FT Ch`).
29    Choice,
30    /// A digital signature field (`/FT Sig`).
31    Signature,
32}
33
34/// A single option in a choice field.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ChoiceOption {
37    /// The export value sent when the form is submitted.
38    pub export_value: String,
39    /// The display value shown to the user.
40    pub display_value: String,
41}
42
43/// A typed value that can be set on a form field.
44#[derive(Debug, Clone, PartialEq)]
45pub enum FieldValue {
46    /// A text string value (for Text fields).
47    String(String),
48    /// A boolean value (for Button/checkbox fields — true = checked).
49    Bool(bool),
50    /// A single choice index (for Choice fields — combo box / list box).
51    Choice(usize),
52    /// Multiple selected indices (for multi-select Choice fields).
53    Indices(Vec<usize>),
54}
55
56/// Form field flag bits (`/Ff`).
57///
58/// ISO 32000-1:2008, Tables 8.70 (common), 8.75 (button), 8.77 (text), 8.79
59/// (choice). Multiple groups share bit positions because the meaning depends
60/// on the field type (e.g. bit 23 is both DoNotSpellCheck for text and
61/// DoNotSpellCheck for choice).
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63pub struct FormFieldFlags(u32);
64
65impl FormFieldFlags {
66    /// Create flags from a raw integer value.
67    pub fn from_bits(bits: u32) -> Self {
68        Self(bits)
69    }
70
71    /// Return the raw bit value.
72    pub fn bits(self) -> u32 {
73        self.0
74    }
75
76    // --- Common to all field types (Table 8.70) ---
77
78    /// Bit 1: Field is read-only.
79    pub fn is_read_only(self) -> bool {
80        self.0 & (1 << 0) != 0
81    }
82
83    /// Bit 2: Field must have a value before the document is submitted.
84    pub fn is_required(self) -> bool {
85        self.0 & (1 << 1) != 0
86    }
87
88    /// Bit 3: Field value must not be exported.
89    pub fn is_no_export(self) -> bool {
90        self.0 & (1 << 2) != 0
91    }
92
93    // --- Button field flags (Table 8.75) ---
94
95    /// Bit 15: Radio button — at least one must be selected at all times.
96    pub fn is_no_toggle_to_off(self) -> bool {
97        self.0 & (1 << 14) != 0
98    }
99
100    /// Bit 16: Field is a set of radio buttons (not a checkbox).
101    pub fn is_radio(self) -> bool {
102        self.0 & (1 << 15) != 0
103    }
104
105    /// Bit 17: Field is a pushbutton (not a checkbox or radio button).
106    pub fn is_push_button(self) -> bool {
107        self.0 & (1 << 16) != 0
108    }
109
110    /// Bit 26: Radio buttons with the same value turn on/off together.
111    /// (Button fields. Note: same bit as `is_rich_text` for text fields.)
112    pub fn is_radios_in_unison(self) -> bool {
113        self.0 & (1 << 25) != 0
114    }
115
116    // --- Text field flags (Table 8.77) ---
117
118    /// Bit 13: Text field allows multiple lines.
119    pub fn is_multiline(self) -> bool {
120        self.0 & (1 << 12) != 0
121    }
122
123    /// Bit 14: Text field is a password (display as bullets).
124    pub fn is_password(self) -> bool {
125        self.0 & (1 << 13) != 0
126    }
127
128    /// Bit 21: Text field value is a file-select path.
129    pub fn is_file_select(self) -> bool {
130        self.0 & (1 << 20) != 0
131    }
132
133    /// Bit 23: Text/choice field should not be spell-checked.
134    pub fn is_do_not_spell_check(self) -> bool {
135        self.0 & (1 << 22) != 0
136    }
137
138    /// Bit 24: Text field should not scroll horizontally.
139    pub fn is_do_not_scroll(self) -> bool {
140        self.0 & (1 << 23) != 0
141    }
142
143    /// Bit 25: Text field uses comb formatting (requires `/MaxLen`).
144    pub fn is_comb(self) -> bool {
145        self.0 & (1 << 24) != 0
146    }
147
148    /// Bit 26: Text field contains rich text (RV entry).
149    /// (Text fields. Note: same bit as `is_radios_in_unison` for button fields.)
150    pub fn is_rich_text(self) -> bool {
151        self.0 & (1 << 25) != 0
152    }
153
154    // --- Choice field flags (Table 8.79) ---
155
156    /// Bit 18: Choice field is a combo box (editable dropdown).
157    pub fn is_combo(self) -> bool {
158        self.0 & (1 << 17) != 0
159    }
160
161    /// Bit 19: Combo box choice field is editable by the user.
162    pub fn is_combo_editable(self) -> bool {
163        self.0 & (1 << 18) != 0
164    }
165
166    /// Bit 20: List box items should be sorted alphabetically.
167    pub fn is_sort(self) -> bool {
168        self.0 & (1 << 19) != 0
169    }
170
171    /// Bit 22: List box allows multiple simultaneous selections.
172    pub fn is_multi_select(self) -> bool {
173        self.0 & (1 << 21) != 0
174    }
175
176    /// Bit 27: Choice field commits the value immediately on selection change.
177    pub fn is_commit_on_sel_change(self) -> bool {
178        self.0 & (1 << 26) != 0
179    }
180
181    /// Set or clear the read-only flag (bit 1).
182    pub fn set_read_only(&mut self, v: bool) {
183        if v {
184            self.0 |= 1 << 0;
185        } else {
186            self.0 &= !(1 << 0);
187        }
188    }
189
190    /// Set or clear the required flag (bit 2).
191    pub fn set_required(&mut self, v: bool) {
192        if v {
193            self.0 |= 1 << 1;
194        } else {
195            self.0 &= !(1 << 1);
196        }
197    }
198
199    /// Set or clear the no-export flag (bit 3).
200    pub fn set_no_export(&mut self, v: bool) {
201        if v {
202            self.0 |= 1 << 2;
203        } else {
204            self.0 &= !(1 << 2);
205        }
206    }
207}
208
209/// A parsed interactive form field.
210#[derive(Debug, Clone)]
211pub struct FormField {
212    /// Fully qualified field name (parent.child format).
213    pub name: String,
214    /// Field type.
215    pub field_type: FormFieldType,
216    /// Current value (`/V`).
217    pub value: Option<String>,
218    /// Default value (`/DV`).
219    pub default_value: Option<String>,
220    /// Field flags (`/Ff`).
221    pub flags: FormFieldFlags,
222    /// Tooltip (`/TU`).
223    pub tooltip: Option<String>,
224    /// Alternate field name for display (`/TU`, same as tooltip — upstream uses this).
225    pub alternate_name: Option<String>,
226    /// Mapping name for export (`/TM`).
227    pub mapping_name: Option<String>,
228    /// For text fields: maximum length (`/MaxLen`).
229    pub max_len: Option<u32>,
230    /// For choice fields: options (`/Opt`).
231    pub options: Vec<ChoiceOption>,
232    /// For button fields: checked/appearance state (`/AS`).
233    pub appearance_state: Option<String>,
234    /// Child fields (for hierarchical fields).
235    pub children: Vec<FormField>,
236    /// Widget controls associated with this field.
237    pub controls: Vec<FormControl>,
238    /// Whether the field value has been modified since parsing.
239    pub dirty: bool,
240    /// Selected indices for choice fields (`/I`).
241    pub selected_indices: Vec<usize>,
242    /// Additional actions for the field (`/AA`).
243    pub additional_actions: Option<AdditionalActions>,
244}
245
246impl FormField {
247    /// Parse a form field from a PDF dictionary.
248    ///
249    /// `parent_ft` is the inherited `/FT` value from the parent field (if any).
250    /// `parent_name` is the accumulated fully-qualified name prefix.
251    pub(crate) fn from_dict<S: PdfSource>(
252        dict: &HashMap<Name, Object>,
253        store: &ObjectStore<S>,
254        parent_ft: Option<&str>,
255        parent_name: &str,
256    ) -> Option<Self> {
257        // /T — partial field name
258        let partial_name = dict
259            .get(&Name::t())
260            .and_then(|o| store.deep_resolve(o).ok())
261            .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
262
263        // Build fully qualified name.
264        //
265        // Prefer the full name resolved by walking the /Parent chain in the object
266        // store (mirrors PDFium's GetFullNameForDict). Fall back to the accumulated
267        // parent_name passed down by the in-memory tree walker.
268        let name = if let Some(store_name) = resolve_full_field_name_from_store(dict, store) {
269            // The store-based walk already produced a dotted full name.
270            store_name
271        } else {
272            // No /Parent reference in the store: use the tree-walker's accumulated name.
273            match (&partial_name, parent_name.is_empty()) {
274                (Some(pn), true) => pn.clone(),
275                (Some(pn), false) => format!("{parent_name}.{pn}"),
276                (None, _) => parent_name.to_string(),
277            }
278        };
279
280        // /FT — field type, inheritable through the /Parent chain (ISO 32000-2 §12.7.3).
281        //
282        // First check the dict directly; if absent, walk the /Parent chain via the
283        // object store (mirrors PDFium's GetFieldAttrRecursive). Finally fall back to
284        // the parent_ft propagated by the in-memory tree walker.
285        let ft_obj = get_field_attr_inherited(dict, &Name::ft(), store);
286        let ft_str = ft_obj
287            .as_ref()
288            .and_then(|o| store.deep_resolve(o).ok())
289            .and_then(|o| o.as_name().map(|n| n.as_str().into_owned()));
290        let ft_ref = ft_str.as_deref().or(parent_ft);
291
292        let field_type = match ft_ref {
293            Some("Tx") => FormFieldType::Text,
294            Some("Btn") => FormFieldType::Button,
295            Some("Ch") => FormFieldType::Choice,
296            Some("Sig") => FormFieldType::Signature,
297            _ => {
298                // No field type and no parent type — skip (non-terminal node
299                // handled by tree walker which recurses into /Kids)
300                return None;
301            }
302        };
303
304        // /V — value (string or name), inheritable through /Parent chain.
305        let value = extract_inherited_string_or_name(dict, &Name::v(), store);
306
307        // /DV — default value, inheritable through /Parent chain.
308        let default_value = extract_inherited_string_or_name(dict, &Name::dv(), store);
309
310        // /Ff — field flags, inheritable through /Parent chain.
311        let ff_obj = get_field_attr_inherited(dict, &Name::ff(), store);
312        let flags = FormFieldFlags::from_bits(
313            ff_obj
314                .as_ref()
315                .and_then(|o| store.deep_resolve(o).ok())
316                .and_then(|o| o.as_i64())
317                .unwrap_or(0) as u32,
318        );
319
320        // /TU — tooltip / alternate name
321        let tooltip = dict
322            .get(&Name::tu())
323            .and_then(|o| store.deep_resolve(o).ok())
324            .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
325
326        // Alternate name is same as tooltip (upstream GetAlternateName reads /TU)
327        let alternate_name = tooltip.clone();
328
329        // /TM — mapping name for export
330        let mapping_name = dict
331            .get(&Name::tm())
332            .and_then(|o| store.deep_resolve(o).ok())
333            .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
334
335        // /MaxLen
336        let max_len = dict
337            .get(&Name::max_len())
338            .and_then(|o| store.deep_resolve(o).ok())
339            .and_then(|o| o.as_i64())
340            .map(|v| v as u32);
341
342        // /Opt — choice field options
343        let options = parse_options(dict, store);
344
345        // /AS — appearance state
346        let appearance_state = dict
347            .get(&Name::as_name())
348            .and_then(|o| store.deep_resolve(o).ok())
349            .and_then(|o| o.as_name().map(|n| n.as_str().into_owned()));
350
351        // /I — selected indices for choice fields
352        let selected_indices = parse_selected_indices(dict, store);
353
354        // Parse widget controls from /Kids that have /Subtype /Widget but no /FT
355        let controls = parse_widget_controls(dict, store, &name);
356
357        // /AA — additional actions
358        let additional_actions = dict
359            .get(&Name::aa())
360            .and_then(|o| parse_additional_actions(o, store).ok());
361
362        Some(FormField {
363            name,
364            field_type,
365            value,
366            default_value,
367            flags,
368            tooltip,
369            alternate_name,
370            mapping_name,
371            max_len,
372            options,
373            appearance_state,
374            children: Vec::new(),
375            controls,
376            dirty: false,
377            selected_indices,
378            additional_actions,
379        })
380    }
381
382    /// Set the field's value with type and constraint validation.
383    ///
384    /// Returns an error if:
385    /// - The value type does not match the field type (e.g. `Bool` on a `Text` field).
386    /// - A text value exceeds `/MaxLen`.
387    /// - A choice index is out of range.
388    pub fn set_value(&mut self, value: FieldValue) -> Result<(), DocError> {
389        match (&self.field_type, &value) {
390            (FormFieldType::Text, FieldValue::String(s)) => {
391                if let Some(max) = self.max_len {
392                    if s.len() > max as usize {
393                        return Err(DocError::ValueTooLong { len: s.len(), max });
394                    }
395                }
396                self.value = Some(s.clone());
397            }
398            (FormFieldType::Button, FieldValue::Bool(checked)) => {
399                self.appearance_state = Some(if *checked {
400                    "Yes".to_string()
401                } else {
402                    "Off".to_string()
403                });
404                self.value = Some(self.appearance_state.as_ref().unwrap().clone());
405            }
406            (FormFieldType::Choice, FieldValue::Choice(idx)) => {
407                if *idx >= self.options.len() {
408                    return Err(DocError::InvalidChoiceIndex {
409                        index: *idx,
410                        count: self.options.len(),
411                    });
412                }
413                self.value = Some(self.options[*idx].export_value.clone());
414            }
415            (FormFieldType::Choice, FieldValue::String(s)) => {
416                // Allow setting a choice field by raw string value (e.g. from FDF import)
417                self.value = Some(s.clone());
418            }
419            (FormFieldType::Choice, FieldValue::Indices(indices)) => {
420                for &idx in indices {
421                    if idx >= self.options.len() {
422                        return Err(DocError::InvalidChoiceIndex {
423                            index: idx,
424                            count: self.options.len(),
425                        });
426                    }
427                }
428                // Store as comma-separated export values
429                let vals: Vec<&str> = indices
430                    .iter()
431                    .map(|&i| self.options[i].export_value.as_str())
432                    .collect();
433                self.value = Some(vals.join(","));
434            }
435            (field_type, val) => {
436                let expected = match field_type {
437                    FormFieldType::Text => "String",
438                    FormFieldType::Button => "Bool",
439                    FormFieldType::Choice => "Choice or Indices",
440                    FormFieldType::Signature => "Signature (read-only)",
441                };
442                let got = match val {
443                    FieldValue::String(_) => "String",
444                    FieldValue::Bool(_) => "Bool",
445                    FieldValue::Choice(_) => "Choice",
446                    FieldValue::Indices(_) => "Indices",
447                };
448                return Err(DocError::TypeMismatch {
449                    expected: expected.to_string(),
450                    got: got.to_string(),
451                });
452            }
453        }
454        self.dirty = true;
455        Ok(())
456    }
457
458    /// Returns the widget controls associated with this field.
459    pub fn controls(&self) -> &[FormControl] {
460        &self.controls
461    }
462
463    /// Returns `true` if the field has been modified and its appearance stream
464    /// needs regeneration.
465    pub fn needs_appearance(&self) -> bool {
466        self.dirty
467    }
468
469    /// Reset the field to its default value (`/DV`), or clear if none.
470    pub fn reset_to_default(&mut self) {
471        self.value = self.default_value.clone();
472        self.dirty = true;
473    }
474
475    // --- Selection APIs for Choice fields ---
476
477    /// Returns the number of currently selected items.
478    ///
479    /// Corresponds to upstream `CPDF_FormField::CountSelectedItems()`.
480    pub fn selected_item_count(&self) -> usize {
481        self.selected_indices.len()
482    }
483
484    /// Upstream-aligned alias for [`selected_item_count()`](Self::selected_item_count).
485    ///
486    /// Corresponds to upstream `CPDF_FormField::CountSelectedItems()`.
487    #[inline]
488    pub fn count_selected_items(&self) -> usize {
489        self.selected_item_count()
490    }
491
492    /// Returns `true` if the option at the given index is selected.
493    pub fn is_item_selected(&self, index: usize) -> bool {
494        self.selected_indices.contains(&index)
495    }
496
497    /// Returns the selected option index at position `sel_index` within the selection list.
498    ///
499    /// Corresponds to upstream `CPDF_FormField::GetSelectedIndex()`.
500    pub fn selected_index(&self, sel_index: usize) -> Option<usize> {
501        self.selected_indices.get(sel_index).copied()
502    }
503
504    /// Upstream-aligned alias for [`selected_index()`](Self::selected_index).
505    ///
506    /// Corresponds to upstream `CPDF_FormField::GetSelectedIndex()`.
507    #[inline]
508    pub fn get_selected_index(&self, sel_index: usize) -> Option<usize> {
509        self.selected_index(sel_index)
510    }
511
512    /// Clear the selection for a choice field.
513    ///
514    /// Clears the in-memory selected indices list. To persist the change to a
515    /// PDF file, use `EditableForm::clear_field_selection` in rpdfium-edit.
516    pub fn clear_selection(&mut self) -> Result<(), DocError> {
517        self.selected_indices.clear();
518        Ok(())
519    }
520
521    /// Set or clear the read-only flag for this field.
522    ///
523    /// Updates the in-memory flags. To persist the change to a PDF file,
524    /// use `EditableForm::set_field_read_only` in rpdfium-edit.
525    pub fn set_read_only(&mut self, v: bool) {
526        self.flags.set_read_only(v);
527    }
528
529    /// Set or clear the required flag for this field.
530    ///
531    /// Updates the in-memory flags. To persist the change to a PDF file,
532    /// use `EditableForm::set_field_required` in rpdfium-edit.
533    pub fn set_required(&mut self, v: bool) {
534        self.flags.set_required(v);
535    }
536
537    /// Set or clear the no-export flag for this field.
538    ///
539    /// Updates the in-memory flags. To persist the change to a PDF file,
540    /// use `EditableForm::set_field_no_export` in rpdfium-edit.
541    pub fn set_no_export(&mut self, v: bool) {
542        self.flags.set_no_export(v);
543    }
544
545    /// Returns the additional actions for this field, if any.
546    pub fn additional_actions(&self) -> Option<&AdditionalActions> {
547        self.additional_actions.as_ref()
548    }
549
550    /// ADR-019 alias for [`additional_actions()`](Self::additional_actions).
551    ///
552    /// Corresponds to `CPDF_FormField::GetAdditionalAction()` in PDFium.
553    #[inline]
554    pub fn get_additional_actions(&self) -> Option<&AdditionalActions> {
555        self.additional_actions()
556    }
557
558    // --- Choice option accessor methods ---
559
560    /// Returns the number of options for choice fields.
561    ///
562    /// Corresponds to upstream `CPDF_FormField::CountOptions()`.
563    pub fn option_count(&self) -> usize {
564        self.options.len()
565    }
566
567    /// Upstream-aligned alias for [`option_count()`](Self::option_count).
568    ///
569    /// Corresponds to upstream `CPDF_FormField::CountOptions()`.
570    #[inline]
571    pub fn count_options(&self) -> usize {
572        self.option_count()
573    }
574
575    /// Returns the display label of the option at `index`, or `None` if out of bounds.
576    ///
577    /// Corresponds to upstream `CPDF_FormField::GetOptionLabel(index)`.
578    pub fn option_label(&self, index: usize) -> Option<&str> {
579        self.options.get(index).map(|o| o.display_value.as_str())
580    }
581
582    /// Upstream-aligned alias for [`option_label()`](Self::option_label).
583    ///
584    /// Corresponds to upstream `CPDF_FormField::GetOptionLabel(index)`.
585    #[inline]
586    pub fn get_option_label(&self, index: usize) -> Option<&str> {
587        self.option_label(index)
588    }
589
590    /// Returns the export value of the option at `index`, or `None` if out of bounds.
591    ///
592    /// Corresponds to upstream `CPDF_FormField::GetOptionValue(index)`.
593    pub fn option_value(&self, index: usize) -> Option<&str> {
594        self.options.get(index).map(|o| o.export_value.as_str())
595    }
596
597    /// Upstream-aligned alias for [`option_value()`](Self::option_value).
598    ///
599    /// Corresponds to upstream `CPDF_FormField::GetOptionValue(index)`.
600    #[inline]
601    pub fn get_option_value(&self, index: usize) -> Option<&str> {
602        self.option_value(index)
603    }
604
605    // --- Control accessor methods ---
606
607    /// Returns the number of widget controls for this field.
608    ///
609    /// Corresponds to upstream `CPDF_FormField::CountControls()`.
610    pub fn control_count(&self) -> usize {
611        self.controls.len()
612    }
613
614    /// Upstream-aligned alias for [`control_count()`](Self::control_count).
615    ///
616    /// Corresponds to upstream `CPDF_FormField::CountControls()`.
617    #[inline]
618    pub fn count_controls(&self) -> usize {
619        self.control_count()
620    }
621
622    /// Returns the control at `index`, or `None` if out of bounds.
623    ///
624    /// Corresponds to upstream `CPDF_FormField::GetControl(index)`.
625    pub fn control_at(&self, index: usize) -> Option<&FormControl> {
626        self.controls.get(index)
627    }
628
629    /// Upstream-aligned alias for [`control_at()`](Self::control_at).
630    ///
631    /// Corresponds to upstream `CPDF_FormField::GetControl(index)`.
632    #[inline]
633    pub fn get_control(&self, index: usize) -> Option<&FormControl> {
634        self.control_at(index)
635    }
636}
637
638/// Maximum depth for parent chain traversal (mirrors PDFium's kGetFieldMaxRecursion = 32).
639const MAX_FIELD_ATTR_DEPTH: usize = 32;
640
641/// Walk the parent chain stored in the object store to find an inherited field attribute.
642///
643/// Equivalent to PDFium's `GetFieldAttrRecursive()` / `GetFieldAttrForDict()`.
644/// Looks up `name` first in `start_dict`, then walks `/Parent` references up to
645/// `MAX_FIELD_ATTR_DEPTH` levels. Returns a clone of the first found value, or `None`.
646///
647/// Cycle detection: tracks the last visited `ObjectId`; a parent that references
648/// itself terminates the walk.
649fn get_field_attr_inherited<S: PdfSource>(
650    start_dict: &HashMap<Name, Object>,
651    name: &Name,
652    store: &ObjectStore<S>,
653) -> Option<Object> {
654    // Try the starting dict first.
655    if let Some(val) = start_dict.get(name) {
656        return Some(val.clone());
657    }
658
659    // Walk parent chain iteratively.
660    let mut current_id: Option<ObjectId> = None;
661    let mut parent_ref = start_dict
662        .get(&Name::parent())
663        .and_then(|v| v.as_reference());
664
665    for _ in 0..MAX_FIELD_ATTR_DEPTH {
666        let parent_id = parent_ref?;
667
668        // Cycle detection: stop if we revisit the same object.
669        if current_id == Some(parent_id) {
670            break;
671        }
672        current_id = Some(parent_id);
673
674        let parent_obj = store.resolve(parent_id).ok()?;
675        let parent_dict = parent_obj.as_dict()?;
676
677        if let Some(val) = parent_dict.get(name) {
678            return Some(val.clone());
679        }
680
681        parent_ref = parent_dict
682            .get(&Name::parent())
683            .and_then(|v| v.as_reference());
684    }
685
686    None
687}
688
689/// Build the fully qualified field name by collecting `/T` partial names from
690/// the parent chain stored in the object store.
691///
692/// Equivalent to PDFium's `CPDF_FormField::GetFullNameForDict()`.
693///
694/// This supplements the `parent_name` argument already accumulated by the
695/// tree walker: if the field dict has a `/Parent` reference pointing into the
696/// object store, we walk that chain to collect any additional `/T` components
697/// that the in-memory stack traversal may not have visited.
698fn resolve_full_field_name_from_store<S: PdfSource>(
699    start_dict: &HashMap<Name, Object>,
700    store: &ObjectStore<S>,
701) -> Option<String> {
702    // Only bother if the dict actually has a /Parent reference.
703    start_dict.get(&Name::parent())?.as_reference()?;
704
705    let mut parts: Vec<String> = Vec::new();
706
707    // Collect /T at the current level.
708    if let Some(t_obj) = start_dict.get(&Name::t()) {
709        if let Ok(resolved) = store.deep_resolve(t_obj) {
710            if let Some(s) = resolved.as_string() {
711                parts.push(s.to_string_lossy());
712            }
713        }
714    }
715
716    // Walk parent chain collecting /T entries.
717    let mut parent_ref = start_dict
718        .get(&Name::parent())
719        .and_then(|v| v.as_reference());
720    let mut current_id: Option<ObjectId> = None;
721
722    for _ in 0..MAX_FIELD_ATTR_DEPTH {
723        let pid = match parent_ref {
724            Some(id) => id,
725            None => break,
726        };
727        if current_id == Some(pid) {
728            break;
729        }
730        current_id = Some(pid);
731
732        let pobj = match store.resolve(pid).ok() {
733            Some(o) => o,
734            None => break,
735        };
736        let pdict = match pobj.as_dict() {
737            Some(d) => d,
738            None => break,
739        };
740
741        if let Some(t_obj) = pdict.get(&Name::t()) {
742            if let Ok(resolved) = store.deep_resolve(t_obj) {
743                if let Some(s) = resolved.as_string() {
744                    parts.push(s.to_string_lossy());
745                }
746            }
747        }
748
749        parent_ref = pdict.get(&Name::parent()).and_then(|v| v.as_reference());
750    }
751
752    if parts.is_empty() {
753        return None;
754    }
755
756    parts.reverse();
757    Some(parts.join("."))
758}
759
760/// Extract a string or name value from a dictionary key, walking the `/Parent` chain
761/// if the key is absent from `dict`.
762///
763/// Equivalent to calling `GetFieldAttrForDict` then interpreting the result as text.
764fn extract_inherited_string_or_name<S: PdfSource>(
765    dict: &HashMap<Name, Object>,
766    key: &Name,
767    store: &ObjectStore<S>,
768) -> Option<String> {
769    let obj = get_field_attr_inherited(dict, key, store)?;
770    let resolved = store.deep_resolve(&obj).ok()?;
771    if let Some(s) = resolved.as_string() {
772        Some(s.to_string_lossy())
773    } else {
774        resolved.as_name().map(|n| n.as_str().into_owned())
775    }
776}
777
778/// Parse `/Opt` array into `ChoiceOption` items.
779///
780/// Each element can be either:
781/// - A string (export_value == display_value)
782/// - A 2-element array [export_value, display_value]
783fn parse_options<S: PdfSource>(
784    dict: &HashMap<Name, Object>,
785    store: &ObjectStore<S>,
786) -> Vec<ChoiceOption> {
787    let opt_obj = match dict.get(&Name::opt()) {
788        Some(o) => o,
789        None => return Vec::new(),
790    };
791    let resolved = match store.deep_resolve(opt_obj).ok() {
792        Some(o) => o,
793        None => return Vec::new(),
794    };
795    let arr = match resolved.as_array() {
796        Some(a) => a,
797        None => return Vec::new(),
798    };
799
800    let mut options = Vec::with_capacity(arr.len());
801    for item in arr {
802        let resolved_item = match store.deep_resolve(item).ok() {
803            Some(o) => o,
804            None => continue,
805        };
806
807        if let Some(sub_arr) = resolved_item.as_array() {
808            // [export_value, display_value]
809            if sub_arr.len() >= 2 {
810                let export = store
811                    .deep_resolve(&sub_arr[0])
812                    .ok()
813                    .and_then(|o| o.as_string().map(|s| s.to_string_lossy()))
814                    .unwrap_or_default();
815                let display = store
816                    .deep_resolve(&sub_arr[1])
817                    .ok()
818                    .and_then(|o| o.as_string().map(|s| s.to_string_lossy()))
819                    .unwrap_or_default();
820                options.push(ChoiceOption {
821                    export_value: export,
822                    display_value: display,
823                });
824            }
825        } else if let Some(s) = resolved_item.as_string() {
826            let val = s.to_string_lossy();
827            options.push(ChoiceOption {
828                export_value: val.clone(),
829                display_value: val,
830            });
831        }
832    }
833    options
834}
835
836/// Parse `/I` (selected indices) array from a choice field dictionary.
837fn parse_selected_indices<S: PdfSource>(
838    dict: &HashMap<Name, Object>,
839    store: &ObjectStore<S>,
840) -> Vec<usize> {
841    let i_obj = match dict.get(&Name::i()) {
842        Some(o) => o,
843        None => return Vec::new(),
844    };
845    let resolved = match store.deep_resolve(i_obj).ok() {
846        Some(o) => o,
847        None => return Vec::new(),
848    };
849    let arr = match resolved.as_array() {
850        Some(a) => a,
851        None => return Vec::new(),
852    };
853
854    arr.iter()
855        .filter_map(|item| item.as_i64().map(|n| n as usize))
856        .collect()
857}
858
859/// Parse widget controls from /Kids that are widget annotations (have /Subtype /Widget
860/// but no /FT of their own).
861fn parse_widget_controls<S: PdfSource>(
862    dict: &HashMap<Name, Object>,
863    store: &ObjectStore<S>,
864    field_name: &str,
865) -> Vec<FormControl> {
866    let kids_obj = match dict.get(&Name::kids()) {
867        Some(o) => o,
868        None => return Vec::new(),
869    };
870    let kids_resolved = match store.deep_resolve(kids_obj).ok() {
871        Some(o) => o,
872        None => return Vec::new(),
873    };
874    let kids_arr = match kids_resolved.as_array() {
875        Some(a) => a,
876        None => return Vec::new(),
877    };
878
879    let mut controls = Vec::new();
880    for kid in kids_arr {
881        let resolved = match store.deep_resolve(kid).ok() {
882            Some(o) => o,
883            None => continue,
884        };
885        let kid_dict = match resolved.as_dict() {
886            Some(d) => d,
887            None => continue,
888        };
889
890        // Check if it's a Widget (has /Subtype /Widget) without its own /FT
891        let is_widget = kid_dict
892            .get(&Name::subtype())
893            .and_then(|o| store.deep_resolve(o).ok())
894            .and_then(|o| o.as_name().map(|n| n.as_str().into_owned()))
895            .is_some_and(|s| s == "Widget");
896        let has_ft = kid_dict.get(&Name::ft()).is_some();
897
898        if is_widget && !has_ft {
899            let rect = parse_rect(kid_dict, store);
900            let appearance_state = kid_dict
901                .get(&Name::as_name())
902                .and_then(|o| store.deep_resolve(o).ok())
903                .and_then(|o| o.as_name().map(|n| n.as_str().into_owned()));
904
905            // /H — highlighting mode
906            let highlighting_mode = kid_dict
907                .get(&Name::h())
908                .and_then(|o| store.deep_resolve(o).ok())
909                .and_then(|o| {
910                    o.as_name()
911                        .map(|n| HighlightingMode::from_name(&n.as_str()))
912                })
913                .unwrap_or_default();
914
915            // /DA — default appearance
916            let default_appearance = kid_dict
917                .get(&Name::da())
918                .and_then(|o| store.deep_resolve(o).ok())
919                .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
920
921            // Parse /MK fields
922            let mk = parse_mk_fields(kid_dict, store);
923
924            controls.push(FormControl {
925                field_name: field_name.to_string(),
926                rect,
927                appearance_state,
928                page_index: None,
929                highlighting_mode,
930                rotation: mk.rotation,
931                border_color: mk.border_color,
932                background_color: mk.background_color,
933                caption: mk.caption,
934                rollover_caption: mk.rollover_caption,
935                alt_caption: mk.alt_caption,
936                default_appearance,
937                normal_icon: mk.normal_icon,
938                rollover_icon: mk.rollover_icon,
939                down_icon: mk.down_icon,
940                icon_fit: mk.icon_fit,
941                text_position: mk.text_position,
942            });
943        }
944    }
945    controls
946}
947
948/// Parsed fields from an MK (appearance characteristics) dictionary.
949#[derive(Default)]
950struct MkFields {
951    rotation: u32,
952    border_color: Option<Vec<f32>>,
953    background_color: Option<Vec<f32>>,
954    caption: Option<String>,
955    rollover_caption: Option<String>,
956    alt_caption: Option<String>,
957    normal_icon: Option<ObjectId>,
958    rollover_icon: Option<ObjectId>,
959    down_icon: Option<ObjectId>,
960    icon_fit: Option<IconFit>,
961    text_position: TextPosition,
962}
963
964/// Parse MK dictionary fields from a widget annotation dictionary.
965fn parse_mk_fields<S: PdfSource>(dict: &HashMap<Name, Object>, store: &ObjectStore<S>) -> MkFields {
966    let mk_obj = match dict.get(&Name::mk()) {
967        Some(o) => o,
968        None => return MkFields::default(),
969    };
970    let resolved = match store.deep_resolve(mk_obj).ok() {
971        Some(o) => o,
972        None => return MkFields::default(),
973    };
974    let mk_dict = match resolved.as_dict() {
975        Some(d) => d,
976        None => return MkFields::default(),
977    };
978
979    let rotation = mk_dict
980        .get(&Name::r())
981        .and_then(|o| o.as_i64())
982        .unwrap_or(0) as u32;
983
984    let border_color = parse_color_array(mk_dict, &Name::bc(), store);
985    let background_color = parse_color_array(mk_dict, &Name::bg_color(), store);
986
987    let caption = mk_dict
988        .get(&Name::ca_display())
989        .and_then(|o| store.deep_resolve(o).ok())
990        .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
991
992    let rollover_caption = mk_dict
993        .get(&Name::rc())
994        .and_then(|o| store.deep_resolve(o).ok())
995        .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
996
997    let alt_caption = mk_dict
998        .get(&Name::ac())
999        .and_then(|o| store.deep_resolve(o).ok())
1000        .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
1001
1002    let normal_icon = mk_dict.get(&Name::i()).and_then(|o| o.as_reference());
1003    let rollover_icon = mk_dict.get(&Name::ri()).and_then(|o| o.as_reference());
1004    let down_icon = mk_dict.get(&Name::ix()).and_then(|o| o.as_reference());
1005
1006    let text_position = mk_dict
1007        .get(&Name::tp())
1008        .and_then(|o| o.as_i64())
1009        .map(|v| TextPosition::from_value(v as u32))
1010        .unwrap_or_default();
1011
1012    let icon_fit = parse_icon_fit(mk_dict, store);
1013
1014    MkFields {
1015        rotation,
1016        border_color,
1017        background_color,
1018        caption,
1019        rollover_caption,
1020        alt_caption,
1021        normal_icon,
1022        rollover_icon,
1023        down_icon,
1024        icon_fit,
1025        text_position,
1026    }
1027}
1028
1029/// Parse a color array from a dictionary (e.g., `/BC`, `/BG` in MK dict).
1030fn parse_color_array<S: PdfSource>(
1031    dict: &HashMap<Name, Object>,
1032    key: &Name,
1033    store: &ObjectStore<S>,
1034) -> Option<Vec<f32>> {
1035    let obj = dict.get(key)?;
1036    let resolved = store.deep_resolve(obj).ok()?;
1037    let arr = resolved.as_array()?;
1038    let colors: Vec<f32> = arr
1039        .iter()
1040        .filter_map(|o| o.as_f64().map(|v| v as f32))
1041        .collect();
1042    if colors.is_empty() {
1043        None
1044    } else {
1045        Some(colors)
1046    }
1047}
1048
1049/// Parse a `/Rect` array from a dictionary.
1050fn parse_rect<S: PdfSource>(
1051    dict: &HashMap<Name, Object>,
1052    store: &ObjectStore<S>,
1053) -> rpdfium_core::Rect {
1054    let default = rpdfium_core::Rect {
1055        left: 0.0,
1056        bottom: 0.0,
1057        right: 0.0,
1058        top: 0.0,
1059    };
1060    let rect_obj = match dict.get(&Name::rect()) {
1061        Some(o) => o,
1062        None => return default,
1063    };
1064    let resolved = match store.deep_resolve(rect_obj).ok() {
1065        Some(o) => o,
1066        None => return default,
1067    };
1068    let arr = match resolved.as_array() {
1069        Some(a) if a.len() >= 4 => a,
1070        _ => return default,
1071    };
1072
1073    let get_f64 = |idx: usize| -> f64 { arr[idx].as_f64().unwrap_or(0.0) };
1074
1075    rpdfium_core::Rect {
1076        left: get_f64(0),
1077        bottom: get_f64(1),
1078        right: get_f64(2),
1079        top: get_f64(3),
1080    }
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085    use super::*;
1086    use rpdfium_core::PdfString;
1087
1088    fn build_store() -> ObjectStore<Vec<u8>> {
1089        let pdf = build_minimal_pdf();
1090        ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
1091    }
1092
1093    fn build_minimal_pdf() -> Vec<u8> {
1094        let mut pdf = Vec::new();
1095        pdf.extend_from_slice(b"%PDF-1.4\n");
1096        let obj1_offset = pdf.len();
1097        pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1098        let obj2_offset = pdf.len();
1099        pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
1100        let xref_offset = pdf.len();
1101        pdf.extend_from_slice(b"xref\n0 3\n");
1102        pdf.extend_from_slice(b"0000000000 65535 f \r\n");
1103        pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
1104        pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
1105        pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
1106        pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
1107        pdf
1108    }
1109
1110    fn str_obj(s: &str) -> Object {
1111        Object::String(PdfString::from_bytes(s.as_bytes().to_vec()))
1112    }
1113
1114    #[test]
1115    fn test_parse_text_field() {
1116        let store = build_store();
1117        let mut dict = HashMap::new();
1118        dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
1119        dict.insert(Name::t(), str_obj("username"));
1120        dict.insert(Name::v(), str_obj("john"));
1121        dict.insert(Name::max_len(), Object::Integer(50));
1122        dict.insert(Name::tu(), str_obj("Enter your name"));
1123
1124        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1125        assert_eq!(field.field_type, FormFieldType::Text);
1126        assert_eq!(field.name, "username");
1127        assert_eq!(field.value.as_deref(), Some("john"));
1128        assert_eq!(field.max_len, Some(50));
1129        assert_eq!(field.tooltip.as_deref(), Some("Enter your name"));
1130    }
1131
1132    #[test]
1133    fn test_parse_button_field_checkbox() {
1134        let store = build_store();
1135        let mut dict = HashMap::new();
1136        dict.insert(Name::ft(), Object::Name(Name::from("Btn")));
1137        dict.insert(Name::t(), str_obj("agree"));
1138        dict.insert(Name::as_name(), Object::Name(Name::from("Yes")));
1139        dict.insert(Name::ff(), Object::Integer(0));
1140
1141        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1142        assert_eq!(field.field_type, FormFieldType::Button);
1143        assert_eq!(field.name, "agree");
1144        assert_eq!(field.appearance_state.as_deref(), Some("Yes"));
1145        assert_eq!(field.flags.bits(), 0);
1146    }
1147
1148    #[test]
1149    fn test_parse_choice_field_with_options() {
1150        let store = build_store();
1151        let mut dict = HashMap::new();
1152        dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
1153        dict.insert(Name::t(), str_obj("color"));
1154
1155        // Options: simple strings
1156        let opts = Object::Array(vec![str_obj("Red"), str_obj("Green"), str_obj("Blue")]);
1157        dict.insert(Name::opt(), opts);
1158
1159        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1160        assert_eq!(field.field_type, FormFieldType::Choice);
1161        assert_eq!(field.options.len(), 3);
1162        assert_eq!(field.options[0].export_value, "Red");
1163        assert_eq!(field.options[0].display_value, "Red");
1164        assert_eq!(field.options[2].export_value, "Blue");
1165    }
1166
1167    #[test]
1168    fn test_parse_choice_field_with_export_display_pairs() {
1169        let store = build_store();
1170        let mut dict = HashMap::new();
1171        dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
1172        dict.insert(Name::t(), str_obj("country"));
1173
1174        // Options: [export_value, display_value] pairs
1175        let opts = Object::Array(vec![Object::Array(vec![
1176            str_obj("US"),
1177            str_obj("United States"),
1178        ])]);
1179        dict.insert(Name::opt(), opts);
1180
1181        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1182        assert_eq!(field.options.len(), 1);
1183        assert_eq!(field.options[0].export_value, "US");
1184        assert_eq!(field.options[0].display_value, "United States");
1185    }
1186
1187    #[test]
1188    fn test_hierarchical_field_name() {
1189        let store = build_store();
1190        let mut dict = HashMap::new();
1191        dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
1192        dict.insert(Name::t(), str_obj("city"));
1193
1194        let field = FormField::from_dict(&dict, &store, None, "address").unwrap();
1195        assert_eq!(field.name, "address.city");
1196    }
1197
1198    #[test]
1199    fn test_inherit_ft_from_parent() {
1200        let store = build_store();
1201        let mut dict = HashMap::new();
1202        // No /FT on this field — inherits from parent
1203        dict.insert(Name::t(), str_obj("line1"));
1204        dict.insert(Name::v(), str_obj("123 Main St"));
1205
1206        let field = FormField::from_dict(&dict, &store, Some("Tx"), "address").unwrap();
1207        assert_eq!(field.field_type, FormFieldType::Text);
1208        assert_eq!(field.name, "address.line1");
1209        assert_eq!(field.value.as_deref(), Some("123 Main St"));
1210    }
1211
1212    #[test]
1213    fn test_field_flags_parsing() {
1214        let store = build_store();
1215        let mut dict = HashMap::new();
1216        dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
1217        dict.insert(Name::t(), str_obj("notes"));
1218        dict.insert(Name::ff(), Object::Integer(4096)); // Multiline flag
1219
1220        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1221        assert_eq!(field.flags.bits(), 4096);
1222    }
1223
1224    #[test]
1225    fn test_no_ft_returns_none() {
1226        let store = build_store();
1227        let mut dict = HashMap::new();
1228        dict.insert(Name::t(), str_obj("orphan"));
1229
1230        let result = FormField::from_dict(&dict, &store, None, "");
1231        assert!(result.is_none());
1232    }
1233
1234    // ---- Mutation tests ----
1235
1236    fn make_field(ft: FormFieldType) -> FormField {
1237        FormField {
1238            name: "test".to_string(),
1239            field_type: ft,
1240            value: None,
1241            default_value: Some("default_val".to_string()),
1242            flags: FormFieldFlags::from_bits(0),
1243            tooltip: None,
1244            alternate_name: None,
1245            mapping_name: None,
1246            max_len: None,
1247            options: Vec::new(),
1248            appearance_state: None,
1249            children: Vec::new(),
1250            controls: Vec::new(),
1251            dirty: false,
1252            selected_indices: Vec::new(),
1253            additional_actions: None,
1254        }
1255    }
1256
1257    #[test]
1258    fn test_set_value_text_field() {
1259        let mut field = make_field(FormFieldType::Text);
1260        field.max_len = Some(10);
1261        field
1262            .set_value(FieldValue::String("hello".to_string()))
1263            .unwrap();
1264        assert_eq!(field.value.as_deref(), Some("hello"));
1265        assert!(field.dirty);
1266    }
1267
1268    #[test]
1269    fn test_set_value_text_field_too_long() {
1270        let mut field = make_field(FormFieldType::Text);
1271        field.max_len = Some(3);
1272        let result = field.set_value(FieldValue::String("toolong".to_string()));
1273        assert!(result.is_err());
1274        assert!(!field.dirty);
1275    }
1276
1277    #[test]
1278    fn test_set_value_button_checked() {
1279        let mut field = make_field(FormFieldType::Button);
1280        field.set_value(FieldValue::Bool(true)).unwrap();
1281        assert_eq!(field.appearance_state.as_deref(), Some("Yes"));
1282        assert_eq!(field.value.as_deref(), Some("Yes"));
1283        assert!(field.dirty);
1284    }
1285
1286    #[test]
1287    fn test_set_value_button_unchecked() {
1288        let mut field = make_field(FormFieldType::Button);
1289        field.set_value(FieldValue::Bool(false)).unwrap();
1290        assert_eq!(field.appearance_state.as_deref(), Some("Off"));
1291    }
1292
1293    #[test]
1294    fn test_set_value_choice_by_index() {
1295        let mut field = make_field(FormFieldType::Choice);
1296        field.options = vec![
1297            ChoiceOption {
1298                export_value: "R".into(),
1299                display_value: "Red".into(),
1300            },
1301            ChoiceOption {
1302                export_value: "G".into(),
1303                display_value: "Green".into(),
1304            },
1305            ChoiceOption {
1306                export_value: "B".into(),
1307                display_value: "Blue".into(),
1308            },
1309        ];
1310        field.set_value(FieldValue::Choice(1)).unwrap();
1311        assert_eq!(field.value.as_deref(), Some("G"));
1312    }
1313
1314    #[test]
1315    fn test_set_value_choice_out_of_bounds() {
1316        let mut field = make_field(FormFieldType::Choice);
1317        field.options = vec![ChoiceOption {
1318            export_value: "R".into(),
1319            display_value: "Red".into(),
1320        }];
1321        let result = field.set_value(FieldValue::Choice(5));
1322        assert!(result.is_err());
1323    }
1324
1325    #[test]
1326    fn test_set_value_type_mismatch() {
1327        let mut field = make_field(FormFieldType::Text);
1328        let result = field.set_value(FieldValue::Bool(true));
1329        assert!(result.is_err());
1330        assert!(!field.dirty);
1331    }
1332
1333    #[test]
1334    fn test_reset_to_default() {
1335        let mut field = make_field(FormFieldType::Text);
1336        field.value = Some("modified".to_string());
1337        field.reset_to_default();
1338        assert_eq!(field.value.as_deref(), Some("default_val"));
1339        assert!(field.dirty);
1340    }
1341
1342    #[test]
1343    fn test_needs_appearance_after_set() {
1344        let mut field = make_field(FormFieldType::Text);
1345        assert!(!field.needs_appearance());
1346        field
1347            .set_value(FieldValue::String("x".to_string()))
1348            .unwrap();
1349        assert!(field.needs_appearance());
1350    }
1351
1352    // ---- FormControl tests ----
1353
1354    #[test]
1355    fn test_controls_accessor_empty() {
1356        let field = make_field(FormFieldType::Text);
1357        assert!(field.controls().is_empty());
1358    }
1359
1360    #[test]
1361    fn test_widget_kids_parsed_as_controls() {
1362        let store = build_store();
1363
1364        // Build a field with /Kids that are widgets (have /Subtype /Widget, no /FT)
1365        let mut widget_dict = HashMap::new();
1366        widget_dict.insert(Name::subtype(), Object::Name(Name::from("Widget")));
1367        widget_dict.insert(
1368            Name::rect(),
1369            Object::Array(vec![
1370                Object::Real(10.0),
1371                Object::Real(20.0),
1372                Object::Real(110.0),
1373                Object::Real(40.0),
1374            ]),
1375        );
1376        widget_dict.insert(Name::as_name(), Object::Name(Name::from("Yes")));
1377
1378        let mut field_dict = HashMap::new();
1379        field_dict.insert(Name::ft(), Object::Name(Name::from("Btn")));
1380        field_dict.insert(Name::t(), str_obj("checkbox"));
1381        field_dict.insert(
1382            Name::kids(),
1383            Object::Array(vec![Object::Dictionary(widget_dict)]),
1384        );
1385
1386        let field = FormField::from_dict(&field_dict, &store, None, "").unwrap();
1387        assert_eq!(field.controls().len(), 1);
1388        assert_eq!(field.controls()[0].field_name, "checkbox");
1389        assert_eq!(field.controls()[0].rect.left, 10.0);
1390        assert_eq!(field.controls()[0].rect.right, 110.0);
1391        assert_eq!(field.controls()[0].appearance_state.as_deref(), Some("Yes"));
1392    }
1393
1394    // ---- Flag helper tests ----
1395
1396    #[test]
1397    fn test_flag_helpers_default() {
1398        let field = make_field(FormFieldType::Text);
1399        assert!(!field.flags.is_read_only());
1400        assert!(!field.flags.is_required());
1401        assert!(!field.flags.is_no_export());
1402        assert!(!field.flags.is_multiline());
1403        assert!(!field.flags.is_password());
1404    }
1405
1406    #[test]
1407    fn test_flag_read_only() {
1408        let mut field = make_field(FormFieldType::Text);
1409        field.flags = FormFieldFlags::from_bits(1 << 0);
1410        assert!(field.flags.is_read_only());
1411    }
1412
1413    #[test]
1414    fn test_flag_required() {
1415        let mut field = make_field(FormFieldType::Text);
1416        field.flags = FormFieldFlags::from_bits(1 << 1);
1417        assert!(field.flags.is_required());
1418    }
1419
1420    #[test]
1421    fn test_flag_no_export() {
1422        let mut field = make_field(FormFieldType::Text);
1423        field.flags = FormFieldFlags::from_bits(1 << 2);
1424        assert!(field.flags.is_no_export());
1425    }
1426
1427    #[test]
1428    fn test_flag_multiline() {
1429        let mut field = make_field(FormFieldType::Text);
1430        field.flags = FormFieldFlags::from_bits(1 << 12);
1431        assert!(field.flags.is_multiline());
1432    }
1433
1434    #[test]
1435    fn test_flag_password() {
1436        let mut field = make_field(FormFieldType::Text);
1437        field.flags = FormFieldFlags::from_bits(1 << 13);
1438        assert!(field.flags.is_password());
1439    }
1440
1441    #[test]
1442    fn test_flag_no_toggle_to_off() {
1443        let mut field = make_field(FormFieldType::Button);
1444        field.flags = FormFieldFlags::from_bits(1 << 14);
1445        assert!(field.flags.is_no_toggle_to_off());
1446    }
1447
1448    #[test]
1449    fn test_flag_radio() {
1450        let mut field = make_field(FormFieldType::Button);
1451        field.flags = FormFieldFlags::from_bits(1 << 15);
1452        assert!(field.flags.is_radio());
1453    }
1454
1455    #[test]
1456    fn test_flag_push_button() {
1457        let mut field = make_field(FormFieldType::Button);
1458        field.flags = FormFieldFlags::from_bits(1 << 16);
1459        assert!(field.flags.is_push_button());
1460    }
1461
1462    #[test]
1463    fn test_flag_combo() {
1464        let mut field = make_field(FormFieldType::Choice);
1465        field.flags = FormFieldFlags::from_bits(1 << 17);
1466        assert!(field.flags.is_combo());
1467    }
1468
1469    #[test]
1470    fn test_flag_combo_editable() {
1471        let mut field = make_field(FormFieldType::Choice);
1472        field.flags = FormFieldFlags::from_bits(1 << 18);
1473        assert!(field.flags.is_combo_editable());
1474    }
1475
1476    #[test]
1477    fn test_flag_sort() {
1478        let mut field = make_field(FormFieldType::Choice);
1479        field.flags = FormFieldFlags::from_bits(1 << 19);
1480        assert!(field.flags.is_sort());
1481    }
1482
1483    #[test]
1484    fn test_flag_file_select() {
1485        let mut field = make_field(FormFieldType::Text);
1486        field.flags = FormFieldFlags::from_bits(1 << 20);
1487        assert!(field.flags.is_file_select());
1488    }
1489
1490    #[test]
1491    fn test_flag_multi_select() {
1492        let mut field = make_field(FormFieldType::Choice);
1493        field.flags = FormFieldFlags::from_bits(1 << 21);
1494        assert!(field.flags.is_multi_select());
1495    }
1496
1497    #[test]
1498    fn test_flag_do_not_spell_check() {
1499        let mut field = make_field(FormFieldType::Text);
1500        field.flags = FormFieldFlags::from_bits(1 << 22);
1501        assert!(field.flags.is_do_not_spell_check());
1502    }
1503
1504    #[test]
1505    fn test_flag_do_not_scroll() {
1506        let mut field = make_field(FormFieldType::Text);
1507        field.flags = FormFieldFlags::from_bits(1 << 23);
1508        assert!(field.flags.is_do_not_scroll());
1509    }
1510
1511    #[test]
1512    fn test_flag_comb() {
1513        let mut field = make_field(FormFieldType::Text);
1514        field.flags = FormFieldFlags::from_bits(1 << 24);
1515        assert!(field.flags.is_comb());
1516    }
1517
1518    #[test]
1519    fn test_flag_rich_text_and_radios_in_unison() {
1520        // kTextRichText and kButtonRadiosInUnison share bit 25.
1521        let mut field = make_field(FormFieldType::Text);
1522        field.flags = FormFieldFlags::from_bits(1 << 25);
1523        assert!(field.flags.is_rich_text());
1524        assert!(field.flags.is_radios_in_unison());
1525    }
1526
1527    #[test]
1528    fn test_flag_commit_on_sel_change() {
1529        let mut field = make_field(FormFieldType::Choice);
1530        field.flags = FormFieldFlags::from_bits(1 << 26);
1531        assert!(field.flags.is_commit_on_sel_change());
1532    }
1533
1534    // ---- Selection API tests ----
1535
1536    #[test]
1537    fn test_selected_indices_empty() {
1538        let field = make_field(FormFieldType::Choice);
1539        assert_eq!(field.selected_item_count(), 0);
1540        assert!(!field.is_item_selected(0));
1541        assert!(field.selected_index(0).is_none());
1542    }
1543
1544    #[test]
1545    fn test_selected_indices_populated() {
1546        let mut field = make_field(FormFieldType::Choice);
1547        field.selected_indices = vec![1, 3];
1548        assert_eq!(field.selected_item_count(), 2);
1549        assert!(!field.is_item_selected(0));
1550        assert!(field.is_item_selected(1));
1551        assert!(!field.is_item_selected(2));
1552        assert!(field.is_item_selected(3));
1553        assert_eq!(field.selected_index(0), Some(1));
1554        assert_eq!(field.selected_index(1), Some(3));
1555        assert!(field.selected_index(2).is_none());
1556    }
1557
1558    // ---- Alternate/mapping name tests ----
1559
1560    #[test]
1561    fn test_alternate_name_from_tooltip() {
1562        let store = build_store();
1563        let mut dict = HashMap::new();
1564        dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
1565        dict.insert(Name::t(), str_obj("field1"));
1566        dict.insert(Name::tu(), str_obj("Enter your name"));
1567        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1568        assert_eq!(field.alternate_name.as_deref(), Some("Enter your name"));
1569        assert_eq!(field.tooltip.as_deref(), Some("Enter your name"));
1570    }
1571
1572    #[test]
1573    fn test_mapping_name_parsed() {
1574        let store = build_store();
1575        let mut dict = HashMap::new();
1576        dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
1577        dict.insert(Name::t(), str_obj("field1"));
1578        dict.insert(Name::tm(), str_obj("export_field1"));
1579        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1580        assert_eq!(field.mapping_name.as_deref(), Some("export_field1"));
1581    }
1582
1583    // ---- Selected indices parsing test ----
1584
1585    #[test]
1586    fn test_selected_indices_parsed_from_dict() {
1587        let store = build_store();
1588        let mut dict = HashMap::new();
1589        dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
1590        dict.insert(Name::t(), str_obj("colors"));
1591        let opts = Object::Array(vec![str_obj("Red"), str_obj("Green"), str_obj("Blue")]);
1592        dict.insert(Name::opt(), opts);
1593        dict.insert(
1594            Name::i(),
1595            Object::Array(vec![Object::Integer(0), Object::Integer(2)]),
1596        );
1597        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1598        assert_eq!(field.selected_indices, vec![0, 2]);
1599        assert!(field.is_item_selected(0));
1600        assert!(!field.is_item_selected(1));
1601        assert!(field.is_item_selected(2));
1602    }
1603
1604    #[test]
1605    fn test_kids_with_ft_not_treated_as_controls() {
1606        let store = build_store();
1607
1608        // A kid with /FT is a child field, not a widget control
1609        let mut child_dict = HashMap::new();
1610        child_dict.insert(Name::subtype(), Object::Name(Name::from("Widget")));
1611        child_dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
1612        child_dict.insert(Name::t(), str_obj("child"));
1613
1614        let mut field_dict = HashMap::new();
1615        field_dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
1616        field_dict.insert(Name::t(), str_obj("parent"));
1617        field_dict.insert(
1618            Name::kids(),
1619            Object::Array(vec![Object::Dictionary(child_dict)]),
1620        );
1621
1622        let field = FormField::from_dict(&field_dict, &store, None, "").unwrap();
1623        // Widget kids with /FT are child fields, not controls
1624        assert!(field.controls().is_empty());
1625    }
1626
1627    // ---- clear_selection test ----
1628
1629    #[test]
1630    fn test_clear_selection_clears_indices() {
1631        let mut field = make_field(FormFieldType::Choice);
1632        field.selected_indices = vec![0, 2];
1633        let result = field.clear_selection();
1634        assert!(result.is_ok());
1635        // In-memory selection cleared
1636        assert!(field.selected_indices.is_empty());
1637    }
1638
1639    // ---- additional_actions accessor test ----
1640
1641    #[test]
1642    fn test_additional_actions_none_by_default() {
1643        let field = make_field(FormFieldType::Text);
1644        assert!(field.additional_actions().is_none());
1645    }
1646
1647    // ---- count_options / get_option_label / get_option_value ----
1648
1649    #[test]
1650    fn test_option_count_returns_options_length() {
1651        let store = build_store();
1652        let mut dict = HashMap::new();
1653        dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
1654        dict.insert(Name::t(), str_obj("colors"));
1655        dict.insert(
1656            Name::opt(),
1657            Object::Array(vec![str_obj("Red"), str_obj("Green"), str_obj("Blue")]),
1658        );
1659        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1660        assert_eq!(field.option_count(), 3);
1661    }
1662
1663    #[test]
1664    fn test_get_option_label_returns_display_value() {
1665        let store = build_store();
1666        let mut dict = HashMap::new();
1667        dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
1668        dict.insert(Name::t(), str_obj("sizes"));
1669        // Two-element array subarray: [export_value, display_value]
1670        dict.insert(
1671            Name::opt(),
1672            Object::Array(vec![
1673                Object::Array(vec![str_obj("sm"), str_obj("Small")]),
1674                Object::Array(vec![str_obj("lg"), str_obj("Large")]),
1675            ]),
1676        );
1677        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1678        assert_eq!(field.get_option_label(0), Some("Small"));
1679        assert_eq!(field.get_option_label(1), Some("Large"));
1680        assert_eq!(field.get_option_label(2), None);
1681    }
1682
1683    #[test]
1684    fn test_get_option_value_returns_export_value() {
1685        let store = build_store();
1686        let mut dict = HashMap::new();
1687        dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
1688        dict.insert(Name::t(), str_obj("sizes"));
1689        dict.insert(
1690            Name::opt(),
1691            Object::Array(vec![
1692                Object::Array(vec![str_obj("sm"), str_obj("Small")]),
1693                Object::Array(vec![str_obj("lg"), str_obj("Large")]),
1694            ]),
1695        );
1696        let field = FormField::from_dict(&dict, &store, None, "").unwrap();
1697        assert_eq!(field.get_option_value(0), Some("sm"));
1698        assert_eq!(field.get_option_value(1), Some("lg"));
1699        assert_eq!(field.get_option_value(99), None);
1700    }
1701
1702    // ---- count_controls / get_control ----
1703
1704    #[test]
1705    fn test_control_count_returns_controls_length() {
1706        let mut field = make_field(FormFieldType::Choice);
1707        assert_eq!(field.control_count(), 0);
1708        use crate::form_control::{FormControl, HighlightingMode, TextPosition};
1709        use rpdfium_core::Rect;
1710        field.controls.push(FormControl {
1711            field_name: "test".to_string(),
1712            rect: Rect::new(0.0, 0.0, 100.0, 20.0),
1713            appearance_state: None,
1714            page_index: None,
1715            highlighting_mode: HighlightingMode::Invert,
1716            rotation: 0,
1717            border_color: None,
1718            background_color: None,
1719            caption: None,
1720            rollover_caption: None,
1721            alt_caption: None,
1722            default_appearance: None,
1723            normal_icon: None,
1724            rollover_icon: None,
1725            down_icon: None,
1726            icon_fit: None,
1727            text_position: TextPosition::CaptionOnly,
1728        });
1729        assert_eq!(field.control_count(), 1);
1730    }
1731
1732    #[test]
1733    fn test_get_control_returns_correct_control() {
1734        let mut field = make_field(FormFieldType::Text);
1735        use crate::form_control::{FormControl, HighlightingMode, TextPosition};
1736        use rpdfium_core::Rect;
1737        let ctrl = FormControl {
1738            field_name: "f".to_string(),
1739            rect: Rect::new(10.0, 10.0, 110.0, 30.0),
1740            appearance_state: None,
1741            page_index: Some(0),
1742            highlighting_mode: HighlightingMode::None,
1743            rotation: 0,
1744            border_color: None,
1745            background_color: None,
1746            caption: None,
1747            rollover_caption: None,
1748            alt_caption: None,
1749            default_appearance: None,
1750            normal_icon: None,
1751            rollover_icon: None,
1752            down_icon: None,
1753            icon_fit: None,
1754            text_position: TextPosition::CaptionOnly,
1755        };
1756        field.controls.push(ctrl);
1757        assert!(field.get_control(0).is_some());
1758        assert_eq!(field.get_control(0).unwrap().page_index, Some(0));
1759        assert!(field.get_control(1).is_none());
1760    }
1761
1762    // -----------------------------------------------------------------------
1763    // Upstream: TEST(CPDFFormFieldTest, IsItemSelected)
1764    //
1765    // Multi-select choice field with /V (values) and /I (selected indices)
1766    // in various combinations. Tests the conflict resolution logic.
1767    // -----------------------------------------------------------------------
1768
1769    /// Upstream: TEST(CPDFFormFieldTest, IsItemSelected)
1770    ///
1771    /// Exercises `is_item_selected()` across 18 sub-cases with different
1772    /// combinations of /V (values), /I (selected indices), and conflicts.
1773    ///
1774    /// In rpdfium, `FormField::from_dict` currently populates `selected_indices`
1775    /// from /I only. The full /V vs /I conflict resolution logic (which
1776    /// `UseSelectedIndicesObject` mediates in upstream) is tested here using
1777    /// direct in-memory construction to validate `is_item_selected()`.
1778    #[test]
1779    fn test_cpdf_form_field_is_item_selected() {
1780        // Build a choice field with 5 options
1781        let options = vec![
1782            ChoiceOption {
1783                export_value: "Alpha".into(),
1784                display_value: "Alpha".into(),
1785            },
1786            ChoiceOption {
1787                export_value: "Beta".into(),
1788                display_value: "Beta".into(),
1789            },
1790            ChoiceOption {
1791                export_value: "Gamma".into(),
1792                display_value: "Gamma".into(),
1793            },
1794            ChoiceOption {
1795                export_value: "Delta".into(),
1796                display_value: "Delta".into(),
1797            },
1798            ChoiceOption {
1799                export_value: "Epsilon".into(),
1800                display_value: "Epsilon".into(),
1801            },
1802        ];
1803
1804        let make_choice_field = |selected: Vec<usize>| -> FormField {
1805            FormField {
1806                name: "multi".to_string(),
1807                field_type: FormFieldType::Choice,
1808                value: None,
1809                default_value: None,
1810                flags: FormFieldFlags::from_bits(1 << 21), // MultiSelect
1811                tooltip: None,
1812                alternate_name: None,
1813                mapping_name: None,
1814                max_len: None,
1815                options: options.clone(),
1816                appearance_state: None,
1817                children: Vec::new(),
1818                controls: Vec::new(),
1819                dirty: false,
1820                selected_indices: selected,
1821                additional_actions: None,
1822            }
1823        };
1824
1825        // Case 1: No selections
1826        {
1827            let field = make_choice_field(vec![]);
1828            for i in 0..5 {
1829                assert!(!field.is_item_selected(i));
1830            }
1831            // Out of range
1832            assert!(!field.is_item_selected(5));
1833        }
1834
1835        // Case 2: Single selection
1836        {
1837            let field = make_choice_field(vec![2]); // Gamma
1838            assert!(!field.is_item_selected(0));
1839            assert!(!field.is_item_selected(1));
1840            assert!(field.is_item_selected(2));
1841            assert!(!field.is_item_selected(3));
1842            assert!(!field.is_item_selected(4));
1843        }
1844
1845        // Case 3: Multiple selections
1846        {
1847            let field = make_choice_field(vec![0, 2, 3]);
1848            assert!(field.is_item_selected(0));
1849            assert!(!field.is_item_selected(1));
1850            assert!(field.is_item_selected(2));
1851            assert!(field.is_item_selected(3));
1852            assert!(!field.is_item_selected(4));
1853        }
1854
1855        // Case 4: Selections with some out-of-range indices
1856        {
1857            let field = make_choice_field(vec![0, 2, 3, 12, 42]);
1858            assert!(field.is_item_selected(0));
1859            assert!(!field.is_item_selected(1));
1860            assert!(field.is_item_selected(2));
1861            assert!(field.is_item_selected(3));
1862            assert!(!field.is_item_selected(4));
1863            // Out-of-range indices are stored but don't match valid options
1864            assert!(!field.is_item_selected(5));
1865            // But is_item_selected just does contains(), so 12 and 42 return true
1866            assert!(field.is_item_selected(12));
1867            assert!(field.is_item_selected(42));
1868        }
1869
1870        // Case 5: selected_item_count
1871        {
1872            let field = make_choice_field(vec![1, 4]);
1873            assert_eq!(field.selected_item_count(), 2);
1874            assert_eq!(field.count_selected_items(), 2);
1875        }
1876
1877        // Case 6: selected_index
1878        {
1879            let field = make_choice_field(vec![1, 4]);
1880            assert_eq!(field.selected_index(0), Some(1));
1881            assert_eq!(field.selected_index(1), Some(4));
1882            assert_eq!(field.selected_index(2), None);
1883        }
1884    }
1885}