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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-10-29 21:25:02 +08:00
1 parent 4d467e1ba6
commit 7e0f635856
8 files changed
+248 -14

No files matched your search

+29
View File
@@ -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<u8> {
let data: String = (1..=5).map(|i| format!("<item><qty>row {i}</qty></item>")).collect();
let xdp = format!(
r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb">
<pageSet><pageArea name="p"><contentArea x="36pt" y="36pt" w="540pt" h="720pt"/><medium short="612pt" long="792pt"/></pageArea></pageSet>
<subform name="page" w="540pt" h="700pt">
<subform name="table" layout="table" columnWidths="200pt" x="36pt" y="72pt">
<subform name="item" layout="row"><occur min="1" max="{max}"/><field name="qty" w="200pt" h="20pt"><ui><textEdit/></ui></field></subform>
</subform>
</subform>
</subform></template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><form><page><table>{data}</table></page></form></xfa:data></xfa:datasets>
</xdp:xdp>"#
);
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}");
}
}
+2 -1
View File
@@ -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
+9 -6
View File
@@ -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)?;
+11 -4
View File
@@ -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`].)
+4 -1
View File
@@ -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()
+3
View File
@@ -177,5 +177,8 @@ pub fn render_into(doc: &mut Document) -> Result<Report, XfaError> {
#[cfg(test)]
mod repeat_tests;
#[cfg(test)]
mod table_repeat_tests;
#[cfg(test)]
mod tests;
+188
View File
@@ -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!("<item><qty>row {i}</qty></item>")).collect();
let table = r#"<subform name="table" layout="table" columnWidths="200pt" POS><border><edge/></border>
<subform name="head" layout="row"><field name="label" w="200pt" h="20pt"><ui><textEdit/></ui></field></subform>
<subform name="item" layout="row"><occur OCCUR/><field name="qty" w="200pt" h="20pt"><ui><textEdit/></ui></field></subform>
<subform name="foot" layout="row"><field name="total" w="200pt" h="20pt"><ui><textEdit/></ui></field></subform>
</subform>"#
.replace("OCCUR", occur);
let after = r#"<field name="after" w="200pt" h="20pt"><ui><textEdit/></ui></field>"#;
let (page, data) = match place {
Place::Positioned => {
(format!(r#"<subform name="page1" w="540pt" h="700pt">{}</subform>"#, table.replace("POS", r#"x="36pt" y="72pt""#)), "page1")
}
Place::PositionedFlow => (
format!(
r#"<subform name="page1" w="540pt" h="700pt"><subform name="wrap" layout="tb" w="200pt" x="36pt" y="72pt">{}{after}</subform></subform>"#,
table.replace("POS", "")
),
"page1><wrap",
),
Place::Flowed => (format!(r#"<subform name="page1" layout="tb" w="540pt">{}{after}</subform>"#, table.replace("POS", "")), "page1"),
};
let close: String = data.split("><").collect::<Vec<_>>().iter().rev().map(|n| format!("</{n}>")).collect();
format!(
r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb">
<pageSet><pageArea name="p"><occur max="-1"/><contentArea x="36pt" y="36pt" w="540pt" h="720pt"/><medium short="612pt" long="792pt"/></pageArea></pageSet>
{page}
</subform></template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><form><{data}><table><head><label>H</label></head><head><label>H2</label></head>{items}<foot><total>T</total></foot></table>{close}</form></xfa:data></xfa:datasets>
</xdp:xdp>"#
)
}
/// A laid-out widget: (field name, SOM, value, rect).
type Laid = (String, String, Option<String>, Rect);
/// The laid-out widgets, in order.
fn laid_out(form: &Form) -> Vec<Laid> {
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<String> {
named(all, "qty").iter().map(|w| w.2.clone().unwrap_or_default()).collect()
}
fn expected(n: usize) -> Vec<String> {
(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<String> = (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<f64> {
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}");
}
+2 -2
View File
@@ -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 (`<bind><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 (`<bind><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"