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}