mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 03:27:21 -06:00
Merge PR #3582: M12: showing an XFA subform leaves its unnamed hidden objects hidden
crates/xfa/src/script.rs: both sides added doc comments to the same functions; kept #3582's (they describe its presence(name, ...) signature).
This commit is contained in:
@@ -96,6 +96,8 @@ pub fn static_shell(data: &str) -> Vec<u8> {
|
||||
assemble(objs)
|
||||
}
|
||||
|
||||
/// A PDF file holding `objs` as objects 1, 2, … in order, with a cross-reference table and a
|
||||
/// trailer whose root is object 1.
|
||||
fn assemble(objs: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
@@ -114,6 +116,30 @@ fn assemble(objs: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
out
|
||||
}
|
||||
|
||||
/// A hidden subform `details` a script may show, holding a named field and three unnamed
|
||||
/// objects: a caption, an old caption the form keeps hidden, and a hidden subform. Unnamed
|
||||
/// objects have no SOM name of their own, so showing `details` must leave the hidden ones hidden.
|
||||
pub fn unnamed_hidden_template() -> String {
|
||||
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="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||||
<subform name="page1" layout="tb">
|
||||
<subform name="details" layout="tb" presence="hidden">
|
||||
<draw w="4in" h="0.3in"><value><text>Current caption</text></value></draw>
|
||||
<draw w="4in" h="0.3in" presence="hidden"><value><text>Old caption</text></value></draw>
|
||||
<subform layout="tb" presence="hidden"><draw w="4in" h="0.3in"><value><text>Old section</text></value></draw></subform>
|
||||
<field name="note" w="4in" h="0.3in"><ui><textEdit/></ui></field>
|
||||
</subform>
|
||||
</subform>
|
||||
</subform>
|
||||
</template>
|
||||
</xdp:xdp>
|
||||
"##
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A scripted form (JavaScript events): `qty` (initialised to 2, validated ≤ 100) × `price`
|
||||
/// (5) = `total` (calculated); a check box `more` that shows the hidden `details` subform;
|
||||
/// a table whose `row` (item, amount) repeats, with `addRow`, `removeRow` and `hello` buttons;
|
||||
|
||||
+28
-18
@@ -188,8 +188,9 @@ impl Walker<'_> {
|
||||
true
|
||||
}
|
||||
|
||||
/// Note the scripted events of the object at `som`: its event scripts, its calculation and
|
||||
/// its validation (with the message a failing one shows).
|
||||
/// Record the scripts of the object at `som` as events: each non-empty event script, then its
|
||||
/// calculate and validate scripts (the validate one with `message`). Once [`MAX_EVENTS`] events
|
||||
/// are recorded, nothing more is and the form is marked truncated.
|
||||
fn push_events(&mut self, som: &str, scripts: &[Script], calculate: Option<&Script>, validate: Option<&Script>, message: Option<&str>) {
|
||||
if self.events.len() >= MAX_EVENTS {
|
||||
self.truncated = true;
|
||||
@@ -231,18 +232,20 @@ impl Walker<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The presence of the object at `som`: a script's override, else the template's `p`.
|
||||
fn presence(&self, som: &str, p: Presence) -> String {
|
||||
self.ov.presence.get(som).cloned().unwrap_or_else(|| presence_name(p).to_string())
|
||||
/// The presence of the object named `name` at `som`: a script's, or else the template's. An
|
||||
/// unnamed object shares its parent's SOM path, so a presence set there is not its own.
|
||||
fn presence(&self, name: Option<&str>, som: &str, p: Presence) -> String {
|
||||
name.and_then(|_| self.ov.presence.get(som)).cloned().unwrap_or_else(|| presence_name(p).to_string())
|
||||
}
|
||||
|
||||
/// The access of the object at `som`: a script's override, else the template's `a`.
|
||||
/// The access of the object at `som`: a script's, or else the template's `a`.
|
||||
fn access(&self, som: &str, a: Access) -> String {
|
||||
self.ov.access.get(som).cloned().unwrap_or_else(|| access_name(a).to_string())
|
||||
}
|
||||
|
||||
/// The live node of field `f`, instance `index` under `parent_som`, valued from the data or
|
||||
/// its template default (`radio`: the value its exclusion group chose), with its events noted.
|
||||
/// The live node of field `f`, child `index` of `parent_som`, with its scripts recorded. In an
|
||||
/// exclusion group (`radio` is the group's selected value) its value is its on value when picked,
|
||||
/// else empty; elsewhere it is its data value (as saved) or its template default.
|
||||
fn field(&mut self, f: &Field, parent_som: &str, index: usize, radio: Option<&str>) -> FormNode {
|
||||
let som = child_som(parent_som, f.common.name.as_deref().or(Some("field")), index);
|
||||
let data_value = self.data.and_then(|d| d.value_at(&data_path(self.tpl, &som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
|
||||
@@ -262,7 +265,7 @@ impl Walker<'_> {
|
||||
kind: Some(NodeKind::Field),
|
||||
value,
|
||||
numeric: f.numeric,
|
||||
presence: self.presence(&som, f.common.presence),
|
||||
presence: self.presence(Some("field"), &som, f.common.presence),
|
||||
access: self.access(&som, f.access),
|
||||
repeatable: false,
|
||||
occur_min: 1,
|
||||
@@ -283,8 +286,9 @@ impl Walker<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The live nodes of template `nodes` under `parent_som`: as many instances of a subform as
|
||||
/// the data or its occurrence asks for, areas, fields, exclusion groups and draws.
|
||||
/// The live nodes of `nodes`, the children of `parent_som`, with their scripts recorded: one per
|
||||
/// instance of a repeating subform (what the data holds, else its initial count). Nothing past
|
||||
/// [`MAX_DEPTH`] is built, and building stops once the object budget is spent.
|
||||
fn children(&mut self, nodes: &[Node], parent_som: &str, depth: usize) -> Vec<FormNode> {
|
||||
let mut out = Vec::new();
|
||||
if depth > MAX_DEPTH {
|
||||
@@ -318,7 +322,7 @@ impl Walker<'_> {
|
||||
kind: Some(NodeKind::Subform),
|
||||
value: String::new(),
|
||||
numeric: false,
|
||||
presence: self.presence(&som, sf.common.presence),
|
||||
presence: self.presence(sf.common.name.as_deref(), &som, sf.common.presence),
|
||||
access: "open".into(),
|
||||
repeatable: sf.occur.max != Some(1),
|
||||
occur_min: sf.occur.min,
|
||||
@@ -336,7 +340,7 @@ impl Walker<'_> {
|
||||
name: a.common.name.clone().unwrap_or_else(|| "#area".into()),
|
||||
som: som.clone(),
|
||||
kind: Some(NodeKind::Area),
|
||||
presence: self.presence(&som, a.common.presence),
|
||||
presence: self.presence(a.common.name.as_deref(), &som, a.common.presence),
|
||||
access: "open".into(),
|
||||
occur_min: 1,
|
||||
occur_max: Some(1),
|
||||
@@ -353,7 +357,7 @@ impl Walker<'_> {
|
||||
som: som.clone(),
|
||||
kind: Some(NodeKind::Draw),
|
||||
value: d.value.plain().unwrap_or_default(),
|
||||
presence: self.presence(&som, d.common.presence),
|
||||
presence: self.presence(d.common.name.as_deref(), &som, d.common.presence),
|
||||
access: "open".into(),
|
||||
occur_min: 1,
|
||||
occur_max: Some(1),
|
||||
@@ -380,7 +384,7 @@ impl Walker<'_> {
|
||||
som: som.clone(),
|
||||
kind: Some(NodeKind::ExclGroup),
|
||||
value: selected.unwrap_or_default(),
|
||||
presence: self.presence(&som, g.common.presence),
|
||||
presence: self.presence(Some("group"), &som, g.common.presence),
|
||||
access: "open".into(),
|
||||
occur_min: 1,
|
||||
occur_max: Some(1),
|
||||
@@ -455,6 +459,9 @@ pub fn has_scripts(tpl: &Template) -> bool {
|
||||
/// 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;
|
||||
@@ -467,9 +474,12 @@ pub fn apply_overrides(tpl: &Template, ov: &Overrides) -> Template {
|
||||
Node::ExclGroup(_) => Some("group"),
|
||||
_ => None,
|
||||
};
|
||||
let som = child_som(parent_som, n.common().name.as_deref().or(name_default), sib);
|
||||
let presence = ov.presence.get(&som).map(|p| Presence::parse(Some(p)));
|
||||
let access = ov.access.get(&som).cloned();
|
||||
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 {
|
||||
|
||||
+44
-1
@@ -7,7 +7,9 @@ use crate::data::{
|
||||
DatasetsWrite, FieldData, FieldDatum, build_data, iso_to_pattern, parse_datasets, pattern_to_iso, read_values, som_to_path, write_datasets,
|
||||
};
|
||||
use crate::data::{add_data_instances, remove_data_instance, write_data_value};
|
||||
use crate::fixtures::{TINY_GIF, fields_template, scripted_template, shell, static_shell, template, template_with_data, tiny_png};
|
||||
use crate::fixtures::{
|
||||
TINY_GIF, fields_template, scripted_template, shell, static_shell, template, template_with_data, tiny_png, unnamed_hidden_template,
|
||||
};
|
||||
use crate::layout::Item;
|
||||
use crate::layout::WidgetKind;
|
||||
use crate::model::*;
|
||||
@@ -575,6 +577,9 @@ fn xfa_bytes(doc: &Document) -> Vec<u8> {
|
||||
s.decoded_within(1 << 20).unwrap()
|
||||
}
|
||||
|
||||
/// Writing field values into the datasets keeps every data node no field binds to as written,
|
||||
/// changes the bound values in place and adds a missing one beside them. Writing the same values
|
||||
/// again changes nothing, and a field bound to structured content is reported, not flattened.
|
||||
#[test]
|
||||
fn writing_values_keeps_the_data_no_field_binds_to() {
|
||||
// Regression: the data element was rebuilt from the fields alone, dropping everything else.
|
||||
@@ -615,6 +620,9 @@ fn writing_values_keeps_the_data_no_field_binds_to() {
|
||||
assert!(String::from_utf8(xfa_bytes(&doc)).unwrap().contains("<name><p>rich</p></name>"));
|
||||
}
|
||||
|
||||
/// A UTF-16 packet in either byte order, with a byte order mark, has its values read exactly and
|
||||
/// is written back as UTF-16 with its mark. A packet in neither UTF-8 nor UTF-16 is left alone,
|
||||
/// and that is reported.
|
||||
#[test]
|
||||
fn utf16_packets_are_read_and_written_back_as_utf16() {
|
||||
let mut doc = Document::open(Arc::new(static_shell("<form1><page1><name>Zoë 日本</name><other>ünbound</other></page1></form1>"))).unwrap();
|
||||
@@ -644,6 +652,9 @@ fn utf16_packets_are_read_and_written_back_as_utf16() {
|
||||
assert_eq!(xfa_bytes(&doc), latin1);
|
||||
}
|
||||
|
||||
/// Hundreds of fields with absurd SOM indices finish within seconds, both writing the datasets and
|
||||
/// building fresh data, create at most the node budget and report what they could not write. A
|
||||
/// moderate index still works, creating the empty instances before it.
|
||||
#[test]
|
||||
fn huge_som_indices_are_capped_not_built() {
|
||||
// Regression: each index created its missing siblings one count at a time (quadratic), with
|
||||
@@ -683,6 +694,9 @@ fn huge_som_indices_are_capped_not_built() {
|
||||
assert!(d.get(&som_to_path("form1[0].row[2]")).is_some(), "the instances in between exist, empty");
|
||||
}
|
||||
|
||||
/// The live form tree carries each object's SOM path, value, presence and access, one instance of
|
||||
/// a repeating row without data, and the template's event, calculate and validate scripts (FormCalc
|
||||
/// when no content type is given). Data adds rows and values, and an override changes presence.
|
||||
#[test]
|
||||
fn the_form_tree_carries_values_presence_instances_and_the_events() {
|
||||
let xml = scripted_template();
|
||||
@@ -717,6 +731,9 @@ fn the_form_tree_carries_values_presence_instances_and_the_events() {
|
||||
assert_eq!(page.children.iter().find(|c| c.name == "details").unwrap().kind, Some(NodeKind::Subform));
|
||||
}
|
||||
|
||||
/// Presence and access overrides are kept on the AcroForm and applied to the template: `note` has
|
||||
/// no widget while `details` is hidden and gets one once an override shows it and the form is laid
|
||||
/// out again. Saving and reopening keeps the overrides and one set of fields; storing none removes the key.
|
||||
#[test]
|
||||
fn overrides_live_in_the_acroform_and_hide_objects_in_the_layout() {
|
||||
let mut doc = Document::open(Arc::new(shell(&scripted_template()))).unwrap();
|
||||
@@ -752,6 +769,32 @@ fn overrides_live_in_the_acroform_and_hide_objects_in_the_layout() {
|
||||
assert!(overrides(&doc).is_empty());
|
||||
}
|
||||
|
||||
/// Showing a hidden subform through an override leaves its unnamed hidden draw and subform hidden,
|
||||
/// in the layout and in the live form scripts see: they share the subform's SOM path, so its
|
||||
/// override is not theirs.
|
||||
#[test]
|
||||
fn showing_a_subform_leaves_its_unnamed_hidden_objects_hidden() {
|
||||
let (tpl, _) = parse(&unnamed_hidden_template()).unwrap();
|
||||
let mut ov = Overrides::default();
|
||||
ov.presence.insert("form[0].page1[0].details[0]".into(), "visible".into());
|
||||
let form = layout(&apply_overrides(&tpl, &ov), None).unwrap();
|
||||
let shown = texts(&form.pages[0]);
|
||||
assert!(shown.iter().any(|t| t == "Current caption"), "{shown:?}");
|
||||
assert!(!shown.iter().any(|t| t == "Old caption" || t == "Old section"), "{shown:?}");
|
||||
assert_eq!(form.fields, 1);
|
||||
// Scripts see the same: the hidden draw stays hidden in the live form.
|
||||
let root = form_tree(&tpl, None, &ov).root;
|
||||
let details = &root.children[0].children[0];
|
||||
assert_eq!(details.presence, "visible");
|
||||
let presence = |text: &str| details.children.iter().find(|c| c.value == text).map(|c| c.presence.clone());
|
||||
assert_eq!(presence("Current caption").as_deref(), Some("visible"));
|
||||
assert_eq!(presence("Old caption").as_deref(), Some("hidden"));
|
||||
assert_eq!(details.children.iter().find(|c| c.kind == Some(NodeKind::Subform)).map(|c| c.presence.as_str()), Some("hidden"));
|
||||
}
|
||||
|
||||
/// Adding and removing data instances edits the datasets packet, creating it when missing: asking
|
||||
/// for no more rows than exist, or removing one that is not there, changes nothing, and removing a
|
||||
/// row moves the later ones up. The layout follows the data, and an absurd count is capped, not built.
|
||||
#[test]
|
||||
fn data_instances_are_added_and_removed_in_the_packet() {
|
||||
let mut doc = Document::open(Arc::new(shell(&scripted_template()))).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user