1use crate::objects::{Dictionary, Object, ObjectId};
4use crate::structure::Destination;
5
6#[derive(Debug, Clone, PartialEq)]
8pub enum ActionType {
9 GoTo,
11 GoToR,
13 GoToE,
15 Launch,
17 Named,
19 URI,
21 SubmitForm,
23 ResetForm,
25 ImportData,
27 JavaScript,
29 SetOCGState,
31 Sound,
33 Movie,
35 Rendition,
37 Trans,
39 GoTo3DView,
41}
42
43impl ActionType {
44 pub fn to_name(&self) -> &'static str {
46 match self {
47 ActionType::GoTo => "GoTo",
48 ActionType::GoToR => "GoToR",
49 ActionType::GoToE => "GoToE",
50 ActionType::Launch => "Launch",
51 ActionType::Named => "Named",
52 ActionType::URI => "URI",
53 ActionType::SubmitForm => "SubmitForm",
54 ActionType::ResetForm => "ResetForm",
55 ActionType::ImportData => "ImportData",
56 ActionType::JavaScript => "JavaScript",
57 ActionType::SetOCGState => "SetOCGState",
58 ActionType::Sound => "Sound",
59 ActionType::Movie => "Movie",
60 ActionType::Rendition => "Rendition",
61 ActionType::Trans => "Trans",
62 ActionType::GoTo3DView => "GoTo3DView",
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub enum Action {
70 GoTo {
72 destination: Destination,
74 },
75 GoToR {
77 file: String,
79 destination: Option<Destination>,
81 new_window: Option<bool>,
83 },
84 URI {
86 uri: String,
88 is_map: bool,
90 },
91 Named {
93 name: String,
95 },
96 Launch {
98 file: String,
100 parameters: Option<String>,
102 new_window: Option<bool>,
104 },
105 Next(Box<Action>),
107}
108
109impl Action {
110 pub fn goto(destination: Destination) -> Self {
112 Action::GoTo { destination }
113 }
114
115 pub fn uri(uri: impl Into<String>) -> Self {
117 Action::URI {
118 uri: uri.into(),
119 is_map: false,
120 }
121 }
122
123 pub fn named(name: impl Into<String>) -> Self {
125 Action::Named { name: name.into() }
126 }
127
128 pub fn goto_remote(file: impl Into<String>, destination: Option<Destination>) -> Self {
130 Action::GoToR {
131 file: file.into(),
132 destination,
133 new_window: None,
134 }
135 }
136
137 pub fn launch(file: impl Into<String>) -> Self {
139 Action::Launch {
140 file: file.into(),
141 parameters: None,
142 new_window: None,
143 }
144 }
145
146 pub fn to_dict(&self) -> Dictionary {
148 let mut dict = Dictionary::new();
149
150 match self {
151 Action::GoTo { destination } => {
152 dict.set("Type", Object::Name("Action".to_string()));
153 dict.set("S", Object::Name("GoTo".to_string()));
154 dict.set("D", Object::Array(destination.to_array().into()));
155 }
156 Action::GoToR {
157 file,
158 destination,
159 new_window,
160 } => {
161 dict.set("Type", Object::Name("Action".to_string()));
162 dict.set("S", Object::Name("GoToR".to_string()));
163 dict.set("F", Object::String(file.clone()));
164
165 if let Some(dest) = destination {
166 dict.set("D", Object::Array(dest.to_array().into()));
167 }
168
169 if let Some(nw) = new_window {
170 dict.set("NewWindow", Object::Boolean(*nw));
171 }
172 }
173 Action::URI { uri, is_map } => {
174 dict.set("Type", Object::Name("Action".to_string()));
175 dict.set("S", Object::Name("URI".to_string()));
176 dict.set("URI", Object::String(uri.clone()));
177
178 if *is_map {
179 dict.set("IsMap", Object::Boolean(true));
180 }
181 }
182 Action::Named { name } => {
183 dict.set("Type", Object::Name("Action".to_string()));
184 dict.set("S", Object::Name("Named".to_string()));
185 dict.set("N", Object::Name(name.clone()));
186 }
187 Action::Launch {
188 file,
189 parameters,
190 new_window,
191 } => {
192 dict.set("Type", Object::Name("Action".to_string()));
193 dict.set("S", Object::Name("Launch".to_string()));
194 dict.set("F", Object::String(file.clone()));
195
196 if let Some(params) = parameters {
197 dict.set("P", Object::String(params.clone()));
198 }
199
200 if let Some(nw) = new_window {
201 dict.set("NewWindow", Object::Boolean(*nw));
202 }
203 }
204 Action::Next(next) => {
205 let next_dict = next.to_dict();
206 dict = next_dict;
207 }
208 }
209
210 dict
211 }
212}
213
214pub struct ActionDictionary {
216 pub action: Action,
218 pub object_id: Option<ObjectId>,
220}
221
222impl ActionDictionary {
223 pub fn new(action: Action) -> Self {
225 Self {
226 action,
227 object_id: None,
228 }
229 }
230
231 pub fn with_object_id(mut self, id: ObjectId) -> Self {
233 self.object_id = Some(id);
234 self
235 }
236
237 pub fn to_dict(&self) -> Dictionary {
239 self.action.to_dict()
240 }
241
242 pub fn to_object(&self) -> Object {
244 if let Some(id) = self.object_id {
245 Object::Reference(id)
246 } else {
247 Object::Dictionary(self.to_dict())
248 }
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::structure::PageDestination;
256
257 #[test]
258 fn test_action_type_names() {
259 assert_eq!(ActionType::GoTo.to_name(), "GoTo");
260 assert_eq!(ActionType::URI.to_name(), "URI");
261 assert_eq!(ActionType::Named.to_name(), "Named");
262 assert_eq!(ActionType::Launch.to_name(), "Launch");
263 }
264
265 #[test]
266 fn test_goto_action() {
267 let dest = Destination::fit(PageDestination::PageNumber(0));
268 let action = Action::goto(dest);
269
270 let dict = action.to_dict();
271 assert_eq!(dict.get("S"), Some(&Object::Name("GoTo".to_string())));
272 assert!(dict.get("D").is_some());
273 }
274
275 #[test]
276 fn test_uri_action() {
277 let action = Action::uri("https://example.com");
278 let dict = action.to_dict();
279
280 assert_eq!(dict.get("S"), Some(&Object::Name("URI".to_string())));
281 assert_eq!(
282 dict.get("URI"),
283 Some(&Object::String("https://example.com".to_string()))
284 );
285 }
286
287 #[test]
288 fn test_named_action() {
289 let action = Action::named("NextPage");
290 let dict = action.to_dict();
291
292 assert_eq!(dict.get("S"), Some(&Object::Name("Named".to_string())));
293 assert_eq!(dict.get("N"), Some(&Object::Name("NextPage".to_string())));
294 }
295
296 #[test]
297 fn test_action_dictionary() {
298 let action = Action::uri("https://example.com");
299 let action_dict = ActionDictionary::new(action).with_object_id(ObjectId::new(10, 0));
300
301 match action_dict.to_object() {
302 Object::Reference(id) => {
303 assert_eq!(id.number(), 10);
304 assert_eq!(id.generation(), 0);
305 }
306 _ => panic!("Expected reference"),
307 }
308 }
309
310 #[test]
311 fn test_all_action_type_names() {
312 assert_eq!(ActionType::GoTo.to_name(), "GoTo");
313 assert_eq!(ActionType::GoToR.to_name(), "GoToR");
314 assert_eq!(ActionType::GoToE.to_name(), "GoToE");
315 assert_eq!(ActionType::Launch.to_name(), "Launch");
316 assert_eq!(ActionType::Named.to_name(), "Named");
317 assert_eq!(ActionType::URI.to_name(), "URI");
318 assert_eq!(ActionType::SubmitForm.to_name(), "SubmitForm");
319 assert_eq!(ActionType::ResetForm.to_name(), "ResetForm");
320 assert_eq!(ActionType::ImportData.to_name(), "ImportData");
321 assert_eq!(ActionType::JavaScript.to_name(), "JavaScript");
322 assert_eq!(ActionType::SetOCGState.to_name(), "SetOCGState");
323 assert_eq!(ActionType::Sound.to_name(), "Sound");
324 assert_eq!(ActionType::Movie.to_name(), "Movie");
325 assert_eq!(ActionType::Rendition.to_name(), "Rendition");
326 assert_eq!(ActionType::Trans.to_name(), "Trans");
327 assert_eq!(ActionType::GoTo3DView.to_name(), "GoTo3DView");
328 }
329
330 #[test]
331 fn test_action_type_debug_clone_partial_eq() {
332 let action_type = ActionType::GoTo;
333 let cloned = action_type.clone();
334 assert_eq!(action_type, cloned);
335
336 let debug_str = format!("{action_type:?}");
337 assert!(debug_str.contains("GoTo"));
338
339 assert_ne!(ActionType::GoTo, ActionType::URI);
341 assert_ne!(ActionType::Named, ActionType::Launch);
342 }
343
344 #[test]
345 fn test_goto_remote_action() {
346 let dest = Destination::fit(PageDestination::PageNumber(5));
347 let action = Action::goto_remote("external.pdf", Some(dest));
348
349 let dict = action.to_dict();
350 assert_eq!(dict.get("S"), Some(&Object::Name("GoToR".to_string())));
351 assert_eq!(
352 dict.get("F"),
353 Some(&Object::String("external.pdf".to_string()))
354 );
355 assert!(dict.get("D").is_some());
356 assert_eq!(dict.get("NewWindow"), None);
357 }
358
359 #[test]
360 fn test_goto_remote_action_with_new_window() {
361 let action = Action::GoToR {
362 file: "external.pdf".to_string(),
363 destination: None,
364 new_window: Some(true),
365 };
366
367 let dict = action.to_dict();
368 assert_eq!(dict.get("S"), Some(&Object::Name("GoToR".to_string())));
369 assert_eq!(
370 dict.get("F"),
371 Some(&Object::String("external.pdf".to_string()))
372 );
373 assert_eq!(dict.get("D"), None);
374 assert_eq!(dict.get("NewWindow"), Some(&Object::Boolean(true)));
375 }
376
377 #[test]
378 fn test_launch_action() {
379 let action = Action::launch("notepad.exe");
380 let dict = action.to_dict();
381
382 assert_eq!(dict.get("S"), Some(&Object::Name("Launch".to_string())));
383 assert_eq!(
384 dict.get("F"),
385 Some(&Object::String("notepad.exe".to_string()))
386 );
387 assert_eq!(dict.get("P"), None);
388 assert_eq!(dict.get("NewWindow"), None);
389 }
390
391 #[test]
392 fn test_launch_action_with_parameters() {
393 let action = Action::Launch {
394 file: "app.exe".to_string(),
395 parameters: Some("--verbose".to_string()),
396 new_window: Some(false),
397 };
398
399 let dict = action.to_dict();
400 assert_eq!(dict.get("S"), Some(&Object::Name("Launch".to_string())));
401 assert_eq!(dict.get("F"), Some(&Object::String("app.exe".to_string())));
402 assert_eq!(
403 dict.get("P"),
404 Some(&Object::String("--verbose".to_string()))
405 );
406 assert_eq!(dict.get("NewWindow"), Some(&Object::Boolean(false)));
407 }
408
409 #[test]
410 fn test_uri_action_with_is_map() {
411 let action = Action::URI {
412 uri: "https://example.com/map".to_string(),
413 is_map: true,
414 };
415
416 let dict = action.to_dict();
417 assert_eq!(dict.get("S"), Some(&Object::Name("URI".to_string())));
418 assert_eq!(
419 dict.get("URI"),
420 Some(&Object::String("https://example.com/map".to_string()))
421 );
422 assert_eq!(dict.get("IsMap"), Some(&Object::Boolean(true)));
423 }
424
425 #[test]
426 fn test_uri_action_without_is_map() {
427 let action = Action::uri("https://example.com");
428
429 match action {
430 Action::URI { uri, is_map } => {
431 assert_eq!(uri, "https://example.com");
432 assert!(!is_map);
433 }
434 _ => panic!("Expected URI action"),
435 }
436 }
437
438 #[test]
439 fn test_next_action() {
440 let inner_action = Action::named("FirstPage");
441 let next_action = Action::Next(Box::new(inner_action));
442
443 let dict = next_action.to_dict();
444 assert_eq!(dict.get("S"), Some(&Object::Name("Named".to_string())));
446 assert_eq!(dict.get("N"), Some(&Object::Name("FirstPage".to_string())));
447 }
448
449 #[test]
450 fn test_action_debug_clone() {
451 let action = Action::uri("https://example.com");
452 let cloned = action.clone();
453
454 let debug_str = format!("{action:?}");
455 assert!(debug_str.contains("URI"));
456 assert!(debug_str.contains("https://example.com"));
457
458 match (action, cloned) {
459 (
460 Action::URI {
461 uri: uri1,
462 is_map: map1,
463 },
464 Action::URI {
465 uri: uri2,
466 is_map: map2,
467 },
468 ) => {
469 assert_eq!(uri1, uri2);
470 assert_eq!(map1, map2);
471 }
472 _ => panic!("Expected URI actions"),
473 }
474 }
475
476 #[test]
477 fn test_action_dictionary_without_object_id() {
478 let action = Action::named("LastPage");
479 let action_dict = ActionDictionary::new(action);
480
481 assert_eq!(action_dict.object_id, None);
482
483 match action_dict.to_object() {
484 Object::Dictionary(dict) => {
485 assert_eq!(dict.get("S"), Some(&Object::Name("Named".to_string())));
486 assert_eq!(dict.get("N"), Some(&Object::Name("LastPage".to_string())));
487 }
488 _ => panic!("Expected dictionary"),
489 }
490 }
491
492 #[test]
493 fn test_action_dictionary_to_dict() {
494 let action = Action::uri("https://test.com");
495 let action_dict = ActionDictionary::new(action);
496
497 let dict = action_dict.to_dict();
498 assert_eq!(dict.get("S"), Some(&Object::Name("URI".to_string())));
499 assert_eq!(
500 dict.get("URI"),
501 Some(&Object::String("https://test.com".to_string()))
502 );
503 }
504
505 #[test]
506 fn test_goto_action_destination_handling() {
507 let dest = Destination::fit(PageDestination::PageNumber(10));
508 let action = Action::goto(dest.clone());
509
510 match action {
511 Action::GoTo { destination } => {
512 assert_eq!(destination.to_array().len(), dest.to_array().len());
514 }
515 _ => panic!("Expected GoTo action"),
516 }
517 }
518
519 #[test]
520 fn test_action_constructor_string_conversion() {
521 let uri_action = Action::uri("test");
523 let named_action = Action::named("test");
524 let remote_action = Action::goto_remote("test.pdf", None);
525 let launch_action = Action::launch("test.exe");
526
527 match uri_action {
528 Action::URI { uri, .. } => assert_eq!(uri, "test"),
529 _ => panic!("Expected URI action"),
530 }
531
532 match named_action {
533 Action::Named { name } => assert_eq!(name, "test"),
534 _ => panic!("Expected Named action"),
535 }
536
537 match remote_action {
538 Action::GoToR { file, .. } => assert_eq!(file, "test.pdf"),
539 _ => panic!("Expected GoToR action"),
540 }
541
542 match launch_action {
543 Action::Launch { file, .. } => assert_eq!(file, "test.exe"),
544 _ => panic!("Expected Launch action"),
545 }
546 }
547
548 #[test]
549 fn test_action_dict_type_field() {
550 let actions = vec![
551 Action::uri("https://example.com"),
552 Action::named("NextPage"),
553 Action::launch("app.exe"),
554 Action::goto_remote("file.pdf", None),
555 ];
556
557 for action in actions {
558 let dict = action.to_dict();
559 assert_eq!(dict.get("Type"), Some(&Object::Name("Action".to_string())));
560 }
561 }
562
563 #[test]
564 fn test_complex_action_chaining() {
565 let inner = Action::named("PrevPage");
566 let next = Action::Next(Box::new(inner));
567
568 let dict = next.to_dict();
569 assert_eq!(dict.get("S"), Some(&Object::Name("Named".to_string())));
571 assert_eq!(dict.get("N"), Some(&Object::Name("PrevPage".to_string())));
572 }
573
574 #[test]
575 fn test_action_object_id_generation_increments() {
576 let action1 =
577 ActionDictionary::new(Action::uri("url1")).with_object_id(ObjectId::new(1, 0));
578 let action2 =
579 ActionDictionary::new(Action::uri("url2")).with_object_id(ObjectId::new(2, 0));
580
581 match (action1.to_object(), action2.to_object()) {
582 (Object::Reference(id1), Object::Reference(id2)) => {
583 assert_eq!(id1.number(), 1);
584 assert_eq!(id2.number(), 2);
585 assert_ne!(id1.number(), id2.number());
586 }
587 _ => panic!("Expected references"),
588 }
589 }
590
591 #[test]
592 fn test_action_pattern_matching() {
593 let actions = vec![
594 Action::goto(Destination::fit(PageDestination::PageNumber(0))),
595 Action::uri("https://example.com"),
596 Action::named("Test"),
597 Action::launch("app.exe"),
598 Action::goto_remote("remote.pdf", None),
599 ];
600
601 let mut goto_count = 0;
602 let mut uri_count = 0;
603 let mut named_count = 0;
604 let mut launch_count = 0;
605 let mut gotor_count = 0;
606
607 for action in actions {
608 match action {
609 Action::GoTo { .. } => goto_count += 1,
610 Action::URI { .. } => uri_count += 1,
611 Action::Named { .. } => named_count += 1,
612 Action::Launch { .. } => launch_count += 1,
613 Action::GoToR { .. } => gotor_count += 1,
614 Action::Next(_) => {}
615 }
616 }
617
618 assert_eq!(goto_count, 1);
619 assert_eq!(uri_count, 1);
620 assert_eq!(named_count, 1);
621 assert_eq!(launch_count, 1);
622 assert_eq!(gotor_count, 1);
623 }
624}