use crate::Result;
use typed_builder::TypedBuilder;
#[derive(Debug, Clone, TypedBuilder)]
pub struct Bookmark {
#[builder(setter(into))]
pub title: String,
pub page: u32,
#[builder(default)]
pub y_position: Option<f64>,
#[builder(default = -1)]
pub parent_index: i32,
#[builder(default = true)]
pub open: bool,
#[builder(default = 0)]
pub level: u32,
}
impl Bookmark {
pub fn new(title: impl Into<String>, page: u32) -> Self {
Self {
title: title.into(),
page,
y_position: None,
parent_index: -1,
open: true,
level: 0,
}
}
pub fn child(title: impl Into<String>, page: u32, parent_index: i32) -> Self {
Self {
title: title.into(),
page,
y_position: None,
parent_index,
open: true,
level: 1,
}
}
}
#[derive(Debug, Clone)]
pub struct HeadingInfo {
pub level: u8,
pub text: String,
pub page: u32,
pub y_position: f64,
}
#[must_use]
pub fn build_bookmark_tree(headings: &[HeadingInfo]) -> Vec<Bookmark> {
if headings.is_empty() {
return Vec::new();
}
let mut bookmarks = Vec::with_capacity(headings.len());
let mut parent_stack: Vec<(u8, i32)> = Vec::new();
for heading in headings {
while let Some(&(parent_level, _)) = parent_stack.last() {
if parent_level >= heading.level {
parent_stack.pop();
} else {
break;
}
}
let parent_index = parent_stack.last().map_or(-1, |&(_, idx)| idx);
let bookmark = Bookmark {
title: heading.text.clone(),
page: heading.page,
y_position: Some(heading.y_position),
parent_index,
open: heading.level <= 2, level: u32::from(heading.level - 1),
};
let current_index = i32::try_from(bookmarks.len()).unwrap_or(i32::MAX);
bookmarks.push(bookmark);
parent_stack.push((heading.level, current_index));
}
bookmarks
}
pub fn add_bookmarks(pdf_data: &[u8], bookmarks: &[Bookmark]) -> Result<Vec<u8>> {
if bookmarks.is_empty() {
return Ok(pdf_data.to_vec());
}
let bookmark_defs: Vec<printwell_sys::BookmarkDef> = bookmarks
.iter()
.map(|b| printwell_sys::BookmarkDef {
title: b.title.clone(),
page: i32::try_from(b.page).unwrap_or(i32::MAX),
y_position: b.y_position.unwrap_or(-1.0),
parent_index: b.parent_index,
open: b.open,
})
.collect();
let result = printwell_sys::ffi::pdf_add_bookmarks(pdf_data, &bookmark_defs)
.map_err(|e| crate::BookmarkError::Operation(format!("Failed to add bookmarks: {e}")))?;
Ok(result)
}
pub fn extract_bookmarks(pdf_data: &[u8]) -> Result<Vec<Bookmark>> {
let bookmark_defs = printwell_sys::ffi::pdf_get_bookmarks(pdf_data).map_err(|e| {
crate::BookmarkError::Operation(format!("Failed to extract bookmarks: {e}"))
})?;
let bookmarks = bookmark_defs
.iter()
.map(|b| Bookmark {
title: b.title.clone(),
page: u32::try_from(b.page).unwrap_or(0),
y_position: if b.y_position >= 0.0 {
Some(b.y_position)
} else {
None
},
parent_index: b.parent_index,
open: b.open,
level: 0, })
.collect();
Ok(bookmarks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_bookmark_tree() {
let headings = vec![
HeadingInfo {
level: 1,
text: "Chapter 1".into(),
page: 1,
y_position: 700.0,
},
HeadingInfo {
level: 2,
text: "Section 1.1".into(),
page: 2,
y_position: 750.0,
},
HeadingInfo {
level: 2,
text: "Section 1.2".into(),
page: 3,
y_position: 750.0,
},
HeadingInfo {
level: 1,
text: "Chapter 2".into(),
page: 4,
y_position: 700.0,
},
HeadingInfo {
level: 2,
text: "Section 2.1".into(),
page: 5,
y_position: 750.0,
},
HeadingInfo {
level: 3,
text: "Subsection 2.1.1".into(),
page: 5,
y_position: 600.0,
},
];
let bookmarks = build_bookmark_tree(&headings);
assert_eq!(bookmarks.len(), 6);
assert_eq!(bookmarks[0].parent_index, -1); assert_eq!(bookmarks[1].parent_index, 0); assert_eq!(bookmarks[2].parent_index, 0); assert_eq!(bookmarks[3].parent_index, -1); assert_eq!(bookmarks[4].parent_index, 3); assert_eq!(bookmarks[5].parent_index, 4); }
}