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> {}