M12: what scripts set on XFA page-master content is applied to that page when laid out again

apply_overrides walks the body only. Page-master content is now set
page by page: each page made from a page area gets its own copy of the
area's content with the overrides its SOM path (Body[n]...) carries, so
a field a script hides on page 2 is hidden there only after a relayout.
The copies are kept for the whole layout, as the measurement cache keys
on node addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410
2026-10-31 07:20:36 +08:00
co-authored by Claude Opus 5.5
parent 63a77901e6
commit 74930f3929
5 changed files with 141 additions and 69 deletions
+18
View File
@@ -155,6 +155,11 @@ struct Layouter<'t> {
/// 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>,
/// What scripts set on page-master content, applied to each page's copy by its own SOM path.
overrides: Option<&'t crate::script::Overrides>,
/// The pages' copies of master content that overrides changed, kept for the whole layout so
/// their addresses stay unique (the measurement cache keys on them).
master_copies: Vec<Rc<Vec<Node>>>,
}
/// How many instances of a repeating subform to lay out: what the data holds once it holds
@@ -167,6 +172,17 @@ pub fn instance_count(occur: &Occur, from_data: usize) -> usize {
/// 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> {
layout_inner(tpl, data, None)
}
/// [`layout`], with what scripts set (`ov`) applied to page-master content page by page: a
/// field hidden on the second page made from a page area is hidden there only. The rest of the
/// template takes `ov` before this ([`crate::script::apply_overrides`]).
pub fn layout_with_overrides(tpl: &Template, data: Option<&DataNode>, ov: &crate::script::Overrides) -> Result<Form, XfaError> {
layout_inner(tpl, data, Some(ov))
}
fn layout_inner(tpl: &Template, data: Option<&DataNode>, overrides: Option<&crate::script::Overrides>) -> Result<Form, XfaError> {
let areas = match &tpl.root.page_set {
Some(ps) if !ps.areas.is_empty() => ps.areas.clone(),
_ => vec![PageArea {
@@ -201,6 +217,8 @@ pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result<Form, XfaError>
measures: Cell::new(0),
leaves: Vec::new(),
frames: Vec::new(),
overrides,
master_copies: Vec::new(),
};
let ctx = Ctx { font: Font::default(), para: Para::default(), som: String::new() };
l.new_page()?;
+15 -3
View File
@@ -2,7 +2,7 @@
use super::EMBED_COPIED;
use super::items::{Item, Page, UNDERLINE_DROP};
use super::{
Block, Border, Ctx, Cursor, FlowMark, Font, Frame, Layouter, MAX_ITEMS, MAX_MEASURES, MAX_PAGES, PAGE_COUNT_MARK, PageRole, Para, Rc, Rect,
Block, Border, Ctx, Cursor, FlowMark, Font, Frame, Layouter, MAX_ITEMS, MAX_MEASURES, MAX_PAGES, Node, PAGE_COUNT_MARK, PageRole, Para, Rc, Rect,
SomIndices, XfaError, text,
};
@@ -107,8 +107,20 @@ impl Layouter<'_> {
let ctx =
Ctx { font: Font::default(), para: Para::default(), som: area.name.clone().map(|n| format!("{n}[{instance}]")).unwrap_or_default() };
let page_rect = Rect::new(0.0, 0.0, area.width, area.height);
let sibs = SomIndices::of(&area.items);
for (i, item) in area.items.iter().enumerate() {
// What scripts set on this page's master content (a button hidden on this page only) goes
// on a copy of it for this page; other pages keep the template's.
let prefix = format!("{}.", ctx.som);
let copy =
self.overrides.filter(|ov| !ctx.som.is_empty() && ov.presence.keys().chain(ov.access.keys()).any(|k| k.starts_with(&prefix))).map(|ov| {
let mut copy = area.items.clone();
crate::script::apply_overrides_to(&mut copy, &ctx.som, ov, 1);
let copy = Rc::new(copy);
self.master_copies.push(Rc::clone(&copy));
copy
});
let items: &[Node] = copy.as_deref().map_or(&area.items, Vec::as_slice);
let sibs = SomIndices::of(items);
for (i, item) in items.iter().enumerate() {
self.draw_positioned_child(item, page_rect, &ctx, 1, sibs.at(i))?;
}
Ok(())
+3 -2
View File
@@ -158,8 +158,9 @@ pub fn render_into(doc: &mut Document) -> Result<Report, XfaError> {
}
let (tpl, parse_warnings) = parse(&packets.xdp)?;
let data = parse_datasets(&packets.xdp);
let laid = apply_overrides(&tpl, &overrides(doc));
let mut form = layout(&laid, data.as_ref())?;
let ov = overrides(doc);
let laid = apply_overrides(&tpl, &ov);
let mut form = layout::layout_with_overrides(&laid, data.as_ref(), &ov)?;
form.warnings.splice(0..0, parse_warnings);
if form.pages.is_empty() {
return Err(XfaError::Malformed("the template laid out to no pages".into()));
+67 -64
View File
@@ -475,81 +475,84 @@ pub fn has_scripts(tpl: &Template) -> bool {
any(&tpl.root.scripts) || walk(&tpl.root.children, 1) || tpl.root.page_set.as_ref().is_some_and(|ps| ps.areas.iter().any(|a| walk(&a.items, 1)))
}
/// The template with `ov` applied to its objects (instance 0 of each; scripts that hide one
/// instance of a repeating subform hide them all).
pub fn apply_overrides(tpl: &Template, ov: &Overrides) -> Template {
/// Set the presence `ov` gives each of `nodes` (children of `parent_som`) and their descendants,
/// down to [`MAX_DEPTH`], and the access it gives a field outside an exclusion group. An unnamed
/// draw, area or subform shares its parent's SOM path, so it takes no override of its own.
fn walk(nodes: &mut [Node], parent_som: &str, ov: &Overrides, depth: usize) {
if depth > MAX_DEPTH {
return;
}
let sibs = SomIndices::of(nodes);
for (i, n) in nodes.iter_mut().enumerate() {
let sib = sibs.at(i);
let name_default = match n {
Node::Field(_) => Some("field"),
Node::ExclGroup(_) => Some("group"),
_ => None,
};
let name = n.common().name.as_deref().or(name_default);
let som = child_som(parent_som, name, sib);
// An unnamed draw, area or subform has no SOM name: `som` is its parent's, and so are
// the overrides under it.
let presence = name.and_then(|_| ov.presence.get(&som)).map(|p| Presence::parse(Some(p)));
let access = name.and_then(|_| ov.access.get(&som)).cloned();
match n {
Node::Subform(s) => {
if let Some(p) = presence {
s.common.presence = p;
}
walk(&mut s.children, &som, ov, depth + 1);
/// Set the presence `ov` gives each of `nodes` (children of `parent_som`) and their descendants,
/// down to [`MAX_DEPTH`], and the access it gives a field outside an exclusion group. An unnamed
/// draw, area or subform shares its parent's SOM path, so it takes no override of its own.
pub(crate) fn apply_overrides_to(nodes: &mut [Node], parent_som: &str, ov: &Overrides, depth: usize) {
if depth > MAX_DEPTH {
return;
}
let sibs = SomIndices::of(nodes);
for (i, n) in nodes.iter_mut().enumerate() {
let sib = sibs.at(i);
let name_default = match n {
Node::Field(_) => Some("field"),
Node::ExclGroup(_) => Some("group"),
_ => None,
};
let name = n.common().name.as_deref().or(name_default);
let som = child_som(parent_som, name, sib);
// An unnamed draw, area or subform has no SOM name: `som` is its parent's, and so are
// the overrides under it.
let presence = name.and_then(|_| ov.presence.get(&som)).map(|p| Presence::parse(Some(p)));
let access = name.and_then(|_| ov.access.get(&som)).cloned();
match n {
Node::Subform(s) => {
if let Some(p) = presence {
s.common.presence = p;
}
Node::Area(a) => {
if let Some(p) = presence {
a.common.presence = p;
}
walk(&mut a.children, &som, ov, depth + 1);
apply_overrides_to(&mut s.children, &som, ov, depth + 1);
}
Node::Area(a) => {
if let Some(p) = presence {
a.common.presence = p;
}
Node::Field(f) => {
if let Some(p) = presence {
f.common.presence = p;
}
if let Some(a) = access {
f.access = match a.as_str() {
"readOnly" => Access::ReadOnly,
"protected" => Access::Protected,
"nonInteractive" => Access::NonInteractive,
_ => Access::Open,
};
}
apply_overrides_to(&mut a.children, &som, ov, depth + 1);
}
Node::Field(f) => {
if let Some(p) = presence {
f.common.presence = p;
}
Node::Draw(d) => {
if let Some(p) = presence {
d.common.presence = p;
}
if let Some(a) = access {
f.access = match a.as_str() {
"readOnly" => Access::ReadOnly,
"protected" => Access::Protected,
"nonInteractive" => Access::NonInteractive,
_ => Access::Open,
};
}
Node::ExclGroup(g) => {
if let Some(p) = presence {
g.common.presence = p;
}
let fields: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
let fsibs = SomIndices::of(&fields);
for (fi, f) in g.fields.iter_mut().enumerate() {
let fsom = child_som(&som, f.common.name.as_deref().or(Some("field")), fsibs.at(fi));
if let Some(p) = ov.presence.get(&fsom) {
f.common.presence = Presence::parse(Some(p));
}
}
Node::Draw(d) => {
if let Some(p) = presence {
d.common.presence = p;
}
}
Node::ExclGroup(g) => {
if let Some(p) = presence {
g.common.presence = p;
}
let fields: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
let fsibs = SomIndices::of(&fields);
for (fi, f) in g.fields.iter_mut().enumerate() {
let fsom = child_som(&som, f.common.name.as_deref().or(Some("field")), fsibs.at(fi));
if let Some(p) = ov.presence.get(&fsom) {
f.common.presence = Presence::parse(Some(p));
}
}
}
}
}
}
/// The template with `ov` applied to its objects (instance 0 of each; scripts that hide one
/// instance of a repeating subform hide them all). Page-master content is left as it is: a page
/// area makes many pages, each with its own SOM path, so `ov` is applied to it page by page as
/// the pages are laid out ([`crate::layout::layout_with_overrides`]).
pub fn apply_overrides(tpl: &Template, ov: &Overrides) -> Template {
let mut t = tpl.clone();
if !ov.is_empty() {
let mut root = vec![Node::Subform(Box::new(t.root.clone()))];
walk(&mut root, "", ov, 0);
apply_overrides_to(&mut root, "", ov, 0);
if let Some(Node::Subform(r)) = root.pop() {
t.root = *r;
}
@@ -617,7 +620,7 @@ pub fn rerender(doc: &mut Document, tpl: &Template) -> Result<Report, XfaError>
let data = parse_datasets(&packets.xdp);
let ov = overrides(doc);
let laid = apply_overrides(tpl, &ov);
let form = crate::layout(&laid, data.as_ref())?;
let form = crate::layout::layout_with_overrides(&laid, data.as_ref(), &ov)?;
if form.pages.is_empty() {
return Err(XfaError::Malformed("the template laid out to no pages".into()));
}
+38
View File
@@ -4186,3 +4186,41 @@ fn clear_password_values_clears_static_xfa_password_fields() {
let packets = read_packets(&doc).unwrap().unwrap();
assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp);
}
/// What a script sets on page-master content (a field hidden on the second page made from a
/// page area) is applied to that page's copy only, when the form is laid out again: the other
/// pages keep the field.
#[test]
fn a_script_hiding_master_content_on_one_page_hides_it_there_only() {
let section = |n: u32| {
format!(
r#"<subform name="s{n}" layout="tb"><breakBefore targetType="pageArea" startNew="1"/><field name="f{n}" w="2in" h="0.3in"><ui><textEdit/></ui></field></subform>"#
)
};
let xdp = format!(
r##"<?xml version="1.0" encoding="UTF-8"?>
<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="Body"><contentArea x="0.5in" y="0.5in" w="7.5in" h="2in"/><medium short="8.5in" long="11in"/>
<field name="pageNo" x="7in" y="10in" w="1in" h="0.3in"><ui><textEdit/></ui></field>
</pageArea>
</pageSet>
{}{}{}
</subform>
</template>
</xdp:xdp>
"##,
section(1),
section(2),
section(3)
);
let (tpl, _) = crate::parse(&xdp).unwrap();
let mut ov = Overrides::default();
ov.presence.insert("Body[1].pageNo[0]".into(), "hidden".into());
let laid = apply_overrides(&tpl, &ov);
let form = crate::layout::layout_with_overrides(&laid, None, &ov).unwrap();
let has: Vec<bool> = form.pages.iter().map(|p| widgets(p).iter().any(|w| w.som.contains("pageNo"))).collect();
assert_eq!(has, [true, false, true], "hidden on the second page only");
}