Every "free online tool" works the same way: you upload your file to a server, hope for the best, and get a result back. I got tired of that model — both as a user (why does a PDF compressor need my documents?) and as a developer (why pay for servers that just shuffle files?).
So I built ToolHex — a set of tools (image compressor/converter, PDF merge/split/compress, JPG→PDF, text tools, calculators) where the browser does everything. Your files never leave your device. There's no backend at all.
Here's how the interesting parts work.
The browser is a pretty good runtime
Most people don't realize how capable the web platform got:
// Canvas API: decode any image, re-encode at any quality
const bitmap = await createImageBitmap(file);
canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h);
canvas.toBlob(blob => saveIt(blob), 'image/jpeg', 0.8);
That's the core of the image compressor. No library needed for the happy path — the browser IS the image library.
mozjpeg in WASM for real JPEG quality
The canvas encoder is fine, but mozjpeg produces ~20% smaller JPEGs at the same quality. It's a C library — running it in the browser means WebAssembly. I vendored the @jsquash/jpeg codec and call it lazily:
const { default: encode } = await import('/assets/vendor/jsquash-jpeg/encode.js');
const buf = await encode(imageData, { quality: 80 });
Self-hosted, ~290 KB of wasm, cached after first use.
PNG is lossless — until you quantize the palette
This one surprised me while building. "PNG compression" tools like TinyPNG don't re-encode losslessly — they do lossy palette quantization (pngquant-style): reduce a photo to ≤256 colors with dithering, then write an indexed-color PNG. The file stays a real PNG, transparency intact, 60–85% smaller.
I did it with UPNG.js + a Floyd–Steinberg dithering pass against the computed palette (with a 5-bit/channel lookup table so nearest-color search stays fast):
const q = UPNG.quantize([rgba.buffer], colors, false);
// extract palette -> dither each pixel against it ->
// UPNG.encode([dithered], w, h, colors) => indexed PNG
Banding without dithering is ugly; with error diffusion a 256-color photo looks nearly identical to the original. Fun rabbit hole.
Ghostscript. In the browser. In WASM.
The PDF compressor is the one that felt impossible: real PDF compression needs a real PDF engine. There's a WASM build of Ghostscript (@okathira/ghostpdl-wasm, ~15 MB — lazy-loaded, cached) with an in-memory filesystem and a callMain API. Which means I can literally run:
Module.FS.writeFile('in.pdf', bytes);
Module.callMain([
'-sDEVICE=pdfwrite', '-dPDFSETTINGS=/ebook',
'-dNOPAUSE', '-dQUIET', '-dBATCH',
'-sOutputFile=out.pdf', 'in.pdf'
]);
const compressed = Module.FS.readFile('out.pdf', { encoding: 'binary' });
The same Ghostscript command that powers server-side tools — except the "server" is the user's own CPU. In benchmarks on my machine, a 19.6 MB scan-heavy PDF compresses to 0.15 MB in ~1.7 seconds.
(One heads-up if you copy this: Ghostscript is AGPL v3. Conveying the binary means shipping the license + source link, which I do on a licenses page. Not legal advice, but read the license before embedding it anywhere commercial.)
PDF surgery with pdf-lib
Merge, split, rotate, delete pages, JPG→PDF — all pdf-lib. Its mental model is simple: load documents into memory, copy page objects between them, save. Lossless by construction since pages are never re-rendered:
const out = await PDFLib.PDFDocument.create();
const pages = await out.copyPages(src, src.getPageIndices());
pages.forEach(p => out.addPage(p));
The infra is one nginx file
Every tool lives on its own subdomain (compress.toolhex.online, pdf.toolhex.online…), which keeps each site focused and SEO-clean. One regex server block routes all of them:
server_name ~^(?<sub>[a-z0-9-]+)\.toolhex\.online$;
root /var/www/toolhex/$sub;
No if blocks, unknown subdomains 404 naturally because their docroot doesn't exist. Wildcard cert from Let's Encrypt via DNS-01 (HTTP-01 can't do wildcards). Deploys are rsync + nginx -t && systemctl reload nginx from a tiny script.
What I learned
- "No upload" is a feature users understand instantly. It's on every page and people genuinely care.
- WASM quietly changed what "client-side" means. mozjpeg and Ghostscript in a browser tab would have sounded absurd a few years ago.
- Client-side ≠ simple. Adaptively stepping quality down until a file actually shrinks, palette quantization, PDF object copying — the edge cases are real.
If you want to see it: toolhex.online — everything's free, no signup, and (obviously) nothing to upload. The image compressor and PDF compressor are the two I'd start with.
Happy to answer questions about the WASM integration or the nginx setup in the comments. And if you have an idea for a tool that belongs in this family, I'm building more.
Top comments (0)