From 29f4edf5be22861731c199483cf51a299935e57d Mon Sep 17 00:00:00 2001 From: ysrdevs <32823553+ysrdevs@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:10:22 -0500 Subject: [PATCH] XFA: a script's time starts when its context is ready, and a value reaches every field bound to its data node (#1589) Two follow-ups the maintainer asked for after #1355. The script session's worker now says when the engine is ready for the script it was sent (its JavaScript context built; FormCalc at once), and the script's time limit runs from then, with the engine's own start limit before it, as the one-shot path always did: building a context is not charged to the script, which matters on a busy machine. A field inside an area shares its data node with a field of the same name outside it (areas are left out of data paths). The in-place refresh of the live form now follows the data path through the tree and gives every field bound to the node the value the tree builder would, each by its own template field (an exclusion group there means a rebuild), and the engine's value setter, with an index of widgets by data node, gives every widget bound to the node the value by its own kind, hidden fields included, writing the data whenever the node's text differs. The script's time now also starts after its host and globals are set up. A stale parity note (choosing a picture for an image field, delivered in #1175) is removed. --- crates/engine/src/tests.rs | 62 +++++++++++++++++++ crates/engine/src/xfa.rs | 115 +++++++++++++++++++++-------------- crates/js/src/tests.rs | 20 ++++++ crates/js/src/xfa.rs | 3 +- crates/js/src/xfa_session.rs | 58 +++++++++++++++--- crates/xfa/src/script.rs | 72 +++++++++++++++++----- parity/acrobat-features.toml | 2 +- 7 files changed, 261 insertions(+), 71 deletions(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ff98c2df..672c654f 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3260,6 +3260,68 @@ fn xfa_rows_in_areas_are_laid_out_read_calculated_and_written_where_adobe_binds_ assert_eq!((v2("days_2"), v2("rest")), ("2".into(), "29".into())); } +#[test] +fn a_value_a_script_sets_reaches_every_field_bound_to_the_same_data_node() { + // `carried` inside the area and a second `carried` outside it bind to one data node (areas + // are not data scopes). In document order: `setter` writes the inner one, `echo` reads the + // outer one in the same event, and the saved data holds the one value. + let tpl = pdfcraft_xfa::fixtures::leave_template("
5
") + .replace( + r#" "#, + r#" + "#, + ) + .replace( + " \n \n", + r#" + + + +"#, + ); + assert!(tpl.contains("name=\"echo\"") && tpl.contains("name=\"setter\""), "the fixture was reshaped"); + let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&tpl)); + let mut s = Session::new().with_clock(|| 1_700_000_000); + let id = s.open("shared.pdf", None, bytes, None).expect("opens"); + let out = s.take_js_output(id); + assert!(out.errors.is_empty(), "{out:?}"); + let values = |s: &Session, id: DocId, n: &str| { + s.get(id).unwrap().form.iter().filter(|f| f.name.starts_with(n)).map(|f| f.value.join("")).collect::>() + }; + assert_eq!(values(&s, id, "carried"), vec!["9".to_string(), "9".to_string()], "both fields show the node's value"); + assert_eq!(values(&s, id, "echo"), vec!["9".to_string()], "read in the same event the setter ran in"); + let cos = pdfcraft_cos::Document::open(s.get(id).unwrap().bytes.clone()).unwrap(); + let d = pdfcraft_xfa::data_of(&cos).unwrap(); + assert_eq!(d.text_at(&pdfcraft_xfa::som_to_path("form[0].page[0].carried[0]")), Some("9")); + assert_eq!(d.count(&pdfcraft_xfa::som_to_path("form[0].page[0]"), "carried"), 1, "one data node"); + // The inner field hidden (no widget): the outer widget still takes the node's value, and + // an unrelated edit does not write the old value back over it. + let hidden = Arc::new(pdfcraft_xfa::fixtures::shell( + &tpl.replace(r#""#, + r#"90"#, + ))); + let id = s.open("check.pdf", None, checkbox, None).expect("opens"); + let carried: Vec<(pdfcraft_forms::FieldKind, Vec)> = + s.get(id).unwrap().form.iter().filter(|f| f.name.starts_with("carried")).map(|f| (f.kind, f.value.clone())).collect(); + assert_eq!( + carried, + vec![(pdfcraft_forms::FieldKind::CheckBox, vec!["9".to_string()]), (pdfcraft_forms::FieldKind::Text, vec!["9".to_string()])], + "{carried:?}" + ); +} + #[test] fn dynamic_xfa_forms_run_formcalc_scripts() { let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::formcalc_template())); diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index 15f617cf..0f37933a 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -258,6 +258,8 @@ struct Runner<'a, 'b> { /// Field values to write, by name (the last wins). field_changes: std::collections::BTreeMap>, by_som: Option>, + /// Widget names by the data node they bind to (built with `by_som`). + by_path: Option>>, fields: Option>, /// The live form as last built; dropped when a script changed the form. cached: Option, @@ -289,6 +291,7 @@ impl<'a, 'b> Runner<'a, 'b> { pending: Vec::new(), field_changes: Default::default(), by_som: None, + by_path: None, fields: None, cached: None, session: None, @@ -341,11 +344,26 @@ impl<'a, 'b> Runner<'a, 'b> { fn by_som(&mut self) -> &HashMap { if self.by_som.is_none() { - self.by_som = Some(pdfcraft_xfa::fields_by_som(self.doc)); + let by_som = pdfcraft_xfa::fields_by_som(self.doc); + let mut by_path: HashMap> = HashMap::new(); + for (som, name) in &by_som { + by_path.entry(pdfcraft_xfa::data_path(self.tpl, som)).or_default().push(name.clone()); + } + for names in by_path.values_mut() { + names.sort(); + } + self.by_path = Some(by_path); + self.by_som = Some(by_som); } self.by_som.get_or_insert_with(Default::default) } + /// The widgets bound to the data node at `path`, in name order. + fn widgets_at(&mut self, path: &pdfcraft_xfa::DataPath) -> Vec { + self.by_som(); + self.by_path.as_ref().and_then(|m| m.get(path)).cloned().unwrap_or_default() + } + fn fields(&mut self) -> &mut HashMap { if self.fields.is_none() { self.fields = Some(field_infos(self.doc)); @@ -387,55 +405,63 @@ impl<'a, 'b> Runner<'a, 'b> { self.invalidate(); } - /// Give the object at `som` the value `value` (field `name` when it has a widget). Returns - /// whether it changed. + /// Give the object at `som` the value `value`: in the data, and in every widget bound to + /// that data node (each valued by its own kind: a check box by its on state, a list box by + /// lines). Returns whether anything changed. fn set_value(&mut self, som: &str, value: &str) -> bool { - let path = pdfcraft_xfa::data_path(self.tpl, som); - let name = self.by_som().get(som).cloned(); - let Some(name) = name else { - // No widget (a hidden field, a draw, a row not laid out yet): the data alone. - if self.data.as_ref().and_then(|d| d.text_at(&path)) == Some(value) { - return false; - } - self.value_op(som, path, value.to_string()); - self.changes.values += 1; - return true; - }; - let Some(info) = self.fields().get(&name).cloned() else { return false }; use pdfcraft_forms::FieldKind as K; - let new_value: Vec = match info.kind { - K::CheckBox => { - if pdfcraft_xfa::data::is_on(value, &info.on_state) || value == info.on_state { - vec![info.on_state.clone()] - } else { - Vec::new() + let path = pdfcraft_xfa::data_path(self.tpl, som); + let names = self.widgets_at(&path); + // The data text: the widget at `som` says how its kind writes the value; without one, + // the first widget bound to the node, else the value itself. + let primary = self.by_som().get(som).cloned().or_else(|| names.first().cloned()); + let mut text = value.to_string(); + let mut changed = false; + for name in &names { + let Some(info) = self.fields().get(name).cloned() else { continue }; + let new_value: Vec = match info.kind { + K::CheckBox => { + if pdfcraft_xfa::data::is_on(value, &info.on_state) || value == info.on_state { + vec![info.on_state.clone()] + } else { + Vec::new() + } } + K::Radio => { + let v = value.trim(); + if v.is_empty() { Vec::new() } else { vec![v.to_string()] } + } + // Read-only fields still take values from scripts (calculated totals are + // read-only); buttons and signatures hold none. + K::PushButton | K::Signature => continue, + K::List if info.multi => value.lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_string).collect(), + _ => vec![value.to_string()], + }; + let same = match info.kind { + K::CheckBox => info.value.is_empty() == new_value.is_empty(), + K::Radio => info.value.first() == new_value.first(), + K::List if info.multi => info.value == new_value, + _ => info.value.first().map_or(value.is_empty(), |v| v == value), + }; + let updated = FieldInfo { value: new_value.clone(), ..info }; + if primary.as_deref() == Some(name.as_str()) { + text = data_text(&updated); } - K::Radio => { - let v = value.trim(); - if v.is_empty() { Vec::new() } else { vec![v.to_string()] } + if same { + continue; } - // Read-only fields still take values from scripts (calculated totals are read-only). - K::PushButton | K::Signature => return false, - K::List if info.multi => value.lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_string).collect(), - _ => vec![value.to_string()], - }; - let same = match info.kind { - K::CheckBox => info.value.is_empty() == new_value.is_empty(), - K::Radio => info.value.first() == new_value.first(), - K::List if info.multi => info.value == new_value, - _ => info.value.first().map_or(value.is_empty(), |v| v == value), - }; - if same { - return false; + self.fields().insert(name.clone(), updated); + self.field_changes.insert(name.clone(), new_value); + changed = true; } - let updated = FieldInfo { value: new_value.clone(), ..info }; - let text = data_text(&updated); - self.fields().insert(name.clone(), updated); - self.field_changes.insert(name, new_value); - self.value_op(som, path, text); - self.changes.values += 1; - true + if self.data.as_ref().and_then(|d| d.text_at(&path)).unwrap_or("") != text { + self.value_op(som, path, text); + changed = true; + } + if changed { + self.changes.values += 1; + } + changed } /// Write what the scripts changed so far: the data as one rewrite, the field values, the @@ -489,6 +515,7 @@ impl<'a, 'b> Runner<'a, 'b> { crate::xfa_redraw(self.doc)?; self.console(crate::undrawn_field_warnings(self.doc)); self.by_som = None; + self.by_path = None; self.fields = None; self.invalidate(); Ok(()) diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index 1d3f5c29..c8ee8121 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -1037,6 +1037,26 @@ mod session { assert!(started.elapsed() < std::time::Duration::from_secs(10), "{:?}", started.elapsed()); } + #[test] + fn a_scripts_time_starts_when_its_context_is_ready() { + // The engine made slow to be ready (300 ms) under a 50 ms script limit: the script is + // not abandoned, its time starting only when the engine says it is ready. + use crate::xfa_session::START_DELAY_MS; + use std::sync::atomic::Ordering::Relaxed; + let mut s = XfaSession::new(XfaDoc::default()); + START_DELAY_MS.store(300, Relaxed); + let mut quick = script("1", false, "form1[0].page1[0].go[0]", "click"); + quick.timeout = std::time::Duration::from_millis(50); + let js = s.run(quick); + let mut quick = script("1", true, "form1[0].page1[0].go[0]", "click"); + quick.timeout = std::time::Duration::from_millis(50); + let fc = s.run(quick); + START_DELAY_MS.store(0, Relaxed); + assert!(!js.abandoned && js.result.as_deref() == Some("1"), "{js:?}"); + assert!(!fc.abandoned && fc.result.as_deref() == Some("1"), "{fc:?}"); + assert_eq!(s.dead(), None); + } + #[test] fn a_script_past_its_time_is_abandoned_and_the_session_is_dead_after_it() { let mut s = XfaSession::new(XfaDoc::default()); diff --git a/crates/js/src/xfa.rs b/crates/js/src/xfa.rs index ac200eb8..a2361da7 100644 --- a/crates/js/src/xfa.rs +++ b/crates/js/src/xfa.rs @@ -1458,7 +1458,6 @@ pub(crate) fn warm_up() { pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNode, limits: Limits, started: &dyn Fn()) -> XfaOutcome { warm_up(); let mut ctx = Context::default(); - started(); ctx.runtime_limits_mut().set_loop_iteration_limit(limits.loop_iterations); ctx.runtime_limits_mut().set_recursion_limit(limits.recursion); let host_state = XHost::new(root, &event.target, doc); @@ -1467,6 +1466,8 @@ pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNo let mut out = XfaOutcome::default(); let result = (|| -> JsResult<()> { install(&mut ctx, event, doc, current)?; + // Ready for the script: its time starts now, the set-up above not charged to it. + started(); ctx.register_global_property(js_string!("__xfa_src"), s(script), Attribute::CONFIGURABLE)?; // Direct eval inside a function called on the node: `this` is the node and the // completion value is the script's last expression (what a calculate script yields). diff --git a/crates/js/src/xfa_session.rs b/crates/js/src/xfa_session.rs index e85fc1af..c2c5ee3c 100644 --- a/crates/js/src/xfa_session.rs +++ b/crates/js/src/xfa_session.rs @@ -13,6 +13,10 @@ use crate::Limits; use crate::xfa::{XfaDoc, XfaEvent, XfaNode, XfaOutcome}; +/// Tests: milliseconds the engine is made to take before it is ready for a script. +#[cfg(test)] +pub(crate) static START_DELAY_MS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + /// What one script run needs. #[derive(Clone, Debug)] pub struct Script { @@ -25,15 +29,29 @@ pub struct Script { pub timeout: std::time::Duration, } -/// Run `s` here: a fresh context (JavaScript) or interpreter (FormCalc) on its copy of the form. -fn run_script(s: Script, doc: &XfaDoc) -> XfaOutcome { +/// Run `s` here: a fresh context (JavaScript) or interpreter (FormCalc) on its copy of the +/// form. `started` is called once the engine is ready for the script (its context built), so +/// a time limit on the script runs from then, not from when it was asked for. +fn run_script(s: Script, doc: &XfaDoc, started: &dyn Fn()) -> XfaOutcome { + // Tests make the engine slow to be ready, to see that the time isn't the script's. + #[cfg(test)] + let started = &|| { + let ms = START_DELAY_MS.load(std::sync::atomic::Ordering::Relaxed); + if ms > 0 { + std::thread::sleep(std::time::Duration::from_millis(ms)); + } + started(); + }; if s.formcalc { + // FormCalc has no engine to start: its time (and its own deadline) begins at once. + started(); return crate::formcalc::run_formcalc_at(&s.source, &s.event, doc, s.root, s.limits, Some(s.timeout)); } if let Some(why) = crate::refuse(&s.source) { + started(); return XfaOutcome { error: Some(why), ..Default::default() }; } - crate::xfa::run_here(&s.source, &s.event, doc, s.root, s.limits, &|| {}) + crate::xfa::run_here(&s.source, &s.event, doc, s.root, s.limits, started) } #[cfg(not(target_arch = "wasm32"))] @@ -45,7 +63,10 @@ mod native { use crate::xfa::{ENGINE_START_LIMIT, XfaDoc, XfaOutcome}; enum Reply { + /// The thread is up. Ready, + /// The engine is ready for the script just sent: its time starts now. + Started, Outcome(Box), } @@ -85,7 +106,10 @@ mod native { // The caller is gone when a send fails: nothing to report then. let _ = reply.send(Reply::Ready); while let Ok(s) = inbox.recv() { - let o = run_script(*s, &doc); + let started = reply.clone(); + let o = run_script(*s, &doc, &move || { + let _ = started.send(Reply::Started); + }); let _ = reply.send(Reply::Outcome(Box::new(o))); } }); @@ -103,8 +127,9 @@ mod native { self.worker.as_mut().ok_or_else(|| "the script engine is not running".into()) } - /// Run one script, waiting at most its `timeout`; past that it is abandoned on its - /// thread and the session is dead. + /// Run one script, waiting at most its `timeout` from when the engine is ready for it + /// (the engine itself gets [`ENGINE_START_LIMIT`] to be ready); past that it is + /// abandoned on its thread and the session is dead. pub fn run(&mut self, script: Script) -> XfaOutcome { let abandoned = |why: String| XfaOutcome { error: Some(why), abandoned: true, ..Default::default() }; let failed = |why: String| XfaOutcome { error: Some(why), ..Default::default() }; @@ -121,15 +146,28 @@ mod native { self.worker = None; return failed("the script engine stopped with an internal error".into()); } - let deadline = Instant::now().checked_add(timeout); + // The engine's start, then the script's own time. + let (mut running, mut limit) = (false, ENGINE_START_LIMIT); + let mut deadline = Instant::now().checked_add(limit); loop { - let left = deadline.map_or(timeout, |d| d.saturating_duration_since(Instant::now())); + let left = deadline.map_or(limit, |d| d.saturating_duration_since(Instant::now())); match worker.from.recv_timeout(left) { Ok(Reply::Outcome(o)) => return *o, + Ok(Reply::Started) => { + // A second call changes nothing: the script's time has already begun. + if !running { + (running, limit) = (true, timeout); + deadline = Instant::now().checked_add(limit); + } + } // Not expected here: wait on for the outcome. Ok(Reply::Ready) => continue, Err(RecvTimeoutError::Timeout) => { - let why = format!("the script ran longer than {:.1} s and was abandoned", timeout.as_secs_f64()); + let why = if running { + format!("the script ran longer than {:.1} s and was abandoned", timeout.as_secs_f64()) + } else { + format!("the script engine took longer than {:.0} s to start", ENGINE_START_LIMIT.as_secs_f64()) + }; self.dead = Some(why.clone()); // Dropping the sender ends the thread's loop once the script is done. self.worker = None; @@ -165,7 +203,7 @@ mod native { } pub fn run(&mut self, script: Script) -> XfaOutcome { - run_script(script, &self.doc) + run_script(script, &self.doc, &|| {}) } } } diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 1a282ba3..ddc52af8 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -632,28 +632,70 @@ fn named_in<'a>(nodes: &'a [Node], name: &str, depth: usize) -> Option<&'a Node> }) } -/// Give the field at `som` in `live` the value [`form_tree`] would give it from `data` now, -/// so a value change needs no rebuild of the tree. `None`: not a plain field the tree can -/// refresh alone (a radio button, an object the template doesn't have): rebuild the tree. +/// Give the field at `som` in `live`, and every other field bound to the same data node (a +/// field inside an area shares its node with one outside, since areas are left out of data +/// paths), the value [`form_tree`] would give it from `data` now, so a value change needs no +/// rebuild of the tree. The walk follows the data path only (through areas and unnamed +/// containers, which have no data segment), so it costs the depth, not the form. `None`: a +/// node bound there is not a plain field the tree can refresh alone (a radio button, an +/// exclusion group, an object the template doesn't have): rebuild the tree. pub fn refresh_value(live: &mut FormNode, tpl: &Template, data: Option<&DataNode>, som: &str) -> Option { - let f = template_field(tpl, som)?; - let value = Walker::plain_field_value(tpl, data, f, som); - fn set(n: &mut FormNode, som: &str, value: &str, depth: usize) -> Option { + template_field(tpl, som)?; + let path = data_path(tpl, som); + if path.is_empty() { + return None; + } + fn walk( + n: &mut FormNode, + tpl: &Template, + data: Option<&DataNode>, + path: &[(String, usize)], + pos: usize, + changed: &mut bool, + depth: usize, + ) -> Option<()> { if depth > MAX_DEPTH { - return None; + return Some(()); } - if n.som == som { - if n.kind != Some(NodeKind::Field) { + for c in n.children.iter_mut() { + // No data segment of their own: looked through. + let transparent = matches!(c.kind, Some(NodeKind::Area)) || (matches!(c.kind, Some(NodeKind::Subform)) && c.name.starts_with('#')); + if transparent { + walk(c, tpl, data, path, pos, changed, depth + 1)?; + continue; + } + let Some((name, index)) = path.get(pos) else { continue }; + if c.name != *name || c.index != *index { + continue; + } + if pos + 1 < path.len() { + walk(c, tpl, data, path, pos + 1, changed, depth + 1)?; + continue; + } + // Bound to the node. + if c.kind != Some(NodeKind::Field) { return None; } - let changed = n.value != value; - n.value = value.to_string(); - return Some(changed); + let f = template_field(tpl, &c.som)?; + let value = Walker::plain_field_value(tpl, data, f, &c.som); + if c.value != value { + c.value = value; + *changed = true; + } } - n.children.iter_mut().find_map(|c| set(c, som, value, depth + 1)) + Some(()) } - // Not in the live tree (hidden, or past the node budget): nothing to refresh. - Some(set(live, som, &value, 0).unwrap_or(false)) + let mut changed = false; + // The root subform is the path's first segment. + let (first, rest) = path.split_first()?; + if live.name != first.0 || live.index != first.1 { + return Some(false); + } + if rest.is_empty() { + return None; + } + walk(live, tpl, data, &path, 1, &mut changed, 0)?; + Some(changed) } /// The data tree of a document's datasets packet. diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index f874d52f..08e4a47c 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5197,7 +5197,7 @@ status = "partial" commands = ["file.open"] tools = ["doc_open", "doc_info", "form_fields", "form_fill", "js_run"] evidence = ["crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/ui_group/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/automation/tests/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/ui_group/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/ui-egui/tests/ui_group/xfa.rs::a_form_whose_scripts_were_turned_off_at_open_says_so", "crates/ui-egui/tests/ui_group/xfa.rs::scripts_turned_off_by_a_click_bring_a_closed_notice_bar_back", "crates/automation/tests/automation.rs::doc_info_says_when_a_runaway_script_turned_the_forms_scripts_off", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite", "crates/xfa/src/tests.rs::choice_lists_signatures_passwords_and_pictures_become_fields_and_images", "crates/xfa/src/tests.rs::choice_values_come_from_the_data_as_saved_values_and_several_for_lists", "crates/xfa/src/tests.rs::pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated", "crates/engine/src/tests.rs::xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save", "crates/xfa/src/tests.rs::repeating_subforms_inside_positioned_content_get_their_instances", "crates/engine/src/tests.rs::xfa_rows_in_areas_are_laid_out_read_calculated_and_written_where_adobe_binds_them"] -notes = "laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters, breaks, repeated header rows, captions, rich text, JPEG/PNG/GIF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes, radio groups, drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields, image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; doc_info reports xfa_layout. XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. All the scripts of one event run on one thread, each in a fresh context on the form as the engine holds it then (nothing shared between scripts; the live form kept up to date in place for values, rebuilt only when the structure changed), so hundreds of calculations run within the open budget. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off, which the app says in a toast and on the notice bar, with a button to the JavaScript console, and doc_info reports as xfa_scripts_off) and what open-time scripts changed is noted. Missing: loops inside nested calls are only stopped by abandoning their thread, and script memory is not capped; the change event per keystroke; xfa.template, connections and data descriptions; a hidden instance of a repeating subform hides them all; choosing a new picture for an image field; barcodes are drawn as their text, not bars; keep rules; metrics are approximate and Acrobat fidelity is eyeballed on real forms, not measured (#60)" +notes = "laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters, breaks, repeated header rows, captions, rich text, JPEG/PNG/GIF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes, radio groups, drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields, image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; doc_info reports xfa_layout. XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. All the scripts of one event run on one thread, each in a fresh context on the form as the engine holds it then (nothing shared between scripts; the live form kept up to date in place for values, rebuilt only when the structure changed), so hundreds of calculations run within the open budget. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off, which the app says in a toast and on the notice bar, with a button to the JavaScript console, and doc_info reports as xfa_scripts_off) and what open-time scripts changed is noted. Missing: loops inside nested calls are only stopped by abandoning their thread, and script memory is not capped; the change event per keystroke; xfa.template, connections and data descriptions; a hidden instance of a repeating subform hides them all; barcodes are drawn as their text, not bars; keep rules; metrics are approximate and Acrobat fidelity is eyeballed on real forms, not measured (#60)" [[feature]] id = "form.xfa-formcalc"