mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
Merge #5124: M12: XFA table rows start at their minimum height, and a growable row group splits at a page break (form.xfa-dynamic)
# Conflicts: # crates/xfa/src/layout/flow.rs # crates/xfa/src/layout/measure.rs # crates/xfa/src/tests.rs
This commit is contained in:
commit
629d211506
3 files changed
+223
-15
No files matched your search
@@ -21,7 +21,12 @@ impl Layouter<'_> {
|
||||
// A field a page break may split starts here, and what doesn't fit goes on in the next
|
||||
// content area (XFA 3.3 pp. 291–292), rather than the whole field moving there.
|
||||
let splits = may_split && matches!(node, Node::Field(f) if f.splits_at_page_break());
|
||||
if !splits && y + h > rect.bottom() + 0.01 && h <= rect.h + 0.01 {
|
||||
// A growable flowing subform (`lr-tb`/`rl-tb`, no `h`) is split where its objects allow
|
||||
// ("splittable: in margins and where consensus exists among contained objects", XFA 3.3
|
||||
// p. 1520), so it starts here and the rest goes on in the next content area, as one
|
||||
// taller than a content area does. Anything else that fits one moves there whole.
|
||||
let flows_on = matches!(node, Node::Subform(s) if matches!(s.layout, Layout::LrTb | Layout::RlTb) && s.common.h.is_none());
|
||||
if !splits && !flows_on && y + h > rect.bottom() + 0.01 && h <= rect.h + 0.01 {
|
||||
self.make_room(h, 0.0)?;
|
||||
}
|
||||
let (rect, y, _) = self.cursor();
|
||||
@@ -65,7 +70,7 @@ impl Layouter<'_> {
|
||||
self.leaves.clear();
|
||||
return self.place_block(node, x, (w, h), ctx, depth, sib, false);
|
||||
}
|
||||
return self.split_block(first, y, drawn);
|
||||
return self.split_block(first, y, drawn, flows_on);
|
||||
};
|
||||
// The field breaks into pieces, each with the field's border all round (a border's
|
||||
// default `break="close"`, XFA 3.3 pp. 629–630): the border drawn round the whole field
|
||||
@@ -84,7 +89,7 @@ impl Layouter<'_> {
|
||||
_ => None,
|
||||
};
|
||||
self.splitting = Some(Splitting { som, rect: Rect::new(x, y, w, h), pieces: Vec::new() });
|
||||
let split = self.split_block(first, y, drawn);
|
||||
let split = self.split_block(first, y, drawn, false);
|
||||
let pieces = self.splitting.take().map(|s| s.pieces).unwrap_or_default();
|
||||
split?;
|
||||
if let Some(b) = border {
|
||||
@@ -123,8 +128,11 @@ impl Layouter<'_> {
|
||||
/// Spread the items the last page holds from index `first` on (a block drawn from `top` to
|
||||
/// `end`, past the bottom of its content area) over this and the following content areas.
|
||||
/// Breaks fall between text, pictures and fields, never through one that fits in an area;
|
||||
/// fills and lines are cut at the break.
|
||||
fn split_block(&mut self, first: usize, top: f64, end: f64) -> Result<(), XfaError> {
|
||||
/// fills and lines are cut at the break. A flowing subform (`flows_on`: one [`Self::flow_block`]
|
||||
/// splits where it would otherwise move whole) breaks the flowed subforms round it there too
|
||||
/// ([`Self::break_frames`]), so they keep their border; anything else just moves on, and
|
||||
/// those subforms are drawn without theirs ([`Self::finish_frame`]).
|
||||
fn split_block(&mut self, first: usize, top: f64, end: f64, flows_on: bool) -> Result<(), XfaError> {
|
||||
let items: Vec<Item> = match self.pages.last_mut() {
|
||||
Some(p) if first <= p.items.len() => p.items.split_off(first),
|
||||
_ => return Ok(()),
|
||||
@@ -281,15 +289,21 @@ impl Layouter<'_> {
|
||||
rest = below;
|
||||
seg_top = cut;
|
||||
first_segment = false;
|
||||
let (rect, y) = if field.is_some() {
|
||||
// A field breaks the flowed subforms round it as a field moving on whole does:
|
||||
// each closes its part at the break and opens a new one, after its top margin,
|
||||
// in the next content area.
|
||||
let (rect, y) = if field.is_some() || flows_on {
|
||||
// A field breaks the flowed subforms round it as a field moving on whole does, and
|
||||
// so does a flowing subform: each closes its part at the break, after its bottom
|
||||
// margin, and opens a new one, after its top margin, in the next content area.
|
||||
let area_h = self.cursor().0.h;
|
||||
if let Some(c) = &mut self.cur {
|
||||
c.y = (cut - shift).min(c.rect.bottom()).max(c.y);
|
||||
c.placed = true;
|
||||
if field.is_some() {
|
||||
c.placed = true;
|
||||
}
|
||||
}
|
||||
self.break_frames(0.0, false)?;
|
||||
// A flowing subform's rest that fits an area has the top margins give way to it,
|
||||
// as `make_room` does.
|
||||
let left = (end - cut).max(0.0);
|
||||
self.break_frames(if flows_on && left <= area_h + 0.01 { left } else { 0.0 }, false)?;
|
||||
let (rect, y, _) = self.cursor();
|
||||
(rect, y)
|
||||
} else {
|
||||
|
||||
@@ -257,7 +257,10 @@ impl Layouter<'_> {
|
||||
/// `ctx` is the field's own and `outer_para` the paragraph around it, which its caption takes.
|
||||
fn field_height(&self, f: &Field, inner_w: f64, ctx: &Ctx, outer_para: &Para) -> f64 {
|
||||
let font = ctx.font.apply(&f.common.font);
|
||||
let lines = if f.multiline { 3.0 } else { 1.0 };
|
||||
// An empty multi-line field is three lines tall, unless it gives a minimum height (`minH`,
|
||||
// XFA 3.3 p. 731): then that is where it starts, as a growable table cell does, and one
|
||||
// line below it; its text, when there is more, still grows it.
|
||||
let lines = if f.multiline && f.common.min_h.is_none() { 3.0 } else { 1.0 };
|
||||
let ui = match f.ui {
|
||||
Ui::CheckButton => f.check_size.unwrap_or(10.0).max(text::line_height(font.size)),
|
||||
// A button shows its caption: as tall as the caption's text in its own font.
|
||||
@@ -310,12 +313,14 @@ impl Layouter<'_> {
|
||||
/// The height the text of a multi-line text field takes (its value in the data, else the
|
||||
/// template's), for a field without a fixed height, which grows to hold its text. `ctx`
|
||||
/// is the field's own (its SOM ends in the field) and `outer_para` the paragraph around it.
|
||||
/// Zero for anything else.
|
||||
/// Zero for anything else. A nameless field takes no part in data binding (XFA 3.3 p. 194),
|
||||
/// and its `ctx` ends in its parent: its text is the template's.
|
||||
fn grown_text_height(&self, f: &Field, inner_w: f64, ctx: &Ctx, outer_para: &Para) -> f64 {
|
||||
if !f.grows_with_text() || f.common.name.is_none() {
|
||||
if !f.grows_with_text() {
|
||||
return 0.0;
|
||||
}
|
||||
let value = self.data.and_then(|d| d.value_at(&data_path(self.tpl, &ctx.som))).filter(|t| !t.trim().is_empty()).or_else(|| f.value.plain());
|
||||
let bound = f.common.name.as_ref().and(self.data);
|
||||
let value = bound.and_then(|d| d.value_at(&data_path(self.tpl, &ctx.som))).filter(|t| !t.trim().is_empty()).or_else(|| f.value.plain());
|
||||
let Some(value) = value.filter(|t| !t.trim().is_empty()) else { return 0.0 };
|
||||
// Beside a caption the text has what the caption leaves; a widget's text has 2pt of
|
||||
// padding on each side, a non-interactive field's (drawn as text) none, so it wraps
|
||||
|
||||
@@ -5407,6 +5407,195 @@ fn fields_by_som_of(mut doc: Document, fields: Vec<Object>) -> std::collections:
|
||||
fields_by_som(&doc)
|
||||
}
|
||||
|
||||
/// An empty multi-line field with a minimum height and no height (a growable table cell, as a
|
||||
/// public form's activity list has) starts at its minimum, not three lines; text it can't hold
|
||||
/// grows it; one without a minimum stays three lines tall.
|
||||
#[test]
|
||||
fn an_empty_multiline_field_with_a_minimum_height_starts_there() {
|
||||
let xml = |cell: &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>
|
||||
<field name="a" w="2in" {cell}><ui><textEdit multiLine="1"/></ui><margin topInset="0.508mm" bottomInset="0.508mm"/></field>
|
||||
</subform></template>"#
|
||||
)
|
||||
};
|
||||
let height = |cell: &str, value: Option<&str>| {
|
||||
let t = crate::parse(&xml(cell)).unwrap().0;
|
||||
let data = value.map(|v| {
|
||||
parse_datasets(&format!(
|
||||
"<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data><form><a>{v}</a></form></xfa:data></xfa:datasets>"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
let form = crate::layout::layout(&t, data.as_ref()).unwrap();
|
||||
widgets(&form.pages[0]).first().map(|w| w.rect.h + 2.0 * 0.508 * 72.0 / 25.4).unwrap()
|
||||
};
|
||||
assert!((height(r#"minH="7.62mm""#, None) - 21.6).abs() < 0.01, "{}", height(r#"minH="7.62mm""#, None));
|
||||
assert!(height(r#"minH="7.62mm""#, Some(&"word ".repeat(80))) > 40.0, "text grows it");
|
||||
assert!(height("", None) > 38.0, "three lines without a minimum: {}", height("", None));
|
||||
}
|
||||
|
||||
/// A growable `lr-tb` subform that doesn't fit what is left of the page is split there (XFA 3.3
|
||||
/// p. 1520), its first rows on this page and the rest on the next, as a public evaluation form's
|
||||
/// sections are; one with a height, a positioned one, still moves whole.
|
||||
#[test]
|
||||
fn a_growable_lr_tb_subform_is_split_at_the_page_break() {
|
||||
let xml = |attrs: &str| {
|
||||
let rows: String = (0..6).map(|i| format!(r#"<field name="f{i}" w="7in" h="1in"/>"#)).collect();
|
||||
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>
|
||||
<field name="tall" w="7in" h="6in"/><subform name="s" w="7.5in" {attrs}>{rows}</subform>
|
||||
</subform></template>"#
|
||||
)
|
||||
};
|
||||
let pages_of = |attrs: &str| {
|
||||
let t = crate::parse(&xml(attrs)).unwrap().0;
|
||||
let form = crate::layout::layout(&t, None).unwrap();
|
||||
(0..6)
|
||||
.map(|i| form.pages.iter().position(|p| widgets(p).iter().any(|w| w.som == format!("form[0].s[0].f{i}[0]"))).unwrap_or(9))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(pages_of(r#"layout="lr-tb""#), [0, 0, 0, 0, 1, 1], "split at the break");
|
||||
assert_eq!(pages_of(r#"layout="lr-tb" h="6in""#), [1, 1, 1, 1, 1, 1], "a fixed height moves whole");
|
||||
}
|
||||
|
||||
/// Where a row group lands against a page break, for each case the splitting rule tells apart:
|
||||
/// a growable `lr-tb`/`rl-tb` one that fits what is left stays whole on this page, one that
|
||||
/// doesn't is split at the break, and one taller than a whole page goes on over pages; one with
|
||||
/// a height, and a positioned one, move whole to the next page when they fit there.
|
||||
#[test]
|
||||
fn a_row_group_against_a_page_break_splits_only_when_growable_and_flowing() {
|
||||
// A 6in field, then the group of `n` 1in rows (7in wide, one to a line) in a 10in area.
|
||||
let pages_of = |attrs: &str, n: usize, positioned: bool| {
|
||||
let rows: String = (0..n)
|
||||
.map(|i| {
|
||||
if positioned {
|
||||
format!(r#"<field name="f{i}" y="{i}in" w="7in" h="1in"/>"#)
|
||||
} else {
|
||||
format!(r#"<field name="f{i}" w="7in" h="1in"/>"#)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let xml = 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>
|
||||
<field name="tall" w="7in" h="6in"/><subform name="s" w="7.5in" {attrs}>{rows}</subform>
|
||||
</subform></template>"#
|
||||
);
|
||||
let t = crate::parse(&xml).unwrap().0;
|
||||
let form = crate::layout::layout(&t, None).unwrap();
|
||||
(0..n)
|
||||
.map(|i| form.pages.iter().position(|p| widgets(p).iter().any(|w| w.som == format!("form[0].s[0].f{i}[0]"))).unwrap_or(99))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// Fits the 4in left: stays on this page, whole.
|
||||
assert_eq!(pages_of(r#"layout="lr-tb""#, 3, false), [0, 0, 0], "a group that fits stays");
|
||||
// Taller than a page (14in): starts here, 4 rows, then 10 on the next page.
|
||||
let mut over = vec![0; 4];
|
||||
over.extend([1; 10]);
|
||||
assert_eq!(pages_of(r#"layout="lr-tb""#, 14, false), over, "a group taller than a page goes on over pages");
|
||||
// Right-to-left rows split at the break as left-to-right ones do.
|
||||
assert_eq!(pages_of(r#"layout="rl-tb""#, 6, false), [0, 0, 0, 0, 1, 1], "rl-tb splits at the break");
|
||||
// A height, or a positioned layout: moved whole when it fits the next page.
|
||||
assert_eq!(pages_of(r#"layout="lr-tb" h="6in""#, 6, false), [1; 6], "a fixed height moves whole");
|
||||
assert_eq!(pages_of("", 6, true), [1; 6], "a positioned group moves whole");
|
||||
}
|
||||
|
||||
/// The rows of a table whose cells are empty multi-line fields with a minimum height and no
|
||||
/// height start at that minimum (XFA 3.3 p. 731), so a table taller than a page puts as many of
|
||||
/// them on it as that height allows, and the rest on the next; cells with a height keep it.
|
||||
#[test]
|
||||
fn table_rows_of_minimum_height_cells_fill_the_page_at_that_height() {
|
||||
let pages = |cell: &str, n: usize| {
|
||||
let rows: String = (0..n)
|
||||
.map(|i| format!(r#"<subform name="r{i}" layout="row"><field name="c" {cell}><ui><textEdit multiLine="1"/></ui></field><field name="d" {cell}><ui><textEdit multiLine="1"/></ui></field></subform>"#))
|
||||
.collect();
|
||||
let xml = 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="t" layout="table" columnWidths="2in 2in" w="4in">{rows}</subform>
|
||||
</subform></template>"#
|
||||
);
|
||||
let t = crate::parse(&xml).unwrap().0;
|
||||
let form = crate::layout::layout(&t, None).unwrap();
|
||||
let per_page: Vec<usize> = form.pages.iter().map(|p| widgets(p).iter().filter(|w| w.som.ends_with(".c[0]")).count()).collect();
|
||||
// Every cell of every row, on every page: a shorter `d` cell, or a shorter row further
|
||||
// down, leaves the counts above as they are.
|
||||
let heights: Vec<f64> =
|
||||
form.pages.iter().flat_map(|p| widgets(p)).filter(|w| w.som.ends_with(".c[0]") || w.som.ends_with(".d[0]")).map(|w| w.rect.h).collect();
|
||||
(per_page, heights)
|
||||
};
|
||||
// 0.4in (28.8pt) rows: 25 to a 10in page. At three lines (38.5pt), 18 would fit.
|
||||
let (per_page, h) = pages(r#"minH="0.4in""#, 30);
|
||||
assert_eq!(per_page, [25, 5], "row heights {h:?}");
|
||||
assert!(h.len() == 60 && h.iter().all(|c| (c - 28.8).abs() < 0.01), "every cell starts at its minimum: {h:?}");
|
||||
// A table that fits keeps all its rows on the page, each at the minimum.
|
||||
let (per_page, h) = pages(r#"minH="0.4in""#, 10);
|
||||
assert_eq!(per_page, [10]);
|
||||
assert!(h.len() == 20 && h.iter().all(|c| (c - 28.8).abs() < 0.01), "every cell starts at its minimum: {h:?}");
|
||||
// Cells with a height: 0.5in rows, 20 to a page.
|
||||
assert_eq!(pages(r#"h="0.5in""#, 30).0, [20, 10]);
|
||||
}
|
||||
|
||||
/// An unnamed growable multi-line field grows to hold its text as a named one does, with a
|
||||
/// minimum height or without. A nameless field takes no part in data binding (XFA 3.3 p. 194),
|
||||
/// so the text it shows, and the layout measures, is the template's.
|
||||
#[test]
|
||||
fn an_unnamed_growable_field_grows_to_hold_its_text() {
|
||||
let lay = |name: &str, min_h: &str| {
|
||||
let body = format!(
|
||||
r#"<subform name="s" layout="tb" w="7.5in"><field {name} w="2in" {min_h}><ui><textEdit multiLine="1"/></ui><value><text>{}</text></value></field><field name="after" w="2in" h="0.3in"/></subform>"#,
|
||||
"word ".repeat(80)
|
||||
);
|
||||
let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
|
||||
let ws = widgets(&form.pages[0]);
|
||||
let field = ws.iter().find(|w| w.name != "after").map(|w| w.rect.h).unwrap();
|
||||
let after = ws.iter().find(|w| w.name == "after").map(|w| w.rect.y).unwrap();
|
||||
(field, after)
|
||||
};
|
||||
for min_h in ["", r#"minH="0.3in""#] {
|
||||
let named = lay(r#"name="a""#, min_h);
|
||||
assert!(named.0 > 100.0, "the named field grows: {named:?}");
|
||||
let unnamed = lay("", min_h);
|
||||
assert!(
|
||||
(unnamed.0 - named.0).abs() < 0.01 && (unnamed.1 - named.1).abs() < 0.01,
|
||||
"{min_h}: unnamed {unnamed:?}, named {named:?} (field height, next field's top)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A growable `lr-tb` subform split at a page break inside a bordered, filled `tb` section breaks
|
||||
/// that section with it, as a break between the section's children does: a closed part on each
|
||||
/// page, its bottom margin ending the first and its top margin starting the second, no warning.
|
||||
#[test]
|
||||
fn a_split_lr_tb_subform_keeps_the_border_of_the_section_round_it() {
|
||||
let rows: String = (0..6).map(|i| format!(r#"<field name="f{i}" w="7in" h="1in"/>"#)).collect();
|
||||
let body = format!(
|
||||
r#"<subform name="box" layout="tb" w="7.5in"><border><edge/><fill><color value="230,230,230"/></fill></border><margin topInset="0.1in" bottomInset="0.2in"/><field name="tall" w="7in" h="6in"/><subform name="s" layout="lr-tb" w="7.5in">{rows}</subform></subform>"#
|
||||
);
|
||||
let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
|
||||
let rows_on: Vec<usize> =
|
||||
(0..6).map(|i| form.pages.iter().position(|p| widgets(p).iter().any(|w| w.name == format!("f{i}"))).unwrap_or(9)).collect();
|
||||
// After 0.1in of margin and the 6in field, three 1in rows fit the 10in area.
|
||||
assert_eq!(rows_on, [0, 0, 0, 1, 1, 1]);
|
||||
let fills = |page: usize| -> Vec<Rect> {
|
||||
form.pages[page].items.iter().filter_map(|i| if let Item::Fill { rect, .. } = i { Some(*rect) } else { None }).collect()
|
||||
};
|
||||
let lines = |page: usize| form.pages[page].items.iter().filter(|i| matches!(i, Item::Line { .. })).count();
|
||||
let (first, second) = (fills(0), fills(1));
|
||||
assert_eq!((first.len(), second.len()), (1, 1), "a part on each page: {:?}", form.warnings);
|
||||
assert_eq!((lines(0), lines(1)), (4, 4), "each part's border is closed");
|
||||
// Page 1: the top margin, the field, three rows, the bottom margin.
|
||||
assert!((first[0].y - 36.0).abs() < 0.01 && (first[0].bottom() - (36.0 + 7.2 + 432.0 + 216.0 + 14.4)).abs() < 0.01, "{:?}", first[0]);
|
||||
// Page 2: the top margin again, the other three rows, the bottom margin.
|
||||
assert!((second[0].y - 36.0).abs() < 0.01 && (second[0].bottom() - (36.0 + 7.2 + 216.0 + 14.4)).abs() < 0.01, "{:?}", second[0]);
|
||||
let f3 = widgets(&form.pages[1]).into_iter().find(|w| w.name == "f3").map(|w| w.rect).unwrap();
|
||||
assert!((f3.y - 43.2).abs() < 0.01, "the second part starts after the top margin: {f3:?}");
|
||||
assert!(form.warnings.is_empty(), "{:?}", form.warnings);
|
||||
}
|
||||
|
||||
/// A form with `draws` in a subform placed by position, on a letter page whose content area is a
|
||||
/// quarter inch in, with `master` on the page itself.
|
||||
fn form_of_draws(master: &str, draws: &str) -> String {
|
||||
|
||||
Reference in new issue
Block a user