close

DEV Community

Kashif A. Khan
Kashif A. Khan

Posted on

73 file tools, zero uploads: what actually breaks when you do everything in the browser

Every online PDF and image tool works the same way. You pick a file, it goes to their server, something happens, you get it back. The convert-a-file business is enormous and it is built entirely on you handing over the document first.

That bothered me, because the documents people convert are exactly the documents they would not email to a stranger. CVs. Contracts. Bank statements. Passport scans, for visa applications. The rivacy policy says the file is deleted after an hour, and maybe it is, but you have already sent it.

So I built Utilade (https://utilade.com) the other way round: the file never leaves your machine. It is 73 tools now — PDF and image — and the browser does all of the work.

It mostly works. What follows is the part nobody writes up: the things that broke, and what they cost.

The page freezes and it is not the algorithm

The first version rendered a PDF page to a canvas on the main thread. On my laptop it felt instant. On a mid-range Android phone it froze the page for "3,733 milliseconds".

Not slow 'frozen'. No scroll, no taps, no spinner animating, because the spinner needs the same thread the decode is sitting on. Nearly four seconds of a page that looks crashed.

Moving it into a Web Worker took the frozen time to "150 ms". The actual work did not get much faster; it stopped happening where the user could feel it.

The thing worth taking away: "my desktop numbers were not just optimistic, they were the wrong measurement entirely." A fast machine hides main-thread blocking completely. If you are doing heavy work in a browser, throttle the CPU 6× in DevTools before you believe anything.

The redaction that was not redacting

The blur tool lets you cover a face or a number plate. Solid box, blur, or pixelate.

Pixelation shrinks the region and blows it back up with smoothing off:

js
ctx.imageSmoothingEnabled = false;
ctx.drawImage(region, 0, 0, small.w, small.h);
ctx.drawImage(small, 0, 0, region.w, region.h);

That looks right. It is what a lot of tutorials show. And it is wrong in a way that matters, because "nearest-neighbour sampling takes one source pixel per block, not the average of the thousands underneath it."

So each fat pixel was a real, un-averaged pixel out of the original photograph. On a number plate or an eye, that is not a redaction, it is a lossy sample of the thing you were trying to destroy. It now computes alpha-weighted block means explicitly.

While I was in there I made the tool say the other quiet part out loud: "blur and pixelation are reversible in principle, and pixelated redactions have been recovered in the real world." For a passport number, use the solid box, which replaces the pixels outright. A privacy tool that lets you feel safe while being recoverable is worse than no tool at all.

Android would not show the photo, and the bytes were perfect

A user cropped a photo on his phone. The download completed. The picture never appeared in his gallery, and Android offered to open it with 'Drive PDF Viewer'.

The bytes were a flawless JPEG. The filename ended in '.jpg'. But the Blob was labelled 'application/pdf' a default left over from when the project was PDF-only and "Android's media scanner files by content type, not by extension." So it indexed a perfectly good photograph as a document.

Nothing in a green test suite looked at 'blob.type'. Not one assertion. The regression test does now.

A test that counted more photograph than there was

This one is my favourite, because the code was right and the test was lying.

The meme generator has a caption layout that grows the canvas and puts text on a bar 'above' the picture, so none of the photo is covered. The test asserts exactly that: every one of the fixture's 800 × 600 pixels must still be present.

It failed, reporting "480,004" pixels of a 480,000-pixel photograph.

Four more than existed. The fixture was flat mid-grey, '#808080', and the counter looked for exactly 'rgb(128,128,128)'. Anti-aliasing black caption text against a white bar walks 'r === g === b' all the way from 255 down to 0 and four of those pixels landed precisely on 128 and got counted as photograph.

The fixture is now a colour with three different channels. Black-on-white anti-aliasing always produces equal channels, so it cannot forge one. The assertion stayed exact; it just stopped being ambiguous.

"A test fixture can be a false positive as easily as it can be a false negative." If your oracle is "count pixels of this colour", pick a colour your rendering cannot accidentally produce.

Rendering the original on every keystroke

The most recent one, and the most embarrassing, because it is not exotic.

Two tools drew their live preview by going back to the original file each time something changed: read the whole file, decode at full resolution, draw at full resolution, encode a full-size PNG, swap the blob URL. Per keystroke. On a 12 megapixel photo.

The fix is obvious once you see it: make "one" downscaled copy when the file is chosen and redraw from that. The debounce then dropped from 250 ms to 90 ms, because it was finally waiting for typing to settle rather than for a decode.

One catch worth knowing if you do this. The text size was a 'share of the picture width', so it survived the downscale untouched but the outline width and shadow were in "pixels", so a 1200 px preview showed a chunky outline the full-size download would never have. Anything measured in absolute pixels has to be scaled by the same ratio, or your preview stops being a preview and becomes a different picture.

Where the browser genuinely loses

I would rather say this than be caught out on it.

"Office conversion needs a server." Turning .docx into a PDF means LibreOffice, and LibreOffice does not run in a tab. Those three tools upload, they say so on the page, the privacy badge is removed from them specifically, and the privacy policy names the exception. A blanket "we never upload" claim across the whole site would have been false the day the first one shipped.

The rest of the honest list: heavy OCR, real PDF repair, and very large files on weak phones, where browser memory limits are real and unforgiving.

Was it worth it

The engineering is harder. There is no server log to check when something breaks on someone else's phone, browsers disagree about image encoders, and every performance problem is somebody else's hardware.

What you get back: no infrastructure bill that scales with usage, so there is no reason to impose the daily caps and file-size limits every competitor has. And a privacy claim you can actually verify open DevTools, watch the network tab, convert a file, see nothing leave.

That last one is the whole point. Not a promise about what happens to your file on a server. "No server."

If you want to poke at it: [compress an image](https://utilade.com/compress-image), blur something out of a photo, or merge a few PDFs. Network tab open, ideally that is the part I would like you to check.

Happy to answer anything about the implementation in the comments.

Top comments (0)