1extern crate alloc;
4
5use crate::annotation::Annotation;
6use crate::types::*;
7use pdf_syntax::object::dict::keys::*;
8use pdf_syntax::object::{Dict, Name, Object};
9
10#[derive(Debug)]
12pub struct LinkAnnotation {
13 pub action: Option<Action>,
15 pub destination: Option<Destination>,
17 pub highlight_mode: HighlightMode,
19 pub quad_points: Option<QuadPoints>,
21}
22
23impl LinkAnnotation {
24 pub fn from_annot(annot: &Annotation<'_>) -> Self {
26 let dict = annot.dict();
27 let action = dict.get::<Dict<'_>>(A).map(|d| Action::from_dict(&d));
28 let destination = if action.is_none() {
29 dict.get::<Object<'_>>(DEST).and_then(parse_destination)
30 } else {
31 None
32 };
33 let highlight_mode = dict
34 .get::<Name>(H)
35 .map(|n| match n.as_ref() {
36 b"N" => HighlightMode::None,
37 b"O" => HighlightMode::Outline,
38 b"P" => HighlightMode::Push,
39 _ => HighlightMode::Invert,
40 })
41 .unwrap_or(HighlightMode::Invert);
42 let quad_points = annot.quad_points();
43 Self {
44 action,
45 destination,
46 highlight_mode,
47 quad_points,
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum ActionType {
55 Uri,
56 GoTo,
57 GoToR,
58 Named,
59 JavaScript,
60 SubmitForm,
61 Launch,
62 ImportData,
63 Unknown(alloc::string::String),
64}
65
66impl ActionType {
67 pub fn from_name(name: &str) -> Self {
68 match name {
69 "URI" => Self::Uri,
70 "GoTo" => Self::GoTo,
71 "GoToR" => Self::GoToR,
72 "Named" => Self::Named,
73 "JavaScript" => Self::JavaScript,
74 "SubmitForm" => Self::SubmitForm,
75 "Launch" => Self::Launch,
76 "ImportData" => Self::ImportData,
77 _ => Self::Unknown(name.into()),
78 }
79 }
80
81 pub fn is_inert_on_flatten(&self) -> bool {
82 matches!(
83 self,
84 Self::JavaScript | Self::SubmitForm | Self::Launch | Self::ImportData
85 )
86 }
87}
88
89#[derive(Debug, Clone)]
91pub enum Action {
92 Uri(alloc::string::String),
94 GoTo(Destination),
96 GoToR {
98 file: alloc::string::String,
100 destination: Option<Destination>,
102 },
103 Named(alloc::string::String),
105 JavaScript(alloc::string::String),
107 SubmitForm {
109 target: Option<alloc::string::String>,
111 },
112 Launch {
114 file: Option<alloc::string::String>,
116 },
117 ImportData {
119 file: Option<alloc::string::String>,
121 },
122 Unknown(alloc::string::String),
124}
125
126impl Action {
127 pub fn from_dict(dict: &Dict<'_>) -> Self {
129 let action_type = dict
130 .get::<Name>(S)
131 .map(|n| alloc::string::String::from(n.as_str()))
132 .unwrap_or_default();
133 match ActionType::from_name(action_type.as_str()) {
134 ActionType::Uri => {
135 let uri = dict
136 .get::<pdf_syntax::object::String>(URI)
137 .map(|s| crate::annotation::pdf_string_to_string(&s))
138 .unwrap_or_default();
139 Self::Uri(uri)
140 }
141 ActionType::GoTo => {
142 let dest = dict
143 .get::<Object<'_>>(D)
144 .and_then(parse_destination)
145 .unwrap_or(Destination::Fit { page_index: None });
146 Self::GoTo(dest)
147 }
148 ActionType::GoToR => {
149 let file = file_spec_string(dict).unwrap_or_default();
150 let destination = dict.get::<Object<'_>>(D).and_then(parse_destination);
151 Self::GoToR { file, destination }
152 }
153 ActionType::Named => {
154 let name = dict
155 .get::<Name>(N)
156 .map(|n| alloc::string::String::from(n.as_str()))
157 .unwrap_or_default();
158 Self::Named(name)
159 }
160 ActionType::JavaScript => {
161 let js = dict
162 .get::<pdf_syntax::object::String>(JS)
163 .map(|s| crate::annotation::pdf_string_to_string(&s))
164 .unwrap_or_default();
165 Self::JavaScript(js)
166 }
167 ActionType::SubmitForm => Self::SubmitForm {
168 target: file_spec_string(dict),
169 },
170 ActionType::Launch => Self::Launch {
171 file: file_spec_string(dict),
172 },
173 ActionType::ImportData => Self::ImportData {
174 file: file_spec_string(dict),
175 },
176 ActionType::Unknown(action_type) => Self::Unknown(action_type),
177 }
178 }
179
180 pub fn action_type(&self) -> ActionType {
181 match self {
182 Self::Uri(_) => ActionType::Uri,
183 Self::GoTo(_) => ActionType::GoTo,
184 Self::GoToR { .. } => ActionType::GoToR,
185 Self::Named(_) => ActionType::Named,
186 Self::JavaScript(_) => ActionType::JavaScript,
187 Self::SubmitForm { .. } => ActionType::SubmitForm,
188 Self::Launch { .. } => ActionType::Launch,
189 Self::ImportData { .. } => ActionType::ImportData,
190 Self::Unknown(action_type) => ActionType::Unknown(action_type.clone()),
191 }
192 }
193}
194
195fn file_spec_string(dict: &Dict<'_>) -> Option<alloc::string::String> {
196 dict.get::<pdf_syntax::object::String>(F)
197 .map(|s| crate::annotation::pdf_string_to_string(&s))
198 .or_else(|| {
199 dict.get::<Dict<'_>>(F).and_then(|fs| {
200 fs.get::<pdf_syntax::object::String>(UF)
201 .or_else(|| fs.get::<pdf_syntax::object::String>(F))
202 .map(|s| crate::annotation::pdf_string_to_string(&s))
203 })
204 })
205}
206
207#[derive(Debug, Clone)]
209pub enum Destination {
210 Xyz {
212 page_index: Option<u32>,
213 left: Option<f32>,
214 top: Option<f32>,
215 zoom: Option<f32>,
216 },
217 Fit { page_index: Option<u32> },
219 FitH {
221 page_index: Option<u32>,
222 top: Option<f32>,
223 },
224 FitV {
226 page_index: Option<u32>,
227 left: Option<f32>,
228 },
229 FitR {
231 page_index: Option<u32>,
232 left: f32,
233 bottom: f32,
234 right: f32,
235 top: f32,
236 },
237 FitB { page_index: Option<u32> },
239 FitBH {
241 page_index: Option<u32>,
242 top: Option<f32>,
243 },
244 FitBV {
246 page_index: Option<u32>,
247 left: Option<f32>,
248 },
249 Named(alloc::string::String),
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum HighlightMode {
256 None,
258 Invert,
260 Outline,
262 Push,
264}
265
266pub fn parse_destination(obj: Object<'_>) -> Option<Destination> {
268 match obj {
269 Object::Array(arr) => {
270 let mut iter = arr.flex_iter();
271 let page_index = iter.next::<i32>().map(|n| n as u32);
272 let dest_type = iter.next::<Name>()?;
273 match dest_type.as_ref() {
274 b"XYZ" => Some(Destination::Xyz {
275 page_index,
276 left: iter.next::<f32>(),
277 top: iter.next::<f32>(),
278 zoom: iter.next::<f32>(),
279 }),
280 b"Fit" => Some(Destination::Fit { page_index }),
281 b"FitB" => Some(Destination::FitB { page_index }),
282 b"FitH" => Some(Destination::FitH {
283 page_index,
284 top: iter.next::<f32>(),
285 }),
286 b"FitBH" => Some(Destination::FitBH {
287 page_index,
288 top: iter.next::<f32>(),
289 }),
290 b"FitV" => Some(Destination::FitV {
291 page_index,
292 left: iter.next::<f32>(),
293 }),
294 b"FitBV" => Some(Destination::FitBV {
295 page_index,
296 left: iter.next::<f32>(),
297 }),
298 b"FitR" => Some(Destination::FitR {
299 page_index,
300 left: iter.next::<f32>().unwrap_or(0.0),
301 bottom: iter.next::<f32>().unwrap_or(0.0),
302 right: iter.next::<f32>().unwrap_or(0.0),
303 top: iter.next::<f32>().unwrap_or(0.0),
304 }),
305 _ => None,
306 }
307 }
308 Object::Name(name) => Some(Destination::Named(alloc::string::String::from(
309 name.as_str(),
310 ))),
311 Object::String(s) => Some(Destination::Named(crate::annotation::pdf_string_to_string(
312 &s,
313 ))),
314 _ => None,
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn security_sensitive_actions_are_known_types() {
324 assert_eq!(ActionType::from_name("SubmitForm"), ActionType::SubmitForm);
325 assert_eq!(ActionType::from_name("Launch"), ActionType::Launch);
326 assert_eq!(ActionType::from_name("ImportData"), ActionType::ImportData);
327 }
328
329 #[test]
330 fn security_sensitive_actions_are_inert_on_flatten() {
331 assert!(ActionType::JavaScript.is_inert_on_flatten());
332 assert!(ActionType::SubmitForm.is_inert_on_flatten());
333 assert!(ActionType::Launch.is_inert_on_flatten());
334 assert!(ActionType::ImportData.is_inert_on_flatten());
335 assert!(!ActionType::GoTo.is_inert_on_flatten());
336 assert!(!ActionType::Uri.is_inert_on_flatten());
337 }
338
339 #[test]
340 fn unknown_action_type_remains_auditable() {
341 assert_eq!(
342 ActionType::from_name("VendorAction"),
343 ActionType::Unknown("VendorAction".into())
344 );
345 }
346}