← Writing

An image converter that never uploads your image

ImagesBrowserTooling

Search "convert PNG to WebP" and you'll get a wall of free online tools. Almost all of them work the same way: you hand over your image, their server does the work, and you download the result. For a meme, fine. For a screenshot of something you'd rather not, a photo of a document, a product mockup under NDA — you've just uploaded it to a stranger's box, and you have no idea how long it lives there.

The thing is, none of that upload is necessary. Browsers have shipped everything you need to convert and compress images locally for years. So I built a converter that does, where the image never leaves the tab. Here's how it works and where the sharp edges are.

The whole engine is three browser APIs

There's no library here. The entire conversion is:

const bitmap = await createImageBitmap(file)   // decode
const canvas = document.createElement("canvas")
canvas.getContext("2d").drawImage(bitmap, 0, 0, w, h)  // draw (and resize)
canvas.toBlob(cb, "image/webp", 0.8)           // re-encode

createImageBitmap decodes whatever the browser can already display — PNG, JPEG, WebP, GIF. Drawing it to a canvas at a chosen width and height does your resize for free. canvas.toBlob re-encodes to the format and quality you ask for. Decode, draw, encode — that's the tool. Resizing is just passing smaller dimensions to drawImage; the longest edge gets capped and the aspect ratio is preserved.

That it's all built in is the whole point: nothing to upload because the browser is already a perfectly good image codec.

Two things that will quietly bite you

The happy path is short. The two bugs worth knowing about are both about formats lying to you.

WebP doesn't always encode. Safari couldn't write WebP until fairly recently (it could read it). The nasty part is the failure mode: canvas.toBlob with an unsupported type doesn't throw — it silently falls back to PNG. So a user picks "WebP," gets a file that's actually PNG with a .webp name, and wonders why it's huge. I probe support up front and just hide formats the browser can't produce:

function canEncode(format: OutputFormat): boolean {
  if (format === "image/png" || format === "image/jpeg") return true
  const c = document.createElement("canvas")
  c.width = c.height = 1
  return c.toDataURL(format).startsWith(`data:${format}`)
}

If toDataURL hands back a data:image/png when you asked for WebP, the browser is telling you it can't, and the option disappears from the dropdown.

JPEG has no alpha channel. Export a transparent PNG to JPEG and every transparent pixel comes out black, because that's what an empty canvas reads as. The fix is one line — fill the canvas white before drawing, but only for JPEG:

if (opts.format === "image/jpeg") {
  ctx.fillStyle = "#ffffff"
  ctx.fillRect(0, 0, width, height)
}
ctx.drawImage(bitmap, 0, 0, width, height)

WebP and PNG keep their transparency; JPEG gets a clean white background instead of black gunk.

Being honest about the tradeoff

The canvas encoder is not the best image compressor in the world. A dedicated encoder like MozJPEG, or a WebAssembly build of one, will squeeze more out of the same quality setting. If you're optimizing a hero image to the last kilobyte, reach for those.

What the canvas gives you instead is instant, private, and zero-setup: drop a file, see the new size, download it — no upload round-trip, no account, no queue, nothing leaving your machine. For the everyday job of "make this PNG smaller" or "I need a JPEG, not a HEIC," that trade is the right one almost every time. The tool says as much on the page, because leading people toward the wrong tool to look clever helps nobody.

Why this keeps being the pattern

This is the third thing in the Lab built on the same bet: the work that doesn't actually need a server shouldn't have one. A Markdown compressor, an SVG tracer, now an image converter — all of them run entirely in your browser, and "your input never leaves the page" isn't a privacy footnote, it's the reason to use them over the ad-riddled upload-it-here alternatives.

Browsers got quietly powerful while everyone was still POSTing files to servers out of habit. Most of the time, the upload was never the point. </content>