Skip to main content

pdf_annot/
link.rs

1//! Link annotations, actions, and destinations.
2
3extern 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/// A link annotation (ISO 32000-2 §12.5.6.5).
11#[derive(Debug)]
12pub struct LinkAnnotation {
13    /// The action associated with the link.
14    pub action: Option<Action>,
15    /// A direct destination.
16    pub destination: Option<Destination>,
17    /// The highlight mode.
18    pub highlight_mode: HighlightMode,
19    /// Optional quad points for the link region.
20    pub quad_points: Option<QuadPoints>,
21}
22
23impl LinkAnnotation {
24    /// Extract link annotation properties.
25    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/// Known PDF action types (ISO 32000-2 §12.6).
53#[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/// An action (ISO 32000-2 §12.6).
90#[derive(Debug, Clone)]
91pub enum Action {
92    /// A URI action.
93    Uri(alloc::string::String),
94    /// A GoTo action.
95    GoTo(Destination),
96    /// A GoToR action.
97    GoToR {
98        /// The file specification.
99        file: alloc::string::String,
100        /// The destination.
101        destination: Option<Destination>,
102    },
103    /// A Named action.
104    Named(alloc::string::String),
105    /// A JavaScript action.
106    JavaScript(alloc::string::String),
107    /// Submit form data to a target URL.
108    SubmitForm {
109        /// The target file or URL from `/F`, if present.
110        target: Option<alloc::string::String>,
111    },
112    /// Launch an external application or document.
113    Launch {
114        /// The target file specification from `/F`, if present.
115        file: Option<alloc::string::String>,
116    },
117    /// Import form data from an external FDF file.
118    ImportData {
119        /// The target file specification from `/F`, if present.
120        file: Option<alloc::string::String>,
121    },
122    /// Unknown action type.
123    Unknown(alloc::string::String),
124}
125
126impl Action {
127    /// Parse an action from an action dictionary.
128    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/// A destination (ISO 32000-2 §12.3.2).
208#[derive(Debug, Clone)]
209pub enum Destination {
210    /// `/XYZ left top zoom`.
211    Xyz {
212        page_index: Option<u32>,
213        left: Option<f32>,
214        top: Option<f32>,
215        zoom: Option<f32>,
216    },
217    /// `/Fit`.
218    Fit { page_index: Option<u32> },
219    /// `/FitH top`.
220    FitH {
221        page_index: Option<u32>,
222        top: Option<f32>,
223    },
224    /// `/FitV left`.
225    FitV {
226        page_index: Option<u32>,
227        left: Option<f32>,
228    },
229    /// `/FitR left bottom right top`.
230    FitR {
231        page_index: Option<u32>,
232        left: f32,
233        bottom: f32,
234        right: f32,
235        top: f32,
236    },
237    /// `/FitB`.
238    FitB { page_index: Option<u32> },
239    /// `/FitBH top`.
240    FitBH {
241        page_index: Option<u32>,
242        top: Option<f32>,
243    },
244    /// `/FitBV left`.
245    FitBV {
246        page_index: Option<u32>,
247        left: Option<f32>,
248    },
249    /// A named destination.
250    Named(alloc::string::String),
251}
252
253/// Link highlight mode.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum HighlightMode {
256    /// No highlighting.
257    None,
258    /// Invert contents.
259    Invert,
260    /// Invert border.
261    Outline,
262    /// Push effect.
263    Push,
264}
265
266/// Parse a destination from an Object.
267pub 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}