From c0a513eed8a940b7b2cd0e7d734e9b8916f34913 Mon Sep 17 00:00:00 2001
From: Eric Kaden <608341+EricKaden@users.noreply.github.com>
Date: Tue, 20 Oct 2026 23:32:16 -0400
Subject: [PATCH] M12: scripts read a date in its data picture as the ISO date
A date the field's data picture writes as 20261018 reaches scripts as
2026-10-18, the rawValue another viewer gives them, so a script that
checks String(rawValue).length == 10 works (DA Form 31's 10b TO).
Doc comments on the functions this branch touches.
---
crates/engine/src/tests.rs | 5 +++++
crates/xfa/src/data.rs | 4 ++++
crates/xfa/src/layout.rs | 4 ++++
crates/xfa/src/parse.rs | 2 ++
crates/xfa/src/script.rs | 19 +++++++++++++++----
crates/xfa/src/tests.rs | 24 ++++++++++++++++++++++++
parity/acrobat-features.toml | 4 ++--
7 files changed, 56 insertions(+), 6 deletions(-)
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
index 9365e744c..27a6a3b4f 100644
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -4155,6 +4155,8 @@ fn dynamic_xfa_forms_open_with_the_values_and_rows_of_their_data() {
assert!(!doc.dirty);
}
+/// A static XFA form's fields take their values from the datasets on opening, and edits are
+/// written back there.
#[test]
fn static_xfa_forms_take_their_values_from_the_datasets_and_write_them_back() {
let original = Arc::new(pdfcraft_xfa::fixtures::static_shell("Ada1"));
@@ -4206,6 +4208,8 @@ fn xfa_dates_are_written_and_read_back_in_their_data_picture() {
assert_eq!(value("when"), vec!["10/18/2026".to_string()]);
}
+/// Same-named date fields are each written in their own data picture, and a data picture
+/// without a day keeps the ISO date.
#[test]
fn each_xfa_date_field_is_written_in_its_own_data_picture_and_a_partial_one_keeps_iso() {
let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::sibling_date_template("")));
@@ -4231,6 +4235,7 @@ fn each_xfa_date_field_is_written_in_its_own_data_picture_and_a_partial_one_keep
}
}
+/// Data no field binds to (other namespaces, history) survives an edit and a save.
#[test]
fn xfa_data_no_field_binds_to_survives_an_edit_and_save() {
// Regression: editing a field replaced the whole data element with the fields' values.
diff --git a/crates/xfa/src/data.rs b/crates/xfa/src/data.rs
index 678b1d975..79ca6e166 100644
--- a/crates/xfa/src/data.rs
+++ b/crates/xfa/src/data.rs
@@ -553,6 +553,8 @@ fn path_of(tpl: Option<&Template>, som: &str) -> DataPath {
}
}
+/// The values the datasets should hold for `fields`, by data path: dates in each field's data
+/// picture (or ISO), check and radio states, choice values as saved.
fn wanted(doc: &Document, fields: &[FieldDatum], tpl: Option<&Template>) -> Vec {
let mut out = Vec::new();
for f in fields {
@@ -1636,6 +1638,8 @@ pub fn read_values_with(doc: &Document, tpl: Option<&Template>, fields: &[FieldD
with_template(doc, tpl, |t| values_at(doc, t, fields))
}
+/// The values the document's datasets hold for `fields`, read in each field's data picture and
+/// shown in its own pattern; only fields the data mentions.
fn values_at(doc: &Document, tpl: Option<&Template>, fields: &[FieldDatum]) -> Vec<(String, FieldData)> {
let Ok(Some(p)) = crate::read_packets(doc) else { return Vec::new() };
let Some(data) = parse_datasets(&p.xdp) else { return Vec::new() };
diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs
index b7c2605a6..478936188 100644
--- a/crates/xfa/src/layout.rs
+++ b/crates/xfa/src/layout.rs
@@ -399,6 +399,8 @@ fn date_pattern(picture: Option<&str>) -> String {
picture.and_then(date_letters).unwrap_or_else(|| "yyyy-mm-dd".into())
}
+/// The PDF action a button's click script becomes: a native one for a single-statement idiom
+/// (reset, print, save, a link), else the script itself.
fn button_action(scripts: &[Script]) -> Option {
let s = scripts.iter().find(|s| s.activity == "click")?;
let t = &s.text;
@@ -1450,6 +1452,8 @@ impl Layouter<'_> {
if *n == 1 { base } else { format!("{base}_{n}") }
}
+ /// Lay out field `f` in `rect`: its caption, its widget of the right kind, and its value
+ /// from the data.
fn draw_field(
&mut self,
f: &Field,
diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs
index ac02e5119..abe3f1a42 100644
--- a/crates/xfa/src/parse.rs
+++ b/crates/xfa/src/parse.rs
@@ -407,6 +407,8 @@ fn parse_items(n: XmlNode, warnings: &mut Vec) -> (Vec, Vec` element as the model's [`Field`]: its widget kind, caption, value, items,
+/// pictures (edit and data), scripts and access.
fn parse_field(n: XmlNode, warnings: &mut Vec) -> Field {
let ui = child(n, "ui");
let ui_el = ui.and_then(|u| u.children().find(|c| c.is_element() && c.tag_name().name() != "extras" && c.tag_name().name() != "picture"));
diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs
index 9d158ac6c..f74293a67 100644
--- a/crates/xfa/src/script.rs
+++ b/crates/xfa/src/script.rs
@@ -235,6 +235,8 @@ impl Walker<'_> {
self.ov.access.get(som).cloned().unwrap_or_else(|| access_name(a).to_string())
}
+ /// The live node of field `f`, instance `index` under `parent_som`: its value from the data
+ /// (as scripts read it) or its default, a radio button's from its group's choice `radio`.
fn field(&mut self, f: &Field, parent_som: &str, index: usize, radio: Option<&str>) -> FormNode {
let som = child_som(parent_som, f.common.name.as_deref().or(Some("field")), index);
let data_value = self.data.and_then(|d| d.value_at(&data_path(self.tpl, &som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
@@ -244,7 +246,7 @@ impl Walker<'_> {
let on = f.items.first().cloned().unwrap_or_default();
if selected == on { on } else { String::new() }
}
- (None, Some(v)) => f.saved_value(&v),
+ (None, Some(v)) => f.saved_value(&Walker::as_scripts_read(f, &v)),
(None, None) => f.value.plain().map(|v| f.saved_value(v.trim())).unwrap_or_default(),
};
self.push_events(&som, &f.scripts, f.calculate.as_ref(), f.validate.as_ref(), f.validate_message.as_deref());
@@ -264,12 +266,19 @@ impl Walker<'_> {
}
}
- /// What [`Self::field`] gives a field outside an exclusion group: its data value (as saved)
- /// or its template default.
+ /// A value the data holds as scripts read it: a date the field's data picture writes in
+ /// another form (`20261018`) is the ISO date (`2026-10-18`) another viewer gives scripts as
+ /// `rawValue`. Other values, and dates that don't match the picture, are as they are.
+ fn as_scripts_read(f: &Field, v: &str) -> String {
+ crate::data::data_to_iso(v, crate::data::data_date_letters(f).as_deref())
+ }
+
+ /// What [`Self::field`] gives a field outside an exclusion group: its data value (as saved,
+ /// as scripts read it) or its template default.
fn plain_field_value(tpl: &Template, data: Option<&DataNode>, f: &Field, som: &str) -> String {
let data_value = data.and_then(|d| d.value_at(&data_path(tpl, som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
match data_value {
- Some(v) => f.saved_value(&v),
+ Some(v) => f.saved_value(&Walker::as_scripts_read(f, &v)),
None => f.value.plain().map(|v| f.saved_value(v.trim())).unwrap_or_default(),
}
}
@@ -602,6 +611,8 @@ pub fn refresh_value(live: &mut FormNode, tpl: &Template, data: Option<&DataNode
if path.is_empty() {
return None;
}
+ /// Down the one branch of `n` that `path` leads to from segment `pos`, giving each field
+ /// bound there its value from `data`; `None` when something there needs a rebuild.
fn walk(
n: &mut FormNode,
tpl: &Template,
diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs
index 0a192479a..b6c861a53 100644
--- a/crates/xfa/src/tests.rs
+++ b/crates/xfa/src/tests.rs
@@ -325,6 +325,7 @@ fn datasets_parse_into_a_tree_and_som_paths_find_their_nodes() {
assert!(parse_datasets("").is_none());
}
+/// Dates convert between ISO and date patterns both ways, and what isn't a date is kept.
#[test]
fn dates_convert_between_iso_and_acrobat_patterns() {
assert_eq!(iso_to_pattern("2001-02-03", "yyyy-mm-dd"), "2001-02-03");
@@ -350,6 +351,7 @@ fn a_date_is_read_from_the_data_in_its_data_picture() {
assert_eq!(by("when").value.as_deref(), Some("10/18/2026"), "no data picture: the data holds ISO");
}
+/// A SOM path names the template field its segment numbers count to, repeating rows included.
#[test]
fn som_paths_name_the_template_field_their_numbers_count_to() {
let xml = r#"
@@ -373,6 +375,7 @@ fn som_paths_name_the_template_field_their_numbers_count_to() {
assert_eq!(picture("form[0].u[0]").as_deref(), Some("inner"));
}
+/// Refreshing a value in place uses the field's own template field, not a same-named one's.
#[test]
fn a_refreshed_field_takes_its_value_from_its_own_template_field() {
// Two text fields named `x` with different defaults; the data holds only the first.
@@ -391,6 +394,24 @@ fn a_refreshed_field_takes_its_value_from_its_own_template_field() {
assert_eq!(value(&live, "form[0].x[1]").as_deref(), Some("second"), "not the first x's default");
}
+/// Scripts read a date as the ISO date another viewer gives them as `rawValue`
+/// (`2026-10-18`), whatever the field's data picture writes in the data (`20261018`), so a
+/// script's `String(rawValue).length == 10` holds. A field without a data picture keeps the
+/// data's ISO date, and refreshing a value in place reads it the same way.
+#[test]
+fn scripts_read_a_date_as_iso_whatever_its_data_picture_writes() {
+ let xml = bound_date_template("");
+ let (tpl, _) = parse(&xml).unwrap();
+ let data = parse_datasets(&xml).unwrap();
+ let mut live = form_tree(&tpl, Some(&data), &Overrides::default()).root;
+ let value = |live: &crate::script::FormNode, som: &str| node_at(live, som).map(|n| n.value.clone());
+ assert_eq!(value(&live, "form[0].page[0].due[0]").as_deref(), Some("2026-10-18"));
+ assert_eq!(value(&live, "form[0].page[0].when[0]").as_deref(), Some("2026-10-18"));
+ let later = parse_datasets(&bound_date_template("")).unwrap();
+ assert_eq!(crate::script::refresh_value(&mut live, &tpl, Some(&later), "form[0].page[0].due[0]"), Some(true));
+ assert_eq!(value(&live, "form[0].page[0].due[0]").as_deref(), Some("2026-10-19"));
+}
+
/// The node of `live` at `som`.
fn node_at<'a>(live: &'a crate::script::FormNode, som: &str) -> Option<&'a crate::script::FormNode> {
if live.som == som {
@@ -399,6 +420,8 @@ fn node_at<'a>(live: &'a crate::script::FormNode, som: &str) -> Option<&'a crate
live.children.iter().find_map(|c| node_at(c, som))
}
+/// A date its data picture can't hold (2050 in a two-digit year) is written as ISO and reads
+/// back as written.
#[test]
fn a_date_its_data_picture_cant_hold_is_written_as_iso() {
use crate::data::{data_to_iso, iso_to_data};
@@ -410,6 +433,7 @@ fn a_date_its_data_picture_cant_hold_is_written_as_iso() {
}
}
+/// The datasets fill the fields and add the rows of repeating subforms.
#[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 e90662870..bd5f0c5a9 100644
--- a/parity/acrobat-features.toml
+++ b/parity/acrobat-features.toml
@@ -5244,8 +5244,8 @@ milestone = "M12"
status = "partial"
commands = ["file.open", "file.save", "form.export_data", "form.import_data"]
tools = ["doc_open", "form_fill", "doc_save", "doc_export_data", "doc_import_data", "form_set_image"]
-evidence = ["crates/xfa/src/tests.rs::the_datasets_packet_is_written_from_the_fields_and_read_back", "crates/xfa/src/tests.rs::a_date_is_read_from_the_data_in_its_data_picture", "crates/engine/src/tests.rs::xfa_dates_are_written_and_read_back_in_their_data_picture", "crates/engine/src/tests.rs::each_xfa_date_field_is_written_in_its_own_data_picture_and_a_partial_one_keeps_iso", "crates/xfa/src/tests.rs::som_paths_name_the_template_field_their_numbers_count_to", "crates/xfa/src/tests.rs::a_refreshed_field_takes_its_value_from_its_own_template_field", "crates/xfa/src/tests.rs::a_date_its_data_picture_cant_hold_is_written_as_iso", "crates/xfa/src/tests.rs::data_fills_the_fields_and_adds_rows", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/xfa/src/tests.rs::data_paths_leave_areas_out", "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::data_exports_as_datasets_xml_or_an_xdp_and_imports_from_any_of_them", "crates/xfa/src/tests.rs::a_picture_chosen_for_an_image_field_is_kept_in_the_data_with_its_content_type", "crates/engine/src/tests.rs::xfa_data_exports_imports_and_pictures_round_trip_through_the_session", "crates/engine/src/tests.rs::xfa_data_import_adds_and_removes_rows_of_a_dynamic_form", "crates/automation/tests/automation.rs::xfa_data_files_export_and_import_through_tools"]
-notes = "the datasets packet inside the PDF: read on open (values, dates in ISO form, or in the field's data picture (``, such as date{YYYYMMDD}) when it has one holding a whole date (one without a day, such as date{YYYYMM}, keeps ISO, as does a date the picture can't hold, such as 2050 in a two-digit year), each field its own even beside same-named siblings, which saving writes them in too, check and radio states, extra rows for repeating subforms) and rewritten after every form edit from the fields' SOM paths (/PCSom), with areas left out of data paths as Adobe binds them. Export Data writes it as XML (the data alone, rooted at the form's root data node, as Acrobat does) or an XDP with the whole datasets and a reference to the PDF; Import Data takes an XDP, datasets or data XML file (or a bare data document) as the form's whole data, keeping data descriptions, lays a dynamic form out again from it (rows added and removed, more than 50 rows reported) and runs its calculations, undoable; a static form's fields take the data and the rest are emptied; the file's data is re-serialized with the packet's own prefix and every packet rewrite is parsed back before it is written; XFDF/FDF/CSV still go the AcroForm way. Files are UTF-8, UTF-16 or ISO-8859-1, at most 16 MB and 64 levels deep; pictures at most 8 MB (PNG, JPEG, GIF). Pictures chosen for image fields are kept in the data as base64 with xfa:contentType, as Designer saves them. Missing: explicit bind refs, data descriptions are kept but not checked, no data connection (WSDL/ODBC)"
+evidence = ["crates/xfa/src/tests.rs::the_datasets_packet_is_written_from_the_fields_and_read_back", "crates/xfa/src/tests.rs::a_date_is_read_from_the_data_in_its_data_picture", "crates/xfa/src/tests.rs::scripts_read_a_date_as_iso_whatever_its_data_picture_writes", "crates/engine/src/tests.rs::xfa_dates_are_written_and_read_back_in_their_data_picture", "crates/engine/src/tests.rs::each_xfa_date_field_is_written_in_its_own_data_picture_and_a_partial_one_keeps_iso", "crates/xfa/src/tests.rs::som_paths_name_the_template_field_their_numbers_count_to", "crates/xfa/src/tests.rs::a_refreshed_field_takes_its_value_from_its_own_template_field", "crates/xfa/src/tests.rs::a_date_its_data_picture_cant_hold_is_written_as_iso", "crates/xfa/src/tests.rs::data_fills_the_fields_and_adds_rows", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/xfa/src/tests.rs::data_paths_leave_areas_out", "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::data_exports_as_datasets_xml_or_an_xdp_and_imports_from_any_of_them", "crates/xfa/src/tests.rs::a_picture_chosen_for_an_image_field_is_kept_in_the_data_with_its_content_type", "crates/engine/src/tests.rs::xfa_data_exports_imports_and_pictures_round_trip_through_the_session", "crates/engine/src/tests.rs::xfa_data_import_adds_and_removes_rows_of_a_dynamic_form", "crates/automation/tests/automation.rs::xfa_data_files_export_and_import_through_tools"]
+notes = "the datasets packet inside the PDF: read on open (values, dates in ISO form, or in the field's data picture (``, such as date{YYYYMMDD}) when it has one holding a whole date (one without a day, such as date{YYYYMM}, keeps ISO, as does a date the picture can't hold, such as 2050 in a two-digit year), each field its own even beside same-named siblings, which saving writes them in too, while scripts still read the ISO date as rawValue, as another viewer gives it, check and radio states, extra rows for repeating subforms) and rewritten after every form edit from the fields' SOM paths (/PCSom), with areas left out of data paths as Adobe binds them. Export Data writes it as XML (the data alone, rooted at the form's root data node, as Acrobat does) or an XDP with the whole datasets and a reference to the PDF; Import Data takes an XDP, datasets or data XML file (or a bare data document) as the form's whole data, keeping data descriptions, lays a dynamic form out again from it (rows added and removed, more than 50 rows reported) and runs its calculations, undoable; a static form's fields take the data and the rest are emptied; the file's data is re-serialized with the packet's own prefix and every packet rewrite is parsed back before it is written; XFDF/FDF/CSV still go the AcroForm way. Files are UTF-8, UTF-16 or ISO-8859-1, at most 16 MB and 64 levels deep; pictures at most 8 MB (PNG, JPEG, GIF). Pictures chosen for image fields are kept in the data as base64 with xfa:contentType, as Designer saves them. Missing: explicit bind refs, data descriptions are kept but not checked, no data connection (WSDL/ODBC)"
[[feature]]
id = "form.xfa-flatten"