Extract the complete bookmark tree from a PDF. Read titles, page destinations, nesting depth, and link targets for every outline entry.
use pdfluent::PdfDocument;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let doc = PdfDocument::open("manual.pdf")?;
if let Some(outline) = doc.outline() {
print_items(outline.items(), 0);
} else {
println!("No bookmarks found.");
}
Ok(())
}
fn print_items(items: &[pdfluent::OutlineItem], depth: usize) {
for item in items {
println!("{}{} -> page {}", " ".repeat(depth), item.title(), item.page_index().unwrap_or(0) + 1);
print_items(item.children(), depth + 1);
}
}Add the pdfluent crate to Cargo.toml.
[dependencies]
pdfluent = "0.9"A read-only borrow is enough to access the outline.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("ebook.pdf")?;Call doc.outline() which returns an Option<Outline>. It is None if the PDF has no bookmarks.
match doc.outline() {
Some(outline) => {
println!("Document has {} top-level bookmarks", outline.items().len());
}
None => {
println!("No outline found in this PDF");
}
}Each OutlineItem has a title(), page_index(), and a children() slice. Use a recursive function or a stack to walk the full tree.
fn walk(items: &[pdfluent::OutlineItem], depth: usize) {
for item in items {
let page = item.page_index().map(|p| p + 1).unwrap_or(0);
println!("{}{} (page {})", " ".repeat(depth), item.title(), page);
walk(item.children(), depth + 1);
}
}
if let Some(outline) = doc.outline() {
walk(outline.items(), 0);
}Flatten the nested tree into a Vec for downstream processing such as building a table of contents.
#[derive(Debug)]
struct BookmarkEntry {
title: String,
page: usize,
depth: usize,
}
fn flatten(items: &[pdfluent::OutlineItem], depth: usize, out: &mut Vec<BookmarkEntry>) {
for item in items {
out.push(BookmarkEntry {
title: item.title().to_string(),
page: item.page_index().unwrap_or(0) + 1,
depth,
});
flatten(item.children(), depth + 1, out);
}
}
let mut entries = Vec::new();
if let Some(outline) = doc.outline() {
flatten(outline.items(), 0, &mut entries);
}
println!("Total outline entries: {}", entries.len());No JVM, no runtime, no DLL dependencies. Ships as a single native binary or WASM module.
Rust's ownership model prevents buffer overflows and use-after-free. No segfaults in PDF parsing.
Same code runs server-side, in Docker, on AWS Lambda, on Cloudflare Workers, or in the browser via WASM.