* 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>
pdfcraft-engine
The façade every frontend talks to. It holds the open documents, their edit history and the tool catalogue; frontends never reach past it into the parsing, rendering or editing crates.
word_count::count provides cancellable, page-at-a-time Unicode word and character statistics
with optional semantic exclusions; see word-count behavior and limits.
- Layer: L6. It depends on most of L1–L4 and on nothing above itself. No UI toolkit —
egui,winit,eframeandrfdare forbidden below L7, andxtask layersenforces it. - The crate denies
unwrap/expect/panic!/todo!. Entry points that run untrusted-document code keep a last-resortcatch_unwindguard so an escaped panic becomes an error and the user keeps their document.
Two frontends exist on top of it today: pdfcraft-ui-egui (desktop and web) and
pdfcraft-automation (headless tools, the CLI and MCP). Anything one can do, the other must be
able to do — see AGENTS.md §3.
When dynamic XFA layout is active, JsRunner does not initialize Acrobat JavaScript with
scripts from the PDF container's /Names /JavaScript tree. Those scripts belong to the
fallback PDF view and can otherwise show a misleading viewer-compatibility warning.
They remain in the document and in Document::document_scripts. Requested console scripts,
field actions and XFA events can still show messages; ordinary PDFs, static XFA and shells
without an active layout keep their document scripts. Converting XFA to a static form and
undoing that conversion switches the context with the document state.
The editing model
Each open document holds a pdfcraft_cos::Document: the object graph plus a copy-on-write overlay.
- An edit runs on a clone. On success the previous state is pushed onto the undo stack. Clones share all unchanged data, so a snapshot is cheap — this is what makes 100 levels of undo affordable on a 200 MB file.
- After every edit the working file is produced by an incremental write: the original bytes plus one appended revision.
- The view is refreshed from those bytes, so what you see is exactly what Save will write.
- Saving rebases onto the written bytes, so the next save appends only new edits.
Consequences worth preserving: the original bytes of an opened file are never rewritten in place; a full rewrite happens only where it must (redaction, PDF/A conversion, print imposition, a changed password); and unknown data survives a round trip because nothing is reconstructed that wasn't understood.
API
let mut s = Session::new();
let id = s.open("report.pdf", Some(path), bytes, None)?; // -> DocId
s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 })?;
let label = s.undo(id)?; // "Rotate pages"
s.redo(id)?;
let doc = s.get(id).ok_or("gone")?; // &Document: queries, never edits
doc.can_undo(); doc.editable(); doc.allows_printing();
doc.text_blocks(page); doc.page_boxes(page); doc.security_summary();
let bytes = s.save_bytes(id)?; // incremental: keeps signatures over earlier revisions valid
let bytes = s.save_full_bytes(id)?; // full rewrite, garbage-collected
s.mark_saved(id, bytes.clone(), path)?;
Edit is one enum of 75 variants — every undoable change in the product, from
RotatePages to AddAnnotation, SetFieldValue, ApplyRedactions and AddWatermark. Adding a
feature means adding a variant and handling it in apply, which is what keeps undo, the dirty
flag, autosave and the automation tools consistent for free.
Session also owns the operations that produce a new document rather than editing one:
create_blank, create_from_images, create_from_text, combine, extract, split,
split_by_size, reduced_bytes, optimized_bytes, print_pdf, export_data, and the signing
family (sign, sign_with_timestamp, timestamp_document, embed_ltv).
Modules
| Module | What it holds |
|---|---|
commands |
The command registry: id, label, menu, shortcut, and a Needs precondition encoding document security. Menus, the ⌘K palette, shortcuts and the control channel all read it, so every surface agrees on what exists and when it is enabled |
catalog |
Acrobat's "All tools" information architecture as data — groups, sections, icons, hues, and an Availability of Ready, Planned(milestone) or Provider. The UI renders what is not built yet, honestly |
actions |
PDF action dispatch (go-to, URI, named, set-layer-state, JavaScript) |
links |
Link creation, properties, and finding URLs in page text |
open_parameters |
A path's PDF open parameters (manual.pdf#nameddest=Intro, #page=12): the file to open and where to show it. The app and doc_open both read them |
js |
The bridge to pdfcraft-js. forms never depends on js directly; the engine injects it, so a build without JavaScript is possible |
xfa, ocr, compare, export |
Façades over the L3/L4 crates of the same names |
AcroForm Keystroke, Validate, Calculate and Format scripts run automatically. Their Print, ShowPanel and SaveAs requests are ignored with a JavaScript console diagnostic; field values, alerts and other requests are retained. Explicit button and console scripts can still request printing (#4088).
Not done here
- Rendering is still borrowed.
pdfcraft-renderwrapshayroand inspects throughlopdf(ADR-0004). The engine only seesinspect() -> DocInfo,RenderPoolandtext::PageText, and that API stays frozen while M2 replaces what is behind it. - Performance budgets (
misc.performance-budgets) are not set or enforced. - Some shipped features are still reachable only from the UI, not through an
automationtool;cargo xtask paritylists them; closing that gap is tracked in the execution plan (D4).
doc.text_edit_preview(page, block) prepares a read-only page background with the selected
paragraph’s glyphs removed, preserving text positioning, images and colours. It shares the
working document’s render configuration and never enters the undo history.