oxidize_pdf/structure/
destination.rs

1//! PDF destinations according to ISO 32000-1 Section 12.3.2
2
3use crate::geometry::Rectangle;
4use crate::objects::{Array, Object, ObjectId};
5
6/// PDF destination types
7#[derive(Debug, Clone, PartialEq)]
8pub enum DestinationType {
9    /// Display page with coordinates (left, top) at upper-left corner
10    XYZ {
11        left: Option<f64>,
12        top: Option<f64>,
13        zoom: Option<f64>,
14    },
15    /// Fit entire page in window
16    Fit,
17    /// Fit width of page in window
18    FitH { top: Option<f64> },
19    /// Fit height of page in window
20    FitV { left: Option<f64> },
21    /// Fit rectangle in window
22    FitR { rect: Rectangle },
23    /// Fit page bounding box in window
24    FitB,
25    /// Fit width of bounding box
26    FitBH { top: Option<f64> },
27    /// Fit height of bounding box
28    FitBV { left: Option<f64> },
29}
30
31/// Page destination reference
32#[derive(Debug, Clone)]
33pub enum PageDestination {
34    /// Page number (0-based)
35    PageNumber(u32),
36    /// Page object reference
37    PageRef(ObjectId),
38}
39
40/// PDF destination
41#[derive(Debug, Clone)]
42pub struct Destination {
43    /// Target page
44    pub page: PageDestination,
45    /// Destination type
46    pub dest_type: DestinationType,
47}
48
49impl Destination {
50    /// Create XYZ destination
51    pub fn xyz(
52        page: PageDestination,
53        left: Option<f64>,
54        top: Option<f64>,
55        zoom: Option<f64>,
56    ) -> Self {
57        Self {
58            page,
59            dest_type: DestinationType::XYZ { left, top, zoom },
60        }
61    }
62
63    /// Create Fit destination
64    pub fn fit(page: PageDestination) -> Self {
65        Self {
66            page,
67            dest_type: DestinationType::Fit,
68        }
69    }
70
71    /// Create FitH destination
72    pub fn fit_h(page: PageDestination, top: Option<f64>) -> Self {
73        Self {
74            page,
75            dest_type: DestinationType::FitH { top },
76        }
77    }
78
79    /// Create FitV destination
80    pub fn fit_v(page: PageDestination, left: Option<f64>) -> Self {
81        Self {
82            page,
83            dest_type: DestinationType::FitV { left },
84        }
85    }
86
87    /// Create FitR destination
88    pub fn fit_r(page: PageDestination, rect: Rectangle) -> Self {
89        Self {
90            page,
91            dest_type: DestinationType::FitR { rect },
92        }
93    }
94
95    /// Create FitB destination
96    pub fn fit_b(page: PageDestination) -> Self {
97        Self {
98            page,
99            dest_type: DestinationType::FitB,
100        }
101    }
102
103    /// Create FitBH destination
104    pub fn fit_bh(page: PageDestination, top: Option<f64>) -> Self {
105        Self {
106            page,
107            dest_type: DestinationType::FitBH { top },
108        }
109    }
110
111    /// Create FitBV destination
112    pub fn fit_bv(page: PageDestination, left: Option<f64>) -> Self {
113        Self {
114            page,
115            dest_type: DestinationType::FitBV { left },
116        }
117    }
118
119    /// Create destination from array
120    pub fn from_array(arr: &Array) -> Result<Self, crate::error::PdfError> {
121        use crate::error::PdfError;
122
123        if arr.len() < 2 {
124            return Err(PdfError::InvalidStructure(
125                "Destination array too short".into(),
126            ));
127        }
128
129        // Get page reference
130        let page = match arr.get(0) {
131            Some(Object::Integer(num)) => PageDestination::PageNumber(*num as u32),
132            Some(Object::Reference(id)) => PageDestination::PageRef(*id),
133            _ => {
134                return Err(PdfError::InvalidStructure(
135                    "Invalid page reference in destination".into(),
136                ))
137            }
138        };
139
140        // Get destination type
141        let type_name = match arr.get(1) {
142            Some(Object::Name(name)) => name,
143            _ => {
144                return Err(PdfError::InvalidStructure(
145                    "Invalid destination type".into(),
146                ))
147            }
148        };
149
150        let dest_type = match type_name.as_str() {
151            "XYZ" => {
152                if arr.len() < 5 {
153                    return Err(PdfError::InvalidStructure(
154                        "XYZ destination missing parameters".into(),
155                    ));
156                }
157                let left = match arr.get(2) {
158                    Some(Object::Real(v)) => Some(*v),
159                    Some(Object::Integer(v)) => Some(*v as f64),
160                    Some(Object::Null) => None,
161                    _ => {
162                        return Err(PdfError::InvalidStructure(
163                            "Invalid XYZ left parameter".into(),
164                        ))
165                    }
166                };
167                let top = match arr.get(3) {
168                    Some(Object::Real(v)) => Some(*v),
169                    Some(Object::Integer(v)) => Some(*v as f64),
170                    Some(Object::Null) => None,
171                    _ => {
172                        return Err(PdfError::InvalidStructure(
173                            "Invalid XYZ top parameter".into(),
174                        ))
175                    }
176                };
177                let zoom = match arr.get(4) {
178                    Some(Object::Real(v)) => Some(*v),
179                    Some(Object::Integer(v)) => Some(*v as f64),
180                    Some(Object::Null) => None,
181                    _ => {
182                        return Err(PdfError::InvalidStructure(
183                            "Invalid XYZ zoom parameter".into(),
184                        ))
185                    }
186                };
187                DestinationType::XYZ { left, top, zoom }
188            }
189            "Fit" => DestinationType::Fit,
190            "FitH" => {
191                if arr.len() < 3 {
192                    return Err(PdfError::InvalidStructure(
193                        "FitH destination missing parameter".into(),
194                    ));
195                }
196                let top = match arr.get(2) {
197                    Some(Object::Real(v)) => Some(*v),
198                    Some(Object::Integer(v)) => Some(*v as f64),
199                    Some(Object::Null) => None,
200                    _ => {
201                        return Err(PdfError::InvalidStructure(
202                            "Invalid FitH top parameter".into(),
203                        ))
204                    }
205                };
206                DestinationType::FitH { top }
207            }
208            "FitV" => {
209                if arr.len() < 3 {
210                    return Err(PdfError::InvalidStructure(
211                        "FitV destination missing parameter".into(),
212                    ));
213                }
214                let left = match arr.get(2) {
215                    Some(Object::Real(v)) => Some(*v),
216                    Some(Object::Integer(v)) => Some(*v as f64),
217                    Some(Object::Null) => None,
218                    _ => {
219                        return Err(PdfError::InvalidStructure(
220                            "Invalid FitV left parameter".into(),
221                        ))
222                    }
223                };
224                DestinationType::FitV { left }
225            }
226            "FitR" => {
227                if arr.len() < 6 {
228                    return Err(PdfError::InvalidStructure(
229                        "FitR destination missing parameters".into(),
230                    ));
231                }
232                let left = match arr.get(2) {
233                    Some(Object::Real(v)) => *v,
234                    Some(Object::Integer(v)) => *v as f64,
235                    _ => {
236                        return Err(PdfError::InvalidStructure(
237                            "Invalid FitR left parameter".into(),
238                        ))
239                    }
240                };
241                let bottom = match arr.get(3) {
242                    Some(Object::Real(v)) => *v,
243                    Some(Object::Integer(v)) => *v as f64,
244                    _ => {
245                        return Err(PdfError::InvalidStructure(
246                            "Invalid FitR bottom parameter".into(),
247                        ))
248                    }
249                };
250                let right = match arr.get(4) {
251                    Some(Object::Real(v)) => *v,
252                    Some(Object::Integer(v)) => *v as f64,
253                    _ => {
254                        return Err(PdfError::InvalidStructure(
255                            "Invalid FitR right parameter".into(),
256                        ))
257                    }
258                };
259                let top = match arr.get(5) {
260                    Some(Object::Real(v)) => *v,
261                    Some(Object::Integer(v)) => *v as f64,
262                    _ => {
263                        return Err(PdfError::InvalidStructure(
264                            "Invalid FitR top parameter".into(),
265                        ))
266                    }
267                };
268                let rect = Rectangle::new(
269                    crate::geometry::Point::new(left, bottom),
270                    crate::geometry::Point::new(right, top),
271                );
272                DestinationType::FitR { rect }
273            }
274            "FitB" => DestinationType::FitB,
275            "FitBH" => {
276                if arr.len() < 3 {
277                    return Err(PdfError::InvalidStructure(
278                        "FitBH destination missing parameter".into(),
279                    ));
280                }
281                let top = match arr.get(2) {
282                    Some(Object::Real(v)) => Some(*v),
283                    Some(Object::Integer(v)) => Some(*v as f64),
284                    Some(Object::Null) => None,
285                    _ => {
286                        return Err(PdfError::InvalidStructure(
287                            "Invalid FitBH top parameter".into(),
288                        ))
289                    }
290                };
291                DestinationType::FitBH { top }
292            }
293            "FitBV" => {
294                if arr.len() < 3 {
295                    return Err(PdfError::InvalidStructure(
296                        "FitBV destination missing parameter".into(),
297                    ));
298                }
299                let left = match arr.get(2) {
300                    Some(Object::Real(v)) => Some(*v),
301                    Some(Object::Integer(v)) => Some(*v as f64),
302                    Some(Object::Null) => None,
303                    _ => {
304                        return Err(PdfError::InvalidStructure(
305                            "Invalid FitBV left parameter".into(),
306                        ))
307                    }
308                };
309                DestinationType::FitBV { left }
310            }
311            _ => {
312                return Err(PdfError::InvalidStructure(format!(
313                    "Unknown destination type: {type_name}"
314                )))
315            }
316        };
317
318        Ok(Self { page, dest_type })
319    }
320
321    /// Convert to PDF array
322    pub fn to_array(&self) -> Array {
323        let mut arr = Array::new();
324
325        // Add page reference
326        match &self.page {
327            PageDestination::PageNumber(num) => {
328                arr.push(Object::Integer(*num as i64));
329            }
330            PageDestination::PageRef(id) => {
331                arr.push(Object::Reference(*id));
332            }
333        }
334
335        // Add destination type
336        match &self.dest_type {
337            DestinationType::XYZ { left, top, zoom } => {
338                arr.push(Object::Name("XYZ".to_string()));
339                arr.push(left.map(Object::Real).unwrap_or(Object::Null));
340                arr.push(top.map(Object::Real).unwrap_or(Object::Null));
341                arr.push(zoom.map(Object::Real).unwrap_or(Object::Null));
342            }
343            DestinationType::Fit => {
344                arr.push(Object::Name("Fit".to_string()));
345            }
346            DestinationType::FitH { top } => {
347                arr.push(Object::Name("FitH".to_string()));
348                arr.push(top.map(Object::Real).unwrap_or(Object::Null));
349            }
350            DestinationType::FitV { left } => {
351                arr.push(Object::Name("FitV".to_string()));
352                arr.push(left.map(Object::Real).unwrap_or(Object::Null));
353            }
354            DestinationType::FitR { rect } => {
355                arr.push(Object::Name("FitR".to_string()));
356                arr.push(Object::Real(rect.lower_left.x));
357                arr.push(Object::Real(rect.lower_left.y));
358                arr.push(Object::Real(rect.upper_right.x));
359                arr.push(Object::Real(rect.upper_right.y));
360            }
361            DestinationType::FitB => {
362                arr.push(Object::Name("FitB".to_string()));
363            }
364            DestinationType::FitBH { top } => {
365                arr.push(Object::Name("FitBH".to_string()));
366                arr.push(top.map(Object::Real).unwrap_or(Object::Null));
367            }
368            DestinationType::FitBV { left } => {
369                arr.push(Object::Name("FitBV".to_string()));
370                arr.push(left.map(Object::Real).unwrap_or(Object::Null));
371            }
372        }
373
374        arr
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use crate::geometry::Point;
382
383    #[test]
384    fn test_destination_type_debug_clone_partial_eq() {
385        let dest_type = DestinationType::XYZ {
386            left: Some(10.0),
387            top: Some(20.0),
388            zoom: None,
389        };
390        let debug_str = format!("{dest_type:?}");
391        assert!(debug_str.contains("XYZ"));
392
393        let cloned = dest_type.clone();
394        assert_eq!(dest_type, cloned);
395
396        let different = DestinationType::Fit;
397        assert_ne!(dest_type, different);
398    }
399
400    #[test]
401    fn test_page_destination_variants() {
402        let page_num = PageDestination::PageNumber(42);
403        let page_ref = PageDestination::PageRef(ObjectId::new(5, 0));
404
405        match page_num {
406            PageDestination::PageNumber(n) => assert_eq!(n, 42),
407            _ => panic!("Wrong variant"),
408        }
409
410        match page_ref {
411            PageDestination::PageRef(id) => {
412                assert_eq!(id.number(), 5);
413                assert_eq!(id.generation(), 0);
414            }
415            _ => panic!("Wrong variant"),
416        }
417    }
418
419    #[test]
420    fn test_page_destination_debug_clone() {
421        let page_dest = PageDestination::PageNumber(10);
422        let debug_str = format!("{page_dest:?}");
423        assert!(debug_str.contains("PageNumber"));
424        assert!(debug_str.contains("10"));
425
426        let cloned = page_dest.clone();
427        match cloned {
428            PageDestination::PageNumber(n) => assert_eq!(n, 10),
429            _ => panic!("Clone failed"),
430        }
431    }
432
433    #[test]
434    fn test_destination_debug_clone() {
435        let dest = Destination::fit(PageDestination::PageNumber(3));
436        let debug_str = format!("{dest:?}");
437        assert!(debug_str.contains("Destination"));
438        assert!(debug_str.contains("Fit"));
439
440        let cloned = dest.clone();
441        match cloned.page {
442            PageDestination::PageNumber(n) => assert_eq!(n, 3),
443            _ => panic!("Clone failed"),
444        }
445    }
446
447    #[test]
448    fn test_xyz_destination() {
449        let dest = Destination::xyz(
450            PageDestination::PageNumber(0),
451            Some(100.0),
452            Some(200.0),
453            Some(1.5),
454        );
455
456        match dest.dest_type {
457            DestinationType::XYZ { left, top, zoom } => {
458                assert_eq!(left, Some(100.0));
459                assert_eq!(top, Some(200.0));
460                assert_eq!(zoom, Some(1.5));
461            }
462            _ => panic!("Wrong destination type"),
463        }
464
465        let arr = dest.to_array();
466        assert_eq!(arr.len(), 5);
467        assert_eq!(arr.get(0), Some(&Object::Integer(0)));
468        assert_eq!(arr.get(1), Some(&Object::Name("XYZ".to_string())));
469        assert_eq!(arr.get(2), Some(&Object::Real(100.0)));
470        assert_eq!(arr.get(3), Some(&Object::Real(200.0)));
471        assert_eq!(arr.get(4), Some(&Object::Real(1.5)));
472    }
473
474    #[test]
475    fn test_fit_destination() {
476        let dest = Destination::fit(PageDestination::PageNumber(5));
477
478        match dest.dest_type {
479            DestinationType::Fit => (),
480            _ => panic!("Wrong destination type"),
481        }
482
483        let arr = dest.to_array();
484        assert_eq!(arr.len(), 2);
485        assert_eq!(arr.get(0), Some(&Object::Integer(5)));
486        assert_eq!(arr.get(1), Some(&Object::Name("Fit".to_string())));
487    }
488
489    #[test]
490    fn test_fit_h_destination() {
491        let dest = Destination::fit_h(PageDestination::PageNumber(1), Some(150.0));
492        match dest.dest_type {
493            DestinationType::FitH { top } => assert_eq!(top, Some(150.0)),
494            _ => panic!("Wrong destination type"),
495        }
496
497        let arr = dest.to_array();
498        assert_eq!(arr.len(), 3);
499        assert_eq!(arr.get(1), Some(&Object::Name("FitH".to_string())));
500        assert_eq!(arr.get(2), Some(&Object::Real(150.0)));
501
502        let dest_none = Destination::fit_h(PageDestination::PageNumber(1), None);
503        match dest_none.dest_type {
504            DestinationType::FitH { top } => assert!(top.is_none()),
505            _ => panic!("Wrong destination type"),
506        }
507    }
508
509    #[test]
510    fn test_fit_v_destination() {
511        let dest = Destination::fit_v(PageDestination::PageNumber(2), Some(75.0));
512        match dest.dest_type {
513            DestinationType::FitV { left } => assert_eq!(left, Some(75.0)),
514            _ => panic!("Wrong destination type"),
515        }
516
517        let arr = dest.to_array();
518        assert_eq!(arr.len(), 3);
519        assert_eq!(arr.get(1), Some(&Object::Name("FitV".to_string())));
520        assert_eq!(arr.get(2), Some(&Object::Real(75.0)));
521    }
522
523    #[test]
524    fn test_fit_r_destination() {
525        let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(150.0, 150.0));
526        let dest = Destination::fit_r(PageDestination::PageNumber(2), rect);
527
528        match dest.dest_type {
529            DestinationType::FitR { rect: r } => {
530                assert_eq!(r.lower_left.x, 50.0);
531                assert_eq!(r.lower_left.y, 50.0);
532                assert_eq!(r.upper_right.x, 150.0);
533                assert_eq!(r.upper_right.y, 150.0);
534            }
535            _ => panic!("Wrong destination type"),
536        }
537
538        let arr = dest.to_array();
539        assert_eq!(arr.len(), 6);
540        assert_eq!(arr.get(1), Some(&Object::Name("FitR".to_string())));
541        assert_eq!(arr.get(2), Some(&Object::Real(50.0)));
542        assert_eq!(arr.get(3), Some(&Object::Real(50.0)));
543        assert_eq!(arr.get(4), Some(&Object::Real(150.0)));
544        assert_eq!(arr.get(5), Some(&Object::Real(150.0)));
545    }
546
547    #[test]
548    fn test_fit_b_destinations() {
549        let dest_b = Destination::fit_b(PageDestination::PageNumber(3));
550        match dest_b.dest_type {
551            DestinationType::FitB => (),
552            _ => panic!("Wrong destination type"),
553        }
554
555        let arr = dest_b.to_array();
556        assert_eq!(arr.len(), 2);
557        assert_eq!(arr.get(1), Some(&Object::Name("FitB".to_string())));
558
559        let dest_bh = Destination::fit_bh(PageDestination::PageNumber(4), Some(100.0));
560        match dest_bh.dest_type {
561            DestinationType::FitBH { top } => assert_eq!(top, Some(100.0)),
562            _ => panic!("Wrong destination type"),
563        }
564
565        let dest_bv = Destination::fit_bv(PageDestination::PageNumber(5), Some(50.0));
566        match dest_bv.dest_type {
567            DestinationType::FitBV { left } => assert_eq!(left, Some(50.0)),
568            _ => panic!("Wrong destination type"),
569        }
570    }
571
572    #[test]
573    fn test_destination_to_array_page_ref() {
574        let page_ref = ObjectId::new(10, 0);
575        let dest = Destination::fit(PageDestination::PageRef(page_ref));
576        let array = dest.to_array();
577
578        assert_eq!(array.len(), 2);
579        assert_eq!(array.get(0), Some(&Object::Reference(page_ref)));
580        assert_eq!(array.get(1), Some(&Object::Name("Fit".to_string())));
581    }
582
583    #[test]
584    fn test_destination_to_array_xyz_with_nulls() {
585        let dest = Destination::xyz(PageDestination::PageNumber(0), None, Some(100.0), None);
586        let array = dest.to_array();
587
588        assert_eq!(array.len(), 5);
589        assert_eq!(array.get(0), Some(&Object::Integer(0)));
590        assert_eq!(array.get(1), Some(&Object::Name("XYZ".to_string())));
591        assert_eq!(array.get(2), Some(&Object::Null));
592        assert_eq!(array.get(3), Some(&Object::Real(100.0)));
593        assert_eq!(array.get(4), Some(&Object::Null));
594    }
595
596    #[test]
597    fn test_destination_to_array_all_types() {
598        // Test all destination types
599        let destinations = vec![
600            Destination::fit(PageDestination::PageNumber(0)),
601            Destination::fit_h(PageDestination::PageNumber(1), Some(50.0)),
602            Destination::fit_v(PageDestination::PageNumber(2), Some(75.0)),
603            Destination::fit_r(
604                PageDestination::PageNumber(3),
605                Rectangle::new(Point::new(10.0, 20.0), Point::new(100.0, 200.0)),
606            ),
607            Destination::fit_b(PageDestination::PageNumber(4)),
608            Destination::fit_bh(PageDestination::PageNumber(5), Some(150.0)),
609            Destination::fit_bv(PageDestination::PageNumber(6), Some(125.0)),
610        ];
611
612        let expected_names = ["Fit", "FitH", "FitV", "FitR", "FitB", "FitBH", "FitBV"];
613        let expected_lengths = [2, 3, 3, 6, 2, 3, 3];
614
615        for (dest, (expected_name, expected_len)) in destinations
616            .iter()
617            .zip(expected_names.iter().zip(expected_lengths.iter()))
618        {
619            let array = dest.to_array();
620            assert_eq!(array.len(), *expected_len);
621            assert_eq!(array.get(1), Some(&Object::Name(expected_name.to_string())));
622        }
623    }
624
625    #[test]
626    fn test_destination_type_all_variants() {
627        let variants = vec![
628            DestinationType::XYZ {
629                left: Some(1.0),
630                top: Some(2.0),
631                zoom: Some(3.0),
632            },
633            DestinationType::Fit,
634            DestinationType::FitH { top: Some(4.0) },
635            DestinationType::FitV { left: Some(5.0) },
636            DestinationType::FitR {
637                rect: Rectangle::new(Point::new(0.0, 0.0), Point::new(10.0, 10.0)),
638            },
639            DestinationType::FitB,
640            DestinationType::FitBH { top: Some(6.0) },
641            DestinationType::FitBV { left: Some(7.0) },
642        ];
643
644        for variant in variants {
645            let _ = format!("{variant:?}"); // Test Debug
646            let _ = variant.clone(); // Test Clone
647        }
648    }
649
650    #[test]
651    fn test_destination_edge_cases() {
652        // Test with page 0
653        let dest = Destination::fit(PageDestination::PageNumber(0));
654        let array = dest.to_array();
655        assert_eq!(array.get(0), Some(&Object::Integer(0)));
656
657        // Test with very high page number
658        let dest = Destination::fit(PageDestination::PageNumber(999999));
659        let array = dest.to_array();
660        assert_eq!(array.get(0), Some(&Object::Integer(999999)));
661
662        // Test with negative coordinates (should work)
663        let dest = Destination::xyz(
664            PageDestination::PageNumber(0),
665            Some(-100.0),
666            Some(-200.0),
667            Some(0.5),
668        );
669        let array = dest.to_array();
670        assert_eq!(array.get(2), Some(&Object::Real(-100.0)));
671        assert_eq!(array.get(3), Some(&Object::Real(-200.0)));
672    }
673
674    #[test]
675    fn test_destination_from_array() {
676        // Test basic from_array parsing
677        let mut array = Array::new();
678        array.push(Object::Integer(5));
679        array.push(Object::Name("XYZ".to_string()));
680        array.push(Object::Real(100.0));
681        array.push(Object::Real(200.0));
682        array.push(Object::Real(1.5));
683
684        let dest = Destination::from_array(&array).expect("Should parse destination");
685        match dest.page {
686            PageDestination::PageNumber(n) => assert_eq!(n, 5),
687            _ => panic!("Wrong page type"),
688        }
689        match dest.dest_type {
690            DestinationType::XYZ { left, top, zoom } => {
691                assert_eq!(left, Some(100.0));
692                assert_eq!(top, Some(200.0));
693                assert_eq!(zoom, Some(1.5));
694            }
695            _ => panic!("Wrong destination type"),
696        }
697    }
698
699    #[test]
700    fn test_destination_from_array_with_nulls() {
701        let mut array = Array::new();
702        array.push(Object::Integer(0));
703        array.push(Object::Name("XYZ".to_string()));
704        array.push(Object::Null);
705        array.push(Object::Real(100.0));
706        array.push(Object::Null);
707
708        let dest = Destination::from_array(&array).expect("Should parse destination");
709        match dest.dest_type {
710            DestinationType::XYZ { left, top, zoom } => {
711                assert!(left.is_none());
712                assert_eq!(top, Some(100.0));
713                assert!(zoom.is_none());
714            }
715            _ => panic!("Wrong destination type"),
716        }
717    }
718
719    #[test]
720    fn test_destination_from_array_with_page_ref() {
721        let page_ref = ObjectId::new(10, 0);
722        let mut array = Array::new();
723        array.push(Object::Reference(page_ref));
724        array.push(Object::Name("Fit".to_string()));
725
726        let dest = Destination::from_array(&array).expect("Should parse destination");
727        match dest.page {
728            PageDestination::PageRef(id) => assert_eq!(id, page_ref),
729            _ => panic!("Wrong page type"),
730        }
731    }
732
733    #[test]
734    fn test_destination_from_array_all_types() {
735        // Test FitH
736        let mut array = Array::new();
737        array.push(Object::Integer(1));
738        array.push(Object::Name("FitH".to_string()));
739        array.push(Object::Real(100.0));
740        let dest = Destination::from_array(&array).expect("Should parse FitH");
741        assert!(matches!(
742            dest.dest_type,
743            DestinationType::FitH { top: Some(100.0) }
744        ));
745
746        // Test FitV
747        let mut array = Array::new();
748        array.push(Object::Integer(2));
749        array.push(Object::Name("FitV".to_string()));
750        array.push(Object::Real(50.0));
751        let dest = Destination::from_array(&array).expect("Should parse FitV");
752        assert!(matches!(
753            dest.dest_type,
754            DestinationType::FitV { left: Some(50.0) }
755        ));
756
757        // Test FitR
758        let mut array = Array::new();
759        array.push(Object::Integer(3));
760        array.push(Object::Name("FitR".to_string()));
761        array.push(Object::Real(10.0));
762        array.push(Object::Real(20.0));
763        array.push(Object::Real(100.0));
764        array.push(Object::Real(200.0));
765        let dest = Destination::from_array(&array).expect("Should parse FitR");
766        match dest.dest_type {
767            DestinationType::FitR { rect } => {
768                assert_eq!(rect.lower_left.x, 10.0);
769                assert_eq!(rect.lower_left.y, 20.0);
770                assert_eq!(rect.upper_right.x, 100.0);
771                assert_eq!(rect.upper_right.y, 200.0);
772            }
773            _ => panic!("Wrong type"),
774        }
775
776        // Test FitB
777        let mut array = Array::new();
778        array.push(Object::Integer(4));
779        array.push(Object::Name("FitB".to_string()));
780        let dest = Destination::from_array(&array).expect("Should parse FitB");
781        assert!(matches!(dest.dest_type, DestinationType::FitB));
782    }
783
784    #[test]
785    fn test_destination_from_array_errors() {
786        // Empty array
787        let empty = Array::new();
788        assert!(Destination::from_array(&empty).is_err());
789
790        // Missing type
791        let mut array = Array::new();
792        array.push(Object::Integer(0));
793        assert!(Destination::from_array(&array).is_err());
794
795        // Invalid type
796        let mut array = Array::new();
797        array.push(Object::Integer(0));
798        array.push(Object::Name("InvalidType".to_string()));
799        assert!(Destination::from_array(&array).is_err());
800
801        // Missing XYZ parameters
802        let mut array = Array::new();
803        array.push(Object::Integer(0));
804        array.push(Object::Name("XYZ".to_string()));
805        // Missing left, top, zoom
806        assert!(Destination::from_array(&array).is_err());
807
808        // Invalid page reference
809        let mut array = Array::new();
810        array.push(Object::String("invalid".to_string()));
811        array.push(Object::Name("Fit".to_string()));
812        assert!(Destination::from_array(&array).is_err());
813    }
814
815    #[test]
816    fn test_destination_from_array_integer_coordinates() {
817        // Test that integer coordinates are properly converted to floats
818        let mut array = Array::new();
819        array.push(Object::Integer(0));
820        array.push(Object::Name("XYZ".to_string()));
821        array.push(Object::Integer(100));
822        array.push(Object::Integer(200));
823        array.push(Object::Integer(2));
824
825        let dest = Destination::from_array(&array).expect("Should parse with integer coords");
826        match dest.dest_type {
827            DestinationType::XYZ { left, top, zoom } => {
828                assert_eq!(left, Some(100.0));
829                assert_eq!(top, Some(200.0));
830                assert_eq!(zoom, Some(2.0));
831            }
832            _ => panic!("Wrong type"),
833        }
834    }
835
836    #[test]
837    fn test_destination_roundtrip() {
838        // Test that to_array and from_array are inverses
839        let original = Destination::xyz(
840            PageDestination::PageNumber(42),
841            Some(123.45),
842            Some(678.90),
843            Some(1.25),
844        );
845
846        let array = original.to_array();
847        let parsed = Destination::from_array(&array).expect("Should parse");
848
849        match parsed.page {
850            PageDestination::PageNumber(n) => assert_eq!(n, 42),
851            _ => panic!("Wrong page"),
852        }
853
854        match parsed.dest_type {
855            DestinationType::XYZ { left, top, zoom } => {
856                assert_eq!(left, Some(123.45));
857                assert_eq!(top, Some(678.90));
858                assert_eq!(zoom, Some(1.25));
859            }
860            _ => panic!("Wrong type"),
861        }
862    }
863}