Skip to main content

pdfrum_doc/form/
field.rs

1//! The AcroForm field model: what a field *is*, what it holds, and what
2//! changing it implies.
3//!
4//! A field is a dictionary reachable from the catalog's `/AcroForm /Fields`,
5//! and the tree it sits in is not the annotation tree: an interior node may
6//! carry no widget at all and exist only to prefix its children's names, and
7//! a leaf may be merged with its own widget annotation into one dictionary.
8//! Both shapes are common and neither is an error, so the walk here collects
9//! **terminal** fields — the nodes that carry an `/FT`, inherited or not —
10//! and treats everything above them as naming structure.
11//!
12//! # Values are not stored here
13//!
14//! [`Field`] is a record of where a field lives, not a copy of what it holds:
15//! its value is read back out of the dictionary on demand. That is what makes
16//! [`FieldValues`] — the edit buffer — the only mutable thing in this module,
17//! and it is why reading a field never has to be told whether someone has
18//! written to it.
19
20use pdfrum_common::{Diagnostics, Limits};
21use pdfrum_object::{Dict, Name, ObjRef, Object, Resolve};
22
23use crate::ap::{self, GeneratedAp};
24use crate::form::attr::{field_attr, full_name};
25use crate::names;
26
27/// What kind of control a form field is (ISO 32000-1 §12.7.4).
28///
29/// The variants are the `/FT` values crossed with the two `/Ff` bits that
30/// split them: a `/Btn` is a push button, a radio button or a check box
31/// depending on bits 17 and 16, and a `/Ch` is a combo box or a list box
32/// depending on bit 18.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum FieldKind {
35    /// A text field (`/Tx`).
36    Text,
37    /// A check box (`/Btn` with neither the push-button nor the radio bit).
38    Check,
39    /// A radio button (`/Btn` with bit 16 set).
40    Radio,
41    /// A push button (`/Btn` with bit 17 set) — it holds no value.
42    Button,
43    /// A drop-down (`/Ch` with bit 18 set).
44    Combo,
45    /// A list box (`/Ch` without bit 18).
46    List,
47    /// A signature field (`/Sig`).
48    Signature,
49}
50
51impl FieldKind {
52    /// Classifies a field from its `/FT` and `/Ff`.
53    ///
54    /// Returns `None` for a node with no field type at all, which is how the
55    /// walk tells a naming-only interior node from a terminal field.
56    ///
57    /// ```
58    /// use pdfrum_doc::form::{FieldFlags, FieldKind};
59    ///
60    /// assert_eq!(FieldKind::classify(b"Tx", FieldFlags::default()), Some(FieldKind::Text));
61    /// // A `/Btn` splits three ways on its flags.
62    /// let radio = FieldFlags::from_bits(1 << 15);
63    /// assert_eq!(FieldKind::classify(b"Btn", radio), Some(FieldKind::Radio));
64    /// assert_eq!(FieldKind::classify(b"Btn", FieldFlags::default()), Some(FieldKind::Check));
65    /// // No field type at all: an interior naming node, not a field.
66    /// assert_eq!(FieldKind::classify(b"", FieldFlags::default()), None);
67    /// ```
68    #[must_use]
69    pub fn classify(field_type: &[u8], flags: FieldFlags) -> Option<FieldKind> {
70        match field_type {
71            b"Tx" => Some(FieldKind::Text),
72            b"Sig" => Some(FieldKind::Signature),
73            b"Btn" => Some(if flags.is_push_button() {
74                FieldKind::Button
75            } else if flags.is_radio() {
76                FieldKind::Radio
77            } else {
78                FieldKind::Check
79            }),
80            b"Ch" => Some(if flags.is_combo() {
81                FieldKind::Combo
82            } else {
83                FieldKind::List
84            }),
85            _ => None,
86        }
87    }
88
89    /// Whether the field holds a value a caller can write.
90    ///
91    /// False only for [`FieldKind::Button`], which fires an action rather
92    /// than storing anything, and [`FieldKind::Signature`], whose value is a
93    /// signature dictionary this crate does not synthesize.
94    ///
95    /// ```
96    /// use pdfrum_doc::form::FieldKind;
97    ///
98    /// assert!(FieldKind::Text.is_writable());
99    /// assert!(!FieldKind::Button.is_writable());
100    /// assert!(!FieldKind::Signature.is_writable());
101    /// ```
102    #[must_use]
103    pub fn is_writable(self) -> bool {
104        !matches!(self, FieldKind::Button | FieldKind::Signature)
105    }
106
107    /// Whether the field is one of the two on/off controls, whose value is a
108    /// state name rather than free text.
109    ///
110    /// ```
111    /// use pdfrum_doc::form::FieldKind;
112    ///
113    /// assert!(FieldKind::Check.is_toggle());
114    /// assert!(FieldKind::Radio.is_toggle());
115    /// assert!(!FieldKind::Combo.is_toggle());
116    /// ```
117    #[must_use]
118    pub fn is_toggle(self) -> bool {
119        matches!(self, FieldKind::Check | FieldKind::Radio)
120    }
121
122    /// Whether the kind picks from `/Opt` — a drop-down or a list box.
123    ///
124    /// These are the two that carry `/I` alongside `/V`, which is why a write
125    /// has to keep the pair agreeing.
126    ///
127    /// ```
128    /// use pdfrum_doc::form::FieldKind;
129    /// assert!(FieldKind::Combo.is_choice());
130    /// assert!(FieldKind::List.is_choice());
131    /// assert!(!FieldKind::Text.is_choice());
132    /// ```
133    #[must_use]
134    pub fn is_choice(self) -> bool {
135        matches!(self, FieldKind::Combo | FieldKind::List)
136    }
137}
138
139/// A field's `/Ff` flag word (ISO 32000-1 tables 227–230).
140///
141/// Kept as the raw word rather than a set, and **deliberately without the
142/// `contains` / `union` algebra its two sibling flag types have**: the meaning
143/// of a bit depends on the field type, so the same bit 26 is "file select" on
144/// a text field and "sort" on a choice field, and `FieldFlags::COMBO |
145/// FieldFlags::MULTILINE` would be a lie. The predicates below — the ones
146/// whose reading is type-independent or whose type is implied by the name —
147/// are the API. [`FieldFlags::bits`] and [`FieldFlags::from_bits`] exist for
148/// round-tripping the word itself, unknown bits included.
149///
150/// ```
151/// use pdfrum_doc::form::FieldFlags;
152///
153/// // Bit 1 is `ReadOnly` on every field type.
154/// assert!(FieldFlags::from_bits(1).is_read_only());
155/// // A reserved bit survives the trip.
156/// assert_eq!(FieldFlags::from_bits(1 << 40).bits(), 1 << 40);
157/// ```
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
159pub struct FieldFlags(i64);
160
161impl FieldFlags {
162    /// The raw `/Ff` word, including every bit no predicate here reads.
163    ///
164    /// ```
165    /// use pdfrum_doc::form::FieldFlags;
166    ///
167    /// assert_eq!(FieldFlags::from_bits(1 << 12).bits(), 1 << 12);
168    /// ```
169    #[must_use]
170    pub const fn bits(self) -> i64 {
171        self.0
172    }
173
174    /// The word as written in the file. **Unknown bits are retained**: a bit
175    /// whose meaning belongs to a `/FT` this type knows nothing about is
176    /// kept, not dropped.
177    ///
178    /// ```
179    /// use pdfrum_doc::form::FieldFlags;
180    ///
181    /// assert!(FieldFlags::from_bits(1).is_read_only());
182    /// // A bit belonging to a `/FT` this type knows nothing about survives.
183    /// assert_eq!(FieldFlags::from_bits(1 << 40).bits(), 1 << 40);
184    /// ```
185    #[must_use]
186    pub const fn from_bits(bits: i64) -> Self {
187        Self(bits)
188    }
189
190    /// Bit 1: the field may not be changed.
191    ///
192    /// ```
193    /// use pdfrum_doc::form::FieldFlags;
194    ///
195    /// assert!(FieldFlags::from_bits(1 << 0).is_read_only());
196    /// ```
197    #[must_use]
198    pub const fn is_read_only(self) -> bool {
199        self.0 & (1 << 0) != 0
200    }
201
202    /// Bit 2: the field must have a value when the form is submitted.
203    ///
204    /// ```
205    /// use pdfrum_doc::form::FieldFlags;
206    ///
207    /// assert!(FieldFlags::from_bits(1 << 1).is_required());
208    /// ```
209    #[must_use]
210    pub const fn is_required(self) -> bool {
211        self.0 & (1 << 1) != 0
212    }
213
214    /// Bit 16, on a `/Btn`: the field is a radio button rather than a check
215    /// box.
216    ///
217    /// ```
218    /// use pdfrum_doc::form::FieldFlags;
219    ///
220    /// assert!(FieldFlags::from_bits(1 << 15).is_radio());
221    /// ```
222    #[must_use]
223    pub const fn is_radio(self) -> bool {
224        self.0 & (1 << 15) != 0
225    }
226
227    /// Bit 17, on a `/Btn`: the field is a push button and holds no value.
228    ///
229    /// ```
230    /// use pdfrum_doc::form::FieldFlags;
231    ///
232    /// assert!(FieldFlags::from_bits(1 << 16).is_push_button());
233    /// ```
234    #[must_use]
235    pub const fn is_push_button(self) -> bool {
236        self.0 & (1 << 16) != 0
237    }
238
239    /// Bit 18, on a `/Ch`: the field is a drop-down rather than a list box.
240    ///
241    /// ```
242    /// use pdfrum_doc::form::FieldFlags;
243    ///
244    /// assert!(FieldFlags::from_bits(1 << 17).is_combo());
245    /// ```
246    #[must_use]
247    pub const fn is_combo(self) -> bool {
248        self.0 & (1 << 17) != 0
249    }
250
251    /// Bit 19, on a `/Ch`: the combo box includes an editable text box.
252    ///
253    /// ```
254    /// use pdfrum_doc::form::FieldFlags;
255    ///
256    /// assert!(FieldFlags::from_bits(1 << 18).is_editable_combo());
257    /// ```
258    #[must_use]
259    pub const fn is_editable_combo(self) -> bool {
260        self.0 & (1 << 18) != 0
261    }
262
263    /// Bit 22, on a `/Ch`: more than one option may be selected at once.
264    ///
265    /// ```
266    /// use pdfrum_doc::form::FieldFlags;
267    ///
268    /// assert!(FieldFlags::from_bits(1 << 21).is_multi_select());
269    /// ```
270    #[must_use]
271    pub const fn is_multi_select(self) -> bool {
272        self.0 & (1 << 21) != 0
273    }
274
275    /// Bit 13, on a `/Tx`: the field accepts more than one line.
276    ///
277    /// ```
278    /// use pdfrum_doc::form::FieldFlags;
279    ///
280    /// assert!(FieldFlags::from_bits(1 << 12).is_multiline());
281    /// ```
282    #[must_use]
283    pub const fn is_multiline(self) -> bool {
284        self.0 & (1 << 12) != 0
285    }
286
287    /// Bit 14, on a `/Tx`: the field's contents are obscured as they are
288    /// typed.
289    ///
290    /// ```
291    /// use pdfrum_doc::form::FieldFlags;
292    ///
293    /// assert!(FieldFlags::from_bits(1 << 13).is_password());
294    /// ```
295    #[must_use]
296    pub const fn is_password(self) -> bool {
297        self.0 & (1 << 13) != 0
298    }
299
300    /// Bit 25, on a `/Tx`: the text is laid out in equally spaced cells.
301    ///
302    /// ```
303    /// use pdfrum_doc::form::FieldFlags;
304    ///
305    /// assert!(FieldFlags::from_bits(1 << 24).is_comb());
306    /// ```
307    #[must_use]
308    #[doc(alias = "Comb")]
309    pub const fn is_comb(self) -> bool {
310        self.0 & (1 << 24) != 0
311    }
312
313    /// Bit 24, on a `/Tx`: whether the field scrolls to fit more text than
314    /// its rectangle holds.
315    ///
316    /// The positive reading of the spec's `DoNotScroll` bit: a field scrolls
317    /// *unless* the bit is set.
318    ///
319    /// ```
320    /// use pdfrum_doc::form::FieldFlags;
321    ///
322    /// // The positive reading: a field scrolls unless `DoNotScroll` is set.
323    /// assert!(FieldFlags::default().scrolls());
324    /// assert!(!FieldFlags::from_bits(1 << 23).scrolls());
325    /// ```
326    #[must_use]
327    #[doc(alias = "DoNotScroll")]
328    pub const fn scrolls(self) -> bool {
329        self.0 & (1 << 23) == 0
330    }
331
332    /// Bit 23, on a `/Tx` or `/Ch`: whether the value is spell-checked.
333    ///
334    /// The positive reading of the spec's `DoNotSpellCheck` bit.
335    ///
336    /// ```
337    /// use pdfrum_doc::form::FieldFlags;
338    ///
339    /// assert!(FieldFlags::default().spell_checks());
340    /// assert!(!FieldFlags::from_bits(1 << 22).spell_checks());
341    /// ```
342    #[must_use]
343    #[doc(alias = "DoNotSpellCheck")]
344    pub const fn spell_checks(self) -> bool {
345        self.0 & (1 << 22) == 0
346    }
347}
348
349/// One terminal form field.
350///
351/// A record of *where* the field is — the dictionary, the reference that names
352/// it, its widgets — plus the classification derived once at load. What it
353/// currently holds is read back through [`Field::value`], because a
354/// [`FieldValues`] edit may have superseded the file's own `/V`.
355#[derive(Debug, Clone, PartialEq)]
356pub struct Field {
357    /// The field's own dictionary.
358    pub dict: Dict,
359    /// The reference that names it, when it has one. A field written inline
360    /// in its parent's `/Kids` has none, and cannot be written back.
361    pub reference: Option<ObjRef>,
362    /// The fully-qualified name: the ancestors' `/T` values and its own,
363    /// joined with dots.
364    pub name: String,
365    /// What kind of control it is.
366    pub kind: FieldKind,
367    /// The `/Ff` flag word, read through the inheritance chain.
368    pub flags: FieldFlags,
369    /// The widget annotations that draw it.
370    ///
371    /// Usually one. A radio group has one per button, and a field whose
372    /// dictionary *is* its widget has one that is the field itself.
373    pub widgets: Vec<Widget>,
374}
375
376/// One widget annotation drawing a field.
377#[derive(Debug, Clone, PartialEq)]
378pub struct Widget {
379    /// The widget's dictionary. Equal to the field's when the two are merged.
380    pub dict: Dict,
381    /// The reference naming it, when it has one.
382    pub reference: Option<ObjRef>,
383}
384
385impl Field {
386    /// The field's current value, as text.
387    ///
388    /// `values` is consulted first, so a field written through
389    /// [`FieldValues::set`] reads back as what was written rather than what
390    /// the file holds. Pass `None` to read the file's own `/V`.
391    ///
392    /// For a check box or radio button this is the *state name* — `Off` for
393    /// clear, and whatever the widget's `/AP /N` calls its on-state
394    /// otherwise. Use [`Field::is_checked`] for the boolean.
395    ///
396    /// ```
397    /// use pdfrum_common::{Diagnostics, Limits};
398    /// use pdfrum_doc::form::Form;
399    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
400    ///
401    /// let field = Dict::from_pairs([
402    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
403    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
404    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
405    /// ]);
406    /// let catalog = Dict::from_pairs([(
407    ///     Name::from("AcroForm"),
408    ///     Object::Dict(Dict::from_pairs([(
409    ///         Name::from("Fields"),
410    ///         Object::Array(Array::of([Object::Dict(field)])),
411    ///     )])),
412    /// )]);
413    ///
414    /// let mut diags = Diagnostics::default();
415    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
416    ///     .expect("the catalog declares an /AcroForm");
417    /// use pdfrum_doc::form::FieldValues;
418    ///
419    /// let field = form.field("name").expect("one terminal field");
420    /// assert_eq!(field.value(None, &NoResolve), "Ada");
421    ///
422    /// // An edit supersedes the file's own `/V`.
423    /// let mut values = FieldValues::new();
424    /// values.set("name", "Grace");
425    /// assert_eq!(field.value(Some(&values), &NoResolve), "Grace");
426    /// ```
427    #[must_use]
428    pub fn value<R: Resolve>(&self, values: Option<&FieldValues>, r: &R) -> String {
429        if let Some(edited) = values.and_then(|values| values.get(&self.name)) {
430            return edited.to_owned();
431        }
432        self.stored_value(r)
433    }
434
435    /// The value the *file* holds, ignoring any edit.
436    ///
437    /// ```
438    /// use pdfrum_common::{Diagnostics, Limits};
439    /// use pdfrum_doc::form::Form;
440    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
441    ///
442    /// let field = Dict::from_pairs([
443    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
444    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
445    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
446    /// ]);
447    /// let catalog = Dict::from_pairs([(
448    ///     Name::from("AcroForm"),
449    ///     Object::Dict(Dict::from_pairs([(
450    ///         Name::from("Fields"),
451    ///         Object::Array(Array::of([Object::Dict(field)])),
452    ///     )])),
453    /// )]);
454    ///
455    /// let mut diags = Diagnostics::default();
456    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
457    ///     .expect("the catalog declares an /AcroForm");
458    ///
459    /// let field = form.field("name").expect("one terminal field");
460    /// assert_eq!(field.stored_value(&NoResolve), "Ada");
461    /// ```
462    #[must_use]
463    pub fn stored_value<R: Resolve>(&self, r: &R) -> String {
464        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
465        field_attr(&self.dict, names::V, r, &limits, &mut diags)
466            .map(|value| value.to_text())
467            .unwrap_or_default()
468    }
469
470    /// The field's default value (`/DV`) — what a form reset restores.
471    #[must_use]
472    pub fn default_value<R: Resolve>(&self, r: &R) -> String {
473        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
474        field_attr(&self.dict, names::DV, r, &limits, &mut diags)
475            .map(|value| value.to_text())
476            .unwrap_or_default()
477    }
478
479    /// Whether a check box or radio button is on.
480    ///
481    /// Always false for a field that is not a toggle. A state of `Off`, and an
482    /// absent state, both read as clear — every other name is on, which is the
483    /// spec's own rule.
484    #[must_use]
485    pub fn is_checked<R: Resolve>(&self, values: Option<&FieldValues>, r: &R) -> bool {
486        if !self.kind.is_toggle() {
487            return false;
488        }
489        let value = self.value(values, r);
490        !value.is_empty() && value != "Off"
491    }
492
493    /// The states a check box or radio button can take, from its widgets'
494    /// `/AP /N` sub-dictionaries.
495    ///
496    /// `Off` is included when a widget offers it. The order is the widgets'
497    /// order, then each widget's own appearance-dictionary order.
498    #[must_use]
499    pub fn states<R: Resolve>(&self, r: &R) -> Vec<String> {
500        let mut out: Vec<String> = Vec::new();
501        for widget in &self.widgets {
502            let Some(ap) = widget.dict.dict(names::AP, r) else {
503                continue;
504            };
505            let Some(normal) = ap.dict(names::N, r) else {
506                continue;
507            };
508            for key in normal.keys() {
509                let state = String::from_utf8_lossy(key.as_bytes()).into_owned();
510                if !out.contains(&state) {
511                    out.push(state);
512                }
513            }
514        }
515        out
516    }
517
518    /// A choice field's selectable options (`/Opt`).
519    ///
520    /// An entry written as a two-element array is an export-value/label pair;
521    /// the label is what a reader shows, and that is what comes back here.
522    #[must_use]
523    pub fn options<R: Resolve>(&self, r: &R) -> Vec<String> {
524        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
525        let Some(opt) = field_attr(&self.dict, names::OPT, r, &limits, &mut diags) else {
526            return Vec::new();
527        };
528        let Some(array) = opt.as_array() else {
529            return Vec::new();
530        };
531        (0..array.len())
532            .map(|index| {
533                let Some(entry) = array.get(index, r) else {
534                    return String::new();
535                };
536                // A pair is [export, label]; a bare string is both.
537                match entry.get() {
538                    Object::Array(pair) => pair
539                        .raw_at(1)
540                        .or_else(|| pair.raw_at(0))
541                        .map(Object::to_text)
542                        .unwrap_or_default(),
543                    object => object.to_text(),
544                }
545            })
546            .collect()
547    }
548
549    /// The field's user-facing tooltip (`/TU`), when it has one.
550    #[must_use]
551    pub fn tooltip<R: Resolve>(&self, r: &R) -> Option<String> {
552        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
553        field_attr(&self.dict, names::TU, r, &limits, &mut diags).map(|value| value.to_text())
554    }
555}
556
557/// Every terminal field of a document's interactive form.
558///
559/// Loaded once from the catalog; the walk is the expensive part and nothing
560/// below repeats it.
561#[derive(Debug, Clone, Default, PartialEq)]
562pub struct Form {
563    /// The terminal fields, in the order the `/Fields` tree reaches them.
564    pub fields: Vec<Field>,
565    /// Whether the form asks a reader to regenerate every widget's appearance
566    /// (`/NeedAppearances`).
567    pub need_appearances: bool,
568}
569
570/// How deep the field tree may nest before the walk gives up.
571///
572/// The same cap the name-tree walks use, for the same reason: a `/Kids` cycle
573/// is stopped by the visited set, but a legitimately deep tree still has to
574/// end somewhere.
575const MAX_FIELD_DEPTH: u32 = 32;
576
577impl Form {
578    /// Loads the document's form, or nothing when the catalog declares none.
579    ///
580    /// A catalog with an `/AcroForm` whose `/Fields` is absent or empty still
581    /// yields a `Form` — an empty form is a different thing from no form, and
582    /// only the second means "this document is not interactive".
583    ///
584    /// ```
585    /// use pdfrum_common::{Diagnostics, Limits};
586    /// use pdfrum_doc::form::Form;
587    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
588    ///
589    /// let field = Dict::from_pairs([
590    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
591    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
592    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
593    /// ]);
594    /// let catalog = Dict::from_pairs([(
595    ///     Name::from("AcroForm"),
596    ///     Object::Dict(Dict::from_pairs([(
597    ///         Name::from("Fields"),
598    ///         Object::Array(Array::of([Object::Dict(field)])),
599    ///     )])),
600    /// )]);
601    ///
602    /// let mut diags = Diagnostics::default();
603    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
604    ///     .expect("the catalog declares an /AcroForm");
605    ///
606    /// assert_eq!(form.len(), 1);
607    /// assert_eq!(form.fields[0].name, "name");
608    /// ```
609    #[must_use]
610    pub fn load<R: Resolve>(
611        catalog: &Dict,
612        r: &R,
613        limits: &Limits,
614        diags: &mut Diagnostics,
615    ) -> Option<Form> {
616        let acro = catalog.dict(names::ACRO_FORM, r)?;
617        let need_appearances = acro
618            .get(names::NEED_APPEARANCES, r)
619            .and_then(|value| value.as_direct().and_then(Object::as_bool))
620            .unwrap_or(false);
621        let mut form = Form {
622            fields: Vec::new(),
623            need_appearances,
624        };
625        let Some(fields) = acro.array(names::FIELDS, r) else {
626            return Some(form);
627        };
628        let mut seen: Vec<ObjRef> = Vec::new();
629        for index in 0..fields.len() {
630            let reference = fields.reference_at(index);
631            let Some(dict) = fields.dict_at(index, r) else {
632                continue;
633            };
634            visit(
635                &dict,
636                reference,
637                0,
638                &mut seen,
639                &mut form.fields,
640                r,
641                limits,
642                diags,
643            );
644        }
645        Some(form)
646    }
647
648    /// How many terminal fields the form has.
649    ///
650    /// ```
651    /// use pdfrum_common::{Diagnostics, Limits};
652    /// use pdfrum_doc::form::Form;
653    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
654    ///
655    /// let field = Dict::from_pairs([
656    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
657    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
658    /// ]);
659    /// let catalog = Dict::from_pairs([(
660    ///     Name::from("AcroForm"),
661    ///     Object::Dict(Dict::from_pairs([(
662    ///         Name::from("Fields"),
663    ///         Object::Array(Array::of([Object::Dict(field)])),
664    ///     )])),
665    /// )]);
666    ///
667    /// let mut diags = Diagnostics::default();
668    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
669    ///     .expect("the catalog declares an /AcroForm");
670    ///
671    /// assert_eq!(form.len(), 1);
672    /// ```
673    #[must_use]
674    pub fn len(&self) -> usize {
675        self.fields.len()
676    }
677
678    /// Whether the form has no fields at all.
679    ///
680    /// ```
681    /// use pdfrum_common::{Diagnostics, Limits};
682    /// use pdfrum_doc::form::Form;
683    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
684    ///
685    /// let field = Dict::from_pairs([
686    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
687    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
688    /// ]);
689    /// let catalog = Dict::from_pairs([(
690    ///     Name::from("AcroForm"),
691    ///     Object::Dict(Dict::from_pairs([(
692    ///         Name::from("Fields"),
693    ///         Object::Array(Array::of([Object::Dict(field)])),
694    ///     )])),
695    /// )]);
696    ///
697    /// let mut diags = Diagnostics::default();
698    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
699    ///     .expect("the catalog declares an /AcroForm");
700    ///
701    /// assert!(!form.is_empty());
702    /// ```
703    #[must_use]
704    pub fn is_empty(&self) -> bool {
705        self.fields.is_empty()
706    }
707
708    /// The field with this fully-qualified name.
709    ///
710    /// ```
711    /// use pdfrum_common::{Diagnostics, Limits};
712    /// use pdfrum_doc::form::Form;
713    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
714    ///
715    /// let field = Dict::from_pairs([
716    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
717    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
718    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
719    /// ]);
720    /// let catalog = Dict::from_pairs([(
721    ///     Name::from("AcroForm"),
722    ///     Object::Dict(Dict::from_pairs([(
723    ///         Name::from("Fields"),
724    ///         Object::Array(Array::of([Object::Dict(field)])),
725    ///     )])),
726    /// )]);
727    ///
728    /// let mut diags = Diagnostics::default();
729    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
730    ///     .expect("the catalog declares an /AcroForm");
731    ///
732    /// assert!(form.field("name").is_some());
733    /// assert!(form.field("absent").is_none());
734    /// ```
735    #[must_use]
736    pub fn field(&self, name: &str) -> Option<&Field> {
737        self.fields.iter().find(|field| field.name == name)
738    }
739
740    /// The order a recalculation visits fields in, read from `/AcroForm /CO`.
741    ///
742    /// Indices into [`Form::fields`], in the order the array lists them.
743    ///
744    /// # An absent `/CO` is the answer, not a fallback
745    ///
746    /// A document with no `/CO` array recalculates **nothing**, however many
747    /// of its fields carry an `/AA /C` script: the sweep that drives
748    /// calculation walks exactly this list and nothing else. So an empty
749    /// answer here is "no calculation runs", and a reader tempted to fall
750    /// back to "every field, in `/Fields` order" would recalculate documents
751    /// that must be left alone — visibly, on any file with a calculation
752    /// script and no `/CO`.
753    ///
754    /// Entries that resolve to nothing, to a non-dictionary, or to a
755    /// dictionary that is not one of this form's terminal fields are dropped.
756    /// Duplicates are kept: the array is the order, and it is indexed
757    /// positionally.
758    ///
759    /// ```
760    /// use pdfrum_common::{Diagnostics, Limits};
761    /// use pdfrum_doc::form::Form;
762    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
763    ///
764    /// let field = Dict::from_pairs([
765    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
766    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
767    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
768    /// ]);
769    /// let catalog = Dict::from_pairs([(
770    ///     Name::from("AcroForm"),
771    ///     Object::Dict(Dict::from_pairs([(
772    ///         Name::from("Fields"),
773    ///         Object::Array(Array::of([Object::Dict(field)])),
774    ///     )])),
775    /// )]);
776    ///
777    /// let mut diags = Diagnostics::default();
778    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
779    ///     .expect("the catalog declares an /AcroForm");
780    ///
781    /// // No `/CO`: nothing recalculates. That is the answer, not a fallback.
782    /// assert!(form.calculation_order(&catalog, &NoResolve).is_empty());
783    /// ```
784    #[must_use]
785    pub fn calculation_order<R: Resolve>(&self, catalog: &Dict, r: &R) -> Vec<usize> {
786        let Some(acro) = catalog.dict(names::ACRO_FORM, r) else {
787            return Vec::new();
788        };
789        let Some(order) = acro.array(names::CALCULATION_ORDER, r) else {
790            return Vec::new();
791        };
792        let mut out = Vec::new();
793        for index in 0..order.len() {
794            // The reference identifies the field where there is one, which is
795            // the ordinary shape — `/CO` holds indirect references to the same
796            // field dictionaries `/Fields` does. A directly-written entry is
797            // matched on the dictionary itself, which is what `GetFieldByDict`
798            // compares.
799            let reference = order.reference_at(index);
800            let dict = order.dict_at(index, r);
801            let found = self
802                .fields
803                .iter()
804                .position(|field| match (reference, &dict) {
805                    (Some(reference), _) if field.reference == Some(reference) => true,
806                    (_, Some(dict)) => field.reference.is_none() && &field.dict == dict,
807                    _ => false,
808                });
809            if let Some(found) = found {
810                out.push(found);
811            }
812        }
813        out
814    }
815}
816
817/// Walks one node of the field tree, collecting the terminal fields under it.
818#[allow(clippy::too_many_arguments)]
819fn visit<R: Resolve>(
820    dict: &Dict,
821    reference: Option<ObjRef>,
822    depth: u32,
823    seen: &mut Vec<ObjRef>,
824    out: &mut Vec<Field>,
825    r: &R,
826    limits: &Limits,
827    diags: &mut Diagnostics,
828) {
829    if depth > MAX_FIELD_DEPTH {
830        diags.record(
831            pdfrum_common::Severity::Suspicious,
832            pdfrum_common::DiagKind::TreeDepthExceeded,
833            None,
834        );
835        return;
836    }
837    // A `/Kids` cycle would otherwise spin forever. Only referenced nodes can
838    // close one; an inline dictionary is a fresh value every time.
839    if let Some(reference) = reference {
840        if seen.contains(&reference) {
841            diags.record(
842                pdfrum_common::Severity::Recovered,
843                pdfrum_common::DiagKind::NavigationCycle,
844                None,
845            );
846            return;
847        }
848        seen.push(reference);
849    }
850
851    let kids = dict.array(names::KIDS, r);
852    let field_type = field_attr(dict, names::FT, r, limits, diags)
853        .map(|value| value.to_byte_string())
854        .unwrap_or_default();
855    let flags = FieldFlags::from_bits(
856        field_attr(dict, names::FF, r, limits, diags)
857            .and_then(|value| value.as_int())
858            .unwrap_or(0),
859    );
860
861    // A node is terminal when it has a field type and its kids — if any — are
862    // widgets rather than further fields. A kid carrying its own `/T` is a
863    // field in its own right, and makes this node naming structure even
864    // though it has an `/FT` to inherit down.
865    let kids_are_fields = kids.as_ref().is_some_and(|kids| {
866        (0..kids.len()).any(|index| {
867            kids.dict_at(index, r)
868                .is_some_and(|kid| kid.contains_key(names::T))
869        })
870    });
871
872    if let Some(kind) = FieldKind::classify(&field_type, flags)
873        && !kids_are_fields
874    {
875        let name = full_name(dict, r);
876        // A field's fully-qualified name is its **identity**, not a label:
877        // the merge below keys on it, `Form::field` is the only public lookup,
878        // and `pdfrum-form` allocates one `FieldId` per distinct name. So an
879        // empty name is not merely an unaddressable field — it is a field that
880        // every *other* unnamed field in the document would be merged into.
881        // ISO 32000-1 §12.7.3.2 makes the fully qualified name the thing an
882        // action, an export or a JavaScript reference names a field by, and a
883        // node with no `/T` anywhere in its ancestry has none, so there is
884        // nothing a caller could do with the entry. Upstream drops it too
885        // (`cpdf_interactiveform.cpp:914-917`, `AddTerminalField`).
886        if name.is_empty() {
887            diags.record(
888                pdfrum_common::Severity::Suspicious,
889                pdfrum_common::DiagKind::FieldSkippedNoName,
890                None,
891            );
892            return;
893        }
894        let widgets = widgets_of(dict, reference, kids.as_ref(), r);
895        // A name already in the tree gets these widgets **added as further
896        // controls** rather than a second field of its own. Upstream's
897        // `AddTerminalField` looks the name up first and only builds a
898        // `CPDF_FormField` when it is new, so two `/Annots` entries sharing a
899        // `/T` are one field with two controls — and the value every one of
900        // them shows is the *field's*, which is the first dictionary's.
901        // `bug_733528` is exactly that: two widgets named `SharedField`, the
902        // first holding `/V (Hello, world)` and the second `/V ()`, and the
903        // golden reports the second drawing the first's text.
904        if let Some(existing) = out.iter_mut().find(|field| field.name == name) {
905            existing.widgets.extend(widgets);
906            return;
907        }
908        out.push(Field {
909            name,
910            kind,
911            flags,
912            widgets,
913            dict: dict.clone(),
914            reference,
915        });
916        return;
917    }
918
919    let Some(kids) = kids else {
920        // No `/FT` on this dictionary or its `/Parent`, and no `/Kids` to
921        // inherit one down: upstream's `AddTerminalField` returns here
922        // (`cpdf_interactiveform.cpp:905-912`, "Key \"FT\" is required for
923        // terminal fields") and the dictionary contributes no field at all.
924        if field_type.is_empty() {
925            diags.record(
926                pdfrum_common::Severity::Suspicious,
927                pdfrum_common::DiagKind::FieldSkippedNoType,
928                None,
929            );
930        }
931        return;
932    };
933    for index in 0..kids.len() {
934        let kid_ref = kids.reference_at(index);
935        // [oracle-bug] A `/Kids` entry that is not a dictionary costs *that
936        // entry* and nothing else. `CPDF_InteractiveForm::LoadField` reads
937        // `kids->GetDictAt(0)` and returns outright when it is null
938        // (`cpdf_interactiveform.cpp:871-874`), so one unresolvable first kid
939        // silently discards every sibling under the node — a whole page of
940        // fields lost to one broken reference. Nothing recovers them: the
941        // walk has already returned, and `FixPageFields` only re-enters
942        // through `/Annots`.
943        //
944        // That `GetDictAt(0)` is a **probe**, not a guard: the two lines after
945        // it (`:876-880`) ask whether the first kid has `/T` or `/Kids` to
946        // decide whether this node is the terminal field or a branch. The
947        // early return is what happens when the probe cannot be taken, and it
948        // throws away the siblings as a side effect rather than as a
949        // decision — a non-dict first kid says nothing about whether the
950        // *array* is a field tree. Our own probe (`kids_are_fields` above)
951        // scans every kid rather than only the first, so a null at index 0
952        // does not blind it and there is nothing to recover from.
953        //
954        // pdf.js is the tiebreaker and skips the entry: `#collectFieldObjects`
955        // (`src/core/document.js`) recurses per kid and its
956        // `if (!(fieldRef instanceof Ref) || visitedRefs.has(fieldRef))`
957        // guard returns from *that* kid alone, leaving the loop to continue
958        // with the siblings. ISO 32000-1 §12.7.3.1 says `/Kids` holds the
959        // field's children and gives no rule making the array's validity
960        // depend on its first element.
961        let Some(kid) = kids.dict_at(index, r) else {
962            continue;
963        };
964        visit(&kid, kid_ref, depth + 1, seen, out, r, limits, diags);
965    }
966}
967
968/// The widgets drawing a terminal field.
969///
970/// Either the field's kids — a radio group's buttons, or a field split across
971/// pages — or the field's own dictionary when the two are merged, which is
972/// the common single-widget shape.
973fn widgets_of<R: Resolve>(
974    dict: &Dict,
975    reference: Option<ObjRef>,
976    kids: Option<&pdfrum_object::Array>,
977    r: &R,
978) -> Vec<Widget> {
979    if let Some(kids) = kids
980        && !kids.is_empty()
981    {
982        let found: Vec<Widget> = (0..kids.len())
983            .filter_map(|index| {
984                let kid = kids.dict_at(index, r)?;
985                Some(Widget {
986                    reference: kids.reference_at(index),
987                    dict: kid,
988                })
989            })
990            .collect();
991        if !found.is_empty() {
992            return found;
993        }
994    }
995    // Merged field-and-widget: the field dictionary is the annotation.
996    if dict.byte_string(names::SUBTYPE, r).as_deref() == Some(b"Widget") {
997        return vec![Widget {
998            dict: dict.clone(),
999            reference,
1000        }];
1001    }
1002    Vec::new()
1003}
1004
1005/// Values written to a form's fields, keyed by fully-qualified name.
1006///
1007/// The edit buffer, and the reason this crate can fill a form without
1008/// mutating anything: the parser's object store is immutable and its objects
1009/// are values, so a write is recorded here and every reader consults it. It
1010/// is the same shape as the appearance
1011/// [`AnnotOverlay`](crate::AnnotOverlay), for the same reason.
1012///
1013/// Turning the buffer into a file is [`apply`]'s job.
1014#[derive(Debug, Clone, Default, PartialEq, Eq)]
1015pub struct FieldValues {
1016    entries: Vec<(String, String)>,
1017}
1018
1019impl FieldValues {
1020    /// An empty buffer.
1021    ///
1022    /// ```
1023    /// use pdfrum_doc::form::FieldValues;
1024    ///
1025    /// assert!(FieldValues::new().is_empty());
1026    /// ```
1027    #[must_use]
1028    pub fn new() -> FieldValues {
1029        FieldValues::default()
1030    }
1031
1032    /// Records a value for the field with this fully-qualified name,
1033    /// replacing any earlier one.
1034    ///
1035    /// ```
1036    /// use pdfrum_doc::form::FieldValues;
1037    ///
1038    /// let mut values = FieldValues::new();
1039    /// values.set("name", "Ada");
1040    /// // Writing again replaces, it does not append.
1041    /// values.set("name", "Grace");
1042    /// assert_eq!(values.get("name"), Some("Grace"));
1043    /// assert_eq!(values.len(), 1);
1044    /// ```
1045    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
1046        let name = name.into();
1047        let value = value.into();
1048        match self.entries.iter_mut().find(|(key, _)| *key == name) {
1049            Some(entry) => entry.1 = value,
1050            None => self.entries.push((name, value)),
1051        }
1052    }
1053
1054    /// What was written for this field, if anything.
1055    ///
1056    /// ```
1057    /// use pdfrum_doc::form::FieldValues;
1058    ///
1059    /// let mut values = FieldValues::new();
1060    /// values.set("name", "Ada");
1061    /// assert_eq!(values.get("name"), Some("Ada"));
1062    /// assert_eq!(values.get("absent"), None);
1063    /// ```
1064    #[must_use]
1065    pub fn get(&self, name: &str) -> Option<&str> {
1066        self.entries
1067            .iter()
1068            .find(|(key, _)| key == name)
1069            .map(|(_, value)| value.as_str())
1070    }
1071
1072    /// Every recorded write, in the order it was first made.
1073    ///
1074    /// ```
1075    /// use pdfrum_doc::form::FieldValues;
1076    ///
1077    /// let mut values = FieldValues::new();
1078    /// values.set("b", "2");
1079    /// values.set("a", "1");
1080    /// // First-write order, not sorted.
1081    /// assert_eq!(values.iter().collect::<Vec<_>>(), [("b", "2"), ("a", "1")]);
1082    /// ```
1083    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
1084        self.entries
1085            .iter()
1086            .map(|(name, value)| (name.as_str(), value.as_str()))
1087    }
1088
1089    /// How many fields have been written to.
1090    ///
1091    /// ```
1092    /// use pdfrum_doc::form::FieldValues;
1093    ///
1094    /// let mut values = FieldValues::new();
1095    /// values.set("name", "Ada");
1096    /// assert_eq!(values.len(), 1);
1097    /// ```
1098    #[must_use]
1099    pub fn len(&self) -> usize {
1100        self.entries.len()
1101    }
1102
1103    /// Whether nothing has been written.
1104    ///
1105    /// ```
1106    /// use pdfrum_doc::form::FieldValues;
1107    ///
1108    /// let mut values = FieldValues::new();
1109    /// values.set("name", "Ada");
1110    /// assert!(!values.is_empty());
1111    /// ```
1112    #[must_use]
1113    pub fn is_empty(&self) -> bool {
1114        self.entries.is_empty()
1115    }
1116}
1117
1118/// One field's edited dictionary, and the widget appearances that follow from
1119/// it.
1120#[derive(Debug, Clone, PartialEq)]
1121pub struct FieldEdit {
1122    /// The reference to replace.
1123    pub reference: ObjRef,
1124    /// The field dictionary with its `/V` — and, for a toggle, its `/AS` —
1125    /// rewritten.
1126    pub dict: Dict,
1127    /// Regenerated appearances for this field's widgets, each with the
1128    /// reference to replace. Empty when the widgets' own appearances already
1129    /// cover the new value, which is the case for a toggle whose `/AP /N`
1130    /// lists the state it was switched to.
1131    pub widgets: Vec<(ObjRef, Dict, GeneratedAp)>,
1132}
1133
1134/// Turns an edit buffer into the object replacements that write it to a file.
1135///
1136/// This is the whole "fill a form and save it" step: it rewrites each edited
1137/// field's `/V`, sets a toggle's widget `/AS` to the state chosen, and
1138/// regenerates the appearance of any widget whose own `/AP` cannot show the
1139/// new value.
1140///
1141/// A field the buffer names but the form does not have is skipped, as is one
1142/// whose dictionary is inline and therefore has no reference to replace.
1143///
1144/// ```
1145/// use pdfrum_common::{Diagnostics, Limits};
1146/// use pdfrum_doc::form::Form;
1147/// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
1148///
1149/// let field = Dict::from_pairs([
1150///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
1151///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
1152///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
1153/// ]);
1154/// let catalog = Dict::from_pairs([(
1155///     Name::from("AcroForm"),
1156///     Object::Dict(Dict::from_pairs([(
1157///         Name::from("Fields"),
1158///         Object::Array(Array::of([Object::Dict(field)])),
1159///     )])),
1160/// )]);
1161///
1162/// let mut diags = Diagnostics::default();
1163/// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
1164///     .expect("the catalog declares an /AcroForm");
1165/// use pdfrum_doc::form::{FieldValues, apply};
1166///
1167/// let mut values = FieldValues::new();
1168/// values.set("name", "Grace");
1169///
1170/// // The field is written inline in `/Fields`, so it has no reference to
1171/// // replace and no edit is produced. `None` for the fonts draws a widget's
1172/// // chrome without laying its value out; pass `FormFonts::load`'s answer to
1173/// // get the body too.
1174/// assert!(apply(&form, &values, &catalog, None, &NoResolve, &mut diags).is_empty());
1175/// ```
1176#[must_use]
1177pub fn apply<R: Resolve>(
1178    form: &Form,
1179    values: &FieldValues,
1180    catalog: &Dict,
1181    fonts: Option<&ap::FormFonts>,
1182    r: &R,
1183    diags: &mut Diagnostics,
1184) -> Vec<FieldEdit> {
1185    let mut out = Vec::new();
1186    for (name, value) in values.iter() {
1187        let Some(field) = form.field(name) else {
1188            continue;
1189        };
1190        let Some(reference) = field.reference else {
1191            continue;
1192        };
1193        if !field.kind.is_writable() {
1194            continue;
1195        }
1196
1197        let dict = rewrite(&field.dict, names::V, value_object(field.kind, value));
1198        // A choice field's `/I` indexes `/Opt`, and a `/V` rewritten without
1199        // it leaves exactly the stale pair `selected_indices_for_interaction`
1200        // has to defend against: it discards `/I` wholesale the moment the two
1201        // disagree. Rewriting it here keeps them agreeing, so the selection
1202        // survives as an index rather than being re-derived from the text.
1203        let dict = if field.kind.is_choice() {
1204            rewrite(&dict, names::I, selected_indices_for(&dict, value, r))
1205        } else {
1206            dict
1207        };
1208        let mut widgets = Vec::new();
1209        for widget in &field.widgets {
1210            let Some(widget_ref) = widget.reference else {
1211                continue;
1212            };
1213            // Merged field-and-widget: the value edit and the widget edit are
1214            // the same object, so the widget's edit starts from the field
1215            // dictionary that already carries the new `/V` — starting from
1216            // the widget's own copy would put its old `/V` back.
1217            let source = if widget_ref == reference {
1218                &dict
1219            } else {
1220                &widget.dict
1221            };
1222            // A toggle's widget selects its appearance with `/AS`; a text or
1223            // choice field's has to have one drawn.
1224            let widget_dict = if field.kind.is_toggle() {
1225                rewrite(
1226                    source,
1227                    names::AS,
1228                    Object::Name(Name::from(value.as_bytes())),
1229                )
1230            } else {
1231                source.clone()
1232            };
1233            // A text or choice field's widget needs its *value* laid out, not
1234            // just its chrome: the page-wide generator reaches the body through
1235            // `generate_with_text`, and a fill that stopped at the chrome would
1236            // store the value and render an empty box.
1237            let generated =
1238                ap::with_text_font(&widget_dict, catalog, fonts, r, |text_font, substitute| {
1239                    match text_font {
1240                        Some(font) => ap::widget::generate_with_text(
1241                            &widget_dict,
1242                            catalog,
1243                            font,
1244                            substitute,
1245                            r,
1246                        ),
1247                        None => ap::widget::generate(&widget_dict, r),
1248                    }
1249                });
1250            if let Some(generated) = generated {
1251                widgets.push((widget_ref, widget_dict, generated));
1252            } else if widget_ref != reference && field.kind.is_toggle() {
1253                // No appearance needed to be drawn, but `/AS` still changed.
1254                widgets.push((
1255                    widget_ref,
1256                    widget_dict,
1257                    GeneratedAp {
1258                        stream: Vec::new(),
1259                        bbox: kurbo::Rect::ZERO,
1260                        matrix: kurbo::Affine::IDENTITY,
1261                        resources: Dict::new(),
1262                        rect_override: None,
1263                        as_override: None,
1264                    },
1265                ));
1266            }
1267        }
1268        let _ = diags;
1269        out.push(FieldEdit {
1270            reference,
1271            dict,
1272            widgets,
1273        });
1274    }
1275    out
1276}
1277
1278/// The object a value is stored as: a name for a toggle's state, a string for
1279/// everything else.
1280/// The `/I` array that agrees with a choice field's newly written `/V`.
1281///
1282/// `/I` holds the **indices into `/Opt`** of what is selected, ascending, and
1283/// a reader that finds it disagreeing with `/V` throws it away and matches the
1284/// text instead. A value that names no option therefore writes an empty array
1285/// rather than guessing: an absent selection and a text-only one are the two
1286/// honest answers, and the empty array says the first without contradicting
1287/// the second.
1288///
1289/// A `FieldValues` entry is one string, so at most one index is written even
1290/// though the array shape allows several.
1291fn selected_indices_for<R: Resolve>(dict: &Dict, value: &str, r: &R) -> Object {
1292    let selected = crate::ap::field_body::options(dict, r)
1293        .into_iter()
1294        .position(|option| option.value == value);
1295    Object::Array(pdfrum_object::Array::of(
1296        selected
1297            .and_then(|index| i64::try_from(index).ok())
1298            .map(Object::Int),
1299    ))
1300}
1301
1302fn value_object(kind: FieldKind, value: &str) -> Object {
1303    if kind.is_toggle() {
1304        Object::Name(Name::from(value.as_bytes()))
1305    } else {
1306        Object::Str(pdfrum_object::PdfString::literal(value.as_bytes()))
1307    }
1308}
1309
1310/// A copy of `dict` with `key` set to `value`, keeping every other entry in
1311/// its original position.
1312fn rewrite(dict: &Dict, key: &Name, value: Object) -> Dict {
1313    let mut out = Dict::new();
1314    let mut replaced = false;
1315    for (existing, held) in dict.iter() {
1316        if existing == key {
1317            if !replaced {
1318                out.push(existing.clone(), value.clone());
1319                replaced = true;
1320            }
1321        } else {
1322            out.push(existing.clone(), held.clone());
1323        }
1324    }
1325    if !replaced {
1326        out.push(key.clone(), value);
1327    }
1328    out
1329}
1330
1331/// Which rows of a choice field an **interaction** treats as selected.
1332///
1333/// # Why this is not the appearance's answer
1334///
1335/// A choice field records its selection twice — `/I` as indices, `/V` as the
1336/// selected options' export values — and the pair is read *differently
1337/// depending on who is asking*. The two readers are not reconcilable and
1338/// pretending they are is how a corpus row moves in the wrong direction:
1339///
1340/// - **Interaction** — "is row `n` selected?", the question a click, an arrow
1341///   key or an embedder's query asks — consults **`/I` first**, as integer
1342///   indices, and falls back to `/V` only when `/I` is not usable. That is
1343///   this function.
1344/// - **Appearance** — what the generated `/AP` draws a band behind — reads
1345///   **`/V` first**, `/I` only when there is no `/V`, and then matches each
1346///   entry's *text* against the option values, so an integer index matches
1347///   nothing. That is `ap::field_body::selected_indices`, and it is
1348///   deliberately the other way round.
1349///
1350/// So `listbox_form.pdf`'s `Listbox_MultiSelectMultipleIndices` — `/I [1 3]`
1351/// and no `/V` — draws **no** selection band while an embedder asking about
1352/// its rows is told 1 and 3 are selected. Both are correct; they are answers
1353/// to different questions.
1354///
1355/// # What "usable" means
1356///
1357/// `indices_are_usable` is the test, and it is strict because its job is to
1358/// catch a stale `/I` left behind by an editor that rewrote `/V`. `/I` is
1359/// usable when either
1360///
1361/// - there is **no `/V` at all** — nothing can contradict it; or
1362/// - `/I` and `/V` **agree exactly**: the same number of entries, every index
1363///   in range, and the multiset of options those indices name equal to the
1364///   multiset `/V` lists. A duplicate on one side must be matched by a
1365///   duplicate on the other, which is why occurrences are counted rather than
1366///   membership tested.
1367///
1368/// One disagreement anywhere discards `/I` entirely — it is not repaired
1369/// entry by entry — and `/V` then decides alone, matched as text exactly as
1370/// the appearance reader does.
1371///
1372/// `options` is the field's `/Opt` in order, as the **values** a selection is
1373/// compared against: an `[export, label]` pair contributes its export, never
1374/// its label.
1375#[must_use]
1376pub fn selected_indices_for_interaction<R: Resolve>(
1377    dict: &Dict,
1378    options: &[String],
1379    r: &R,
1380) -> Vec<usize> {
1381    // `/V` and `/I` are inheritable field attributes, and a damaged
1382    // inheritance chain is not this function's to report on: it answers with
1383    // what it could reach.
1384    let (limits, mut diags) = (Limits::default(), Diagnostics::default());
1385    let value = field_attr(dict, names::V, r, &limits, &mut diags);
1386    let indices = field_attr(dict, names::I, r, &limits, &mut diags);
1387    if let Some(indices) = indices.as_ref()
1388        && indices_are_usable(indices, value.as_ref(), options, r)
1389    {
1390        return listed_indices(indices, r)
1391            .into_iter()
1392            .filter_map(|index| usize::try_from(index).ok())
1393            .filter(|index| *index < options.len())
1394            .collect();
1395    }
1396    let Some(value) = value else {
1397        return Vec::new();
1398    };
1399    let wanted: Vec<String> = match value.as_array() {
1400        Some(array) => (0..array.len())
1401            .map(|slot| {
1402                array
1403                    .get(slot, r)
1404                    .as_deref()
1405                    .map(Object::to_text)
1406                    .unwrap_or_default()
1407            })
1408            .collect(),
1409        None => vec![value.to_text()],
1410    };
1411    wanted
1412        .into_iter()
1413        .filter_map(|text| options.iter().position(|option| *option == text))
1414        .collect()
1415}
1416
1417/// `/I`'s entries as raw integers, or nothing when any entry is not a number.
1418///
1419/// A bare number stands for a one-entry array, which is the shape
1420/// `UseSelectedIndicesObject` admits alongside the array.
1421fn listed_indices<R: Resolve>(indices: &Object, r: &R) -> Vec<i64> {
1422    match indices.as_array() {
1423        Some(array) => (0..array.len())
1424            .map(|slot| array.get(slot, r).as_deref().and_then(Object::as_int))
1425            .collect::<Option<Vec<i64>>>()
1426            .unwrap_or_default(),
1427        None => indices.as_int().into_iter().collect(),
1428    }
1429}
1430
1431/// Whether `/I` may be believed in preference to `/V`.
1432///
1433/// See [`selected_indices_for_interaction`] for the rule and why it is all or
1434/// nothing.
1435fn indices_are_usable<R: Resolve>(
1436    indices: &Object,
1437    value: Option<&Object>,
1438    options: &[String],
1439    r: &R,
1440) -> bool {
1441    // No `/V` to contradict it.
1442    let Some(value) = value else {
1443        return true;
1444    };
1445    // A non-number entry anywhere fails outright: `/I` is trusted whole or
1446    // not at all, and an empty answer here would be indistinguishable from a
1447    // genuinely empty `/I`.
1448    let listed = listed_indices(indices, r);
1449    let declared = match indices.as_array() {
1450        Some(array) => array.len(),
1451        None => usize::from(indices.as_int().is_some()),
1452    };
1453    if listed.len() != declared || declared == 0 {
1454        return false;
1455    }
1456
1457    // `/V`'s texts, as counts, so a repeated value needs a repeated index.
1458    let mut wanted: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
1459    if let Some(array) = value.as_array() {
1460        if array.len() != listed.len() {
1461            return false;
1462        }
1463        for slot in 0..array.len() {
1464            // Only strings are counted — upstream ignores any other type
1465            // here, which then leaves a count `/I` cannot satisfy.
1466            if let Some(object) = array.get(slot, r)
1467                && object.as_string().is_some()
1468            {
1469                *wanted.entry(object.to_text()).or_default() += 1;
1470            }
1471        }
1472    } else {
1473        // A lone string is the one-selection spelling, so it can only ever
1474        // account for one index.
1475        if listed.len() != 1 {
1476            return false;
1477        }
1478        if value.as_string().is_some() {
1479            *wanted.entry(value.to_text()).or_default() += 1;
1480        }
1481    }
1482
1483    for index in listed {
1484        let Ok(index) = usize::try_from(index) else {
1485            return false;
1486        };
1487        let Some(option) = options.get(index) else {
1488            return false;
1489        };
1490        let Some(count) = wanted.get_mut(option) else {
1491            return false;
1492        };
1493        *count -= 1;
1494        if *count == 0 {
1495            wanted.remove(option);
1496        }
1497    }
1498    wanted.is_empty()
1499}
1500
1501#[cfg(test)]
1502mod tests {
1503    use super::*;
1504    use pdfrum_object::{Array, NoResolve, PdfString};
1505
1506    fn dict(pairs: &[(&str, Object)]) -> Dict {
1507        Dict::from_pairs(
1508            pairs
1509                .iter()
1510                .map(|(k, v)| (Name::from(*k), v.clone()))
1511                .collect::<Vec<_>>(),
1512        )
1513    }
1514
1515    fn text(value: &str) -> Object {
1516        Object::Str(PdfString::literal(value.as_bytes()))
1517    }
1518
1519    fn name(value: &str) -> Object {
1520        Object::Name(Name::from(value))
1521    }
1522
1523    /// The four `listbox_form.pdf` shapes, as the interaction reader sees
1524    /// them. Contrast `ap::field_body::selected_indices`, which answers the
1525    /// appearance's question and disagrees on the first of these on purpose.
1526    fn opts() -> Vec<String> {
1527        ["Albania", "Belgium", "Croatia", "Denmark", "Estonia"]
1528            .iter()
1529            .map(|s| (*s).to_owned())
1530            .collect()
1531    }
1532
1533    fn selected(pairs: &[(&str, Object)]) -> Vec<usize> {
1534        selected_indices_for_interaction(&dict(pairs), &opts(), &NoResolve)
1535    }
1536
1537    fn strings(values: &[&str]) -> Object {
1538        Object::Array(Array::of(
1539            values.iter().map(|v| text(v)).collect::<Vec<_>>(),
1540        ))
1541    }
1542
1543    #[test]
1544    fn indices_alone_are_believed_because_nothing_contradicts_them() {
1545        // `Listbox_MultiSelectMultipleIndices`: `/I [1 3]`, no `/V`.
1546        assert_eq!(
1547            selected(&[(
1548                "I",
1549                Object::Array(Array::of([Object::Int(1), Object::Int(3)]))
1550            )]),
1551            vec![1, 3]
1552        );
1553        // A bare number is the one-entry spelling.
1554        assert_eq!(selected(&[("I", Object::Int(2))]), vec![2]);
1555    }
1556
1557    #[test]
1558    fn a_value_alone_selects_every_option_it_names() {
1559        // `Listbox_MultiSelectMultipleValues`, restated over these options.
1560        assert_eq!(
1561            selected(&[("V", strings(&["Belgium", "Denmark"]))]),
1562            vec![1, 3]
1563        );
1564        // And a lone string is the single-selection spelling.
1565        assert_eq!(selected(&[("V", text("Croatia"))]), vec![2]);
1566        // A value naming no option selects nothing rather than guessing.
1567        assert_eq!(selected(&[("V", text("Zambia"))]), Vec::<usize>::new());
1568    }
1569
1570    #[test]
1571    fn consistent_indices_win_over_the_values_they_agree_with() {
1572        // Same count, in range, naming exactly what `/V` lists.
1573        assert_eq!(
1574            selected(&[
1575                ("V", strings(&["Belgium", "Denmark"])),
1576                (
1577                    "I",
1578                    Object::Array(Array::of([Object::Int(1), Object::Int(3)]))
1579                ),
1580            ]),
1581            vec![1, 3]
1582        );
1583        // Occurrences are counted, not sequences compared, so the two may be
1584        // listed in different orders — and `/I`'s order is what comes back.
1585        assert_eq!(
1586            selected(&[
1587                ("V", strings(&["Denmark", "Belgium"])),
1588                (
1589                    "I",
1590                    Object::Array(Array::of([Object::Int(3), Object::Int(1)]))
1591                ),
1592            ]),
1593            vec![3, 1]
1594        );
1595    }
1596
1597    #[test]
1598    fn inconsistent_indices_are_discarded_whole_and_the_values_decide() {
1599        // `Listbox_MultiSelectMultipleMismatch`'s shape: three indices
1600        // against two values, so the counts differ and `/I` is rejected
1601        // before any index is looked up.
1602        assert_eq!(
1603            selected(&[
1604                ("V", strings(&["Albania", "Croatia"])),
1605                (
1606                    "I",
1607                    Object::Array(Array::of([Object::Int(1), Object::Int(3), Object::Int(4),])),
1608                ),
1609            ]),
1610            vec![0, 2]
1611        );
1612        // Equal counts, but an index naming an option `/V` does not list.
1613        assert_eq!(
1614            selected(&[
1615                ("V", strings(&["Albania"])),
1616                ("I", Object::Array(Array::of([Object::Int(1)]))),
1617            ]),
1618            vec![0]
1619        );
1620        // An index out of range poisons the whole array rather than being
1621        // dropped on its own.
1622        assert_eq!(
1623            selected(&[
1624                ("V", strings(&["Albania", "Croatia"])),
1625                (
1626                    "I",
1627                    Object::Array(Array::of([Object::Int(0), Object::Int(9)]))
1628                ),
1629            ]),
1630            vec![0, 2]
1631        );
1632        // Two indices naming one option cannot satisfy two distinct values.
1633        assert_eq!(
1634            selected(&[
1635                ("V", strings(&["Albania", "Belgium"])),
1636                (
1637                    "I",
1638                    Object::Array(Array::of([Object::Int(0), Object::Int(0)]))
1639                ),
1640            ]),
1641            vec![0, 1]
1642        );
1643        // A non-number entry fails the whole array too.
1644        assert_eq!(
1645            selected(&[
1646                ("V", strings(&["Albania"])),
1647                ("I", Object::Array(Array::of([text("0")]))),
1648            ]),
1649            vec![0]
1650        );
1651    }
1652
1653    #[test]
1654    fn a_field_declaring_neither_selects_nothing() {
1655        assert_eq!(selected(&[]), Vec::<usize>::new());
1656    }
1657
1658    fn load(catalog: &Dict) -> Option<Form> {
1659        load_with_diags(catalog).0
1660    }
1661
1662    /// `load`, plus the diagnostics the walk recorded.
1663    fn load_with_diags(catalog: &Dict) -> (Option<Form>, Diagnostics) {
1664        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
1665        let form = Form::load(catalog, &NoResolve, &limits, &mut diags);
1666        (form, diags)
1667    }
1668
1669    /// The single field a one-field fixture is expected to have.
1670    fn only_field(form: &Form) -> &Field {
1671        assert_eq!(form.len(), 1, "the fixture has exactly one field");
1672        form.fields.first().expect("one field")
1673    }
1674
1675    fn catalog_with(fields: Vec<Object>) -> Dict {
1676        let acro = dict(&[("Fields", Object::Array(Array::of(fields)))]);
1677        dict(&[("AcroForm", Object::Dict(acro))])
1678    }
1679
1680    #[test]
1681    fn a_catalog_without_an_acroform_has_no_form() {
1682        assert_eq!(load(&Dict::new()), None);
1683    }
1684
1685    #[test]
1686    fn an_acroform_without_fields_is_an_empty_form_not_an_absent_one() {
1687        let catalog = dict(&[("AcroForm", Object::Dict(Dict::new()))]);
1688        let form = load(&catalog).expect("an AcroForm is a form");
1689        assert!(form.is_empty());
1690    }
1691
1692    #[test]
1693    fn a_terminal_field_is_classified_from_its_type_and_flags() {
1694        let catalog = catalog_with(vec![Object::Dict(dict(&[
1695            ("FT", name("Tx")),
1696            ("T", text("greeting")),
1697            ("V", text("hello")),
1698        ]))]);
1699        let form = load(&catalog).expect("form");
1700        assert_eq!(form.len(), 1);
1701        let field = &only_field(&form);
1702        assert_eq!(field.name, "greeting");
1703        assert_eq!(field.kind, FieldKind::Text);
1704        assert_eq!(field.stored_value(&NoResolve), "hello");
1705    }
1706
1707    #[test]
1708    fn the_button_flags_split_the_three_button_kinds() {
1709        // No bits: a check box.
1710        assert_eq!(
1711            FieldKind::classify(b"Btn", FieldFlags::from_bits(0)),
1712            Some(FieldKind::Check)
1713        );
1714        // Bit 16: a radio button.
1715        assert_eq!(
1716            FieldKind::classify(b"Btn", FieldFlags::from_bits(1 << 15)),
1717            Some(FieldKind::Radio)
1718        );
1719        // Bit 17 wins over bit 16: a push button.
1720        assert_eq!(
1721            FieldKind::classify(b"Btn", FieldFlags::from_bits((1 << 16) | (1 << 15))),
1722            Some(FieldKind::Button)
1723        );
1724    }
1725
1726    #[test]
1727    fn a_choice_field_splits_on_the_combo_bit() {
1728        assert_eq!(
1729            FieldKind::classify(b"Ch", FieldFlags::from_bits(0)),
1730            Some(FieldKind::List)
1731        );
1732        assert_eq!(
1733            FieldKind::classify(b"Ch", FieldFlags::from_bits(1 << 17)),
1734            Some(FieldKind::Combo)
1735        );
1736    }
1737
1738    #[test]
1739    fn a_node_with_no_field_type_is_not_a_field() {
1740        assert_eq!(FieldKind::classify(b"", FieldFlags::from_bits(0)), None);
1741        assert_eq!(
1742            FieldKind::classify(b"Nonsense", FieldFlags::from_bits(0)),
1743            None
1744        );
1745    }
1746
1747    #[test]
1748    fn a_naming_node_contributes_its_children_and_its_name_prefix() {
1749        // An interior node with a `/T` but no `/FT`, whose kids are fields.
1750        // The kid carries the `/Parent` back-pointer a real file writes,
1751        // because that is the edge `full_name` walks — the tree is navigated
1752        // downwards to find fields and upwards to name them.
1753        let parent_dict = dict(&[("T", text("address"))]);
1754        let kid = dict(&[
1755            ("FT", name("Tx")),
1756            ("T", text("street")),
1757            ("Parent", Object::Dict(parent_dict)),
1758        ]);
1759        let parent = dict(&[
1760            ("T", text("address")),
1761            ("Kids", Object::Array(Array::of([Object::Dict(kid)]))),
1762        ]);
1763        let catalog = catalog_with(vec![Object::Dict(parent)]);
1764        let form = load(&catalog).expect("form");
1765        assert_eq!(form.len(), 1);
1766        // The name is qualified through the parent, which is the whole point
1767        // of the interior node.
1768        assert_eq!(only_field(&form).name, "address.street");
1769    }
1770
1771    #[test]
1772    fn a_field_whose_kids_are_widgets_stays_one_field() {
1773        // A radio group: the `/FT` is on the parent, the kids are widgets
1774        // with no `/T` of their own.
1775        let on = dict(&[("Subtype", name("Widget")), ("AS", name("A"))]);
1776        let off = dict(&[("Subtype", name("Widget")), ("AS", name("Off"))]);
1777        let group = dict(&[
1778            ("FT", name("Btn")),
1779            ("Ff", Object::Int(1 << 15)),
1780            ("T", text("choice")),
1781            ("V", name("A")),
1782            (
1783                "Kids",
1784                Object::Array(Array::of([Object::Dict(on), Object::Dict(off)])),
1785            ),
1786        ]);
1787        let catalog = catalog_with(vec![Object::Dict(group)]);
1788        let form = load(&catalog).expect("form");
1789        assert_eq!(form.len(), 1, "a radio group is one field, not two");
1790        let field = &only_field(&form);
1791        assert_eq!(field.kind, FieldKind::Radio);
1792        assert_eq!(field.widgets.len(), 2);
1793        assert!(field.is_checked(None, &NoResolve));
1794    }
1795
1796    #[test]
1797    fn a_merged_field_and_widget_reports_itself_as_its_widget() {
1798        let merged = dict(&[
1799            ("FT", name("Tx")),
1800            ("T", text("box")),
1801            ("Subtype", name("Widget")),
1802        ]);
1803        let catalog = catalog_with(vec![Object::Dict(merged)]);
1804        let form = load(&catalog).expect("form");
1805        assert_eq!(only_field(&form).widgets.len(), 1);
1806        assert_eq!(
1807            only_field(&form).widgets.first().expect("one widget").dict,
1808            only_field(&form).dict
1809        );
1810    }
1811
1812    #[test]
1813    fn a_toggle_reads_off_and_absent_as_clear_and_everything_else_as_set() {
1814        let make = |value: Option<Object>| {
1815            let mut pairs = vec![("FT", name("Btn")), ("T", text("t"))];
1816            if value.is_some() {
1817                pairs.push(("V", value.clone().unwrap_or(Object::Null)));
1818            }
1819            let catalog = catalog_with(vec![Object::Dict(dict(&pairs))]);
1820            let form = load(&catalog).expect("form");
1821            only_field(&form).is_checked(None, &NoResolve)
1822        };
1823        assert!(!make(None), "absent is clear");
1824        assert!(!make(Some(name("Off"))), "Off is clear");
1825        assert!(make(Some(name("Yes"))), "any other state is set");
1826    }
1827
1828    #[test]
1829    fn a_push_button_and_a_signature_hold_no_writable_value() {
1830        assert!(!FieldKind::Button.is_writable());
1831        assert!(!FieldKind::Signature.is_writable());
1832        assert!(FieldKind::Text.is_writable());
1833        assert!(FieldKind::Check.is_writable());
1834    }
1835
1836    #[test]
1837    fn writing_a_value_records_it_and_reading_sees_it() {
1838        let catalog = catalog_with(vec![Object::Dict(dict(&[
1839            ("FT", name("Tx")),
1840            ("T", text("greeting")),
1841            ("V", text("hello")),
1842        ]))]);
1843        let form = load(&catalog).expect("form");
1844        let mut values = FieldValues::new();
1845        values.set("greeting", "goodbye");
1846        // The edit wins over the file.
1847        assert_eq!(
1848            only_field(&form).value(Some(&values), &NoResolve),
1849            "goodbye"
1850        );
1851        // And the file is unchanged.
1852        assert_eq!(only_field(&form).stored_value(&NoResolve), "hello");
1853    }
1854
1855    #[test]
1856    fn setting_the_same_field_twice_keeps_the_last_write_and_one_entry() {
1857        let mut values = FieldValues::new();
1858        values.set("a", "one");
1859        values.set("a", "two");
1860        assert_eq!(values.len(), 1);
1861        assert_eq!(values.get("a"), Some("two"));
1862    }
1863
1864    #[test]
1865    fn an_option_pair_reports_its_label_rather_than_its_export_value() {
1866        let pair = Object::Array(Array::of([text("export"), text("Label")]));
1867        let catalog = catalog_with(vec![Object::Dict(dict(&[
1868            ("FT", name("Ch")),
1869            ("T", text("pick")),
1870            ("Opt", Object::Array(Array::of([text("Plain"), pair]))),
1871        ]))]);
1872        let form = load(&catalog).expect("form");
1873        assert_eq!(only_field(&form).options(&NoResolve), ["Plain", "Label"]);
1874    }
1875
1876    #[test]
1877    fn the_states_of_a_toggle_come_from_its_widgets_appearances() {
1878        let normal = dict(&[("Off", Object::Null), ("Yes", Object::Null)]);
1879        let ap = dict(&[("N", Object::Dict(normal))]);
1880        let widget = dict(&[
1881            ("FT", name("Btn")),
1882            ("T", text("t")),
1883            ("Subtype", name("Widget")),
1884            ("AP", Object::Dict(ap)),
1885        ]);
1886        let catalog = catalog_with(vec![Object::Dict(widget)]);
1887        let form = load(&catalog).expect("form");
1888        assert_eq!(only_field(&form).states(&NoResolve), ["Off", "Yes"]);
1889    }
1890
1891    #[test]
1892    fn a_field_with_no_reference_cannot_be_written_back() {
1893        // The field is written inline in `/Fields`, so nothing names it.
1894        let catalog = catalog_with(vec![Object::Dict(dict(&[
1895            ("FT", name("Tx")),
1896            ("T", text("inline")),
1897        ]))]);
1898        let form = load(&catalog).expect("form");
1899        assert_eq!(only_field(&form).reference, None);
1900        let mut values = FieldValues::new();
1901        values.set("inline", "x");
1902        let mut diags = Diagnostics::default();
1903        assert!(
1904            apply(&form, &values, &catalog, None, &NoResolve, &mut diags).is_empty(),
1905            "an unnamed field produces no replacement"
1906        );
1907    }
1908
1909    #[test]
1910    fn rewriting_a_key_keeps_the_dictionary_order() {
1911        let source = dict(&[
1912            ("A", Object::Int(1)),
1913            ("V", text("old")),
1914            ("B", Object::Int(2)),
1915        ]);
1916        let out = rewrite(&source, &Name::from("V"), text("new"));
1917        let keys: Vec<&[u8]> = out.keys().map(pdfrum_object::Name::as_bytes).collect();
1918        assert_eq!(keys, [b"A".as_slice(), b"V".as_slice(), b"B".as_slice()]);
1919        assert_eq!(
1920            out.text(&Name::from("V"), &NoResolve).as_deref(),
1921            Some("new")
1922        );
1923    }
1924
1925    #[test]
1926    fn rewriting_an_absent_key_appends_it() {
1927        let out = rewrite(&Dict::new(), &Name::from("V"), text("v"));
1928        assert_eq!(out.len(), 1);
1929    }
1930
1931    #[test]
1932    fn a_field_with_no_name_anywhere_in_its_ancestry_is_dropped() {
1933        // ISO 32000-1 §12.7.3.2: a field is addressed by its fully qualified
1934        // name, and this one has none — no `/T` on itself and no `/Parent`
1935        // carrying one — so no action, export or script could ever name it.
1936        // `AddTerminalField` drops it (`cpdf_interactiveform.cpp:914-917`).
1937        let catalog = catalog_with(vec![Object::Dict(dict(&[
1938            ("FT", name("Tx")),
1939            ("V", text("unreachable")),
1940        ]))]);
1941        let (form, diags) = load_with_diags(&catalog);
1942        let form = form.expect("an AcroForm is still a form");
1943        assert!(form.is_empty(), "an unnamed terminal field is not a field");
1944        assert!(diags.contains(&pdfrum_common::DiagKind::FieldSkippedNoName));
1945    }
1946
1947    #[test]
1948    fn unnamed_fields_do_not_collapse_into_one() {
1949        // The reason the drop is the *correct* answer and not merely the
1950        // oracle's: `name` is the identity the merge below keys on, so
1951        // keeping the empty name would fold every unnamed field in the
1952        // document into a single field carrying all their widgets — a field
1953        // that is not in the file. Two unnamed entries plus a real one must
1954        // leave exactly the real one.
1955        let unnamed = || Object::Dict(dict(&[("FT", name("Tx")), ("V", text("a"))]));
1956        let catalog = catalog_with(vec![
1957            unnamed(),
1958            unnamed(),
1959            Object::Dict(dict(&[
1960                ("FT", name("Tx")),
1961                ("T", text("real")),
1962                ("V", text("b")),
1963            ])),
1964        ]);
1965        let form = load(&catalog).expect("a form");
1966        assert_eq!(only_field(&form).name, "real");
1967    }
1968
1969    #[test]
1970    fn a_field_named_only_by_an_ancestor_survives() {
1971        // The drop is about the *fully qualified* name, not about `/T` on the
1972        // node itself: a kid with no `/T` inherits its parent's name and is
1973        // addressable as it, so it must be kept.
1974        let kid = Object::Dict(dict(&[
1975            ("Subtype", name("Widget")),
1976            (
1977                "Parent",
1978                Object::Dict(dict(&[("FT", name("Tx")), ("T", text("parent"))])),
1979            ),
1980        ]));
1981        let catalog = catalog_with(vec![Object::Dict(dict(&[
1982            ("FT", name("Tx")),
1983            ("T", text("parent")),
1984            ("Kids", Object::Array(Array::of([kid]))),
1985        ]))]);
1986        let form = load(&catalog).expect("a form");
1987        assert_eq!(only_field(&form).name, "parent");
1988    }
1989
1990    #[test]
1991    fn two_fields_entries_sharing_a_name_are_one_field_with_two_widgets() {
1992        // `AddTerminalField` looks the fully-qualified name up before it
1993        // builds anything, so the second entry becomes another *control* of
1994        // the first's field rather than a field of its own. `bug_733528` is
1995        // that shape, and its golden has the second widget drawing the
1996        // first's value.
1997        let widget = |value: &str| {
1998            Object::Dict(dict(&[
1999                ("Type", name("Annot")),
2000                ("Subtype", name("Widget")),
2001                ("FT", name("Tx")),
2002                ("T", text("SharedField")),
2003                ("V", text(value)),
2004            ]))
2005        };
2006        let catalog = dict(&[(
2007            "AcroForm",
2008            Object::Dict(dict(&[(
2009                "Fields",
2010                Object::Array(Array::of([widget("Hello, world"), widget("")])),
2011            )])),
2012        )]);
2013        let form = load(&catalog).expect("a form");
2014        let field = only_field(&form);
2015        assert_eq!(field.name, "SharedField");
2016        assert_eq!(field.widgets.len(), 2);
2017        // The field's value is the **first** entry's, which is what both
2018        // controls show.
2019        assert_eq!(field.value(None, &NoResolve), "Hello, world");
2020    }
2021
2022    #[test]
2023    fn two_fields_entries_with_different_names_stay_two_fields() {
2024        let widget = |field_name: &str| {
2025            Object::Dict(dict(&[
2026                ("Type", name("Annot")),
2027                ("Subtype", name("Widget")),
2028                ("FT", name("Tx")),
2029                ("T", text(field_name)),
2030            ]))
2031        };
2032        let catalog = dict(&[(
2033            "AcroForm",
2034            Object::Dict(dict(&[(
2035                "Fields",
2036                Object::Array(Array::of([widget("one"), widget("two")])),
2037            )])),
2038        )]);
2039        assert_eq!(load(&catalog).expect("a form").len(), 2);
2040    }
2041
2042    #[test]
2043    fn the_need_appearances_flag_is_read_off_the_acroform() {
2044        let acro = dict(&[("NeedAppearances", Object::Bool(true))]);
2045        let catalog = dict(&[("AcroForm", Object::Dict(acro))]);
2046        assert!(load(&catalog).expect("form").need_appearances);
2047        // Absent reads as false.
2048        let catalog = dict(&[("AcroForm", Object::Dict(Dict::new()))]);
2049        assert!(!load(&catalog).expect("form").need_appearances);
2050    }
2051
2052    #[test]
2053    fn the_flag_word_accessors_read_the_documented_bits() {
2054        let f = FieldFlags::from_bits;
2055        assert!(f(1).is_read_only());
2056        assert!(f(2).is_required());
2057        assert!(f(1 << 12).is_multiline());
2058        assert!(f(1 << 13).is_password());
2059        assert!(f(1 << 18).is_editable_combo());
2060        assert!(f(1 << 21).is_multi_select());
2061        assert!(f(1 << 24).is_comb());
2062        assert!(!f(0).is_read_only());
2063        assert!(!f(0).is_editable_combo());
2064        assert!(!f(0).is_multi_select());
2065        assert!(!f(0).is_comb());
2066        // Neighbouring bits must not alias: combo (bit 18) is not editable
2067        // combo (bit 19), and do-not-spell-check (bit 23) is not do-not-scroll
2068        // (bit 24).
2069        assert!(!f(1 << 17).is_editable_combo());
2070    }
2071
2072    /// The two negative spec bits, read positively. `DoNotScroll` and
2073    /// `DoNotSpellCheck` are adjacent, so the alias test is also an
2074    /// anti-aliasing test.
2075    #[test]
2076    fn the_negative_spec_bits_read_positively() {
2077        let f = FieldFlags::from_bits;
2078        assert!(f(0).scrolls());
2079        assert!(!f(1 << 23).scrolls());
2080        assert!(f(1 << 22).scrolls());
2081        assert!(f(0).spell_checks());
2082        assert!(!f(1 << 22).spell_checks());
2083        assert!(f(1 << 23).spell_checks());
2084    }
2085
2086    /// `/Ff` has no set algebra by design (§6): the round trip is the whole
2087    /// contract, and a bit belonging to a `/FT` nothing here reads survives.
2088    #[test]
2089    fn the_flag_word_round_trips_unknown_bits() {
2090        let raw = (1 << 40) | (1 << 25) | 1;
2091        let f = FieldFlags::from_bits(raw);
2092        assert_eq!(f.bits(), raw);
2093        assert!(f.is_read_only());
2094        assert_eq!(FieldFlags::default().bits(), 0);
2095    }
2096
2097    // ---- `/CO`, the calculation order ----
2098
2099    /// A map-backed resolver, because `/CO` is a list of *references* and
2100    /// `NoResolve` cannot follow one.
2101    struct Store(std::collections::HashMap<u32, std::sync::Arc<Object>>);
2102
2103    impl Store {
2104        fn of(pairs: impl IntoIterator<Item = (u32, Object)>) -> Store {
2105            Store(
2106                pairs
2107                    .into_iter()
2108                    .map(|(num, obj)| (num, std::sync::Arc::new(obj)))
2109                    .collect(),
2110            )
2111        }
2112    }
2113
2114    impl Resolve for Store {
2115        fn fetch(&self, r: ObjRef) -> Result<std::sync::Arc<Object>, pdfrum_object::Error> {
2116            self.0
2117                .get(&r.num)
2118                .map(std::sync::Arc::clone)
2119                .ok_or(pdfrum_object::Error::UnresolvedRef(r))
2120        }
2121    }
2122
2123    fn reference(num: u32) -> Object {
2124        Object::Ref(ObjRef { num, generation: 0 })
2125    }
2126
2127    #[test]
2128    fn a_junk_first_kid_costs_that_kid_and_not_its_siblings() {
2129        // [oracle-bug] `LoadField` returns when `kids->GetDictAt(0)` is null
2130        // (`cpdf_interactiveform.cpp:871-874`), losing `real` along with the
2131        // broken entry. pdf.js skips the entry and keeps walking
2132        // (`#collectFieldObjects`, `src/core/document.js`), and so do we —
2133        // one unresolvable reference must not cost a page of fields.
2134        //
2135        // Object 9 is not in the store, so `/Kids[0]` resolves to nothing;
2136        // object 2 is a real text field.
2137        let store = Store::of([(
2138            2,
2139            Object::Dict(dict(&[
2140                ("FT", name("Tx")),
2141                ("T", text("real")),
2142                ("V", text("kept")),
2143            ])),
2144        )]);
2145        let catalog = catalog_with(vec![Object::Dict(dict(&[(
2146            "Kids",
2147            Object::Array(Array::of([reference(9), reference(2)])),
2148        )]))]);
2149
2150        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
2151        let form = Form::load(&catalog, &store, &limits, &mut diags).expect("a form");
2152
2153        let field = only_field(&form);
2154        assert_eq!(field.name, "real");
2155        assert_eq!(field.stored_value(&store), "kept");
2156    }
2157
2158    #[test]
2159    fn a_junk_first_kid_does_not_blind_the_terminal_probe() {
2160        // The oracle's `GetDictAt(0)` is a probe as well as a guard: the
2161        // lines after it (`:876-880`) ask the *first* kid whether it carries
2162        // `/T` or `/Kids` to decide branch-versus-terminal. Ours scans every
2163        // kid instead (`kids_are_fields`), so a null at index 0 cannot make a
2164        // branch node look terminal. Here `/Kids[1]` is a named field, so the
2165        // parent is naming structure and the kid is the field — even though
2166        // `/Kids[0]` says nothing.
2167        // The kid carries a `/Parent` back to the branch, which is how a
2168        // real file writes it: that is what its `/FT` and the first half of
2169        // its qualified name are inherited through.
2170        let parent = dict(&[("FT", name("Tx")), ("T", text("parent"))]);
2171        let store = Store::of([(
2172            2,
2173            Object::Dict(dict(&[
2174                ("T", text("kid")),
2175                ("V", text("v")),
2176                ("Parent", Object::Dict(parent.clone())),
2177            ])),
2178        )]);
2179        let catalog = catalog_with(vec![Object::Dict(dict(&[
2180            ("FT", name("Tx")),
2181            ("T", text("parent")),
2182            (
2183                "Kids",
2184                Object::Array(Array::of([reference(9), reference(2)])),
2185            ),
2186        ]))]);
2187
2188        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
2189        let form = Form::load(&catalog, &store, &limits, &mut diags).expect("a form");
2190
2191        // The parent is a branch, so the field is the kid, qualified by it.
2192        assert_eq!(only_field(&form).name, "parent.kid");
2193    }
2194
2195    /// Three text fields as objects 1, 2 and 3, and the catalog that lists
2196    /// them — `co` becomes the `/CO` array when it is `Some`.
2197    fn three_fields(co: Option<Object>) -> (Dict, Store) {
2198        let field_of = |title: &str| {
2199            Object::Dict(dict(&[
2200                ("FT", name("Tx")),
2201                ("T", text(title)),
2202                ("V", text("")),
2203            ]))
2204        };
2205        let store = Store::of([(1, field_of("a")), (2, field_of("b")), (3, field_of("c"))]);
2206        let mut acro = vec![(
2207            "Fields",
2208            Object::Array(Array::of([reference(1), reference(2), reference(3)])),
2209        )];
2210        if let Some(co) = co {
2211            acro.push(("CO", co));
2212        }
2213        let catalog = dict(&[("AcroForm", Object::Dict(dict(&acro)))]);
2214        (catalog, store)
2215    }
2216
2217    fn order_of(co: Option<Object>) -> Vec<usize> {
2218        let (catalog, store) = three_fields(co);
2219        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
2220        let form = Form::load(&catalog, &store, &limits, &mut diags).expect("form");
2221        assert_eq!(form.len(), 3, "the fixture has three fields");
2222        form.calculation_order(&catalog, &store)
2223    }
2224
2225    /// The rule the whole feature turns on: no `/CO`, no calculation. Falling
2226    /// back to "every field" here would recalculate documents that must be
2227    /// left alone.
2228    #[test]
2229    fn a_document_with_no_calculation_order_calculates_nothing() {
2230        assert_eq!(order_of(None), Vec::<usize>::new());
2231        // An empty array is the same answer arrived at the other way.
2232        assert_eq!(order_of(Some(Object::Array(Array::new()))), Vec::new());
2233    }
2234
2235    /// The array's order is the answer, and it need not be `/Fields`' order.
2236    #[test]
2237    fn the_array_is_the_order() {
2238        assert_eq!(
2239            order_of(Some(Object::Array(Array::of([
2240                reference(3),
2241                reference(1),
2242                reference(2),
2243            ])))),
2244            vec![2, 0, 1]
2245        );
2246        // A subset is legal: only the fields listed are calculated.
2247        assert_eq!(
2248            order_of(Some(Object::Array(Array::of([reference(2)])))),
2249            vec![1]
2250        );
2251    }
2252
2253    /// `GetFieldByDict` answers null for anything it cannot map, and the
2254    /// sweep skips it rather than stopping.
2255    #[test]
2256    fn entries_that_resolve_to_nothing_are_dropped() {
2257        assert_eq!(
2258            order_of(Some(Object::Array(Array::of([
2259                reference(9),   // no such object
2260                Object::Int(7), // not a dictionary at all
2261                reference(2),
2262            ])))),
2263            vec![1]
2264        );
2265        // A `/CO` that is not an array is not an order.
2266        assert_eq!(order_of(Some(Object::Int(1))), Vec::<usize>::new());
2267    }
2268
2269    /// The oracle indexes the array positionally, so a repeated field is
2270    /// calculated twice rather than de-duplicated.
2271    #[test]
2272    fn duplicates_are_kept_because_the_array_is_indexed_positionally() {
2273        assert_eq!(
2274            order_of(Some(Object::Array(Array::of(
2275                [reference(1), reference(1),]
2276            )))),
2277            vec![0, 0]
2278        );
2279    }
2280}