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
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(())
}
  1. Add PDFluent with the html feature and install the browser bridge

    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.

    rust
    # 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-chrome
  2. Render an HTML string to PDF bytes

    HtmlToPdf::new() launches a headless browser, loads the HTML, and triggers the browser print-to-PDF function. The bytes are returned in memory.

    rust
    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)?;
  3. Render a URL to PDF

    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.

    rust
    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)?;
  4. Render an HTML file from disk

    Pass a file:// URL or use render_file(). All relative paths (images, CSS) must be resolvable from the file location.

    rust
    let pdf_bytes = HtmlToPdf::new()
        .page_size_a4()
        .render_file("/tmp/invoice.html")?;
    
    std::fs::write("invoice.pdf", &pdf_bytes)?;
  5. Set custom print options

    Control page size, margins, header/footer, and background printing.

    rust
    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)?;
  • PDFluent uses the Chrome DevTools Protocol (CDP) to control the browser. Chromium or Chrome must be installed. There is no bundled browser.
  • The browser bridge is a separate process. First render takes 300-800 ms for browser startup. Subsequent renders on the same HtmlToPdf instance reuse the browser process and take 50-200 ms.
  • For server deployments without a display, run Chrome with --no-sandbox --disable-gpu flags. These are set automatically by PDFluent on Linux.
  • CSS page breaks (@page, break-before, break-after) are respected by Chromium and appear correctly in the output PDF.
  • For pure programmatic PDF generation without a browser dependency, use PdfDocument::new() and add content directly. See the Create a PDF from Scratch guide.

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, 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(())
}
  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 FileAttachment from a file on disk

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

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

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

    rust
    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");
  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 FileAttachment 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.