mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:35:21 +00:00
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:
co-authored by
Claude Opus 5.5
parent
63a77901e6
commit
74930f3929
@@ -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()?;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
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(())
|
||||
|
||||
@@ -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
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user