From a499ebbd6a9d3aee209cdd0be6028eb7bfa5f4ab Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:45:48 +0800 Subject: [PATCH] M12: a flowed XFA subform in a table cell is as tall as its content A `tb` (or `lr-tb`, `table`) subform that is a cell of a table row was laid out at the height it gives itself, even when its content is much shorter, so the rows after it and the next section were pushed down, onto an extra page on a public form. Adobe Acrobat and PDF-XChange Editor both lay such a cell out as tall as its content, although a height makes a container fixed (XFA 3.3 p. 275); this follows them. A positioned cell, and a flowed subform outside a table, keep their heights. `measure`'s content height moves into `content_height` so both can use it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK --- crates/xfa/src/layout/measure.rs | 81 ++++++++++++++++++-------------- crates/xfa/src/tests.rs | 29 ++++++++++++ 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs index 2fffb3dfa0..5c1ab99b3c 100644 --- a/crates/xfa/src/layout/measure.rs +++ b/crates/xfa/src/layout/measure.rs @@ -71,39 +71,7 @@ impl Layouter<'_> { let w = clamp_opt(w, c.min_w, c.max_w); let h = match c.h { Some(h) => h, - None => { - let inner_w = (w - c.margin.horizontal()).max(1.0); - let content = match node { - Node::Draw(d) => self.value_height(&d.value, inner_w, &ctx), - Node::Field(f) => self.field_height(f, inner_w, &ctx), - Node::ExclGroup(g) => self.group_height(g, inner_w, &ctx, depth), - Node::Area(a) => self.positioned_height(&a.children, inner_w, &ctx, depth), - Node::Subform(s) => match s.layout { - Layout::Positioned => self.positioned_height(&s.children, inner_w, &ctx, depth), - // Each instance of a repeating child takes its height again. - Layout::Tb => s - .children - .iter() - .filter(|k| k.common().presence.occupies()) - .map(|k| self.size_of(k, inner_w, &ctx, depth + 1).1 * self.instances(k, &som) as f64) - .sum(), - Layout::LrTb | Layout::RlTb => self.pack_instances(&s.children, inner_w, &ctx, depth, &som).1, - Layout::Table => { - let cols = self.columns(s, inner_w); - s.children - .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), - _ => self.size_of(k, inner_w, &ctx, depth + 1).1, - }) - .sum() - } - Layout::Row => self.row_height(s, &self.columns(s, inner_w), inner_w, &ctx, depth + 1), - }, - }; - content + c.margin.vertical() - } + None => self.content_height(node, (w - c.margin.horizontal()).max(1.0), &ctx, &som, depth) + c.margin.vertical(), }; // A flowed subform is as tall as what it holds: other viewers do not stretch one to // the `minH` a designer left on it. @@ -114,6 +82,40 @@ impl Layouter<'_> { (w, clamp_opt(h, min_h, c.max_h)) } + /// The height of `node`'s content within `inner_w` (inside its margins), whatever height it + /// gives itself. `ctx` is the node's own context ([`Ctx::with_som`]) and `som` its SOM path. + fn content_height(&self, node: &Node, inner_w: f64, ctx: &Ctx, som: &str, depth: usize) -> f64 { + match node { + Node::Draw(d) => self.value_height(&d.value, inner_w, ctx), + Node::Field(f) => self.field_height(f, inner_w, ctx), + Node::ExclGroup(g) => self.group_height(g, inner_w, ctx, depth), + Node::Area(a) => self.positioned_height(&a.children, inner_w, ctx, depth), + Node::Subform(s) => match s.layout { + Layout::Positioned => self.positioned_height(&s.children, inner_w, ctx, depth), + // Each instance of a repeating child takes its height again. + Layout::Tb => s + .children + .iter() + .filter(|k| k.common().presence.occupies()) + .map(|k| self.size_of(k, inner_w, ctx, depth + 1).1 * self.instances(k, som) as f64) + .sum(), + Layout::LrTb | Layout::RlTb => self.pack_instances(&s.children, inner_w, ctx, depth, som).1, + Layout::Table => { + let cols = self.columns(s, inner_w); + s.children + .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), + _ => self.size_of(k, inner_w, ctx, depth + 1).1, + }) + .sum() + } + Layout::Row => self.row_height(s, &self.columns(s, inner_w), inner_w, ctx, depth + 1), + }, + } + } + /// The natural width of a container's content within `avail_w`. fn content_width(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> f64 { let group: Vec; @@ -350,7 +352,18 @@ impl Layouter<'_> { } let span = k.common().col_span.max(1); let w = if col < cols.len() { cols.iter().skip(col).take(span).sum::() } else { k.common().w.unwrap_or((inner_w - x).max(1.0)) }; - let (_, ch) = self.size_of(k, w, &ctx, depth + 1); + let (cw, ch) = self.size_of(k, w, &ctx, depth + 1); + // A flowed subform in a cell is as tall as its content even when it gives a height: + // other viewers (Adobe Acrobat, PDF-XChange Editor) lay a section of a table row out + // that way, though a height makes a container fixed (XFA 3.3 p. 276). + let ch = match k { + Node::Subform(s) if s.common.h.is_some() && matches!(s.layout, Layout::Tb | Layout::LrTb | Layout::RlTb | Layout::Table) => { + let own = ctx.with_som(&s.common, 0); + let inner = (cw - s.common.margin.horizontal()).max(1.0); + clamp_opt(self.content_height(k, inner, &own, &own.som, depth + 1) + s.common.margin.vertical(), None, s.common.max_h) + } + _ => ch, + }; out.push((i, Rect::new(x, 0.0, w, ch))); h = h.max(ch); x += w; diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index e424be5441..99c1b02a24 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4145,3 +4145,32 @@ fn clear_password_values_clears_static_xfa_password_fields() { let packets = read_packets(&doc).unwrap().unwrap(); assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp); } + +/// A flowed subform in a table cell is as tall as its content even when it gives a height, as +/// other viewers lay out a public form's per-child section (a `tb` subform with `h="186mm"` in a +/// row); a positioned cell, and a flowed subform outside a table, keep their heights. +#[test] +fn a_flowed_subform_in_a_table_cell_is_as_tall_as_its_content() { + let after_y = |cell: &str| { + let body = format!( + r#"{cell} +"# + ); + let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap(); + widgets(&form.pages[0]).iter().find(|w| w.name == "after").map(|w| w.rect.y).unwrap() + }; + let fields = r#""#; + // The table starts at 0.5in (36pt); its content is two 0.3in fields (43.2pt). + let flowed = after_y(&format!(r#"{fields}"#)); + assert!((flowed - (36.0 + 43.2)).abs() < 0.5, "flowed cell: the next object at {flowed}"); + let positioned = after_y(r#""#); + assert!((positioned - (36.0 + 360.0)).abs() < 0.5, "positioned cell keeps h: {positioned}"); + let outside = { + let body = format!( + r#"{fields}"# + ); + let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap(); + widgets(&form.pages[0]).iter().find(|w| w.name == "after").map(|w| w.rect.y).unwrap() + }; + assert!((outside - (36.0 + 360.0)).abs() < 0.5, "a flowed subform outside a table keeps h: {outside}"); +}