Skip to main content

pdfium_render/pdf/document/
bookmark.rs

1//! Defines the [PdfBookmark] struct, exposing functionality related to a single bookmark
2//! in a [PdfBookmarks] collection.
3
4use crate::bindgen::{FPDF_BOOKMARK, FPDF_DOCUMENT};
5use crate::pdf::action::PdfAction;
6use crate::pdf::destination::PdfDestination;
7use crate::pdf::document::bookmarks::PdfBookmarksIterator;
8use crate::pdfium::PdfiumLibraryBindingsAccessor;
9use crate::utils::mem::create_byte_buffer;
10use crate::utils::utf16le::get_string_from_pdfium_utf16le_bytes;
11use std::hash::{Hash, Hasher};
12use std::marker::PhantomData;
13use std::os::raw::c_void;
14
15#[cfg(doc)]
16use {
17    crate::pdf::action::PdfActionType, crate::pdf::document::bookmarks::PdfBookmarks,
18    crate::pdf::document::PdfDocument,
19};
20
21/// A single bookmark in a [PdfBookmarks] collection.
22#[derive(Clone)]
23pub struct PdfBookmark<'a> {
24    bookmark_handle: FPDF_BOOKMARK,
25    parent: Option<FPDF_BOOKMARK>,
26    document_handle: FPDF_DOCUMENT,
27    lifetime: PhantomData<&'a FPDF_BOOKMARK>,
28}
29
30impl<'a> PartialEq for PdfBookmark<'a> {
31    fn eq(&self, other: &Self) -> bool {
32        // Two bookmarks are equal if they are the same bookmark from the same PdfDocument.
33
34        // Equality does not consider whether or not the bookmark's parent is available. One
35        // PdfBookmark object will compare equal to another if they refer to the same bookmark,
36        // even if the parent is available for one of but not the other.
37
38        // Two bookmarks derived from different PdfDocument objects will always compare unequal,
39        // even if both PdfDocument objects have opened the same underlying PDF file and both
40        // PdfBookmark objects refer to the same bookmark.
41
42        // Consequently, it is sufficient to only consider bookmark_handle because:
43        // - The handle is a pointer to an internal Pdfium structure, so different bookmarks must
44        //   have different handles.
45        // - The structure is allocated and retained by Pdfium for as long as the document is open,
46        //   so the same bookmark will always give the same handle.
47
48        self.bookmark_handle == other.bookmark_handle
49    }
50}
51
52impl<'a> Eq for PdfBookmark<'a> {}
53
54impl<'a> Hash for PdfBookmark<'a> {
55    fn hash<H>(&self, state: &mut H)
56    where
57        H: Hasher,
58    {
59        self.bookmark_handle.hash(state);
60    }
61}
62
63impl<'a> PdfBookmark<'a> {
64    pub(crate) fn from_pdfium(
65        bookmark_handle: FPDF_BOOKMARK,
66        parent: Option<FPDF_BOOKMARK>,
67        document_handle: FPDF_DOCUMENT,
68    ) -> Self {
69        PdfBookmark {
70            bookmark_handle,
71            parent,
72            document_handle,
73            lifetime: PhantomData,
74        }
75    }
76
77    /// Returns the internal `FPDF_BOOKMARK` handle for this [PdfBookmark].
78    #[inline]
79    pub(crate) fn bookmark_handle(&self) -> FPDF_BOOKMARK {
80        self.bookmark_handle
81    }
82
83    /// Returns the internal `FPDF_DOCUMENT` handle of the [PdfDocument] containing this [PdfBookmark].
84    #[inline]
85    pub(crate) fn document_handle(&self) -> FPDF_DOCUMENT {
86        self.document_handle
87    }
88
89    /// Returns the title of this [PdfBookmark], if any.
90    pub fn title(&self) -> Option<String> {
91        // Retrieving the bookmark title from Pdfium is a two-step operation. First, we call
92        // FPDFBookmark_GetTitle() with a null buffer; this will retrieve the length of
93        // the bookmark title in bytes. If the length is zero, then there is no title.
94
95        // If the length is non-zero, then we reserve a byte buffer of the given
96        // length and call FPDFBookmark_GetTitle() again with a pointer to the buffer;
97        // this will write the bookmark title to the buffer in UTF16-LE format.
98
99        let buffer_length = unsafe {
100            self.bindings()
101                .FPDFBookmark_GetTitle(self.bookmark_handle, std::ptr::null_mut(), 0)
102        };
103
104        if buffer_length == 0 {
105            // No title is defined.
106
107            return None;
108        }
109
110        let mut buffer = create_byte_buffer(buffer_length as usize);
111
112        let result = unsafe {
113            self.bindings().FPDFBookmark_GetTitle(
114                self.bookmark_handle,
115                buffer.as_mut_ptr() as *mut c_void,
116                buffer_length,
117            )
118        };
119
120        assert_eq!(result, buffer_length);
121
122        get_string_from_pdfium_utf16le_bytes(buffer)
123    }
124
125    /// Returns the [PdfAction] associated with this [PdfBookmark], if any.
126    ///
127    /// The action indicates the behaviour that will occur when the user interacts with the
128    /// bookmark in a PDF viewer. For most bookmarks, this will be a local navigation action
129    /// of type [PdfActionType::GoToDestinationInSameDocument], but the PDF file format supports
130    /// a variety of other actions.
131    pub fn action(&self) -> Option<PdfAction<'a>> {
132        let handle = unsafe { self.bindings().FPDFBookmark_GetAction(self.bookmark_handle) };
133
134        if handle.is_null() {
135            None
136        } else {
137            Some(PdfAction::from_pdfium(
138                handle,
139                self.document_handle,
140                self.bindings(),
141            ))
142        }
143    }
144
145    /// Returns the [PdfDestination] associated with this [PdfBookmark], if any.
146    ///
147    /// The destination specifies the page and region, if any, that will be the target
148    /// of the action behaviour specified by [PdfBookmark::action()].
149    pub fn destination(&self) -> Option<PdfDestination<'a>> {
150        let handle = unsafe {
151            self.bindings()
152                .FPDFBookmark_GetDest(self.document_handle, self.bookmark_handle)
153        };
154
155        if handle.is_null() {
156            None
157        } else {
158            Some(PdfDestination::from_pdfium(self.document_handle, handle))
159        }
160    }
161
162    /// Returns this [PdfBookmark] object's direct parent, if available.
163    #[inline]
164    pub fn parent(&self) -> Option<PdfBookmark<'a>> {
165        self.parent.map(|parent_handle| {
166            PdfBookmark::from_pdfium(parent_handle, None, self.document_handle)
167        })
168    }
169
170    /// Returns the number of direct children of this [PdfBookmark].
171    #[inline]
172    pub fn children_len(&self) -> usize {
173        // If there are N child bookmarks, then FPDFBookmark_GetCount returns a
174        // N if the bookmark tree should be displayed open by default, and -N if
175        // the child tree should be displayed closed by deafult.
176        (unsafe {
177            self.bindings()
178                .FPDFBookmark_GetCount(self.bookmark_handle)
179                .unsigned_abs()
180        }) as usize
181    }
182
183    /// Returns the first child [PdfBookmark] of this [PdfBookmark], if any.
184    pub fn first_child(&self) -> Option<PdfBookmark<'a>> {
185        let handle = unsafe {
186            self.bindings()
187                .FPDFBookmark_GetFirstChild(self.document_handle, self.bookmark_handle)
188        };
189
190        if handle.is_null() {
191            None
192        } else {
193            Some(PdfBookmark::from_pdfium(
194                handle,
195                Some(self.bookmark_handle),
196                self.document_handle,
197            ))
198        }
199    }
200
201    /// Returns the next [PdfBookmark] at the same tree level as this [PdfBookmark], if any.
202    pub fn next_sibling(&self) -> Option<PdfBookmark<'a>> {
203        let handle = unsafe {
204            self.bindings()
205                .FPDFBookmark_GetNextSibling(self.document_handle, self.bookmark_handle)
206        };
207
208        if handle.is_null() {
209            None
210        } else {
211            Some(PdfBookmark::from_pdfium(
212                handle,
213                self.parent,
214                self.document_handle,
215            ))
216        }
217    }
218
219    /// Returns an iterator over all [PdfBookmark] sibling nodes of this [PdfBookmark].
220    #[inline]
221    pub fn iter_siblings(&self) -> PdfBookmarksIterator<'a> {
222        match self.parent {
223            Some(parent_handle) => {
224                // Siblings by definition all share the same parent. We can achieve a more
225                // consistent result, irrespective of whether we are the parent's first direct
226                // child or not, by iterating over all the parent's children.
227
228                PdfBookmarksIterator::new(
229                    PdfBookmark::from_pdfium(parent_handle, None, self.document_handle)
230                        .first_child(),
231                    false,
232                    // Signal that the iterator should skip over this bookmark when iterating
233                    // the parent's direct children.
234                    Some(self.clone()),
235                    self.document_handle(),
236                )
237            }
238            None => {
239                // Since no handle to the parent is available, the best we can do is create an iterator
240                // that repeatedly calls Self::next_sibling(). If we are not the first direct child
241                // of a parent node, then this approach may not include all the parent's children.
242
243                PdfBookmarksIterator::new(
244                    Some(self.clone()),
245                    false,
246                    // Signal that the iterator should skip over this bookmark when iterating
247                    // the parent's direct children.
248                    Some(self.clone()),
249                    self.document_handle(),
250                )
251            }
252        }
253    }
254
255    /// Returns an iterator over all [PdfBookmark] child nodes of this [PdfBookmark].
256    /// Only direct children of this [PdfBookmark] will be traversed by the iterator;
257    /// grandchildren, great-grandchildren, and other descendant nodes will be ignored.
258    /// To visit all child nodes, including children of children, use [PdfBookmark::iter_all_descendants()].
259    #[inline]
260    pub fn iter_direct_children(&self) -> PdfBookmarksIterator<'a> {
261        PdfBookmarksIterator::new(self.first_child(), false, None, self.document_handle())
262    }
263
264    /// Returns an iterator over all [PdfBookmark] descendant nodes of this [PdfBookmark],
265    /// including any children of those nodes. To visit only direct children of this [PdfBookmark],
266    /// use [PdfBookmark::iter_direct_children()].
267    #[inline]
268    pub fn iter_all_descendants(&self) -> PdfBookmarksIterator<'a> {
269        PdfBookmarksIterator::new(self.first_child(), true, None, self.document_handle())
270    }
271}
272
273impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfBookmark<'a> {}
274
275#[cfg(feature = "thread_safe")]
276unsafe impl<'a> Send for PdfBookmark<'a> {}
277
278#[cfg(feature = "thread_safe")]
279unsafe impl<'a> Sync for PdfBookmark<'a> {}
280
281#[cfg(test)]
282mod tests {
283    use crate::prelude::*;
284    use crate::utils::test::test_bind_to_pdfium;
285    use std::hash::{DefaultHasher, Hash, Hasher};
286
287    #[test]
288    fn test_bookmarks() -> Result<(), PdfiumError> {
289        fn title(bookmark: PdfBookmark) -> String {
290            bookmark.title().expect("Bookmark Title")
291        }
292
293        fn hash(b: &PdfBookmark) -> u64 {
294            let mut s = DefaultHasher::new();
295            b.hash(&mut s);
296            s.finish()
297        }
298
299        let pdfium = test_bind_to_pdfium();
300        let document = pdfium.load_pdf_from_file("./test/test-toc.pdf", None)?;
301
302        // Should be able to find Sections 3 and 4
303        let section3 = document.bookmarks().find_first_by_title("Section 3")?;
304        let section4 = document.bookmarks().find_first_by_title("Section 4")?;
305
306        // Section 3 should have five direct children, sections 3.1 through 3.5,
307        // each of which should show up exactly once.
308        let direct_children: Vec<String> = section3.iter_direct_children().map(title).collect();
309        let expected: Vec<String> = (1..6).map(|i| format!("Section 3.{i}")).collect();
310        assert_eq!(direct_children, expected);
311        assert_eq!(section3.children_len(), 5);
312
313        // Section 4 has no children
314        assert_eq!(section4.iter_direct_children().count(), 0);
315        assert_eq!(section4.children_len(), 0);
316
317        // Section 3 has many descendents
318        let all_children: Vec<String> = section3.iter_all_descendants().map(title).collect();
319        let expected = [
320            "Section 3.1",
321            "Section 3.2",
322            "Section 3.2.1",
323            "Section 3.2.2",
324            "Section 3.2.3",
325            "Section 3.2.4",
326            "Section 3.2.5",
327            "Section 3.2.6",
328            "Section 3.2.7",
329            "Section 3.2.8",
330            "Section 3.2.9",
331            "Section 3.2.10",
332            "Section 3.2.11",
333            "Section 3.2.12",
334            "Section 3.3",
335            "Section 3.3.1",
336            "Section 3.3.2",
337            "Section 3.3.2.1",
338            "Section 3.3.2.2",
339            "Section 3.3.2.3",
340            "Section 3.3.3",
341            "Section 3.3.4",
342            "Section 3.3.5",
343            "Section 3.3.6",
344            "Section 3.4",
345            "Section 3.4.1",
346            "Section 3.4.2",
347            "Section 3.5",
348            "Section 3.5.1",
349            "Section 3.5.2",
350        ];
351        assert_eq!(all_children, expected);
352
353        // Section 4 has no childern
354        assert_eq!(section4.iter_all_descendants().count(), 0);
355
356        // Section 3 has no parents, so when iterating siblings, we expect only
357        // sections 4 and 5.
358        let siblings: Vec<String> = section3.iter_siblings().map(title).collect();
359        assert_eq!(siblings, ["Section 4", "Section 5"]);
360
361        // Find section 3.2.6 in a way that we'll know its parent.
362        let section3_2_6 = section3
363            .iter_all_descendants()
364            .find(|bookmark| bookmark.title() == Some("Section 3.2.6".into()))
365            .expect("§3.2.6");
366        assert!(section3_2_6.parent().is_some());
367        // Then we expect the siblings to be sections 3.2.1 .. 3.2.12, excluding
368        // ourselves.
369        let siblings: Vec<String> = section3_2_6.iter_siblings().map(title).collect();
370        let expected = [
371            "Section 3.2.1",
372            "Section 3.2.2",
373            "Section 3.2.3",
374            "Section 3.2.4",
375            "Section 3.2.5",
376            "Section 3.2.7",
377            "Section 3.2.8",
378            "Section 3.2.9",
379            "Section 3.2.10",
380            "Section 3.2.11",
381            "Section 3.2.12",
382        ];
383        assert_eq!(siblings, expected);
384
385        // Section 5.3.3.1 is an only child.
386        let section5_3_3_1 = document
387            .bookmarks()
388            .find_first_by_title("Section 5.3.3.1")?;
389        assert!(section5_3_3_1.parent().is_none());
390        assert_eq!(section5_3_3_1.iter_siblings().count(), 0);
391        // Even if we know its parent.
392        let section5_3_3_1 = document
393            .bookmarks()
394            .iter()
395            .find(|bookmark| bookmark.title() == Some("Section 5.3.3.1".into()))
396            .expect("§5.3.3.1");
397        assert!(section5_3_3_1.parent().is_some());
398        assert_eq!(section5_3_3_1.iter_siblings().count(), 0);
399
400        // Check that the parent is set right
401        for bookmark in document.bookmarks().iter() {
402            match bookmark.parent() {
403                // If there is no parent, then this should be a top-level
404                // section, which doesn't have a . in its name.
405                None => assert!(!title(bookmark).contains('.')),
406                Some(parent) => {
407                    // If you take this section's title, and lop off the last
408                    // dot and what comes after it, you should have the parent
409                    // section's title.
410                    let this_title = title(bookmark);
411                    let last_dot = this_title.rfind('.').unwrap();
412                    assert_eq!(title(parent), this_title[..last_dot]);
413                }
414            }
415        }
416
417        let all_bookmarks: Vec<_> = document.bookmarks().iter().map(title).collect();
418        let expected = [
419            "Section 1",
420            "Section 2",
421            "Section 3",
422            "Section 3.1",
423            "Section 3.2",
424            "Section 3.2.1",
425            "Section 3.2.2",
426            "Section 3.2.3",
427            "Section 3.2.4",
428            "Section 3.2.5",
429            "Section 3.2.6",
430            "Section 3.2.7",
431            "Section 3.2.8",
432            "Section 3.2.9",
433            "Section 3.2.10",
434            "Section 3.2.11",
435            "Section 3.2.12",
436            "Section 3.3",
437            "Section 3.3.1",
438            "Section 3.3.2",
439            "Section 3.3.2.1",
440            "Section 3.3.2.2",
441            "Section 3.3.2.3",
442            "Section 3.3.3",
443            "Section 3.3.4",
444            "Section 3.3.5",
445            "Section 3.3.6",
446            "Section 3.4",
447            "Section 3.4.1",
448            "Section 3.4.2",
449            "Section 3.5",
450            "Section 3.5.1",
451            "Section 3.5.2",
452            "Section 4",
453            "Section 5",
454            "Section 5.1",
455            "Section 5.2",
456            "Section 5.3",
457            "Section 5.3.1",
458            "Section 5.3.2",
459            "Section 5.3.3",
460            "Section 5.3.3.1",
461            "Section 5.3.4",
462            "Section 5.4",
463            "Section 5.4.1",
464            "Section 5.4.1.1",
465            "Section 5.4.1.2",
466            "Section 5.4.2",
467            "Section 5.4.2.1",
468            "Section 5.4.3",
469            "Section 5.4.4",
470            "Section 5.4.5",
471            "Section 5.4.6",
472            "Section 5.4.7",
473            "Section 5.4.8",
474            "Section 5.4.8.1",
475            "Section 5.5",
476            "Section 5.5.1",
477            "Section 5.5.2",
478            "Section 5.5.3",
479            "Section 5.5.4",
480            "Section 5.5.5",
481            "Section 5.5.5.1",
482            "Section 5.5.5.2",
483            "Section 5.5.5.3",
484            "Section 5.5.6",
485            "Section 5.5.6.1",
486            "Section 5.5.6.2",
487            "Section 5.5.6.3",
488            "Section 5.5.7",
489            "Section 5.5.7.1",
490            "Section 5.5.7.2",
491            "Section 5.5.7.3",
492            "Section 5.5.7.4",
493            "Section 5.5.7.5",
494            "Section 5.5.7.6",
495            "Section 5.5.8",
496            "Section 5.5.8.1",
497            "Section 5.5.8.2",
498            "Section 5.5.8.3",
499            "Section 5.5.8.4",
500            "Section 5.5.8.5",
501            "Section 5.5.8.6",
502            "Section 5.5.8.7",
503            "Section 5.5.8.8",
504            "Section 5.5.8.9",
505            "Section 5.5.9",
506            "Section 5.5.10",
507            "Section 5.6",
508            "Section 5.7",
509            "Section 5.7.1",
510            "Section 5.7.2",
511            "Section 5.7.3",
512            "Section 5.7.3.1",
513            "Section 5.7.3.2",
514            "Section 5.7.3.3",
515            "Section 5.7.3.4",
516            "Section 5.7.4",
517            "Section 5.7.4.1",
518            "Section 5.7.4.2",
519            "Section 5.7.4.3",
520        ];
521        assert_eq!(all_bookmarks, expected);
522
523        // Test that each bookmark is equal it itself but no two bookmarks are equal to each other.
524        let all_bookmarks: Vec<_> = document.bookmarks().iter().collect();
525        for i in 0..all_bookmarks.len() {
526            assert!(all_bookmarks[i] == all_bookmarks[i]);
527            assert_eq!(hash(&all_bookmarks[i]), hash(&all_bookmarks[i]));
528            for j in (i + 1)..all_bookmarks.len() {
529                assert!(all_bookmarks[i] != all_bookmarks[j]);
530            }
531        }
532
533        // Get all the bookmarks again, and test that the second iteration returned an equal set of
534        // bookmarks
535        let all_bookmarks2: Vec<_> = document.bookmarks().iter().collect();
536        assert!(all_bookmarks == all_bookmarks2);
537
538        // Bookmarks should be equal to their clone
539        let the_clone = all_bookmarks[0].clone();
540        assert!(all_bookmarks[0] == the_clone);
541        assert_eq!(hash(&all_bookmarks[0]), hash(&the_clone));
542
543        // Load the document a second time, and assert that the bookmarks are different.
544        let document2 = pdfium.load_pdf_from_file("./test/test-toc.pdf", None)?;
545        let all_bookmarks2: Vec<_> = document2.bookmarks().iter().collect();
546        assert_eq!(all_bookmarks.len(), all_bookmarks2.len());
547        for i in 0..all_bookmarks.len() {
548            for j in 0..all_bookmarks.len() {
549                assert!(all_bookmarks[i] != all_bookmarks2[j]);
550            }
551        }
552
553        Ok(())
554    }
555}