mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 15:22:22 +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>
pdfcraft-forms
Interactive forms (AcroForm, ISO 32000-2 §12.7). Layer L3; depends on pdfcraft-cos and
pdfcraft-fonts.
API
let all: Vec<Field> = fields(&doc); // terminal fields with widgets, options, flags, /DA
let snapshot = fields_with_adopted(&doc); // fields and page-only adoption count in one pass
let all = snapshot.items;
let adopted = snapshot.adopted_page_fields;
set_value(&mut doc, "name", &FieldValue::Text("Ada".into()))?;
set_value(&mut doc, "agree", &FieldValue::Check(true))?;
set_value(&mut doc, "size", &FieldValue::Radio(Some("L".into())))?;
set_value(&mut doc, "toppings", &FieldValue::Choice(vec!["Ham".into()]))?;
// Many values: as set_value one by one, but reading the form once and recalculating once.
let results: Vec<Result<(), FormError>> = set_values(&mut doc, &[("name".into(), FieldValue::Text("Ada".into()))])?;
reset(&mut doc, None)?; // Clear form (back to /DV)
// Prepare a form
let name = add_field(&mut doc, 0, [72.0, 700.0, 216.0, 722.0], &NewField::Text { multiline: false }, None)?; // "Text1"
set_props(&mut doc, &name, &FieldProps { name: Some("email".into()), required: Some(true), ..Default::default() })?;
// A partial appearance edit keeps each widget's other styles and custom /DA font.
set_props(&mut doc, "email", &FieldProps {
appearance: Some(LookPatch { width: Some(3.0), fill: Some(None), ..Default::default() }),
..Default::default()
})?;
delete_field(&mut doc, "email")?;
// A table of text fields (view space, y down), laid out and named before any of it is added:
// refused whole for a cell under MIN_FIELD_SIDE or a name with a period, repeated or taken.
let names = all.iter().map(|f| f.name.as_str());
let base = table::free_base(names.clone()); // "Table1", or the next one free
let mut grid = table::FieldTable::new([40.0, 240.0, 240.0, 360.0], &base);
grid.set_columns(2, &base);
let cells: Vec<table::Cell> = grid.cells_for(names)?; // "Table1_1", "Table1_c2_1", …
Behaviour
- Inheritable attributes (
/FT,/Ff,/V,/DV,/DA,/Q,/MaxLen) are resolved down the field tree; the form's/DAand/Qare the last fallback. - Text and choice fields get new appearance streams (
/Tx BMC … EMC): auto-size (0 Tf), comb (cells parted in the border colour), multiline wrapping, password masking, quadding,/MKbackground and border, list-box selection. The/DAfont is used when/DRhas it as a simple font or a composite font with a predefined Unicode CMap; otherwise Helvetica (WinAnsi). Thai in a text field or combo box that the font can't hold is drawn from the craft-fonts Thai face with Add text's writer (pdfcraft_fonts::shaped_text), beside the field's font, keeping/DA; a build without a Thai face refuses it. Any other value or button caption the font can't hold (Chinese in a Helvetica field, a character beyond U+FFFF in a UCS-2 CMap) is kept, as Acrobat keeps it:/Vis stored as typed, the appearance draws?for those characters, the widget is marked with the first one (pdfcraft_fonts::UNDRAWN_MARK, cleared once all of it can be drawn) and the form sets/NeedAppearances, so other viewers draw the field from its value with fonts that have them. Not in a document that declares PDF/A, which forbids that flag (declares_pdfa: the bytespdfaid:partin XMP metadata each filter stage of which decodes within 8 MiB, the cap preflight's Standards panel reads it with; preflight reads the identifier by its namespace, so a packet that gives that namespace another prefix declares PDF/A there and not here).undrawn_fieldslists the marked fields; the engine reports each edit that draws one, and flattening refuses to make that?permanent. - Displaying a form that leaves its appearances to the viewer (
/NeedAppearances, or a text or choice widget with no/AP /N):with_field_appearancesreturns a copy with those widgets drawn from their values, as typing them would draw them; the engine shows and prints that copy and saves the file as it was (#3470).with_field_appearances_cachedkeeps each widget's drawing while the objects it was drawn from are the same (FieldAppearanceCache), so typing in one field redraws that field, not the form. Check boxes and radio buttons with no appearance for their state are drawn from/MK(border, background, and when on the/MK /CAZapfDingbats symbol in the/DAcolour), and with/NeedAppearancesso is every one that has an/MK, over the appearance the file carries; push buttons with none are drawn from/MK,/BSand their caption. - Check boxes and radio buttons switch
/Vand each widget's/AS. Widgets with no appearances get PdfCraft's own (a drawn check mark or dot; no symbol font needed). - Imported
/MaxLen 0is treated as unlimited (also clearing an inherited limit), while the original entry is preserved. Positive limits still apply. - Values are validated (options,
MaxLen, single vs multi-select, NoToggleToOff, read-only) and a failed call changes nothing. - Native AF recognition stops at the first genuine matching function call. If its arguments
are unsupported, the complete action remains a custom script for the caller's
Scriptsinterpreter; a later literal call must not replace the original conditional validation. Async for-of loop headers and bodies do not expose AF-looking text inside regex literals. - Moving choice fields preserves their own options rather than copying an ancestor's
/Opt; inherited check-box and radio-button export mappings remain intact. FieldProps::appearanceapplies aLookPatchindependently to each widget.Nonekeeps a property;Some(None)removes a border/fill colour. Unchanged/DA, custom font resources, widget-specific/DAoverrides, indirect/MKand/BScontents, dash arrays and unknown dictionary keys are preserved. It is mutually exclusive with the completelookreplacement. Use the engine'sEdit::Batchto change several fields atomically in one undo step./RV(rich text) is removed when a value is set so it can't contradict/V.- New fields follow Acrobat: names Text1, Check Box1, Group1 (radio groups), Dropdown1, List Box1,
Button1, Date1, Signature1;
/Helvis added to/DR; appearances are drawn at once. Radio buttons joining a group become kid widgets with their own export state. Renaming changes only the last part of a dotted name. - Renaming a field renames it in every Calculate script that reads it
(
af::rename_in_calculation):AFSimple_Calculatefield lists, simplified field notation (theBVCALCcomment and its translation), andgetFieldcalls whose argument is the name as one string literal. The rest of each script is kept as written. Names built at run time, a script stream that isn't UTF-8, other events' scripts and a name another field still has are left alone.calculations_changedcompares two readings of a form to say which formulas an edit rewrote. - The calculation order (
/CO, §12.7.3):calculation_orderlists the calculated fields in the orderrecalculateruns them, each with its field object (two can share a name in a malformed form), with the fields each formula reads (Calculate::reads,af::calculation_reads; names built at run time aren't known) and what is wrong with the order: a field reading one that calculates later, a loop, a field/COleaves out, and/COentries that calculate nothing. Two issues are about the formula itself: it reads a field the form doesn't have (MissingField; the field of that name and a field under it count as there, as recalculation andgetFieldfind them, but not an Acrobat widget name such asprice.0, which neither resolves), or its script builds names at run time (BuiltNames,af::reads_built_names), so its reads are incomplete.CalculationOrder::fixedis Fix order: a stable topological sort that keeps the current order wherever it can and leaves loops as they are.set_calculation_orderwrites/COafresh. A field that gets a calculation joins the end of the order; one whose calculation changes keeps its place. Widget::captionreads a push button's normal label (/MK /CA, including indirect values).FieldProps { caption: Some((widget_index, text)), ..Default::default() }changes or clears that label, independently of the field name. The index is 0-based. A label-only edit redraws just that widget, copying shared/indirect/MKand appearance dictionaries before editing. Other widgets, actions, icons, rotation and unknown entries are preserved. Existing icon-only layouts keep hiding the caption; caption rendering retains the existing Helvetica/WinAnsi limits and reports unavailable characters as above.
Not yet
Rich-text values and exact appearance metrics/layout remain incomplete. JavaScript
execution and FDF/XFDF exchange are integrated through the engine; this crate provides
field actions and native AF rules. /MK /R rotation (0, 90, 180, 270) is drawn into the
appearance.
Button export values
FieldProps::export_value edits a zero-based widget's /Opt value while preserving appearance names, /AP, /AS, /V and /DV. Widgets sharing an on-state remain consistent. Export-only changes do not regenerate appearances. Invalid indices, empty/Off values and ambiguous/conflicting mappings are rejected. Field::export_values resolves selected states for data export.