If you've ever built a file upload feature, you've probably hit this: a user uploads a photo from their iPhone, and your app breaks. No preview, no compression, just a silent failure — because the file is .heic and browsers can't read it.
HEIC is Apple's default photo format since iOS 11. Every iPhone photo taken today is HEIC. And virtually every JS image compression library just ignores the problem.
I spent a weekend building PixSqueeze to fix that.
The problem with HEIC in the browser
Browsers use the <canvas> API to compress images. You draw the image onto a canvas, call canvas.toBlob(), and get a compressed file back. Clean, client-side, zero server cost.
The problem: canvas.drawImage() only works if the browser can decode the image first. And no major browser can decode HEIC natively — not Chrome, not Firefox, not even Safari on macOS (Safari on iOS can, but that doesn't help your web app).
So when a user picks a HEIC file, your image element fires onerror, your canvas stays blank, and your compression pipeline silently does nothing.
The solution: server-side conversion, then client-side compression
PixSqueeze handles this in two stages:
Stage 1 — Server converts the format
HEIC (and TIFF, camera RAW) files get sent to a small Express server. The server uses heic-convert running in a dedicated worker thread to convert to JPEG. Worker threads matter here — HEIC decoding is CPU-intensive WASM work, and running it on the main event loop would block every other request.
Stage 2 — Client compresses the result
The converted JPEG comes back to the browser, where the normal canvas-based compression pipeline takes over. Quality, resize, watermark hooks — all the usual options.
The detection is important too. You can't just check file.type — iOS often sets HEIC files with an empty MIME type. PixSqueeze checks the ISO Base Media File Format magic bytes directly:
async function isHeicFile(file) {
const buffer = await file.slice(0, 12).arrayBuffer();
const bytes = new Uint8Array(buffer);
// ftyp box at offset 4, brand starts at offset 8
const brand = String.fromCharCode(bytes[8], bytes[9], bytes[10], bytes[11]);
return ['heic', 'heix', 'hevc', 'hevx', 'mif1', 'msf1'].includes(brand.toLowerCase());
}
Getting started
Install it:
npm install pixsqueeze
Basic usage — compress any image before upload:
import PixSqueeze from "pixsqueeze";
new PixSqueeze(file, {
quality: 0.6,
success: (result) => uploadToServer(result),
error: (err) => console.error(err.message),
});
Full HEIC pipeline — detect, convert on server, compress:
import PixSqueeze from "pixsqueeze";
async function handleFile(file) {
// Convert HEIC on server if needed
const resolvedFile = (await isHeicFile(file))
? await convertOnServer(file, "/api/convert/heic")
: file;
// Compress client-side as usual
new PixSqueeze(resolvedFile, {
quality: 0.6,
success: (result) => console.log("Ready:", result),
error: (err) => console.error(err.message),
});
}
async function convertOnServer(file, endpoint) {
const formData = new FormData();
formData.append("file", file);
const res = await fetch(endpoint, { method: "POST", body: formData });
const blob = await res.blob();
return new File([blob], file.name.replace(/\.\w+$/, ".jpg"), { type: "image/jpeg" });
}
The bundled server (npm run server) exposes three endpoints:
-
POST /api/convert/heic— HEIC/HEIF → JPEG -
POST /api/convert/tiff— TIFF → JPEG (including multi-page) -
POST /api/convert/raw— Camera RAW → JPEG (.cr2, .nef, .arw, .dng, and more)
Other things it can do
Resize while compressing:
new PixSqueeze(file, {
maxWidth: 1280,
maxHeight: 1280,
quality: 0.7,
success: (result) => console.log(result),
});
Add a watermark:
new PixSqueeze(file, {
drew(context, canvas) {
context.font = "bold 2rem sans-serif";
context.fillStyle = "rgba(255,255,255,0.6)";
context.fillText("© Your Brand", 20, canvas.height - 20);
},
success: (result) => console.log(result),
});
Convert to grayscale:
new PixSqueeze(file, {
beforeDraw(context) {
context.filter = "grayscale(100%)";
},
success: (result) => console.log(result),
});
Try the live demo
There's a playground at avlisodraude.github.io/compressme — drop any image (including HEIC if you have one) and see compression live.
Links
- npm: npmjs.com/package/pixsqueeze
- GitHub: github.com/avlisodraude/compressme
- Demo: avlisodraude.github.io/compressme
Single-image compression is free forever. Batch processing is coming — follow @pixsqueeze for updates.
Built with heic-convert, sharp, and the browser Canvas API.
Top comments (0)