This guide covers common PDF manipulation tasks using the PDFluent SDK. It is for Rust developers who need to process PDF files programmatically.
Add the pdfluent crate to your Cargo.toml. No system libraries are required.
[dependencies]
pdfluent = "1.0.0"Combine multiple PDF files into one document. PDFluent preserves bookmarks across all input files.
use pdfluent::prelude::*;
fn main() -> Result<()> {
let merged = PdfMerger::new()
.add(PdfDocument::open("part1.pdf")?)
.add(PdfDocument::open("part2.pdf")?)
.add(PdfDocument::open("part3.pdf")?)
.build()?;
merged.save("combined.pdf")?;
println!("Merged {} pages into combined.pdf", merged.page_count());
Ok(())
}Complete program, compiled against the published crate: Code on GitHub →
The same code, typed and run on screen. Watch on YouTube
Open every source PDF with PdfDocument::open. The merger takes full PdfDocument values by move, so consume each input once.
use pdfluent::prelude::*;
let a = PdfDocument::open("invoice_jan.pdf")?;
let b = PdfDocument::open("invoice_feb.pdf")?;
let c = PdfDocument::open("invoice_mar.pdf")?;Create a PdfMerger, chain .add() for each document. Inputs are appended in the order they are added.
let merger = PdfMerger::new()
.add(a)
.add(b)
.add(c);Choose a BookmarkMergeStrategy. Concat (the default) groups each source's bookmarks under a top-level entry; FlattenAll sequences them; Discard drops all bookmarks. In 1.0 Concat has dedicated treatment; FlattenAll and Discard fall back to the underlying concatenation.
let merger = merger
.with_bookmarks(BookmarkMergeStrategy::Concat)
.with_page_labels(true); // 1.0: accepted but currently a no-opCall .build() to produce a merged PdfDocument, then save or serialise it. build() is the terminating step and consumes the merger.
let merged = merger.build()?;
merged.save("annual_report.pdf")?;
println!("Total pages: {}", merged.page_count());Combine multiple PDF files into one and insert a generated table of contents page with clickable bookmark links.
use pdfluent::{PdfDocument, PdfMerger, BookmarkMergeStrategy};
fn main() -> pdfluent::Result<()> {
let merged = PdfMerger::new()
.add(PdfDocument::open("intro.pdf")?)
.add(PdfDocument::open("body.pdf")?)
.with_bookmarks(BookmarkMergeStrategy::Concat)
.with_page_labels(true)
.build()?;
merged.save("merged.pdf")?;
Ok(())
}Each MergeInput specifies a source file and an optional title used for the TOC entry and bookmark.
use pdfluent::{PdfDocument, PdfMerger, BookmarkMergeStrategy};
let merger = PdfMerger::new()
.add(PdfDocument::open("section1.pdf")?)
.add(PdfDocument::open("section2.pdf")?)
.add(PdfDocument::open("appendix.pdf")?);Enable TOC generation and optionally configure the TOC page style, font, and bookmark depth.
// Concatenate each source's bookmarks under a top-level entry (navigation TOC).
// Page labels preserve each section's original numbering.
let merger = merger
.with_bookmarks(BookmarkMergeStrategy::Concat)
.with_page_labels(true);Document::merge_with_options returns a new Document. The TOC page is inserted at the position specified in the options.
let merged = merger.build()?;The merge operation adds bookmark entries that match the TOC. Verify they are present.
for bookmark in merged.outlines()? {
println!("'{}' -> page {:?}", bookmark.title, bookmark.page);
}Write the final merged file.
merged.save("merged_with_toc.pdf")?;Extract one or more page ranges from a PDF and write each range to a separate file. Useful for splitting chapters, invoices, or reports.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("report.pdf")?;
for (i, page) in doc.split_pages()?.into_iter().enumerate() {
page.save(format!("page-{}.pdf", i + 1))?;
}
Ok(())
}Load the PDF you want to split. PDFluent reads the file lazily, so opening a 500-page document uses minimal memory until pages are accessed.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("quarterly_report.pdf")?;
println!("Source has {} pages", doc.page_count());Create PageRange values for each segment. Pages are 1-indexed. Ranges can overlap if you need the same page in multiple output files.
// Ranges to extract (0-based, end-exclusive)
let ranges = [0..5usize, 5..18, 18..22];Pass the ranges to split_by_ranges(). Use {n} in the output pattern for the range index, or provide a Vec of explicit output paths.
for (i, range) in ranges.iter().enumerate() {
doc.extract_pages(range.clone())?.save(format!("segment_{}.pdf", i + 1))?;
}
// Produces: segment_1.pdf, segment_2.pdf, segment_3.pdfWhen you need specific filenames, pass a slice of paths with the same length as the ranges slice.
let names = ["cover.pdf", "body.pdf", "appendix.pdf"];
for (range, name) in ranges.iter().zip(names) {
doc.extract_pages(range.clone())?.save(name)?;
}If you need to serve the split files over HTTP without touching disk, use to_bytes_vec() instead.
for (i, range) in ranges.iter().enumerate() {
let bytes = doc.extract_pages(range.clone())?.to_bytes()?;
println!("Segment {}: {} bytes", i + 1, bytes.len());
}Use the PDF outline to split a document into sections automatically. Each top-level bookmark becomes its own output file.
use pdfluent::PdfDocument;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let doc = PdfDocument::open("manual.pdf")?;
let results = doc
.split_by_top_level_bookmarks()
.write_files("{title}.pdf")?;
for r in &results {
println!("{} -> {} pages", r.filename, r.page_count);
}
Ok(())
}Check that the document has top-level bookmarks before splitting. PDFluent exposes the full outline tree via outline().
use pdfluent::PdfDocument;
let doc = PdfDocument::open("manual.pdf")?;
let outline = doc.outline()?;
println!("Top-level sections: {}", outline.len());
for item in &outline {
println!(" {} -> page {}", item.title, item.destination_page);
}split_by_top_level_bookmarks() computes the page range for each bookmark automatically. The range ends where the next bookmark starts.
let splitter = doc.split_by_top_level_bookmarks();Use {title} in the pattern to name each file after its bookmark. PDFluent sanitises the title to produce a valid filename.
splitter.write_files("{title}.pdf")?;
// "Introduction.pdf", "Chapter 1.pdf", "Chapter 2.pdf", ...To split at second-level bookmarks instead of the top level, set the depth parameter.
use pdfluent::SplitDepth;
doc.split_by_bookmarks(SplitDepth::Level(2))
.write_files("section_{n}.pdf")?;If you need the split data in memory, use to_vec() to get a Vec of SplitSegment values without writing to disk.
let segments = doc
.split_by_top_level_bookmarks()
.to_vec()?;
for seg in segments {
println!("{}: {} bytes", seg.title, seg.data.len());
// upload seg.data to S3, etc.
}Pick individual pages or non-contiguous sets and write them to a new PDF. Works with page numbers, page labels, or a custom predicate.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("report.pdf")?;
let subset = doc.extract_pages(0..3)?;
subset.save("first-three.pdf")?;
Ok(())
}Open the document you want to extract pages from. Page count is available immediately after opening.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("source.pdf")?;
println!("{} pages total", doc.page_count());Pass a slice of 1-based page numbers. Pages appear in the output in the order given, so you can reorder them freely.
// Extract a contiguous page range (0-based, end-exclusive): pages 2-4
let extracted = doc.extract_pages(1..4)?;
extracted.save("pages_2_to_4.pdf")?;If the PDF uses custom page labels such as "i", "ii", "A-1", pass label strings instead of integers.
// Split into single-page documents
for (i, page) in doc.split_pages()?.into_iter().enumerate() {
page.save(format!("page_{}.pdf", i + 1))?;
}Use extract_pages_where() to filter programmatically. The closure receives a PageInfo struct with page number, label, width, height, and rotation.
// Extract a range and read the bytes (e.g. to send over HTTP)
let bytes = doc.extract_pages(0..3)?.to_bytes()?;
println!("Extracted PDF is {} bytes", bytes.len());Call save() to write to disk, or to_bytes() to get the PDF data as a Vec<u8> for streaming or further processing.
// Extract the final two pages by range and save
let n = doc.page_count();
doc.extract_pages(n.saturating_sub(2)..n)?.save("last_pages.pdf")?;Build a new document from the pages you keep. 1.0 removes a page by extracting the runs around it and merging them.
use pdfluent::{PdfDocument, PdfMerger};
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("report.pdf")?;
let total = doc.page_count();
// Keep everything except page 5. Page numbers are 1-based and both ends of
// the range are included -- and a range must not be empty, so the run after
// page 5 is only added when there is one.
let mut merger = PdfMerger::new().add(doc.extract_pages(1..=4)?);
if total > 5 {
merger = merger.add(doc.extract_pages(6..=total)?);
}
let trimmed = merger.build()?;
trimmed.save("report_trimmed.pdf")?;
println!("Remaining pages: {}", trimmed.page_count());
Ok(())
}The count is what the runs you keep are built from, and the source document is never modified.
use pdfluent::{PdfDocument, PdfMerger};
let doc = PdfDocument::open("draft.pdf")?;
let total = doc.page_count();
println!("Before: {} pages", total);Dropping the first or the last page leaves a single run, so no merge is needed. extract_pages returns a new document.
// Without the first page.
let without_first = doc.extract_pages(2..=total)?;
// Without the last page.
let without_last = doc.extract_pages(1..=total - 1)?;
without_last.save("draft_without_last.pdf")?;Extract what is on either side of the run and concatenate the two with PdfMerger. Guard the second range the same way: a run that would start past the last page is an error, not an empty document.
// Drop pages 3 to 6.
let mut merger = PdfMerger::new().add(doc.extract_pages(1..=2)?);
if total > 6 {
merger = merger.add(doc.extract_pages(7..=total)?);
}
let trimmed = merger.build()?;
println!("After the range: {} pages", trimmed.page_count());Walk the page numbers you are dropping in ascending order and add the run that sits before each of them, then whatever is left at the end.
// Drop pages 2, 5 and 8.
let drop = [2usize, 5, 8];
let mut merger = PdfMerger::new();
let mut start = 1usize;
for page in drop {
if page > start {
merger = merger.add(doc.extract_pages(start..=page - 1)?);
}
start = page + 1;
}
if start <= total {
merger = merger.add(doc.extract_pages(start..=total)?);
}
merger.build()?.save("draft_cleaned.pdf")?;Insert a blank page, a page from another PDF, or multiple pages at any position in an existing document.
use pdfluent::{PdfDocument, PageSize};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut doc = PdfDocument::open("report.pdf")?;
// Insert a blank A4 page after page 2 (at index 2)
doc.insert_blank_page(2, PageSize::A4)?;
doc.save("report_with_divider.pdf")?;
println!("Page inserted. New count: {}", doc.page_count());
Ok(())
}Load the document you want to modify.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("annual_report.pdf")?;
println!("Current page count: {}", doc.page_count());insert_blank_page(index, size) inserts a new empty page before the page at that index. Use index = page_count() to append at the end.
use pdfluent::PageSize;
// Insert blank A4 page before the third page (index 2)
doc.insert_blank_page(2, PageSize::A4)?;
// Append a blank Letter page at the end
doc.insert_blank_page(doc.page_count(), PageSize::Letter)?;Open a second document and copy pages from it into the target at a specific position.
let source = PdfDocument::open("cover_page.pdf")?;
// Insert the first page of source before page 0 (prepend)
doc.insert_page_from(0, &source, 0)?;
// Insert pages 1-3 from source after the current last page
for i in 1..=3 {
let pos = doc.page_count();
doc.insert_page_from(pos, &source, i)?;
}Write the result to disk.
doc.save("report_expanded.pdf")?;
println!("New page count: {}", doc.page_count());Assemble a document in the page order you want by merging one-page extracts. Reverse, interleave, or move a single page.
use pdfluent::{PdfDocument, PdfMerger};
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("input.pdf")?;
let count = doc.page_count();
// The page numbers, in the order you want them. Move the last to the front.
let mut order: Vec<usize> = (1..=count).collect();
order.rotate_right(1);
let mut merger = PdfMerger::new();
for page in &order {
merger = merger.add(doc.extract_pages(*page..=*page)?);
}
merger.build()?.save("reordered.pdf")?;
Ok(())
}Load the document and read the page count; the order you build is a permutation of those page numbers.
use pdfluent::{PdfDocument, PdfMerger};
let doc = PdfDocument::open("input.pdf")?;
let count = doc.page_count();1-based page numbers in the order you want to read them. Every page you want in the result has to appear in it.
// Reverse the document.
let order = (1..=count).rev().collect::<Vec<usize>>();Extract each page on its own and concatenate them. This is what 1.0 offers; there is no call that rewrites the page tree in place. The source file is untouched.
use pdfluent::PdfMerger;
let mut merger = PdfMerger::new();
for page in &order {
merger = merger.add(doc.extract_pages(*page..=*page)?);
}
merger.build()?.save("reordered.pdf")?;Change the order vector rather than the document: take the page number out and put it back where you want it.
let mut order = (1..=count).collect::<Vec<usize>>();
// Move page 5 to second position.
let page = order.remove(4);
order.insert(1, page);Set page rotation to 90, 180, or 270 degrees. Rotate a single page, a range, or the whole document in a few lines of Rust.
use pdfluent::{PdfDocument, Rotation};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("scan.pdf")?;
for page in 1..=doc.page_count() {
doc.rotate_page(page, Rotation::Clockwise90)?;
}
doc.save("rotated.pdf")?;
Ok(())
}Load the file you want to rotate.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("scanned_contract.pdf")?;Call doc.rotate_page(page, rotation) with a 1-based page number. The Rotation enum has Clockwise90, Clockwise180 and Clockwise270; there is no variant for 0, because rotating by nothing is not an operation.
use pdfluent::Rotation;
// Rotate page 3 90 degrees clockwise. Pages are 1-based.
doc.rotate_page(3, Rotation::Clockwise90)?;
// Rotate page 4 upside-down
doc.rotate_page(4, Rotation::Clockwise180)?;Loop over a range of page indices to rotate a contiguous section.
// Rotate pages 5 through 8
for i in 5..=8 {
doc.rotate_page(i, Rotation::Clockwise270)?;
}Iterate over all pages and apply the same rotation, then write the output.
let count = doc.page_count();
for i in 1..=count {
doc.rotate_page(i, Rotation::Clockwise90)?;
}
doc.save("document_rotated.pdf")?;
println!("Rotated {} pages", count);Read the total number of pages from a PDF file. PDFluent reads the page count from the document catalog without parsing every page's content stream.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("file.pdf")?;
println!("{} pages", doc.page_count());
Ok(())
}Complete program, compiled against the published crate: Code on GitHub →
The same code, typed and run on screen. Watch on YouTube
page_count() reads the /Count entry in the PDF page tree. It doesn't parse page content streams; for a well-formed PDF the call is effectively O(1).
use pdfluent::prelude::*;
let doc = PdfDocument::open("invoice.pdf")?;
let count = doc.page_count();
println!("This PDF has {} page(s)", count);If the PDF is already in memory (from a network response or database blob), use from_bytes() instead of open().
let pdf_bytes: Vec<u8> = fetch_pdf_from_database()?;
let doc = PdfDocument::from_bytes(&pdf_bytes)?;
println!("Pages: {}", doc.page_count());Some malformed PDFs have an incorrect /Count entry. If you need a physically-verified count, iterate doc.pages() and count as you go — each iteration step resolves the next page from the tree.
let doc = PdfDocument::open("maybe_corrupt.pdf")?;
let physical_count = doc.pages().count();
let declared_count = doc.page_count();
assert_eq!(physical_count, declared_count, "declared /Count != physical pages");Loop over files and collect counts. PdfDocument opens in-memory, so drop each doc between iterations to keep allocations bounded.
use pdfluent::prelude::*;
use std::fs;
for entry in fs::read_dir("./invoices")? {
let path = entry?.path();
if path.extension().map(|e| e == "pdf").unwrap_or(false) {
match PdfDocument::open(&path) {
Ok(doc) => println!("{}: {} pages", path.display(), doc.page_count()),
Err(e) => eprintln!("{}: error - {}", path.display(), e),
}
}
}Read the MediaBox, CropBox, and BleedBox from any PDF page. Convert between points and millimetres or inches for printing and layout workflows.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("file.pdf")?;
for i in 1..=doc.page_count() {
let (w, h) = doc.page(i)?.dimensions();
println!("page {}: {} x {} pt", i, w, h);
}
Ok(())
}Open the document and iterate or index pages directly.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("blueprint.pdf")?;
let page = doc.page(1)?; // first pageThe MediaBox defines the full physical extent of the page in PDF user units (points). 1 point = 1/72 inch.
let (width, height) = page.dimensions();
println!("Width: {:.2} pt", width);
println!("Height: {:.2} pt", height);CropBox is the visible page area. BleedBox is for print bleed. Both fall back to the MediaBox if not set.
// PDFluent exposes the effective page size via dimensions()
let (width, height) = page.dimensions();
println!("Page: {:.1} x {:.1} pt", width, height);PDF points convert to mm with factor 25.4/72 and to inches with factor 1/72.
fn pt_to_mm(pt: f64) -> f64 { pt * 25.4 / 72.0 }
fn pt_to_inch(pt: f64) -> f64 { pt / 72.0 }
let (w, h) = page.dimensions();
println!(
"Page size: {:.1} x {:.1} mm ({:.3} x {:.3} in)",
pt_to_mm(w), pt_to_mm(h), pt_to_inch(w), pt_to_inch(h),
);