If you've ever tried to open a photo taken on an iPhone on a
Windows PC, you've probably run into the HEIC format — Apple's
default image format since iOS 11. It's efficient, but almost
nothing outside the Apple ecosystem can open it natively.
Most online converters solve this by uploading your file to a
server, converting it there, and sending it back. That works,
but it means your photos — sometimes personal ones — are
leaving your device.
I wanted to build something different: a converter that runs
entirely in the browser, with zero uploads. Here's how I
did it with WebAssembly.
Why WebAssembly?
The HEIC format is complex. Decoding it requires a C library
called libheif, which isn't natively available in JavaScript.
But thanks to WebAssembly, we can compile C/C++ code and run
it directly in the browser at near-native speed.
The library I used: libheif.js — a WebAssembly port of
libheif that runs entirely client-side.
The Basic Flow
// 1. User drops a .heic file
// 2. Read it as ArrayBuffer
// 3. Pass to libheif.js for decoding
// 4. Get raw pixel data back
// 5. Draw on Canvas
// 6. Export as JPG via canvas.toBlob()
async function convertHEICtoJPG(file) {
const buffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(buffer);
// Initialize libheif decoder
const decoder = new libheif.HeifDecoder();
const data = decoder.decode(uint8Array);
// Get first image
const image = data[0];
const width = image.get_width();
const height = image.get_height();
// Draw to canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
await new Promise((resolve, reject) => {
image.display(
{ data: ctx.createImageData(width, height),
width, height },
(displayData) => {
if (!displayData) return reject('Failed');
ctx.putImageData(displayData, 0, 0);
resolve();
}
);
});
// Export as JPG blob
return new Promise(resolve => {
canvas.toBlob(resolve, 'image/jpeg', 0.92);
});
}
The Privacy Advantage
Since everything runs in the browser:
- ✅ No file is ever uploaded to a server
- ✅ Works offline after first load
- ✅ No file size limits imposed by server costs
- ✅ Conversion speed depends on the user's device, not yours
This matters more than people think. HEIC files are often
personal photos. Users shouldn't have to trust a random server
with them.
Handling Multiple Files
Batch conversion is straightforward — just iterate and
trigger downloads:
async function convertBatch(files) {
for (const file of files) {
const blob = await convertHEICtoJPG(file);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name.replace(/\.heic$/i, '.jpg');
a.click();
URL.revokeObjectURL(url);
}
}
Performance Considerations
WebAssembly is fast, but HEIC decoding is still CPU-intensive.
A few things I learned:
- Web Workers: Move the decoding off the main thread to avoid freezing the UI
- Progress feedback: For large files or batches, show progress — users will wait if they know something is happening
-
Memory management: Call
image.free()after each conversion to avoid memory leaks with large batches
The Result
The converter is now live at
AwesomeToolkit.com/tools/heic-to-jpg
as part of a larger free toolkit I'm building.
It handles single files, batch conversion, and outputs either
JPG or PNG — all without a single byte leaving your browser.
What's Next
I'm applying the same approach to other conversions that can
run client-side:
- WebP ↔ JPG/PNG (using Canvas API directly)
- PNG to ICO/favicon (multi-resolution, no server needed)
- Image compression (using browser-native APIs)
For heavier operations like video compression or PDF
processing, I'm using FFmpeg.wasm — same concept, different
library. I'll write about that next.
If you're building something similar or have questions about
the libheif.js implementation, drop a comment — happy to help.
Top comments (0)