use std::collections::HashMap;
use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectStore};
use crate::action::{Action, parse_action};
use crate::destination::{Destination, parse_destination};
use crate::error::DocResult;
#[derive(Debug, Clone)]
pub struct Bookmark {
pub title: String,
pub destination: Option<Destination>,
pub action: Option<Action>,
pub children: Vec<Bookmark>,
pub is_open: bool,
}
impl Bookmark {
pub fn title(&self) -> &str {
&self.title
}
#[inline]
pub fn bookmark_get_title(&self) -> &str {
self.title()
}
#[deprecated(note = "use `bookmark_get_title()` — matches upstream `FPDFBookmark_GetTitle`")]
#[inline]
pub fn get_title(&self) -> &str {
self.title()
}
pub fn first_child(&self) -> Option<&Bookmark> {
self.children.first()
}
#[inline]
pub fn bookmark_get_first_child(&self) -> Option<&Bookmark> {
self.first_child()
}
#[deprecated(
note = "use `bookmark_get_first_child()` — matches upstream `FPDFBookmark_GetFirstChild`"
)]
#[inline]
pub fn get_first_child(&self) -> Option<&Bookmark> {
self.first_child()
}
pub fn count(&self) -> i32 {
let n = self.children.len() as i32;
if self.is_open { n } else { -n }
}
#[inline]
pub fn bookmark_get_count(&self) -> i32 {
self.count()
}
#[deprecated(note = "use `bookmark_get_count()` — matches upstream `FPDFBookmark_GetCount`")]
#[inline]
pub fn get_count(&self) -> i32 {
self.count()
}
pub fn dest(&self) -> Option<&Destination> {
self.destination.as_ref()
}
#[inline]
pub fn bookmark_get_dest(&self) -> Option<&Destination> {
self.dest()
}
#[deprecated(note = "use `bookmark_get_dest()` — matches upstream `FPDFBookmark_GetDest`")]
#[inline]
pub fn get_dest(&self) -> Option<&Destination> {
self.dest()
}
pub fn action(&self) -> Option<&Action> {
self.action.as_ref()
}
#[inline]
pub fn bookmark_get_action(&self) -> Option<&Action> {
self.action()
}
#[deprecated(note = "use `bookmark_get_action()` — matches upstream `FPDFBookmark_GetAction`")]
#[inline]
pub fn get_action(&self) -> Option<&Action> {
self.action()
}
pub fn next_sibling_in<'a>(&self, siblings: &'a [Bookmark]) -> Option<&'a Bookmark> {
siblings
.windows(2)
.find(|w| std::ptr::eq(&w[0] as *const Bookmark, self as *const Bookmark))
.map(|w| &w[1])
}
#[inline]
pub fn bookmark_get_next_sibling<'a>(&self, siblings: &'a [Bookmark]) -> Option<&'a Bookmark> {
self.next_sibling_in(siblings)
}
#[deprecated(
note = "use `bookmark_get_next_sibling()` — matches upstream `FPDFBookmark_GetNextSibling`"
)]
#[inline]
pub fn get_next_sibling_in<'a>(&self, siblings: &'a [Bookmark]) -> Option<&'a Bookmark> {
self.next_sibling_in(siblings)
}
}
pub fn next_sibling_bookmark<'a>(
siblings: &'a [Bookmark],
bookmark: &Bookmark,
) -> Option<&'a Bookmark> {
bookmark.next_sibling_in(siblings)
}
pub fn find_bookmark<'a>(bookmarks: &'a [Bookmark], title: &str) -> Option<&'a Bookmark> {
let mut stack: Vec<&Bookmark> = bookmarks.iter().rev().collect();
while let Some(bm) = stack.pop() {
if bm.title == title {
return Some(bm);
}
for child in bm.children.iter().rev() {
stack.push(child);
}
}
None
}
#[deprecated(note = "use `find_bookmark()`")]
#[inline]
pub fn get_bookmark_by_title<'a>(bookmarks: &'a [Bookmark], title: &str) -> Option<&'a Bookmark> {
find_bookmark(bookmarks, title)
}
pub(crate) fn parse_single_bookmark<S: PdfSource>(
dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> DocResult<Bookmark> {
let title = dict
.get(&Name::title())
.and_then(|obj| store.deep_resolve(obj).ok())
.and_then(|obj| obj.as_string().map(|s| s.to_string_lossy()))
.unwrap_or_default();
let destination = dict
.get(&Name::dest())
.and_then(|obj| parse_destination(obj, store).ok());
let action = dict
.get(&Name::a())
.and_then(|obj| parse_action(obj, store).ok());
let is_open = dict
.get(&Name::count())
.and_then(|obj| obj.as_i64())
.map(|c| c > 0)
.unwrap_or(false);
Ok(Bookmark {
title,
destination,
action,
children: Vec::new(),
is_open,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_bookmark(title: &str, children: Vec<Bookmark>) -> Bookmark {
Bookmark {
title: title.into(),
destination: None,
action: None,
children,
is_open: false,
}
}
#[test]
fn test_find_bookmark_found_at_root() {
let bookmarks = vec![
make_bookmark("Chapter 1", vec![]),
make_bookmark("Chapter 2", vec![]),
];
let result = find_bookmark(&bookmarks, "Chapter 2");
assert!(result.is_some());
assert_eq!(result.unwrap().title, "Chapter 2");
}
#[test]
fn test_find_bookmark_not_found() {
let bookmarks = vec![make_bookmark("Chapter 1", vec![])];
assert!(find_bookmark(&bookmarks, "Missing").is_none());
assert!(find_bookmark(&bookmarks, "chapter 1").is_none());
assert!(find_bookmark(&[], "Chapter 1").is_none());
}
#[test]
fn test_find_bookmark_nested() {
let child = make_bookmark("Section 1.1", vec![]);
let parent = make_bookmark("Chapter 1", vec![child]);
let bookmarks = vec![parent];
let result = find_bookmark(&bookmarks, "Section 1.1");
assert!(result.is_some());
assert_eq!(result.unwrap().title, "Section 1.1");
}
#[test]
fn test_next_sibling_in_basic() {
let bms = vec![
make_bookmark("A", vec![]),
make_bookmark("B", vec![]),
make_bookmark("C", vec![]),
];
let next = bms[0].next_sibling_in(&bms);
assert!(next.is_some());
assert_eq!(next.unwrap().title, "B");
let next = bms[1].next_sibling_in(&bms);
assert!(next.is_some());
assert_eq!(next.unwrap().title, "C");
assert!(bms[2].next_sibling_in(&bms).is_none());
}
#[test]
fn test_next_sibling_in_single() {
let bms = vec![make_bookmark("Only", vec![])];
assert!(bms[0].next_sibling_in(&bms).is_none());
}
#[test]
fn test_next_sibling_in_alias_matches() {
let bms = vec![make_bookmark("X", vec![]), make_bookmark("Y", vec![])];
assert_eq!(
bms[0].next_sibling_in(&bms).map(|b| &b.title),
bms[0].bookmark_get_next_sibling(&bms).map(|b| &b.title),
);
}
#[test]
fn test_next_sibling_bookmark_free_fn() {
let bms = vec![make_bookmark("P", vec![]), make_bookmark("Q", vec![])];
let result = next_sibling_bookmark(&bms, &bms[0]);
assert!(result.is_some());
assert_eq!(result.unwrap().title, "Q");
}
#[test]
fn test_find_bookmark_dfs_order_returns_first_match() {
let ch1_1 = make_bookmark("Deep", vec![]);
let ch1_2 = make_bookmark("Ch1.2", vec![]);
let ch2_1 = make_bookmark("Deep", vec![]);
let ch1 = make_bookmark("Ch1", vec![ch1_1, ch1_2]);
let ch2 = make_bookmark("Ch2", vec![ch2_1]);
let bookmarks = vec![ch1, ch2];
let result = find_bookmark(&bookmarks, "Deep");
assert!(result.is_some());
assert_eq!(result.unwrap().title, "Deep");
let alias_result = find_bookmark(&bookmarks, "Deep");
assert!(alias_result.is_some());
}
}