diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f125c41da..41d1c8dcf 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -4469,6 +4469,20 @@ fn xfa_scripts_of_one_event_see_the_form_as_the_engine_holds_it() { assert_eq!(value(&s, "summary"), vec!["Country: CA".to_string()]); } +/// A row of dates worked out the way leave forms do it: the calculation checks that the raw value +/// is a whole ISO date, then adds days to the date as `formattedValue` shows it (`YYYYMMDD`). +#[test] +fn xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it() { + let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::date_row_template())); + let mut s = Session::new().with_clock(|| 1_700_000_000); + let id = s.open("row.pdf", None, bytes, None).expect("opens"); + let value = |s: &Session, n: &str| s.get(id).unwrap().form.iter().find(|f| f.name == n).map(|f| f.value.clone()).unwrap_or_default(); + s.apply(id, Edit::SetFieldValue { name: "from".into(), value: FieldValue::Text("20261020".into()) }).unwrap(); + s.apply(id, Edit::SetFieldValue { name: "days".into(), value: FieldValue::Text("3".into()) }).unwrap(); + let out = s.take_js_output(id); + assert_eq!(value(&s, "to"), ["20261022"], "{:?}", out.errors); +} + #[test] fn xfa_scripts_initialize_calculate_validate_toggle_and_add_rows() { let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template())); diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index 0f37933af..43e00a578 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -149,6 +149,7 @@ fn to_js(n: &FormNode) -> XfaNode { occur_max: n.occur_max, index: n.index, children: n.children.iter().map(to_js).collect(), + formatted: n.display_picture.as_deref().and_then(|p| pdfcraft_xfa::formatted_value(p, &n.value)), } } diff --git a/crates/js/src/formcalc.rs b/crates/js/src/formcalc.rs index dc32d457b..729cb1a88 100644 --- a/crates/js/src/formcalc.rs +++ b/crates/js/src/formcalc.rs @@ -1100,7 +1100,11 @@ impl Interp<'_> { fn node_member(&mut self, i: usize, name: &str) -> R { let Some(n) = self.node(i) else { return Ok(Val::Null) }; Ok(match name { - "rawValue" | "value" | "formattedValue" => self.node_value(i), + "formattedValue" => match &n.formatted { + Some(f) => Val::Str(f.clone()), + None => self.node_value(i), + }, + "rawValue" | "value" => self.node_value(i), "name" => Val::Str(n.name.clone()), "somExpression" => Val::Str(n.som.clone()), "className" => Val::Str(n.kind.class_name().into()), diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index 61d3a71e5..b04f3c06c 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -395,6 +395,38 @@ mod xfa_model { assert_eq!(o.effects, vec![XfaEffect::SetValue { som: "form1[0].page1[0].total[0]".into(), value: "5".into() }]); } + /// A form whose one field `d` holds the ISO date 2026-10-20, shown as 20261020. + pub(super) fn dated_form() -> XfaNode { + let d = XfaNode { formatted: Some("20261020".into()), ..field("d", "form1[0].page1[0].d[0]", "2026-10-20", false) }; + XfaNode { + name: "form1".into(), + som: "form1[0]".into(), + kind: Some(XfaKind::Subform), + children: vec![XfaNode { + name: "page1".into(), + som: "form1[0].page1[0]".into(), + kind: Some(XfaKind::Subform), + children: vec![d], + ..Default::default() + }], + ..Default::default() + } + } + + /// `formattedValue` reads the value as the field's picture shows it, and `rawValue` the + /// value itself; once the script sets the value, `formattedValue` gives that value. + #[test] + fn formatted_value_reads_the_shown_value_until_the_script_sets_one() { + let ev = XfaEvent { activity: "calculate".into(), target: "form1[0].page1[0].d[0]".into(), ..Default::default() }; + let doc = XfaDoc::default(); + let run = |script: &str| run_xfa(script, &ev, &doc, &dated_form(), Limits::default()); + let o = run("this.formattedValue + ' ' + this.rawValue"); + assert_eq!(o.error, None, "{o:?}"); + assert_eq!(o.result.as_deref(), Some("20261020 2026-10-20")); + let o = run("this.rawValue = '2026-10-21'; this.formattedValue"); + assert_eq!(o.result.as_deref(), Some("2026-10-21")); + } + #[test] fn long_values_and_results_are_cut_where_they_are_stored() { // The value kept for later reads is the one the effect saves. @@ -607,6 +639,21 @@ mod formcalc { o.result.unwrap_or_default() } + /// FormCalc reads `formattedValue` as the field's picture shows it too, until the script + /// sets the value. + #[test] + fn formcalc_formatted_value_reads_the_shown_value_until_the_script_sets_one() { + let ev = XfaEvent { activity: "calculate".into(), target: "form1[0].page1[0].d[0]".into(), ..Default::default() }; + let doc = XfaDoc::default(); + let run = |script: &str| run_formcalc(script, &ev, &doc, &super::xfa_model::dated_form(), Limits::default()); + let o = run("Concat($.formattedValue, \" \", $.rawValue)"); + assert_eq!(o.error, None, "{o:?}"); + assert_eq!(o.result.as_deref(), Some("20261020 2026-10-20")); + let o = run("$.rawValue = \"2026-10-21\"\n$.formattedValue"); + assert_eq!(o.error, None, "{o:?}"); + assert_eq!(o.result.as_deref(), Some("2026-10-21")); + } + #[test] fn arithmetic_strings_and_comparisons_follow_formcalc_rules() { assert_eq!(calc("1 + 2 * 3"), "7"); diff --git a/crates/js/src/xfa.rs b/crates/js/src/xfa.rs index a2361da7e..a1be639ca 100644 --- a/crates/js/src/xfa.rs +++ b/crates/js/src/xfa.rs @@ -64,6 +64,9 @@ pub struct XfaNode { /// This instance's index among its same-named siblings. pub index: usize, pub children: Vec, + /// Fields: the value shown in the field's display picture, what `formattedValue` reads; + /// `None` when the picture doesn't change it (the raw value is read then). + pub formatted: Option, } /// The event a script runs for. @@ -205,6 +208,8 @@ pub(crate) struct HNode { pub(crate) children: Vec, /// Removed by `removeInstance`: skipped everywhere. pub(crate) gone: bool, + /// See [`XfaNode::formatted`]; dropped when a script sets the value, which it no longer shows. + pub(crate) formatted: Option, } /// The form as a script sees and changes it, with the effects it records. @@ -346,6 +351,7 @@ impl XHost { // the snapshot stays bounded however often a long value is written. let (text, cut) = clip_value(text); n.value = text.clone(); + n.formatted = None; let som = n.som.clone(); if cut { self.clipped_values += 1; @@ -381,6 +387,7 @@ fn flatten(n: &XfaNode, parent: Option, out: &mut Vec, depth: usiz parent, children: Vec::new(), gone: false, + formatted: n.formatted.clone(), }); let mut kids = Vec::new(); for c in &n.children { @@ -618,6 +625,8 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option, depth: usize) parent, children: Vec::new(), gone: false, + // The copy holds no value, so nothing is shown in its picture either. + formatted: None, }, n.children.clone(), ) @@ -853,7 +862,7 @@ fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult JsValue::from(idx as f64), "rawValue" | "value" => host(ctx)?.borrow().nodes.get(idx).map_or(JsValue::null(), value_js), - "formattedValue" => s(&value), + "formattedValue" => s(&host(ctx)?.borrow().nodes.get(idx).and_then(|n| n.formatted.clone()).unwrap_or(value)), "name" => s(&name), "somExpression" => s(&som), "className" => s(kind.class_name()), diff --git a/crates/xfa/src/fixtures.rs b/crates/xfa/src/fixtures.rs index 54bb9bc21..4c8f29df5 100644 --- a/crates/xfa/src/fixtures.rs +++ b/crates/xfa/src/fixtures.rs @@ -352,6 +352,47 @@ pub fn many_calculations_template(sections: usize, rows: usize) -> String { ) } +/// Shaped like a leave form's row of dates: `from`, a date field typed and shown as `YYYYMMDD` +/// (its data keeps ISO dates), `days`, an invisible `minus1` one less than `days`, and `to`, +/// which a calculation works out as `from` plus `minus1` days. As such forms do, it checks that +/// `from`'s raw value is a whole ISO date, then does its date arithmetic on `formattedValue`. +pub fn date_row_template() -> String { + r##" + + + +"## + .to_string() +} + /// A PDF shell around `xdp`: one placeholder page, `/NeedsRendering true`, no fields. pub fn shell(xdp: &str) -> Vec { let objs: Vec> = vec![ diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs index 62d7beedf..75ffae923 100644 --- a/crates/xfa/src/layout.rs +++ b/crates/xfa/src/layout.rs @@ -405,6 +405,20 @@ fn date_pattern(picture: Option<&str>) -> String { inner.replace('E', "d").to_ascii_lowercase() } +/// What scripts read as a field's `formattedValue` when its display picture is `picture`: the +/// ISO date `raw` shown in a date picture (`date{YYYYMMDD}` turns `2026-10-20` into +/// `20261020`; of several alternatives, the first). `None` for other kinds of picture, for a +/// clause this can't read and for a value that isn't an ISO date: the raw value stands then. +pub fn formatted_value(picture: &str, raw: &str) -> Option { + let first = picture.split('|').next()?.trim(); + let rest = first.strip_prefix("date")?; + if !rest.starts_with(['{', '(']) { + return None; + } + let shown = iso_to_pattern(raw, &date_pattern(Some(first))); + (shown != raw).then_some(shown) +} + fn button_action(scripts: &[Script]) -> Option { let s = scripts.iter().find(|s| s.activity == "click")?; let t = &s.text; diff --git a/crates/xfa/src/lib.rs b/crates/xfa/src/lib.rs index bf4414970..4b42d3961 100644 --- a/crates/xfa/src/lib.rs +++ b/crates/xfa/src/lib.rs @@ -95,7 +95,7 @@ pub use data::{ pattern_to_iso, read_values, read_values_with, remove_data_instance, som_to_path, write_data_ops, write_data_value, write_datasets, write_datasets_reusing, write_datasets_with, }; -pub use layout::{Action, BorderShape, Form, Item, MAX_PAGES, Page, Widget, WidgetKind, layout}; +pub use layout::{Action, BorderShape, Form, Item, MAX_PAGES, Page, Widget, WidgetKind, formatted_value, layout}; pub use packets::{Encoding, Packets, decode as decode_packet, encode as encode_packet, read_packets}; pub use parse::{measure, parse}; pub use pdf::{CLICK_KEY, LAYOUT_KEY, SOM_KEY, Stripped, XfaShape, existing_layout, strip, xfa_shape}; diff --git a/crates/xfa/src/model.rs b/crates/xfa/src/model.rs index 572675f95..327f7fc41 100644 --- a/crates/xfa/src/model.rs +++ b/crates/xfa/src/model.rs @@ -503,6 +503,9 @@ pub struct Field { pub validate_message: Option, /// Numeric fields (numericEdit, or a decimal/integer/float value) give scripts numbers. pub numeric: bool, + /// The display picture clause (``), e.g. `date{YYYYMMDD}`: how the value + /// shows when not being edited, and what scripts read as `formattedValue`. + pub display_picture: Option, } impl Field { diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index 4582d59b2..c753e5aed 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -475,6 +475,7 @@ fn parse_field(n: XmlNode, warnings: &mut Vec) -> Field { validate: validate_el.and_then(|v| child(v, "script")).map(|s| parse_script(s, "validate")), validate_message, numeric, + display_picture: child(n, "format").and_then(|f| child(f, "picture")).map(text_of).map(|p| p.trim().to_string()).filter(|p| !p.is_empty()), } } diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 17a037664..f75235e20 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -44,6 +44,8 @@ pub struct FormNode { pub occur_max: Option, pub index: usize, pub children: Vec, + /// Fields: the display picture (see [`Field::display_picture`]), for `formattedValue`. + pub display_picture: Option, } /// A script the template runs for an event of one node instance. @@ -261,6 +263,7 @@ impl Walker<'_> { occur_max: Some(1), index, children: Vec::new(), + display_picture: f.display_picture.clone(), } } @@ -314,6 +317,7 @@ impl Walker<'_> { occur_max: sf.occur.max, index, children, + display_picture: None, }); } } diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 03eb2c67a..963056f10 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -336,6 +336,32 @@ fn dates_convert_between_iso_and_acrobat_patterns() { assert_eq!(pattern_to_iso("hello", "mm/dd/yyyy"), None); } +/// What scripts read as `formattedValue`: a date display picture shows the ISO value in its +/// pattern (the first of several alternatives); other kinds of picture, and values that aren't +/// ISO dates, leave the raw value. +#[test] +fn formatted_values_follow_date_display_pictures() { + assert_eq!(crate::formatted_value("date{YYYYMMDD}", "2026-10-20").as_deref(), Some("20261020")); + assert_eq!(crate::formatted_value("date{MM/DD/YYYY}", "2026-10-20").as_deref(), Some("10/20/2026")); + assert_eq!(crate::formatted_value("date{YYYYMMDD}|date{DDMMMYY}", "2026-10-20").as_deref(), Some("20261020")); + assert_eq!(crate::formatted_value("date{YYYY-MM-DD}", "2026-10-20"), None, "shown as it is"); + assert_eq!(crate::formatted_value("num{zzz9}", "2026-10-20"), None); + assert_eq!(crate::formatted_value("date{YYYYMMDD}", "20261020"), None, "not an ISO date"); + assert_eq!(crate::formatted_value("date{YYYYMMDD}", ""), None); +} + +/// A field's `` is its display picture, kept apart from its `` edit +/// picture; a field without one has none. +#[test] +fn display_pictures_are_parsed_apart_from_edit_pictures() { + let (tpl, _) = parse(&crate::fixtures::date_row_template()).unwrap(); + let Node::Subform(page) = &tpl.root.children[0] else { panic!() }; + let Node::Field(from) = &page.children[0] else { panic!() }; + assert_eq!((from.picture.as_deref(), from.display_picture.as_deref()), (Some("date{YYYYMMDD}"), Some("date{YYYYMMDD}"))); + let Node::Field(days) = &page.children[1] else { panic!() }; + assert_eq!(days.display_picture, None); +} + #[test] fn data_fills_the_fields_and_adds_rows() { let form = layout_xml(&template_with_data(1, DATA)).unwrap(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 7aca7d5c8..0c99de7b6 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5220,8 +5220,8 @@ milestone = "M12" 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", "crates/xfa/src/tests.rs::page_area_without_occur_serves_every_body_page", "apps/pdfcraft-cli/tests/xfa.rs::info_reports_the_laid_out_pages_and_fields", "apps/pdfcraft-cli/tests/xfa.rs::text_reads_the_layout_including_a_page_absent_from_the_placeholder", "apps/pdfcraft-cli/tests/xfa.rs::render_matches_the_engine_layout_on_the_second_page", "apps/pdfcraft-cli/tests/xfa.rs::a_laid_out_form_keeps_the_original_copy_permissions"] -notes = "laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters (a page area with no occur repeats without limit, one with occur max=1 is used once, one with occur min=1 the body never reached gets a page after the body, as Acrobat Pro does on a synthetic form), 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. CLI info reports the laid-out page/field counts and dimensions while retaining the input file size and raw counts as pdf_pages/acroform_fields; text and render use those pages too, preserving copy permissions and leaving the source file unchanged (#1645). 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: a page area's relevant=\"-print\" (on a synthetic form Acrobat still prints the page but leaves it blank; PdfCraft prints its content, #2574); a page break before the first section (Acrobat opens an empty body page first when the first section has breakBefore targetType=pageArea, with or without startNew, as seen on synthetic forms; PdfCraft skips a break on an empty page and lays out one page fewer, #2574); 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)" +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/engine/src/tests.rs::xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it", "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", "crates/xfa/src/tests.rs::page_area_without_occur_serves_every_body_page", "apps/pdfcraft-cli/tests/xfa.rs::info_reports_the_laid_out_pages_and_fields", "apps/pdfcraft-cli/tests/xfa.rs::text_reads_the_layout_including_a_page_absent_from_the_placeholder", "apps/pdfcraft-cli/tests/xfa.rs::render_matches_the_engine_layout_on_the_second_page", "apps/pdfcraft-cli/tests/xfa.rs::a_laid_out_form_keeps_the_original_copy_permissions"] +notes = "laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters (a page area with no occur repeats without limit, one with occur max=1 is used once, one with occur min=1 the body never reached gets a page after the body, as Acrobat Pro does on a synthetic form), 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. CLI info reports the laid-out page/field counts and dimensions while retaining the input file size and raw counts as pdf_pages/acroform_fields; text and render use those pages too, preserving copy permissions and leaving the source file unchanged (#1645). XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, formattedValue in a date display picture's pattern (number and text display pictures still give the raw value), 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: a page area's relevant=\"-print\" (on a synthetic form Acrobat still prints the page but leaves it blank; PdfCraft prints its content, #2574); a page break before the first section (Acrobat opens an empty body page first when the first section has breakBefore targetType=pageArea, with or without startNew, as seen on synthetic forms; PdfCraft skips a break on an empty page and lays out one page fewer, #2574); 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"