mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
* M6: calculations read their notation only from a comment, and a regex after a condition as a regex Two findings from CodeRabbit's review of #3550, both reproduced first: - parse_calculate and calculation_sites found BVCALC and EVCALC anywhere in the script, so `var s = "BVCALC a EVCALC";` read as the notation `a`: renaming `a` rewrote the string, and the form calculated `a` instead of running the script. The lexer now keeps the text of each comment, and the notation is read from the first comment that holds BVCALC and the EVCALC after it (notation_range), by both the Calculate tab and a rename. A line comment counts, and a comment that never ends runs to the end of the script, as before. - A `/` after any `)` divided, so after the condition of `if`, `while`, `for` or `with` a regular expression was read as code: renaming `a` rewrote `if (flag) /getField("a")/.test(x)`, and in `if (flag) /"/.test(x)` the quote started a string that hid the script's real getField calls. The lexer now notes which `(` opened such a condition (not a method of that name, `o.if(x)`), and a `/` after its `)` starts a regular expression. af::tests::notation_is_read_only_from_a_comment, af::tests::a_regex_after_a_condition_is_not_read_as_code and tests::a_script_that_spells_the_markers_in_a_string_runs_as_a_script fail without the fix. Parity, ROADMAP.md and docs/roadmap.md updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z * M6: form.props-calculate notes that a regex right after a brace is still read as code From CodeRabbit's review of this PR. The lexer still divides after `}`, which ends a block as often as an object literal, so in `if (x) { } /"/.test(s); event.value = getField("a").value;` the quote starts a string and the getField is hidden from a rename and from the calculation order. Checked with a scratch test before writing it down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z * M6: a line comment ends at any line terminator, so code after `\r` is still read From CodeRabbit's review of this PR. A `//` comment ran to the next `\n`, so with `\r` line endings it hid the rest of the script: a rename missed the getField calls after it (since #3550), and a string after it that spells BVCALC ... EVCALC read as the notation. A `/` taken for the start of a regular expression likewise looked past `\r` for its end, and could swallow the next line's code. Both now stop at any of JavaScript's line terminators (`\n`, `\r`, U+2028, U+2029; line_ends_at), and U+2028 and U+2029 between tokens are skipped rather than read as the start of a word. af::tests::a_line_comment_ends_at_any_line_terminator fails without each of the three changes: with `\r`, the string read as notation `a`; with the comment fixed but not the regex scan, `i++ / 2;\r` swallowed the getField after it; without skipping U+2028, the getField after it was not a call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z * M6: a word, number or regex's flags end at U+2028 and U+2029 From CodeRabbit's review of this PR. The scans for an identifier, a number and a regular expression's flags took every byte past ASCII as part of a word, U+2028 and U+2029 included, so in `x\u{2028}getField("a")` the getField was part of one word and not a call: a rename missed it, and the calculation order didn't read it. `word` now takes a position and is false where line_ends_at is true, so the three scans share one rule. The arm that skips U+2028 and U+2029 between tokens stays: without it, the punctuation arm would step one byte into the character. af::tests::a_token_ends_at_a_line_separator fails without the fix, for a word, a number and a regex's flags alike; it and a_line_comment_ends_at_any_line_terminator fail without the skipping arm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z * M6: the `while` that ends a `do` loop counts as a condition, said and tested A `/` after the `)` of a do-while starts the next statement, so it starts a regular expression there too. The comment on BEFORE_CONDITION says so, and a_regex_after_a_condition_is_not_read_as_code covers it (from review). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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()]))?;
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, 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). A 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).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 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. 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, 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.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.