Every "free" PDF tool I've ever used works the same way: you upload your file to a server, a script on the other end does the actual work, and you download the result a few seconds later. That's fine until you remember what people actually put in PDFs — signed contracts, medical records, tax documents, resumes with home addresses on them — and realize you just handed all of it to a company you've never heard of, in exchange for a "merge" button.
That bothered me enough that I spent the last several months building ihatepdf.cv, a PDF toolkit with 46 tools — merge, split, compress, OCR, encrypt, redact, convert to and from almost every format, even a GST invoice generator — where the file never leaves the browser. No upload step exists in the architecture, because there's no server-side processing to upload to.
Here's what building that actually looked like.
The constraint that shaped everything
The moment you say "no server," you've ruled out the easy path for basically every PDF operation. Compression, OCR, format conversion — these are normally solved with a beefy backend running Ghostscript, Tesseract, or LibreOffice headless. None of that exists in a browser tab.
So the entire toolkit is built on WebAssembly modules doing the heavy lifting client-side, wrapped in a PWA shell so the app itself works offline after the first load. A few examples of how that split played out:
- Compression — three presets (Light/Medium/Heavy) implemented by re-encoding embedded images and re-serializing the PDF object graph in-browser, rather than shelling out to Ghostscript.
- OCR — Tesseract compiled to WASM, run entirely client-side against scanned pages to produce a searchable text layer.
- Office format conversion (Word, Excel, PowerPoint ↔ PDF) — this is normally the hardest thing to do without a server, since it usually means Microsoft's own rendering engine. Getting formatting, merged cells, and embedded fonts to survive the round trip without a backend was the single most time-consuming part of the build.
- AI features (chat-with-PDF, summarization) — the one place a network call is unavoidable, since on-device LLMs aren't practical yet for this. Everything else stays local.
The handwriting-to-PDF problem
The tool that took the most iteration was converting handwritten notes into a searchable PDF. My first few attempts ran OCR and dumped the recognized text back onto the page — and it wrecked any document with actual structure: tables, multi-column notes, forms. Text would get recognized correctly but placed in the wrong reading order, or a two-column page would get flattened into one garbled column.
I went through a few architectures before landing on the right one:
- Tune Tesseract's page segmentation mode (PSM) per document type — no single PSM setting worked for both dense paragraphs and sparse tables.
- Add line-grouping logic to reconstruct reading order from bounding boxes instead of trusting raw OCR output order.
- Add gap-based column splitting to detect multi-column layouts before running line grouping.
All of that improved accuracy, but it was still fundamentally fragile — I was trying to reproduce the original layout from OCR output, which throws away information every time.
The fix was to stop trying to reconstruct the page at all. The tool now produces a "searchable scan": the original handwritten image stays exactly as-is as the visual layer, and the OCR'd text sits invisibly underneath it, positioned to match. You see your actual handwriting; Ctrl+F finds the words anyway. It's a much smaller claim architecturally — don't recreate the document, just make it searchable — and it's the version that actually holds up on messy real-world input.
Service workers are where PWAs go to die
The offline-first requirement meant getting a service worker right, which is its own genre of pain. The specific bug that cost me the most time was a TypeError thrown when the SW's cache lookup resolved to undefined — happening intermittently, only on certain navigation patterns, which made it miserable to reproduce. The fix ended up being a full rewrite of the fetch-handling logic to explicitly guard every cache read instead of assuming a Response would always come back. If you're building a PWA and haven't hit this yet, you will — write the defensive check before you need it.
Why this matters beyond the privacy pitch
"No upload" is usually pitched as a privacy feature, and it is one. But it's also just a product advantage that's easy to undersell:
- It works on a plane. Once the PWA is cached, there's no network dependency for 45 of the 46 tools.
- There's no file size ceiling imposed by a server timeout or upload limit — processing is bounded by the user's own device, not by what a backend is willing to accept.
- The infra bill doesn't scale with usage. Every PDF operation is a cost on the user's CPU, not mine. That's a very different scaling story than a SaaS with a processing queue.
The tradeoff, to be honest about it: client-side WASM bundles are heavy, and shipping 46 tools' worth of processing code without tanking Core Web Vitals (especially LCP and TBT) has been an ongoing tuning problem, not a solved one.
If you're curious about the rest of the toolkit or want to see how a specific conversion holds up on your own file, it's live at ihatepdf.cv — genuinely free, no watermark, no signup, and your file really doesn't go anywhere. I've also got a running technical blog where I document architecture decisions like these in more depth.
Happy to go deeper on any part of this in the comments — the OCR layout problem in particular I could talk about for a while.
Top comments (0)