DEV Community

BellSal
BellSal

Posted on

Free PDF tools paywall the one button that matters. Here's why — and how I avoided it

Almost every "free" PDF tool lets you merge, split, rotate and reorder pages for free — and then puts a paywall on the one button you actually came for: Export. I got annoyed enough to build a PDF toolkit that doesn't, so here's the honest technical reason it happens and what it takes to avoid it.

Reading a PDF is cheap. Writing a valid one back is not.

Rendering a PDF to show it on screen is a solved, cheap problem — the browser and a dozen libraries do it. The expensive part is going the other way: taking your edits and serializing a spec-valid PDF back out. The PDF format is a cross-referenced object graph with an xref table, object streams, and byte-offset bookkeeping that has to stay internally consistent or the file won't open.

That asymmetry is why so many tools happily show you a preview and let you rearrange thumbnails, then charge at export: the preview is free, the write-out is the work.

The second reason: they uploaded your file to do it

The other common pattern is a server round-trip. Your document goes up to their backend, a headless tool (Ghostscript, a licensed SDK, LibreOffice) does the manipulation, and the result comes back. That costs them real money per file — CPU, bandwidth, storage — which is exactly why it eventually needs to cost you money, and why the free tier is capped at "3 files a day."

For a contract, an ID scan, or a payslip, silently shipping the document to someone's server is also a privacy problem people don't think about until later.

Doing it client-side with pdf-lib

The fix for both problems is the same: never upload, and do the write-out in the browser. pdf-lib is the key piece — it can create and modify existing PDFs entirely in JS, including the parts that are annoying to get right:

import { PDFDocument } from 'pdf-lib';

// merge: copy pages between documents, re-embedding fonts/resources
const out = await PDFDocument.create();
for (const bytes of files) {
  const src = await PDFDocument.load(bytes);
  const pages = await out.copyPages(src, src.getPageIndices());
  pages.forEach((p) => out.addPage(p));
}
const merged = await out.save(); // valid xref + object streams, in the browser
Enter fullscreen mode Exit fullscreen mode

copyPages is the bit that matters: it deep-copies the page's resource dictionary (fonts, images, shared objects) into the target so the merged file is actually valid, not just concatenated bytes. The same document model handles split, extract/remove/reorder, rotate, N-up, page numbers and watermarks — all as object-graph edits, then one save().

A few things that bite:

  • Compression / grayscale aren't first-class in pdf-lib; you work at the content-stream and image-XObject level, which is fiddly but keeps it local.
  • OCR (making a scanned PDF searchable) needs a separate WASM engine (Tesseract) — heavy, so lazy-load it only when the user actually asks.
  • Editing existing body text is the one thing you genuinely can't fake: PDF has no reflowable text model, so "change the words" still means a real editor like LibreOffice Draw. Be honest about that limit instead of pretending.

Why bother

Client-side means the file never leaves the device, there's no per-file server cost, so the export can just be free — permanently — and static hosting keeps it that way. That's the whole reason pdfonlinefree.com exists: merge, split, compress, sign, OCR, unlock, page numbers, watermark, JPG↔PDF — export included, nothing uploaded.

If you're building something similar: put the expensive write-out in the browser, lazy-load the WASM parts, and don't paywall the button people came for.

Top comments (0)