Files
pdfcraft/crates/forms/src/scripting.rs
T

369 lines
15 KiB
Rust

//! Field scripts beyond the AF calls: the hook through which the engine runs JavaScript.
//!
//! `forms` does not depend on a JavaScript engine (architecture §3: `forms → js` is not an
//! allowed edge). It calls a [`Scripts`] implementation at the points Acrobat runs field
//! events — Keystroke (on commit) and Validate before a value is accepted, Calculate in the
//! calculation order, Format when the appearance is drawn — and applies what the script asks
//! for: the event's value, `rc`, and changes to other fields.
use std::collections::HashSet;
use pdfcraft_cos::{Document, ObjRef, Object, PdfString};
use crate::{Field, FieldKind, FieldValue, FormError, fields, flags};
/// The field events forms run scripts for.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FieldEvent {
Keystroke,
Validate,
Calculate,
Format,
}
impl FieldEvent {
/// `event.name`.
pub fn name(self) -> &'static str {
match self {
FieldEvent::Keystroke => "Keystroke",
FieldEvent::Validate => "Validate",
FieldEvent::Calculate => "Calculate",
FieldEvent::Format => "Format",
}
}
}
/// A change a script made to a field (`None`: unchanged).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FieldChange {
pub name: String,
pub value: Option<Vec<String>>,
pub read_only: Option<bool>,
pub required: Option<bool>,
/// `display.visible` 0, `hidden` 1, `noPrint` 2, `noView` 3.
pub display: Option<i32>,
/// Move/resize the first widget, in PDF user space.
pub rect: Option<[f64; 4]>,
}
/// What a script did.
#[derive(Clone, Debug, PartialEq)]
pub struct ScriptResult {
/// `event.rc`.
pub rc: bool,
/// `event.value` afterwards.
pub value: String,
/// Whether the script assigned `event.value`, even the value it already had.
pub value_set: bool,
pub changes: Vec<FieldChange>,
/// Why it rejected the value (its alert), for the error shown.
pub message: Option<String>,
}
/// Runs field scripts (JavaScript).
pub trait Scripts {
/// Run `script` for `event` on `target`, whose value is `value`; `fields` is the whole form.
fn run(&mut self, event: FieldEvent, script: &str, target: &Field, value: &str, fields: &[Field]) -> ScriptResult;
/// [`Self::run`] again for an appearance, with dates written in English names (`util.printd`)
/// because no font of the field can draw the date language's (#5303); what it logs was
/// already logged by the first run. `None` when it can't: no English dates here, or the
/// script failed. What it returns is what the script made, even when that is the value.
fn rerun_with_english_dates(
&mut self,
_event: FieldEvent,
_script: &str,
_target: &Field,
_value: &str,
_fields: &[Field],
) -> Option<ScriptResult> {
None
}
}
/// No script engine (or JavaScript turned off): scripts change nothing.
pub struct NoScripts;
impl Scripts for NoScripts {
fn run(&mut self, _: FieldEvent, _: &str, _: &Field, value: &str, _: &[Field]) -> ScriptResult {
ScriptResult { rc: true, value: value.to_string(), value_set: false, changes: Vec::new(), message: None }
}
}
/// Run the Keystroke (commit) and Validate scripts for a new text value. Returns the value to
/// store, or why it is rejected.
pub(crate) fn accept(doc: &mut Document, f: &Field, value: &str, scripts: &mut dyn Scripts) -> Result<String, FormError> {
let mut v = value.to_string();
for (event, script, default) in [
(FieldEvent::Keystroke, &f.actions.scripts.keystroke, "The value entered does not match the format of the field"),
(FieldEvent::Validate, &f.actions.scripts.validate, "The value entered is not valid for the field"),
] {
let Some(js) = script else { continue };
let all = fields(doc);
let r = scripts.run(event, js, f, &v, &all);
apply_changes(doc, &r.changes, &f.name)?;
if !r.rc {
return Err(FormError::Invalid(r.message.unwrap_or_else(|| format!("{default} [ {} ]", f.name))));
}
v = r.value;
}
Ok(v)
}
/// The text a custom Format script shows for `value`, if the field has one.
pub(crate) fn formatted(doc: &Document, f: &Field, value: &str, scripts: &mut dyn Scripts) -> Option<String> {
let js = f.actions.scripts.format.as_ref()?;
let r = scripts.run(FieldEvent::Format, js, f, value, &fields(doc));
Some(r.value)
}
/// [`formatted`] with dates in English names, for widgets whose fonts can't draw the date
/// language's; `None` when the field has no Format script or the English run didn't happen.
pub(crate) fn formatted_in_english(doc: &Document, f: &Field, value: &str, scripts: &mut dyn Scripts) -> Option<String> {
let js = f.actions.scripts.format.as_ref()?;
scripts.rerun_with_english_dates(FieldEvent::Format, js, f, value, &fields(doc)).map(|r| r.value)
}
/// Apply script changes to other fields (values written without re-running their scripts;
/// read-only, required and visibility as flags). `except` is the event's own field, whose value
/// the event sets.
pub(crate) fn apply_changes(doc: &mut Document, changes: &[FieldChange], except: &str) -> Result<(), FormError> {
if changes.is_empty() {
return Ok(());
}
let all = fields(doc);
for c in changes {
let Some(f) = all.iter().find(|f| f.name == c.name) else { continue };
if let Some(v) = &c.value
&& c.name != except
&& *v != f.value
{
let value = match f.kind {
FieldKind::Text => FieldValue::Text(v.first().cloned().unwrap_or_default()),
FieldKind::CheckBox => FieldValue::Check(!v.is_empty()),
FieldKind::Radio => FieldValue::Radio(v.first().cloned()),
FieldKind::Combo | FieldKind::List => FieldValue::Choice(v.clone()),
_ => continue,
};
let mut tmp = f.clone();
tmp.flags &= !flags::NO_TOGGLE_TO_OFF;
tmp.actions = Default::default();
crate::write_value(doc, &tmp, &value, &mut NoScripts)?;
}
let mut ff = f.flags;
for (on, bit) in [(c.read_only, flags::READ_ONLY), (c.required, flags::REQUIRED)] {
match on {
Some(true) => ff |= bit,
Some(false) => ff &= !bit,
None => {}
}
}
if ff != f.flags {
doc.update_dict(f.obj, |d| d.set(b"Ff".to_vec(), Object::Int(ff as i64)))?;
}
if let Some(display) = c.display {
for w in &f.widgets {
let old = doc.get(w.obj).as_dict().and_then(|d| d.int(b"F")).unwrap_or(0);
// Hidden 2, Print 4, NoView 32 (Table 167).
let base = old & !(2 | 4 | 32);
let new = match display {
1 => base | 2 | 4,
2 => base,
3 => base | 4 | 32,
_ => base | 4,
};
if new != old {
doc.update_dict(w.obj, |d| d.set(b"F".to_vec(), Object::Int(new)))?;
}
}
}
if let Some(rect) = c.rect {
crate::set_props(doc, &c.name, &crate::FieldProps { rect: Some((0, rect)), ..Default::default() })?;
}
}
Ok(())
}
/// Apply what a button or console script changed (values, read-only, required, visibility),
/// then recalculate.
pub fn apply_script_changes(doc: &mut Document, changes: &[FieldChange], scripts: &mut dyn Scripts) -> Result<(), FormError> {
apply_changes(doc, changes, "")?;
crate::recalculate_with(doc, scripts)?;
Ok(())
}
/// Document-level JavaScripts (the catalog's `/Names /JavaScript` tree), in name order: they
/// define the functions field scripts call.
pub fn document_scripts(doc: &Document) -> Vec<String> {
document_scripts_named(doc).into_iter().map(|(_, js)| js).collect()
}
/// Most `/Kids` entries the document JavaScript name tree walk reads in all.
const MAX_QUEUED_KIDS: usize = 1 << 20;
/// [`document_scripts`] with their names.
pub fn document_scripts_named(doc: &Document) -> Vec<(String, String)> {
let Some(root) = doc.root() else { return Vec::new() };
let cat = doc.get(root);
let Some(names) = cat.as_dict().and_then(|c| c.get(b"Names")).map(|n| doc.resolve(n)) else { return Vec::new() };
let Some(tree) = names.as_dict().and_then(|n| n.get(b"JavaScript")).cloned() else { return Vec::new() };
let mut out: Vec<(String, String)> = Vec::new();
let mut scripts = crate::script_budget();
// Kids are indirect references (ISO 32000-2 §7.9.6), queued unresolved, each the first time
// it is listed: a node listed many times, or among its own kids, must not multiply the work,
// and the stack holds only references (a direct object there would be copied whole, once
// for every node listing it). Only the root may be direct. Nodes are told apart by object
// number, as `Document::get` finds them, so `5 0 R` and `5 1 R` are one node.
let mut seen: HashSet<u32> = tree.as_ref().map(|r| r.num).into_iter().collect();
// The `/Names` arrays read, by object number: nodes sharing one would otherwise each list
// all its scripts again.
let mut names_read: HashSet<u32> = HashSet::new();
let mut stack = vec![tree];
let mut visited = 0;
// Every `/Kids` entry read, over the whole walk. Nodes sharing one long `/Kids` array would
// otherwise read all of it once per node.
let mut queued = 0usize;
while let Some(raw) = stack.pop() {
visited += 1;
if visited > 10_000 {
break;
}
let node = doc.resolve(&raw);
let Some(d) = node.as_dict() else { continue };
if let Some(kids) = d.get(b"Kids") {
in_array(doc, kids, |kids| {
// Past the limit nothing more is read, but the nodes already queued still are.
// Pushed in reverse, so the kids are read in the order listed.
let room = MAX_QUEUED_KIDS.saturating_sub(queued);
queued = queued.saturating_add(kids.len().min(room));
let new: Vec<ObjRef> = kids.iter().take(room).filter_map(Object::as_ref).filter(|r| seen.insert(r.num)).collect();
stack.extend(new.into_iter().rev().map(Object::Ref));
});
}
if let Some(pairs) = d.get(b"Names")
&& pairs.as_ref().is_none_or(|r| names_read.insert(r.num))
{
in_array(doc, pairs, |pairs| {
for pair in pairs.chunks(2) {
let [k, v] = pair else { continue };
let key = match &*doc.resolve(k) {
Object::String(s) => s.to_text(),
_ => continue,
};
if let Some(js) = crate::script(doc, v, &mut scripts) {
out.push((key, js));
}
}
});
}
}
out.sort();
out
}
/// Call `f` on the array `o` is, or refers to, read in place: resolving a direct array would
/// copy it.
fn in_array(doc: &Document, o: &Object, f: impl FnOnce(&[Object])) {
match o {
Object::Ref(r) => {
if let Some(a) = doc.get(*r).as_array() {
f(a);
}
}
Object::Array(a) => f(a),
_ => {}
}
}
/// Add (or replace) a document-level JavaScript named `name`.
pub fn set_document_script(doc: &mut Document, name: &str, js: Option<&str>) -> Result<(), FormError> {
let root = doc.root().ok_or(FormError::NoForm)?;
let cat = doc.get(root).as_dict().cloned().unwrap_or_default();
let mut names = cat.get(b"Names").map(|n| doc.resolve(n)).and_then(|n| n.as_dict().cloned()).unwrap_or_default();
let tree = names.get(b"JavaScript").map(|t| doc.resolve(t)).and_then(|t| t.as_dict().cloned()).unwrap_or_default();
if tree.contains(b"Kids") {
return Err(FormError::Invalid("the document's JavaScript name tree has kids; edit it with a full editor".into()));
}
let mut pairs: Vec<Object> = tree.get(b"Names").map(|n| doc.resolve(n)).and_then(|n| n.as_array().cloned()).unwrap_or_default();
let mut kept = Vec::new();
for pair in pairs.chunks(2) {
if let [k, v] = pair
&& !matches!(&*doc.resolve(k), Object::String(s) if s.to_text() == name)
{
kept.push((k.clone(), v.clone()));
}
}
if let Some(js) = js {
let mut action = pdfcraft_cos::Dict::new();
action.set(b"S".to_vec(), Object::name("JavaScript"));
action.set(b"JS".to_vec(), PdfString::text(js));
let a = doc.add(Object::Dict(action));
kept.push((Object::String(PdfString::text(name)), Object::Ref(a)));
}
kept.sort_by_key(|(k, _)| match k {
Object::String(s) => s.to_text(),
_ => String::new(),
});
pairs = kept.into_iter().flat_map(|(k, v)| [k, v]).collect();
let mut t = pdfcraft_cos::Dict::new();
t.set(b"Names".to_vec(), Object::Array(pairs));
names.set(b"JavaScript".to_vec(), Object::Dict(t));
doc.update_dict(root, |c| c.set(b"Names".to_vec(), Object::Dict(names)))?;
Ok(())
}
/// Field Properties ▸ Format / Validate / Calculate ▸ custom script, or Actions ▸ Mouse Up ▸
/// Run a JavaScript: set (or remove, with `None`) field `name`'s script for `event` — one of
/// `keystroke`, `format`, `validate`, `calculate` (the field's `/AA` K, F, V, C) or `mouse_up`
/// (its widgets' `/A`). Calculated fields join the calculation order (`/CO`).
pub fn set_field_script(doc: &mut Document, name: &str, event: &str, js: Option<&str>) -> Result<(), FormError> {
let all = fields(doc);
let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?.clone();
let action = |doc: &mut Document, js: &str| {
let mut a = pdfcraft_cos::Dict::new();
a.set(b"S".to_vec(), Object::name("JavaScript"));
a.set(b"JS".to_vec(), PdfString::text(js));
Object::Ref(doc.add(Object::Dict(a)))
};
if event == "mouse_up" {
for w in &f.widgets {
let a = js.map(|j| action(doc, j));
doc.update_dict(w.obj, |d| match a {
Some(a) => d.set(b"A".to_vec(), a),
None => {
d.remove(b"A");
}
})?;
}
return Ok(());
}
let key: &[u8] = match event {
"keystroke" => b"K",
"format" => b"F",
"validate" => b"V",
"calculate" => b"C",
other => return Err(FormError::Invalid(format!("unknown field event {other:?} (keystroke, format, validate, calculate, mouse_up)"))),
};
let mut aa = doc.get(f.obj).as_dict().and_then(|d| d.get(b"AA").map(|a| doc.resolve(a))).and_then(|a| a.as_dict().cloned()).unwrap_or_default();
match js {
Some(j) => {
let a = action(doc, j);
aa.set(key.to_vec(), a);
}
None => {
aa.remove(key);
}
}
doc.update_dict(f.obj, |d| {
if aa.is_empty() {
d.remove(b"AA");
} else {
d.set(b"AA".to_vec(), Object::Dict(aa));
}
})?;
if key == b"C" {
crate::calc_order::keep_listed(doc, f.obj, js.is_some())?;
}
Ok(())
}