Skip to main content

pdfplumber_core/
bookmark.rs

1//! PDF bookmark / outline / table of contents types.
2//!
3//! Provides [`Bookmark`] for representing entries in the PDF document outline
4//! tree (bookmarks / table of contents).
5
6/// A single entry in the PDF document outline (bookmark / table of contents).
7///
8/// Bookmarks are extracted from the `/Outlines` dictionary in the PDF catalog.
9/// Each bookmark has a title, a nesting level, and optionally a destination
10/// page number and y-coordinate.
11#[derive(Debug, Clone, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct Bookmark {
14    /// The bookmark title text.
15    pub title: String,
16    /// Nesting depth (0-indexed). Top-level bookmarks have level 0.
17    pub level: usize,
18    /// The 0-indexed destination page number, if resolvable.
19    pub page_number: Option<usize>,
20    /// The y-coordinate on the destination page (top of view), if available.
21    pub dest_top: Option<f64>,
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27
28    #[test]
29    fn bookmark_with_all_fields() {
30        let bm = Bookmark {
31            title: "Chapter 1".to_string(),
32            level: 0,
33            page_number: Some(0),
34            dest_top: Some(792.0),
35        };
36        assert_eq!(bm.title, "Chapter 1");
37        assert_eq!(bm.level, 0);
38        assert_eq!(bm.page_number, Some(0));
39        assert_eq!(bm.dest_top, Some(792.0));
40    }
41
42    #[test]
43    fn bookmark_without_destination() {
44        let bm = Bookmark {
45            title: "Appendix".to_string(),
46            level: 1,
47            page_number: None,
48            dest_top: None,
49        };
50        assert_eq!(bm.title, "Appendix");
51        assert_eq!(bm.level, 1);
52        assert!(bm.page_number.is_none());
53        assert!(bm.dest_top.is_none());
54    }
55
56    #[test]
57    fn bookmark_clone_and_eq() {
58        let bm1 = Bookmark {
59            title: "Section 2.1".to_string(),
60            level: 2,
61            page_number: Some(5),
62            dest_top: Some(500.0),
63        };
64        let bm2 = bm1.clone();
65        assert_eq!(bm1, bm2);
66    }
67
68    #[test]
69    fn bookmark_nested_levels() {
70        let bookmarks = vec![
71            Bookmark {
72                title: "Chapter 1".to_string(),
73                level: 0,
74                page_number: Some(0),
75                dest_top: None,
76            },
77            Bookmark {
78                title: "Section 1.1".to_string(),
79                level: 1,
80                page_number: Some(2),
81                dest_top: None,
82            },
83            Bookmark {
84                title: "Section 1.1.1".to_string(),
85                level: 2,
86                page_number: Some(3),
87                dest_top: None,
88            },
89            Bookmark {
90                title: "Chapter 2".to_string(),
91                level: 0,
92                page_number: Some(10),
93                dest_top: None,
94            },
95        ];
96        assert_eq!(bookmarks.len(), 4);
97        assert_eq!(bookmarks[0].level, 0);
98        assert_eq!(bookmarks[1].level, 1);
99        assert_eq!(bookmarks[2].level, 2);
100        assert_eq!(bookmarks[3].level, 0);
101    }
102}