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-beta.18"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(())
}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")?;Remove one page, a range of pages, or a custom set of page indices from a PDF. Bookmarks and named destinations are updated automatically.
use pdfluent::PdfDocument;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut doc = PdfDocument::open("report.pdf")?;
// Remove page 5 (zero-based index 4)
doc.delete_page(4)?;
doc.save("report_trimmed.pdf")?;
println!("Remaining pages: {}", doc.page_count());
Ok(())
}Load the document you want to modify.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("draft.pdf")?;
println!("Before: {} pages", doc.page_count());Call delete_page() with the zero-based index of the page to remove. After deletion, indices of subsequent pages shift down by one.
// Remove the last page
let last = doc.page_count() - 1;
doc.delete_page(last)?;
// Remove the first page
doc.delete_page(0)?;delete_page_range(start..end) removes all pages from start (inclusive) to end (exclusive). This is faster than looping with delete_page().
// Remove pages 3 through 6 (indices 2..6, exclusive end)
doc.delete_page_range(2..6)?;
println!("After range delete: {} pages", doc.page_count());Pass a Vec or slice of indices to delete_pages(). Provide indices in any order; PDFluent sorts and removes them correctly.
// Remove pages at indices 1, 4, and 7
doc.delete_pages(&[1, 4, 7])?;
doc.save("draft_cleaned.pdf")?;
println!("Final page count: {}", doc.page_count());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());Rearrange, reverse, or interleave pages in a PDF document using a page index map.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("input.pdf")?;
// Move the last page to the front
let count = doc.page_count();
let mut order: Vec<usize> = (0..count).collect();
order.rotate_right(1);
doc.reorder_pages(&order)?;
doc.save("reordered.pdf")?;
Ok(())
}Load the document and check the page count so you can build the index map.
let mut doc = PdfDocument::open("input.pdf")?;
let count = doc.page_count();Create a Vec<usize> where each element is the zero-based index of the original page you want at that position. The vec must contain every index exactly once.
// Reverse all pages
let order: Vec<usize> = (0..count).rev().collect();Call reorder_pages with the index slice. PDFluent rewrites the /Pages tree; no content streams are copied or duplicated.
doc.reorder_pages(&order)?;Remove a page index from its current position and insert it at the target position.
let mut order: Vec<usize> = (0..count).collect();
// Move page at index 4 to position 1
let page = order.remove(4);
order.insert(1, page);
doc.reorder_pages(&order)?;Write the reordered document to a file.
doc.save("reordered.pdf")?;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 0..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")?;Get a mutable reference to the page and call set_rotation(). The Rotation enum has variants for 0, 90, 180, and 270 degrees.
use pdfluent::Rotation;
// Rotate page 3 (index 2) 90 degrees clockwise
doc.rotate_page(2, Rotation::Clockwise90)?;
// Rotate page 4 upside-down
doc.rotate_page(3, Rotation::Clockwise180)?;Loop over a range of page indices to rotate a contiguous section.
// Rotate pages 5 through 8 (indices 4-7)
for i in 4..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 0..count {
doc.rotate_page(i, Rotation::Clockwise90)?;
}
doc.save("document_rotated.pdf")?;
println!("Rotated {} pages", count);Set the CropBox on any page to define the visible area without removing content from the file.
use pdfluent::{PdfDocument, Rect};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("input.pdf")?;
// Crop the first page to a 400x500 pt region starting at (50, 100)
let crop_box = Rect::new(50.0, 100.0, 450.0, 600.0);
doc.page_mut(0)?.set_crop_box(crop_box);
doc.save("cropped.pdf")?;
Ok(())
}Load the file into a mutable Document. The document must be opened with write access so you can modify page boxes.
let mut doc = PdfDocument::open("input.pdf")?;PDF coordinates start at the bottom-left corner of the page. A Rect is defined as (x_min, y_min, x_max, y_max) in points (1 pt = 1/72 inch). A standard A4 page is 595 x 842 pt.
// A4 page: bottom-left (0,0), top-right (595, 842)
// This rect keeps a 20 pt margin on all sides:
let rect = Rect::new(20.0, 20.0, 575.0, 822.0);The CropBox controls what viewers display. Content outside the CropBox is hidden but not deleted. You can remove the CropBox later to restore the full page.
let crop_box = Rect::new(50.0, 100.0, 450.0, 600.0);
doc.page_mut(0)?.set_crop_box(crop_box);Iterate over all pages and set the same CropBox, or compute a per-page crop based on the MediaBox dimensions.
let page_count = doc.page_count();
for i in 0..page_count {
let page = doc.page_mut(i)?;
let media = page.media_box();
// Remove a 30 pt border on all sides
let crop = Rect::new(
media.x_min + 30.0,
media.y_min + 30.0,
media.x_max - 30.0,
media.y_max - 30.0,
);
page.set_crop_box(crop);
}Write the result to a new file. The original content is preserved inside the file; only the CropBox annotation changes.
doc.save("cropped.pdf")?;Rescale or replace the MediaBox on each page to change the physical dimensions of a PDF.
use pdfluent::{PdfDocument, PageSize, Rect};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("input.pdf")?;
let page_count = doc.page_count();
for i in 0..page_count {
doc.page_mut(i)?.resize(PageSize::A4, true)?;
}
doc.save("resized.pdf")?;
Ok(())
}Load the PDF into a mutable Document handle.
let mut doc = PdfDocument::open("input.pdf")?;PDFluent provides a PageSize enum with standard ISO and North American sizes. You can also supply a custom Rect in points.
use pdfluent::PageSize;
// Standard sizes
let a4 = PageSize::A4; // 595 x 842 pt
let letter = PageSize::Letter; // 612 x 792 pt
let a3 = PageSize::A3; // 842 x 1191 pt
// Custom size: 4 x 6 inches
let custom = PageSize::Custom(Rect::new(0.0, 0.0, 288.0, 432.0));The second parameter controls whether the content stream is scaled proportionally to fill the new MediaBox. Pass false to keep content at original scale (it may clip or leave whitespace).
doc.page_mut(0)?.resize(PageSize::A4, true)?;Iterate over every page index and apply the same resize operation.
let count = doc.page_count();
for i in 0..count {
doc.page_mut(i)?.resize(PageSize::Letter, true)?;
}Write the resized document to disk.
doc.save("resized.pdf")?;Tile 2, 4, 6, or 9 source pages onto a single output sheet, useful for printing booklets or handouts.
use pdfluent::{PdfDocument, nup::{NUpLayout, NUpOptions}};
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("input.pdf")?;
// 2-up: two A4 source pages side by side on one A3 sheet
let opts = NUpOptions::new(NUpLayout::TwoUp);
let output = doc.nup(opts)?;
output.save("2up.pdf")?;
Ok(())
}The source pages are scaled and positioned into the output sheet. The original file is not modified.
let doc = PdfDocument::open("input.pdf")?;NUpLayout provides common configurations. Each layout specifies the grid dimensions and the output sheet size.
use pdfluent::nup::NUpLayout;
let layout = NUpLayout::TwoUp; // 1x2 grid, landscape A4
// NUpLayout::FourUp // 2x2 grid, A4
// NUpLayout::SixUp // 2x3 grid, A4
// NUpLayout::NineUp // 3x3 grid, A4
// NUpLayout::Custom { cols: 3, rows: 2, sheet: PageSize::A3 }NUpOptions lets you set the gap between cells and the outer margin in points.
use pdfluent::nup::NUpOptions;
let opts = NUpOptions::new(NUpLayout::FourUp)
.gap(8.0) // 8 pt gap between cells
.margin(20.0); // 20 pt outer marginCall doc.nup(opts) to produce a new Document where each output page contains the specified number of source pages.
let output = doc.nup(opts)?;Write the output file. The page count of the output is ceil(source_pages / n).
println!(
"Input pages: {}, Output pages: {}",
doc.page_count(),
output.page_count(),
);
output.save("nup_output.pdf")?;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(())
}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 0..doc.page_count() {
let (w, h) = doc.page(i)?.dimensions();
println!("page {}: {} x {} pt", i + 1, 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),
);