mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 22:02:33 +00:00
feat(forms): page_scripts reads a page's open/close actions and its annotations' page actions (#169)
Co-authored-by: Peter Yatsyshin <p.yatsyshin@googlemail.com>
This commit is contained in:
1 parent
e13d0a04ca
commit
a0b4c88ba8
4 files changed
+99
-4
No files matched your search
@@ -31,8 +31,8 @@ pub use author::{
|
||||
};
|
||||
pub use calc_order::{CalculatedField, CalculationIssue, CalculationOrder, calculation_order, set_calculation_order};
|
||||
pub use scripting::{
|
||||
FieldChange, FieldEvent, NoScripts, ScriptResult, Scripts, apply_script_changes, document_scripts, document_scripts_named, set_document_script,
|
||||
set_field_script,
|
||||
FieldChange, FieldEvent, NoScripts, PageEvent, PageScript, ScriptResult, Scripts, apply_script_changes, document_scripts, document_scripts_named,
|
||||
page_scripts, set_document_script, set_field_script,
|
||||
};
|
||||
|
||||
mod appearances;
|
||||
@@ -46,7 +46,7 @@ pub use read::selected_options;
|
||||
pub use read::{Fields, acroform, adopted_page_fields, fields, fields_with_adopted};
|
||||
#[cfg(test)]
|
||||
use read::{MAX_SCRIPT_BYTES, PageWalk, enumerate, form_reads};
|
||||
use read::{first_by_export, script, script_budget};
|
||||
use read::{first_by_export, qualified_name, script, script_budget};
|
||||
pub use tab_order::{TabOrder, move_in_tab_order, set_tab_order, tab_orders};
|
||||
#[cfg(test)]
|
||||
use values::has_password_flag;
|
||||
|
||||
@@ -85,7 +85,7 @@ pub(crate) fn action_fields(doc: &Document, o: &Object) -> Vec<String> {
|
||||
|
||||
/// The fully qualified name of the field that `r` (a field or one of its widgets) belongs to,
|
||||
/// from the partial names (`/T`) up its `/Parent` chain.
|
||||
fn qualified_name(doc: &Document, r: ObjRef) -> Option<String> {
|
||||
pub(crate) fn qualified_name(doc: &Document, r: ObjRef) -> Option<String> {
|
||||
let mut parts = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut cur = Some(r);
|
||||
|
||||
@@ -468,6 +468,73 @@ fn in_array(doc: &Document, o: &Object, f: impl FnOnce(&[Object])) {
|
||||
}
|
||||
}
|
||||
|
||||
/// When a page's actions run: as it becomes the page being read, or stops being it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PageEvent {
|
||||
Open,
|
||||
Close,
|
||||
}
|
||||
|
||||
/// A script a page runs as it opens or closes, with the field whose widget carries it (`None`:
|
||||
/// the page's own action, or an annotation that isn't a field's).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PageScript {
|
||||
pub field: Option<String>,
|
||||
pub script: String,
|
||||
}
|
||||
|
||||
/// Most annotations of one page whose actions are read.
|
||||
const MAX_PAGE_ANNOTATIONS: usize = 10_000;
|
||||
|
||||
/// The JavaScript page `page` (0-based) runs for `event`, in the order it runs: the page's own
|
||||
/// action (`/AA /O` or `/C`), then each annotation's, in the page's `/Annots` order (`/AA /PO`
|
||||
/// then `/PV` as the page opens, `/PC` then `/PI` as it closes). Other kinds of action, and
|
||||
/// actions chained with `/Next`, are left out.
|
||||
pub fn page_scripts(doc: &Document, page: usize, event: PageEvent) -> Vec<PageScript> {
|
||||
let Some(page_ref) = crate::page_refs(doc).get(page).copied() else { return Vec::new() };
|
||||
let page_obj = doc.get(page_ref);
|
||||
let Some(pd) = page_obj.as_dict() else { return Vec::new() };
|
||||
let (own, keys): (&[u8], [&[u8]; 2]) = match event {
|
||||
PageEvent::Open => (b"O", [b"PO", b"PV"]),
|
||||
PageEvent::Close => (b"C", [b"PC", b"PI"]),
|
||||
};
|
||||
let additional = |d: &pdfcraft_cos::Dict| d.get(b"AA").map(|a| doc.resolve(a)).and_then(|a| a.as_dict().cloned());
|
||||
let mut budget = crate::script_budget();
|
||||
let mut out = Vec::new();
|
||||
if let Some(action) = additional(pd).and_then(|aa| aa.get(own).cloned())
|
||||
&& let Some(script) = javascript(doc, &action, &mut budget)
|
||||
{
|
||||
out.push(PageScript { field: None, script });
|
||||
}
|
||||
let annots = pd.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default();
|
||||
for item in annots.iter().take(MAX_PAGE_ANNOTATIONS) {
|
||||
let annot = doc.resolve(item);
|
||||
let Some(ad) = annot.as_dict() else { continue };
|
||||
let Some(aa) = additional(ad) else { continue };
|
||||
let field = match item {
|
||||
Object::Ref(r) if ad.name(b"Subtype") == Some(b"Widget".as_slice()) => crate::qualified_name(doc, *r),
|
||||
_ => None,
|
||||
};
|
||||
for k in keys {
|
||||
if let Some(action) = aa.get(k)
|
||||
&& let Some(script) = javascript(doc, action, &mut budget)
|
||||
{
|
||||
out.push(PageScript { field: field.clone(), script });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The script of a JavaScript action (`/S /JavaScript`); `None` for any other action.
|
||||
fn javascript(doc: &Document, action: &Object, budget: &mut pdfcraft_cos::DecodeBudget) -> Option<String> {
|
||||
let a = doc.resolve(action);
|
||||
if a.as_dict()?.name(b"S")? != b"JavaScript" {
|
||||
return None;
|
||||
}
|
||||
crate::script(doc, action, budget)
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
|
||||
@@ -7317,3 +7317,31 @@ fn simple_calculation_counts_the_first_value_of_a_multiple_selection_list() {
|
||||
recalculate(&mut doc).unwrap();
|
||||
assert_eq!(field(&fields(&doc), "total").value, ["4"]);
|
||||
}
|
||||
|
||||
/// A page with open and close actions of its own (one script a stream), an animation's widget
|
||||
/// with page actions (one of them not a script), buttons acting on mouse down, and a screen
|
||||
/// annotation with a page-open action.
|
||||
fn page_actions_fixture() -> Document {
|
||||
document(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R 6 0 R] >> >>".into(), // 1
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), // 2
|
||||
"<< /Type /Page /Parent 2 0 R /AA << /O << /S /JavaScript /JS (page_open\\(\\);) >> /C << /S /JavaScript /JS 7 0 R >> >> /Annots [4 0 R 5 0 R 6 0 R 8 0 R] >>".into(), // 3
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (anim) /Rect [0 0 100 100] /P 3 0 R /AA << /PO << /S /JavaScript /JS (start\\(\\);) >> /PV << /S /JavaScript /JS (start\\(\\);) >> /PC << /S /JavaScript /JS (stop\\(\\);) >> /PI << /S /URI /URI (https://example.com) >> >> >>".into(), // 4
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (step) /Rect [0 0 10 10] /P 3 0 R /AA << /D << /S /JavaScript /JS (step\\(\\);) >> >> >>".into(), // 5
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (both) /Rect [20 0 30 10] /P 3 0 R /AA << /D << /S /JavaScript /JS (down\\(\\);) >> /U << /S /JavaScript /JS (up\\(\\);) >> >> >>".into(), // 6
|
||||
"<< /Length 7 >>\nstream\nclose()\nendstream".into(), // 7
|
||||
"<< /Type /Annot /Subtype /Screen /Rect [0 0 1 1] /AA << /PO << /S /JavaScript /JS (screen\\(\\);) >> >> >>".into(), // 8
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_scripts_are_the_pages_own_then_each_annotations_in_order() {
|
||||
let doc = page_actions_fixture();
|
||||
let open = page_scripts(&doc, 0, PageEvent::Open);
|
||||
let got: Vec<(Option<&str>, &str)> = open.iter().map(|s| (s.field.as_deref(), s.script.as_str())).collect();
|
||||
assert_eq!(got, [(None, "page_open();"), (Some("anim"), "start();"), (Some("anim"), "start();"), (None, "screen();")]);
|
||||
let close = page_scripts(&doc, 0, PageEvent::Close);
|
||||
let got: Vec<(Option<&str>, &str)> = close.iter().map(|s| (s.field.as_deref(), s.script.trim())).collect();
|
||||
assert_eq!(got, [(None, "close()"), (Some("anim"), "stop();")], "a URI action is not a script");
|
||||
assert!(page_scripts(&doc, 1, PageEvent::Open).is_empty(), "no such page");
|
||||
}
|
||||
Reference in new issue
Block a user