mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 16:32:04 +00:00
* 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>
176 lines
18 KiB
Markdown
176 lines
18 KiB
Markdown
# AcroForm script workflows
|
|
|
|
This contribution connects the existing AcroForm object model to the native viewer and the
|
|
shared automation tools. The regression fixture is generated in Rust; it contains no private
|
|
document, employee, signature image or real PIN.
|
|
|
|
## Runtime and lifecycle
|
|
|
|
Each open document owns a `ScriptSession`. On native targets its Boa context lives on one
|
|
worker thread with a dedicated 64 MiB stack reservation (the transient stateless runner retains
|
|
its separate stack policy). Document scripts initialize once with `Doc/Open`, before the requested event;
|
|
button, console, format, validate and calculate scripts then share that context. Field objects
|
|
read the latest snapshot supplied by the engine, including objects retained by earlier scripts.
|
|
Exposed Field objects are cached by PDF object identity; synthetic fields use name keys. Separate documents cannot share globals or
|
|
icon objects. `eval` and dynamic Function construction remain disabled.
|
|
|
|
Changing the document-script sources recreates the context. Closing, reopening, reloading,
|
|
Revert, disabling JavaScript, and saving a full rewrite release/reset the context. Incremental saves retain it. A rewrite can reuse PDF object numbers, so old Field and icon objects must not be carried across it. PDF Undo/Redo restores PDF state;
|
|
it does not serialize or rewind arbitrary JavaScript variables. Globals are not saved into the
|
|
PDF. A worker fault resets its context; the existing cooperative source/loop/recursion bounds
|
|
remain in force. The source preflight additionally caps active arrow chains at 256 and right-associative
|
|
assignment/exponentiation chains at 4,096 across open bracket levels. Independent statements
|
|
and comma-separated sibling expressions reset only their own level; comparisons do not count.
|
|
Keyword unary/new chains share the prefix
|
|
depth bound of 64. These conservative limits protect recursive parser/compiler paths before
|
|
execution on the smaller worker stack. Boundary and refusal cases run in subprocess regressions.
|
|
These bounds do not establish a hard JavaScript heap quota.
|
|
|
|
`calculateNow()` evaluates the current calculation list synchronously, in `/CO` order, using
|
|
pending writes. Later statements in the same invocation see the new values. The caller's
|
|
event is restored after calculation, including failure. Reentrancy is capped at eight levels.
|
|
Field writes and their dependent recalculation use the existing single undo transaction.
|
|
Pure caption, geometry, style and visibility updates do not run the calculation chain.
|
|
Page geometry is read once per edit runner. Calculation sources/order are analyzed once on its
|
|
first callback and reused; field values and appearance properties still refresh on every callback.
|
|
Structural/document changes between batch children refresh the snapshot, and editing a field's
|
|
calculation script or calculation order invalidates the cached plan before recalculating.
|
|
`numPages`, `documentFileName` and `info` read the current event snapshot, including page/info edits,
|
|
Undo/Redo and completed Save As, without reinitializing script globals.
|
|
|
|
## Fields, appearances and icons
|
|
|
|
- `buttonGetCaption()` reads the first widget. An unindexed `buttonSetCaption()` updates every
|
|
child widget, including when the first already has the requested text. An unchanged uniform
|
|
caption does not create an edit. Only normal face `0` is implemented.
|
|
- `rect` reads/writes rotated user-space `[left, top, right, bottom]`. Writes affect widget 0,
|
|
as specified for an unindexed Field. Conversion uses its page rotation and effective Crop
|
|
box. Small/zero-area rectangles remain representable, rather than applying the interactive
|
|
field-authoring minimum size to a script's overlay.
|
|
- `textColor`, `fillColor`, `strokeColor`, `textSize`, `lineWidth`, `borderStyle`, `userName`,
|
|
`readonly` and `display` flow to the form model and saved appearances. Colour input supports
|
|
`T`, `G`, `RGB` and `CMYK`; text cannot be transparent. The existing appearance editor writes
|
|
its canonical RGB representation. Partial changes preserve unrelated fonts and properties.
|
|
- `buttonGetIcon()` returns a host-minted object for a normal Form XObject icon. `buttonSetIcon`
|
|
accepts only a handle minted in the same context, or null to clear it. It reuses the form and
|
|
its resources, including transparency, instead of rasterizing a signature/background. The
|
|
existing button appearance generator draws it. Other icon faces are refused.
|
|
- `getPageBox` reads effective/inherited page boxes in rotated user space. `zoom`,
|
|
`app.runtimeHighlight` and `setFocus()` communicate with the native viewer. Automation
|
|
reports viewer requests and does not open an external application or change a real viewer.
|
|
|
|
## Native menus and input
|
|
|
|
`app.popUpMenuEx` supports nested menus, separators, marked/disabled items, explicit return
|
|
values and cancellation. `app.response` supports named and positional parameters, defaults,
|
|
password masking and null cancellation. Alert replies use the documented numeric button ids.
|
|
Only the worker waits for a reply; egui continues drawing/input. The central modal gate blocks
|
|
document shortcuts and page input while a prompt is visible. Idle prompt polling uses a
|
|
one-second repaint interval; replying wakes the UI immediately and worker polling remains 16 ms. A reply resumes the same
|
|
invocation, without replaying the script or its side effects.
|
|
|
|
An explicit button captures document identity, generation and view geometry. Closing/switching
|
|
the document, editing it, disabling scripts or changing the relevant view discards a stale
|
|
completion and invalidates the old runtime. A reply is checked against an enabled menu leaf
|
|
and its enabled ancestors; invented/disabled return values fail with a script error. Prompts
|
|
have at most 1,024 entries, 16 submenu levels, 4,096-byte strings and 32 calls per invocation.
|
|
The native desktop admits one pending button interaction. A document worker refuses a second
|
|
run while the first is active, including while waiting for a prompt; it never queues a
|
|
synchronous field callback behind an unanswered dialog.
|
|
|
|
The native prompt UI is not implemented for the web build. The existing headless/unsupported
|
|
host path cancels unanswered popups and response dialogs; no web-interaction parity is claimed.
|
|
This work does not add timer execution, PDF annotations/layers to the JS object model, or the
|
|
remaining document action triggers.
|
|
|
|
## Automation
|
|
|
|
The existing `js_run` accepts `answers`, an optional array of up to 32 strings or null values:
|
|
|
|
```json
|
|
{
|
|
"doc": 1,
|
|
"script": "var t=app.popUpMenuEx({cName:'Manual',cReturn:'manual'}); if(t==='manual') getField('time').value=app.response({cQuestion:'Time'});",
|
|
"answers": ["manual", "07:45"]
|
|
}
|
|
```
|
|
|
|
The schema advertises a 4,096-character maximum per answer; the handler additionally enforces
|
|
a 4,096-byte UTF-8 budget, so multi-byte strings can reach the byte limit earlier.
|
|
Replies are consumed in prompt order. Null/missing replies cancel a menu/response; alert
|
|
defaults choose acknowledgement, cancellation or No according to the button set. No direct
|
|
file, process or network API is exposed by a prompt. Root confinement and permission-checked
|
|
document edits continue through the existing automation/engine boundary.
|
|
|
|
## Related contributions
|
|
|
|
The persistent runtime uses the upstream work from #5304 for synchronous calculations,
|
|
#5501 for page boxes, #5499 for colour parsing and writeback, and #5505 for normal captions.
|
|
The integration supplies the live document state, exact field identity, and grouped field
|
|
selection needed by the persistent context.
|
|
|
|
The bounded `popUpMenuEx` parser in `crates/js/src/interaction.rs` was adapted from #2081.
|
|
The prompt channel and default menu renderer support interactive form scripts. #2081 can
|
|
supply an anchored renderer through the UI menu callback when it adds link actions. Once
|
|
the shared parser lands upstream, this integration can use it directly.
|
|
|
|
`ScriptSession::run_host_task` gives future timer and page actions (#4751) access to the
|
|
retained realm on its own worker.
|
|
|
|
Document actions and widget triggers (ported from #344 by @DWAA1660) run through the same
|
|
realm. On opening, the document scripts run, then the catalog `/OpenAction`'s JavaScript
|
|
and its `/Next` chain (event `Doc/Open`); Revert runs them again in a fresh realm.
|
|
`Session::run_document_actions` runs `/AA /WS` before a save and `/AA /WP` before printing:
|
|
the app's Save and Print and the `doc_save` and `doc_print` tools call it first. A failing
|
|
action is reported and never stops the save or print. `Session::field_trigger` runs a
|
|
widget's Mouse Enter, Mouse Exit, Mouse Down, Mouse Up, Focus or Blur JavaScript; the app
|
|
queues them from the pointer and the focus, and `js_event` runs them headlessly. A check
|
|
box's or radio button's Mouse Up runs after its click has stored the new value (#247).
|
|
Scripts that run unasked (open, Will Save, Will Print, pointer over, focus) may only ask for
|
|
highlighting, zoom, focus or a page.
|
|
|
|
The lexical preflight recognizes unambiguous top-level and block-body ASI boundaries before identifiers, including declarations and assignments separated by line breaks or comments. It preserves counters after operators and member access, and across binary keyword operators. It distinguishes control-statement parentheses for regexp literals; inside expression parentheses, array/object literals and template interpolations it remains conservative; this is not a complete ECMAScript parser or ASI implementation.
|
|
|
|
Native persistent workers are capped process-wide at four on 32-bit targets and sixteen on 64-bit targets. Each reserves 64 MiB of stack; closing a document releases its slot after the worker exits. Capacity errors ask the caller to close a scripted document and retry; other documents are never silently evicted. The transient stateless runner has its separate existing stack policy.
|
|
|
|
Synchronous runs have one ten-second host deadline covering startup and every prompt round trip. A timeout replaces the document realm; late globals and custom Field properties cannot reach later runs. This is isolation, not forcible termination of an OS thread: cooperative engine limits still govern the detached worker, whose capacity slot stays charged until it exits. No hard CPU or heap quota is claimed.
|
|
|
|
An asynchronous completion stays reserved until the engine accepts it. An intervening edit forks a fresh realm before any field calculation, including when the edit later fails. Pending results carry runtime identity as well as generation, so discarding an old result never clears a newer realm. Script prompts render last and explicitly raise their retained egui layer, including when another app dialog opens later. Null prompt answers cancel popup/response input; alert answers instead select the button set's default (OK, Cancel or No).
|
|
|
|
`resetForm()` updates the live field shadow synchronously, including retained Field references and named parent groups. Reset requests still reach the PDF engine for default-value and appearance semantics; the atomic writeback performs resets before applying the final script delta. Delta comparison uses the reset baseline, so a later script assignment restoring the original pre-reset value is preserved. Doc Open and the first button/console event share this ordering, including after JavaScript is disabled and re-enabled. PDF Undo/Redo and save/reopen preserve the final values.
|
|
|
|
The reset comparison baseline is obtained by running the existing forms reset path on an isolated document copy. It therefore includes unnamed built-in Calculate text/combo dependents, without executing custom Calculate scripts during the preview. Normal automatic calculation after script value writes remains in force; this does not establish a policy that explicit formula-field writes override their formulas. Tests cover saved defaults left untouched by the event and a non-default typed native response as well as existing default/cancellation paths.
|
|
|
|
Reset writeback is admitted before request copies, preview cloning and PDF mutation only when it fits 64 reset calls, 4,096 aggregate selectors, and 2^24 estimated work units. Named resets pay for the bounded selector scan and the fields actually matched. The estimate charges both preview/commit passes, field/calculation/dependency scans, calculation source lengths and selector scans/bytes. Excess work returns an explicit error without committing PDF edits or an undo entry. Under-budget requests retain their order; duplicate resets are not silently merged because repeated recalculation can be observable. This is an operation/amplification bound, not a hard UI wall-clock deadline. Native start/completion tests cover the 64-call boundary, 65/10,000-call refusal, selector overflow, aggregate large-form refusal, reset/restore, Undo/Redo and save/reopen.
|
|
|
|
The reset admission estimate also charges field-name copies, selector comparison bytes, and the worst-case expansion of each named reset to all fields (including both validation and membership scans). This conservative bound can refuse a named reset even if its eventual selection would be small; full-form resets avoid that named-selection cost. Refusal tests compare saved bytes as well as generation and undo state.
|
|
|
|
Reset admission includes the total widget count for preview and commit. The shared JavaScript string-array converter also enforces a 32 MiB aggregate escaped UTF-8 text budget before copying beyond it (including scalar input), independently of later request clipping. Exact Unicode and unpaired-surrogate output bytes are charged; the existing array-item limit remains. JavaScript ToString/getters may still execute script code before conversion; this is not a general JavaScript heap limit.
|
|
|
|
Unknown PDF defaults (failed stream decoding or a spent shared value budget) are marked in the JavaScript snapshot. A reset preserves their live values, matching the forms reset policy; explicit assignments before/after reset remain effective. The policy follows inherited defaults too. Menu and response properties now use the bounded escaped UTF-8 conversion at 4096 bytes per property before copying past that limit; user conversion exceptions are preserved. Existing item/depth/prompt/reply limits remain separate. Immediate dependent reads can use resetForm(...); calculateNow(); the reset callback timing itself has not been changed without an Acrobat parity oracle.
|
|
|
|
The 64-reset call budget is now enforced in the worker before argument conversion and field scanning, shared by document initialization and its first event, and reset for later jobs. A sticky outcome flag rejects PDF writeback even if JavaScript catches the exception. The existing engine admission/work guard remains. Calculations are resolved by PDF object reference to the matching snapshot position; event targets and duplicate-name value and property deltas retain object identity through the host, authoring helpers and redraw. Public getField(name) retains first-match behavior. Tests cover both calculation orders, target reads/writes, native/console completion, Undo/Redo and save/reopen.
|
|
|
|
AFSimple_Calculate expands exact/group/duplicate field matches without materializing their cross-product; numeric conversion and empty-result fallbacks remain unchanged. Selector comparison work is admitted up to 2^24 estimated units. The Field cache uses canonical PDF identities (name-only keys for synthetic callers), prunes removed fields between jobs and covers every exposed field, including more than 4096; renames preserve retained references/custom properties. Script color changes detach outside widgets that share the target group's indirect MK, including bounded reference aliases, while keeping target widgets' indirect object and unrelated MK entries. Native button clicks carry exact identities through hit testing, action dispatch and event construction. Legacy name-only engine/UI entry points remain wrappers. Headless form_fields returns field_id; js_run accepts that optional 48-bit identity with a matching field name, validates it before execution, and retains deterministic prompt replies. IDs are document-local and must be rediscovered after structural changes.
|
|
|
|
Field instance tokens live outside PDF undo snapshots. Successful edits and undo/redo retire
|
|
removed field instances immediately, even when no script job observes the gap. Reusing the
|
|
same PDF object number does not revive an old Field accessor; document globals survive history.
|
|
A refused synchronous/native writeback invalidates its document realm before returning an error.
|
|
The host-backed event accessor remains usable when globalThis is frozen. AFSimple_Calculate
|
|
shares a 2^24 selector-work budget across all calculation sources in an event job; later jobs
|
|
receive a fresh budget. Native simple calculations include every selected list export value,
|
|
and calculateNow uses the regular value setter's checkbox/radio normalization.
|
|
|
|
Native UI feedback distinguishes a refused busy click, a stale interaction and a timeout.
|
|
Timeouts report discarded results without claiming worker termination. Visible comparison halves
|
|
may complete; switching away, changing the view or editing still invalidates stale interactions.
|
|
Focus reveals a laid-out widget's page, and image import carries exact clicked identity through
|
|
the picker and SetFieldImageTarget. The headless form_set_image tool accepts the same optional
|
|
field_id and validates exact name/identity pairs; omitted IDs preserve legacy lookup. Mixed widget icons emit field-wide changes only when an
|
|
icon setter runs. UI waiting helpers use elapsed deadlines rather than machine-speed retry counts.
|
|
|
|
Comparison script document requests use their source view. Save As captures the source document without changing the comparison tab. Print activates and retains the source tab because the existing print dialog reads the active document throughout its lifetime. Missing source views never fall back to another active document. Regression coverage distinguishes a two-page source from a one-page comparison file and checks saved bytes, print-dialog page state and untouched comparison content.
|
|
|
|
Explicit calculation logs ordinary field script errors and continues in calculation order. Each job can evaluate at most 4,096 calculation scripts, including nested calls and document initialization on the first event. Exhaustion remains in effect after a JavaScript catch and prevents PDF writeback.
|