Add and read annotations, links, and bookmarks

This guide shows developers how to use PDFluent to manipulate PDF structure and markup. It is for Rust developers working with PDF documents.

Add PDFluent to your project

Add the pdfluent crate to Cargo.toml.

toml
[dependencies]
pdfluent = "1.0.0-beta.18"

Add highlight, comment, and stamp annotations in Rust

Add sticky notes, text highlights, underlines, and rubber-stamp annotations to PDF pages. All annotation types conform to the PDF 1.7 spec.

rust
use pdfluent::{PdfDocument, Annotation, Color, Rect};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut doc = PdfDocument::open("report.pdf")?;
    let page = doc.page_mut(0)?;

    page.add_annotation(
        Annotation::highlight(Rect::new(72.0, 680.0, 350.0, 695.0))
            .color(Color::yellow())
            .author("Jasper")
            .contents("Check this figure"),
    )?;

    doc.save("report_annotated.pdf")?;
    Ok(())
}
  1. Open the document and get a mutable page

    Open the PDF and borrow a mutable reference to the page you want to annotate. Page indices are zero-based.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("draft.pdf")?;
    let page = doc.page_mut(0)?; // first page
  2. Add a sticky note (text) annotation

    A text annotation appears as a small icon on the page. Viewers show the contents as a pop-up note.

    rust
    use pdfluent::{Annotation, Color, Point};
    
    page.add_annotation(
        Annotation::text(Point::new(100.0, 750.0))
            .contents("Needs legal review before publishing.")
            .author("Jasper")
            .color(Color::rgb(1.0, 0.8, 0.0)), // yellow icon
    )?;
  3. Add a highlight annotation

    Highlight annotations draw a translucent colour over a text region. Supply the bounding rectangle in PDF points (72 points per inch, origin at bottom-left).

    rust
    use pdfluent::{Annotation, Color, Rect};
    
    page.add_annotation(
        Annotation::highlight(Rect::new(72.0, 640.0, 400.0, 655.0))
            .color(Color::rgb(1.0, 1.0, 0.0))
            .contents("Revenue figure confirmed"),
    )?;
  4. Add a stamp annotation and save

    Stamp annotations display predefined labels such as "DRAFT", "APPROVED", or "CONFIDENTIAL" over the page.

    rust
    use pdfluent::{Annotation, Rect, StampStyle};
    
    page.add_annotation(
        Annotation::stamp(Rect::new(400.0, 700.0, 550.0, 740.0))
            .style(StampStyle::Approved)
            .author("Manager"),
    )?;
    
    doc.save("draft_annotated.pdf")?;
  • PDF coordinates use points (1/72 inch). Page height on A4 at 72 DPI is 841.9 points. Y=0 is the bottom of the page.
  • Annotation colours are set per annotation. The Color struct accepts RGB values in the 0.0 to 1.0 range.
  • Some PDF viewers display annotation author names in the review pane. Use .author() to set a meaningful value.
  • To add multiple annotations on the same page, call add_annotation() multiple times before saving.

Read and parse annotations from a PDF in Rust

Iterate over every annotation on every page. Read annotation type, author, contents, bounding box, colour, and creation date.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("reviewed.pdf")?;
    for page in 0..doc.page_count() {
        for a in doc.annotations(page)? {
            println!("{}: {:?}", a.subtype, a.contents);
        }
    }
    Ok(())
}
  1. Open the PDF

    Open the file as a read-only document. You do not need a mutable reference just to read annotations.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("reviewed_contract.pdf")?;
  2. Iterate over pages and annotations

    Call annotations() on each page to get a slice of Annotation objects. Each object exposes its type, position, and metadata.

    rust
    for page_idx in 0..doc.page_count() {
        let annotations = doc.annotations(page_idx)?;
        println!("Page {} has {} annotations", page_idx + 1, annotations.len());
    
        for ann in &annotations {
            println!("  Type: {}", ann.subtype);
            println!("  Rect: {:?}", ann.rect);
            println!("  Contents: {:?}", ann.contents);
        }
    }
  3. Filter by annotation type

    Use annotation_type() to select only the types you care about. AnnotationType is an enum with variants for each standard PDF annotation type.

    rust
    for page_idx in 0..doc.page_count() {
        for ann in doc.annotations(page_idx)? {
            if ann.subtype == "Highlight" {
                println!("Highlight at {:?}: {}", ann.rect, ann.contents.unwrap_or_default());
            }
        }
    }
  4. Export annotations to JSON

    Collect all annotations into a serialisable struct. This is useful for syncing review comments to an external system.

    rust
    // AnnotationInfo exposes: subtype, rect (Option<[f64;4]>), contents (Option<String>)
    let mut records = Vec::new();
    for page_idx in 0..doc.page_count() {
        for ann in doc.annotations(page_idx)? {
            records.push(format!(
                "{{\"page\":{},\"type\":\"{}\",\"contents\":{:?}}}",
                page_idx + 1, ann.subtype, ann.contents.unwrap_or_default()
            ));
        }
    }
    println!("[{}]", records.join(","));
  • annotations() returns an empty slice on pages with no annotations. No error is raised.
  • FreeText annotations may have content rendered on the page. The text is also available via contents().
  • Ink annotations have a list of point paths instead of a single rect. Use ann.ink_paths() to read them.
  • Reply annotations (threaded comments) expose reply_to() which returns the ID of the parent annotation.

Add bookmarks (outline) to a PDF in Rust

Build a nested bookmark outline and attach it to any PDF. Bookmarks appear in the navigation panel of PDF viewers and make long documents easier to navigate.

rust
use pdfluent::PdfDocument;
use pdfluent::structure::Outline;

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("report.pdf")?;
    doc.set_outlines(&[
        Outline::new("Introduction", 0),
        Outline::new("Results", 4),
    ])?;
    doc.save("bookmarked.pdf")?;
    Ok(())
}
  1. Open the PDF

    Load the PDF you want to add bookmarks to. The document must already exist; bookmarks reference page indices in the file.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("manual.pdf")?;
    println!("Pages: {}", doc.page_count());
  2. Build the outline structure

    Create an Outline and add OutlineItem entries. Each item needs a title and a target page index (zero-based). Nest children with .child().

    rust
    use pdfluent::structure::Outline;
    
    let mut part1 = Outline::new("Part I: Basics", 2);
    part1.children.push(Outline::new("Chapter 1", 2));
    part1.children.push(Outline::new("Chapter 2", 6));
    
    let mut part2 = Outline::new("Part II: Advanced", 10);
    part2.children.push(Outline::new("Chapter 3", 10));
    part2.children.push(Outline::new("Chapter 4", 14));
    
    let outline = vec![
        Outline::new("Cover", 0),
        Outline::new("Table of Contents", 1),
        part1,
        part2,
        Outline::new("Index", 18),
    ];
  3. Set the outline on the document

    Call set_outline() to replace any existing bookmark tree with the new one. The previous outline is discarded.

    rust
    doc.set_outlines(&outline)?;
  4. Save the document

    Write the PDF with the new bookmark tree to disk.

    rust
    doc.save("manual_with_bookmarks.pdf")?;
    println!("Bookmarks written.");
  • Page indices in OutlineItem are zero-based. Page 1 in the viewer is index 0.
  • Nesting depth is unlimited, but most PDF viewers display up to 3-4 levels in the outline panel.
  • set_outline() replaces the entire outline. To append to an existing outline, read it first with doc.outline(), modify the tree, then set it back.
  • OutlineItem also accepts a named destination with .destination("dest_name") instead of a page index.

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() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("report.pdf")?;
    for o in doc.outlines()? {
        println!("{} -> page {:?}", o.title, o.page);
    }
    Ok(())
}
  1. Open the PDF

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

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("ebook.pdf")?;
  2. Access the outline

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

    rust
    let outlines = doc.outlines()?;
    
    if outlines.is_empty() {
        println!("No bookmarks found in this PDF");
    } else {
        println!("Document has {} top-level bookmarks", outlines.len());
    }
  3. 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
    use pdfluent::structure::Outline;
    
    fn walk(items: &[Outline], depth: usize) {
        for item in items {
            let page = item.page.map(|p| p + 1).unwrap_or(0);
            println!("{}{} (page {})", "  ".repeat(depth), item.title, page);
            walk(&item.children, depth + 1);
        }
    }
    
    walk(&doc.outlines()?, 0);
  4. 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
    use pdfluent::structure::Outline;
    
    #[derive(Debug)]
    struct BookmarkEntry {
        title: String,
        page: usize,
        depth: usize,
    }
    
    fn flatten(items: &[Outline], depth: usize, out: &mut Vec<BookmarkEntry>) {
        for item in items {
            out.push(BookmarkEntry {
                title: item.title.clone(),
                page: item.page.unwrap_or(0),
                depth,
            });
            flatten(&item.children, depth + 1, out);
        }
    }
    
    let mut entries = Vec::new();
    flatten(&doc.outlines()?, 0, &mut entries);
  • 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.