Skip to main content

pdfium_render/pdf/document/
bookmarks.rs

1//! Defines the [PdfBookmarks] struct, exposing functionality related to the
2//! bookmarks contained within a single [PdfDocument].
3
4use crate::bindgen::{FPDF_BOOKMARK, FPDF_DOCUMENT};
5use crate::error::{PdfiumError, PdfiumInternalError};
6use crate::pdf::document::bookmark::PdfBookmark;
7use crate::pdfium::PdfiumLibraryBindingsAccessor;
8use std::collections::HashSet;
9use std::marker::PhantomData;
10
11#[cfg(doc)]
12use crate::pdf::document::PdfDocument;
13
14/// The bookmarks contained within a single [PdfDocument].
15///
16/// Bookmarks in PDF files form a tree structure, branching out from a top-level root bookmark.
17/// The [PdfBookmarks::root()] returns the root bookmark in the containing [PdfDocument], if any;
18/// use the root's [PdfBookmark::first_child()] and [PdfBookmark::next_sibling()] functions to
19/// traverse the bookmark tree.
20///
21/// To search the tree for a bookmark with a specific title, use the [PdfBookmarks::find_first_by_title()]
22/// and [PdfBookmarks::find_all_by_title()] functions. To traverse the tree breadth-first, visiting
23/// every bookmark in the tree, create an iterator using the [PdfBookmarks::iter()] function.
24pub struct PdfBookmarks<'a> {
25    document_handle: FPDF_DOCUMENT,
26    lifetime: PhantomData<&'a FPDF_DOCUMENT>,
27}
28
29impl<'a> PdfBookmarks<'a> {
30    #[inline]
31    pub(crate) fn from_pdfium(document_handle: FPDF_DOCUMENT) -> Self {
32        Self {
33            document_handle,
34            lifetime: PhantomData,
35        }
36    }
37
38    /// Returns the internal `FPDF_DOCUMENT` handle of the [PdfDocument] containing
39    /// this [PdfBookmarks] collection.
40    #[inline]
41    pub(crate) fn document_handle(&self) -> FPDF_DOCUMENT {
42        self.document_handle
43    }
44
45    /// Returns the root [PdfBookmark] in the containing [PdfDocument], if any.
46    pub fn root(&self) -> Option<PdfBookmark<'_>> {
47        let bookmark_handle = unsafe {
48            self.bindings()
49                .FPDFBookmark_GetFirstChild(self.document_handle, std::ptr::null_mut())
50        };
51
52        if bookmark_handle.is_null() {
53            None
54        } else {
55            Some(PdfBookmark::from_pdfium(
56                bookmark_handle,
57                None,
58                self.document_handle,
59            ))
60        }
61    }
62
63    /// Returns the first [PdfBookmark] in the containing [PdfDocument] that has a title matching
64    /// the given string.
65    ///
66    /// Note that bookmarks are not required to have unique titles, so in theory any number of
67    /// bookmarks could match a given title. This function only ever returns the first. To return
68    /// all matches, use [PdfBookmarks::find_all_by_title()].
69    pub fn find_first_by_title(&self, title: &str) -> Result<PdfBookmark<'_>, PdfiumError> {
70        let handle = unsafe {
71            self.bindings()
72                .FPDFBookmark_Find_str(self.document_handle, title)
73        };
74
75        if handle.is_null() {
76            Err(PdfiumError::PdfiumLibraryInternalError(
77                PdfiumInternalError::Unknown,
78            ))
79        } else {
80            Ok(PdfBookmark::from_pdfium(handle, None, self.document_handle))
81        }
82    }
83
84    /// Returns all [PdfBookmark] objects in the containing [PdfDocument] that have a title
85    /// matching the given string.
86    ///
87    /// Note that bookmarks are not required to have unique titles, so in theory any number of
88    /// bookmarks could match a given title. This function returns all matches by performing
89    /// a complete breadth-first traversal of the entire bookmark tree. To return just the first
90    /// match, use [PdfBookmarks::find_first_by_title()].
91    pub fn find_all_by_title(&self, title: &str) -> Vec<PdfBookmark<'_>> {
92        self.iter()
93            .filter(|bookmark| match bookmark.title() {
94                Some(bookmark_title) => bookmark_title == title,
95                None => false,
96            })
97            .collect()
98    }
99
100    /// Returns a depth-first prefix-order iterator over all the [PdfBookmark]
101    /// objects in the containing [PdfDocument], starting from the top-level
102    /// root bookmark.
103    #[inline]
104    pub fn iter(&self) -> PdfBookmarksIterator<'_> {
105        PdfBookmarksIterator::new(self.root(), true, None, self.document_handle())
106    }
107}
108
109impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfBookmarks<'a> {}
110
111#[cfg(feature = "thread_safe")]
112unsafe impl<'a> Send for PdfBookmarks<'a> {}
113
114#[cfg(feature = "thread_safe")]
115unsafe impl<'a> Sync for PdfBookmarks<'a> {}
116
117/// An iterator over all the [PdfBookmark] objects in a [PdfBookmarks] collection.
118pub struct PdfBookmarksIterator<'a> {
119    document_handle: FPDF_DOCUMENT,
120
121    // If true, recurse into descendants.
122    include_descendants: bool,
123
124    // A stack of pairs of (Bookmark Node, Node's Parent). The parent may be NULL
125    // if it is a root node, or if the parent is unknown.
126    pending_stack: Vec<(FPDF_BOOKMARK, FPDF_BOOKMARK)>,
127
128    // The set of nodes already visited. This ensures we terminate if the document's
129    // bookmark graph is cyclic.
130    visited: HashSet<FPDF_BOOKMARK>,
131
132    // This bookmark will not be returned by the iterator (but its siblings and
133    // descendants will be explored). May be NULL.
134    skip_sibling: FPDF_BOOKMARK,
135
136    lifetime: PhantomData<&'a FPDF_BOOKMARK>,
137}
138
139impl<'a> PdfBookmarksIterator<'a> {
140    pub(crate) fn new(
141        start_node: Option<PdfBookmark<'a>>,
142        include_descendants: bool,
143        skip_sibling: Option<PdfBookmark<'a>>,
144        document_handle: FPDF_DOCUMENT,
145    ) -> Self {
146        let mut result = PdfBookmarksIterator {
147            document_handle,
148            include_descendants,
149            pending_stack: Vec::with_capacity(20),
150            visited: HashSet::new(),
151            skip_sibling: std::ptr::null_mut(),
152            lifetime: PhantomData,
153        };
154
155        // If we have a skip-sibling, record its handle.
156        if let Some(skip_sibling) = skip_sibling {
157            result.skip_sibling = skip_sibling.bookmark_handle();
158        }
159
160        // Push the start node onto the stack to initiate graph traversal.
161        if let Some(start_node) = start_node {
162            result.pending_stack.push((
163                start_node.bookmark_handle(),
164                start_node
165                    .parent()
166                    .map(|parent| parent.bookmark_handle())
167                    .unwrap_or(std::ptr::null_mut()),
168            ));
169        }
170
171        result
172    }
173}
174
175impl<'a> Iterator for PdfBookmarksIterator<'a> {
176    type Item = PdfBookmark<'a>;
177
178    fn next(&mut self) -> Option<Self::Item> {
179        // A straightforward tail-recursive function to walk the bookmarks might
180        // look like this:
181        //
182        // pub fn walk(node: Option<PdfBookmark<'a>>) {
183        //     if let Some(node) = node) {
184        //         visit(&node);
185        //         walk(node.first_child());
186        //         walk(node.next_sibling());
187        //     }
188        // }
189        //
190        // This iterator implements that algorithm with the following additional
191        // complexities:
192        //
193        // - Iterators, of course, can't take advantage of recursion. So the
194        //   call stack which is implicit in the recursive version becomes an
195        //   explicit stack retained in PdfIterator::pending_stack.
196        // - For efficiency, the iterator internally operates with FPDF_BOOKMARK
197        //   handles, and only constructs PdfBookmark objects right before
198        //   they're returned.
199        // - PdfIterator::visited keeps a HashSet of visited nodes, to ensure
200        //   termination even if the PDF's bookmark graph is cyclic.
201        // - PdfIterator::skip_sibling keeps a FPDF_BOOKMARK that will not be
202        //   returned by the iterator (but, importantly, its siblings will
203        //   still be explored).
204
205        while let Some((node, parent)) = self.pending_stack.pop() {
206            if node.is_null() || self.visited.contains(&node) {
207                continue;
208            }
209            self.visited.insert(node);
210
211            // Add our next sibling to the stack first, so we'll come back to it
212            // after having addressed our descendants. It's okay if it's NULL,
213            // we'll handle that when it comes off the stack.
214            self.pending_stack.push((
215                unsafe {
216                    self.bindings()
217                        .FPDFBookmark_GetNextSibling(self.document_handle, node)
218                },
219                parent,
220            ));
221
222            // Add our first descendant to the stack if we should include them.
223            // Again, it's okay if it's NULL.
224            if self.include_descendants {
225                self.pending_stack.push((
226                    unsafe {
227                        self.bindings()
228                            .FPDFBookmark_GetFirstChild(self.document_handle, node)
229                    },
230                    node,
231                ));
232            }
233
234            // If the present node isn't the one we're meant to skip, return it.
235            if node != self.skip_sibling {
236                let parent = if parent.is_null() { None } else { Some(parent) };
237                return Some(PdfBookmark::from_pdfium(node, parent, self.document_handle));
238            }
239        }
240
241        // If we get here then the stack is empty and we're done.
242        None
243    }
244}
245
246impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfBookmarksIterator<'a> {}
247
248#[cfg(feature = "thread_safe")]
249unsafe impl<'a> Send for PdfBookmarksIterator<'a> {}
250
251#[cfg(feature = "thread_safe")]
252unsafe impl<'a> Sync for PdfBookmarksIterator<'a> {}