This guide shows developers how to use PDFluent to manipulate PDF structure and markup. It is for Rust developers working with PDF documents.
Add the pdfluent crate to Cargo.toml.
[dependencies]
pdfluent = "1.0.0-beta.18"Add sticky notes, text highlights, underlines, and rubber-stamp annotations to PDF pages. All annotation types conform to the PDF 1.7 spec.
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(())
}Open the PDF and borrow a mutable reference to the page you want to annotate. Page indices are zero-based.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("draft.pdf")?;
let page = doc.page_mut(0)?; // first pageA text annotation appears as a small icon on the page. Viewers show the contents as a pop-up note.
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
)?;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).
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"),
)?;Stamp annotations display predefined labels such as "DRAFT", "APPROVED", or "CONFIDENTIAL" over the page.
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")?;Iterate over every annotation on every page. Read annotation type, author, contents, bounding box, colour, and creation date.
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(())
}Open the file as a read-only document. You do not need a mutable reference just to read annotations.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("reviewed_contract.pdf")?;Call annotations() on each page to get a slice of Annotation objects. Each object exposes its type, position, and metadata.
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);
}
}Use annotation_type() to select only the types you care about. AnnotationType is an enum with variants for each standard PDF annotation type.
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());
}
}
}Collect all annotations into a serialisable struct. This is useful for syncing review comments to an external system.
// 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(","));Create clickable link annotations that open URLs or jump to other pages within the same document.
use pdfluent::{PdfDocument, Rect, annotation::{LinkAnnotation, LinkTarget}};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("input.pdf")?;
// Add a URL link on the first page
let rect = Rect::new(72.0, 700.0, 250.0, 720.0);
let link = LinkAnnotation::url(rect, "https://pdfluent.com");
doc.page_mut(0)?.add_annotation(link);
doc.save("with_links.pdf")?;
Ok(())
}Annotations are stored per-page. Open the file with a mutable Document.
let mut doc = PdfDocument::open("input.pdf")?;A link annotation needs a bounding box (the clickable area) in page coordinates (points, origin at bottom-left).
use pdfluent::Rect;
// Clickable area: x1=72, y1=700, x2=250, y2=720
let rect = Rect::new(72.0, 700.0, 250.0, 720.0);LinkAnnotation::url builds a URI action annotation. The URL must be a valid absolute URI.
use pdfluent::annotation::{LinkAnnotation, LinkBorder};
let link = LinkAnnotation::url(rect, "https://example.com")
.border(LinkBorder::none()); // hide the default blue borderUse LinkTarget::Page to link to a specific page number within the same document. Page numbers are zero-based.
use pdfluent::annotation::LinkTarget;
let dest_rect = Rect::new(72.0, 600.0, 300.0, 620.0);
let internal_link = LinkAnnotation::destination(
dest_rect,
LinkTarget::page(4, None), // jump to page 5 (0-indexed = 4)
);Call add_annotation for each link. Multiple annotations can be added to the same page.
let page = doc.page_mut(0)?;
page.add_annotation(link);
page.add_annotation(internal_link);
doc.save("with_links.pdf")?;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.
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(())
}Load the PDF you want to add bookmarks to. The document must already exist; bookmarks reference page indices in the file.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("manual.pdf")?;
println!("Pages: {}", doc.page_count());Create an Outline and add OutlineItem entries. Each item needs a title and a target page index (zero-based). Nest children with .child().
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),
];Call set_outline() to replace any existing bookmark tree with the new one. The previous outline is discarded.
doc.set_outlines(&outline)?;Write the PDF with the new bookmark tree to disk.
doc.save("manual_with_bookmarks.pdf")?;
println!("Bookmarks written.");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() -> pdfluent::Result<()> {
let doc = PdfDocument::open("report.pdf")?;
for o in doc.outlines()? {
println!("{} -> page {:?}", o.title, o.page);
}
Ok(())
}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.
let outlines = doc.outlines()?;
if outlines.is_empty() {
println!("No bookmarks found in this PDF");
} else {
println!("Document has {} top-level bookmarks", outlines.len());
}Each OutlineItem has a title(), page_index(), and a children() slice. Use a recursive function or a stack to walk the full tree.
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);Flatten the nested tree into a Vec for downstream processing such as building a table of contents.
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);