Files
pdfcraft/crates/xfa/src/layout.rs
T
Grzegorz MartensandClaude Opus 5.5 2afbf55cd2 M12: an XFA draw placed by position turns about its anchor
Nothing read rotate, so a turned line, picture or label was drawn as if it
stood upright: part of #5561. XFA gives the angle in degrees counter-clockwise
about the anchor, and Acrobat's flattened page for that issue's form puts the
three turned lines exactly there, which the test holds it to.

A turned draw is one item: a page break moves it whole, and what it holds is
written unturned under one matrix, so text, pictures and borders need nothing
of their own. The page count and link annotations look inside it, and its
container grows to the box it stands in. Fields, subforms and draws in a flow
don't turn yet.

That issue's own form is static, so it already shows Acrobat's page: this is
for dynamic forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 02:59:32 +01:00

236 lines
8.6 KiB
Rust

//! The layout engine: places a template's subforms, draws and fields on pages (XFA 3.3 part 2,
//! "Layout"). Supports positioned, top-to-bottom, left-to-right and table/row layouts, page
//! masters with content areas, `breakBefore`/`breakAfter`, and overflow leaders (header rows
//! repeated after a page break). Repeating subforms get their initial instances.
//!
//! Output is a list of pages of resolution-independent items; `pdf` turns them into PDF.
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::Arc;
use base64::Engine;
use crate::XfaError;
use crate::data::{DataNode, data_date_letters, data_path, data_to_iso, date_letters, is_on, iso_to_pattern};
use crate::model::{
Access, Area, Border, Common, Draw, ExclGroup, Field, Font, HAlign, Layout, Margin, Node, Occur, PageArea, PageRole, Para, Placement, Presence,
Rect, Script, SomIndices, Stroke, Subform, Template, Ui, VAlign, Value,
};
use crate::text::{self, Block, Face, Span};
mod common;
mod draw;
mod field;
mod flow;
mod items;
mod measure;
mod pages;
use common::collect_values;
pub(crate) use common::date_pattern;
pub use common::formatted_value;
pub(crate) use items::UNDERLINE_DROP;
pub use items::{Action, BorderShape, Form, Item, Page, Rounded, Turned, Widget, WidgetBorder, WidgetKind};
/// Most pages a form may lay out to.
pub const MAX_PAGES: usize = 500;
/// Most items (text spans, lines, widgets…) across all pages.
const MAX_ITEMS: usize = 400_000;
/// Deepest nesting followed.
const MAX_DEPTH: usize = 64;
/// Most size measurements of one layout. Measurements of containers are memoized, so a real
/// form stays far below this; it bounds the work a hostile template can cause.
const MAX_MEASURES: usize = 2_000_000;
/// Most initial instances of one repeating subform.
const MAX_INSTANCES: usize = 50;
/// Largest picture taken from the data (decoded bytes), as for template pictures.
const MAX_DATA_IMAGE: usize = 32 << 20;
/// Most picture bytes one layout keeps, all copies together. Each laid-out picture holds its
/// own copy, so a large picture in repeating subforms would otherwise multiply into gigabytes
/// (a failed allocation aborts the app); a real form's logos and photos stay far below.
const MAX_IMAGE_BYTES: usize = 256 << 20;
/// Placeholder for the page count in text laid out before the count is known.
const PAGE_COUNT_MARK: char = '\u{E000}';
/// Inherited text defaults and the SOM path of the enclosing containers.
#[derive(Clone)]
struct Ctx {
font: Font,
para: Para,
som: String,
}
impl Ctx {
fn child(&self, c: &Common) -> Ctx {
Ctx { font: self.font.apply(&c.font), para: self.para.apply(&c.para), som: self.som.clone() }
}
/// [`Self::child`] for object `c`, its SOM path extended by its name and `index` among the
/// siblings of that name (an unnamed object adds nothing).
fn with_som(&self, c: &Common, index: usize) -> Ctx {
let mut out = self.child(c);
if let Some(n) = &c.name {
if !out.som.is_empty() {
out.som.push('.');
}
out.som.push_str(&format!("{n}[{index}]"));
}
out
}
}
/// (address of a subform or area in the template, available width bits, depth).
type MeasureKey = (usize, u64, usize);
/// A cached size, with the inherited text defaults it was measured with.
struct Measured {
font: Font,
para: Para,
size: (f64, f64),
}
/// A point in the flow ([`Layouter::flow_mark`]): the page and content area, the height
/// reached there, and how many items the page had then.
#[derive(Clone, Copy, Debug)]
struct FlowMark {
page: usize,
area: usize,
content: usize,
y: f64,
items: usize,
}
/// A flowed subform being laid out ([`Layouter::frames`]): where it sits across, its margins and
/// border, its part on the content area the flow is in, and its parts on earlier ones (page
/// index, the page's first item of the part, the part's box).
struct Frame {
x: f64,
w: f64,
margin: Margin,
border: Option<Border>,
part: FlowMark,
parts: Vec<(usize, usize, Rect)>,
/// Something inside didn't fit its content area: an item cut across areas, or one placed past
/// the area's bottom (a table row taller than an area, an overflow leader and the row after it
/// that don't fit together). The border is left out ([`Layouter::finish_frame`]).
lost: bool,
}
struct Cursor {
area: usize,
content: usize,
rect: Rect,
y: f64,
/// Something from the flow was placed in this content area.
placed: bool,
}
struct Layouter<'t> {
tpl: &'t Template,
data: Option<&'t DataNode>,
/// Shared so a page's master items keep their addresses (the measurement cache keys on them).
areas: Rc<Vec<PageArea>>,
uses: Vec<usize>,
pages: Vec<Page>,
cur: Option<Cursor>,
names: HashMap<String, usize>,
/// Field values by `id`, for embedded fields in rich text.
values: HashMap<String, String>,
warnings: Vec<String>,
warned: HashSet<String>,
fields: usize,
items: usize,
/// Picture bytes laid out so far (see [`MAX_IMAGE_BYTES`]).
image_bytes: usize,
/// Container sizes by (address of the subform or area in the template, available width,
/// depth), with the inherited font and paragraph they were measured with.
measured: RefCell<HashMap<MeasureKey, Measured>>,
/// Measurements made so far (see [`MAX_MEASURES`]).
measures: Cell<usize>,
/// The fields and draws of the block being placed: (first item, one past the last, top,
/// bottom) on the last page. A page break keeps each whole (see `split_block`).
leaves: Vec<(usize, usize, f64, f64)>,
/// The flowed subforms being laid out, outermost first: a break between their children
/// closes their parts on one content area and opens new ones on the next.
frames: Vec<Frame>,
}
/// How many instances of a repeating subform to lay out: what the data holds once it holds
/// any (rows added or removed), else the template's initial count; never below one, never
/// above the maximum.
pub fn instance_count(occur: &Occur, from_data: usize) -> usize {
let n = if from_data > 0 { from_data } else { occur.initial };
n.min(occur.max.unwrap_or(usize::MAX)).clamp(1, MAX_INSTANCES)
}
/// Lay a template out, with the values and repeat counts of `data` (the `xfa:data` element).
pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result<Form, XfaError> {
let areas = match &tpl.root.page_set {
Some(ps) if !ps.areas.is_empty() => ps.areas.clone(),
_ => vec![PageArea {
name: None,
width: 612.0,
height: 792.0,
content: vec![Rect::new(36.0, 36.0, 540.0, 720.0)],
items: Vec::new(),
occur_max: None,
occur_min: 0,
}],
};
let mut values = HashMap::new();
// One copy of the root for the whole layout: measurements are cached by node address.
let root_node = Node::Subform(Box::new(tpl.root.clone()));
collect_values(std::slice::from_ref(&root_node), &mut values, 0);
let mut l = Layouter {
tpl,
data,
uses: vec![0; areas.len()],
areas: Rc::new(areas),
pages: Vec::new(),
cur: None,
names: HashMap::new(),
values,
warnings: Vec::new(),
warned: HashSet::new(),
fields: 0,
items: 0,
image_bytes: 0,
measured: RefCell::new(HashMap::new()),
measures: Cell::new(0),
leaves: Vec::new(),
frames: Vec::new(),
};
let ctx = Ctx { font: Font::default(), para: Para::default(), som: String::new() };
l.new_page()?;
if tpl.root.common.presence.occupies() {
l.flow_subform(&root_node, None, &ctx, 0, 0)?;
}
l.satisfy_page_area_minimums()?;
l.check_measures()?;
let total = l.pages.len();
let count = total.to_string();
for p in &mut l.pages {
fill_page_count(&mut p.items, &count);
}
Ok(Form { pages: l.pages, fields: l.fields, warnings: l.warnings })
}
/// Puts the page count, known once every page is, where `items` left room for it.
fn fill_page_count(items: &mut [Item], count: &str) {
for it in items {
match it {
Item::Text(s) if s.text.contains(PAGE_COUNT_MARK) => s.text = s.text.replace(PAGE_COUNT_MARK, count),
Item::Turned(t) => fill_page_count(&mut t.items, count),
_ => {}
}
}
}
// Bytes of embedded field values copied out on this thread, for tests that bound layout work.
#[cfg(test)]
thread_local! {
pub(crate) static EMBED_COPIED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}