A practical guide for Rust developers. Learn how to perform six core PDF operations using the PDFluent SDK.
Creating PDFs from scratch requires only the base crate.
# Cargo.toml
[dependencies]
pdfluent = "1.0.0-beta.18"Build a PDF programmatically. Add pages, text, images, and set document metadata without any source file.
use pdfluent::{PdfDocument, Page, PageSize, TextOptions, Color, Font};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::new();
doc.set_title("Quarterly Report Q1 2024");
doc.set_author("Finance Team");
let mut page = Page::new(PageSize::A4);
page.add_text(
"Quarterly Report",
TextOptions {
x: 50.0,
y: 780.0,
font_size: 28.0,
color: Color::rgb(10, 10, 10),
..TextOptions::default()
},
)?;
page.add_text(
"Q1 2024 — Revenue: EUR 1,240,000",
TextOptions {
x: 50.0,
y: 740.0,
font_size: 14.0,
color: Color::rgb(80, 80, 80),
..TextOptions::default()
},
)?;
doc.add_page(page);
doc.save("quarterly-report.pdf")?;
Ok(())
}PdfDocument::new() creates an empty PDF 1.7 document. Set XMP and DocInfo metadata before adding content.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::new();
doc.set_title("Quarterly Report Q1 2024");
doc.set_author("Finance Team");
doc.set_subject("Financial summary");
doc.set_creator("pdfluent 0.9");Page::new() accepts a PageSize enum or custom dimensions in points. Common sizes: A4 is 595x842 pt, US Letter is 612x792 pt.
use pdfluent::{Page, PageSize};
// Standard A4
let mut page = Page::new(PageSize::A4);
// Custom size: 200 x 100 mm
let mut page = Page::new(PageSize::custom_mm(200.0, 100.0));
println!("Page: {}x{} pt", page.width(), page.height());PDF coordinates start at the bottom-left corner. Use font_size, color, and an optional embedded font to control appearance.
use pdfluent::{TextOptions, Color};
page.add_text(
"Invoice #INV-2024-042",
TextOptions {
x: 50.0,
y: 780.0,
font_size: 22.0,
color: Color::black(),
..TextOptions::default()
},
)?;
page.add_text(
"Due: 2024-05-01",
TextOptions {
x: 50.0,
y: 750.0,
font_size: 11.0,
color: Color::rgb(100, 100, 100),
..TextOptions::default()
},
)?;Use the page drawing API for visual structure. Add a horizontal rule under a header or a bounding box around a table.
use pdfluent::{Image, ImagePosition, Rect, StrokeOptions};
// Horizontal line
page.draw_line(50.0, 720.0, 545.0, 720.0, StrokeOptions {
width: 1.0,
color: Color::rgb(200, 200, 200),
})?;
// Filled rectangle
page.draw_rect(Rect {
x: 50.0, y: 100.0, width: 495.0, height: 40.0,
}, Color::rgb(240, 245, 255), None)?;
// Image
let logo = Image::from_file("logo.png")?;
page.add_image(&logo, ImagePosition {
x: 400.0, y: 780.0, width: 100.0, height: 40.0,
})?;
// Add page to document and save
doc.add_page(page);
doc.save("invoice.pdf")?;PDFluent connects to a headless browser via its browser bridge to render HTML to PDF. This gives you accurate CSS rendering, web fonts, and SVG support.
chrome --headless --disable-gpu --print-to-pdf=out.pdf input.htmlUse Chrome/Chromium's built-in headless mode to convert HTML to PDF. Run this command in your shell.
chrome --headless --disable-gpu --print-to-pdf=out.pdf input.htmlEverything after the conversion is PDFluent: watermark, compress, convert to PDF/A, sign, redact, extract text. This example is compiled in CI from crates/pdfluent/examples/site_snippets.rs, so it builds against the published release.
// Chrome wrote the PDF; everything after that is PDFluent.
let mut doc = PdfDocument::open("out.pdf")?;
doc.add_watermark("DRAFT", WatermarkOptions::centered())?;
let report = doc.compress(CompressOptions::default())?;
println!("{} streams compressed", report.streams_compressed);
// convert_to_pdfa returns a new document rather than changing this one.
let archived = doc.convert_to_pdfa(PdfAProfile::A2b)?;
archived.save("invoice-archived.pdf")?;Shrink a PDF in-memory with CompressOptions. Three presets cover the common cases: strict (default), lossy, and archival.
use pdfluent::{PdfDocument, CompressOptions};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("large.pdf")?;
let report = doc.compress(CompressOptions::archival())?;
println!("{} streams compressed", report.streams_compressed);
doc.save("compressed.pdf")?;
Ok(())
}compress takes &mut self and rewrites the in-memory document. You save afterwards to persist.
use pdfluent::prelude::*;
let mut doc = PdfDocument::open("report.pdf")?;CompressOptions::strict() is the default and enables every pass: font subsetting, stream compression, duplicate-stream deduplication, unused-object removal. CompressOptions::lossy() matches strict() today; it reserves the slot for 1.1 lossy image downsampling. CompressOptions::archival() keeps unused objects (safer for incremental updates and signed appearance streams).
// full stack — recommended default
let opts = CompressOptions::strict();
// reserved for 1.1 lossy passes; today identical to strict
let opts = CompressOptions::lossy();
// keep unused objects — safest for signed / incremental-update docs
let opts = CompressOptions::archival();compress returns a CompressReport with counters for each pass. font_subset is an Option<FontSubsetReport> — None when font subsetting is disabled, Some with per-pass counters otherwise.
let report = doc.compress(CompressOptions::strict())?;
println!("streams compressed: {}", report.streams_compressed);
println!("streams deduplicated: {}", report.streams_deduplicated);
println!("unused removed: {}", report.unused_removed);
if let Some(fs) = &report.font_subset {
println!("fonts subsetted: {} of {}", fs.fonts_subsetted, fs.fonts_processed);
println!("font bytes saved: {}", fs.bytes_saved);
}save_with lets you opt into overwrite. Without with_overwrite(true), the SDK refuses to clobber an existing file (RFC 0001 §1.2). Point it at a new filename to skip the flag.
doc.save_with(
"report_compressed.pdf",
SaveOptions::new().with_overwrite(true),
)?;Use PDFluent's recovery parser to rebuild the cross-reference table and salvage as many objects as possible from a damaged file.
use pdfluent::{PdfDocument, OpenOptions};
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open_with(
"broken.pdf",
OpenOptions::new().with_repair(true),
)?;
println!("recovered {} pages", doc.page_count());
Ok(())
}Standard parsing is faster. Only fall back to recovery mode if the standard open returns an error.
use pdfluent::{PdfDocument, Error};
let result = PdfDocument::open("damaged.pdf");
match result {
Ok(doc) => println!("Opened normally."),
Err(Error::BrokenXref | Error::UnexpectedEof | Error::InvalidStructure(_)) => {
println!("Standard open failed, trying recovery mode...");
}
Err(e) => return Err(e),
}Recovery mode uses a linear scan of the file to find all PDF objects rather than relying on the cross-reference table.
use pdfluent::{PdfDocument, OpenOptions};
let doc = PdfDocument::open_with(
"damaged.pdf",
OpenOptions::new().recovery_mode(true),
)?;The repair report describes what was found and what could not be recovered.
let report = doc.repair_report();
println!("Objects recovered: {}", report.objects_recovered());
println!("Objects missing: {}", report.objects_missing());
println!("Xref rebuilt: {}", report.xref_rebuilt());
println!("Truncated at byte: {:?}", report.truncated_at());Check that the expected pages are present. Some pages may be unrecoverable if their stream data was overwritten.
println!("Pages recovered: {}", doc.page_count());
for (i, page) in doc.pages().enumerate() {
let text = page.text().unwrap_or_default();
println!("Page {}: {} chars", i + 1, text.len());
}Write the repaired document. The output is a structurally valid PDF even if some content was lost.
doc.save("repaired.pdf")?;
println!("Saved repaired.pdf");Attach any file (XML, CSV, XLSX, images) as an embedded file stream inside a PDF. The attachment travels with the document and can be extracted by any conforming viewer.
use pdfluent::{PdfDocument, Attachment};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut doc = PdfDocument::open("invoice.pdf")?;
doc.attach_file(
Attachment::from_file("invoice_data.xml")?
.description("Machine-readable invoice data (ZUGFeRD)")
.mime_type("application/xml"),
)?;
doc.save("invoice_with_attachment.pdf")?;
println!("File attached.");
Ok(())
}Load the document that will receive the attachment.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("invoice.pdf")?;Attachment::from_file() reads the file bytes, determines a filename, and sets a creation date. All fields can be overridden.
use pdfluent::Attachment;
let attachment = Attachment::from_file("supporting_data.csv")?
.description("Raw data used to generate the figures in this report")
.mime_type("text/csv")
.filename("data.csv");If the file content is already in memory, use Attachment::from_bytes() instead.
let xml_bytes = generate_xml_data(); // your function
let attachment = Attachment::from_bytes(xml_bytes)
.filename("invoice.xml")
.mime_type("application/xml")
.description("ZUGFeRD structured invoice data");attach_file() embeds the file in the document-level EmbeddedFiles name tree. Save afterwards.
doc.attach_file(attachment)?;
// Verify
println!("Attached files: {}", doc.attachments().len());
doc.save("invoice_with_attachment.pdf")?;List and extract all embedded file streams from a PDF document. Save attachments to disk or read them directly as byte buffers.
use pdfluent::PdfDocument;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let doc = PdfDocument::open("invoice_with_attachment.pdf")?;
for attachment in doc.attachments() {
let filename = attachment.filename();
let data = attachment.read_data()?;
fs::write(format!("output/{}", filename), &data)?;
println!("Extracted {} ({} bytes)", filename, data.len());
}
Ok(())
}Open the document. A read-only borrow is enough for reading attachments.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("package.pdf")?;Call doc.attachments() to get attachment metadata. No file data is read at this point.
let attachments = doc.attachments();
println!("Found {} attachment(s):", attachments.len());
for att in &attachments {
println!(
" {} - {} - {} bytes",
att.filename(),
att.mime_type().unwrap_or("unknown"),
att.size(),
);
}Find the attachment you want by filename and extract its bytes.
let xml_att = doc
.attachments()
.into_iter()
.find(|a| a.filename().ends_with(".xml"));
if let Some(att) = xml_att {
let data = att.read_data()?;
std::fs::write("extracted_invoice.xml", &data)?;
println!("Extracted: {} bytes", data.len());
} else {
println!("No XML attachment found.");
}Loop over all attachments and save each to a target folder.
use std::fs;
use std::path::Path;
let output_dir = Path::new("extracted_files");
fs::create_dir_all(output_dir)?;
for att in doc.attachments() {
let dest = output_dir.join(att.filename());
let data = att.read_data()?;
fs::write(&dest, &data)?;
println!("Saved: {}", dest.display());
}