M12: an XFA subformSet is transparent to layout, so its members sit where their container puts them

A subformSet was always laid out top to bottom. In a positioned subform its members, which have
no x or y of their own, were stacked instead of overlapping at their positions, so a two-column
section pushed its second column past the page (XFA 3.3 p. 357: "The layout process treats
subformSet objects as transparent"). A set now takes its container's layout; in a table or a row
its members still go one under another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
This commit is contained in:
mtlcl410
2026-10-29 13:34:40 +08:00
co-authored by Claude Opus 5.5
parent 3109bb2359
commit ab2db45dfc
2 changed files with 41 additions and 12 deletions
+15 -12
View File
@@ -602,7 +602,7 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> PageS
if content.is_empty() {
content.push(Rect::new(0.0, 0.0, width, height));
}
let items = pa.children().filter(|c| c.is_element()).filter_map(|c| parse_node(c, warnings, depth + 1)).collect();
let items = pa.children().filter(|c| c.is_element()).filter_map(|c| parse_node(c, warnings, depth + 1, Layout::Positioned)).collect();
// A page area with no `<occur>` has no limit and no minimum (a subform's default of one
// would exhaust it, and demand a page for every master).
let occur = child(pa, "occur").map(|o| parse_occur(Some(o)));
@@ -618,15 +618,16 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> PageS
PageSet { areas }
}
fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subform {
fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize, set_layout: Option<Layout>) -> Subform {
let common = parse_common(n);
let layout = set_layout.unwrap_or_else(|| Layout::parse(n.attribute("layout")));
let mut children_out = Vec::new();
let mut page_set = None;
for c in n.children().filter(|c| c.is_element()) {
match c.tag_name().name() {
"pageSet" => page_set = Some(parse_page_set(c, warnings, depth + 1)),
_ => {
if let Some(node) = parse_node(c, warnings, depth + 1) {
if let Some(node) = parse_node(c, warnings, depth + 1, layout) {
children_out.push(node);
}
}
@@ -637,7 +638,7 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subfor
let break_to_page = |name: &str| children(n, name).any(|b| b.attribute("targetType").is_none_or(|t| t == "pageArea" || t == "contentArea"));
Subform {
common,
layout: Layout::parse(n.attribute("layout")),
layout,
children: children_out,
scripts: parse_events(n),
page_set,
@@ -650,21 +651,23 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subfor
}
}
fn parse_node(c: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Option<Node> {
/// `parent`: the layout of the container `c` is in, which a `subformSet` takes on.
fn parse_node(c: XmlNode, warnings: &mut Vec<String>, depth: usize, parent: Layout) -> Option<Node> {
if depth > MAX_DEPTH {
return None;
}
Some(match c.tag_name().name() {
"subform" => Node::Subform(Box::new(parse_subform(c, warnings, depth))),
"subform" => Node::Subform(Box::new(parse_subform(c, warnings, depth, None))),
// A set of related subforms is transparent to layout (XFA 3.3 p. 357): its members are laid out
// as its container lays out its own children, at their own positions in a positioned one. In a
// table or a row they go one under another.
"subformSet" => {
// A set of alternative subforms: lay out its members in order.
let mut sf = parse_subform(c, warnings, depth);
sf.layout = Layout::Tb;
Node::Subform(Box::new(sf))
let layout = if matches!(parent, Layout::Table | Layout::Row) { Layout::Tb } else { parent };
Node::Subform(Box::new(parse_subform(c, warnings, depth, Some(layout))))
}
"area" => Node::Area(Box::new(Area {
common: parse_common(c),
children: c.children().filter(|k| k.is_element()).filter_map(|k| parse_node(k, warnings, depth + 1)).collect(),
children: c.children().filter(|k| k.is_element()).filter_map(|k| parse_node(k, warnings, depth + 1, Layout::Positioned)).collect(),
})),
"field" => Node::Field(Box::new(parse_field(c, warnings))),
"draw" => Node::Draw(Box::new(Draw { common: parse_common(c), value: parse_value(child(c, "value"), warnings) })),
@@ -742,7 +745,7 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
};
let mut warnings = Vec::new();
let sf = children(template, "subform").next().ok_or_else(|| XfaError::Malformed("the template has no root subform".into()))?;
let mut root_sf = parse_subform(sf, &mut warnings, 0);
let mut root_sf = parse_subform(sf, &mut warnings, 0, None);
if root_sf.page_set.is_none() {
// A page set may sit beside the root subform.
root_sf.page_set = child(template, "pageSet").map(|ps| parse_page_set(ps, &mut warnings, 0));
+26
View File
@@ -4145,3 +4145,29 @@ fn clear_password_values_clears_static_xfa_password_fields() {
let packets = read_packets(&doc).unwrap().unwrap();
assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp);
}
/// A `subformSet` is transparent to layout (XFA 3.3 p. 357): in a positioned subform its members
/// sit at their own positions, side by side as a public stamp-duty form's two columns do, not one
/// under the other; in a flowed one they still go one under another.
#[test]
fn a_subform_set_in_a_positioned_subform_places_its_members_where_they_are() {
let xml = |layout: &str| {
format!(
r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb">
<pageSet><pageArea name="p"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
<subform name="box" w="7in" {layout}>
<subformSet name="cols">
<subform name="left" w="3in" h="2in"><draw name="a" x="0in" y="0.5in" w="2in" h="0.3in"><value><text>left</text></value></draw></subform>
<subform name="right" w="3in" h="2in"><draw name="b" x="4in" y="0.5in" w="2in" h="0.3in"><value><text>right</text></value></draw></subform>
</subformSet>
</subform>
</subform></template>"#
)
};
let baseline = |layout: &str, text: &str| {
let form = crate::layout::layout(&crate::parse(&xml(layout)).unwrap().0, None).unwrap();
form.pages[0].items.iter().find_map(|i| if let Item::Text(s) = i { (s.text == text).then_some(s.baseline) } else { None }).unwrap()
};
assert!((baseline(r#"h="3in""#, "left") - baseline(r#"h="3in""#, "right")).abs() < 0.01, "positioned: side by side");
assert!(baseline(r#"layout="tb""#, "right") - baseline(r#"layout="tb""#, "left") > 140.0, "flowed: one under the other");
}