Conflicts: xfa Template keeps main's embedded/embedded_paths and this
PR's cache (parse() sets all three); engine xfa.rs tests keep both
sides' tests; xfa README combines main's sheet numbers with this PR's
property objects; parity form.xfa-dynamic keeps this PR's notes plus
main's floating-field sentences and both sides' evidence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Conflict: layout/flow.rs imports take both MAX_INSTANCES (this PR) and
MAX_MEASURES (main). Main's new check-box width arm in measure() now
passes the &Ctx this PR's per-instance measure takes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Conflicts:
- crates/xfa/src/parse.rs: main added quarter_turns before parse_common
(and rotate in its body); this branch gave parse_common a warnings
parameter for the property trees. Kept quarter_turns and the branch's
signature; the body holds both props and rotate.
- crates/xfa/src/tests.rs: both sides added tests at the end of the file
(this branch's packet-size test; main's rotated-draw tests). Kept both.
- parity/acrobat-features.toml (form.xfa-dynamic): main put the
rotated-draw test at the front of the evidence and a sentence at the
front of the notes; kept this branch's lines with main's additions
prepended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyG234xeYkJfr41E6iP
Conflicts:
- parity/acrobat-features.toml (form.xfa-dynamic): main added the
rotated-draw test to the front of the evidence and a sentence to the
front of the notes; this branch appended the prototype tests to the
evidence and put "prototypes first: ..." into the layout sentence of
the notes. Kept both on main's lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyG234xeYkJfr41E6iP
Conflicts:
- crates/xfa/src/layout/measure.rs (positioned width and height): this
branch measures each child under its own SOM index (size_at with
sibs.at(i)); main reads the extent through standing_box so a turned
draw counts as the box it stands in. Kept both: size_at for the size,
standing_box(w, h).right() / .bottom() for the extent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyG234xeYkJfr41E6iP
Conflicts:
- crates/xfa/README.md: main added the sentence on turned draws where this
branch added the one on caption fonts; both kept, main's first.
- parity/acrobat-features.toml (dynamic XFA layout entry): main's evidence
and notes kept, with this branch's evidence test appended and its
"captions (in their field's font unless they name their own)" note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
A prototype shared by many elements was reached again from each of them and its own
reference read again every time, so a long reference (1 MB) on a prototype used by
60,000 fields cost tens of gigabytes of hashing while the written template stayed small.
Each element's reference is now resolved once. A SOM expression longer than 1,024 bytes
names nothing (the longest in 198 local Designer templates is 109 bytes).
Tests: a "billion laughs" template (ten levels, each prototype referencing the level below
ten times) fails as a whole within the output cap and the form is read as written; a long
Designer-style form resolves in full with no warning; the shared long reference; the SOM
length limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Nothing read rotate, so a turned line, picture or label was drawn as if it
stood upright: part of #5561. XFA gives the angle in degrees counter-clockwise
about the anchor, and Acrobat's flattened page for that issue's form puts the
three turned lines exactly there, which the test holds it to.
A turned draw is one item: a page break moves it whole, and what it holds is
written unturned under one matrix, so text, pictures and borders need nothing
of their own. The page count and link annotations look inside it, and its
container grows to the box it stands in. Fields, subforms and draws in a flow
don't turn yet.
That issue's own form is static, so it already shows Acrobat's page: this is
for dynamic forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A SOM look-up cut short by its budget was cached as "not found", so later
references warned that they named nothing and the template was only partly
resolved. It now stops the expansion: the template is read unresolved with
one warning, as the README says.
Merging looked at every child of every prototype layer, also those that
write nothing (proto, unchosen ui/value alternatives), so a prototype with
many proto children used by many elements did quadratic work bounded only
by output. Merging now charges children, attributes and namespaces against
a 64 million budget before looking at them, and leaves out the skipped
classes before indexing them; namespace look-ups use a set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
Designer style sheets give fields and draws their size, anchor, borders,
fills, fonts and caption placement through usehref references to elements
in a <proto> 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
XFA 3.3, "Formatting Text That Appears as Fixed or Variable Content"
(p. 43): text, caption text included, is formatted by its containing
element's font properties, inherited from an ancestor container when
it names none. A field's caption was drawn in the font around the
field, so a form whose fields set Arial and whose captions set nothing
showed its labels in the 10pt Courier default. Captions are now drawn,
and placed, in the field's font with the caption's own properties on
top, as they were already measured; a button's caption likewise. The
paragraph of a field caption is unchanged.
Part of #3432.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A table subform (layout="table") placed in positioned content was drawn
through draw_node, which drew each row subform once: the rows the data
holds past the first, and an occur initial count, were dropped with no
warning, while the same table in flowed content repeated them (XFA 3.3,
"Tables", p. 327; "Repeating Elements using Occurrence Limits", p. 263;
Chapter 9, p. 336).
The positioned table now draws each row once per instance, one under
another with its own SOM index, and is measured with them, so what
follows it in a top-to-bottom container moves down. The count and the
warning for the rows cut come from one helper, laid_out_instances, now
used by every layout that repeats a subform (flowed subforms, flowed
tables, positioned top-to-bottom, wrapped and table layouts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A repeating subform laid out at most 50 instances, so rows 51 and on of a
long list or table (from the data, or added by a button's addInstance)
were left out. XFA 3.3, "The occur element" (p. 786): max -1 sets no
upper boundary.
The per-subform bound is now 1,000: the largest occur count the parser
reads and the most rows one script run may add (MAX_ADDED). The layout
budgets still bound the whole form. Opening a form with 1,000 one-field
rows takes about 3 s in a debug build and 2,000 rows about 13 s, so a
much larger bound would let one list stall the app. Rows past the occur
max or the bound are still reported, and the warning now names the
layout's bound when that is what cut them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Scripts got an object that takes anything for `this.border`, `this.ui`,
`this.validate` and the like (XFA 3.3 ch. 3, "Properties vs. Children",
p. 78), so `ui.oneOfChild.className == "choiceList"` was never true,
`this.validate.nullTest == "error"` never matched, and
`border.getElement("edge", 0).color.value` threw a TypeError.
The parser now keeps each object's own attributes and its property
elements (border, font, para, margin, ui, validate, occur, break,
breakBefore, breakAfter, assist, format, value, bind, desc, extras, keep,
caption, items, calculate) as a generic tree, bounded per object: 256
elements, 16 KiB of text, 8 levels, 64 attributes an element; past that
the rest is left out with one warning. An element keeps only its own
text, not that of children left out (an extras, a script); a validation
or a calculation keeps its first script with its text outside the
budget, as forms read it, within the script engine's 256 KiB limit on a
script. A field's first two `<items>` (a field has two at most, p. 726)
are kept as lists: each counts against the budget as one element, and
its entries, outside it, are bounded by the list's own limits (1,000
entries), as the field's lists are; a further `<items>` (one public form
has a third, empty one) is an ordinary property, every entry charged.
The tree is shared through an Arc by the template, every instance of the
object and every script's snapshot, and the engine keeps one converted
copy of each tree in the form's template, so it is made once per opened
form rather than per event or per script, behind the form's own lock,
and goes when the form is closed. On a 10,000-field form with 17
property elements each, a script's snapshot takes 3.24 ms against
1.46 ms on main (median of 15 interleaved runs); the property trees are
converted once per form, and each later snapshot adds one walk over the
form and a lookup per tree.
In JavaScript a property object is a Proxy naming its node and path. It
reads what the template writes, or the default XFA 3.3 gives for the
properties the 367 inventoried public forms read: presence of border,
edge, corner, fill (p. 629, 687, 663, 734); colours (a fill white,
p. 734; a font fill, edges and corners black, p. 734, 687, 663); font
typeface/size/weight (pp. 741-744); para hAlign/vAlign (p. 800); margin
insets (p. 776); validate nullTest/scriptTest/formatTest (pp. 880-881);
occur min/max/initial (p. 786); break before/after (p. 632); bind ref
(p. 623); keep next/previous none and intact by container (pp. 762-763);
a comb's numberOfCells (p. 657); a gradient's type (p. 770, 806, 820); a
fill's type solid (p. 734) and a field's widget textEdit (p. 877); a
calculation's override, "disabled" where the template has no calculate
element and "error" where it has one that doesn't say (pp. 641-642).
Where XFA 3.3 leaves the outcome open, or where every reader tested
contradicts its text, reads follow the readers. A border's presence
reads "hidden" when the object has no border element ("visible" with
one that doesn't say, p. 629). For the edges and corners a border
doesn't list, p. 38 says the last one listed is reused; readers draw
such a side from the edge or corner on the same side instead (two
listed alternate, three reuse the second for the fourth, one serves
every side), and reads follow the readers: a side past those listed
reads as the one drawn there (with none listed, the defaults: black,
0.5pt, solid, a square cap, a corner's radius 0in, square join, not
inverted, p. 687, p. 663), and is made as a copy of it (with what a
script already set on it) when a script reaches it, so a write then
changes only that side. A border has four sides: a fifth or later edge
or corner, even one the template lists, is out of reach, as the layout
draws four. The copies have their own budget (8 MiB a run), apart from
the run's text budget, so reaching sides never crowds out a script's
writes; past it a side still reads as the one drawn there. A fill type
the fill doesn't have (its pattern when it is solid, p. 734) can't be
read: the read raises an error the script can catch (GeneralError,
"operation failed"), the fill keeps its type, and `"pattern" in fill`
is false. Anything else unspecified reads as an empty value that
compares as "" and never throws; further defaults are added as forms
need them.
Property objects have className, oneOfChild, children by name
(desc.vMandatory, message.scriptTest) and by class (ui["#textEdit"]),
getElement(name, index), isPropertySpecified, getAttribute,
setAttribute, rawValue (a desc's first text), a validate's or
calculate's script text and message (p. 827), resolveNode(s),
somExpression and nodes (length/item/namedItem); nodes get getAttribute,
getElement, isPropertySpecified, and mandatory, validationMessage,
formatMessage and mandatoryMessage read from their validate's nullTest
and message texts (the text of that name, as the template or a script
named it, else a single unnamed one, which serves every test: p. 782,
p. 880). Setting a test's message sets its named text, else overwrites a
single unnamed one (so every test then reads it), else adds a text of
that name, as readers do; reading validate.message.<test> where no such
text exists raises a script error, as in readers. fillColor, fontColor
and borderColor read from the border fill, font fill and first edge
colours; borderColor set colours all four edges. A child of the same
name comes before a property, an attribute or a member, on nodes and on
property objects alike, for reads and writes (a write to a member a
child shadows leaves the property alone), as XFA SOM resolves name
clashes in favour of the explicitly named object, and `#name` reaches
the property (p. 99). A SOM expression from a node, or from
xfa.resolveNode, that runs into objects' properties
(resolveNode("ui.#textEdit"), "border.edge[1]",
"row[*].border.fill.color", "extras.arrayGroup[*].groupName", "ui.*")
gives those property objects, each instance's, 4,096 at most (about 30
of the forms do this); `value.#text` on a draw stays null, as before,
until a node's value is its value element.
Writes (border.fill.color.value = ..., validate.nullTest = ...,
rawValue, setAttribute, mandatory, validationMessage = ...) are kept for
the rest of the script run and read back through the same properties,
bounded by the run's text budget and 64 attributes a property, but have
no effect on the form yet.
The caption (XFA 3.3 p. 643) and a field's items (p. 758) are property
objects too. A caption's value holds its label as text or rich text,
read by name, by class (`['#text']`), through resolveNode and
oneOfChild; rich text reads as its words; the content a value doesn't
hold reads as null (one of them, p. 598). Items list their entries
through `nodes`, each with the text the choice list or check button
shows. A field's `items` is its first `<items>` (the shown list), a
second is reached by index (`items[1]`), and a field with none reads an
items object with no entries. Items reads follow the template:
`HNode::items` is the hook PdfCraft's choice-list methods (addItem,
clearItems; not on main yet) will set, so that reads agree with them
once they land. exData.loadXML and saveXML do nothing yet. Over the 163
dynamic forms (118,208 objects) no object reaches the budget: elements
p99 30, max 41 (limit 256); bytes p99 773, max 8,500 (limit
16 KiB); item entries per list p99 51, max 342. The choice-list
methods (selectedIndex reads still give an object that takes anything),
execEvent and script objects are handled separately. The one known gap
is value.text and value.exData (about 15 of the 367 forms): a node's
value stays its raw value, and changing what value returns is left to a
follow-up with a full-corpus regression check. FormCalc does not read
these properties yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
* 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.
ROADMAP.md, docs/roadmap.md: all status lines and progress-log lines kept; M12 row takes both 'Done since' insertions.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
ROADMAP.md, docs/roadmap.md: both status lines and progress-log lines kept; M12 row takes both 'Done since' insertions.
crates/automation/tests/automation.rs, crates/xfa/src/fixtures.rs: union of #2624's and #2619's tests and fixtures.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
crates/xfa/src/script.rs: template_field and named_in moved to crate::data in #3397; field_grows_with_text calls crate::data::template_field.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
An XFA form with no AcroForm fields was always classified as dynamic, so
opening it laid the template out into new pages and appended them as a
revision: the pages the user saw, and any later save, were PdfCraft's
layout rather than the file's own pages, with nothing saying so. Adobe
Designer writes <dynamicRender>forbidden</dynamicRender> in the config
packet of a static PDF form; such a form is now static and its pages are
kept. /NeedsRendering still makes a form dynamic, and with no word from
the config a form without fields stays dynamic as before.
The config packet is found in an /XFA array or a whole-XDP stream, read
with a 16 MiB decode cap, and scanned for the one element.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Flattening the fields of a laid-out XFA form used to leave the XFA in
the file: Adobe's viewers then laid the form out again from the
template over the baked pages, so the "flattened" PDF was still a live
form there and a plain one everywhere else.
pdfcraft_xfa::strip takes the XFA out of a document: the packets
(template, data and all), /NeedsRendering on the catalog and the
AcroForm, the layout mark, the overrides, and the private keys on
every field down the tree. The engine calls it after Flatten form
fields on a document with XFA packets, so the result is a plain PDF
every viewer shows the same, and for the new Edit::XfaToStatic
("Convert to a static form"; tool xfa_to_static), which keeps the
laid-out pages and the fields with their values as an ordinary
AcroForm that no viewer lays out again and whose scripts no longer
run. Both undoable: the engine's post-edit refresh re-reads the
layout mark to drop or restore its XFA state and the cached template.
What the packets held beyond the fields is reported (packets by
name, data no field showed, buttons left inert, the layout's own
warnings, Reader usage rights) as document warnings, and a dynamic
form that was never laid out is refused rather than left with
nothing. Applying redactions shares the same strip. Closes the last
planned XFA parity entry (form.xfa-flatten shipped).
* XFA: one script thread per event, and the live form kept up to date in place
A form with hundreds of calculate scripts used to spend its open-time
budget on set-up: every script got a thread of its own, and the engine
dropped its live form after every value a script settled, rebuilding
the whole tree for the next script.
The js crate gains xfa_session::XfaSession: the scripts of one event
run on one thread (with the script stack), each in a fresh JavaScript
context or FormCalc interpreter on a copy of the form as the engine
holds it then, so scripts share nothing, a script that fails halfway
leaves nothing behind, and what a script changed reaches the next one
only through the engine. A script past its time limit is abandoned on
the thread and the session is dead from then on; a fault in the engine
(a panic) loses only that script, the thread is replaced. On wasm the
scripts run inline, as before.
The engine's runner keeps one session per event and, for a value, now
refreshes the one field in its live form in place (xfa::refresh_value,
computing the value as the tree builder would: saved values, template
defaults) instead of rebuilding the tree; rows, presence, access,
resets, relayouts and data rewrites still rebuild it. Under test, the
live form is compared with a fresh build before every script and at
the end of every event, and a difference ends the test binary (a panic
would be caught by the engine's guard and turned into a script error).
210 calculate scripts (one per row of 10 sections x 20 rows, plus a sum
per section), debug build, instrumented inside the engine: the script
event on open 495 ms -> 204 ms (of which live-form rebuilds 217 ms ->
0), on a change 300 ms -> 172 ms; through the tools, layout included,
open 1.27 s -> 0.97 s and a change 0.44 s -> 0.22 s. The rebuild cost
removed grew with form size times script count.
* CI: the GoToR tests' case lists are only grown on Unix, so Windows must not warn on their mut
Export Data on an XFA form writes its data as Acrobat does: XML (the
data alone, rooted at the form's root data node, its namespaces
declared) or an XDP holding the whole datasets and a reference to the
PDF (.xdp, a new format). Import Data takes an XDP, a datasets or data element, or a
data document whose root is the form's root data node, and makes it
the form's whole data, keeping data descriptions; a laid-out form is
laid out again from it (rows added and removed) and its calculations
run; XFDF, FDF and tables still go the AcroForm way. Undoable.
Image fields are now buttons marked /PCXfaImage (no PDF action): the
forms layer treats one as an image field, so clicking it (or
form_set_image) picks a picture, which is kept in the data as base64
with its xfa:contentType, as Designer saves it, and the laid-out form
shows it from there.
xfa: replace_data, write_data_image (hostile content types cleaned,
32 MB cap), export_data, import_data (64 MB cap, the parser's node
limit, self-closing data elements). engine: export routing by
document kind, import routing by the file's root element, re-layout
and calculation after the data changes outside the fields, the
picture's content type from its bytes. UI: XML and XDP in the export
dialog of an XFA form, .xdp in the import filter. Tool texts, README
and parity (form.xfa-data) updated.
Hardened after an adversarial review pass: the file's data is held as
a tree and written with the packet's own namespace prefix (never its
markup), every packet rewrite is parsed back before it is written,
files capped at 16 MB and 64 levels (a tag scan before the recursive
parser, also guarding the datasets parser), pictures at 8 MB and PNG/
JPEG/GIF only, the file's shape and root name checked, UTF-16 and
ISO-8859-1 decoded, pictures into data groups refused, cleaned names
for pictures, static forms empty the fields the file doesn't hold,
more rows than the layout cap reported, one datasets stream per edit,
and only image fields take pictures into the data.
A Designer form whose table (a top-to-bottom subform inside an area of a
positioned page) asks for several rows with `<occur min="10"/>` was laid
out with one row, and a FormCalc total looping over the rows stayed
empty.
- layout: a repeating subform inside a top-to-bottom container in
positioned content is drawn and measured once per instance (occur and
data), with its own SOM index, as flowed content already was. One
`instances` helper now serves flowing, table rows and this case.
- data: areas are named in SOM expressions but are not data scopes.
`data_path(tpl, som)` leaves them out (unknown segments are kept), and
layout, the script tree, reading and writing the datasets and the
engine's value and row operations use it. Before, values Adobe's
viewers saved were not read, what was filled here went under a new
element named after the area (which Adobe's viewers would most likely
not read), and scripts counted the rows there: one, so a loop over
the table's rows failed on the missing ones.
- write_datasets_with / read_values_with take the parsed template when
the caller has it (the engine's cached one); the old functions parse
the document's.
- Tests: layout regression, data_path rules, and an engine round trip
shaped like such a form (rows, Adobe-written values, a FormCalc total
over every row, the data written back, reopening).
Signed-off-by: Hadi Chokr <hadichokr@icloud.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 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 <noreply@anthropic.com>
* 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 <codex@openai.com>
* M7: select and edit grouped Form XObject figures
Co-Authored-By: Codex <noreply@openai.com>
* M14: let neutral Windows locales fall through to display language
Co-authored-by: Codex <codex@openai.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
---------
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* XFA: run FormCalc scripts
A FormCalc interpreter (XFA 3.3 part 8) in the js crate, over the same
form table, effects and budgets as the XFA JavaScript model, so the
engine runs both languages alike: expressions and FormCalc coercions,
var, if/while/for/foreach/func, accessors ($, $host, $form, $record,
$layout, $event, xfa., .., [n], [*], _name instance managers) and the
arithmetic, logical, string, date/time, financial and unit built-ins.
Get/Post/Put refuse.
The JavaScript model's XHost, HNode, SOM resolution and instance
operations are shared (pub(crate)) with Context-free add_instance and
remove_instance that the boa wrappers call.
Bounded like the JavaScript path: steps, loop iterations, recursion,
parse depth, string and list sizes, clamped dates, the instance cap,
and the per-activity time budget with abandonment.
Tests: language, built-ins, control flow, form objects and hostile
scripts in the js crate; a FormCalc fixture form in the engine that
initializes, calculates, validates, adds and removes rows, shows a
subform and round-trips through save; hostile scripts at the engine
level. parity form.xfa-formcalc planned -> partial.
Hardened after two adversarial review passes: statement nesting and
chain length count against the limits (verified on a 1 MiB stack), the
script honours the engine's deadline itself, strings are capped after
every built-in and messages/values clipped and noted (shared host),
lists hold each object once and the shared SOM resolver deduplicates,
values go into fields only (shared host, JavaScript too), saturating
Stuff and time arithmetic, FormCalc-specific size check instead of the
JavaScript refuse scan. Semantics: bare row[*] and row[n] from inside a
row see every row, relative [-n]/[+n] indexes, Count of non-null,
Parse with a date picture gives the ISO date, assignment is an
expression.
* form.xfa-formcalc: bound FormCalc values, results, errors and copying (#301 review)
- XHost::set_value cuts a value to 64 Ki chars before storing it, so later
reads match what is saved and the snapshot stays bounded.
- A script's completion value (calculate result) is cut to 64 Ki chars with a
note, in both the FormCalc and the JavaScript paths.
- throw clips its value like any message.
- Copying, dereferencing and comparing long strings is charged to the step
budget (len/256), so wasm (no clock) can't spend unbounded time on it;
OneOf charges per comparison.
- Exists propagates step/deadline/depth/size limits, answering 0 only for
lookup failures.
- IPmt/PPmt charge a step per period.
- The parser counts each top-level expression's chain from zero, so an
elseif condition no longer inherits the previous arm's count.
- Comment on the interpreter's depth limit fixed; parity notes say memory
isn't capped and Uuid/Date/Time are deterministic on wasm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M12: run XFA forms' JavaScript: calculations, validation, rows, visibility (#60)
Laid-out XFA forms had their fields but none of their behaviour: totals
didn't compute, "Add row" buttons did nothing, and subforms a check box
should reveal stayed hidden.
The js crate gains the XFA scripting object model as native boa objects
over a snapshot of the form tree: xfa.form navigation by name, SOM
resolution (absolute, relative with the containing-subform search,
descendants, indexes and wildcards), rawValue/presence/access, instance
managers (_name, addInstance, removeInstance, setInstances, count),
xfa.host (messageBox, resetData, print, setFocus, gotoURL), xfa.layout
page numbers, xfa.event, app, console and util, plus unqualified sibling
names as XFA resolves them. Scripts run with `this` bound to the node and
yield their completion value; everything they change or ask for comes
back as effects.
The xfa crate builds the live form tree (instances from the data, values,
presence and access with overrides) and the event table from the
template, keeps presence and access overrides on the AcroForm
(/PCXfaOverrides) so undo and reopening preserve them, adds and removes
row instances in the datasets packet through the existing merge
pipeline, and lays the form out again on demand.
The engine runs initialize and calculate scripts on open (an appended
revision, nothing dirty), exit, validate and calculate after a field
changes (a failing validate shows its message and keeps the value, as
Acrobat does), and click scripts for buttons through run_javascript, so
the app's buttons and the js_run tool work unchanged. Values a script
sets go to the fields and the data at once; rows and visibility changes
re-lay the form out. All of it is one undo step. JavaScript off turns it
off. FormCalc scripts are recognised and reported, not run.
Bounds: script length and nesting as before, loop and recursion limits,
at most 1000 instances added per run, 2000 scripts and 50 re-layouts per
event; a calculate script that adds rows, or a click script looping on
addInstance/setInstances, ends quickly with a bounded form (tested).
Checked on IRCC's IMM 5669: opens with its page-number scripts, the Print
button's xfa.host.print runs through js_run, fill and save keep the
original bytes.
* M12: Save and SaveAs named actions run the file commands (#60)
XFA forms' Save buttons are app.execMenuItem("SaveAs"), which the layout
turns into a SaveAs named action; the shell answered it with "the SaveAs
action isn't supported yet". Named Save and SaveAs now run file.save and
file.save_as, like Named Print opens the print dialog.
The buttons test covers Save against a file on disk (SaveAs opens the
native dialog, which a headless test must not).
* M12: bound XFA form scripts against hostile forms (#60)
Review fixes for the XFA scripting PR (#230):
- Effects: values, presence and access set again on the same object merge
(last wins, behind order-dependent effects); 10k effects, 100 message boxes
and 1000 console lines per script and per event, reported when hit. An
event's data writes are kept in memory and written as one rewrite of the
datasets packet; later writes in the same edit replace that stream in place.
- form_tree stops at 200k objects and says so; forms without scripts build no
live form and run nothing.
- Only click scripts may print, save, open links or move the focus; others are
noted in the console. Open-time messages and errors show right after open.
- Lower loop limits for initialize/calculate/validate, a time budget per event
after which remaining scripts are skipped and reported, and a per-script time
limit after which a runaway script is abandoned and the document's scripts
turned off.
- What open-time scripts change is noted in the XFA warnings.
- A document's Named /Save no longer writes the file on click (SaveAs opens
the Save As dialog, as a script's execMenuItem does).
- XFA click scripts are no longer written as /S /JavaScript actions: the
button carries /PCXfaClick and runs by SOM path. Private keys documented.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>