* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures The xfa crate lays out the field kinds that were left blank or shown as text fields: - Choice lists become combo boxes (editable with textEntry) or list boxes (open="always"; multi-select with open="multiSelect"), with the shown items and the saved values of <items save="1"> as /Opt pairs, /V, /I and /DV as saved values, several on separate lines in the data. Field::saved_value maps shown text to saved values for the layout and the script tree alike, so scripts read what is saved. - Password fields are password text fields; signature fields are /Sig fields the app can sign. - Image fields show the picture the data (base64) or the template holds; barcode fields show their value as text, with a warning that no bars are drawn. - PNG and GIF pictures (fields and draws) are decoded to Flate image XObjects with a soft mask for transparency; JPEGs stay as they were. Sizes come from the headers, so layout never decodes; decoding is capped at 40 million pixels and failures are warnings, not errors. Engine: multi-select list values from scripts and data are one per line. README and parity notes updated (form.xfa-dynamic). Hardened after an adversarial review pass: picture sizes refused from the header past 20 million pixels or 65,535 a side, the GIF decoder given limits (a frame far larger than its screen is refused before allocation), template pictures read whole up to 32 MB, pictures decoded once and remembered in the catalog by content hash so re-layouts reuse them, data pictures that aren't images reported by field; choice values matched against saved values first, several values deduplicated and capped, /I ascending and unique, /DV through the same mapping, <value> children read, the item cap reported; the reopen path takes another viewer's choice values (several per line); CMYK JPEGs inverted only with Adobe APP14. * D5: README for cos and engine, and an honest cos status line Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets. Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes. Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true. Co-Authored-By: Claude Opus 5 <[email protected]> * Preserve alternate appearances and indirect push-button values Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test. Co-authored-by: Codex <[email protected]> * M7: select and edit grouped Form XObject figures Co-Authored-By: Codex <[email protected]> * M14: let neutral Windows locales fall through to display language Co-authored-by: Codex <[email protected]> * pdfcraft-cli --help lists `mcp` only when the build has it Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed `pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command". USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead of calling it an unknown command. - tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and mcp_without_the_feature_says_it_was_left_out (without the feature) Co-Authored-By: Claude Opus 5.5 <[email protected]> * engine README: the execution plan is local-only, so don't link a path that doesn't exist * Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher (not stable across Rust releases) and read back from the file. Each image XObject now records the SHA-256 of its picture's bytes, and a re-layout finds the XObjects on the pages it replaces by that hash; nothing goes in the catalog. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Tanish Manku <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]> Co-authored-by: sanyamsingh001 <[email protected]> Co-authored-by: Codex <[email protected]> Co-authored-by: Flowershangfromthebranches <[email protected]> Co-authored-by: Codex <[email protected]> Co-authored-by: Streetblock <[email protected]> Co-authored-by: lloyd <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
pdfcraft-xfa
XFA forms (XFA 3.3, the template and layout parts). Layer L3; depends on pdfcraft-cos and
pdfcraft-fonts only.
A dynamic XFA form is a PDF shell around an XML template (/AcroForm /XFA): one placeholder page
("requires Adobe Reader") and /NeedsRendering true. Viewers without an XFA engine show the
placeholder. This crate reads the template, lays it out, and writes ordinary pages and AcroForm
fields into the document, so everything else in PdfCraft (rendering, filling, saving, printing,
the automation tools) works on it unchanged. The engine does this when it opens such a form.
What it does
-
Packets (
read_packets): the XDP from one stream or the(name, stream)array; UTF-8 and UTF-16; capped at 64 MB. -
Template (
parse): subforms, areas, fields, draws, exclusion groups, page sets with page areas, content areas and media; measurements in mm, cm, in, pt and px; fonts, paragraphs, margins, borders (edge order rules), captions with reserves, check-button items, pictures, rich text (exDataXHTML: paragraphs,br, bold/italic/underline/size spans, embedded fields), inline JPEG, PNG and GIF images, lines, rectangles, occurrences,breakBefore/breakAfter, overflow leaders,columnWidthsandcolSpan. -
Layout (
layout): positioned,tb,lr-tb/rl-tb,tableandrowlayouts; a container without a width is as wide as its content; flow across content areas and pages with page masters chosen by occurrence; a table's header row repeats after a break; presencehiddentakes no space,invisibletakes space; repeating subforms get their initial instances; fields that compute the page number or count on layout show the numbers. -
PDF (
pdf::write_form): pages with content streams in the standard 14 fonts (Arial and friends → Helvetica, Courier New → Courier, serif faces → Times; nothing is embedded), JPEG XObjects, and widgets: text (multi-line, max length, alignment), date (Acrobat'sAFDateformat and keystroke actions from the picture clause), check boxes and radio groups (fromexclGroup), push buttons with Reset / Print / Save As / URI actions recognised from their scripts. Appearance streams are left empty for the forms layer to generate. Each field keeps its SOM path in/PCSom; the AcroForm gets/PCXfaLayoutso a saved form is not laid out twice. The XFA packets stay, so Adobe's viewers keep rendering the form from them. -
Data (
data): thedatasetspacket. On layout, a field takes its value from the data node at its SOM path (dates in ISO form, check and radio states by their on values), and a repeating subform or row gets as many instances as the data has.write_datasetsmerges the AcroForm fields' values (by/PCSom, or by the Designer field names of a static form) into the existingxfa:data: only the bound nodes' text changes and missing nodes are added, so unbound data, other namespaces, attributes and comments stay byte for byte. Values it can't write (a node holding structured content, absurd SOM indices, a packet that is neither UTF-8 nor UTF-16) come back as warnings; UTF-16 packets are written back as UTF-16.read_valuesgoes the other way, so a form filled here shows its values in Adobe's viewers and a form filled there shows them here. The engine does both: values on open (recorded in the document's warnings), the packet after every form edit.
Layout measures each container once per (node, width): width-less containers would otherwise be measured exponentially often in their nesting. A measurement budget backs this up.
-
Scripts (
script): the live form as scripts see it (form_tree: the instance tree with values, presence and access, and theScriptEvents in document order, built withinMAX_FORM_NODESobjects and cut short, withtruncatedset, past that;has_scriptssays whether a template has any, and the engine skips all of this for forms without), the presence and access overrides scripts make, rows added and removed in the data (DataOps, applied to the in-memoryDataNodewhile an event runs and written bywrite_data_opsas one rewrite of the datasets packet per event), andrerender, which lays the form out again from template, data and overrides. The scripts themselves run in thejscrate's XFA object model; the engine runs initialize and calculate on open, change, exit, validate and calculate after a field changes, and click for buttons.Private keys PdfCraft writes (other viewers ignore them):
Key Where Holds /PCSomgenerated fields the field's SOM path, so values go back to the data /PCXfaLayoutAcroForm what laying out produced, so a saved form is not laid out again /PCXfaOverridesAcroForm << /Presence << /som (hidden) … >> /Access << /som (readOnly) … >> >>: presence and access scripts set, by SOM path (instance 0 of a repeating subform stands for all); applied to the template before every layout, so undo, save and reopen keep them. Adobe's viewers draw from the XFA packets and don't read it; at most 10 000 entries are read or written/PCXfaClickgenerated push buttons true: the template has a click script for this button. Such buttons carry no/A: XFA JavaScript is not Acrobat JavaScript, so it is never written as a/S /JavaScriptaction other viewers would run. PdfCraft finds the script by the field's/PCSom
API sketch
if pdfcraft_xfa::is_dynamic(&doc) {
let report = pdfcraft_xfa::render_into(&mut doc)?; // pages, fields, warnings
for f in pdfcraft_forms::fields(&doc) { pdfcraft_forms::redraw_field(&mut doc, &f.name)?; }
}
let form = pdfcraft_xfa::layout_xml(template_xml)?; // pages of items, for tests
Deliberately not done (yet)
- Data binding is the default one only: explicit
bind refexpressions, global binding and data descriptions are not followed, and no standalone XML or XDP data file is imported or exported. - Scripting. JavaScript (boa) and FormCalc (a native interpreter,
pdfcraft_js::formcalc) share one object model, one set of effects and one set of budgets. FormCalc covers the language and the common built-ins (arithmetic, logical, string, date/time, financial, unit); not locale-aware pictures beyond simple number and date ones, orGet/Post/Put(refused). The object model covers what forms commonly use (xfa.formnavigation and SOM resolution,rawValue,presence,access, instance managers,xfa.hostmessages, reset, print, focus and URLs,xfa.layoutpage numbers,xfa.event); notxfa.template, data descriptions,xfa.connectionSet,border/font/uiproperties (reads give a sink that accepts writes),execEvent, or the change event per keystroke. Validate failures show their message and keep the value, as Acrobat does for scripts. A hidden instance of a repeating subform hides every instance (overrides apply to the template). A repeating subform shows what its data holds once the data holds any instance, else itsinitialcount, and never fewer than one (occur min="0"still shows one row). Each script runs in a fresh engine on its own thread; a form with hundreds of calculate scripts pays that on every field change (a shared engine per event is the obvious next step). - Script limits. Forms run their scripts on open without being asked, so the engine bounds each event: a script's effects (values set again on the same object merge, the last wins; 10 000 effects, 100 message boxes, 1 000 console lines per script and per event), its loop iterations (100 000 per call frame for initialize, calculate and validate, 1 000 000 for click and change), 2 000 scripts and 50 relayouts per event, and 3 s (open, changes) or 5 s (click) of scripts per event, after which the rest are skipped and reported. Only a click may print, save, open a link or move the focus; other events asking are noted in the console. What open-time scripts change is noted in the document's XFA warnings. Known gaps: the engine's loop limit is per call frame, so loops inside nested calls can run far longer; such a script is abandoned after 1 s (3 s for a click) on its thread, which keeps running until the engine's own limits end it, and the document's scripts are turned off. Memory a script allocates is not capped (the same holds for AcroForm JavaScript).
- Static XFA forms (
/NeedsRenderingabsent, AcroForm fields present) keep their AcroForm; only their data is read and written. - Field kinds. Choice lists become combo boxes (editable with
textEntry) or list boxes (open="always", multi-select withopen="multiSelect"), with the shown items and the saved values of<items save="1">as the options; the data holds saved values, several on separate lines. Password fields are password text fields. Signature fields are/Sigfields the app can sign. Image fields show the picture the data or template holds (JPEG, PNG, GIF) but a new picture can't be chosen; barcode fields show their value as text, not as bars. - Missing:
keepconstraints,subformSetrelations,rl-tbis mirroredlr-tb, font metrics are the approximate Helvetica ones, and line heights are 1.15 × size. - Acrobat cannot be an oracle here (clean-room rules): layout follows the specification, and fidelity has been checked by eye on real government forms, not pixel-compared.