mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 13:24:35 +00:00
M12: measure each XFA table cell under its own same-name SOM index
cells() (behind row_height) measured every cell with size_of(), at SOM index 0, while draw_row() draws cell i at its index among its same-named siblings. With a short qty[0] beside a long qty[1] the row took qty[0]'s height (38.5pt), and the long cell ran into the following rows and content; a non-interactive cell drew its text over them. Each cell is now measured with size_at() at SomIndices::of(..).at(i), the index it is drawn and bound at (XFA 3.3, "Tables", p. 327; "Growable Containers", p. 271). content_width(), group_height() and draw_group() measured their children at index 0 too and now use the same index. size_of() had no callers left and is removed, so every measurement names its index. Tests: a row with short qty[0] and long qty[1] is as tall as the long cell, with the following rows, footer and content after it, in the positioned, positioned-in-flow and flowed placements; a non-interactive long qty[1] keeps its lines inside its own row. Both fail with the index forced back to 0 (38.5pt rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
This commit is contained in:
1 parent
5172a1b7ea
commit
0d0162bf81
3 files changed
+112
-16
No files matched your search
@@ -273,6 +273,8 @@ impl Layouter<'_> {
|
||||
let inner = rect.inset(&g.common.margin);
|
||||
let group = self.unique_name(g.common.name.as_deref().unwrap_or("group"));
|
||||
let nodes: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
|
||||
// Each field is measured under the SOM index it is drawn at.
|
||||
let sibs = SomIndices::of(&nodes);
|
||||
let placed: Vec<(usize, Rect)> = match g.layout {
|
||||
Layout::Tb => {
|
||||
let mut y = 0.0;
|
||||
@@ -281,7 +283,7 @@ impl Layouter<'_> {
|
||||
if !k.common().presence.occupies() {
|
||||
continue;
|
||||
}
|
||||
let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1);
|
||||
let (w, h) = self.size_at(k, inner.w, &ctx, depth + 1, sibs.at(i));
|
||||
out.push((i, Rect::new(0.0, y, w, h)));
|
||||
y += h;
|
||||
}
|
||||
@@ -293,14 +295,13 @@ impl Layouter<'_> {
|
||||
.filter(|(_, k)| k.common().presence.occupies())
|
||||
.map(|(i, k)| {
|
||||
let c = k.common();
|
||||
let (w, h) = self.size_of(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), &ctx, depth + 1);
|
||||
let (w, h) = self.size_at(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), &ctx, depth + 1, sibs.at(i));
|
||||
let (x, y) = c.top_left(w, h);
|
||||
(i, Rect::new(x, y, w, h))
|
||||
})
|
||||
.collect(),
|
||||
_ => self.pack(&nodes, inner.w, &ctx, depth).0,
|
||||
};
|
||||
let sibs = SomIndices::of(&nodes);
|
||||
for (i, r) in placed {
|
||||
if let Some(f) = g.fields.get(i) {
|
||||
let on = f.items.first().cloned().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| (i + 1).to_string());
|
||||
|
||||
@@ -6,11 +6,6 @@ use super::{
|
||||
};
|
||||
|
||||
impl Layouter<'_> {
|
||||
/// [`Self::size_at`] for the first instance of `node` (one that doesn't repeat).
|
||||
pub(super) fn size_of(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> (f64, f64) {
|
||||
self.size_at(node, avail_w, ctx, depth, 0)
|
||||
}
|
||||
|
||||
/// The width and height instance `index` of `node` under `ctx` takes in `avail_w`: its own
|
||||
/// size, else what its content needs (containers are measured once per width, depth and
|
||||
/// instance, and measuring stops at its caps). Instances differ when the data gives each its
|
||||
@@ -156,20 +151,24 @@ impl Layouter<'_> {
|
||||
Node::Area(a) => (&a.children, Layout::Positioned),
|
||||
_ => return avail_w,
|
||||
};
|
||||
let visible = children.iter().filter(|k| k.common().presence.occupies());
|
||||
// Each child under the SOM index it is drawn at.
|
||||
let sibs = SomIndices::of(children);
|
||||
let visible = children.iter().enumerate().filter(|(_, k)| k.common().presence.occupies());
|
||||
match layout {
|
||||
Layout::LrTb | Layout::RlTb | Layout::Row => {
|
||||
let sum: f64 = visible.map(|k| self.size_of(k, avail_w, ctx, depth + 1).0).sum();
|
||||
let sum: f64 = visible.map(|(i, k)| self.size_at(k, avail_w, ctx, depth + 1, sibs.at(i)).0).sum();
|
||||
if sum <= avail_w + 0.05 { sum } else { avail_w }
|
||||
}
|
||||
Layout::Positioned => visible
|
||||
.map(|k| {
|
||||
let (w, h) = self.size_of(k, avail_w, ctx, depth + 1);
|
||||
.map(|(i, k)| {
|
||||
let (w, h) = self.size_at(k, avail_w, ctx, depth + 1, sibs.at(i));
|
||||
k.common().top_left(w, h).0 + w
|
||||
})
|
||||
.fold(0.0, f64::max)
|
||||
.min(avail_w),
|
||||
Layout::Tb | Layout::Table => visible.map(|k| self.size_of(k, avail_w, ctx, depth + 1).0).fold(0.0, f64::max).min(avail_w),
|
||||
Layout::Tb | Layout::Table => {
|
||||
visible.map(|(i, k)| self.size_at(k, avail_w, ctx, depth + 1, sibs.at(i)).0).fold(0.0, f64::max).min(avail_w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +307,15 @@ impl Layouter<'_> {
|
||||
fn group_height(&self, g: &ExclGroup, inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
|
||||
let nodes: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
|
||||
match g.layout {
|
||||
Layout::Tb => nodes.iter().filter(|k| k.common().presence.occupies()).map(|k| self.size_of(k, inner_w, ctx, depth + 1).1).sum(),
|
||||
Layout::Tb => {
|
||||
let sibs = SomIndices::of(&nodes);
|
||||
nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, k)| k.common().presence.occupies())
|
||||
.map(|(i, k)| self.size_at(k, inner_w, ctx, depth + 1, sibs.at(i)).1)
|
||||
.sum()
|
||||
}
|
||||
Layout::Positioned => self.positioned_height(&nodes, inner_w, ctx, depth),
|
||||
_ => self.packed_height(&nodes, inner_w, ctx, depth),
|
||||
}
|
||||
@@ -364,20 +371,24 @@ impl Layouter<'_> {
|
||||
self.pack(children, inner_w, ctx, depth).1
|
||||
}
|
||||
|
||||
/// Cell rectangles of a row (relative to the row's inner box) and the row height.
|
||||
/// Cell rectangles of a row (relative to the row's inner box) and the row height. Each cell
|
||||
/// is measured under its own SOM index among its same-named siblings (`qty[1]`, not
|
||||
/// `qty[0]`), the index [`Self::draw_row`] draws it at, so a cell that grows with its value
|
||||
/// takes its own value's height.
|
||||
pub(super) fn cells(&self, row: &Subform, cols: &[f64], inner_w: f64, ctx: &Ctx, depth: usize) -> (Vec<(usize, Rect)>, f64) {
|
||||
let ctx = ctx.child(&row.common);
|
||||
let mut out = Vec::new();
|
||||
let mut col = 0usize;
|
||||
let mut x = 0.0;
|
||||
let mut h: f64 = 0.0;
|
||||
let sibs = SomIndices::of(&row.children);
|
||||
for (i, k) in row.children.iter().enumerate() {
|
||||
if !k.common().presence.occupies() {
|
||||
continue;
|
||||
}
|
||||
let span = k.common().col_span.max(1);
|
||||
let w = if col < cols.len() { cols.iter().skip(col).take(span).sum::<f64>() } else { k.common().w.unwrap_or((inner_w - x).max(1.0)) };
|
||||
let (_, ch) = self.size_of(k, w, &ctx, depth + 1);
|
||||
let (_, ch) = self.size_at(k, w, &ctx, depth + 1, sibs.at(i));
|
||||
out.push((i, Rect::new(x, 0.0, w, ch)));
|
||||
h = h.max(ch);
|
||||
x += w;
|
||||
|
||||
@@ -279,3 +279,87 @@ fn each_row_instance_takes_the_height_of_its_own_growing_text() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A multi-line `qty` text field 100pt wide that grows with its text, with `access` and
|
||||
/// template value `value`.
|
||||
fn qty_cell(access: &str, value: &str) -> String {
|
||||
format!(
|
||||
r#"<field name="qty" w="100pt" minH="20pt" access="{access}"><ui><textEdit multiLine="1"/></ui><value><text>{value}</text></value></field>"#
|
||||
)
|
||||
}
|
||||
|
||||
/// [`table_form`] with two same-named cells per `item` row, `qty[0]` and `qty[1]` (`first` and
|
||||
/// `second`), in two 100pt columns, and the data of row `i` holding `data[i]`.
|
||||
fn same_name_cells_form(place: Place, first: &str, second: &str, data: [&str; 3]) -> String {
|
||||
let mut xml = table_form(place, r#"min="1" max="-1""#, 3)
|
||||
.replace(r#"columnWidths="200pt""#, r#"columnWidths="100pt 100pt""#)
|
||||
.replace(r#"<field name="qty" w="200pt" h="20pt"><ui><textEdit/></ui></field>"#, &format!("{first}{second}"));
|
||||
for (i, d) in data.iter().enumerate() {
|
||||
xml = xml.replace(&format!("<item><qty>row {}</qty></item>", i + 1), &format!("<item>{d}</item>"));
|
||||
}
|
||||
xml
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_is_as_tall_as_its_tallest_same_named_cell() {
|
||||
// Each cell of a row is measured under its own SOM index among its same-named siblings,
|
||||
// the index it is drawn and bound at (XFA 3.3, "Tables", p. 327; "Growable Containers",
|
||||
// p. 271; Chapter 9, p. 336): a short `qty[0]` beside a long `qty[1]` makes the row as tall
|
||||
// as the long one, and the rows and content after it start below it.
|
||||
let long = "a long value that wraps over several lines of the cell".repeat(6);
|
||||
let cell = qty_cell("open", "");
|
||||
for place in [Place::Positioned, Place::PositionedFlow, Place::Flowed] {
|
||||
let data = ["<qty>s1</qty><qty>t1</qty>", &format!("<qty>s2</qty><qty>{long}</qty>"), "<qty>s3</qty><qty>t3</qty>"];
|
||||
let form = layout_xml(&same_name_cells_form(place, &cell, &cell, data)).unwrap();
|
||||
let all = laid_out(&form);
|
||||
let first = named(&all, "qty");
|
||||
let second: Vec<&Laid> = all.iter().filter(|w| w.1.ends_with(".qty[1]")).collect();
|
||||
assert_eq!(first.iter().map(|w| w.2.clone().unwrap_or_default()).collect::<Vec<_>>(), ["s1", "s2", "s3"], "{place:?}");
|
||||
assert_eq!(second.len(), 3, "{place:?}");
|
||||
assert_eq!(second[1].2.as_deref().map(str::trim), Some(long.trim()), "{place:?}");
|
||||
let tall = second[1].3;
|
||||
assert!(tall.h > 60.0, "{place:?}: the long cell {tall:?} did not grow");
|
||||
// Both cells of the row take its height, and the next row starts below it.
|
||||
assert!((first[1].3.h - tall.h).abs() < 0.01, "{place:?}: qty[0] {:?} beside qty[1] {tall:?}", first[1].3);
|
||||
for cells in [&first, &second] {
|
||||
for pair in cells.windows(2) {
|
||||
let (a, b) = (pair[0].3, pair[1].3);
|
||||
assert!((b.y - a.bottom()).abs() < 0.01, "{place:?}: row at {b:?} after {a:?}");
|
||||
}
|
||||
}
|
||||
let foot = named(&all, "total")[0].3;
|
||||
assert!((foot.y - second[2].3.bottom()).abs() < 0.01, "{place:?}: footer {foot:?} after {:?}", second[2].3);
|
||||
if let Some(after) = named(&all, "after").first() {
|
||||
assert!((after.3.y - foot.bottom()).abs() < 0.01, "{place:?}: after {:?}, footer {foot:?}", after.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_non_interactive_same_named_cell_stays_in_its_row() {
|
||||
// A non-interactive `qty[1]` is drawn as text, not as a widget; measured under its own SOM
|
||||
// index (where the data holds nothing, so its long template value is what shows), each row
|
||||
// holds its lines, and none run into the row after it or the footer.
|
||||
let long = "longword ".repeat(12);
|
||||
for place in [Place::Positioned, Place::PositionedFlow, Place::Flowed] {
|
||||
let data = ["<qty>s1</qty>", "<qty>s2</qty>", "<qty>s3</qty>"];
|
||||
let form = layout_xml(&same_name_cells_form(place, &qty_cell("open", ""), &qty_cell("nonInteractive", &long), data)).unwrap();
|
||||
let all = laid_out(&form);
|
||||
let rows: Vec<Rect> = named(&all, "qty").iter().map(|w| w.3).collect();
|
||||
assert_eq!(rows.len(), 3, "{place:?}");
|
||||
let lines: Vec<f64> = form
|
||||
.pages
|
||||
.iter()
|
||||
.flat_map(|p| p.items.iter())
|
||||
.filter_map(|it| if let Item::Text(s) = it { Some(s) } else { None })
|
||||
.filter(|s| s.text.contains("longword"))
|
||||
.map(|s| s.baseline)
|
||||
.collect();
|
||||
assert!(lines.len() >= 6, "{place:?}: {lines:?}");
|
||||
let per_row: Vec<usize> = rows.iter().map(|r| lines.iter().filter(|&&b| b > r.y && b <= r.bottom()).count()).collect();
|
||||
assert_eq!(per_row.iter().sum::<usize>(), lines.len(), "{place:?}: lines {lines:?} outside rows {rows:?}");
|
||||
assert!(per_row.iter().all(|&n| n == per_row[0] && n > 1), "{place:?}: lines per row {per_row:?}");
|
||||
let foot = named(&all, "total")[0].3;
|
||||
assert!((foot.y - rows[2].bottom()).abs() < 0.01, "{place:?}: footer {foot:?} after {:?}", rows[2]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user