mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
# Conflicts: # crates/engine/src/xfa.rs # crates/xfa/src/layout/pages.rs # crates/xfa/src/lib.rs # crates/xfa/src/script.rs # crates/xfa/src/tests.rs
This commit is contained in:
commit
ab2d4382ff
12 files changed
+568
-99
No files matched your search
@@ -8423,6 +8423,57 @@ fn repeated_xfa_print_buttons_run_their_click_script() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A button on the page master runs its click script as the copy on the page that was clicked:
|
||||
/// the field it writes next to it changes on that page only.
|
||||
#[test]
|
||||
fn a_page_master_button_writes_the_field_on_its_own_page() {
|
||||
let template = pdfcraft_xfa::fixtures::repeated_print_button_template().replace(
|
||||
"</pageArea>",
|
||||
r#"<field name="stamp" x="200pt" y="24pt" w="100pt" h="25pt"><ui><textEdit/></ui></field>
|
||||
<field name="mark" x="320pt" y="24pt" w="80pt" h="25pt"><ui><button/></ui>
|
||||
<event activity="click"><script contentType="application/x-javascript">stamp.rawValue = "clicked";</script></event>
|
||||
</field>
|
||||
</pageArea>"#,
|
||||
);
|
||||
let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&template));
|
||||
let mut s = Session::new();
|
||||
let id = s.open("stamp.pdf", None, bytes, None).unwrap();
|
||||
assert_eq!(s.get(id).unwrap().info.pages.len(), 3);
|
||||
let out = s.run_javascript(id, "throw new Error('AcroForm fallback ran')", Some("mark_2")).unwrap();
|
||||
assert_eq!(out.error, None, "{out:?}");
|
||||
let value = |s: &Session, n: &str| s.get(id).unwrap().form.iter().find(|f| f.name == n).unwrap().value.clone();
|
||||
assert_eq!(value(&s, "stamp_2"), vec!["clicked".to_string()], "the page clicked");
|
||||
for other in ["stamp", "stamp_3"] {
|
||||
assert_ne!(value(&s, other), vec!["clicked".to_string()], "{other} is on another page");
|
||||
}
|
||||
}
|
||||
|
||||
/// An initialize script that hides the last page's subform takes a page away; the page-master
|
||||
/// button of that page, listed to initialize before it went, doesn't run with `this` at the
|
||||
/// form instead and hide all of it.
|
||||
#[test]
|
||||
fn a_page_master_script_of_a_page_gone_does_not_hide_the_form() {
|
||||
let template = pdfcraft_xfa::fixtures::repeated_print_button_template()
|
||||
.replace(
|
||||
r#"<caption><value><text>Print form</text></value></caption>"#,
|
||||
r#"<caption><value><text>Print form</text></value></caption>
|
||||
<event activity="initialize"><script contentType="application/x-javascript">this.presence = "hidden";</script></event>"#,
|
||||
)
|
||||
.replace(
|
||||
r#"<subform name="page3" layout="tb"><breakBefore targetType="pageArea"/>"#,
|
||||
r#"<subform name="page3" layout="tb"><breakBefore targetType="pageArea"/>
|
||||
<event activity="initialize"><script contentType="application/x-javascript">this.presence = "hidden";</script></event>"#,
|
||||
);
|
||||
let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&template));
|
||||
let mut s = Session::new();
|
||||
let id = s.open("gone.pdf", None, bytes, None).unwrap();
|
||||
let doc = s.get(id).unwrap();
|
||||
assert_eq!(doc.info.pages.len(), 2, "page 3 is hidden");
|
||||
for note in ["note1", "note2"] {
|
||||
assert!(doc.form.iter().any(|f| f.name == note), "{note} is still shown");
|
||||
}
|
||||
}
|
||||
|
||||
/// An XFA form's scripts initialize, calculate, validate, show subforms and add and remove rows,
|
||||
/// with undo and save/reopen.
|
||||
#[test]
|
||||
@@ -12363,6 +12414,164 @@ fn repeated_backdrop_artwork_survives_background_undo_redo_and_both_save_modes()
|
||||
black(&s, rewritten);
|
||||
}
|
||||
|
||||
/// A page still exists after its only master widget is hidden: its master nodes stay in the
|
||||
/// script model, so a later script can address them again (test from wireless89's review of
|
||||
/// #5368).
|
||||
#[test]
|
||||
fn xfa_page_master_instance_survives_hiding_its_last_widget() {
|
||||
let template = pdfcraft_xfa::fixtures::repeated_print_button_template()
|
||||
.replace(r#"xfa.host.print(1, "0", (xfa.host.numPages -1).toString(), 0, 0, 0, 0, 0);"#, r#"this.presence = "hidden";"#)
|
||||
.replace(
|
||||
"</subform></template>",
|
||||
r#"<field name="restore" w="100pt" h="25pt"><ui><button/></ui>
|
||||
<event activity="click"><script contentType="application/x-javascript">xfa.resolveNode("master[2].PrintButton1").presence = "visible";</script></event></field></subform></template>"#,
|
||||
);
|
||||
let (tpl, _) = pdfcraft_xfa::parse(&template).unwrap();
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000).with_xfa_time_scale(100);
|
||||
let id = s.open("hidden-master.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&template)), None).unwrap();
|
||||
let out = s.run_javascript(id, "", Some("PrintButton1_3")).unwrap();
|
||||
assert_eq!(out.error, None, "{out:?}");
|
||||
let cos = &s.get(id).unwrap().editor.as_ref().unwrap().cos;
|
||||
assert_eq!(pdfcraft_organize::page_count(cos).unwrap(), 3, "the body still fills three pages");
|
||||
assert!(!s.get(id).unwrap().form.iter().any(|f| f.name == "PrintButton1_3"));
|
||||
let uses = pdfcraft_xfa::page_area_uses(cos, &tpl);
|
||||
let restored = s.run_javascript(id, "", Some("restore")).unwrap();
|
||||
assert_eq!((uses.get("master").copied(), restored.error), (Some(3), None), "the third page master remains addressable by scripts");
|
||||
}
|
||||
|
||||
/// Page-local values and a globally bound control survive relayout, disappearing and
|
||||
/// reappearing pages, incremental saves and vector printing (test from wireless89's review of #5368).
|
||||
#[test]
|
||||
fn xfa_master_values_survive_page_changes_save_reopen_and_print() {
|
||||
fn name(s: &Session, id: DocId, som: &str) -> String {
|
||||
let d = s.get(id).unwrap();
|
||||
pdfcraft_xfa::fields_by_som(&d.editor.as_ref().unwrap().cos).get(som).unwrap_or_else(|| panic!("missing {som}")).clone()
|
||||
}
|
||||
fn set(s: &mut Session, id: DocId, som: &str, text: &str) {
|
||||
let n = name(s, id, som);
|
||||
s.apply(id, Edit::SetFieldValue { name: n, value: FieldValue::Text(text.into()) }).unwrap();
|
||||
}
|
||||
fn check(s: &Session, id: DocId, expected: &[&str]) {
|
||||
let d = s.get(id).unwrap();
|
||||
// Inspect the actual PDF tree: refreshing the cached view after XFA relayout is
|
||||
// a separate issue (#4604), not part of this master-value regression.
|
||||
assert_eq!(pdfcraft_organize::page_count(&d.editor.as_ref().unwrap().cos).unwrap(), expected.len());
|
||||
let by_som = pdfcraft_xfa::fields_by_som(&d.editor.as_ref().unwrap().cos);
|
||||
assert_eq!(by_som.keys().filter(|som| som.ends_with(".stamp[0]")).count(), expected.len());
|
||||
for (page, value) in expected.iter().enumerate() {
|
||||
for (field, expected) in [("stamp", *value), ("shared", "GLOBAL_VALUE")] {
|
||||
let n = name(s, id, &format!("master[{page}].{field}[0]"));
|
||||
assert_eq!(d.form.iter().find(|f| f.name == n).unwrap().value, [expected], "page {page}: {field}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let template = master_round_trip_review_template();
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000).with_xfa_time_scale(100);
|
||||
let mut id = s.open("masters.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&template)), None).unwrap();
|
||||
assert_eq!(s.get(id).unwrap().info.pages.len(), 3);
|
||||
for (page, value) in ["MASTER_A", "MASTER_B", "MASTER_C"].iter().enumerate() {
|
||||
set(&mut s, id, &format!("master[{page}].stamp[0]"), value);
|
||||
}
|
||||
check(&s, id, &["MASTER_A", "MASTER_B", "MASTER_C"]);
|
||||
let mark = name(&s, id, "master[1].mark[0]");
|
||||
let out = s.run_javascript(id, "throw new Error('AcroForm fallback ran')", Some(&mark)).unwrap();
|
||||
assert_eq!(out.error, None, "{out:?}");
|
||||
check(&s, id, &["MASTER_A", "MASTER_B_CLICK", "MASTER_C"]);
|
||||
let more = name(&s, id, "form[0].page1[0].more[0]");
|
||||
s.apply(id, Edit::SetFieldValue { name: more.clone(), value: FieldValue::Check(false) }).unwrap();
|
||||
assert!(s.take_js_output(id).errors.is_empty());
|
||||
check(&s, id, &["MASTER_A", "MASTER_B_CLICK"]);
|
||||
s.undo(id).unwrap();
|
||||
check(&s, id, &["MASTER_A", "MASTER_B_CLICK", "MASTER_C"]);
|
||||
s.redo(id).unwrap();
|
||||
check(&s, id, &["MASTER_A", "MASTER_B_CLICK"]);
|
||||
s.apply(id, Edit::SetFieldValue { name: more, value: FieldValue::Check(true) }).unwrap();
|
||||
check(&s, id, &["MASTER_A", "MASTER_B_CLICK", "MASTER_C"]);
|
||||
for cycle in 0..3 {
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
let cos = pdfcraft_cos::Document::open(saved.clone()).unwrap();
|
||||
let data = pdfcraft_xfa::data_of(&cos).unwrap();
|
||||
for (page, value) in ["MASTER_A", "MASTER_B_CLICK", "MASTER_C"].iter().enumerate() {
|
||||
assert_eq!(data.text_at(&pdfcraft_xfa::som_to_path(&format!("master[{page}].stamp[0]"))), Some(*value), "cycle {cycle}: page {page}");
|
||||
}
|
||||
assert_eq!(data.text_at(&pdfcraft_xfa::som_to_path("master[0].shared[0]")), Some("GLOBAL_VALUE"));
|
||||
id = s.open("again.pdf", None, saved, None).unwrap();
|
||||
assert!(s.take_js_output(id).errors.is_empty());
|
||||
check(&s, id, &["MASTER_A", "MASTER_B_CLICK", "MASTER_C"]);
|
||||
}
|
||||
let printed = s.print_pdf(id, &print::Settings { pages: vec![0, 1, 2], ..Default::default() }).unwrap();
|
||||
let pid = s.open("printed.pdf", None, Arc::new(printed), None).unwrap();
|
||||
let texts = page_texts(&s, pid);
|
||||
assert_eq!(texts.len(), 3);
|
||||
for (page, value) in ["MASTER_A", "MASTER_B_CLICK", "MASTER_C"].iter().enumerate() {
|
||||
assert!(texts[page].contains(value) && texts[page].contains("GLOBAL_VALUE"), "page {page}: {:?}", texts[page]);
|
||||
for other in ["MASTER_A", "MASTER_B_CLICK", "MASTER_C"].iter().filter(|other| *other != value) {
|
||||
assert!(!texts[page].contains(other), "page {page} contains another page's value: {other}");
|
||||
}
|
||||
}
|
||||
}
|
||||
fn master_round_trip_review_template() -> String {
|
||||
let template = pdfcraft_xfa::fixtures::repeated_print_button_template()
|
||||
.replace(r#"name="PrintButton1" x="36pt""#, r#"name="PrintButton1" x="480pt""#)
|
||||
.replace("</pageArea>", r#"
|
||||
<field name="stamp" x="36pt" y="24pt" w="160pt" h="25pt"><ui><textEdit/></ui></field>
|
||||
<field name="shared" x="200pt" y="24pt" w="150pt" h="25pt"><ui><textEdit/></ui><bind match="global"/></field>
|
||||
<field name="mark" x="360pt" y="24pt" w="80pt" h="25pt"><ui><button/></ui>
|
||||
<event activity="click"><script contentType="application/x-javascript">stamp.rawValue = "MASTER_B_CLICK";</script></event>
|
||||
</field>
|
||||
</pageArea>"#)
|
||||
.replace(r#"<subform name="page1" layout="tb">"#, r#"<subform name="page1" layout="tb">
|
||||
<field name="more" w="20pt" h="20pt"><ui><checkButton/></ui><items><integer>1</integer><integer>0</integer></items><value><integer>1</integer></value>
|
||||
<event activity="change"><script contentType="application/x-javascript">xfa.resolveNode("form.page3").presence = this.rawValue == 1 ? "visible" : "hidden";</script></event>
|
||||
</field>"#);
|
||||
format!(
|
||||
r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">{template}
|
||||
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><master><shared>GLOBAL_VALUE</shared></master></xfa:data></xfa:datasets></xdp:xdp>"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Hiding the first, middle, last or every page-master widget keeps every master
|
||||
/// instance addressable, live and after save and reopen (wireless89, #5368).
|
||||
#[test]
|
||||
fn xfa_hidden_master_instances_remain_addressable_across_reopen_matrix() {
|
||||
let mut failures = Vec::new();
|
||||
for targets in [vec![0usize], vec![1], vec![2], vec![0, 1, 2]] {
|
||||
let hide =
|
||||
targets.iter().map(|page| format!("xfa.resolveNode('master[{page}].PrintButton1').presence = 'hidden';")).collect::<Vec<_>>().join(" ");
|
||||
let restore =
|
||||
targets.iter().map(|page| format!("xfa.resolveNode('master[{page}].PrintButton1').presence = 'visible';")).collect::<Vec<_>>().join(" ");
|
||||
let template = pdfcraft_xfa::fixtures::repeated_print_button_template().replace(
|
||||
r#"xfa.host.print(1, "0", (xfa.host.numPages -1).toString(), 0, 0, 0, 0, 0);"#, &hide,
|
||||
).replace("</subform></template>", &format!(r#"<field name="restore" w="100pt" h="25pt"><ui><button/></ui><event activity="click"><script contentType="application/x-javascript">{restore}</script></event></field></subform></template>"#));
|
||||
let (tpl, _) = pdfcraft_xfa::parse(&template).unwrap();
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000).with_xfa_time_scale(100);
|
||||
let id = s.open("hidden.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&template)), None).unwrap();
|
||||
assert_eq!(s.run_javascript(id, "", Some("PrintButton1")).unwrap().error, None);
|
||||
s.undo(id).unwrap();
|
||||
assert_eq!(
|
||||
pdfcraft_xfa::fields_by_som(&s.get(id).unwrap().editor.as_ref().unwrap().cos).keys().filter(|name| name.contains("PrintButton1")).count(),
|
||||
3
|
||||
);
|
||||
s.redo(id).unwrap();
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
for (stage, current) in [("live", id), ("reopened", s.open("saved.pdf", None, saved, None).unwrap())] {
|
||||
let cos = &s.get(current).unwrap().editor.as_ref().unwrap().cos;
|
||||
assert_eq!(pdfcraft_organize::page_count(cos).unwrap(), 3);
|
||||
let count = pdfcraft_xfa::page_area_uses(cos, &tpl).get("master").copied();
|
||||
let error = s.run_javascript(current, "", Some("restore")).unwrap().error;
|
||||
let restored = targets.iter().all(|page| {
|
||||
pdfcraft_xfa::fields_by_som(&s.get(current).unwrap().editor.as_ref().unwrap().cos)
|
||||
.contains_key(&format!("master[{page}].PrintButton1[0]"))
|
||||
});
|
||||
eprintln!("targets={targets:?} stage={stage} count={count:?} error={error:?} restored={restored}");
|
||||
if count != Some(3) || error.is_some() || !restored {
|
||||
failures.push(format!("{targets:?}/{stage}: count={count:?}, error={error:?}, restored={restored}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
|
||||
haystack.windows(needle.len()).any(|w| w == needle)
|
||||
}
|
||||
|
||||
@@ -308,6 +308,10 @@ fn find_container<'a>(n: &'a FormNode, som: &str, depth: usize) -> Option<&'a Fo
|
||||
n.children.iter().find_map(|c| find_container(c, som, depth + 1))
|
||||
}
|
||||
|
||||
fn has_node(n: &FormNode, som: &str, depth: usize) -> bool {
|
||||
depth <= 64 && (n.som == som || n.children.iter().any(|c| has_node(c, som, depth + 1)))
|
||||
}
|
||||
|
||||
fn count_instances(root: &FormNode, parent_som: &str, name: &str) -> usize {
|
||||
let parent = if parent_som.is_empty() { Some(root) } else { find_container(root, parent_som, 0) };
|
||||
parent.map_or(0, |p| p.children.iter().filter(|c| c.name == name).count())
|
||||
@@ -538,7 +542,8 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
|
||||
fn live(&mut self) -> &LiveForm {
|
||||
if self.cached.is_none() {
|
||||
let live = pdfcraft_xfa::form_tree(self.tpl, self.data.as_ref(), &self.ov);
|
||||
let uses = pdfcraft_xfa::page_area_uses(self.doc, self.tpl);
|
||||
let live = pdfcraft_xfa::form_tree_paged(self.tpl, self.data.as_ref(), &self.ov, &uses);
|
||||
if live.truncated && !self.truncated_warned {
|
||||
self.truncated_warned = true;
|
||||
self.error(format!(
|
||||
@@ -933,6 +938,12 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
self.skipped += 1;
|
||||
return Ok(None);
|
||||
}
|
||||
// An event listed before a script relaid the form out can name an object that is gone
|
||||
// (a page the form no longer fills, a row removed): it isn't run, rather than run with
|
||||
// `this` at some other object.
|
||||
if !has_node(&self.live().root, &ev.som, 0) {
|
||||
return Ok(None);
|
||||
}
|
||||
self.runs += 1;
|
||||
#[cfg(test)]
|
||||
self.check_live();
|
||||
@@ -1039,7 +1050,8 @@ impl Runner<'_, '_> {
|
||||
#[cfg(test)]
|
||||
fn check_live(&mut self) {
|
||||
let Some(live) = self.cached.as_ref() else { return };
|
||||
let fresh = pdfcraft_xfa::form_tree(self.tpl, self.data.as_ref(), &self.ov);
|
||||
let uses = pdfcraft_xfa::page_area_uses(self.doc, self.tpl);
|
||||
let fresh = pdfcraft_xfa::form_tree_paged(self.tpl, self.data.as_ref(), &self.ov, &uses);
|
||||
let (mut have, mut want) = (Vec::new(), Vec::new());
|
||||
collect_fields(&live.root, &mut have, 0);
|
||||
collect_fields(&fresh.root, &mut want, 0);
|
||||
@@ -1344,11 +1356,11 @@ pub(crate) fn clickable_som(doc: &pdfcraft_cos::Document, tpl: &Template, field:
|
||||
|
||||
/// The SOM path of a generated field whose template has an `activity` script.
|
||||
pub(crate) fn scripted_som(doc: &pdfcraft_cos::Document, tpl: &Template, field: &pdfcraft_forms::Field, activity: &str) -> Option<String> {
|
||||
// Page-master copies have distinct PDF field names but share a SOM path. Reversing
|
||||
// fields_by_som would lose every copy except the last: use the field itself.
|
||||
// The field's own SOM path: each page-master copy has one of its own (`master[1]…`).
|
||||
let object = doc.get(field.obj);
|
||||
let som = object.as_dict()?.get(pdfcraft_xfa::SOM_KEY).map(|s| doc.resolve(s))?.as_string()?.to_text();
|
||||
let live = pdfcraft_xfa::form_tree(tpl, pdfcraft_xfa::data_of(doc).as_ref(), &pdfcraft_xfa::overrides(doc));
|
||||
let uses = pdfcraft_xfa::page_area_uses(doc, tpl);
|
||||
let live = pdfcraft_xfa::form_tree_paged(tpl, pdfcraft_xfa::data_of(doc).as_ref(), &pdfcraft_xfa::overrides(doc), &uses);
|
||||
live.events.iter().any(|e| e.som == som && e.activity == activity).then_some(som)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,11 @@ pub fn data_path(tpl: &Template, som: &str) -> DataPath {
|
||||
// The template children the next segment is looked up in; `None` until the root matched,
|
||||
// and again once a segment is not a container the template has.
|
||||
let mut level: Option<&[Node]> = None;
|
||||
for (i, (name, index)) in som_to_path(som).into_iter().enumerate() {
|
||||
let segments = som_to_path(som);
|
||||
let page_area_index = page_area_index(tpl, &segments);
|
||||
for (i, (name, index)) in segments.into_iter().enumerate() {
|
||||
// A page area's SOM index is the page made from it; see [`page_area_index`].
|
||||
let index = if i == 0 { page_area_index.unwrap_or(index) } else { index };
|
||||
let found = match level {
|
||||
None if i == 0 && tpl.root.common.name.as_deref() == Some(name.as_str()) => Some(Container::Children(&tpl.root.children)),
|
||||
None => None,
|
||||
@@ -77,6 +81,36 @@ pub fn data_path(tpl: &Template, som: &str) -> DataPath {
|
||||
out
|
||||
}
|
||||
|
||||
/// For a SOM path that starts at a page area (`Area[n].…`), the index its first segment takes in
|
||||
/// the data path, `None` for any other path. Each page made from a page area has its own SOM
|
||||
/// index, so the master content repeated on each page (a page number, an office-use box) binds
|
||||
/// apart on every page. A field whose bind shares one data node (`match="global"`, or an
|
||||
/// explicit data reference) binds as instance 0 on each page instead, so it shows the same value
|
||||
/// on all of them (XFA 3.3, "Page Background": a page-background field binds only through an
|
||||
/// explicit data reference).
|
||||
fn page_area_index(tpl: &Template, segments: &[(String, usize)]) -> Option<usize> {
|
||||
let ((first, instance), rest) = segments.split_first()?;
|
||||
if tpl.root.common.name.as_deref() == Some(first.as_str()) {
|
||||
return None;
|
||||
}
|
||||
let areas = tpl.root.page_set.as_ref()?;
|
||||
let area = areas.areas.iter().find(|a| a.name.as_deref() == Some(first.as_str()))?;
|
||||
// The object the rest of the path names, inside the page area's own content.
|
||||
let mut nodes: &[Node] = &area.items;
|
||||
let mut target = None;
|
||||
for (name, index) in rest {
|
||||
let found = named_at(nodes, name, *index, 0)?;
|
||||
target = Some(found);
|
||||
nodes = match found {
|
||||
Node::Subform(sf) => &sf.children,
|
||||
Node::Area(a) => &a.children,
|
||||
_ => &[],
|
||||
};
|
||||
}
|
||||
let shared = matches!(target, Some(Node::Field(f)) if f.data_shared);
|
||||
Some(if shared { 0 } else { *instance })
|
||||
}
|
||||
|
||||
enum Container<'a> {
|
||||
Children(&'a [Node]),
|
||||
/// Contents bind under the area's parent.
|
||||
|
||||
@@ -200,6 +200,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
|
||||
@@ -212,6 +217,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 {
|
||||
@@ -253,6 +269,8 @@ pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result<Form, XfaError>
|
||||
field_border: None,
|
||||
views: Views::ALL,
|
||||
frames: Vec::new(),
|
||||
overrides,
|
||||
master_copies: Vec::new(),
|
||||
};
|
||||
let ctx = Ctx { font: Font::default(), para: Para::default(), som: String::new() };
|
||||
l.new_page()?;
|
||||
|
||||
@@ -161,6 +161,8 @@ pub struct Page {
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
pub items: Vec<Item>,
|
||||
/// The name of the page area the page was made from (`None` for an unnamed one).
|
||||
pub area: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use super::EMBED_COPIED;
|
||||
use super::items::{Item, Page, UNDERLINE_DROP};
|
||||
use super::{
|
||||
Block, Border, Common, Ctx, Cursor, FlowMark, Font, Frame, Layouter, MAX_DRAW_CHARS, MAX_ITEMS, MAX_MEASURES, MAX_PAGES, PAGE_COUNT_MARK,
|
||||
Block, Border, Common, Ctx, Cursor, FlowMark, Font, Frame, Layouter, MAX_DRAW_CHARS, MAX_ITEMS, MAX_MEASURES, MAX_PAGES, Node, PAGE_COUNT_MARK,
|
||||
PageRole, Para, Rc, Rect, SomIndices, Views, XfaError, data_path, text,
|
||||
};
|
||||
|
||||
@@ -144,17 +144,33 @@ impl Layouter<'_> {
|
||||
if let Some(u) = self.uses.get_mut(idx) {
|
||||
*u += 1;
|
||||
}
|
||||
self.pages.push(Page { width: area.width, height: area.height, items: Vec::new() });
|
||||
self.pages.push(Page { width: area.width, height: area.height, items: Vec::new(), area: area.name.clone() });
|
||||
let content = area.content.first().copied().unwrap_or(Rect::new(0.0, 0.0, area.width, area.height));
|
||||
self.cur = Some(Cursor { area: idx, content: 0, rect: content, y: content.y, placed: false });
|
||||
// Master page content: positioned draws and fields.
|
||||
let ctx = Ctx { font: Font::default(), para: Para::default(), som: area.name.clone().map(|n| format!("{n}[0]")).unwrap_or_default() };
|
||||
// Master page content: positioned draws and fields. The page area's own SOM index counts
|
||||
// the pages made from it, so a field repeated on each page (a page number, an office-use
|
||||
// box) has a path of its own per page rather than every page sharing `[0]`.
|
||||
let instance = self.uses.get(idx).map_or(0, |u| u.saturating_sub(1));
|
||||
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);
|
||||
// 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);
|
||||
// The master page's own objects: shown where they say, whatever the body was drawing
|
||||
// when it needed this page.
|
||||
let body = std::mem::replace(&mut self.views, Views::ALL);
|
||||
let drawn = area.items.iter().enumerate().try_for_each(|(i, item)| self.draw_positioned_child(item, page_rect, &ctx, 1, sibs.at(i)));
|
||||
let drawn = items.iter().enumerate().try_for_each(|(i, item)| self.draw_positioned_child(item, page_rect, &ctx, 1, sibs.at(i)));
|
||||
self.views = body;
|
||||
drawn
|
||||
}
|
||||
|
||||
@@ -101,12 +101,12 @@ pub use data::{
|
||||
pub use layout::{Action, BorderShape, Form, Item, MAX_PAGES, Page, Widget, WidgetKind, formatted_value, layout};
|
||||
pub use packets::{Encoding, Packets, decode as decode_packet, encode as encode_packet, read_packets};
|
||||
pub use parse::{measure, numbers_pages, parse};
|
||||
pub use pdf::{CLICK_KEY, DATE_KEY, LAYOUT_KEY, NODE_KEY, SOM_KEY, Stripped, XfaShape, existing_layout, strip, xfa_shape};
|
||||
pub use pdf::{AREA_KEY, CLICK_KEY, DATE_KEY, LAYOUT_KEY, NODE_KEY, SOM_KEY, Stripped, XfaShape, existing_layout, strip, xfa_shape};
|
||||
pub use picture::typed_date;
|
||||
pub use script::{
|
||||
FormNode, LiveForm, MAX_FORM_NODES, NodeKind, OVERRIDES_KEY, Overrides, ScriptEvent, apply_overrides, data_of, embedded_fields,
|
||||
field_grows_with_text, field_is_embedded, fields_by_som, form_tree, has_focus_scripts, has_scripts, overrides, path_is_embedded, refresh_value,
|
||||
rerender, reset_default, set_overrides, template_of,
|
||||
field_grows_with_text, field_is_embedded, fields_by_som, form_tree, form_tree_paged, has_focus_scripts, has_scripts, overrides, page_area_uses,
|
||||
path_is_embedded, refresh_value, rerender, reset_default, set_overrides, template_of,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -161,8 +161,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()));
|
||||
|
||||
@@ -636,6 +636,10 @@ pub struct Field {
|
||||
/// The display picture clause (`<format><picture>`), e.g. `date{YYYYMMDD}`: how the value
|
||||
/// shows when not being edited, and what scripts read as `formattedValue`.
|
||||
pub display_picture: Option<String>,
|
||||
/// Its bind shares one data node among every field that names it: `match="global"`, or an
|
||||
/// explicit data reference (`ref`). On a page area, such a field shows the same value on each
|
||||
/// page made from it (XFA 3.3, "Page Background").
|
||||
pub data_shared: bool,
|
||||
/// `<keep intact="contentArea">` or `"pageArea"`: a page break doesn't split the field (XFA 3.3
|
||||
/// p. 292). A field's default is `none`.
|
||||
pub keep_intact: bool,
|
||||
|
||||
@@ -832,6 +832,8 @@ fn parse_field(n: XmlNode, warnings: &mut Vec<String>) -> Field {
|
||||
},
|
||||
picture: ui.and_then(|u| child(u, "picture")).map(text_of).map(|p| p.trim().to_string()).filter(|p| !p.is_empty()),
|
||||
data_picture: child(n, "bind").and_then(|b| child(b, "picture")).map(text_of).map(|p| p.trim().to_string()).filter(|p| !p.is_empty()),
|
||||
data_shared: child(n, "bind")
|
||||
.is_some_and(|b| b.attribute("match").is_some_and(|m| m.trim() == "global") || b.attribute("ref").is_some_and(|r| !r.trim().is_empty())),
|
||||
scripts,
|
||||
calculate: child(n, "calculate").and_then(|c| child(c, "script")).map(|s| parse_script(s, "calculate")),
|
||||
validate: validate_el.and_then(|v| child(v, "script")).map(|s| parse_script(s, "validate")),
|
||||
|
||||
@@ -42,6 +42,11 @@ pub const DATE_KEY: &[u8] = b"PCXfaDate";
|
||||
/// fields they hold.
|
||||
pub const NODE_KEY: &[u8] = b"PCXfaNode";
|
||||
|
||||
/// Private key on each page laying the form out writes: the name of the page area the page was
|
||||
/// made from. The n-th page with a name is that area's `[n-1]` instance, which scripts address
|
||||
/// even when no field on the page is left to show it (see `page_area_uses`).
|
||||
pub const AREA_KEY: &[u8] = b"PCXfaArea";
|
||||
|
||||
pub struct Written {
|
||||
pub pages: usize,
|
||||
pub fields: usize,
|
||||
@@ -871,6 +876,9 @@ impl Emitter<'_> {
|
||||
pd.set(b"MediaBox".to_vec(), Object::Array([0.0, 0.0, page.width, page.height].iter().map(|v| Object::Real(*v)).collect()));
|
||||
pd.set(b"Resources".to_vec(), Object::Dict(res));
|
||||
pd.set(b"Contents".to_vec(), Object::Ref(contents));
|
||||
if let Some(area) = &page.area {
|
||||
pd.set(AREA_KEY.to_vec(), Object::String(PdfString::text(area)));
|
||||
}
|
||||
if !annots.is_empty() {
|
||||
pd.set(b"Annots".to_vec(), Object::Array(annots));
|
||||
}
|
||||
|
||||
+135
-82
@@ -418,32 +418,48 @@ impl Walker<'_> {
|
||||
/// from the template and `ov`) and every scripted event, in document order, within
|
||||
/// [`MAX_FORM_NODES`] objects.
|
||||
pub fn form_tree(tpl: &Template, data: Option<&DataNode>, ov: &Overrides) -> LiveForm {
|
||||
form_tree_paged(tpl, data, ov, &HashMap::new())
|
||||
}
|
||||
|
||||
/// [`form_tree`] for a laid-out form whose page areas made `uses` pages each, by name
|
||||
/// ([`page_area_uses`]): the master content of each of those pages is a node of its own
|
||||
/// (`master[0]`, `master[1]`, …), as the layout names its fields, so a button repeated on every
|
||||
/// page runs its click script as itself. A page area not in `uses` gets one node.
|
||||
pub fn form_tree_paged(tpl: &Template, data: Option<&DataNode>, ov: &Overrides, uses: &HashMap<String, usize>) -> LiveForm {
|
||||
let mut w = Walker { tpl, data, ov, events: Vec::new(), script_bytes: 0, nodes: 0, truncated: false };
|
||||
let root = Node::Subform(Box::new(tpl.root.clone()));
|
||||
let mut nodes = w.children(std::slice::from_ref(&root), "", 0);
|
||||
let mut root_node = nodes.pop().unwrap_or_default();
|
||||
// Page masters' fields (Print and Save buttons, page numbers) belong to the layout model
|
||||
// in XFA; they hang off the root here, under their page area's name, as the layouter
|
||||
// names them, so their click scripts can run.
|
||||
// in XFA; they hang off the root here, under their page area's name, one node per page made
|
||||
// from it, as the layouter names them, so their click scripts can run.
|
||||
if let Some(ps) = &tpl.root.page_set {
|
||||
for (i, area) in ps.areas.iter().enumerate() {
|
||||
let som = area.name.clone().map(|n| format!("{n}[0]")).unwrap_or_else(|| format!("#pageArea[{i}]"));
|
||||
let children = w.children(&area.items, &som, 1);
|
||||
if children.is_empty() {
|
||||
continue;
|
||||
// A name two areas share stays one node: its pages can't be told apart by it.
|
||||
let unique = area.name.as_ref().is_some_and(|n| ps.areas.iter().filter(|a| a.name.as_ref() == Some(n)).count() == 1);
|
||||
let pages = match &area.name {
|
||||
Some(n) if unique => uses.get(n).copied().unwrap_or(1).clamp(1, crate::layout::MAX_PAGES),
|
||||
_ => 1,
|
||||
};
|
||||
for page in 0..pages {
|
||||
let som = area.name.clone().map(|n| format!("{n}[{page}]")).unwrap_or_else(|| format!("#pageArea[{i}]"));
|
||||
let children = w.children(&area.items, &som, 1);
|
||||
if children.is_empty() {
|
||||
break;
|
||||
}
|
||||
root_node.children.push(FormNode {
|
||||
name: area.name.clone().unwrap_or_else(|| "#pageArea".into()),
|
||||
som,
|
||||
kind: Some(NodeKind::Area),
|
||||
presence: "visible".into(),
|
||||
access: "open".into(),
|
||||
occur_min: 1,
|
||||
occur_max: Some(1),
|
||||
index: page,
|
||||
children,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
root_node.children.push(FormNode {
|
||||
name: area.name.clone().unwrap_or_else(|| "#pageArea".into()),
|
||||
som,
|
||||
kind: Some(NodeKind::Area),
|
||||
presence: "visible".into(),
|
||||
access: "open".into(),
|
||||
occur_min: 1,
|
||||
occur_max: Some(1),
|
||||
index: 0,
|
||||
children,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
LiveForm { root: root_node, events: w.events, truncated: w.truncated }
|
||||
@@ -495,81 +511,84 @@ pub fn has_focus_scripts(tpl: &Template) -> bool {
|
||||
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;
|
||||
}
|
||||
@@ -656,6 +675,40 @@ pub fn reset_default(tpl: &Template, som: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many pages the layout made from each named page area of `tpl`: the master content of the
|
||||
/// n-th page from area `master` is `master[n-1]`. Each page the layout wrote names its area
|
||||
/// ([`crate::AREA_KEY`]), so a page whose master fields are all hidden still counts; a file laid
|
||||
/// out before pages were named shows it by its generated fields. Areas neither shows are left out
|
||||
/// (one page's worth, to [`form_tree_paged`]).
|
||||
pub fn page_area_uses(doc: &Document, tpl: &Template) -> HashMap<String, usize> {
|
||||
let mut out: HashMap<String, usize> = HashMap::new();
|
||||
let Some(ps) = &tpl.root.page_set else { return out };
|
||||
let is_area = |name: &str| tpl.root.common.name.as_deref() != Some(name) && ps.areas.iter().any(|a| a.name.as_deref() == Some(name));
|
||||
// The flat page list laying the form out writes.
|
||||
let pages = doc.root().and_then(|r| doc.get(r).as_dict().and_then(|c| c.get(b"Pages")).map(|p| doc.resolve(p)));
|
||||
let kids =
|
||||
pages.and_then(|p| p.as_dict().and_then(|d| d.get(b"Kids")).map(|k| doc.resolve(k))).and_then(|k| k.as_array().cloned()).unwrap_or_default();
|
||||
for page in kids.iter().filter_map(Object::as_ref).take(crate::layout::MAX_PAGES) {
|
||||
let page = doc.get(page);
|
||||
let Some(name) = page.as_dict().and_then(|d| d.get(crate::AREA_KEY)).and_then(Object::as_string).map(PdfString::to_text) else { continue };
|
||||
if is_area(&name) {
|
||||
let pages = out.entry(name).or_insert(0);
|
||||
*pages = pages.saturating_add(1).min(crate::layout::MAX_PAGES);
|
||||
}
|
||||
}
|
||||
for som in fields_by_som(doc).keys() {
|
||||
let first = som.split('.').next().unwrap_or_default();
|
||||
let Some((name, rest)) = first.split_once('[') else { continue };
|
||||
let Some(index) = rest.strip_suffix(']').and_then(|i| i.parse::<usize>().ok()) else { continue };
|
||||
if !is_area(name) {
|
||||
continue;
|
||||
}
|
||||
let pages = out.entry(name.to_string()).or_insert(1);
|
||||
*pages = (*pages).max(index.saturating_add(1)).min(crate::layout::MAX_PAGES);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Lay the form out again from its template, data and overrides, replacing the pages and the
|
||||
/// generated fields (a script added a row or hid a subform).
|
||||
pub fn rerender(doc: &mut Document, tpl: &Template) -> Result<Report, XfaError> {
|
||||
@@ -663,7 +716,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()));
|
||||
}
|
||||
|
||||
@@ -2783,6 +2783,47 @@ fn page_area_without_occur_serves_every_body_page() {
|
||||
assert_eq!(soms[4].iter().filter(|s| s.contains("PrintButton")).count(), 1, "the required page is laid out once: {soms:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_master_content_repeated_on_each_page_has_its_own_som_index() {
|
||||
// One page area (no occur limit) serves three body pages and carries a field in its own
|
||||
// master content. Each page's copy of that field gets the page area's instance index, so the
|
||||
// three do not share one SOM path (and one data node).
|
||||
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>
|
||||
<field name="total" x="1in" y="10in" w="1in" h="0.3in"><ui><textEdit/></ui><bind match="global"/></field>
|
||||
</pageArea>
|
||||
</pageSet>
|
||||
{}{}{}
|
||||
</subform>
|
||||
</template>
|
||||
</xdp:xdp>
|
||||
"##,
|
||||
section(1),
|
||||
section(2),
|
||||
section(3)
|
||||
);
|
||||
let form = layout_xml(&xdp).unwrap();
|
||||
let page_nos: Vec<String> = form.pages.iter().flat_map(|p| widgets(p)).filter(|w| w.som.contains("pageNo")).map(|w| w.som.clone()).collect();
|
||||
assert_eq!(page_nos, ["Body[0].pageNo[0]", "Body[1].pageNo[0]", "Body[2].pageNo[0]"], "each page's master field has its own index");
|
||||
// Its data path keeps that index, so each page binds apart; a field whose bind is global
|
||||
// shares one data node, so every page binds it as instance 0 (XFA 3.3, "Page Background").
|
||||
let (tpl, _) = crate::parse(&xdp).unwrap();
|
||||
let dp = |som: &str| crate::data_path(&tpl, som);
|
||||
assert_eq!(dp("Body[2].pageNo[0]"), crate::som_to_path("Body[2].pageNo[0]"));
|
||||
assert_eq!(dp("Body[2].total[0]"), crate::som_to_path("Body[0].total[0]"), "a global field binds as instance 0 on each page");
|
||||
}
|
||||
|
||||
/// A page area's `occur min` is a page it must have even when the body never reaches it, and a
|
||||
/// body that already used it does not get another. An area with no `<occur>` needs none.
|
||||
#[test]
|
||||
@@ -5295,6 +5336,75 @@ fn a_row_outside_a_table_keeps_its_minimum_height() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
|
||||
/// Hidden master widgets still count their page area, and removed pages leave no
|
||||
/// stale count when a page set has several page areas (wireless89, #5368).
|
||||
#[test]
|
||||
fn page_area_uses_follow_hidden_masters_and_shrink_with_multiple_areas() {
|
||||
let xml = fixtures::repeated_print_button_template()
|
||||
.replace(
|
||||
"<pageSet><pageArea name=\"master\"><occur max=\"-1\"/>",
|
||||
r#"<pageSet><pageArea name="Cover"><occur max="1"/><contentArea w="540pt" h="648pt"/><medium short="612pt" long="792pt"/>
|
||||
<field name="hidden" presence="hidden"><ui><textEdit/></ui></field></pageArea><pageArea name="master"><occur max="-1"/>"#,
|
||||
)
|
||||
.replace("<occur max=\"-1\"/>", "<occur min=\"0\" max=\"-1\"/>")
|
||||
.replace("name=\"PrintButton1\"", "name=\"PrintButton1\" presence=\"hidden\"");
|
||||
let (tpl, _) = parse(&xml).unwrap();
|
||||
let mut doc = Document::open(Arc::new(shell(&xml))).unwrap();
|
||||
render_into(&mut doc).unwrap();
|
||||
let uses = page_area_uses(&doc, &tpl);
|
||||
assert_eq!(uses.get("Cover"), Some(&1));
|
||||
assert_eq!(uses.get("master"), Some(&2));
|
||||
let mut ov = Overrides::default();
|
||||
ov.presence.insert("form[0].page3[0]".into(), "hidden".into());
|
||||
set_overrides(&mut doc, &ov).unwrap();
|
||||
rerender(&mut doc, &tpl).unwrap();
|
||||
assert_eq!(page_area_uses(&doc, &tpl).get("master"), Some(&1));
|
||||
ov.presence.insert("form[0].page2[0]".into(), "hidden".into());
|
||||
set_overrides(&mut doc, &ov).unwrap();
|
||||
rerender(&mut doc, &tpl).unwrap();
|
||||
let uses = page_area_uses(&doc, &tpl);
|
||||
assert_eq!(uses.get("Cover"), Some(&1));
|
||||
assert!(!uses.contains_key("master"), "removed pages leave no stale occurrence metadata: {uses:?}");
|
||||
}
|
||||
|
||||
/// The pages of `form[0].last[0].f[0]` and of the field `after` that follows `last`, when `last`
|
||||
/// holds `before` ahead of `f` and `after` behind it. A breakAfter acts after its subform, so the
|
||||
/// page of `after` shows whether it broke.
|
||||
|
||||
Reference in new issue
Block a user