Skip to main content

oxidize_pdf/actions/
goto_action.rs

1//! GoTo actions for navigating within and between documents
2
3use crate::objects::{Dictionary, Object};
4use crate::structure::{Destination, PageDestination};
5
6/// GoTo action - navigate to a destination in the current document
7#[derive(Debug, Clone)]
8pub struct GoToAction {
9    /// The destination
10    pub destination: Destination,
11}
12
13impl GoToAction {
14    /// Create new GoTo action
15    pub fn new(destination: Destination) -> Self {
16        Self { destination }
17    }
18
19    /// Create action to go to specific page
20    pub fn to_page(page_number: u32) -> Self {
21        Self {
22            destination: Destination::fit(PageDestination::PageNumber(page_number)),
23        }
24    }
25
26    /// Create action to go to page with zoom
27    pub fn to_page_xyz(page_number: u32, x: f64, y: f64, zoom: Option<f64>) -> Self {
28        Self {
29            destination: Destination::xyz(
30                PageDestination::PageNumber(page_number),
31                Some(x),
32                Some(y),
33                zoom,
34            ),
35        }
36    }
37
38    /// Convert to dictionary
39    pub fn to_dict(&self) -> Dictionary {
40        let mut dict = Dictionary::new();
41        dict.set("Type", Object::Name("Action".to_string()));
42        dict.set("S", Object::Name("GoTo".to_string()));
43        dict.set("D", Object::Array(self.destination.to_array().into()));
44        dict
45    }
46}
47
48/// Remote GoTo action - navigate to a destination in another PDF document
49#[derive(Debug, Clone)]
50pub struct RemoteGoToAction {
51    /// File specification (path to the PDF)
52    pub file: String,
53    /// Destination in the remote document
54    pub destination: Option<RemoteDestination>,
55    /// Whether to open in new window
56    pub new_window: Option<bool>,
57}
58
59/// Remote destination specification
60#[derive(Debug, Clone)]
61pub enum RemoteDestination {
62    /// Page number (0-based)
63    PageNumber(u32),
64    /// Named destination
65    Named(String),
66    /// Explicit destination
67    Explicit(Destination),
68}
69
70impl RemoteGoToAction {
71    /// Create new remote GoTo action
72    pub fn new(file: impl Into<String>) -> Self {
73        Self {
74            file: file.into(),
75            destination: None,
76            new_window: None,
77        }
78    }
79
80    /// Set destination by page number
81    pub fn to_page(mut self, page: u32) -> Self {
82        self.destination = Some(RemoteDestination::PageNumber(page));
83        self
84    }
85
86    /// Set destination by name
87    pub fn to_named(mut self, name: impl Into<String>) -> Self {
88        self.destination = Some(RemoteDestination::Named(name.into()));
89        self
90    }
91
92    /// Set explicit destination
93    pub fn to_destination(mut self, dest: Destination) -> Self {
94        self.destination = Some(RemoteDestination::Explicit(dest));
95        self
96    }
97
98    /// Set whether to open in new window
99    pub fn in_new_window(mut self, new_window: bool) -> Self {
100        self.new_window = Some(new_window);
101        self
102    }
103
104    /// Convert to dictionary
105    pub fn to_dict(&self) -> Dictionary {
106        let mut dict = Dictionary::new();
107        dict.set("Type", Object::Name("Action".to_string()));
108        dict.set("S", Object::Name("GoToR".to_string()));
109        dict.set("F", Object::String(self.file.clone()));
110
111        if let Some(dest) = &self.destination {
112            match dest {
113                RemoteDestination::PageNumber(page) => {
114                    dict.set("D", Object::Integer(*page as i64));
115                }
116                RemoteDestination::Named(name) => {
117                    dict.set("D", Object::String(name.clone()));
118                }
119                RemoteDestination::Explicit(destination) => {
120                    dict.set("D", Object::Array(destination.to_array().into()));
121                }
122            }
123        }
124
125        if let Some(nw) = self.new_window {
126            dict.set("NewWindow", Object::Boolean(nw));
127        }
128
129        dict
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_goto_action_to_page() {
139        let action = GoToAction::to_page(5);
140        let dict = action.to_dict();
141
142        assert_eq!(dict.get("S"), Some(&Object::Name("GoTo".to_string())));
143        assert!(dict.get("D").is_some());
144    }
145
146    #[test]
147    fn test_goto_action_xyz() {
148        let action = GoToAction::to_page_xyz(2, 100.0, 200.0, Some(1.5));
149        let dict = action.to_dict();
150
151        assert_eq!(dict.get("S"), Some(&Object::Name("GoTo".to_string())));
152
153        // Verify destination array
154        if let Some(Object::Array(dest_array)) = dict.get("D") {
155            assert!(dest_array.len() >= 5); // Page, XYZ, left, top, zoom
156        } else {
157            panic!("Expected destination array");
158        }
159    }
160
161    #[test]
162    fn test_remote_goto_action() {
163        let action = RemoteGoToAction::new("other.pdf")
164            .to_page(10)
165            .in_new_window(true);
166
167        let dict = action.to_dict();
168
169        assert_eq!(dict.get("S"), Some(&Object::Name("GoToR".to_string())));
170        assert_eq!(
171            dict.get("F"),
172            Some(&Object::String("other.pdf".to_string()))
173        );
174        assert_eq!(dict.get("D"), Some(&Object::Integer(10)));
175        assert_eq!(dict.get("NewWindow"), Some(&Object::Boolean(true)));
176    }
177
178    #[test]
179    fn test_remote_goto_named() {
180        let action = RemoteGoToAction::new("document.pdf").to_named("Chapter1");
181
182        let dict = action.to_dict();
183
184        assert_eq!(dict.get("D"), Some(&Object::String("Chapter1".to_string())));
185    }
186
187    #[test]
188    fn test_goto_action_debug() {
189        let action = GoToAction::to_page_xyz(0, 100.0, 200.0, Some(1.5));
190        let _ = format!("{action:?}");
191    }
192
193    #[test]
194    fn test_goto_action_clone() {
195        let action = GoToAction::to_page_xyz(0, 100.0, 200.0, Some(1.5));
196        let cloned = action.clone();
197
198        let dict1 = action.to_dict();
199        let dict2 = cloned.to_dict();
200        assert_eq!(dict1.get("S"), dict2.get("S"));
201        assert_eq!(dict1.get("D"), dict2.get("D"));
202    }
203
204    #[test]
205    fn test_goto_action_from_destination() {
206        use crate::structure::{Destination, PageDestination};
207
208        // Test with explicit destination creation
209        let dest = Destination::fit(PageDestination::PageNumber(5));
210        let action = GoToAction::new(dest);
211        let dict = action.to_dict();
212
213        assert_eq!(dict.get("S"), Some(&Object::Name("GoTo".to_string())));
214        assert!(dict.get("D").is_some());
215    }
216
217    #[test]
218    fn test_goto_action_various_destinations() {
219        use crate::structure::{Destination, PageDestination};
220
221        // Test fit destination
222        let fit_dest = Destination::fit(PageDestination::PageNumber(3));
223        let action1 = GoToAction::new(fit_dest);
224        let dict1 = action1.to_dict();
225        assert!(dict1.get("D").is_some());
226
227        // Test XYZ destination with page_xyz method
228        let action2 = GoToAction::to_page_xyz(1, 100.0, 200.0, Some(1.5));
229        let dict2 = action2.to_dict();
230        if let Some(Object::Array(dest)) = dict2.get("D") {
231            assert!(dest.len() >= 4); // Should have page, type, and coordinates
232        }
233
234        // Test XYZ destination with null zoom
235        let action3 = GoToAction::to_page_xyz(2, 0.0, 0.0, None);
236        let dict3 = action3.to_dict();
237        assert!(dict3.get("D").is_some());
238    }
239
240    #[test]
241    fn test_goto_action_page_destinations() {
242        use crate::structure::{Destination, PageDestination};
243
244        // Test different page destination types
245        let page_num_dest = Destination::fit(PageDestination::PageNumber(10));
246        let action = GoToAction::new(page_num_dest);
247        let dict = action.to_dict();
248
249        assert_eq!(dict.get("Type"), Some(&Object::Name("Action".to_string())));
250        assert_eq!(dict.get("S"), Some(&Object::Name("GoTo".to_string())));
251        assert!(dict.get("D").is_some());
252    }
253
254    #[test]
255    fn test_goto_action_coordinate_precision() {
256        let action = GoToAction::to_page_xyz(0, 123.456, 789.012, Some(1.25));
257        let dict = action.to_dict();
258
259        // Verify the action was created successfully
260        assert_eq!(dict.get("S"), Some(&Object::Name("GoTo".to_string())));
261        assert!(dict.get("D").is_some());
262
263        // The exact format of the destination array depends on the Destination implementation
264        if let Some(Object::Array(dest)) = dict.get("D") {
265            assert!(!dest.is_empty());
266        }
267    }
268
269    #[test]
270    fn test_remote_goto_action_creation() {
271        let action = RemoteGoToAction::new("external.pdf");
272        let dict = action.to_dict();
273
274        assert_eq!(dict.get("S"), Some(&Object::Name("GoToR".to_string())));
275        assert_eq!(
276            dict.get("F"),
277            Some(&Object::String("external.pdf".to_string()))
278        );
279        assert!(dict.get("D").is_none()); // No destination set yet
280        assert!(dict.get("NewWindow").is_none()); // Default is not set
281    }
282
283    #[test]
284    fn test_remote_goto_action_window_options() {
285        let action1 = RemoteGoToAction::new("doc1.pdf").in_new_window(true);
286        let dict1 = action1.to_dict();
287        assert_eq!(dict1.get("NewWindow"), Some(&Object::Boolean(true)));
288
289        let action2 = RemoteGoToAction::new("doc2.pdf").in_new_window(false);
290        let dict2 = action2.to_dict();
291        assert_eq!(dict2.get("NewWindow"), Some(&Object::Boolean(false)));
292
293        let action3 = RemoteGoToAction::new("doc3.pdf"); // No window setting
294        let dict3 = action3.to_dict();
295        assert!(dict3.get("NewWindow").is_none());
296    }
297
298    #[test]
299    fn test_remote_goto_action_destination_types() {
300        // Test page destination
301        let action1 = RemoteGoToAction::new("target.pdf").to_page(5);
302        let dict1 = action1.to_dict();
303        assert_eq!(dict1.get("D"), Some(&Object::Integer(5)));
304
305        // Test named destination
306        let action2 = RemoteGoToAction::new("target.pdf").to_named("Introduction");
307        let dict2 = action2.to_dict();
308        assert_eq!(
309            dict2.get("D"),
310            Some(&Object::String("Introduction".to_string()))
311        );
312    }
313
314    #[test]
315    fn test_remote_goto_action_clone_debug() {
316        let action = RemoteGoToAction::new("test.pdf")
317            .to_page(3)
318            .in_new_window(true);
319
320        let cloned = action.clone();
321        let dict1 = action.to_dict();
322        let dict2 = cloned.to_dict();
323
324        assert_eq!(dict1.get("F"), dict2.get("F"));
325        assert_eq!(dict1.get("D"), dict2.get("D"));
326        assert_eq!(dict1.get("NewWindow"), dict2.get("NewWindow"));
327
328        // Test debug formatting
329        let _ = format!("{action:?}");
330    }
331
332    #[test]
333    fn test_goto_action_edge_cases() {
334        use crate::structure::{Destination, PageDestination};
335
336        // Test with page 0 (first page)
337        let action = GoToAction::to_page(0);
338        let dict = action.to_dict();
339        assert_eq!(dict.get("S"), Some(&Object::Name("GoTo".to_string())));
340        assert!(dict.get("D").is_some());
341
342        // Test with high page number
343        let action = GoToAction::to_page(9999);
344        let dict = action.to_dict();
345        assert_eq!(dict.get("S"), Some(&Object::Name("GoTo".to_string())));
346        assert!(dict.get("D").is_some());
347
348        // Test with explicit destination for page 0
349        let dest = Destination::fit(PageDestination::PageNumber(0));
350        let action = GoToAction::new(dest);
351        let dict = action.to_dict();
352        assert!(dict.get("D").is_some());
353    }
354
355    #[test]
356    fn test_goto_action_comprehensive() {
357        use crate::structure::{Destination, PageDestination};
358
359        // Test all available methods
360        let action1 = GoToAction::to_page(5);
361        let dict1 = action1.to_dict();
362        assert_eq!(dict1.get("S"), Some(&Object::Name("GoTo".to_string())));
363
364        let action2 = GoToAction::to_page_xyz(3, 100.0, 200.0, Some(1.5));
365        let dict2 = action2.to_dict();
366        assert_eq!(dict2.get("S"), Some(&Object::Name("GoTo".to_string())));
367
368        // Test with explicit destinations
369        let xyz_dest = Destination::xyz(
370            PageDestination::PageNumber(1),
371            Some(50.0),
372            Some(75.0),
373            Some(2.0),
374        );
375        let action3 = GoToAction::new(xyz_dest);
376        let dict3 = action3.to_dict();
377        assert_eq!(dict3.get("S"), Some(&Object::Name("GoTo".to_string())));
378    }
379}