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
This commit is contained in:
Grzegorz MartensandClaude Opus 5.5 committed 2026-11-01 02:59:32 +01:00
1 parent 13655d0992
commit 2afbf55cd2
11 files changed
+516 -21

No files matched your search

+4 -1
View File
@@ -32,7 +32,10 @@ that warning and keeps the existing layout and accepted widget values.
page masters chosen by occurrence; a table's header row repeats after a break; presence
`hidden` takes no space, `invisible` takes space; repeating subforms get their initial
instances, in flowed content and in a top-to-bottom container inside positioned content (a
table in an area) alike; fields that compute the page number or count on layout show the numbers.
table in an area) alike; fields that compute the page number or count on layout show the numbers;
a draw placed by position turns about its anchor (`rotate`: degrees counter-clockwise, a multiple
of 90), as one item a page break doesn't cut, and its container grows to the box it stands in. A
field, a subform or a draw in a flow doesn't turn.
- **PDF** (`pdf::write_form`): pages with content streams in the standard 14 fonts (Arial and
friends → Helvetica, Courier New → Courier, serif faces → Times; nothing is embedded), JPEG
XObjects, and widgets: text (multi-line, max length, alignment), date (Acrobat's `AFDate`
+13 -8
View File
@@ -32,7 +32,7 @@ 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, Widget, WidgetBorder, WidgetKind};
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;
@@ -212,17 +212,22 @@ pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result<Form, XfaError>
let total = l.pages.len();
let count = total.to_string();
for p in &mut l.pages {
for it in &mut p.items {
if let Item::Text(s) = it
&& s.text.contains(PAGE_COUNT_MARK)
{
s.text = s.text.replace(PAGE_COUNT_MARK, &count);
}
}
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! {
+6
View File
@@ -51,6 +51,7 @@ pub(super) fn solid_extent(item: &Item) -> Option<(f64, f64)> {
Item::Text(s) => Some((s.baseline - s.face.size * 0.8, s.baseline + s.face.size * 0.2)),
Item::Image { rect, .. } => Some((rect.y, rect.bottom())),
Item::Widget(w) => Some((w.rect.y, w.rect.bottom())),
Item::Turned(t) => Some(t.extent),
Item::Fill { .. } | Item::Line { .. } | Item::Rounded(_) => None,
}
}
@@ -130,6 +131,11 @@ pub(super) fn moved(mut item: Item, dx: f64, dy: f64) -> Item {
w.rect.x += dx;
w.rect.y += dy;
}
Item::Turned(t) => {
t.about = (t.about.0 + dx, t.about.1 + dy);
t.extent = (t.extent.0 + dy, t.extent.1 + dy);
t.items = std::mem::take(&mut t.items).into_iter().map(|i| moved(i, dx, dy)).collect();
}
}
item
}
+22 -2
View File
@@ -1,5 +1,5 @@
use super::common::clean_name;
use super::items::{Item, Rounded};
use super::items::{Item, Rounded, Turned};
use super::{
Arc, Border, Ctx, Draw, ExclGroup, Face, Font, Layout, Layouter, MAX_DEPTH, MAX_IMAGE_BYTES, Node, Para, Presence, Rect, SomIndices, Stroke,
Subform, Value, XfaError, data_path, text,
@@ -35,7 +35,27 @@ impl Layouter<'_> {
}
let (w, h) = self.size_of(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
let (x, y) = c.top_left(w, h);
self.draw_node(k, Rect::new(inner.x + x, inner.y + y, w, h), ctx, depth + 1, sib)
let rect = Rect::new(inner.x + x, inner.y + y, w, h);
let start = self.item_count();
self.draw_node(k, rect, ctx, depth + 1, sib)?;
if c.rotate != 0 && matches!(k, Node::Draw(_)) {
let stands = k.standing_box(w, h);
let extent = (inner.y + stands.y, inner.y + stands.bottom());
self.turn(start, (inner.x + c.x.unwrap_or(0.0), inner.y + c.y.unwrap_or(0.0)), c.rotate, extent);
}
Ok(())
}
/// Turn the items the last page got since `start`, a draw, about `about`.
fn turn(&mut self, start: usize, about: (f64, f64), quarters: u8, extent: (f64, f64)) {
let Some(page) = self.pages.last_mut().filter(|p| start < p.items.len()) else { return };
let items = page.items.split_off(start);
page.items.push(Item::Turned(Box::new(Turned { about, quarters, extent, items })));
// One item now, kept whole by what it is: the record of its items, the last one kept,
// would fall on the next.
if self.leaves.last().is_some_and(|leaf| leaf.0 == start) {
self.leaves.pop();
}
}
pub(super) fn draw_node(&mut self, node: &Node, rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
+16
View File
@@ -27,6 +27,22 @@ pub enum Item {
content_type: String,
},
Widget(Box<Widget>),
/// A draw turned about its anchor.
Turned(Box<Turned>),
}
/// A draw turned about its anchor ([`Item::Turned`]). It stays one item, so a page break doesn't
/// cut it, and what it holds is written unturned under one matrix.
#[derive(Clone, Debug, PartialEq)]
pub struct Turned {
/// The point it turns about.
pub about: (f64, f64),
/// Quarter turns counter-clockwise, 1 to 3.
pub quarters: u8,
/// Top and bottom of its box once turned.
pub extent: (f64, f64),
/// The draw as it stands before turning.
pub items: Vec<Item>,
}
/// A box with rounded corners ([`Item::Rounded`]).
+2 -2
View File
@@ -141,7 +141,7 @@ impl Layouter<'_> {
Layout::Positioned => visible
.map(|k| {
let (w, h) = self.size_of(k, avail_w, ctx, depth + 1);
k.common().top_left(w, h).0 + w
k.standing_box(w, h).right()
})
.fold(0.0, f64::max)
.min(avail_w),
@@ -297,7 +297,7 @@ impl Layouter<'_> {
.map(|k| {
let c = k.common();
let (w, h) = self.size_of(k, (inner_w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
c.top_left(w, h).1 + h
k.standing_box(w, h).bottom()
})
.fold(0.0, f64::max)
}
+23
View File
@@ -334,6 +334,9 @@ pub struct Common {
pub x: Option<f64>,
pub y: Option<f64>,
pub anchor: Anchor,
/// Quarter turns counter-clockwise about the anchor (`rotate`), 0 to 3. Only a draw placed by
/// position is turned.
pub rotate: u8,
pub w: Option<f64>,
pub h: Option<f64>,
pub min_w: Option<f64>,
@@ -366,6 +369,26 @@ impl Common {
}
impl Node {
/// The box a positioned object of `w` by `h` stands in, in its parent: from `x`, `y` and the
/// anchor, and for a draw, the only object that turns, turned about the anchor.
pub fn standing_box(&self, w: f64, h: f64) -> Rect {
let c = self.common();
let (x, y) = c.top_left(w, h);
let upright = Rect::new(x, y, w, h);
if !matches!(self, Node::Draw(_)) {
return upright;
}
let (ax, ay) = (c.x.unwrap_or(0.0), c.y.unwrap_or(0.0));
let (left, top, right, bottom) = (x - ax, y - ay, x + w - ax, y + h - ay);
// Counter-clockwise on a page whose y grows down, from the anchor.
match c.rotate {
1 => Rect::new(ax + top, ay - right, h, w),
2 => Rect::new(ax - right, ay - bottom, w, h),
3 => Rect::new(ax - bottom, ay + left, h, w),
_ => upright,
}
}
pub fn common(&self) -> &Common {
match self {
Node::Subform(s) => &s.common,
+11
View File
@@ -198,6 +198,16 @@ fn parse_para(n: Option<XmlNode>) -> ParaSpec {
}
}
/// `rotate` as quarter turns (XFA 3.3: degrees counter-clockwise, a multiple of 90, `deg` optional):
/// anything else leaves the object as it stands.
fn quarter_turns(rotate: Option<&str>) -> u8 {
let degrees = rotate.map(str::trim).map(|v| v.strip_suffix("deg").unwrap_or(v).trim_end());
let turns = degrees.and_then(|v| v.parse::<f64>().ok()).map(|v| v.rem_euclid(360.0) / 90.0);
// Whole, which NaN and infinities aren't, and in range, so the cast is exact; a tiny negative
// angle rounds to a full turn.
turns.filter(|t| t.fract() == 0.0).map_or(0, |t| t as u8 % 4)
}
fn parse_common(n: XmlNode) -> Common {
Common {
name: n.attribute("name").map(str::to_string).filter(|s| !s.is_empty()),
@@ -205,6 +215,7 @@ fn parse_common(n: XmlNode) -> Common {
x: attr_measure(n, "x"),
y: attr_measure(n, "y"),
anchor: Anchor::parse(n.attribute("anchorType")),
rotate: quarter_turns(n.attribute("rotate")),
w: attr_measure(n, "w"),
h: attr_measure(n, "h"),
min_w: attr_measure(n, "minW"),
+78 -6
View File
@@ -8,7 +8,7 @@ use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream};
use pdfcraft_fonts::{literal, win_ansi};
use crate::XfaError;
use crate::layout::{Action, BorderShape, Form, Item, Page, Rounded, Widget, WidgetKind};
use crate::layout::{Action, BorderShape, Form, Item, Page, Rounded, Turned, Widget, WidgetKind};
use crate::model::Rect;
use crate::text::{Face, Family, Span};
@@ -567,12 +567,30 @@ impl Emitter<'_> {
let h = page.height;
// The link the text before ended in, which a next piece of it on the line widens.
let mut last_link: Option<LinkPiece> = None;
for (i, item) in page.items.iter().enumerate() {
let steps = steps(&page.items);
let mut turn = None;
for (i, step) in steps.iter().enumerate() {
let item = match step {
Step::Item(item) => *item,
Step::Turn(t) => {
let matrix = turn_matrix(t, h);
turn = Some(matrix);
last_link = None;
content.extend(format!("q {} cm\n", matrix.map(n).join(" ")).bytes());
continue;
}
Step::EndTurn => {
turn = None;
last_link = None;
content.extend_from_slice(b"Q\n");
continue;
}
};
// A link's pieces join only across text and the underline drawn just before it: a fill,
// picture, field or any other line (a rule) between them ends the link there.
let keeps_link = match item {
Item::Text(_) => true,
Item::Line { from, to, .. } => matches!(page.items.get(i + 1), Some(Item::Text(s))
Item::Line { from, to, .. } => matches!(steps.get(i + 1), Some(Step::Item(Item::Text(s)))
if (from.1 - to.1).abs() < 0.01
&& (from.0 - s.x).abs() < 0.01
&& (from.1 - (s.baseline + s.face.size * crate::layout::UNDERLINE_DROP)).abs() < 0.01),
@@ -627,7 +645,7 @@ impl Emitter<'_> {
);
content.extend(literal(&win_ansi(&s.text)));
content.extend_from_slice(format!(" Tj{close}\n").as_bytes());
if let Some(r) = self.text_link(s, h, page_ref, &mut last_link)? {
if let Some(r) = self.text_link(s, h, page_ref, &mut last_link, turn)? {
annots.push(Object::Ref(r));
}
}
@@ -637,6 +655,8 @@ impl Emitter<'_> {
xobjects.set(name.clone().into_bytes(), Object::Ref(r));
content.extend(format!("q {} 0 0 {} {} {} cm /{name} Do Q\n", n(rect.w), n(rect.h), n(rect.x), n(h - rect.bottom())).bytes());
}
// Opened out by `steps`.
Item::Turned(_) => {}
Item::Widget(w) => {
if let Some(r) = self.widget(w, page, page_ref)? {
annots.push(Object::Ref(r));
@@ -675,12 +695,25 @@ impl Emitter<'_> {
/// A span that goes on the link `last` ended in, on the same line, widens that annotation
/// instead (a link whose text changes style partway is one link). An address PdfCraft
/// wouldn't open from a document leaves the text as plain text, with one warning for the form.
fn text_link(&mut self, s: &Span, page_height: f64, page_ref: ObjRef, last: &mut Option<LinkPiece>) -> Result<Option<ObjRef>, XfaError> {
fn text_link(
&mut self,
s: &Span,
page_height: f64,
page_ref: ObjRef,
last: &mut Option<LinkPiece>,
turn: Option<[f64; 6]>,
) -> Result<Option<ObjRef>, XfaError> {
let piece = self.link_piece(s, page_height);
let Some((target, rect)) = piece else {
let Some((target, mut rect)) = piece else {
*last = None;
return Ok(None);
};
// Turned text is drawn by a matrix, and clicked where that puts it. Its pieces don't join:
// that goes by the line they stand on before turning.
if let Some(m) = turn {
rect = turned_rect(rect, m);
*last = None;
}
if let Some(prev) = last.as_mut()
&& prev.uri == target
&& (prev.baseline - s.baseline).abs() < 0.01
@@ -765,6 +798,45 @@ fn write_button_action(action: Option<&Action>, d: &mut Dict) {
}
}
/// What a page's content is written from: its items, with a turned draw opened out.
enum Step<'a> {
Item(&'a Item),
Turn(&'a Turned),
EndTurn,
}
fn steps(items: &[Item]) -> Vec<Step<'_>> {
let mut out = Vec::with_capacity(items.len());
for item in items {
if let Item::Turned(t) = item {
out.push(Step::Turn(t));
out.extend(t.items.iter().map(Step::Item));
out.push(Step::EndTurn);
} else {
out.push(Step::Item(item));
}
}
out
}
/// The matrix that turns `t` about its point, on a page `page_height` tall.
fn turn_matrix(t: &Turned, page_height: f64) -> [f64; 6] {
let (cos, sin) = match t.quarters {
1 => (0.0, 1.0),
2 => (-1.0, 0.0),
_ => (0.0, -1.0),
};
let (x, y) = (t.about.0, page_height - t.about.1);
[cos, sin, -sin, cos, x - cos * x + sin * y, y - sin * x - cos * y]
}
/// Rectangle `r`, `[left, bottom, right, top]`, under quarter-turn matrix `m`.
fn turned_rect(r: [f64; 4], m: [f64; 6]) -> [f64; 4] {
let at = |x: f64, y: f64| (m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]);
let (a, b) = (at(r[0], r[1]), at(r[2], r[3]));
[a.0.min(b.0), a.1.min(b.1), a.0.max(b.0), a.1.max(b.1)]
}
/// The link annotation a page's text last ended in: the next piece of the same link on the same
/// line widens it.
struct LinkPiece {
+339
View File
@@ -4145,3 +4145,342 @@ fn clear_password_values_clears_static_xfa_password_fields() {
let packets = read_packets(&doc).unwrap().unwrap();
assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp);
}
/// 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 {
format!(
r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb"><pageSet><pageArea name="p">{master}<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/><medium stock="letter" short="8.5in" long="11in"/></pageArea></pageSet><subform name="s" w="8in" h="2.5in">{draws}</subform></subform></template></xdp:xdp>"#
)
}
/// A line from the top-left to the bottom-right of a box 0.7in × 0.4in, as #5561's form draws.
fn line_draw(x: &str, y: &str, rotate: &str) -> String {
format!(r#"<draw x="{x}" y="{y}" w="0.7in" h="0.4in" {rotate}><value><line><edge thickness="2.5pt"/></line></value></draw>"#)
}
/// `xdp` laid out into its document, and that document's first page.
fn written(xdp: &str) -> (Document, Dict) {
let mut doc = Document::open(Arc::new(shell(xdp))).unwrap();
render_into(&mut doc).unwrap();
let root = doc.get(doc.root().unwrap());
let pages = doc.get(root.as_dict().unwrap().reference(b"Pages").unwrap());
let kids = pages.as_dict().unwrap().get(b"Kids").map(|k| doc.resolve(k)).unwrap();
let page = doc.get(kids.as_array().unwrap()[0].as_ref().unwrap());
let page = page.as_dict().unwrap().clone();
(doc, page)
}
fn page_content(doc: &Document, page: &Dict) -> String {
let contents = doc.get(page.reference(b"Contents").unwrap());
let Object::Stream(s) = &*contents else { panic!("not a stream") };
String::from_utf8_lossy(&s.decoded().unwrap()).into_owned()
}
/// The ends of each line `content` strokes, on the page: through the matrices around it.
fn stroked_lines(content: &str) -> Vec<[(f64, f64); 2]> {
let (mut stack, mut matrix) = (Vec::new(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
let (mut numbers, mut from, mut lines): (Vec<f64>, _, _) = (Vec::new(), (0.0, 0.0), Vec::new());
for token in content.split_whitespace() {
let at = |m: [f64; 6], x: f64, y: f64| (m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]);
let last = |numbers: &[f64]| (numbers[numbers.len() - 2], numbers[numbers.len() - 1]);
match token {
"q" => stack.push(matrix),
"Q" => matrix = stack.pop().unwrap(),
"cm" => {
let n: [f64; 6] = numbers[numbers.len() - 6..].try_into().unwrap();
let m = matrix;
matrix = [
n[0] * m[0] + n[1] * m[2],
n[0] * m[1] + n[1] * m[3],
n[2] * m[0] + n[3] * m[2],
n[2] * m[1] + n[3] * m[3],
n[4] * m[0] + n[5] * m[2] + m[4],
n[4] * m[1] + n[5] * m[3] + m[5],
];
}
"m" => {
let (x, y) = last(&numbers);
from = at(matrix, x, y);
}
"l" => {
let (x, y) = last(&numbers);
lines.push([from, at(matrix, x, y)]);
}
_ => {}
}
match token.parse() {
Ok(v) => numbers.push(v),
Err(_) => numbers.clear(),
}
}
lines
}
fn turned(page: &Page) -> Vec<&crate::layout::Turned> {
page.items.iter().filter_map(|i| if let Item::Turned(t) = i { Some(t.as_ref()) } else { None }).collect()
}
/// A draw turns counter-clockwise about its anchor. The figures are where Acrobat put these lines
/// on the page it flattened for #5561's form: from the anchor by (+50.4, -28.8) unturned, and by
/// (+28.8, +50.4), (-50.4, +28.8) and (-28.8, -50.4) at 90, 180 and 270 degrees.
#[test]
fn a_draw_turns_counter_clockwise_about_its_anchor() {
let draws = [
line_draw("1in", "0.95in", ""),
line_draw("3in", "0.95in", r#"rotate="90""#),
line_draw("5in", "0.95in", r#"rotate="180""#),
line_draw("7in", "0.95in", r#"rotate="270""#),
]
.concat();
let (doc, page) = written(&form_of_draws("", &draws));
let lines = stroked_lines(&page_content(&doc, &page));
assert_eq!(lines.len(), 4, "{lines:?}");
let top = 792.0 - 18.0 - 0.95 * 72.0;
let acrobat = [((90.0, top), (50.4, -28.8)), ((234.0, top), (28.8, 50.4)), ((378.0, top), (-50.4, 28.8)), ((522.0, top), (-28.8, -50.4))];
for ([from, to], (anchor, by)) in lines.iter().zip(acrobat) {
assert!((from.0 - anchor.0).abs() < 0.01 && (from.1 - anchor.1).abs() < 0.01, "{from:?} for {anchor:?}");
let moved = (to.0 - from.0, to.1 - from.1);
assert!((moved.0 - by.0).abs() < 0.01 && (moved.1 - by.1).abs() < 0.01, "{moved:?} for {by:?}");
}
}
/// The anchor is the point it turns about, whichever corner that is: a box anchored at its bottom
/// right stands up and to the left of it, not where a top-left anchor would turn it.
#[test]
fn a_draw_turns_about_the_anchor_it_names() {
let draw = r#"<draw x="3in" y="1in" w="0.7in" h="0.4in" anchorType="bottomRight" rotate="90"><value><line><edge thickness="1pt"/></line></value></draw>"#;
let (doc, page) = written(&form_of_draws("", draw));
let lines = stroked_lines(&page_content(&doc, &page));
let anchor = (18.0 + 216.0, 792.0 - 18.0 - 72.0);
// Its bottom right is 0.7in right and 0.4in down of its top left, here turned to 0.7in up and
// 0.4in right: so the top left is that far back from the anchor, and the line ends on it.
let [from, to] = lines[0];
assert!((to.0 - anchor.0).abs() < 0.01 && (to.1 - anchor.1).abs() < 0.01, "{to:?} for {anchor:?}");
assert!((from.0 - (anchor.0 - 28.8)).abs() < 0.01 && (from.1 - (anchor.1 - 50.4)).abs() < 0.01, "{from:?}");
}
/// Everything a draw shows turns with it, under one matrix: its text, its picture and its border,
/// rounded or not, each written as if it stood upright.
#[test]
fn text_pictures_and_borders_turn_with_their_draw() {
let picture = format!(
r#"<draw x="1in" y="1in" w="1in" h="1in" rotate="90"><value><image contentType="image/png">{}</image></value></draw>"#,
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, tiny_png())
);
for (draw, shows) in [
(r#"<draw x="1in" y="1in" w="2in" h="0.5in" rotate="90"><value><text>upright</text></value></draw>"#.to_string(), "(upright) Tj"),
(picture, " Do "),
(r#"<draw x="1in" y="1in" w="2in" h="0.5in" rotate="90"><value><rectangle><edge thickness="1pt"/></rectangle></value></draw>"#.to_string(), " l S"),
(
r#"<draw x="1in" y="1in" w="2in" h="0.5in" rotate="90"><value><rectangle><edge thickness="1pt"/><corner radius="5pt"/></rectangle></value></draw>"#.to_string(),
" c",
),
(r#"<draw x="1in" y="1in" w="2in" h="0.5in" rotate="90"><border><fill><color value="255,0,0"/></fill></border><value><text>x</text></value></draw>"#.to_string(), " re f"),
] {
let (doc, page) = written(&form_of_draws("", &draw));
let content = page_content(&doc, &page);
let open = content.find("0 1 -1 0 ").unwrap_or_else(|| panic!("no matrix for {draw}: {content}"));
let close = open + content[open..].find("\nQ\n").unwrap_or_else(|| panic!("no end for {draw}: {content}"));
assert!(content[open..close].contains(shows), "{shows} outside the matrix: {content}");
}
}
/// `rotate` as a multiple of 90 degrees, with a unit or not, past a full turn or below none;
/// anything else leaves the draw as it stands.
#[test]
fn rotate_reads_as_quarter_turns() {
let quarters = |rotate: &str| {
let form = layout_xml(&form_of_draws("", &line_draw("1in", "1in", &format!(r#"rotate="{rotate}""#)))).unwrap();
turned(&form.pages[0]).first().map_or(0, |t| t.quarters)
};
for (rotate, want) in [("90", 1), ("180", 2), ("270", 3), ("0", 0), ("360", 0), ("450", 1), ("-90", 3), ("90deg", 1), (" 90 ", 1), ("90.0", 1)] {
assert_eq!(quarters(rotate), want, "{rotate}");
}
for rotate in ["45", "89.5", "135", "225", "abc", "", "deg", "NaN", "inf", "-inf"] {
assert_eq!(quarters(rotate), 0, "{rotate}");
}
// Far past what a turn can be told apart at, or just under none: in range whatever it reads as.
for rotate in ["1e300", "-1e-300"] {
assert!(quarters(rotate) < 4, "{rotate}");
}
}
/// A turned draw is one item: a page break moves it whole, what it holds moving with the point it
/// turns about, and doesn't cut it, and the draws around it stay where they belong.
#[test]
fn a_page_break_moves_a_turned_draw_whole() {
let draws = format!(
r#"<draw x="6in" y="10.4in" w="1in" h="0.5in"><border><edge thickness="1pt"/></border><value><text>across</text></value></draw><draw x="1in" y="1in" w="2in" h="0.5in"><value><text>first</text></value></draw>{}{}<draw x="1in" y="13in" w="2in" h="0.5in"><value><text>last</text></value></draw>"#,
[
line_draw("3in", "12in", r#"rotate="90""#),
r#"<draw x="5in" y="12in" w="2in" h="0.5in" rotate="90"><border><edge thickness="1pt"/></border><value><text>sideways</text></value></draw>"#.to_string(),
]
.concat(),
(2..8).map(|y| line_draw("1in", &format!("{y}in"), "")).collect::<String>()
);
let xdp = form_of_draws("", &draws).replace(r#"h="2.5in""#, r#"h="14in""#);
let form = layout_xml(&xdp).unwrap();
assert!(form.pages.len() >= 2, "{}", form.pages.len());
assert_eq!(texts(&form.pages[0]), ["first"]);
// Written after the turned draws, these lines still belong at the top of the block: what the
// break knew of the draw's many items mustn't carry over to those that now stand in their places.
let lines = form.pages[0].items.iter().filter(|i| matches!(i, Item::Line { .. })).count();
assert_eq!(lines, 6, "{:?}", form.pages[0].items);
assert_eq!(turned(&form.pages[0]).len(), 0);
let moved = turned(&form.pages[1]);
assert_eq!(moved.len(), 2, "{:?}", form.pages[1].items);
let Item::Line { from, .. } = &moved[0].items[0] else { panic!("not a line") };
assert_eq!(*from, moved[0].about, "the line moved with the point it turns about");
// Turned 90 degrees, a box 0.7in wide stands 0.7in up from its anchor, on a second page.
assert!(moved[0].about.1 < 792.0, "{:?}", moved[0].about);
assert!((moved[0].extent.1 - moved[0].about.1).abs() < 0.01, "{:?}", moved[0]);
assert!((moved[0].about.1 - moved[0].extent.0 - 50.4).abs() < 0.01, "{:?}", moved[0]);
let moved_text = texts(&form.pages[1]);
assert!(moved_text.contains(&"last".to_string()) && moved_text.contains(&"across".to_string()), "{moved_text:?}");
}
/// What a page break goes by is the box as it stands once turned, whichever way, about whichever
/// corner: from a top-left anchor, it stands up, up or down of it; from a bottom-right one, the
/// box starts behind the anchor and the same turns take it the other way.
#[test]
fn a_turned_draw_knows_where_it_stands() {
let box_at = |anchor: &str, rotate: u32| {
let draw = format!(
r#"<draw x="3in" y="1in" w="0.7in" h="0.4in" {anchor} rotate="{rotate}"><value><line><edge thickness="1pt"/></line></value></draw>"#
);
let form = layout_xml(&form_of_draws("", &draw)).unwrap();
let held = turned(&form.pages[0]);
let t = held.first().expect("turned");
(t.extent.0 - t.about.1, t.extent.1 - t.about.1)
};
let close = |got: (f64, f64), want: (f64, f64)| (got.0 - want.0).abs() < 0.01 && (got.1 - want.1).abs() < 0.01;
for (rotate, want) in [(90, (-50.4, 0.0)), (180, (-28.8, 0.0)), (270, (0.0, 50.4))] {
assert!(close(box_at("", rotate), want), "{rotate}: {:?}", box_at("", rotate));
}
for (rotate, want) in [(90, (0.0, 50.4)), (180, (0.0, 28.8)), (270, (-50.4, 0.0))] {
let got = box_at(r#"anchorType="bottomRight""#, rotate);
assert!(close(got, want), "{rotate}: {got:?}");
}
}
/// The page count is filled in inside a turned draw too, as it comes last, when every page is known.
#[test]
fn the_page_count_reaches_a_turned_draw() {
let master = r##"<draw x="7in" y="10in" w="1in" h="0.2in" rotate="90"><value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p>of <span xfa:embed="#pageCount" xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"/></p></body></exData></value></draw><field name="pageCount" id="pageCount" presence="hidden" w="1in" h="0.2in"><ui><textEdit/></ui><event activity="ready" ref="$layout"><script contentType="application/x-javascript">this.rawValue = xfa.layout.pageCount();</script></event></field>"##;
let form = layout_xml(&form_of_draws(master, &line_draw("1in", "1in", ""))).unwrap();
let held = turned(&form.pages[0]);
assert_eq!(held.len(), 1, "{:?}", form.pages[0].items);
let text: Vec<&str> = held[0].items.iter().filter_map(|i| if let Item::Text(s) = i { Some(s.text.as_str()) } else { None }).collect();
assert_eq!(text.concat().trim(), "of 1", "{text:?}");
}
/// A link in turned text is clicked where the text is drawn, whichever way it turned: the box it
/// has unturned, turned about the draw's anchor.
#[test]
fn a_link_turns_with_its_text() {
let rect_at = |rotate: &str| {
let draw = format!(
r#"<draw x="1in" y="3in" w="2in" h="0.5in" {rotate}><value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p><a href="https://example.com/a">a link in turned text</a></p></body></exData></value></draw>"#
);
let (doc, page) = written(&form_of_draws("", &draw));
let annots = page.get(b"Annots").map(|a| doc.resolve(a)).expect("the link");
let annot = doc.get(annots.as_array().unwrap()[0].as_ref().unwrap());
let rect: Vec<f64> = annot.as_dict().unwrap().get(b"Rect").unwrap().as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
rect
};
let upright = rect_at("");
let (ax, ay) = (18.0 + 72.0, 792.0 - 18.0 - 216.0);
let (left, bottom, right, top) = (upright[0] - ax, upright[1] - ay, upright[2] - ax, upright[3] - ay);
assert!(left >= -0.01 && top <= 0.01 && right - left > 3.0 * (top - bottom), "it stands right and down of its anchor: {upright:?}");
for (rotate, want) in [
("90", [ax - top, ay + left, ax - bottom, ay + right]),
("180", [ax - right, ay - top, ax - left, ay - bottom]),
("270", [ax + bottom, ay - right, ax + top, ay - left]),
] {
let got = rect_at(&format!(r#"rotate="{rotate}""#));
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 0.01), "{rotate}: {got:?} for {want:?}");
}
}
/// Only a draw placed by position turns: a field, and a draw in a flow, stand as they did.
#[test]
fn only_positioned_draws_turn() {
let field = r#"<field name="f" x="1in" y="1in" w="2in" h="0.5in" rotate="90"><ui><textEdit/></ui></field>"#;
let form = layout_xml(&form_of_draws("", field)).unwrap();
assert!(turned(&form.pages[0]).is_empty());
assert_eq!(widgets(&form.pages[0]).len(), 1);
let flowed = form_of_draws("", &line_draw("1in", "1in", r#"rotate="90""#))
.replace(r#"<subform name="s" w="8in" h="2.5in">"#, r#"<subform name="s" w="8in" layout="tb">"#);
let form = layout_xml(&flowed).unwrap();
assert!(turned(&form.pages[0]).is_empty(), "{:?}", form.pages[0].items);
assert!(form.pages[0].items.iter().any(|i| matches!(i, Item::Line { .. })), "it is still drawn");
// A draw that shows nothing has nothing to turn.
let form = layout_xml(&form_of_draws("", &line_draw("1in", "1in", r#"rotate="90" presence="invisible""#))).unwrap();
assert!(form.pages[0].items.is_empty(), "{:?}", form.pages[0].items);
}
/// A block carried over to a content area further across takes a turned draw with it, point and
/// items both.
#[test]
fn a_turned_draw_moves_across_with_its_content_area() {
let xdp = form_of_draws("", &line_draw("1in", "6in", r#"rotate="90""#))
.replace(
r#"<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>"#,
r#"<contentArea x="0.25in" y="0.25in" w="3in" h="5in"/><contentArea x="4in" y="0.25in" w="3in" h="5in"/>"#,
)
.replace(r#"h="2.5in""#, r#"h="7in""#);
let form = layout_xml(&xdp).unwrap();
let moved = form.pages.iter().flat_map(turned).next().expect("turned");
let Item::Line { from, .. } = &moved.items[0] else { panic!("not a line") };
assert_eq!(*from, moved.about);
assert!((moved.about.0 - (4.0 + 1.0) * 72.0).abs() < 0.01, "{:?}", moved.about);
}
/// A positioned subform with no height grows to hold what it holds as it stands, so what comes next
/// neither runs through a turned draw nor leaves the room its upright box would have had.
#[test]
fn a_container_grows_to_a_turned_draw_as_it_stands() {
let next = r#"</subform><subform name="next" w="8in" h="1in"><draw x="0in" y="0in" w="2in" h="0.5in"><value><text>next</text></value></draw></subform></subform></template>"#;
let baseline = |rotate: &str| {
let xdp = form_of_draws("", &line_draw("1in", "1in", rotate)).replace(r#" h="2.5in""#, "").replace("</subform></subform></template>", next);
let form = layout_xml(&xdp).unwrap();
assert_eq!(form.pages.len(), 1);
form.pages[0].items.iter().find_map(|i| if let Item::Text(s) = i { Some(s.baseline) } else { None }).expect("the next block")
};
// Upright the box ends 0.4in below the anchor, and the next block starts there.
let upright = baseline("");
for (rotate, bottom) in [("90", 0.0), ("180", 0.0), ("270", 50.4)] {
let got = baseline(&format!(r#"rotate="{rotate}""#)) - (upright - 28.8);
assert!((got - bottom).abs() < 0.01, "{rotate}: the next block starts {got} below the anchor, not {bottom}");
}
}
/// A subform with no width is as wide as what it holds as it stands, whichever way that turned,
/// so a neighbour in a row doesn't stand off from it or over it.
#[test]
fn a_container_is_as_wide_as_a_turned_draw_stands() {
let row = |rotate: &str| {
let xdp = form_of_draws(
"",
&format!(
r#"<subform name="turned">{}</subform><subform name="beside" w="1in" h="1in"><draw x="0in" y="0in" w="1in" h="0.5in"><value><text>beside</text></value></draw></subform>"#,
line_draw("1in", "1in", rotate)
),
)
.replace(r#"<subform name="s" w="8in" h="2.5in">"#, r#"<subform name="s" w="8in" h="2.5in" layout="lr-tb">"#);
let form = layout_xml(&xdp).unwrap();
form.pages[0].items.iter().find_map(|i| if let Item::Text(s) = i { Some(s.x) } else { None }).expect("the neighbour")
};
let upright = row("");
// Upright it ends 0.7in right of the anchor; turned, 0.4in right, at the anchor, and at it again.
for (rotate, right) in [("90", 28.8), ("180", 0.0), ("270", 0.0)] {
let got = row(&format!(r#"rotate="{rotate}""#)) - (upright - 50.4);
assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}");
}
// From a bottom-right anchor the box starts behind it, and the same turns take it the other way.
for (rotate, right) in [("90", 0.0), ("180", 50.4), ("270", 28.8)] {
let got = row(&format!(r#"anchorType="bottomRight" rotate="{rotate}""#)) - (upright - 50.4);
assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}");
}
}
+2 -2
View File
@@ -5320,8 +5320,8 @@ milestone = "M12"
status = "partial"
commands = ["file.open"]
tools = ["doc_open", "doc_info", "form_fields", "form_fill", "js_run"]
evidence = ["crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/it/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/it/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/engine/src/tests.rs::xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it", "crates/automation/tests/it/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/it/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/ui-egui/tests/it/xfa.rs::a_form_whose_scripts_were_turned_off_at_open_says_so", "crates/ui-egui/tests/it/xfa.rs::scripts_turned_off_by_a_click_bring_a_closed_notice_bar_back", "crates/automation/tests/it/automation.rs::doc_info_says_when_a_runaway_script_turned_the_forms_scripts_off", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite", "crates/xfa/src/tests.rs::choice_lists_signatures_passwords_and_pictures_become_fields_and_images", "crates/xfa/src/tests.rs::choice_values_come_from_the_data_as_saved_values_and_several_for_lists", "crates/xfa/src/tests.rs::pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated", "crates/engine/src/tests.rs::xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save", "crates/xfa/src/tests.rs::repeating_subforms_inside_positioned_content_get_their_instances", "crates/engine/src/tests.rs::xfa_rows_in_areas_are_laid_out_read_calculated_and_written_where_adobe_binds_them", "crates/xfa/src/tests.rs::page_area_without_occur_serves_every_body_page", "apps/pdfcraft-cli/tests/it/xfa.rs::info_reports_the_laid_out_pages_and_fields", "apps/pdfcraft-cli/tests/it/xfa.rs::text_reads_the_layout_including_a_page_absent_from_the_placeholder", "apps/pdfcraft-cli/tests/it/xfa.rs::render_matches_the_engine_layout_on_the_second_page", "apps/pdfcraft-cli/tests/it/xfa.rs::a_laid_out_form_keeps_the_original_copy_permissions", "crates/engine/src/tests.rs::filling_growable_xfa_text_reflows_and_undo_restores_the_layout", "crates/engine/src/tests.rs::xfa_text_growth_refreshes_widget_geometry_but_fixed_height_fields_keep_their_widgets", "crates/automation/tests/it/automation.rs::growable_xfa_text_paginates_after_fill_save_and_print", "crates/engine/src/tests.rs::an_xfa_script_that_fails_partway_keeps_what_it_changed_before_the_error", "crates/engine/src/tests.rs::repeated_xfa_print_buttons_run_their_click_script", "crates/automation/tests/it/automation.rs::repeated_xfa_print_buttons_work_through_tools_and_after_reopen", "crates/xfa/src/tests.rs::rich_text_max_length_reaches_the_field_and_layout", "crates/automation/tests/it/automation.rs::xfa_rich_text_max_length_is_reported_and_enforced_through_tools", "crates/xfa/src/tests.rs::bmp_and_tiff_pictures_are_decoded_and_drawn", "crates/engine/src/tests.rs::active_xfa_does_not_run_pdf_container_scripts_but_keeps_requested_alerts", "crates/engine/src/tests.rs::active_xfa_without_template_scripts_does_not_initialize_pdf_fallback_scripts", "crates/engine/src/tests.rs::inactive_xfa_and_ordinary_pdfs_keep_document_helpers_and_alerts", "crates/automation/tests/it/automation.rs::active_xfa_skips_container_fallback_scripts_after_save_and_reopen", "crates/xfa/src/tests.rs::laying_a_form_out_takes_off_the_usage_rights_it_voids", "crates/engine/src/tests.rs::a_dynamic_xfa_form_is_saved_without_the_usage_rights_its_layout_voided", "crates/xfa/src/tests.rs::faces_measure_text_at_their_standard_14_widths", "crates/xfa/src/tests.rs::rich_text_links_keep_their_address", "crates/xfa/src/tests.rs::links_in_rich_text_become_link_annotations_one_per_line", "crates/xfa/src/tests.rs::a_links_pieces_share_one_address", "crates/xfa/src/tests.rs::a_rule_between_two_pieces_of_a_link_keeps_them_apart", "crates/xfa/src/tests.rs::links_to_anything_but_web_and_email_addresses_stay_plain_text", "crates/automation/tests/it/automation.rs::links_in_xfa_rich_text_are_listed_through_tools", "crates/xfa/src/tests.rs::rounded_corners_make_a_border_one_rounded_shape", "crates/xfa/src/testsLine truncated
notes = "Active XFA layout skips PDF-container document-script initialization for the Acrobat console/field context, so fallback-viewer scripts do not run there. Scripts remain in the file/listing; explicit console/field and XFA alerts still run. Ordinary PDF, static XFA and unlaid-out shells keep document scripts; static-conversion undo restores the XFA policy. Changed growable multiline text (no fixed h) reflows from final datasets after edits, with or without scripts; pages/widgets refresh and undo/redo, save/reopen and print retain all text. Oversized text still uses the existing read-only page-text fallback with a warning; fixed-height fields do not reflow. Repeated page-master buttons resolve their own PDF field to the shared XFA click script; the numPages-based print script requests printing from every copy, including after save/reopen (#2572). Rich-text exData/maxLength is carried into AcroForm MaxLen, reported by form_fields and enforced by form_fill (Unicode characters); limits and accepted values survive save/reopen (#2570). laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters (a page area with no occur repeats without limit, one with occur max=1 is used once, one with occur min=1 the body never reached gets a page after the body, as Acrobat Pro does on a synthetic form), breaks, repeated header rows, borders and fills of flowed subforms (a page break between their children gives each page's part a closed border, the margins repeated at the cut, as measured on a synthetic form in another viewer, and a child's own page break breaks them the same way; one holding an item that doesn't fit on its page, cut across pages or placed past the content area's bottom, is drawn without its border, with a layout warning), captions, rounded and inverted border corners, rich text (its http, https and mailto links clickable), JPEG/PNG/GIF/BMP/TIFF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes and radio groups (each box placed by its field's margins, caption and paragraph alignment as measured on two synthetic forms in another viewer: with no caption at the right of the content area, beside a reserved caption where the alignment puts it, and a caption without a reserve taking the rest of the field; a check box's own margin larger than the box draws a bigger box there, which isn't copied), drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields (each named by its SOM path, under a generated field for each step above it, so that another viewer, which lays the form out again, finds a signature made here, as seen on a synthetic form, and a box signed here keeps its signature, and that name, when a script lays the form out again; a box whose path, or a part of it, the file already gives another field keeps its own name, and its signature isn't found there yet), image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; usage rights the form carried (/Perms /UR3) are taken off with a note, as the layout voids them and a form with void usage rights opens with every field locked; doc_info reports xfa_layout. CLI info reports the laid-out page/field counts and dimensions while retaining the input file size and raw counts as pdf_pages/acroform_fields; text and render use those pages too, preserving copy permissions and leaving the source file unchanged (#1645). XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, formattedValue in a date display picture's pattern (number and text display pictures still give the raw value), presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. All the scripts of one event run on one thread, each in a fresh context on the form as the engine holds it then (nothing shared between scripts; the live form kept up to date in place for values, rebuilt only when the structure changed), so hundreds of calculations run within the open budget. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off, which the app says in a toast and on the notice bar, with a button to the JavaScript console, and doc_info reports as xfa_scriptLine truncated
evidence = ["crates/xfa/src/tests.rs::a_draw_turns_counter_clockwise_about_its_anchor", "crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/it/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/it/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/engine/src/tests.rs::xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it", "crates/automation/tests/it/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/it/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/ui-egui/tests/it/xfa.rs::a_form_whose_scripts_were_turned_off_at_open_says_so", "crates/ui-egui/tests/it/xfa.rs::scripts_turned_off_by_a_click_bring_a_closed_notice_bar_back", "crates/automation/tests/it/automation.rs::doc_info_says_when_a_runaway_script_turned_the_forms_scripts_off", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite", "crates/xfa/src/tests.rs::choice_lists_signatures_passwords_and_pictures_become_fields_and_images", "crates/xfa/src/tests.rs::choice_values_come_from_the_data_as_saved_values_and_several_for_lists", "crates/xfa/src/tests.rs::pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated", "crates/engine/src/tests.rs::xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save", "crates/xfa/src/tests.rs::repeating_subforms_inside_positioned_content_get_their_instances", "crates/engine/src/tests.rs::xfa_rows_in_areas_are_laid_out_read_calculated_and_written_where_adobe_binds_them", "crates/xfa/src/tests.rs::page_area_without_occur_serves_every_body_page", "apps/pdfcraft-cli/tests/it/xfa.rs::info_reports_the_laid_out_pages_and_fields", "apps/pdfcraft-cli/tests/it/xfa.rs::text_reads_the_layout_including_a_page_absent_from_the_placeholder", "apps/pdfcraft-cli/tests/it/xfa.rs::render_matches_the_engine_layout_on_the_second_page", "apps/pdfcraft-cli/tests/it/xfa.rs::a_laid_out_form_keeps_the_original_copy_permissions", "crates/engine/src/tests.rs::filling_growable_xfa_text_reflows_and_undo_restores_the_layout", "crates/engine/src/tests.rs::xfa_text_growth_refreshes_widget_geometry_but_fixed_height_fields_keep_their_widgets", "crates/automation/tests/it/automation.rs::growable_xfa_text_paginates_after_fill_save_and_print", "crates/engine/src/tests.rs::an_xfa_script_that_fails_partway_keeps_what_it_changed_before_the_error", "crates/engine/src/tests.rs::repeated_xfa_print_buttons_run_their_click_script", "crates/automation/tests/it/automation.rs::repeated_xfa_print_buttons_work_through_tools_and_after_reopen", "crates/xfa/src/tests.rs::rich_text_max_length_reaches_the_field_and_layout", "crates/automation/tests/it/automation.rs::xfa_rich_text_max_length_is_reported_and_enforced_through_tools", "crates/xfa/src/tests.rs::bmp_and_tiff_pictures_are_decoded_and_drawn", "crates/engine/src/tests.rs::active_xfa_does_not_run_pdf_container_scripts_but_keeps_requested_alerts", "crates/engine/src/tests.rs::active_xfa_without_template_scripts_does_not_initialize_pdf_fallback_scripts", "crates/engine/src/tests.rs::inactive_xfa_and_ordinary_pdfs_keep_document_helpers_and_alerts", "crates/automation/tests/it/automation.rs::active_xfa_skips_container_fallback_scripts_after_save_and_reopen", "crates/xfa/src/tests.rs::laying_a_form_out_takes_off_the_usage_rights_it_voids", "crates/engine/src/tests.rs::a_dynamic_xfa_form_is_saved_without_the_usage_rights_its_layout_voided", "crates/xfa/src/tests.rs::faces_measure_text_at_their_standard_14_widths", "crates/xfa/src/tests.rs::rich_text_links_keep_their_address", "crates/xfa/src/tests.rs::links_in_rich_text_become_link_annotations_one_per_line", "crates/xfa/src/tests.rs::a_links_pieces_share_one_address", "crates/xfa/src/tests.rs::a_rule_between_two_pieces_of_a_link_keeps_them_apart", "crates/xfa/src/tests.rs::links_to_anything_but_web_and_email_addresses_stay_plain_text", "crates/automation/tests/it/automation.rs::links_in_xfa_rich_text_are_listed_through_tools", "crates/xfa/src/testsLine truncated
notes = "A draw placed by position turns about its anchor (rotate); fields, subforms and draws in a flow don't. Active XFA layout skips PDF-container document-script initialization for the Acrobat console/field context, so fallback-viewer scripts do not run there. Scripts remain in the file/listing; explicit console/field and XFA alerts still run. Ordinary PDF, static XFA and unlaid-out shells keep document scripts; static-conversion undo restores the XFA policy. Changed growable multiline text (no fixed h) reflows from final datasets after edits, with or without scripts; pages/widgets refresh and undo/redo, save/reopen and print retain all text. Oversized text still uses the existing read-only page-text fallback with a warning; fixed-height fields do not reflow. Repeated page-master buttons resolve their own PDF field to the shared XFA click script; the numPages-based print script requests printing from every copy, including after save/reopen (#2572). Rich-text exData/maxLength is carried into AcroForm MaxLen, reported by form_fields and enforced by form_fill (Unicode characters); limits and accepted values survive save/reopen (#2570). laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters (a page area with no occur repeats without limit, one with occur max=1 is used once, one with occur min=1 the body never reached gets a page after the body, as Acrobat Pro does on a synthetic form), breaks, repeated header rows, borders and fills of flowed subforms (a page break between their children gives each page's part a closed border, the margins repeated at the cut, as measured on a synthetic form in another viewer, and a child's own page break breaks them the same way; one holding an item that doesn't fit on its page, cut across pages or placed past the content area's bottom, is drawn without its border, with a layout warning), captions, rounded and inverted border corners, rich text (its http, https and mailto links clickable), JPEG/PNG/GIF/BMP/TIFF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes and radio groups (each box placed by its field's margins, caption and paragraph alignment as measured on two synthetic forms in another viewer: with no caption at the right of the content area, beside a reserved caption where the alignment puts it, and a caption without a reserve taking the rest of the field; a check box's own margin larger than the box draws a bigger box there, which isn't copied), drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields (each named by its SOM path, under a generated field for each step above it, so that another viewer, which lays the form out again, finds a signature made here, as seen on a synthetic form, and a box signed here keeps its signature, and that name, when a script lays the form out again; a box whose path, or a part of it, the file already gives another field keeps its own name, and its signature isn't found there yet), image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; usage rights the form carried (/Perms /UR3) are taken off with a note, as the layout voids them and a form with void usage rights opens with every field locked; doc_info reports xfa_layout. CLI info reports the laid-out page/field counts and dimensions while retaining the input file size and raw counts as pdf_pages/acroform_fields; text and render use those pages too, preserving copy permissions and leaving the source file unchanged (#1645). XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, formattedValue in a date display picture's pattern (number and text display pictures still give the raw value), presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. All the scripts of one event run on one thread, each in a fresh context on the form as the engine holds it then (nothing shared between scripts; the live form kept up to date in place for values, rebuilt only when the structure changed), so hundreds of calculations run within the open budget. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off, which the app says in a tLine truncated
[[feature]]
id = "form.xfa-formcalc"