mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 17:01:23 +00:00
* 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>
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.