- engine: the on_changes test's XfaRun literals set highlight; the script
runner reads its template before borrowing the live form for to_js; has_event
reads the paged live form, as the rest of the runner does, so focus scripts
find a page-master copy by its own SOM path.
- js: property reads compare and copy the shared (Arc<str>) node text, and the
property tests build XHost with its current signature.
- xfa: the Emitter test literal sets groups and used; a field split that is
undone also restores the names handed out; rustfmt on a merged import.
- parity: drop the use/usehref gap from form.xfa-dynamic, as prototypes are now
resolved before the template is parsed.
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
Main's DataOp::Value now replaces a multi-select list's value-group picks in the
packet and in the data copy, so the reset writes through it and the separate
DataOp::Reset (and Want::picks) is dropped. The reset keeps the PR's indexed
targets and its pass over fields without widgets, beside main's floating-field
pass (which the unshown pass leaves embedded fields to), and finish() lays out
again for either an owed relayout or a changed embedded value. Parity notes and
evidence take both sides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
* fix(js): decode ', numeric, and HTML named character references in FormCalc Decode
* test(js): verify all HTML named entities and 8-byte name boundary (#6464)
* docs(js): remove duplicate decode_entities summary and document LATIN1_ENTITIES order (#6464)
* Forms: date fields name months in the date language, not always English (#5303)
`AFDate_FormatEx("dd-mmm-yy")` wrote "18-Oct-26" in an Italian interface,
where a localized Acrobat writes "18-ott-26". Form date formats,
`util.printd` and `util.scand` now take their month and weekday names
from the date language (Preferences > Date format > Language, which
follows the interface language until one is picked), and still read
English names typed in. The names are process-wide, so their test runs
in its own binary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: dates read back in every date language (#5303)
Trying every date language showed what a field wrote not always reading
back: names holding digits ("10月"), spaces ("tháng 10"), dots ("ม.ค.")
or combining vowel signs (Thai, Hindi, Telugu) fell apart word by word,
and Italian, Spanish, Romanian and Albanian share some short weekday and
month names ("mar"). The language's names are now found whole in the
text first, the longest at each place, and read as their English names;
the format's order of `mmm` and `ddd` settles a shared name. Both
AFDate parsing and util.scand do this, on text up to 1 KB. A test reads
back every language, each name format, and hostile text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: date names no field font can draw are drawn in English (#5303)
With the names in the date language, a Russian dd-mmm-yy field drawn in
Helvetica showed "18-????.-26": no /DR font held "окт." and the WinAnsi
stand-in drew each letter as ?. The same held for Japanese, Chinese,
Greek, Arabic, Thai and others, and for Latin names outside WinAnsi
(Czech "říj"). Before, the names were English and always drew. An
AFDate/AFTime field now formats its value again with English names for
a widget whose fonts can't draw the localized text; a custom Format
script is run again with util.printd writing English names for such a
widget, and its result kept only when it draws whole. A field whose
font holds the names keeps them, and scripts otherwise still print the
date language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: an English weekday before the month keeps the month (#5303)
With Italian dates, "Tue 17 Mar 2026" in a "ddd d mmm yyyy" field read
as no date: the language's names were found in the text, but not
English ones, so "Tue" left the format's weekday unfilled and "Mar",
which Italian shares between Tuesday and March, was taken as the
weekday. Both AFDate parsing and util.scand lost the month, and
Spanish, Romanian and Albanian ("mar") did the same. English names are
now found too and fill their field of the format as localized ones do.
A test reads English and mixed weekdays before the month in those
languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: an English date run is drawn even when it prints the value (#5303)
A Helvetica field storing "18-Oct-26" whose Format script prints it
again as dd-mmm-yy still showed "18-???.-26" in Russian. The English
run printed "18-Oct-26", but it was dropped for equalling the stored
value, which was how a failed run was told apart: a failing script
handed back the value unchanged. The English run now says whether it
ran (`None` when the script failed or the runner can't write English
dates), and what it made is drawn whenever it draws whole. A test
reprints the stored value under Russian, and checks that a failing
script keeps its text and is reported undrawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: a short weekday typed without its dot is the weekday (#5303)
With French dates, "mar 6 janv 2026" in a "ddd d mmm yyyy" field read
as no date through util.scand, and "mar 06/01/2026" in "ddd dd/mm/yyyy"
through AFDate too. French writes Tuesday as "mar.", which a name
search only found with its dot; since English names are found as well,
the English "Mar" (March) was the only name of that length, so the
weekday became a month. Romanian "mar" did the same. A short name
ending in a dot is now also found typed without it, as the same kind of
name, so the format's order of name fields decides between it and an
equal name of the other kind. Both copies of the name search change. A
test reads French and Romanian weekdays typed without their dot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: a shared name is the month when no weekday is typed (#5303)
With Italian dates, "17 mar 2026" in a "ddd d mmm yyyy" field read as
no date, nor did "mar 17, 2026" in "dddd, mmmm d, yyyy". AFDate leaves
weekdays out of what it reads (they are shown, not typed), but the name
search still expected the format's weekday first, so "mar", which
Italian shares between Tuesday and March, filled it and left no month.
Spanish and Albanian "mar" and Albanian "sht" (Saturday, September) did
the same, and the keystroke check refused those dates. Where such a
read finds no date, the names are now read again with no weekday
expected, so the shared name is the month; a typed weekday still comes
first ("mar 17 mar 2026"). util.scand reads its fields in order and
needs the weekday, so it is unchanged. A test types those dates with the
weekday left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: Turkish names typed in capitals are read (#5303)
With Turkish dates, "1 MAYIS 2026" and "1 EKİM 2026" in a d mmmm yyyy
field read as no date through AFDate and util.scand, and "SALI 6 OCAK
2026" through util.scand. Names were compared lower-cased, which turns
I into i, not the dotless ı of "Mayıs" and "Salı", and İ into i with a
combining dot above, which "Ekim" doesn't have. Names and typed text
are now folded alike, the capital I and the dotted and dotless i all as
i and the combining dot dropped, one piece for each char so positions
in the text stay aligned: in both copies of the name search, the AFDate
month reader and util.scand's name matcher. English reads as before. A
test types Turkish names in capitals and English ones as a check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: Greek names typed in capitals are read again (#5303)
Folding names char by char turned a capital Σ into σ, so "ΙΑΝΟΥΆΡΙΟΣ"
no longer matched "Ιανουάριος", whose last letter is the final ς. The
fold now makes ς a σ on both sides, in the forms and JS parsers alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
`num_text` printed every non-integer with twelve fixed decimals. A value with
six or more digits before the point then shows the binary noise of the double
that holds it: a FormCalc calculate `qty * price` with price 279893.88 gave
`279893.880000000005`, and a Sum() of five such amounts gave
`1466917.350000000093`. Real invoice forms (USACE ENG Form 93, a dynamic XFA
payment estimate) show these in their amount and total fields.
Print at most 15 significant digits (the most a double holds for every value)
and at most 12 decimals, as before for small values, so sums that should be
zero still print 0. Only a fraction loses trailing zeros: past 15 digits before
the point no decimals are left and the integer keeps its zeros.
Co-authored-by: seanGSISG <seanGSISG@users.noreply.github.com>
On a large dynamic XFA form, every script of an event copied the whole form
twice before it ran (the engine's live form to the script engine's tree, then
that tree to the flattened form the host works on), and every resetData call
rewrote and rescanned the whole document. On a 1,300-script form opening spent
its 3 s budget on these copies and on twenty resets, and skipped the last 850
initialize and calculate scripts.
The engine now sends the scripts of an event one shared tree (Arc) until the
live form changes in any way, and the script thread keeps the flattened form
it built from that tree; each script starts from a copy whose text is shared
(Arc<str>), so it costs a fraction of a flatten. Each script still gets its
own copy: nothing one script changes reaches the next except through the
engine. Resets in a row in one script's effects are applied as one reset of
the names together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
`row.border.edge[0]` with a field `border` in the row no longer falls back
to the row's own border when the field has no `edge`; `row.#border` still
names the property by class (XFA 3.3 p. 96). `namedItem` on a list of
property objects reads each name as written in the run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
`row[*].border` with a field named `border` in one row and only the border
property in another listed the field and dropped the other row's property:
the property was looked up only when no child matched at all. Each object
the expression names now decides on its own: a child named as the property
comes first, and an object without one gives its property, in order.
resolveNodes' list holds both, and item() and namedItem() return a node or
a property object accordingly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
XFA 3.3 p. 38 gives a border's corners in the order top-left, top-right,
bottom-right, bottom-left, as its edges go top, right, bottom, left, and
says that when fewer than four are listed the last one listed is reused
for the rest. Readers don't do that: every reader tested draws a side not
listed from the edge or corner on the same side, one listed serving all
four sides, two alternating (0, 1, 0, 1), three reusing the second for
the fourth (0, 1, 2, 1). The layout already drew edges the readers' way,
but drew corners as p. 38's text says, so with corners [round, square]
only the top-left was round where readers round the top-left and the
bottom-right.
Corners now follow the readers too, through one helper, drawn_side, that
edges and corners share: with [round, square] the top-left and
bottom-right are round and the top-right and bottom-left square. Edges
are drawn as before. The scripting host's reads of a corner past those
listed (from the previous commit) mirror the same pattern, so a script
reads the corner the page draws.
Real forms (358 public forms with XFA, templates read through the
engine): 337 borders in 51 forms list two or three corners that differ,
all only in a square corner's presence, which the layout doesn't draw
differently; none has a rounded or inverted corner that would move, so
no rendered page changes.
Tests: one, two and three corners, two in both orders, and the
rounded-corners test's two-corner box (now [r1, r2, r1, r2]) fail with
the change reverted; a script test reads corners past those listed by
the same pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
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
* M6: form script messages keep their title, icon and buttons, and every one is shown (#4293, #4294)
* M6: a form script message waits for a notice the toast already shows
* M6: form script messages past the queue's room are counted and reported
* M6: a sentence break in the merged parity note
* M6: an object that names no parameter is a message's text, as a String object or a caught error
* M6: the count of left-out script messages is said again if another notice replaces it before its time
* M6: a full stop between two sentences of the FormCalc parity note
* chore: re-run CI (a frame-budget timing test missed on a shared runner)
Five XFA tests failed at random on a busy machine. The scripts of an
opening or a change share a 3 s wall-clock budget (a click's share 5 s),
each script has 1 s or 3 s, and the engine has 10 s to start; under load
the tests went past one of them, and the scripts they check were skipped
or did not start.
Session::with_xfa_time_scale (and Automation::with_xfa_time_scale)
multiplies each of these limits for a test; the default stays 1. The
script session counts the engine threads it starts on the calling thread
(xfa_session::engines_started).
The hundreds-of-calculations tests run with the limits raised, and the
engine's test now checks that opening starts one engine and a change one
more. A new test sets the factor to 0 and checks that the budget skips
all 210 calculations and says so. Three more tests that failed the same
way under load run with the limits raised.
crates/js/src/lib.rs: module doc app/util list has execMenuItem and scand; install() doc comment from main.
crates/js/src/tests.rs, crates/engine/src/tests.rs: both sides' appended tests kept.
parity/acrobat-features.toml: js notes: execMenuItem wording plus scand wording.
* 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>
Found while testing crates/js/src/formcalc.rs:2328-2330 ("format" number-picture branch, `if num < 0.0 { text = format!("-{text}") }`)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>