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.1.0"There is no browser code in the SDK. You run a headless browser yourself, for example `chrome --headless --print-to-pdf`, then open the result with `PdfDocument::open()`.
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(())
}Complete program, compiled against the published crate: Code on GitHub →
The same code, typed and run on screen. Watch on YouTube
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),
)?;Open a damaged file -- the cross-reference table is rebuilt without asking -- and read what had to be repaired out of the diagnostics.
use pdfluent::{LeniencyReport, PdfDocument};
fn main() -> pdfluent::Result<()> {
// Recovery is unconditional: a damaged cross-reference table or page tree
// is rebuilt during the open, without asking for it.
let doc = PdfDocument::open("broken.pdf")?;
println!("recovered {} pages", doc.page_count());
// What it had to repair is in the diagnostics buffer.
let report = LeniencyReport::from_diagnostics(&doc.diagnostics());
println!("{} repair or decode event(s)", report.events.len());
println!("{} warning(s), {} critical", report.warning_count, report.critical_count);
// Get the content out here, where the recovered document is. Saving writes
// a copy through a second parser and is not itself the repair.
for page in doc.pages() {
println!("page {}: {} chars", page.number(), page.text()?.len());
}
Ok(())
}There is no recovery switch to turn on. The parser falls back to scanning the file for objects whenever the cross-reference table or the page tree does not hold up, for every document.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("damaged.pdf")?;
println!("{} pages", doc.page_count());When recovery finds nothing usable the open fails with Error::InvalidPdf, carrying the byte offset where parsing gave up when that is known.
use pdfluent::{Error, PdfDocument};
match PdfDocument::open("damaged.pdf") {
Ok(doc) => println!("opened: {} pages", doc.page_count()),
Err(Error::InvalidPdf { byte_offset, reason, .. }) => {
println!("unrecoverable at {byte_offset:?}: {reason}");
}
Err(Error::FileNotFound { path, .. }) => {
println!("no such file: {}", path.display());
}
Err(e) => return Err(e),
}LeniencyReport::from_diagnostics keeps the repair and decode events out of the diagnostics buffer and counts them. Every event carries a stable code you can match on, and the page or object it concerns when that is known.
use pdfluent::{DiagnosticCategory, LeniencyReport};
let report = LeniencyReport::from_diagnostics(&doc.diagnostics());
println!("{} event(s), {} distinct", report.events.len(), report.unique_event_count);
for event in &report.events {
if event.category == DiagnosticCategory::Repair {
println!("[{}] {} (page {:?})", event.code, event.message, event.page);
}
}A page that was recovered structurally can still have lost its content stream. Walking the pages is also the reliable way to get the content out of a damaged file: extraction and rendering go through the parser that did the recovery.
let recovered = doc.page_count();
for page in doc.pages() {
let text = page.text().unwrap_or_default();
println!("page {}: {} chars", page.number(), text.len());
}
println!("{recovered} page(s) recovered");save() writes the document from the second of the two parsers this library loads a file with, and the structural recovery happens in the first. A file that only the engine could make sense of is therefore not necessarily written back out repaired -- what you can rely on is the content you just read, and the diagnostics that say what was wrong. Treat the output as a copy, and verify it before handing it on.
use pdfluent::PdfDocument;
doc.save("copy.pdf")?;
// Verify rather than assume: open the copy and see what it says about itself.
let again = PdfDocument::open("copy.pdf")?;
println!("copy has {} page(s)", again.page_count());
println!("{} event(s) on the copy", again.diagnostics().len());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());
}