Opening a static XFA form gives its fields the values in its datasets
(a form filled in by another application). Each value went through
forms::set_value, which reads the whole field tree to find the field,
writes it, and recalculates, reading the tree again. With n values that
is about 2n readings of n fields: quadratic. A public 136-page form with
about 6,000 fields took 261 s to open, a 95-page one with about 4,000
took 112 s; 8,000 synthetic fields took over ten minutes.
forms::set_values fills a batch as set_value fills each in turn, reading
the form once and recalculating once after the last value (the
calculation order, ISO 32000-2 Table 224 /CO, runs once over the final
values). Fields with the same fully qualified name (12.7.4.2) still share
the value. A name the batch already wrote, or a twin of it, makes the
form be read again first, so each write starts from what the earlier one
left, as one by one. Each value reports its own result; a failed
recalculation fails the batch, as it fails set_value. The engine collects
the changed values and fills them with set_values, finding fields by name
through a map rather than a scan.
The two forms now open in 4 s and 3 s, with the same fields, values,
warnings and document information as before; 8,000 synthetic fields open
in 1.5 s.
Tests: set_values matches set_value one by one (values, twins, the
calculated total, every appearance, saved; refusals for an unknown name,
a read-only field, an unknown option and clearing a no-toggle-off radio
group the batch selected); it reads the form a fixed number of times; the
engine takes 59 data values on a 60-field static form exactly as filling
them one by one does (the read-only one refused); and opening 800 fields
takes under 16 times as long as 100 (about 4x now, about 48x before).
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M9: name a laid-out XFA form's signature fields by their SOM path, so other viewers find the signature
* M9: doc comments on the functions the signature naming touches
* M9: say other viewers in the signature naming's comments
* M9: a new layout keeps a signature a generated signature box holds
* M9: the re-layout test checks the signature's picture is kept too
* M9: a signature field named by its SOM path doesn't repeat a name a field already has
* M9: every name a field of the pass takes is known before a signature field is named, in either order
* M9: the signed-box test's doc comment is back on its test
A merge had left it above the next test, which has its own.
* refactor(xfa): replace model wildcard imports with explicit names (Sonar S2208)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(xfa): complete explicit model imports
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(xfa): drop unused model imports
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Import Corner, which main's parse.rs now uses
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M12: XFA check boxes sit where their margins, caption and alignment put them
* M12: a check box's automatic caption room and auto height follow the box as drawn
* M12: a caption reserve of zero sizes the caption to its content, as no reserve does
parity/acrobat-features.toml: form.xfa-dynamic evidence: both sides' entries; notes: PR's metrics sentence replaces 'metrics are approximate' on top of main's text (word-level merge).
parity/acrobat-features.toml: form.fill-text-field evidence is the union of both sides; notes keep both sides' sentences (DoNotScroll clause and the Password sentence).
* chore: resolve SonarCloud maintainability issues
* fix(packaging): use verify_sha256 helper function
* style: cargo fmt
* fix: restore wildcard imports and fix method-reference type errors
Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep XObject decodability check without binding content in interp
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
A text field PdfCraft added showed its typed text in Acrobat only while
the field had focus: as soon as the user clicked elsewhere the text
vanished (it was still stored, and printed). PDF-XChange Editor and
PDFgear were fine, and the same field made by MuPDF was fine in Acrobat.
Bisected in Acrobat with one-change variants of the same file: only the
variants whose /DR fonts are objects (or that have no /DR) keep the text.
The appearance stream's own fonts, /P, the appearance layout and /DA 0
made no difference. PdfCraft wrote Helv, TiRo and Cour inline in /DR.
ensure_form now adds them as objects, and turns fonts an earlier save
wrote inline into objects too (same dictionaries). An indirect /DR or
/Font stays the object it is. The XFA-to-AcroForm writer does the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text layout cut each embedded value to the bytes its block had left, but
the resolver cloned the whole value first. With a byte or two left and a
value starting with a multibyte character, every later run cloned the full
value and kept none of it. The resolver is given the bytes left and returns
only a prefix that fits.
A positioned root draws its children instead of flowing them, so nested
repeating subforms under it multiplied their instances without spending
the measurement budget that flowed instances spend. Drawing a subform
instance spends it too.
Writing a value at a high index creates every missing sibling before it,
each with its own copy of the name. The node budget counted siblings but not
their names, so a long name with a high index made thousands of copies of
it. The write's budget also counts the bytes of the names it creates; a
path past it is skipped with the existing warning.
Converting an imported data file copied each element's and attribute's
resolved namespace URI, so a long namespace declared once on the root was
copied into every element that inherits it; only the node count was
limited. The import counts the names, namespaces, attributes and text it
copies against a byte budget of four times the file-size limit, and refuses
the file as too large past it.
write_data_ops expanded every instance-count op into one queued node per
instance, then deduplicated the pass. A script adding rows one at a time
asks for one more instance each time, so n rows queued about n*n/2 nodes
before the deduplication. Each op queues only the instances the pass has
not queued yet, and a budget bounds the instances queued across passes
that removals separate, with a warning past it.
Every instance of a repeated subform records its own copy of each script,
and only the number of events was capped. A long script inside nested
repeating subforms was copied once per instance before any script ran. The
events hold at most as much script text as the XFA packets may; past that
the live form is marked truncated, as it is past the event cap.
Rich text resolved every embedded-field reference to a fresh copy of the
field's value, so a template repeating one reference to a long value many
times laid out a copy per reference. One text block expands at most as many
bytes of embedded values as a text of its own may hold; past that the
values are cut.