Skip to content

Repository files navigation

PDF.

A PDF workbench that runs entirely in the browser. Merge, split, reorder, rotate, crop, compress, number, watermark, lock, unlock, convert to and from images, and add a searchable text layer — without a byte of the document leaving the device.

Why it is built this way

Upload-based PDF sites ask you to trust a retention policy. PDF. removes the question: there is no server in a document's life, and two structural choices keep it that way rather than leaving it to good intentions.

  • A browser-enforced lock. next.config.ts serves the app under connect-src 'self' and form-action 'none', so exfiltration is blocked by the browser, not by our good intentions.
  • Fully static. Every route prerenders. No server action, no API route, no request that could carry a document. OCR and rendering assets are vendored into public/ by scripts/vendor-assets.mjs, so nothing is fetched from a CDN at runtime.

One bench, not twenty tools

iLovePDF is a directory of endpoints because each one is a different server job. With no server there is nothing to route to, so the whole app is a single pipeline with three stations:

  1. Bring pages in — PDFs and images land in the same grid. A JPG or PNG becomes a page; there is no "image to PDF" tool. A locked PDF asks for its password inline at intake; there is no "unlock" tool.
  2. Arrange them — select, drag, rotate, delete, crop. Crop is one overlay applied to the current selection, not a mode or a route.
  3. Decide how they come out — the export bar reads as one plain-English sentence. Optional finishes (page numbers, watermark, password, searchable text) are added as chips, never configured to off.

Landing-page cards are doors into that same room with a different starting position, not separate tools.

Stack

Concern Choice Licence
Framework Next.js 16 (App Router, Turbopack), React 19 MIT
Structural PDF edits, stamps, encryption @cantoo/pdf-lib — maintained fork of pdf-lib MIT
Rendering and rasterising pdfjs-dist v6 Apache-2.0
OCR tesseract.js (self-hosted core, LSTM only) Apache-2.0
Worker bridge comlink Apache-2.0
Zip fflate MIT
Styling Tailwind CSS v4 MIT

MuPDF and Ghostscript were both considered for compression and rejected: they are AGPL, which would force this app's source open and would need a commercial licence for any proprietary or hosted use.

How the pieces fit

main thread                          worker
───────────                          ──────
pdf.js  → thumbnails, rasterising    pdf-lib → merge, split, rotate, crop,
  ↑ its own worker                             stamp, encrypt, rebuild
tesseract → text layer                 ↑ src/lib/pdf/worker.ts via Comlink
  ↑ its own worker

src/lib/pdf/operations.ts and finish.ts hold the pdf-lib logic with no worker coupling, so they can be exercised directly under Bun. worker.ts only exposes them.

Export is a chain of bytes-to-bytes steps, in this order:

assemble → compress → searchable text → stamps + encryption → download

Encryption is last because the intermediate bytes are handed to pdf.js for rasterising and OCR.

Two things worth knowing

Compression re-renders each page to a JPEG at a chosen DPI and rebuilds the PDF around those images. It cuts scans dramatically and makes text non-selectable. The UI says so at the point of choice rather than burying it.

Searchable text does not do that. Tesseract emits an invisible text layer only, which is drawn over the untouched original page — so OCR costs nothing in fidelity.

Running it

bun install
bun run dev      # http://localhost:3000
bun run build
bun run lint
bun run test     # the pure logic: page ops, output planning, rotation maths

Node 24 LTS (see .nvmrc), Bun 1.3+.

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages