Every "free PDF tool" site works the same way: you upload your document to someone's server, they process it, you download the result. You are trusting a stranger with a contract, a payslip, a medical form, and the privacy policy that governs it is usually three paragraphs of nothing. 😬
I wanted to know how many of those tools actually need a server. The answer turned out to be: most of them don't. 49 out of 59, in my case.
Open Doc Tools has 59 tools. 49 of them run entirely in your browser tab. The file is never uploaded, because there is nothing to upload to. 🔒
🧰 What runs client-side, and with what
Most of it is unglamorous library work:
- pdf-lib: merge, split, rotate, crop, page numbers, watermarks, form filling. Everything structural.
- pdfjs-dist: rendering pages to canvas for previews, extracting text, and reading PDF attachments (which is how the e-invoice viewer pulls the XML out of a ZUGFeRD hybrid PDF).
- tesseract.js: OCR. The language data is ~10 MB per language, fetched on demand when the user picks one.
- @imgly/background-removal: a segmentation model running on ONNX Runtime Web via WebAssembly.
- exceljs / docx / pptxgenjs / mammoth: reading and writing Office formats without touching a server.
- upscaler + tfjs: image upscaling with ESRGAN.
The browser is a genuinely capable document runtime now. The gap between "needs a server" and "doesn't" is much smaller than the market implies.
☁️ What genuinely does need a server
Ten tools do. Mostly format conversions that need a real office suite: DOCX to PDF, XLSX to PDF, PPTX to PDF. Those run LibreOffice headless in an isolated container, alongside qpdf and Ghostscript.
These are the only tools where your file leaves your device, and that is exactly why they are labelled differently from the rest.
I could have hidden this. Instead every tool carries a badge saying where it runs, and the honest number, 49 of 59, is in the page title. A privacy claim that quietly excludes one tool in six is not a privacy claim.
🛡️ The Content Security Policy fight
This is the part that cost me the most time, and the part nobody warns you about.
A strict CSP and WebAssembly do not get along. Compiling WASM needs 'wasm-unsafe-eval' in script-src:
script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob:
It is narrower than it sounds. Unlike 'unsafe-eval', it cannot turn a string into code, only compile WASM. But without it the background remover fails, and it fails only in production, because the dev server ships no CSP. That is a fun afternoon. 🙃
blob: is there for a second reason: the ONNX runtime dynamically imports a module it generates at runtime from a blob URL. A blob script can only be created by code already running on your own origin, so this does not open the door to a third party. But you have to know that before you feel good about adding it.
Yes, 'unsafe-inline' is in there too. It is not ideal, and I would like to tighten it.
🐛 The bug that taught me the most
I split money handling into two modules: one with node:fs for persistence, one pure for formatting. Then I imported the wrong one into a client component.
The build passed. The page rendered. And then it crashed in the browser with h(...).join is not a function, because Next had bundled a server-only module into the client, and node:fs does not exist there.
The lesson I actually took from it: "shared utility module" is a smell when half of it is server-only. Split by where it runs, not by what it is about.
⚠️ What doesn't work well
Client-side is not magic, and I would rather tell you than have you find out:
- Big files are slower. Your device is doing the work, not a data-centre CPU. Large scanned PDFs through OCR take a while.
- Low-end phones can struggle. Heavy tools like background removal and upscaling need a fair amount of memory.
- OCR needs a download first. The ~10 MB language pack has to arrive before the first run.
- Some conversions can't be done in a browser. That is the ten server-side tools above.
💡 What I would do differently
- Decide the CSP first. Retrofitting one around WASM, blob workers and third-party scripts is far more painful than starting strict and widening deliberately.
- Count the marketing claims in code. The "49 of 59" on my homepage is computed from the tool registry, so it cannot drift from what actually ships. I added that after nearly publishing a wrong number.
- Test the region-dependent paths. Consent defaults that differ by country are invisible from wherever you happen to be sitting.
It is at opendoctools.com: free, no account, no watermarks. Happy to answer anything about the implementation. 🙌
Top comments (0)