Skip to main content

folio_nav/
action.rs

1//! PDF actions — operations triggered by events.
2
3use folio_cos::PdfObject;
4use indexmap::IndexMap;
5
6/// PDF action types (ISO 32000-2 Table 198).
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ActionType {
9    GoTo,
10    GoToR,
11    GoToE,
12    Launch,
13    Thread,
14    URI,
15    Sound,
16    Movie,
17    Hide,
18    Named,
19    SubmitForm,
20    ResetForm,
21    ImportData,
22    JavaScript,
23    SetOCGState,
24    Rendition,
25    Trans,
26    GoTo3DView,
27    RichMediaExecute,
28    Unknown,
29}
30
31impl ActionType {
32    pub fn from_name(name: &[u8]) -> Self {
33        match name {
34            b"GoTo" => Self::GoTo,
35            b"GoToR" => Self::GoToR,
36            b"GoToE" => Self::GoToE,
37            b"Launch" => Self::Launch,
38            b"Thread" => Self::Thread,
39            b"URI" => Self::URI,
40            b"Sound" => Self::Sound,
41            b"Movie" => Self::Movie,
42            b"Hide" => Self::Hide,
43            b"Named" => Self::Named,
44            b"SubmitForm" => Self::SubmitForm,
45            b"ResetForm" => Self::ResetForm,
46            b"ImportData" => Self::ImportData,
47            b"JavaScript" => Self::JavaScript,
48            b"SetOCGState" => Self::SetOCGState,
49            b"Rendition" => Self::Rendition,
50            b"Trans" => Self::Trans,
51            b"GoTo3DView" => Self::GoTo3DView,
52            b"RichMediaExecute" => Self::RichMediaExecute,
53            _ => Self::Unknown,
54        }
55    }
56}
57
58/// A parsed PDF action.
59#[derive(Debug, Clone)]
60pub struct Action {
61    /// The action type.
62    pub action_type: ActionType,
63    /// The raw action dictionary.
64    pub dict: IndexMap<Vec<u8>, PdfObject>,
65}
66
67impl Action {
68    /// Parse an action from a PDF dictionary.
69    pub fn from_dict(dict: &IndexMap<Vec<u8>, PdfObject>) -> Self {
70        let action_type = dict
71            .get(b"S".as_slice())
72            .and_then(|o| o.as_name())
73            .map(ActionType::from_name)
74            .unwrap_or(ActionType::Unknown);
75
76        Self {
77            action_type,
78            dict: dict.clone(),
79        }
80    }
81
82    /// Parse from a PdfObject (must be a dict).
83    pub fn from_object(obj: &PdfObject) -> Option<Self> {
84        let dict = obj.as_dict()?;
85        Some(Self::from_dict(dict))
86    }
87
88    /// Get the URI for a URI action.
89    pub fn uri(&self) -> Option<String> {
90        if self.action_type != ActionType::URI {
91            return None;
92        }
93        self.dict
94            .get(b"URI".as_slice())?
95            .as_str()
96            .map(|s| String::from_utf8_lossy(s).into_owned())
97    }
98
99    /// Get the JavaScript source for a JavaScript action.
100    pub fn javascript(&self) -> Option<String> {
101        if self.action_type != ActionType::JavaScript {
102            return None;
103        }
104        match self.dict.get(b"JS".as_slice())? {
105            PdfObject::Str(s) => Some(String::from_utf8_lossy(&s).into_owned()),
106            _ => None,
107        }
108    }
109
110    /// Get the destination for a GoTo action.
111    pub fn destination(&self) -> Option<&PdfObject> {
112        if self.action_type != ActionType::GoTo {
113            return None;
114        }
115        self.dict.get(b"D".as_slice())
116    }
117
118    /// Get the named action name (for Named actions).
119    pub fn named_action(&self) -> Option<String> {
120        if self.action_type != ActionType::Named {
121            return None;
122        }
123        self.dict
124            .get(b"N".as_slice())?
125            .as_name()
126            .map(|n| String::from_utf8_lossy(n).into_owned())
127    }
128
129    /// Get the next action in the chain.
130    pub fn next(&self) -> Option<&PdfObject> {
131        self.dict.get(b"Next".as_slice())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_action_type_parsing() {
141        assert_eq!(ActionType::from_name(b"GoTo"), ActionType::GoTo);
142        assert_eq!(ActionType::from_name(b"URI"), ActionType::URI);
143        assert_eq!(ActionType::from_name(b"JavaScript"), ActionType::JavaScript);
144        assert_eq!(ActionType::from_name(b"Unknown"), ActionType::Unknown);
145    }
146}