* feat(js, forms): field setItems, setAction and a bounded toSource (#269)
Ported from #344 by @DWAA1660 onto the per-document script session.
- `field.setItems(items)` replaces a combo or list box's items (text, or
[text, export] pairs). The value stays when an item still has it, and
otherwise becomes the first item's. Written as the field's /Opt (pairs
only where the export value differs), /I dropped, field redrawn.
- `field.setAction(trigger, script)` installs a JavaScript action for one
trigger (MouseUp/Down/Enter/Exit, OnFocus/OnBlur, Keystroke, Validate,
Calculate, Format), replacing only that trigger's action.
- `Object.prototype.toSource()` writes plain data (strings, numbers,
booleans, null, undefined, arrays, objects) that eval reads back. It
refuses functions, cycles, more than 64 levels, 10,000 values or 1 MiB
with an error the script can catch.
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* feat(engine): run a form's open, Will Save and Will Print actions and its widgets' pointer and focus scripts (#269)
Ported from #344 by @DWAA1660 onto the per-document script session: each
runs in the document's retained realm, so state the document scripts and
earlier events left is kept.
- The catalog /OpenAction's JavaScript (and its /Next chain, bounded and
cycle-safe; other action types skipped) runs once on opening, after the
document scripts, for background opens and reloads too, and again on
Revert in a fresh realm. Event Doc/Open.
- `Session::run_document_actions` runs /AA /WS (Will Save) or /AA /WP
(Will Print). A failing action is reported and never stops the save or
print. doc_save and doc_print run them first and report what they said
(`will_save`, `will_print`).
- Scripts that run unasked (open, Will Save, Will Print, pointer over,
focus) may only ask for highlight, zoom, focus or a page; a click's
Mouse Down / Mouse Up keep their requests.
- `Session::field_trigger` runs a widget's Mouse Enter, Mouse Exit, Mouse
Down, Mouse Up, Focus or Blur JavaScript (one undoable edit), reading only
that widget's dictionary. `field_focus` now runs an AcroForm widget's On
Focus / On Blur as well as XFA enter/exit.
- New tool `js_event`; form_focus's description says AcroForm focus
scripts run too.
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* feat(ui): Will Save before saving, Will Print before printing, and widget pointer scripts in the app (#269, #247)
Ported from #344 by @DWAA1660.
- Save (both paths) runs the document's Will Save actions first, and Print
its Will Print actions; what they change is saved or printed, and what
they say is shown.
- The pointer moving onto or off a form widget queues its Mouse Enter /
Mouse Exit script, a press its Mouse Down. A click on a check box or radio
button queues its Mouse Up after the toggle, so the script reads the value
the click stored (#247). The queue runs after the frame's edit.
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* docs(parity): document actions and widget triggers are partly supported (#269)
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* test: check toSource's text (scripts can't eval it) and the trigger event names (#269)
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* test: a check box's Mouse Up reads its new value; keep the fixtures on screen and the realm across an in-place save (#247, #269)
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* fix(engine, forms, js, ui): review fixes for document actions and field triggers
- widget trigger actions follow /Next, each action its own edit; errors don't stop the chain
- setAction installs on the field the object stands for when names repeat
- toSource refuses non-plain objects and a __proto__ key
- doc_print checks the destination before Will Print; js_event returns only its own output
- combo/list clicks run Mouse Up; a refused click's Mouse Up doesn't run
- label crops are re-read when Will Print changed the document
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* forms: run a pointer trigger on the field it hit and refresh the edited field's draft
A trigger now carries the field's object, so of fields sharing a name the one under the
pointer runs its own script (Session::field_trigger_target); js_event passes the field it
resolved. A trigger script changing the field being edited shows in its editor unless the
user already typed there, as focus scripts do.
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* forms: carry the field's identity through focus scripts and js_event
Focus transitions now track the field object, so moving between fields sharing a name is a
move and the field that gained or lost focus runs its own script
(Session::field_focus_target). js_event takes an optional field_id from form_fields to pick
one of fields sharing a name.
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
---------
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
* AcroForm: integrate persistent timesheet script workflows
* AcroForm: address review caching, worker stacks and live metadata
* M6: address second review of AcroForm script workflows
* M6: handle top-level ASI boundaries and verify cyclic calculation results
* M6: bound script workers and isolate pending completions across edits
* M6: make form resets visible before subsequent script reads
* M6: include calculated dependents in reset comparison baselines
* M6: bound reset writeback work before native completion
* M6: charge expanded reset selections and field name bytes
* M6: bound string conversion bytes and charge reset widgets
* M6: preserve unread defaults and bound prompt text conversion
* M6: bound reset calls early and preserve calculated field identity
* AcroForm: address grouped calculations, field identity and widget ownership
* AcroForm: retire field instances and address native review edge cases
* AcroForm: route comparison Save As and Print to the source document
* M6: dispatch text widget mouse-up scripts
* M6: honor fit bounds for form button icons
* AcroForm: bound calculation work and preserve reset and save integrity
* Harden AcroForm script requests and worker recovery
* Keep form callbacks and focus consistent across edits
* AcroForm: preserve reset ordering and bound edit callbacks
* Fix reset appearance ordering and XFA focus after row removal
* Keep XFA focus indexes and stopped reset appearances consistent
* Document form script execution and reset helpers
* Keep script workers bounded and reject partial callback edits
* Keep form events available after a document script reaches VM limits
* Keep main's first-value Simple calculation and restore the calculateNow helper test
AFSimple_Calculate (JavaScript) and the native Simple calculation count each field's first
value again, as #5304 landed them; summing every selection of a multiple-selection list is
a separate behaviour change. Restores the document-helper calculateNow test and the
AFSimple_Calculate doc comment, and adds native and JavaScript multiple-selection cases.
* Patch the callback field snapshot in place instead of rebuilding it
Each field callback rebuilt all field states (3,129 on the benchmark form), and the
retained realm held the previous snapshot, so stamping instance tokens copied it again.
The runner now keeps its last snapshot and patches only the values and flags that
changed; ScriptSession::stamp_fields stamps it before dispatch, the worker copies only
when a token changes, and the realm drops the job's snapshot when the job ends (field
identity is checked against the cached keys instead).
* An oversized job script is that script's error, not a refused edit
Realm::run refused a script too long or too deep to run as an execution refusal, which
made the engine reject the whole edit (setting a 300 KiB calculation script failed). As on
main it is now a plain script error that keeps the value; the realm is not touched.
* Read a leading #! line as a comment in the chain scan; follow main's refusal and conditional limits in tests
The nesting guard from #5079 now checks brackets, prefixes and conditionals; the document
worker's preflight still bounds arrow-function and right-associative chains and skips a
leading #! line as the guard does. A refused script is an error, not a refused edit, so
the preflight test now checks the edit goes through with the error reported.
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Fix#5211: persist JavaScript field colours
* Read current document state in field scripts
* Redraw form fields after validation changes
* Format validation redraw regression test
* Keep field list alive in redraw test
* Refresh fields only after same-field scripts
* Format conditional field refresh
* Borrow accepted field value for storage
* Redraw calculated fields from current appearance
* Format JavaScript field state merge
* Restore JavaScript runner document state
* Group JavaScript runner form inputs
* Read current field display state in scripts
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Fix#5211: make calculateNow synchronous
* Bound and cache calculation execution
* fix(engine): invalidate field calculation cache
* perf(js): build calculation scripts lazily
* Refresh calculation cache after page insertion
* Load calculations for document script helpers
* Assert imported calculation result
* Initialize formatted field-change state
* Update formatted calculation test expectations
* Include the referenced field in calculation test
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
crates/forms/src/values.rs: the read imports take both listed_indices and
selected_options (list selection) and main's listing_fields_named (radio
fields sharing a name).
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
* M5: a comment whose colour is written [r g b null] is drawn, as CMYK with the null read as 0 (view.render-annotation-appearances)
ISO 32000-2 Table 166 /C (and /IC, Tables 178, 180, 181; /MK /BC and /BG,
Table 192) is an array of 0, 1, 3 or 4 numbers whose length picks the
colour space. A four-entry array with a non-number in it, such as
[1 0 0 null] (PDFBOX-2699), made the /C parser give up, so a shape
without an appearance stream was not drawn at all. PDFium, MuPDF and
Poppler all read such an array as CMYK with the non-number as 0.
- annot: a four-entry /C or /IC reads non-numbers as 0; other lengths
still need every entry to be a number.
- annot: the properties' fill uses the same parser (it read
[r g b null] as RGB before), and /C no longer drops a non-number
to make three numbers.
- forms: /MK /BC and /BG read a four-entry array the same way instead
of dropping the non-number and drawing RGB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: comment properties report the stroke colour as drawn; a reference among four colour entries is not read as 0 (view.render-annotation-appearances)
props() reads a non-FreeText /C through the appearance parser, so a
[1 0 0 null] stroke reports CMYK cyan as drawn, and grey and CMYK
strokes report their converted colour. A reference among the four
entries of a /C array (ISO 32000-2 7.3.10) is left unreadable instead
of standing in as 0, so /C [1 0 0 6 0 R] is never drawn cyan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: re-run CI (fill_sign Escape test depends on the clock's second, passes locally)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: the comment list reports a comment's colour as drawn (view.render-annotation-appearances)
summaries() accepted only a three-entry /C, so a Square with /C [1 0 0 null],
drawn cyan and reported cyan by props(), had no colour in the comment list;
grey and CMYK colours were dropped the same way. Non-FreeText comments now
read /C through drawn_color (the array itself followed when it is given by
reference), keeping the three-number fallback and the FreeText /DA handling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: the render inspector lists a comment's colour as drawn (view.render-annotation-appearances)
The inspector accepted only a three-entry /C, with a referenced entry read
as 0, so a /C [1 0 0 null], a grey or a CMYK colour listed none, unlike the
comment list and the drawing. Non-FreeText comments now read /C by the
appearance builder's rule (one entry grey, four CMYK with a non-number as 0
and a reference unreadable, finite and clamped), with the three-number
fallback reading referenced entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
---------
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Refs #2354. Thai typed into these tools is shaped and drawn from the Thai face through Add text's writer (now pdfcraft_fonts::shaped_text), searchable and copyable, instead of being written as ?????; a build without a Thai face refuses it with a message.
A widget's /AP /N that is neither a stream nor a state subdictionary with
a stream for /AS draws nothing (ISO 32000-2 12.5.5, Table 170), yet the
display copy treated any /N as the file's own appearance. Text, choice and
push-button widgets whose /N is null, a form XObject dictionary without its
stream data, or a subdictionary without /AS now get the drawn appearance a
widget without /N gets, and check boxes and radio buttons whose state entry
is not a stream are drawn from /MK. The test is the same one the renderer
uses to pick the stream.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
qualified_name reads each ancestor through Document::resolve, which follows an object that is
nothing but a reference to another, as document inspection does, so both name the kid the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- fill: the first same-named field's read-only flag still applies when a later one draws the
choice, and no radio field of the name is written when any of them is read-only.
- enumerate: a nameless /Fields widget its parent already claimed is not read again as a nameless
field; one its parent's /Kids omits is still read on its own.
- walk: only kids whose /Subtype is /Widget replace a merged field's own widget.
- inspection: a kid whose /Parent is another field is named from that chain even under a direct
listing field, and widget annotations beside named kids are listed as a field of the parent's
name (ISO 32000-2 §12.7.4), as the form reader lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA