extern crate alloc;
use crate::annotation::Annotation;
use crate::types::*;
use pdf_syntax::object::dict::keys::*;
use pdf_syntax::object::{Dict, Name, Object};
#[derive(Debug)]
pub struct LinkAnnotation {
pub action: Option<Action>,
pub destination: Option<Destination>,
pub highlight_mode: HighlightMode,
pub quad_points: Option<QuadPoints>,
}
impl LinkAnnotation {
pub fn from_annot(annot: &Annotation<'_>) -> Self {
let dict = annot.dict();
let action = dict.get::<Dict<'_>>(A).map(|d| Action::from_dict(&d));
let destination = if action.is_none() {
dict.get::<Object<'_>>(DEST).and_then(parse_destination)
} else {
None
};
let highlight_mode = dict
.get::<Name>(H)
.map(|n| match n.as_ref() {
b"N" => HighlightMode::None,
b"O" => HighlightMode::Outline,
b"P" => HighlightMode::Push,
_ => HighlightMode::Invert,
})
.unwrap_or(HighlightMode::Invert);
let quad_points = annot.quad_points();
Self {
action,
destination,
highlight_mode,
quad_points,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionType {
Uri,
GoTo,
GoToR,
Named,
JavaScript,
SubmitForm,
Launch,
ImportData,
Unknown(alloc::string::String),
}
impl ActionType {
pub fn from_name(name: &str) -> Self {
match name {
"URI" => Self::Uri,
"GoTo" => Self::GoTo,
"GoToR" => Self::GoToR,
"Named" => Self::Named,
"JavaScript" => Self::JavaScript,
"SubmitForm" => Self::SubmitForm,
"Launch" => Self::Launch,
"ImportData" => Self::ImportData,
_ => Self::Unknown(name.into()),
}
}
pub fn is_inert_on_flatten(&self) -> bool {
matches!(
self,
Self::JavaScript | Self::SubmitForm | Self::Launch | Self::ImportData
)
}
}
#[derive(Debug, Clone)]
pub enum Action {
Uri(alloc::string::String),
GoTo(Destination),
GoToR {
file: alloc::string::String,
destination: Option<Destination>,
},
Named(alloc::string::String),
JavaScript(alloc::string::String),
SubmitForm {
target: Option<alloc::string::String>,
},
Launch {
file: Option<alloc::string::String>,
},
ImportData {
file: Option<alloc::string::String>,
},
Unknown(alloc::string::String),
}
impl Action {
pub fn from_dict(dict: &Dict<'_>) -> Self {
let action_type = dict
.get::<Name>(S)
.map(|n| alloc::string::String::from(n.as_str()))
.unwrap_or_default();
match ActionType::from_name(action_type.as_str()) {
ActionType::Uri => {
let uri = dict
.get::<pdf_syntax::object::String>(URI)
.map(|s| crate::annotation::pdf_string_to_string(&s))
.unwrap_or_default();
Self::Uri(uri)
}
ActionType::GoTo => {
let dest = dict
.get::<Object<'_>>(D)
.and_then(parse_destination)
.unwrap_or(Destination::Fit { page_index: None });
Self::GoTo(dest)
}
ActionType::GoToR => {
let file = file_spec_string(dict).unwrap_or_default();
let destination = dict.get::<Object<'_>>(D).and_then(parse_destination);
Self::GoToR { file, destination }
}
ActionType::Named => {
let name = dict
.get::<Name>(N)
.map(|n| alloc::string::String::from(n.as_str()))
.unwrap_or_default();
Self::Named(name)
}
ActionType::JavaScript => {
let js = dict
.get::<pdf_syntax::object::String>(JS)
.map(|s| crate::annotation::pdf_string_to_string(&s))
.unwrap_or_default();
Self::JavaScript(js)
}
ActionType::SubmitForm => Self::SubmitForm {
target: file_spec_string(dict),
},
ActionType::Launch => Self::Launch {
file: file_spec_string(dict),
},
ActionType::ImportData => Self::ImportData {
file: file_spec_string(dict),
},
ActionType::Unknown(action_type) => Self::Unknown(action_type),
}
}
pub fn action_type(&self) -> ActionType {
match self {
Self::Uri(_) => ActionType::Uri,
Self::GoTo(_) => ActionType::GoTo,
Self::GoToR { .. } => ActionType::GoToR,
Self::Named(_) => ActionType::Named,
Self::JavaScript(_) => ActionType::JavaScript,
Self::SubmitForm { .. } => ActionType::SubmitForm,
Self::Launch { .. } => ActionType::Launch,
Self::ImportData { .. } => ActionType::ImportData,
Self::Unknown(action_type) => ActionType::Unknown(action_type.clone()),
}
}
}
fn file_spec_string(dict: &Dict<'_>) -> Option<alloc::string::String> {
dict.get::<pdf_syntax::object::String>(F)
.map(|s| crate::annotation::pdf_string_to_string(&s))
.or_else(|| {
dict.get::<Dict<'_>>(F).and_then(|fs| {
fs.get::<pdf_syntax::object::String>(UF)
.or_else(|| fs.get::<pdf_syntax::object::String>(F))
.map(|s| crate::annotation::pdf_string_to_string(&s))
})
})
}
#[derive(Debug, Clone)]
pub enum Destination {
Xyz {
page_index: Option<u32>,
left: Option<f32>,
top: Option<f32>,
zoom: Option<f32>,
},
Fit {
page_index: Option<u32>,
},
FitH {
page_index: Option<u32>,
top: Option<f32>,
},
FitV {
page_index: Option<u32>,
left: Option<f32>,
},
FitR {
page_index: Option<u32>,
left: f32,
bottom: f32,
right: f32,
top: f32,
},
FitB {
page_index: Option<u32>,
},
FitBH {
page_index: Option<u32>,
top: Option<f32>,
},
FitBV {
page_index: Option<u32>,
left: Option<f32>,
},
Named(alloc::string::String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HighlightMode {
None,
Invert,
Outline,
Push,
}
pub fn parse_destination(obj: Object<'_>) -> Option<Destination> {
match obj {
Object::Array(arr) => {
let mut iter = arr.flex_iter();
let page_index = iter.next::<i32>().map(|n| n as u32);
let dest_type = iter.next::<Name>()?;
match dest_type.as_ref() {
b"XYZ" => Some(Destination::Xyz {
page_index,
left: iter.next::<f32>(),
top: iter.next::<f32>(),
zoom: iter.next::<f32>(),
}),
b"Fit" => Some(Destination::Fit { page_index }),
b"FitB" => Some(Destination::FitB { page_index }),
b"FitH" => Some(Destination::FitH {
page_index,
top: iter.next::<f32>(),
}),
b"FitBH" => Some(Destination::FitBH {
page_index,
top: iter.next::<f32>(),
}),
b"FitV" => Some(Destination::FitV {
page_index,
left: iter.next::<f32>(),
}),
b"FitBV" => Some(Destination::FitBV {
page_index,
left: iter.next::<f32>(),
}),
b"FitR" => Some(Destination::FitR {
page_index,
left: iter.next::<f32>().unwrap_or(0.0),
bottom: iter.next::<f32>().unwrap_or(0.0),
right: iter.next::<f32>().unwrap_or(0.0),
top: iter.next::<f32>().unwrap_or(0.0),
}),
_ => None,
}
}
Object::Name(name) => Some(Destination::Named(alloc::string::String::from(
name.as_str(),
))),
Object::String(s) => Some(Destination::Named(crate::annotation::pdf_string_to_string(
&s,
))),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn security_sensitive_actions_are_known_types() {
assert_eq!(ActionType::from_name("SubmitForm"), ActionType::SubmitForm);
assert_eq!(ActionType::from_name("Launch"), ActionType::Launch);
assert_eq!(ActionType::from_name("ImportData"), ActionType::ImportData);
}
#[test]
fn security_sensitive_actions_are_inert_on_flatten() {
assert!(ActionType::JavaScript.is_inert_on_flatten());
assert!(ActionType::SubmitForm.is_inert_on_flatten());
assert!(ActionType::Launch.is_inert_on_flatten());
assert!(ActionType::ImportData.is_inert_on_flatten());
assert!(!ActionType::GoTo.is_inert_on_flatten());
assert!(!ActionType::Uri.is_inert_on_flatten());
}
#[test]
fn unknown_action_type_remains_auditable() {
assert_eq!(
ActionType::from_name("VendorAction"),
ActionType::Unknown("VendorAction".into())
);
}
}