use std::collections::HashMap;
use rpdfium_core::PdfSource;
use rpdfium_parser::{Object, ObjectStore};
use crate::action::{Action, parse_action};
use crate::error::{DocError, DocResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AActionType {
CursorEnter,
CursorExit,
ButtonDown,
ButtonUp,
GetFocus,
LoseFocus,
PageOpen,
PageClose,
PageVisible,
PageInvisible,
OpenPage,
ClosePage,
KeyStroke,
Format,
Validate,
Calculate,
CloseDocument,
SaveDocument,
DocumentSaved,
PrintDocument,
DocumentPrinted,
DocumentOpen,
}
impl AActionType {
pub fn pdf_key(self) -> Option<&'static str> {
match self {
Self::CursorEnter => Some("E"),
Self::CursorExit => Some("X"),
Self::ButtonDown => Some("D"),
Self::ButtonUp => Some("U"),
Self::GetFocus => Some("Fo"),
Self::LoseFocus => Some("Bl"),
Self::PageOpen => Some("PO"),
Self::PageClose => Some("PC"),
Self::PageVisible => Some("PV"),
Self::PageInvisible => Some("PI"),
Self::OpenPage => Some("O"),
Self::ClosePage => Some("C"),
Self::KeyStroke => Some("K"),
Self::Format => Some("F"),
Self::Validate => Some("V"),
Self::Calculate => Some("C"),
Self::CloseDocument => Some("WC"),
Self::SaveDocument => Some("WS"),
Self::DocumentSaved => Some("DS"),
Self::PrintDocument => Some("WP"),
Self::DocumentPrinted => Some("DP"),
Self::DocumentOpen => None,
}
}
pub fn is_user_input(self) -> bool {
matches!(self, Self::ButtonUp | Self::ButtonDown | Self::KeyStroke)
}
}
#[derive(Debug, Clone)]
pub struct AdditionalActions {
pub entries: HashMap<String, Action>,
}
impl AdditionalActions {
pub fn action_exists(&self, event_type: AActionType) -> bool {
event_type
.pdf_key()
.is_some_and(|key| self.entries.contains_key(key))
}
pub fn action(&self, event_type: AActionType) -> Option<&Action> {
let key = event_type.pdf_key()?;
self.entries.get(key)
}
#[deprecated(
note = "use `action()` — there is no public `FPDFAAction_GetAction` API (internal `CPDF_AAction::GetAction`)"
)]
#[inline]
pub fn get_action(&self, event_type: AActionType) -> Option<&Action> {
self.action(event_type)
}
}
pub fn parse_additional_actions<S: PdfSource>(
obj: &Object,
store: &ObjectStore<S>,
) -> DocResult<AdditionalActions> {
let resolved = store
.deep_resolve(obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let dict = resolved.as_dict().ok_or(DocError::UnexpectedType)?;
let mut entries = HashMap::new();
for (key, value) in dict {
let event_name = key.as_str().into_owned();
if let Ok(action) = parse_action(value, store) {
entries.insert(event_name, action);
}
}
Ok(AdditionalActions { entries })
}
#[cfg(test)]
mod tests {
use super::*;
use rpdfium_core::Name;
use rpdfium_parser::ObjectStore;
use std::collections::HashMap;
fn build_store() -> ObjectStore<Vec<u8>> {
let pdf = build_minimal_pdf();
ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
}
fn build_minimal_pdf() -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
pdf
}
fn str_obj(s: &str) -> Object {
Object::String(rpdfium_core::PdfString::from_bytes(s.as_bytes().to_vec()))
}
#[test]
fn test_aaction_type_pdf_key() {
assert_eq!(AActionType::CursorEnter.pdf_key(), Some("E"));
assert_eq!(AActionType::CursorExit.pdf_key(), Some("X"));
assert_eq!(AActionType::ButtonDown.pdf_key(), Some("D"));
assert_eq!(AActionType::ButtonUp.pdf_key(), Some("U"));
assert_eq!(AActionType::GetFocus.pdf_key(), Some("Fo"));
assert_eq!(AActionType::LoseFocus.pdf_key(), Some("Bl"));
assert_eq!(AActionType::PageOpen.pdf_key(), Some("PO"));
assert_eq!(AActionType::PageClose.pdf_key(), Some("PC"));
assert_eq!(AActionType::KeyStroke.pdf_key(), Some("K"));
assert_eq!(AActionType::Format.pdf_key(), Some("F"));
assert_eq!(AActionType::Validate.pdf_key(), Some("V"));
assert_eq!(AActionType::CloseDocument.pdf_key(), Some("WC"));
assert_eq!(AActionType::SaveDocument.pdf_key(), Some("WS"));
assert_eq!(AActionType::DocumentSaved.pdf_key(), Some("DS"));
assert_eq!(AActionType::PrintDocument.pdf_key(), Some("WP"));
assert_eq!(AActionType::DocumentPrinted.pdf_key(), Some("DP"));
assert_eq!(AActionType::DocumentOpen.pdf_key(), None);
}
#[test]
fn test_aaction_type_is_user_input() {
assert!(AActionType::ButtonUp.is_user_input());
assert!(AActionType::ButtonDown.is_user_input());
assert!(AActionType::KeyStroke.is_user_input());
assert!(!AActionType::CursorEnter.is_user_input());
assert!(!AActionType::GetFocus.is_user_input());
assert!(!AActionType::SaveDocument.is_user_input());
}
#[test]
fn test_additional_actions_get_action() {
let store = build_store();
let mut e_action = HashMap::new();
e_action.insert(Name::s(), Object::Name(Name::from("Named")));
e_action.insert(Name::n(), Object::Name(Name::from("NextPage")));
let mut aa_dict = HashMap::new();
aa_dict.insert(Name::from("E"), Object::Dictionary(e_action));
let obj = Object::Dictionary(aa_dict);
let aa = parse_additional_actions(&obj, &store).unwrap();
assert!(aa.action_exists(AActionType::CursorEnter));
assert!(!aa.action_exists(AActionType::ButtonDown));
assert!(aa.action(AActionType::CursorEnter).is_some());
assert!(aa.action(AActionType::ButtonDown).is_none());
assert!(!aa.action_exists(AActionType::DocumentOpen));
assert!(aa.action(AActionType::DocumentOpen).is_none());
}
#[test]
fn test_parse_additional_actions_multiple_events() {
let store = build_store();
let mut e_action = HashMap::new();
e_action.insert(Name::s(), Object::Name(Name::from("Named")));
e_action.insert(Name::n(), Object::Name(Name::from("NextPage")));
let mut fo_action = HashMap::new();
fo_action.insert(Name::s(), Object::Name(Name::from("URI")));
fo_action.insert(Name::uri(), str_obj("https://example.com"));
let mut aa_dict = HashMap::new();
aa_dict.insert(Name::from("E"), Object::Dictionary(e_action));
aa_dict.insert(Name::from("Fo"), Object::Dictionary(fo_action));
let obj = Object::Dictionary(aa_dict);
let aa = parse_additional_actions(&obj, &store).unwrap();
assert_eq!(aa.entries.len(), 2);
assert!(aa.entries.contains_key("E"));
assert!(aa.entries.contains_key("Fo"));
match &aa.entries["E"] {
Action::Named(n) => assert_eq!(n, "NextPage"),
_ => panic!("expected Named action"),
}
}
#[test]
fn test_parse_additional_actions_empty() {
let store = build_store();
let aa_dict: HashMap<Name, Object> = HashMap::new();
let obj = Object::Dictionary(aa_dict);
let aa = parse_additional_actions(&obj, &store).unwrap();
assert!(aa.entries.is_empty());
}
#[test]
fn test_parse_additional_actions_skips_invalid() {
let store = build_store();
let mut aa_dict = HashMap::new();
let bad = HashMap::new();
aa_dict.insert(Name::from("K"), Object::Dictionary(bad));
let mut good = HashMap::new();
good.insert(Name::s(), Object::Name(Name::from("Sound")));
aa_dict.insert(Name::from("V"), Object::Dictionary(good));
let obj = Object::Dictionary(aa_dict);
let aa = parse_additional_actions(&obj, &store).unwrap();
assert_eq!(aa.entries.len(), 1);
assert!(aa.entries.contains_key("V"));
}
}