From 8e85e0b7ab913191263d01642416bfff27ca010e Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:38:15 +0800 Subject: [PATCH 1/7] M12: resolve XFA use/usehref prototypes before parsing the template Designer style sheets give fields and draws their size, anchor, borders, fills, fonts and caption placement through usehref references to elements in a subform (XFA 3.3, chapter 6, "Prototypes"). The parser did not follow them, so such forms lost their section bars, input boxes and check-box placement. The new crates/xfa/src/proto.rs writes the template again with every reference into the same template merged in, and parse() reads that: - references: usehref "#id", "#som(expr)" (with or without the legacy "."), and use "#id" or a SOM expression; usehref wins over use; - rules from "Overriding Prototype Properties": own attributes, data content and children win; id and name are not inherited; children match by class and position, and a matched prototype child is the prototype of the element's own; a ui widget or value kind and rich text come from the nearest layer that has one; - prototypes in other documents are not followed (no file or network access); a missing target, a loop (Example 6.8) or a long chain is left out with a warning; - caps: 32 merged layers, 256 levels of nesting, 2 million elements of SOM search, and growth to 4x the template (1 MB floor, 64 MB ceiling); past a cap the template is read unresolved, with a warning. The resolver is ported from the closed #1566 by EULArule, who invited reuse, and changed: the name attribute is no longer inherited, use takes SOM expressions, ids and SOM look-ups are indexed and cached under one search budget, loops through ancestors are cut, the output cap is relative, every fallback is a warning, and the saved form state is still read from the original XDP. Part of #3432. Co-authored-by: EULA <90616501+EULArule@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC --- crates/xfa/README.md | 17 ++ crates/xfa/src/lib.rs | 1 + crates/xfa/src/parse.rs | 16 +- crates/xfa/src/proto.rs | 399 ++++++++++++++++++++++++++++++++++ crates/xfa/src/proto/tests.rs | 229 +++++++++++++++++++ parity/acrobat-features.toml | 4 +- 6 files changed, 663 insertions(+), 3 deletions(-) create mode 100644 crates/xfa/src/proto.rs create mode 100644 crates/xfa/src/proto/tests.rs diff --git a/crates/xfa/README.md b/crates/xfa/README.md index 2d08a74bb2..c48058385e 100644 --- a/crates/xfa/README.md +++ b/crates/xfa/README.md @@ -27,6 +27,18 @@ that warning and keeps the existing layout and accepted widget values. text-field limits (`text/maxChars` and `exData/maxLength`, carried into AcroForm `/MaxLen`), inline JPEG, PNG and GIF images, lines, rectangles, occurrences, `breakBefore`/`breakAfter`, overflow leaders, `columnWidths` and `colSpan`. +- **Prototypes** (`proto`, XFA 3.3 chapter 6, "Prototypes"): before parsing, every `use` and + `usehref` reference into the same template is resolved and the template is written again + with it merged in, so Designer style sheets (`usehref=".#som($template.#subform.styles.X)"`) + give their fields and draws size, anchor, borders, fills, fonts and captions. The element's + own attributes, content and children win; `id` and `name` are not inherited; children match + by class and position, a matched prototype child being the prototype of the element's own; + a `ui` widget or `value` kind and rich text are taken from the nearest layer that has one. + References are `#id`, `#som(…)` (or `.#…`) and, for `use`, a SOM expression. Loops are cut + where they close, chains merge at most 32 layers, nesting stops at 256 elements, SOM look-ups + share a budget of 2 million elements, and the written template may grow to 4 times its size + (at least 1 MB, at most 64 MB); past a budget the template is read unresolved. Each case is a + warning. - **Layout** (`layout`): positioned, `tb`, `lr-tb`/`rl-tb`, `table` and `row` layouts; 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; presence @@ -159,6 +171,11 @@ let form = pdfcraft_xfa::layout_xml(template_xml)?; // pages of items, for file's data is held as a tree and written with the packet's own namespace prefix, never as the file's markup, and every rewrite of the packet is parsed back before it is written; the engine then lays a dynamic form out again from it and runs its calculations. +- **Prototypes in other documents** (`usehref="file.xdp#…"`) are not followed: that would + mean reading files or the network on opening a form. The reference is left out with a + warning. SOM references cover names, `#class` and `[n]`, not `[*]`, `..` or relative forms; + traversal declarations are merged like other children, not knitted together as "Resolving + Prototypes with Traversals" describes. - Missing: `keep` constraints, `subformSet` relations, `rl-tb` is mirrored `lr-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 diff --git a/crates/xfa/src/lib.rs b/crates/xfa/src/lib.rs index 7e63218c90..07b75a2042 100644 --- a/crates/xfa/src/lib.rs +++ b/crates/xfa/src/lib.rs @@ -16,6 +16,7 @@ pub mod packets; pub mod parse; pub mod pdf; pub mod picture; +mod proto; pub mod script; pub mod text; diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index 838d1730ea..b6526eb955 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -740,7 +740,21 @@ pub fn parse(xml: &str) -> Result<(Template, Vec), XfaError> { } else { root.descendants().find(is_template).ok_or_else(|| XfaError::Malformed("no