From 9ed12cb6a8aca3f86b7fefec85cabca1155a8f90 Mon Sep 17 00:00:00 2001 From: regendorf-ux Date: Thu, 15 Oct 2026 02:37:30 -0400 Subject: [PATCH] Forms: simplified field notation written as real JavaScript, not AFMakeNumber(0) (#1995) calculate_js wrote a field's simplified field notation as "/** BVCALC expr EVCALC **/ event.value = AFMakeNumber(0);". PdfCraft reads the comment, but a viewer that runs the script instead (Acrobat among them) computed 0. notation_js now writes Acrobat's own translation after the comment: each field name becomes AFMakeNumber(this.getField("name").value), with \ escapes in names honoured, numbers and operators kept. Co-authored-by: Osvaldo Duran <338149330+regendorf-ux@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- crates/forms/src/af.rs | 45 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/crates/forms/src/af.rs b/crates/forms/src/af.rs index a3eea73236..b50ed4c1ed 100644 --- a/crates/forms/src/af.rs +++ b/crates/forms/src/af.rs @@ -446,7 +446,9 @@ pub fn calculate_js(c: &Calculate) -> Option { js_str(op.code()), fields.iter().map(|f| js_str(f)).collect::>().join(", ") )), - Calculate::Notation(expr) => Some(format!("/** BVCALC {expr} EVCALC **/ event.value = AFMakeNumber(0);")), + // Acrobat's own translation follows the comment, so a viewer that runs the script instead + // of reading the comment computes the same value (a placeholder here showed 0). + Calculate::Notation(expr) => Some(format!("/** BVCALC {expr} EVCALC **/ event.value = {};", notation_js(expr))), } } @@ -864,6 +866,41 @@ pub fn validate(v: &Validate, value: &str) -> Result<(), String> { }) } +/// Simplified field notation as the JavaScript Acrobat writes after its `BVCALC` comment: +/// each field name becomes `AFMakeNumber(this.getField("name").value)`, the rest stays. +pub fn notation_js(expr: &str) -> String { + let mut out = String::new(); + let cs: Vec = expr.chars().collect(); + let mut i = 0; + while let Some(&c) = cs.get(i) { + if c.is_whitespace() || "+-*/()".contains(c) { + out.push(c); + i += 1; + } else if c.is_ascii_digit() || c == '.' { + while let Some(&d) = cs.get(i).filter(|d| d.is_ascii_digit() || **d == '.') { + out.push(d); + i += 1; + } + } else { + let mut name = String::new(); + while let Some(&n) = cs.get(i).filter(|n| !(n.is_whitespace() || "+-*/()".contains(**n))) { + // `\` escapes a space or an operator inside a name, as Acrobat writes them. + if n == '\\' + && let Some(&next) = cs.get(i + 1) + { + name.push(next); + i += 2; + continue; + } + name.push(n); + i += 1; + } + out.push_str(&format!("AFMakeNumber(this.getField({}).value)", js_str(&name))); + } + } + out +} + /// Evaluate simplified field notation: + - * / and parentheses over numbers and field names /// (`\` escapes spaces and operators in names, as Acrobat writes them). fn eval_notation(src: &str, value_of: &dyn Fn(&str) -> f64) -> Option { @@ -1024,6 +1061,12 @@ mod tests { assert_eq!(parse_calculate(&calculate_js(&c).unwrap()), Some(c)); let c = Calculate::Notation("a + b".into()); assert_eq!(parse_calculate(&calculate_js(&c).unwrap()), Some(c)); + // The JavaScript after the comment is Acrobat's translation, so viewers that run it + // instead of reading the comment compute the same value (not 0). + assert_eq!( + calculate_js(&Calculate::Notation("(Unit\\ Price * Qty) / 2".into())).unwrap(), + "/** BVCALC (Unit\\ Price * Qty) / 2 EVCALC **/ event.value = (AFMakeNumber(this.getField(\"Unit Price\").value) * AFMakeNumber(this.getField(\"Qty\").value)) / 2;" + ); } #[test]