Skip to main content

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;
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;
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!(
622                array.get(1),
623                Some(&Object::Name((*expected_name).to_string()))
624            );
625        }
626    }
627
628    #[test]
629    fn test_destination_type_all_variants() {
630        let variants = vec![
631            DestinationType::XYZ {
632                left: Some(1.0),
633                top: Some(2.0),
634                zoom: Some(3.0),
635            },
636            DestinationType::Fit,
637            DestinationType::FitH { top: Some(4.0) },
638            DestinationType::FitV { left: Some(5.0) },
639            DestinationType::FitR {
640                rect: Rectangle::new(Point::new(0.0, 0.0), Point::new(10.0, 10.0)),
641            },
642            DestinationType::FitB,
643            DestinationType::FitBH { top: Some(6.0) },
644            DestinationType::FitBV { left: Some(7.0) },
645        ];
646
647        for variant in variants {
648            let _ = format!("{variant:?}"); // Test Debug
649            let _ = variant.clone(); // Test Clone
650        }
651    }
652
653    #[test]
654    fn test_destination_edge_cases() {
655        // Test with page 0
656        let dest = Destination::fit(PageDestination::PageNumber(0));
657        let array = dest.to_array();
658        assert_eq!(array.get(0), Some(&Object::Integer(0)));
659
660        // Test with very high page number
661        let dest = Destination::fit(PageDestination::PageNumber(999999));
662        let array = dest.to_array();
663        assert_eq!(array.get(0), Some(&Object::Integer(999999)));
664
665        // Test with negative coordinates (should work)
666        let dest = Destination::xyz(
667            PageDestination::PageNumber(0),
668            Some(-100.0),
669            Some(-200.0),
670            Some(0.5),
671        );
672        let array = dest.to_array();
673        assert_eq!(array.get(2), Some(&Object::Real(-100.0)));
674        assert_eq!(array.get(3), Some(&Object::Real(-200.0)));
675    }
676
677    #[test]
678    fn test_destination_from_array() {
679        // Test basic from_array parsing
680        let mut array = Array::new();
681        array.push(Object::Integer(5));
682        array.push(Object::Name("XYZ".to_string()));
683        array.push(Object::Real(100.0));
684        array.push(Object::Real(200.0));
685        array.push(Object::Real(1.5));
686
687        let dest = Destination::from_array(&array).expect("Should parse destination");
688        match dest.page {
689            PageDestination::PageNumber(n) => assert_eq!(n, 5),
690            _ => panic!("Wrong page type"),
691        }
692        match dest.dest_type {
693            DestinationType::XYZ { left, top, zoom } => {
694                assert_eq!(left, Some(100.0));
695                assert_eq!(top, Some(200.0));
696                assert_eq!(zoom, Some(1.5));
697            }
698            _ => panic!("Wrong destination type"),
699        }
700    }
701
702    #[test]
703    fn test_destination_from_array_with_nulls() {
704        let mut array = Array::new();
705        array.push(Object::Integer(0));
706        array.push(Object::Name("XYZ".to_string()));
707        array.push(Object::Null);
708        array.push(Object::Real(100.0));
709        array.push(Object::Null);
710
711        let dest = Destination::from_array(&array).expect("Should parse destination");
712        match dest.dest_type {
713            DestinationType::XYZ { left, top, zoom } => {
714                assert!(left.is_none());
715                assert_eq!(top, Some(100.0));
716                assert!(zoom.is_none());
717            }
718            _ => panic!("Wrong destination type"),
719        }
720    }
721
722    #[test]
723    fn test_destination_from_array_with_page_ref() {
724        let page_ref = ObjectId::new(10, 0);
725        let mut array = Array::new();
726        array.push(Object::Reference(page_ref));
727        array.push(Object::Name("Fit".to_string()));
728
729        let dest = Destination::from_array(&array).expect("Should parse destination");
730        match dest.page {
731            PageDestination::PageRef(id) => assert_eq!(id, page_ref),
732            _ => panic!("Wrong page type"),
733        }
734    }
735
736    #[test]
737    fn test_destination_from_array_all_types() {
738        // Test FitH
739        let mut array = Array::new();
740        array.push(Object::Integer(1));
741        array.push(Object::Name("FitH".to_string()));
742        array.push(Object::Real(100.0));
743        let dest = Destination::from_array(&array).expect("Should parse FitH");
744        assert!(matches!(
745            dest.dest_type,
746            DestinationType::FitH { top: Some(100.0) }
747        ));
748
749        // Test FitV
750        let mut array = Array::new();
751        array.push(Object::Integer(2));
752        array.push(Object::Name("FitV".to_string()));
753        array.push(Object::Real(50.0));
754        let dest = Destination::from_array(&array).expect("Should parse FitV");
755        assert!(matches!(
756            dest.dest_type,
757            DestinationType::FitV { left: Some(50.0) }
758        ));
759
760        // Test FitR
761        let mut array = Array::new();
762        array.push(Object::Integer(3));
763        array.push(Object::Name("FitR".to_string()));
764        array.push(Object::Real(10.0));
765        array.push(Object::Real(20.0));
766        array.push(Object::Real(100.0));
767        array.push(Object::Real(200.0));
768        let dest = Destination::from_array(&array).expect("Should parse FitR");
769        match dest.dest_type {
770            DestinationType::FitR { rect } => {
771                assert_eq!(rect.lower_left.x, 10.0);
772                assert_eq!(rect.lower_left.y, 20.0);
773                assert_eq!(rect.upper_right.x, 100.0);
774                assert_eq!(rect.upper_right.y, 200.0);
775            }
776            _ => panic!("Wrong type"),
777        }
778
779        // Test FitB
780        let mut array = Array::new();
781        array.push(Object::Integer(4));
782        array.push(Object::Name("FitB".to_string()));
783        let dest = Destination::from_array(&array).expect("Should parse FitB");
784        assert!(matches!(dest.dest_type, DestinationType::FitB));
785    }
786
787    #[test]
788    fn test_destination_from_array_errors() {
789        // Empty array
790        let empty = Array::new();
791        assert!(Destination::from_array(&empty).is_err());
792
793        // Missing type
794        let mut array = Array::new();
795        array.push(Object::Integer(0));
796        assert!(Destination::from_array(&array).is_err());
797
798        // Invalid type
799        let mut array = Array::new();
800        array.push(Object::Integer(0));
801        array.push(Object::Name("InvalidType".to_string()));
802        assert!(Destination::from_array(&array).is_err());
803
804        // Missing XYZ parameters
805        let mut array = Array::new();
806        array.push(Object::Integer(0));
807        array.push(Object::Name("XYZ".to_string()));
808        // Missing left, top, zoom
809        assert!(Destination::from_array(&array).is_err());
810
811        // Invalid page reference
812        let mut array = Array::new();
813        array.push(Object::String("invalid".to_string()));
814        array.push(Object::Name("Fit".to_string()));
815        assert!(Destination::from_array(&array).is_err());
816    }
817
818    #[test]
819    fn test_destination_from_array_integer_coordinates() {
820        // Test that integer coordinates are properly converted to floats
821        let mut array = Array::new();
822        array.push(Object::Integer(0));
823        array.push(Object::Name("XYZ".to_string()));
824        array.push(Object::Integer(100));
825        array.push(Object::Integer(200));
826        array.push(Object::Integer(2));
827
828        let dest = Destination::from_array(&array).expect("Should parse with integer coords");
829        match dest.dest_type {
830            DestinationType::XYZ { left, top, zoom } => {
831                assert_eq!(left, Some(100.0));
832                assert_eq!(top, Some(200.0));
833                assert_eq!(zoom, Some(2.0));
834            }
835            _ => panic!("Wrong type"),
836        }
837    }
838
839    #[test]
840    fn test_destination_roundtrip() {
841        // Test that to_array and from_array are inverses
842        let original = Destination::xyz(
843            PageDestination::PageNumber(42),
844            Some(123.45),
845            Some(678.90),
846            Some(1.25),
847        );
848
849        let array = original.to_array();
850        let parsed = Destination::from_array(&array).expect("Should parse");
851
852        match parsed.page {
853            PageDestination::PageNumber(n) => assert_eq!(n, 42),
854            _ => panic!("Wrong page"),
855        }
856
857        match parsed.dest_type {
858            DestinationType::XYZ { left, top, zoom } => {
859                assert_eq!(left, Some(123.45));
860                assert_eq!(top, Some(678.90));
861                assert_eq!(zoom, Some(1.25));
862            }
863            _ => panic!("Wrong type"),
864        }
865    }
866}