Migrate legacy PDFs and use WASM

This guide shows developers how to migrate from a legacy PDF stack and prepare PDF operations for WebAssembly.

Migrate from a legacy PDF stack

Many teams accumulate a mix of old tools: PDFBox for reading, iText for writing, custom scripts for forms, and shell wrappers around Ghostscript. Consolidate to one Rust crate.
  1. Step 1

    Audit your current PDF toolchain

    Before replacing anything, list every library and tool touching PDFs in your system. Common legacy stacks include PDFBox for reading, iText for writing, a separate form library, and Ghostscript invoked via Runtime.exec() or shell scripts. Each one has its own dependency, license, and failure mode.

    Before
    rust
    # Typical legacy PDF stack
    # pom.xml: iText 5 (AGPL), PDFBox 2.0 (Apache)
    # scripts/fill_form.sh: calls pdftk or ghostscript
    # lib/pdf_util.py: Python wrapper around pdfminer
    # DockerFile: installs ghostscript, pdftk, java, python
    #
    # Result: 4 runtimes, 3 licenses, 500+ MB image
    After (PDFluent)
    rust
    # PDFluent replaces all of the above
    # Cargo.toml: pdfluent = "1.1.0"  (commercial license)
    #
    # One crate for: reading, writing, text extraction,
    # form filling, XFA, annotations, and metadata.
    # Dockerfile: copies a single static binary.
  2. Step 2

    Replace reading and text extraction first

    Start with the lowest-risk part of the stack: reading documents and extracting text. This is typically handled by PDFBox or pdfminer and is safe to swap without changing any downstream logic. Verify output parity before moving to the next operation.

    Before
    rust
    // PDFBox text extraction
    PDDocument document = PDDocument.load(new File("report.pdf"));
    PDFTextStripper stripper = new PDFTextStripper();
    stripper.setStartPage(1);
    stripper.setEndPage(document.getNumberOfPages());
    String text = stripper.getText(document);
    document.close();
    After (PDFluent)
    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("report.pdf")?;
    let pages = doc.page_count();
    let mut all_text = String::new();
    for i in 0..pages {
        all_text.push_str(&doc.page(i)?.text()?);
    }
  3. Step 3

    Consolidate form filling and document writing

    Legacy stacks often use a different library for writing than for reading. Replace iText or pdftk form-filling with PDFluent's form_mut setters. Replace Ghostscript shell invocations with PDFluent's document manipulation methods. Each replacement removes a runtime dependency from your Docker image.

    Before
    rust
    // iText 5 form filling (AGPL)
    PdfReader reader = new PdfReader("template.pdf");
    PdfStamper stamper = new PdfStamper(
        reader,
        new FileOutputStream("filled.pdf")
    );
    AcroFields form = stamper.getAcroFields();
    form.setField("company_name", "Acme Corp");
    form.setField("invoice_date", "2024-04-14");
    stamper.setFormFlattening(true);
    stamper.close();
    reader.close();
    After (PDFluent)
    rust
    let mut doc = PdfDocument::open("template.pdf")?;
    let mut form = doc.form_mut();
    form.set_text("company_name", "Acme Corp")?;
    form.set_text("invoice_date", "2024-04-14")?;
    doc.flatten_forms()?;
    doc.save("filled.pdf")?;
  • iText 5 is AGPL-licensed. If your team has been quietly ignoring this, migration to PDFluent resolves the license risk.
  • Shell wrappers around pdftk or Ghostscript are fragile — they depend on specific installed versions and break silently when the binary is missing. PDFluent eliminates all external process invocations.
  • If you are using PDFBox 2.x, note that PDFBox 3.0 changed several APIs. Rather than upgrading PDFBox, migrating to PDFluent at this point may be less work.
  • Migrate one operation type at a time and run both the old and new code in parallel on the same documents to verify output parity before decommissioning the old tool.
Our legacy stack has years of workarounds for specific PDFs. Will PDFluent handle them?
PDFluent is tolerant of malformed and non-standard PDFs encountered in production. For edge cases that matter, the migration period of running old and new code in parallel will surface any differences before you cut over.
We rely on Ghostscript for PDF/A conversion. Does PDFluent support that?
PDFluent supports PDF/A validation and conversion. This is one of the highest-value replacements for Ghostscript shell invocations, as it removes a large system dependency and enables serverless deployment.
How much smaller will our Docker image be?
A Java + Ghostscript + Python stack carries a JVM, Ghostscript and a Python runtime in the image. A Rust binary with PDFluent can be deployed in a debian:slim image without them.
Can we migrate team members who only know Java or Python?
PDFluent provides Python and Node bindings in addition to the native Rust crate. Your Java or Python team can use the bindings while the Rust team migrates the core service, reducing the learning curve.

From server PDF to WASM

Move PDF operations from a server endpoint to a WebAssembly module running in the browser. PDFluent compiles to WASM. No server round-trips for basic PDF operations.
  1. Step 1

    Identify which PDF operations can move to the client

    Not every PDF operation belongs in the browser. Operations that work well in WASM: form filling, text extraction, annotation, metadata editing, and basic PDF creation. Operations that should stay server-side: high-security signing, server-only data injection, and compliance workflows requiring an audit trail.

    Before
    rust
    // Current: every PDF operation hits your server
    // POST /api/pdf/fill-form
    // POST /api/pdf/extract-text
    // POST /api/pdf/add-annotation
    // POST /api/pdf/merge
    //
    // Round-trip: one network request per operation
    // Server load: scales with user count
    // Offline use: impossible
    After (PDFluent)
    rust
    // Target: common operations run in the browser
    // WASM module loaded once, runs locally
    //
    // Round-trip: none (runs in the tab)
    // Server load: only for signing and compliance
    // Offline use: fully supported
  2. Step 2

    Build PDFluent as a WASM target

    PDFluent compiles to WebAssembly with wasm-pack. Add the wasm32 target and run wasm-pack build to get an npm-compatible package plus a JavaScript glue module. For scale, the published @pdfluent/sdk-wasm@1.0.0 .wasm is 15.3 MB unpacked and 3.8 MB with Brotli [B07, 2026-09-16]. Your own build will differ.

    Before
    rust
    // Server endpoint (Node.js / Express example)
    app.post('/api/pdf/fill-form', async (req, res) => {
        const { pdfBytes, fields } = req.body;
        // Server receives PDF bytes from client,
        // processes them, returns filled PDF.
        // Every user action requires a network round-trip.
        const filled = await fillFormOnServer(pdfBytes, fields);
        res.send(filled);
    });
    After (PDFluent)
    rust
    # Build PDFluent for WASM
    rustup target add wasm32-unknown-unknown
    cargo install wasm-pack
    wasm-pack build --target web --release
    
    # Output: pkg/pdfluent_bg.wasm
    #         pkg/pdfluent.js      (glue module)
  3. Step 3

    Load the WASM module and replace the server calls

    Import the wasm-pack output as an ES module in your frontend code. Initialize the WASM binary once on page load, then call PDFluent operations directly in the browser. Replace fetch() calls to your server endpoint with direct WASM calls on the PDF ArrayBuffer.

    Before
    rust
    // Before: round-trip to server
    async function fillForm(pdfBytes, fields) {
        const response = await fetch('/api/pdf/fill-form', {
            method: 'POST',
            body: JSON.stringify({ pdfBytes: Array.from(pdfBytes), fields }),
            headers: { 'Content-Type': 'application/json' },
        });
        return new Uint8Array(await response.arrayBuffer());
    }
    After (PDFluent)
    rust
    // After: runs in the browser via WASM
    import init, { fill_form_fields } from './pkg/pdfluent.js';
    
    await init(); // load .wasm once at startup
    
    async function fillForm(pdfBytes, fields) {
        // Runs locally — no network, no server
        return fill_form_fields(pdfBytes, fields);
    }
  • The published @pdfluent/sdk-wasm@1.0.0 .wasm file is 15.3 MB unpacked and 3.8 MB with Brotli [B07, 2026-09-16]. Load it once and cache it with a Service Worker for offline support; a build of your own will differ.
  • PDF signing with a server-held private key must stay server-side. WASM is appropriate for user-facing, non-sensitive operations.
  • WASM runs in a single thread by default in browsers. For large PDFs, consider Web Workers to avoid blocking the main thread.
  • If users upload PDFs to your server for processing, shifting to WASM also reduces GDPR surface area — the PDF bytes never leave the browser.
How large is the PDFluent WASM bundle?
The published @pdfluent/sdk-wasm@1.0.0 .wasm is 15.3 MB unpacked, 3.8 MB Brotli, 5.5 MB gzip [B07, 2026-09-16]. It loads once and is cached by the browser.
Can WASM access the local file system to read PDFs?
WASM runs in the browser sandbox with no direct file system access. Use the File API or a file picker to read PDFs into an ArrayBuffer, pass the bytes to PDFluent, and download the result via a Blob URL.
Does PDFluent WASM support the same operations as the native Rust crate?
The WASM build supports text extraction, form filling (AcroForm and XFA), annotation, and basic PDF creation. Advanced rendering and some signing features may differ — check the WASM feature matrix in the documentation.
What happens when a user's browser does not support WASM?
All major browsers have supported WebAssembly since 2017. For the remaining edge cases, retain your server endpoint as a fallback and detect WASM support with typeof WebAssembly !== "undefined".