use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectStore};
use crate::destination::{Destination, parse_destination};
use crate::error::{DocError, DocResult};
const MAX_ACTION_CHAIN_DEPTH: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ActionType {
Unsupported = 0,
GoTo = 1,
RemoteGoTo = 2,
Uri = 3,
Launch = 4,
EmbeddedGoTo = 5,
}
#[derive(Debug, Clone)]
pub enum Action {
GoTo(Destination),
Uri(String),
Named(String),
GoToR {
file: String,
dest: Destination,
new_window: Option<bool>,
},
Launch { file: String },
JavaScript { code: String },
SubmitForm { url: String, fields: Vec<String> },
ResetForm { fields: Vec<String> },
ImportData { file: String },
Sound,
Movie,
Rendition,
GoToE {
file_spec: Option<String>,
destination: Option<String>,
new_window: Option<bool>,
},
Thread { thread_ref: Option<String> },
Hide { target: Option<String>, hide: bool },
SetOCGState { state: Vec<String> },
Trans,
GoTo3DView,
Unknown(String),
}
impl Action {
pub fn has_fields(&self) -> bool {
!self.all_fields().is_empty()
}
pub fn all_fields(&self) -> &[String] {
match self {
Action::SubmitForm { fields, .. } => fields,
Action::ResetForm { fields } => fields,
_ => &[],
}
}
#[deprecated(since = "0.1.0", note = "use all_fields() instead")]
#[inline]
pub fn fields(&self) -> &[String] {
self.all_fields()
}
pub fn action_type(&self) -> ActionType {
match self {
Action::GoTo(_) => ActionType::GoTo,
Action::GoToR { .. } => ActionType::RemoteGoTo,
Action::Uri(_) => ActionType::Uri,
Action::Launch { .. } => ActionType::Launch,
Action::GoToE { .. } => ActionType::EmbeddedGoTo,
_ => ActionType::Unsupported,
}
}
#[inline]
pub fn action_get_type(&self) -> ActionType {
self.action_type()
}
#[deprecated(note = "use `action_get_type()` — matches upstream `FPDFAction_GetType`")]
#[inline]
pub fn get_type(&self) -> ActionType {
self.action_type()
}
pub fn dest(&self) -> Option<&Destination> {
match self {
Action::GoTo(d) => Some(d),
Action::GoToR { dest, .. } => Some(dest),
_ => None,
}
}
#[inline]
pub fn action_get_dest(&self) -> Option<&Destination> {
self.dest()
}
#[deprecated(note = "use `action_get_dest()` — matches upstream `FPDFAction_GetDest`")]
#[inline]
pub fn get_dest(&self) -> Option<&Destination> {
self.dest()
}
pub fn file_path(&self) -> Option<&str> {
match self {
Action::Launch { file } => Some(file),
Action::GoToR { file, .. } => Some(file),
_ => None,
}
}
#[inline]
pub fn action_get_file_path(&self) -> Option<&str> {
self.file_path()
}
#[deprecated(note = "use `action_get_file_path()` — matches upstream `FPDFAction_GetFilePath`")]
#[inline]
pub fn get_file_path(&self) -> Option<&str> {
self.file_path()
}
pub fn uri_path(&self) -> Option<&str> {
match self {
Action::Uri(uri) => Some(uri),
_ => None,
}
}
#[inline]
pub fn action_get_uri_path(&self) -> Option<&str> {
self.uri_path()
}
#[deprecated(note = "use `action_get_uri_path()` — matches upstream `FPDFAction_GetURIPath`")]
#[inline]
pub fn get_uri_path(&self) -> Option<&str> {
self.uri_path()
}
#[deprecated(note = "use `action_get_uri_path()` — matches upstream `FPDFAction_GetURIPath`")]
#[inline]
pub fn get_uri(&self) -> Option<&str> {
self.uri_path()
}
pub fn hide_status(&self) -> Option<bool> {
match self {
Action::Hide { hide, .. } => Some(*hide),
_ => None,
}
}
#[inline]
pub fn get_hide_status(&self) -> Option<bool> {
self.hide_status()
}
pub fn named_action(&self) -> Option<&str> {
match self {
Action::Named(name) => Some(name),
_ => None,
}
}
#[inline]
pub fn get_named_action(&self) -> Option<&str> {
self.named_action()
}
pub fn flags(&self) -> Option<u32> {
match self {
Action::SubmitForm { .. } => Some(0),
_ => None,
}
}
#[inline]
pub fn get_flags(&self) -> Option<u32> {
self.flags()
}
#[inline]
pub fn get_all_fields(&self) -> &[String] {
self.all_fields()
}
pub fn maybe_javascript(&self) -> Option<&str> {
match self {
Action::JavaScript { code } => Some(code),
_ => None,
}
}
#[inline]
pub fn maybe_get_javascript(&self) -> Option<&str> {
self.maybe_javascript()
}
pub fn javascript(&self) -> String {
match self {
Action::JavaScript { code } => code.clone(),
_ => String::new(),
}
}
#[deprecated(note = "use `javascript()` — there is no public `FPDFAction_GetJavaScript` API")]
#[inline]
pub fn get_javascript(&self) -> String {
self.javascript()
}
}
#[derive(Debug, Clone)]
pub struct ActionChain {
pub action: Action,
pub next: Vec<ActionChain>,
}
impl ActionChain {
pub fn sub_action_count(&self) -> usize {
self.next.len()
}
#[inline]
pub fn get_sub_actions_count(&self) -> usize {
self.sub_action_count()
}
pub fn sub_action(&self, index: usize) -> Option<&ActionChain> {
self.next.get(index)
}
#[inline]
pub fn get_sub_action(&self, index: usize) -> Option<&ActionChain> {
self.sub_action(index)
}
}
pub fn parse_action_chain<S: PdfSource>(
obj: &Object,
store: &ObjectStore<S>,
) -> DocResult<ActionChain> {
parse_action_chain_inner(obj, store, 0)
}
fn parse_action_chain_inner<S: PdfSource>(
obj: &Object,
store: &ObjectStore<S>,
depth: usize,
) -> DocResult<ActionChain> {
if depth >= MAX_ACTION_CHAIN_DEPTH {
return Err(DocError::DepthExceeded);
}
let action = parse_action(obj, store)?;
let resolved = store
.deep_resolve(obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let dict = resolved.as_dict().ok_or(DocError::UnexpectedType)?;
let next = if let Some(next_obj) = dict.get(&Name::next()) {
let next_resolved = store
.deep_resolve(next_obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
if let Some(arr) = next_resolved.as_array() {
let mut chain = Vec::new();
for item in arr {
if let Ok(sub) = parse_action_chain_inner(item, store, depth + 1) {
chain.push(sub);
}
}
chain
} else if next_resolved.as_dict().is_some() {
match parse_action_chain_inner(next_obj, store, depth + 1) {
Ok(sub) => vec![sub],
Err(_) => Vec::new(),
}
} else {
Vec::new()
}
} else {
Vec::new()
};
Ok(ActionChain { action, next })
}
pub fn parse_action<S: PdfSource>(obj: &Object, store: &ObjectStore<S>) -> DocResult<Action> {
let resolved = store
.deep_resolve(obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let dict = resolved.as_dict().ok_or(DocError::UnexpectedType)?;
let subtype = dict
.get(&Name::s())
.and_then(|o| o.as_name())
.map(|n| n.as_str().into_owned())
.ok_or_else(|| DocError::MissingKey("/S".into()))?;
match subtype.as_str() {
"GoTo" => {
let dest_obj = dict
.get(&Name::d())
.ok_or_else(|| DocError::MissingKey("/D".into()))?;
let dest = parse_destination(dest_obj, store)?;
Ok(Action::GoTo(dest))
}
"URI" => {
let uri_obj = dict
.get(&Name::uri())
.ok_or_else(|| DocError::MissingKey("/URI".into()))?;
let resolved_uri = store
.deep_resolve(uri_obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let uri = resolved_uri
.as_string()
.map(|s| s.to_string_lossy())
.ok_or(DocError::UnexpectedType)?;
Ok(Action::Uri(uri))
}
"Named" => {
let name_obj = dict
.get(&Name::n())
.ok_or_else(|| DocError::MissingKey("/N".into()))?;
let resolved_name = store
.deep_resolve(name_obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let name = resolved_name
.as_name()
.map(|n| n.as_str().into_owned())
.ok_or(DocError::UnexpectedType)?;
Ok(Action::Named(name))
}
"GoToR" => {
let file_obj = dict
.get(&Name::f())
.ok_or_else(|| DocError::MissingKey("/F".into()))?;
let resolved_file = store
.deep_resolve(file_obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let file = resolved_file
.as_string()
.map(|s| s.to_string_lossy())
.ok_or(DocError::UnexpectedType)?;
let dest_obj = dict
.get(&Name::d())
.ok_or_else(|| DocError::MissingKey("/D".into()))?;
let dest = parse_destination(dest_obj, store)?;
let new_window = dict.get(&Name::new_window()).and_then(|o| o.as_bool());
Ok(Action::GoToR {
file,
dest,
new_window,
})
}
"Launch" => {
let file_obj = dict
.get(&Name::f())
.ok_or_else(|| DocError::MissingKey("/F".into()))?;
let resolved_file = store
.deep_resolve(file_obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let file = resolved_file
.as_string()
.map(|s| s.to_string_lossy())
.ok_or(DocError::UnexpectedType)?;
Ok(Action::Launch { file })
}
"JavaScript" => {
let code = extract_js_code(dict, store)?;
Ok(Action::JavaScript { code })
}
"SubmitForm" => {
let url = extract_file_string(dict, store).unwrap_or_default();
let fields = extract_fields_array(dict, store);
Ok(Action::SubmitForm { url, fields })
}
"ResetForm" => {
let fields = extract_fields_array(dict, store);
Ok(Action::ResetForm { fields })
}
"ImportData" => {
let file = extract_file_string(dict, store)
.ok_or_else(|| DocError::MissingKey("/F".into()))?;
Ok(Action::ImportData { file })
}
"Sound" => Ok(Action::Sound),
"Movie" => Ok(Action::Movie),
"Rendition" => Ok(Action::Rendition),
"GoToE" => {
let file_spec = extract_file_string(dict, store);
let destination = dict
.get(&Name::d())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let new_window = dict.get(&Name::new_window()).and_then(|o| o.as_bool());
Ok(Action::GoToE {
file_spec,
destination,
new_window,
})
}
"Thread" => {
let thread_ref = dict
.get(&Name::d())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
Ok(Action::Thread { thread_ref })
}
"Hide" => {
let target = dict
.get(&Name::t())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let hide = dict
.get(&Name::h())
.and_then(|o| o.as_bool())
.unwrap_or(true);
Ok(Action::Hide { target, hide })
}
"SetOCGState" => {
let state = dict
.get(&Name::state())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| {
o.as_array().map(|arr| {
arr.iter()
.filter_map(|item| item.as_name().map(|n| n.as_str().into_owned()))
.collect::<Vec<String>>()
})
})
.unwrap_or_default();
Ok(Action::SetOCGState { state })
}
"Trans" => Ok(Action::Trans),
"GoTo3DView" => Ok(Action::GoTo3DView),
other => Ok(Action::Unknown(other.to_string())),
}
}
fn extract_js_code<S: PdfSource>(
dict: &std::collections::HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> DocResult<String> {
let js_obj = dict
.get(&Name::js())
.ok_or_else(|| DocError::MissingKey("/JS".into()))?;
let resolved = store
.deep_resolve(js_obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
if let Some(s) = resolved.as_string() {
return Ok(s.to_string_lossy());
}
if resolved.as_stream_dict().is_some() {
let data = store
.decode_stream(resolved)
.map_err(|e| DocError::Parser(e.to_string()))?;
return Ok(String::from_utf8_lossy(&data).into_owned());
}
Err(DocError::UnexpectedType)
}
fn extract_file_string<S: PdfSource>(
dict: &std::collections::HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Option<String> {
let f_obj = dict.get(&Name::f())?;
let resolved = store.deep_resolve(f_obj).ok()?;
if let Some(s) = resolved.as_string() {
Some(s.to_string_lossy())
} else if let Some(f_dict) = resolved.as_dict() {
f_dict
.get(&Name::f())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()))
} else {
None
}
}
fn extract_fields_array<S: PdfSource>(
dict: &std::collections::HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Vec<String> {
let fields_obj = match dict.get(&Name::fields()) {
Some(o) => o,
None => return Vec::new(),
};
let resolved = match store.deep_resolve(fields_obj).ok() {
Some(o) => o,
None => return Vec::new(),
};
let arr = match resolved.as_array() {
Some(a) => a,
None => return Vec::new(),
};
arr.iter()
.filter_map(|item| {
let r = store.deep_resolve(item).ok()?;
if let Some(s) = r.as_string() {
Some(s.to_string_lossy())
} else {
r.as_name().map(|n| n.as_str().into_owned())
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
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_goto_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("GoTo")));
dict.insert(
Name::d(),
Object::String(rpdfium_core::PdfString::from_bytes(b"chapter1".to_vec())),
);
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::GoTo(Destination::Named(name)) => assert_eq!(name, "chapter1"),
_ => panic!("expected GoTo with named destination"),
}
}
#[test]
fn test_uri_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("URI")));
dict.insert(
Name::uri(),
Object::String(rpdfium_core::PdfString::from_bytes(
b"https://example.com".to_vec(),
)),
);
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::Uri(uri) => assert_eq!(uri, "https://example.com"),
_ => panic!("expected Uri action"),
}
}
#[test]
fn test_named_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("Named")));
dict.insert(Name::n(), Object::Name(Name::from("NextPage")));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::Named(name) => assert_eq!(name, "NextPage"),
_ => panic!("expected Named action"),
}
}
#[test]
fn test_goto_r_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("GoToR")));
dict.insert(
Name::f(),
Object::String(rpdfium_core::PdfString::from_bytes(b"other.pdf".to_vec())),
);
dict.insert(
Name::d(),
Object::String(rpdfium_core::PdfString::from_bytes(b"target".to_vec())),
);
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::GoToR {
file,
dest,
new_window,
} => {
assert_eq!(file, "other.pdf");
assert!(new_window.is_none());
match dest {
Destination::Named(name) => assert_eq!(name, "target"),
_ => panic!("expected named dest"),
}
}
_ => panic!("expected GoToR action"),
}
}
#[test]
fn test_javascript_action_string() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("JavaScript")));
dict.insert(Name::js(), str_obj("app.alert('Hello');"));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::JavaScript { code } => assert_eq!(code, "app.alert('Hello');"),
_ => panic!("expected JavaScript action"),
}
}
#[test]
fn test_submit_form_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("SubmitForm")));
dict.insert(Name::f(), str_obj("https://example.com/submit"));
dict.insert(
Name::fields(),
Object::Array(vec![str_obj("name"), str_obj("email")]),
);
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::SubmitForm { url, fields } => {
assert_eq!(url, "https://example.com/submit");
assert_eq!(fields, vec!["name", "email"]);
}
_ => panic!("expected SubmitForm action"),
}
}
#[test]
fn test_reset_form_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("ResetForm")));
dict.insert(
Name::fields(),
Object::Array(vec![str_obj("field1"), str_obj("field2")]),
);
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::ResetForm { fields } => {
assert_eq!(fields, vec!["field1", "field2"]);
}
_ => panic!("expected ResetForm action"),
}
}
#[test]
fn test_import_data_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("ImportData")));
dict.insert(Name::f(), str_obj("data.fdf"));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::ImportData { file } => assert_eq!(file, "data.fdf"),
_ => panic!("expected ImportData action"),
}
}
#[test]
fn test_sound_movie_rendition_detection() {
let store = build_store();
for (action_type, expected_variant) in [
("Sound", "Sound"),
("Movie", "Movie"),
("Rendition", "Rendition"),
] {
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from(action_type)));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
let variant = format!("{action:?}");
assert!(
variant.starts_with(expected_variant),
"expected {expected_variant} variant, got {variant}"
);
}
}
#[test]
fn test_javascript_from_js_string_value() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("JavaScript")));
dict.insert(
Name::js(),
Object::String(rpdfium_core::PdfString::from_bytes(
b"this.print();".to_vec(),
)),
);
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::JavaScript { code } => assert_eq!(code, "this.print();"),
_ => panic!("expected JavaScript action"),
}
}
#[test]
fn test_parse_action_chain_no_next() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("Named")));
dict.insert(Name::n(), Object::Name(Name::from("PrevPage")));
let obj = Object::Dictionary(dict);
let chain = parse_action_chain(&obj, &store).unwrap();
match &chain.action {
Action::Named(n) => assert_eq!(n, "PrevPage"),
_ => panic!("expected Named action"),
}
assert!(chain.next.is_empty());
}
#[test]
fn test_parse_action_chain_single_next() {
let store = build_store();
let mut sub = HashMap::new();
sub.insert(Name::s(), Object::Name(Name::from("Sound")));
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("Named")));
dict.insert(Name::n(), Object::Name(Name::from("FirstPage")));
dict.insert(Name::next(), Object::Dictionary(sub));
let obj = Object::Dictionary(dict);
let chain = parse_action_chain(&obj, &store).unwrap();
assert_eq!(chain.next.len(), 1);
assert!(matches!(&chain.next[0].action, Action::Sound));
}
#[test]
fn test_parse_action_chain_array_next() {
let store = build_store();
let mut sub1 = HashMap::new();
sub1.insert(Name::s(), Object::Name(Name::from("Sound")));
let mut sub2 = HashMap::new();
sub2.insert(Name::s(), Object::Name(Name::from("Movie")));
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("Named")));
dict.insert(Name::n(), Object::Name(Name::from("LastPage")));
dict.insert(
Name::next(),
Object::Array(vec![Object::Dictionary(sub1), Object::Dictionary(sub2)]),
);
let obj = Object::Dictionary(dict);
let chain = parse_action_chain(&obj, &store).unwrap();
assert_eq!(chain.next.len(), 2);
assert!(matches!(&chain.next[0].action, Action::Sound));
assert!(matches!(&chain.next[1].action, Action::Movie));
}
#[test]
fn test_parse_action_chain_nested() {
let store = build_store();
let mut inner = HashMap::new();
inner.insert(Name::s(), Object::Name(Name::from("Movie")));
let mut middle = HashMap::new();
middle.insert(Name::s(), Object::Name(Name::from("Sound")));
middle.insert(Name::next(), Object::Dictionary(inner));
let mut outer = HashMap::new();
outer.insert(Name::s(), Object::Name(Name::from("Named")));
outer.insert(Name::n(), Object::Name(Name::from("FirstPage")));
outer.insert(Name::next(), Object::Dictionary(middle));
let obj = Object::Dictionary(outer);
let chain = parse_action_chain(&obj, &store).unwrap();
assert_eq!(chain.next.len(), 1);
assert_eq!(chain.next[0].next.len(), 1);
assert!(matches!(&chain.next[0].next[0].action, Action::Movie));
}
#[test]
fn test_goto_r_with_new_window() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("GoToR")));
dict.insert(
Name::f(),
Object::String(rpdfium_core::PdfString::from_bytes(b"doc.pdf".to_vec())),
);
dict.insert(
Name::d(),
Object::String(rpdfium_core::PdfString::from_bytes(b"page1".to_vec())),
);
dict.insert(Name::new_window(), Object::Boolean(true));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::GoToR { new_window, .. } => {
assert_eq!(new_window, Some(true));
}
_ => panic!("expected GoToR action"),
}
}
#[test]
fn test_goto_e_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("GoToE")));
dict.insert(Name::f(), str_obj("embedded.pdf"));
dict.insert(Name::d(), str_obj("page1"));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::GoToE {
file_spec,
destination,
new_window,
} => {
assert_eq!(file_spec.as_deref(), Some("embedded.pdf"));
assert_eq!(destination.as_deref(), Some("page1"));
assert!(new_window.is_none());
}
_ => panic!("expected GoToE action"),
}
}
#[test]
fn test_thread_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("Thread")));
dict.insert(Name::d(), str_obj("thread-1"));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::Thread { thread_ref } => {
assert_eq!(thread_ref.as_deref(), Some("thread-1"));
}
_ => panic!("expected Thread action"),
}
}
#[test]
fn test_hide_action_default_true() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("Hide")));
dict.insert(Name::t(), str_obj("annot-1"));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::Hide { target, hide } => {
assert_eq!(target.as_deref(), Some("annot-1"));
assert!(hide);
}
_ => panic!("expected Hide action"),
}
}
#[test]
fn test_hide_action_explicit_false() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("Hide")));
dict.insert(Name::t(), str_obj("annot-2"));
dict.insert(Name::h(), Object::Boolean(false));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::Hide { target, hide } => {
assert_eq!(target.as_deref(), Some("annot-2"));
assert!(!hide);
}
_ => panic!("expected Hide action"),
}
}
#[test]
fn test_set_ocg_state_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("SetOCGState")));
dict.insert(
Name::state(),
Object::Array(vec![
Object::Name(Name::from("ON")),
Object::Name(Name::from("OFF")),
Object::Name(Name::from("Toggle")),
]),
);
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::SetOCGState { state } => {
assert_eq!(state, vec!["ON", "OFF", "Toggle"]);
}
_ => panic!("expected SetOCGState action"),
}
}
#[test]
fn test_trans_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("Trans")));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
assert!(matches!(action, Action::Trans));
}
#[test]
fn test_go_to_3d_view_action() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("GoTo3DView")));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
assert!(matches!(action, Action::GoTo3DView));
}
#[test]
fn test_unknown_action_type_preserved() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::s(), Object::Name(Name::from("FutureAction")));
let obj = Object::Dictionary(dict);
let action = parse_action(&obj, &store).unwrap();
match action {
Action::Unknown(s) => assert_eq!(s, "FutureAction"),
_ => panic!("expected Unknown action"),
}
}
#[test]
fn test_submit_form_has_fields() {
let action = Action::SubmitForm {
url: "https://example.com".into(),
fields: vec!["name".into(), "email".into()],
};
assert!(action.has_fields());
assert_eq!(action.all_fields(), &["name", "email"]);
}
#[test]
fn test_submit_form_no_fields() {
let action = Action::SubmitForm {
url: "https://example.com".into(),
fields: vec![],
};
assert!(!action.has_fields());
assert!(action.all_fields().is_empty());
}
#[test]
fn test_reset_form_has_fields() {
let action = Action::ResetForm {
fields: vec!["field1".into()],
};
assert!(action.has_fields());
assert_eq!(action.all_fields(), &["field1"]);
}
#[test]
fn test_non_form_action_no_fields() {
let action = Action::Named("NextPage".into());
assert!(!action.has_fields());
assert!(action.all_fields().is_empty());
}
}