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.
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.tsserves the app underconnect-src 'self'andform-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/byscripts/vendor-assets.mjs, so nothing is fetched from a CDN at runtime.
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:
- 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.
- Arrange them — select, drag, rotate, delete, crop. Crop is one overlay applied to the current selection, not a mode or a route.
- 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.
| 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.
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.
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.
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 mathsNode 24 LTS (see .nvmrc), Bun 1.3+.