How-to guides/Document Structure

Read the bookmark outline from a PDF in Rust

Extract the complete bookmark tree from a PDF. Read titles, page destinations, nesting depth, and link targets for every outline entry.

rust
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);
    }
}
Install:cargo add pdfluentDownload SDK →

Step by step

1

Add PDFluent to your project

Add the pdfluent crate to Cargo.toml.

rust
[dependencies]
pdfluent = "0.9"
2

Open the PDF

A read-only borrow is enough to access the outline.

rust
use pdfluent::PdfDocument;

let doc = PdfDocument::open("ebook.pdf")?;
3

Access the outline

Call doc.outline() which returns an Option<Outline>. It is None if the PDF has no bookmarks.

rust
match doc.outline() {
    Some(outline) => {
        println!("Document has {} top-level bookmarks", outline.items().len());
    }
    None => {
        println!("No outline found in this PDF");
    }
}
4

Traverse the bookmark tree

Each OutlineItem has a title(), page_index(), and a children() slice. Use a recursive function or a stack to walk the full tree.

rust
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);
}
5

Export the outline to a flat list

Flatten the nested tree into a Vec for downstream processing such as building a table of contents.

rust
#[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());

Notes and tips

  • page_index() returns a zero-based index. Add 1 to convert to the human-readable page number.
  • Some bookmarks point to named destinations rather than a direct page index. Use item.destination() to read those.
  • Bookmarks can also link to external URIs or other documents. Check item.action_type() before assuming a page target.
  • outline() returns None for PDFs without an /Outlines entry in the document catalog.

Why PDFluent for this

Pure Rust

No JVM, no runtime, no DLL dependencies. Ships as a single native binary or WASM module.

Memory safe

Rust's ownership model prevents buffer overflows and use-after-free. No segfaults in PDF parsing.

Runs anywhere

Same code runs server-side, in Docker, on AWS Lambda, on Cloudflare Workers, or in the browser via WASM.

Frequently asked questions