From 7e0f635856fb980905f60505cd02dd5ffefbc712 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:52:31 +0800 Subject: [PATCH] M12: XFA table rows repeat in positioned containers A table subform (layout="table") placed in positioned content was drawn through draw_node, which drew each row subform once: the rows the data holds past the first, and an occur initial count, were dropped with no warning, while the same table in flowed content repeated them (XFA 3.3, "Tables", p. 327; "Repeating Elements using Occurrence Limits", p. 263; Chapter 9, p. 336). The positioned table now draws each row once per instance, one under another with its own SOM index, and is measured with them, so what follows it in a top-to-bottom container moves down. The count and the warning for the rows cut come from one helper, laid_out_instances, now used by every layout that repeats a subform (flowed subforms, flowed tables, positioned top-to-bottom, wrapped and table layouts). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC --- crates/engine/tests/it/xfa_repeat.rs | 29 +++++ crates/xfa/README.md | 3 +- crates/xfa/src/layout/draw.rs | 15 ++- crates/xfa/src/layout/flow.rs | 15 ++- crates/xfa/src/layout/measure.rs | 5 +- crates/xfa/src/lib.rs | 3 + crates/xfa/src/table_repeat_tests.rs | 188 +++++++++++++++++++++++++++ parity/acrobat-features.toml | 4 +- 8 files changed, 248 insertions(+), 14 deletions(-) create mode 100644 crates/xfa/src/table_repeat_tests.rs diff --git a/crates/engine/tests/it/xfa_repeat.rs b/crates/engine/tests/it/xfa_repeat.rs index 57cc54cf76..70c05f85f8 100644 --- a/crates/engine/tests/it/xfa_repeat.rs +++ b/crates/engine/tests/it/xfa_repeat.rs @@ -52,3 +52,32 @@ fn a_button_adding_rows_past_fifty_lays_them_all_out() { let out = s.take_js_output(id); assert!(!out.console.iter().any(|l| l.contains("only the first")), "no row is cut: {:?}", out.console); } + +/// A table (`layout="table"`) placed at its own position in a positioned subform, its `item` row +/// repeating with `occur max` `max`, and five rows in its data (XFA 3.3, "Tables", p. 327). +fn positioned_table(max: &str) -> Vec { + let data: String = (1..=5).map(|i| format!("row {i}")).collect(); + let xdp = format!( + r#" + +
{data}
+
"# + ); + pdfcraft_xfa::fixtures::shell(&xdp) +} + +#[test] +fn a_table_in_a_positioned_subform_gets_a_row_per_data_row_up_to_its_max() { + let mut s = Session::new(); + for (max, want) in [("-1", 5), ("3", 3)] { + let id = s.open("table.pdf", None, Arc::new(positioned_table(max)), None).expect("opens"); + assert_eq!(rows(&s, id), want, "occur max {max}"); + } +} diff --git a/crates/xfa/README.md b/crates/xfa/README.md index 423e07c26d..3aef875681 100644 --- a/crates/xfa/README.md +++ b/crates/xfa/README.md @@ -48,7 +48,8 @@ that warning and keeps the existing layout and accepted widget values. - **Data** (`data`): the `datasets` packet. On layout, a field takes its value from the data node at its data path: its SOM path without areas, which are named in SOM expressions but are not data scopes (`data_path`, as Adobe's viewers bind) (dates in ISO form, check and radio states by their on values), and a - repeating subform or row gets as many instances as the data has, up to its `occur max` and at + repeating subform or table row gets as many instances as the data has (in flowed and positioned + containers alike), up to its `occur max` and at most 1 000 (`MAX_INSTANCES`, a work bound; the rows past it are left out with a layout warning). `write_datasets` merges the AcroForm fields' values (by `/PCSom`, or by the Designer field names of a static form) into diff --git a/crates/xfa/src/layout/draw.rs b/crates/xfa/src/layout/draw.rs index cc8ff5b670..2b3ccd0971 100644 --- a/crates/xfa/src/layout/draw.rs +++ b/crates/xfa/src/layout/draw.rs @@ -99,8 +99,7 @@ impl Layouter<'_> { // A repeating subform here (a table in an area of a positioned page, // say) gets its instances, as in a flowed one, one under another, // and the rows past them are reported as there. - let instances = self.instances(k, &ctx.som); - self.warn_truncated(k, &ctx.som, instances); + let instances = self.laid_out_instances(k, &ctx.som); for instance in 0..instances { self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, sibs.at(i) + instance)?; y += h; @@ -111,8 +110,7 @@ impl Layouter<'_> { // A repeating subform here (rows of fields under a heading) gets its // instances, as in the other layouts, and the rows past them are reported. for k in s.children.iter().filter(|k| k.common().presence.occupies()) { - let instances = self.instances(k, &ctx.som); - self.warn_truncated(k, &ctx.som, instances); + self.laid_out_instances(k, &ctx.som); } let (placed, _) = self.pack_instances(&s.children, inner.w, &ctx, depth, &ctx.som); for (i, instance, r) in placed { @@ -133,9 +131,14 @@ impl Layouter<'_> { if let Node::Subform(r) = k && r.layout == Layout::Row { + // A repeating row (a table in a positioned subform, say) gets its + // instances one under another, as in a flowed table (XFA 3.3, + // "Tables", p. 327), and the rows past them are reported. let h = self.row_height(r, &cols, inner.w, &ctx, depth + 1); - self.draw_row(r, &cols, Rect::new(inner.x, y, inner.w, h), &ctx, depth + 1, csib)?; - y += h; + for instance in 0..self.laid_out_instances(k, &ctx.som) { + self.draw_row(r, &cols, Rect::new(inner.x, y, inner.w, h), &ctx, depth + 1, csib + instance)?; + y += h; + } } else { let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1); self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, csib)?; diff --git a/crates/xfa/src/layout/flow.rs b/crates/xfa/src/layout/flow.rs index 797ec6aa6c..422902b61b 100644 --- a/crates/xfa/src/layout/flow.rs +++ b/crates/xfa/src/layout/flow.rs @@ -158,8 +158,7 @@ impl Layouter<'_> { self.break_frames(0.0, true)?; } // Data adds instances of a repeating subform (rows added in another viewer). - let instances = self.instances(node, &ctx.som); - self.warn_truncated(node, &ctx.som, instances); + let instances = self.laid_out_instances(node, &ctx.som); let ctx_parent = ctx; for i in 0..instances { // Every instance spends budget, so nested repeats that place nothing still stop. @@ -246,8 +245,7 @@ impl Layouter<'_> { && row.layout == Layout::Row { // Rows added as data (in another viewer) repeat the row. - let instances = self.instances(child, &ctx.som); - self.warn_truncated(child, &ctx.som, instances); + let instances = self.laid_out_instances(child, &ctx.som); for inst in 0..instances { let h = self.row_height(row, &cols, inner_w, ctx, depth + 1); let led = lead.filter(|(li, _)| *li != ci); @@ -292,6 +290,15 @@ impl Layouter<'_> { instance_count(&sf.occur, from_data) } + /// [`Self::instances`] for laying `node` out: the count every layout that repeats a subform + /// draws (flowed or positioned, top to bottom, wrapped or a table's rows), with the data rows + /// past it reported ([`Self::warn_truncated`]). + pub(super) fn laid_out_instances(&mut self, node: &Node, parent_som: &str) -> usize { + let instances = self.instances(node, parent_som); + self.warn_truncated(node, parent_som, instances); + instances + } + /// The data holds more instances of `node` than `laid_out`: say so, since calculations /// then see only those, and say why. (The template's own counts can't ask for more: the /// parser reads `initial` and `max` up to [`MAX_INSTANCES`].) diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs index 2fffb3dfa0..2ab5f66727 100644 --- a/crates/xfa/src/layout/measure.rs +++ b/crates/xfa/src/layout/measure.rs @@ -94,7 +94,10 @@ impl Layouter<'_> { .iter() .filter(|k| k.common().presence.occupies()) .map(|k| match k { - Node::Subform(r) if r.layout == Layout::Row => self.row_height(r, &cols, inner_w, &ctx, depth + 1), + // A repeating row takes its height once per instance, as drawn. + Node::Subform(r) if r.layout == Layout::Row => { + self.row_height(r, &cols, inner_w, &ctx, depth + 1) * self.instances(k, &som) as f64 + } _ => self.size_of(k, inner_w, &ctx, depth + 1).1, }) .sum() diff --git a/crates/xfa/src/lib.rs b/crates/xfa/src/lib.rs index d00f45d73b..c27c3a8e90 100644 --- a/crates/xfa/src/lib.rs +++ b/crates/xfa/src/lib.rs @@ -177,5 +177,8 @@ pub fn render_into(doc: &mut Document) -> Result { #[cfg(test)] mod repeat_tests; +#[cfg(test)] +mod table_repeat_tests; + #[cfg(test)] mod tests; diff --git a/crates/xfa/src/table_repeat_tests.rs b/crates/xfa/src/table_repeat_tests.rs new file mode 100644 index 0000000000..5897cc6ca5 --- /dev/null +++ b/crates/xfa/src/table_repeat_tests.rs @@ -0,0 +1,188 @@ +//! A table (`layout="table"`, its rows `layout="row"`) placed in positioned content repeats a +//! row subform once per instance, as a table in flowed content does: the template's `occur` +//! and the data's rows decide how many (XFA 3.3, "Tables", p. 327; "Repeating Elements using +//! Occurrence Limits", p. 263; Chapter 9, p. 336), and the data rows past `occur max` or the +//! layout's own limit are reported, not dropped silently. + +use super::*; +use crate::layout::Item; +use crate::model::Rect; + +/// Where the table sits. +#[derive(Clone, Copy, Debug)] +enum Place { + /// At its own `x`/`y` in a positioned subform. + Positioned, + /// In a top-to-bottom subform at a position in a positioned subform, with a field after it. + PositionedFlow, + /// In the flowed body, with a field after it. + Flowed, +} + +/// A form with a table whose header row (`head`, shown once) and footer row (`foot`) frame a +/// repeating `item` row with `occur` attributes `occur`, and data holding `rows` items (and two +/// header rows, which the template shows once). Each row is 20pt tall. +fn table_form(place: Place, occur: &str, rows: usize) -> String { + let items: String = (1..=rows).map(|i| format!("row {i}")).collect(); + let table = r#" + + + + "# + .replace("OCCUR", occur); + let after = r#""#; + let (page, data) = match place { + Place::Positioned => { + (format!(r#"{}"#, table.replace("POS", r#"x="36pt" y="72pt""#)), "page1") + } + Place::PositionedFlow => ( + format!( + r#"{}{after}"#, + table.replace("POS", "") + ), + "page1> (format!(r#"{}{after}"#, table.replace("POS", "")), "page1"), + }; + let close: String = data.split("><").collect::>().iter().rev().map(|n| format!("")).collect(); + format!( + r#" + +
<{data}>{items}T
{close}
+
"# + ) +} + +/// A laid-out widget: (field name, SOM, value, rect). +type Laid = (String, String, Option, Rect); + +/// The laid-out widgets, in order. +fn laid_out(form: &Form) -> Vec { + form.pages + .iter() + .flat_map(|p| p.items.iter()) + .filter_map(|it| if let Item::Widget(w) = it { Some((w.name.clone(), w.som.clone(), w.value.clone(), w.rect)) } else { None }) + .collect() +} + +fn named<'a>(all: &'a [Laid], field: &str) -> Vec<&'a Laid> { + all.iter().filter(|w| w.1.ends_with(&format!(".{field}[0]"))).collect() +} + +fn qty_values(all: &[Laid]) -> Vec { + named(all, "qty").iter().map(|w| w.2.clone().unwrap_or_default()).collect() +} + +fn expected(n: usize) -> Vec { + (1..=n).map(|i| format!("row {i}")).collect() +} + +fn cut_warning(form: &Form) -> Option<&String> { + form.warnings.iter().find(|w| w.contains("rows; only the first")) +} + +/// Header, the rows and footer one under another, 20pt apart, from the header's top. +fn assert_stacked(all: &[Laid], rows: usize, what: &str) { + let head = named(all, "label"); + assert_eq!(head.len(), 1, "{what}: the header row is shown once: {head:?}"); + let top = head[0].3.y; + let qty = named(all, "qty"); + assert_eq!(qty.len(), rows, "{what}"); + for (i, w) in qty.iter().enumerate() { + assert!((w.3.y - (top + 20.0 * (i as f64 + 1.0))).abs() < 0.01, "{what}: row {i} at {:?}, header at {top}", w.3); + } + let foot = named(all, "total"); + assert_eq!(foot.len(), 1, "{what}: the footer row is shown once"); + assert!((foot[0].3.y - (top + 20.0 * (rows as f64 + 1.0))).abs() < 0.01, "{what}: footer at {:?} after {rows} rows", foot[0].3); +} + +#[test] +fn a_table_in_positioned_content_gets_a_row_per_data_row() { + for place in [Place::Positioned, Place::PositionedFlow] { + let form = layout_xml(&table_form(place, r#"min="1" max="-1""#, 5)).unwrap(); + let all = laid_out(&form); + assert_eq!(qty_values(&all), expected(5), "{place:?}"); + let soms: Vec<&str> = named(&all, "qty").iter().map(|w| w.1.as_str()).collect(); + let prefix = if matches!(place, Place::Positioned) { "form[0].page1[0].table[0]" } else { "form[0].page1[0].wrap[0].table[0]" }; + let want: Vec = (0..5).map(|i| format!("{prefix}.item[{i}].qty[0]")).collect(); + assert_eq!(soms, want, "{place:?}: each row with its own SOM index"); + assert_stacked(&all, 5, &format!("{place:?}")); + assert_eq!(named(&all, "label")[0].2.as_deref(), Some("H"), "{place:?}: the header takes the first header data row"); + assert!(cut_warning(&form).is_none(), "{place:?}: {:?}", form.warnings); + } +} + +#[test] +fn a_table_in_positioned_content_is_measured_with_its_rows() { + // What follows the table in a top-to-bottom container comes after the footer, not after one + // row: the table is measured with every instance. + let form = layout_xml(&table_form(Place::PositionedFlow, r#"min="1" max="-1""#, 5)).unwrap(); + let all = laid_out(&form); + let foot = named(&all, "total")[0].3; + let after = named(&all, "after")[0].3; + assert!((after.y - foot.bottom()).abs() < 0.01, "after {after:?}, footer {foot:?}"); +} + +#[test] +fn occur_max_bounds_a_positioned_table_and_the_rows_cut_are_reported() { + for place in [Place::Positioned, Place::PositionedFlow, Place::Flowed] { + let form = layout_xml(&table_form(place, r#"min="1" max="3""#, 5)).unwrap(); + let all = laid_out(&form); + assert_eq!(qty_values(&all), expected(3), "{place:?}"); + assert_stacked(&all, 3, &format!("{place:?}")); + let warning = cut_warning(&form).unwrap_or_else(|| panic!("{place:?}: {:?}", form.warnings)); + assert!(warning.contains("item") && warning.contains("5 rows") && warning.contains("first 3"), "{place:?}: {warning}"); + } +} + +#[test] +fn without_data_rows_a_positioned_table_takes_its_initial_count() { + let form = layout_xml(&table_form(Place::Positioned, r#"min="1" max="-1" initial="2""#, 0)).unwrap(); + let all = laid_out(&form); + assert_stacked(&all, 2, "initial 2"); + assert!(cut_warning(&form).is_none(), "{:?}", form.warnings); +} + +#[test] +fn a_positioned_table_lays_out_the_rows_a_flowed_one_does() { + // One rule for both: the same rows, values, spacing and warnings, wherever the table sits. + for (occur, rows) in [(r#"min="1" max="-1""#, 5), (r#"min="2" max="4""#, 7), (r#"min="1" max="-1""#, 0), (r#"min="3""#, 1)] { + let flowed = layout_xml(&table_form(Place::Flowed, occur, rows)).unwrap(); + let positioned = layout_xml(&table_form(Place::Positioned, occur, rows)).unwrap(); + let (f, p) = (laid_out(&flowed), laid_out(&positioned)); + assert_eq!(qty_values(&p), qty_values(&f), "{occur} with {rows} rows"); + let offsets = |all: &[Laid]| -> Vec { + let top = named(all, "label").first().map_or(0.0, |w| w.3.y); + all.iter().filter(|w| w.0 != "after").map(|w| w.3.y - top).collect() + }; + assert_eq!(offsets(&p), offsets(&f), "{occur} with {rows} rows"); + assert_eq!(cut_warning(&positioned).is_some(), cut_warning(&flowed).is_some(), "{occur} with {rows} rows"); + } +} + +#[test] +fn hostile_occur_values_and_data_floods_lay_a_positioned_table_out_within_bounds() { + for occur in [ + r#"min="-3" max="-7" initial="99999999999999999999""#, + r#"min="abc" max="1e9" initial="-1""#, + r#"min="999999" max="999999" initial="999999""#, + r#"max="0""#, + r#"min="0" max="0" initial="0""#, + ] { + for rows in [0, 4] { + let form = layout_xml(&table_form(Place::Positioned, occur, rows)).unwrap(); + let flowed = layout_xml(&table_form(Place::Flowed, occur, rows)).unwrap(); + assert_eq!(qty_values(&laid_out(&form)).len(), qty_values(&laid_out(&flowed)).len(), "{occur} with {rows} rows"); + } + } + // More data rows than the layout lays out: bounded, and said. + let rows = 1_200; + let form = layout_xml(&table_form(Place::Positioned, r#"min="1" max="-1""#, rows)).unwrap(); + let n = qty_values(&laid_out(&form)).len(); + assert!((1..rows).contains(&n), "{n} of {rows} rows"); + let warning = cut_warning(&form).unwrap_or_else(|| panic!("{:?}", form.warnings)); + assert!(warning.contains(&format!("{rows} rows")) && warning.contains(&format!("first {n}")), "{warning}"); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index fbbe3a2dde..f8dbdc6256 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5332,8 +5332,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/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/it/automation.rs::xfa_data_files_export_and_import_through_tools", "crates/xfa/src/repeat_tests.rs::every_data_row_past_fifty_is_laid_out_and_paginated", "crates/xfa/src/repeat_tests.rs::occur_max_still_stops_the_rows_and_says_so", "crates/xfa/src/repeat_tests.rs::rows_past_the_instance_limit_are_cut_with_a_warning", "crates/xfa/src/repeat_tests.rs::an_initial_count_lays_out_whole_up_to_the_limit", "crates/engine/tests/it/xfa_repeat.rs::a_button_adding_rows_past_fifty_lays_them_all_out"] -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: a repeating subform lays out every row up to its occur max, or up to 1,000 when it has none, and the rows past that are 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, BMP, TIFF). 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/it/automation.rs::xfa_data_files_export_and_import_through_tools", "crates/xfa/src/repeat_tests.rs::every_data_row_past_fifty_is_laid_out_and_paginated", "crates/xfa/src/repeat_tests.rs::occur_max_still_stops_the_rows_and_says_so", "crates/xfa/src/repeat_tests.rs::rows_past_the_instance_limit_are_cut_with_a_warning", "crates/xfa/src/repeat_tests.rs::an_initial_count_lays_out_whole_up_to_the_limit", "crates/engine/tests/it/xfa_repeat.rs::a_button_adding_rows_past_fifty_lays_them_all_out", "crates/xfa/src/table_repeat_tests.rs::a_table_in_positioned_content_gets_a_row_per_data_row", "crates/xfa/src/table_repeat_tests.rs::occur_max_bounds_a_positioned_table_and_the_rows_cut_are_reported", "crates/xfa/src/table_repeat_tests.rs::a_positioned_table_lays_out_the_rows_a_flowed_one_does", "crates/engine/tests/it/xfa_repeat.rs::a_table_in_a_positioned_subform_gets_a_row_per_data_row_up_to_its_max"] +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: a repeating subform or table row, in a flowed or a positioned container, lays out every row up to its occur max, or up to 1,000 when it has none, and the rows past that are 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, BMP, TIFF). 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"