DEV Community

Nawal Shrestha
Nawal Shrestha

Posted on Originally published at getsnapshrink.com

I Added Client-Side HEIC to JPG Conversion to My Image Tool — No Server, No Upload

Most "free HEIC to JPG" tools work the same way: you upload your iPhone photos to someone's server, it converts them, you download the result. For a format conversion — one of the most local operations imaginable — that's a strange trade.

I wanted HEIC to JPG support in SnapShrink (my in-browser batch image compressor) without breaking its core promise: nothing ever uploads. Here's how I did it.

The decoder: heic2any, lazy-loaded

Browsers can't decode HEIC natively (unlike AVIF, which Chrome/Edge/Firefox handle via createImageBitmap). So the conversion needs a real decoder. I used heic2any, pinned to a fixed version, loaded via dynamic import() only when a HEIC file is actually dropped.

That last part matters: the decoder is a chunk of WASM/JS you don't want in the initial bundle for the 95% of users who never touch HEIC. Lazy-loading keeps the first paint fast for everyone.

The pipeline

Once decoded, a HEIC file is just pixels — it flows into the exact same pipeline as every other format:

  1. Decode → ImageData/canvas
  2. Quality slider or Target KB mode (binary-search the quality setting until the output fits)
  3. Encode to JPG/PNG/WebP, batch, ZIP download

No HEIC-specific special cases downstream. That was the whole point of the architecture: new input formats are a decode step, not a new app. AVIF support later followed the same path — same idea, native decode.

The UX details that matter

  • Files get a small "HEIC" badge in the drop list so users can see what was detected.
  • A misnamed file (.heic that's actually a JPEG) gets a clear error instead of a cryptic decoder failure.
  • Target KB mode works on HEIC too — type 100, get ≤100KB. This is the feature people actually write in about: forms with hard size caps. (How it works.)

Try it

Drop a real iPhone photo on getsnapshrink.com/heic-to-jpg/ and watch DevTools → Network while it converts. Zero file uploads — that's the entire architecture, and HEIC didn't change it.

First 10 images per session free, unlimited is a one-time $9. Happy to answer questions about the implementation in the comments.

Top comments (6)

Collapse
 
omyvnss profile image
Om Yaduvanshi •

nice. one builder's question on the misnamed-file error: does it sniff magic bytes or just the extension? the files people misname are exactly the ones an extension check misses.

Collapse
 
nawalshrestha profile image
Nawal Shrestha •

Good catch — honest answer: no magic-byte sniffing. Routing is MIME type first, .heic/.heif extension as fallback.

The misnamed-file error fires one step later: heic2any attempts to parse the container, and if the ISO-BMFF structure isn't there it throws, which surfaces as "not a valid HEIC/HEIF image (despite the .heic/.heif name)". So a JPEG renamed to .heic gets caught at decode and skipped with a clear message while the batch continues.

The gap is the reverse: a real HEIC named .jpg never reaches the HEIC path at all. A small ftyp sniff on intake would close that — it's on the list now. Sharp question, thanks.

Collapse
 
omyvnss profile image
Om Yaduvanshi •

honestly the ftyp sniff replaces the whole mime-first chain, not just the .jpg edge. headers lie, extensions lie, ftyp boxes mostly don't. cheap too, first 12 bytes off a slice.

Thread Thread
 
nawalshrestha profile image
Nawal Shrestha •

You're right — ftyp up front covers the whole chain, not just the .jpg edge. MIME-first was just the least code that worked when most uploads were clean files. 12 bytes off a slice is cheap, so I'll lead with the ftyp sniff and keep MIME/extension as fallback. Will update the post once it ships. Thanks for the nudge.

Thread Thread
 
omyvnss profile image
Om Yaduvanshi •

looking forward to the updated post. nice how the fallback ordering flips too, mime/extension stops being load-bearing the second ftyp leads.

Thread Thread
 
nawalshrestha profile image
Nawal Shrestha •

Shipped. The ftyp-first detection is live in production now: first 12 bytes off a slice, brand check against the HEIC family, MIME/extension sitting behind as fallback exactly like you described. Updating the post with the details today. Thanks again for the nudge, strictly better than the MIME-first version.