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.
# 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# 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.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.
// 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();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()?);
}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.
// 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();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")?;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.
// 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// 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 supportedPDFluent 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.
// 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);
});# 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)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: 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: 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);
}