runlocally

runlocally engineering notes

WebP to JPG

How WebP to JPG is built

By Geppetto · · Open WebP to JPG →

These are the engineering notes for WebP to JPG: the technologies it is built on, what each one is, and how it is used in the tool. Where new pieces appear they get a proper introduction; ones covered in earlier notes are referenced rather than re-explained.

Tech used

The format: WebP, decoded by the browser

WebP is Google’s image format — pixels coded with VP8 / VP8L (the intra-frame coding from the VP8/VP9 video lineage) inside a RIFF container. The decisive fact for this tool: every modern browser already decodes WebP. So, unlike HEIC — whose HEVC-coded pixels needed libheif compiled to WebAssembly — there is no decoder library and no WASM here. The browser is the decoder.

Decoding with an <img> and an object URL

The decoder is an <img> element. URL.createObjectURL(file) mints a short-lived URL pointing at the in-memory file Blob; assigning it to img.src makes the browser decode the WebP, and the load event fires with naturalWidth / naturalHeight ready to read. The object URL is revoked once the image is decoded.

Encoding with the Canvas 2D API

A <canvas> is the encoder. The decoded image is painted onto a canvas with getContext('2d').drawImage(img, …), and canvas.toBlob(callback, type, quality) returns a JPEG or PNG Blob. That same drawImage call also resizes — mapping the source rectangle onto a smaller destination rectangle with imageSmoothingQuality: 'high' — and the JPEG quality argument (default 0.92) is passed straight through to the encoder.

Shell: Astro, Preact, Service Worker

The static Astro + Preact island shell and the Service-Worker PWA are shared across these tools and were introduced in the HEIC notes — see there for how the island hydrates and how the Service Worker precaches the app shell for offline use. What’s worth pointing out here is what’s absent: no Web Worker, no OffscreenCanvas, no WASM. A native decode and a canvas encode are cheap enough to run on the main thread.

Implementation & operational notes

White matte for JPEG. JPEG has no alpha channel, so drawing a transparent WebP straight to JPEG would composite the transparency to black. For JPEG output the canvas is filled white before drawImage; PNG output keeps the alpha.

Animated WebP yields one frame. Decoding through an <img> produces only the poster/first frame, so an animated WebP converts to a single still — a deliberate limitation of the <img> route.

Orientation comes for free. Because the browser decodes the <img>, any embedded orientation is already applied; there is no separate rotation pass.

A simpler support story than HEIC. Native <img> decode plus a plain DOM <canvas> works on older Safari with no OffscreenCanvas dependency — the Worker / OffscreenCanvas fallback the HEIC tool needs simply isn’t required here.

Same routing and caching as the rest. The tool is physically nested under /webp-to-jpg/ (no Astro base) and scopes the long-lived immutable cache to /webp-to-jpg/_assets/* only — the same Cloudflare Pages routing and _headers traps written up in the HEIC notes.

Try it / source