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