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-beta.18"

Create a new PDF document from scratch in Rust

Build a PDF programmatically. Add pages, text, images, and set document metadata without any source file.

rust
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(())
}
  1. Create a new document and set metadata

    PdfDocument::new() creates an empty PDF 1.7 document. Set XMP and DocInfo metadata before adding content.

    rust
    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");
  2. Create a page and set its size

    Page::new() accepts a PageSize enum or custom dimensions in points. Common sizes: A4 is 595x842 pt, US Letter is 612x792 pt.

    rust
    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());
  3. Add text with position and style

    PDF coordinates start at the bottom-left corner. Use font_size, color, and an optional embedded font to control appearance.

    rust
    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()
        },
    )?;
  4. Draw lines, rectangles, and add images

    Use the page drawing API for visual structure. Add a horizontal rule under a header or a bounding box around a table.

    rust
    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")?;
  • The 14 standard PDF fonts (Helvetica, Times-Roman, Courier) do not need embedding. For any other typeface, load a TTF/OTF and call doc.embed_font() before use.
  • Pages are added in the order you call doc.add_page(). Insert a page at a specific position with doc.insert_page(index, page).
  • For multi-page documents with repeated headers and footers, build a shared layout function that accepts a &mut Page and call it for each page.
  • doc.save() writes to a file. Use doc.to_bytes() to get a Vec<u8> for in-memory handling, S3 uploads, or HTTP responses.

Convert HTML to PDF in Rust

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.

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(())
}
  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

Use PDFluent's recovery parser to rebuild the cross-reference table and salvage as many objects as possible from a damaged file.

rust
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(())
}
  1. Try to open with standard parsing first

    Standard parsing is faster. Only fall back to recovery mode if the standard open returns an error.

    rust
    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),
    }
  2. Open in recovery mode

    Recovery mode uses a linear scan of the file to find all PDF objects rather than relying on the cross-reference table.

    rust
    use pdfluent::{PdfDocument, OpenOptions};
    
    let doc = PdfDocument::open_with(
        "damaged.pdf",
        OpenOptions::new().recovery_mode(true),
    )?;
  3. Read the repair report

    The repair report describes what was found and what could not be recovered.

    rust
    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());
  4. Verify page count and content

    Check that the expected pages are present. Some pages may be unrecoverable if their stream data was overwritten.

    rust
    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());
    }
  5. Save the recovered file

    Write the repaired document. The output is a structurally valid PDF even if some content was lost.

    rust
    doc.save("repaired.pdf")?;
    println!("Saved repaired.pdf");
  • Recovery mode cannot reconstruct content streams that are physically absent or overwritten. It can only find objects that are present in the file bytes.
  • A truncated file (download cut short) is one of the most common corruption causes. Recovery mode handles truncation by treating the end-of-file as the end of the last recoverable object.
  • Encryption prevents recovery of encrypted streams without the password. If the file was encrypted before corruption, decrypt first if possible.
  • Recovery mode is significantly slower than standard parsing because it reads the entire file byte by byte.

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.