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"PDFluent does not render HTML itself. Pair it with headless Chrome or Chromium, which is free to run, to convert the HTML to PDF, then use PDFluent for everything after that: watermarking, compression, and PDF/A conversion.
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());
}