mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 14:41:51 +00:00
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:
co-authored by
Claude Opus 5.5
parent
3109bb2359
commit
ab2db45dfc
+15
-12
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user