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.
use pdfluent::HtmlToPdf;
fn main() -> pdfluent::Result<()> {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 40px; }
h1 { color: #1a1a1a; }
.total { font-weight: bold; color: #2563eb; }
</style>
</head>
<body>
<h1>Invoice #INV-2024-042</h1>
<p>Due: 2024-05-01</p>
<p class="total">Total: EUR 1,200.00</p>
</body>
</html>
"#;
let pdf_bytes = HtmlToPdf::new()
.page_size_a4()
.margin_mm(20.0)
.render_html(html)?;
std::fs::write("invoice.pdf", &pdf_bytes)?;
println!("Saved invoice.pdf ({} bytes)", pdf_bytes.len());
Ok(())
}The html feature requires a Chromium or Chrome installation on the machine. PDFluent calls it via the Chrome DevTools Protocol (CDP). Install the browser bridge with the provided CLI helper.
# Cargo.toml
[dependencies]
pdfluent = { version = "0.9", features = ["html"] }
# Install Chromium for the bridge (Linux)
apt-get install -y chromium-browser
# macOS
brew install --cask chromium
# Or set a custom Chrome path via environment variable
# PDFLUENT_CHROME_PATH=/usr/bin/google-chromeHtmlToPdf::new() launches a headless browser, loads the HTML, and triggers the browser print-to-PDF function. The bytes are returned in memory.
use pdfluent::HtmlToPdf;
let html = "<h1>Hello, PDF</h1><p>This is a test.</p>";
let pdf_bytes = HtmlToPdf::new()
.page_size_a4()
.margin_mm(15.0)
.render_html(html)?;
std::fs::write("output.pdf", &pdf_bytes)?;render_url() navigates to a URL and waits for the page to fully load before printing. Useful for reports served from a local web server.
let pdf_bytes = HtmlToPdf::new()
.page_size_a4()
.wait_for_idle_ms(1000) // wait 1 s for JS to finish
.render_url("http://localhost:3000/invoice/42")?;
std::fs::write("invoice-42.pdf", &pdf_bytes)?;Pass a file:// URL or use render_file(). All relative paths (images, CSS) must be resolvable from the file location.
let pdf_bytes = HtmlToPdf::new()
.page_size_a4()
.render_file("/tmp/invoice.html")?;
std::fs::write("invoice.pdf", &pdf_bytes)?;Control page size, margins, header/footer, and background printing.
use pdfluent::{HtmlToPdf, PageSize};
let pdf_bytes = HtmlToPdf::new()
.page_size(PageSize::Letter)
.margin_top_mm(15.0)
.margin_bottom_mm(15.0)
.margin_left_mm(20.0)
.margin_right_mm(20.0)
.print_background(true)
.header_template("<div style='font-size:10px'>Report</div>")
.footer_template("<div style='font-size:10px'>Page <span class='pageNumber'></span></div>")
.render_html(&html)?;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, FileAttachment};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut doc = PdfDocument::open("invoice.pdf")?;
doc.attach_file(
FileAttachment::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")?;FileAttachment::from_file() reads the file bytes, determines a filename, and sets a creation date. All fields can be overridden.
use pdfluent::FileAttachment;
let attachment = FileAttachment::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 FileAttachment::from_bytes() instead.
let xml_bytes = generate_xml_data(); // your function
let attachment = FileAttachment::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());
}