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