Create, convert, compress, repair, and attach files

A practical guide for Rust developers. Learn how to perform six core PDF operations using the PDFluent SDK.

Add PDFluent to Cargo.toml

Creating PDFs from scratch requires only the base crate.

toml
# Cargo.toml
[dependencies]
pdfluent = "1.0.0"

Convert HTML to PDF in Rust

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.

rust
chrome --headless --disable-gpu --print-to-pdf=out.pdf input.html
  1. Render HTML to PDF using headless Chrome

    Use Chrome/Chromium's built-in headless mode to convert HTML to PDF. Run this command in your shell.

    rust
    chrome --headless --disable-gpu --print-to-pdf=out.pdf input.html
  2. Process the result with PDFluent

    Everything 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.

    rust
    // 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")?;
  • PDFluent does not include HTML rendering because browsers implement complex standards. A partial implementation would produce subtly incorrect output.
  • For batch processing, wrap the Chrome command in a shell script or use std::process::Command.
  • Watermark text supports basic formatting (font, size, opacity) but not HTML/CSS.
  • PDF/A conversion may fail if the input PDF uses features incompatible with the selected profile.

Compress a PDF in Rust

Shrink a PDF in-memory with CompressOptions. Three presets cover the common cases: strict (default), lossy, and archival.

rust
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

  1. Open the PDF with a mutable binding

    compress takes &mut self and rewrites the in-memory document. You save afterwards to persist.

    rust
    use pdfluent::prelude::*;
    
    let mut doc = PdfDocument::open("report.pdf")?;
  2. Pick a preset

    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).

    rust
    // 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();
  3. Run compress and read the CompressReport

    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.

    rust
    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);
    }
  4. Save the compressed output

    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.

    rust
    doc.save_with(
        "report_compressed.pdf",
        SaveOptions::new().with_overwrite(true),
    )?;
  • compress is idempotent — running it twice on the same document produces byte-identical output on the second pass (up to writer nondeterminism).
  • subset_fonts (bundled into compress) never increases font stream size — if a font can't be reduced, it's left untouched.
  • For signed documents, prefer CompressOptions::archival() so unused objects remain addressable from the incremental-update chain.
  • Compression runs entirely in-process — no external tools, no subprocess. Memory usage peaks around 2× the input during the pass.

Attempt to recover and repair a corrupted PDF in Rust

Open a damaged file -- the cross-reference table is rebuilt without asking -- and read what had to be repaired out of the diagnostics.

rust
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(())
}
  1. Open the file

    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.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("damaged.pdf")?;
    println!("{} pages", doc.page_count());
  2. Handle the file that cannot be opened at all

    When recovery finds nothing usable the open fails with Error::InvalidPdf, carrying the byte offset where parsing gave up when that is known.

    rust
    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),
    }
  3. Read what the recovery had to do

    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.

    rust
    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);
        }
    }
  4. Check what survived, and get the content out

    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.

    rust
    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");
  5. Saving is not the repair

    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.

    rust
    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());
  • Recovery is not a mode you switch on. OpenOptions::with_repair exists and is accepted, but it is advisory: the engine already scans for objects whenever normal parsing fails, for every document.
  • Recovery cannot reconstruct bytes that are not in the file. A stream that was overwritten or truncated away is gone; what recovery restores is the index to what is still there.
  • A truncated download is the common case, and it is the one recovery handles best: the last complete object becomes the end of the document.
  • If the file was encrypted before it was damaged, the streams cannot be decoded without the password. Supply it with OpenOptions::with_password.
  • Saving a recovered document is not a repair tool. The library parses a file twice -- once in the engine, where the recovery happens, and once in the writer's own object model -- and save writes the second. Read the content you need out of the document you opened; treat a saved copy as something to verify, not as a fixed file.
  • diagnostics() reads the buffer without emptying it; take_diagnostics() empties it. Use the second one if you process many documents in one process and want each report to stand on its own.

Embed a file attachment inside a PDF in Rust

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.

rust
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(())
}
  1. Open the PDF

    Load the document that will receive the attachment.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("invoice.pdf")?;
  2. Build the Attachment from a file on disk

    Attachment::from_file() reads the file bytes, determines a filename, and sets a creation date. All fields can be overridden.

    rust
    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");
  3. Attach from in-memory bytes

    If the file content is already in memory, use Attachment::from_bytes() instead.

    rust
    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");
  4. Add the attachment to the document and save

    attach_file() embeds the file in the document-level EmbeddedFiles name tree. Save afterwards.

    rust
    doc.attach_file(attachment)?;
    
    // Verify
    println!("Attached files: {}", doc.attachments().len());
    
    doc.save("invoice_with_attachment.pdf")?;
  • Attached files are stored as EmbeddedFile streams in the PDF. They are not visible on any page unless you also add a Attachment annotation.
  • MIME type is optional but recommended. PDF/A-3 requires it for embedded files.
  • Multiple files can be attached. Call attach_file() once per file.
  • Attached file size adds directly to the PDF file size. Compress large attachments before embedding.

Extract embedded file attachments from a PDF in Rust

List and extract all embedded file streams from a PDF document. Save attachments to disk or read them directly as byte buffers.

rust
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(())
}
  1. Open the PDF

    Open the document. A read-only borrow is enough for reading attachments.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("package.pdf")?;
  2. List all attachments

    Call doc.attachments() to get attachment metadata. No file data is read at this point.

    rust
    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(),
        );
    }
  3. Extract a specific attachment by name

    Find the attachment you want by filename and extract its bytes.

    rust
    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.");
    }
  4. Extract all attachments to a directory

    Loop over all attachments and save each to a target folder.

    rust
    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());
    }
  • attachments() returns document-level embedded files from the EmbeddedFiles name tree. Page-level file annotations are accessed separately via page.annotations().
  • read_data() decompresses the embedded file stream and returns raw bytes. No temporary files are created.
  • Attachments can be any file type: XML, CSV, XLSX, images, or other PDFs.
  • If the PDF is encrypted, decrypt it first. Otherwise, attachment streams are not accessible.