HEIC from iPhone — what browsers can and cannot decode locally
If you only have time to read three lines: an iPhone photo saved as HEIC is a perfectly fine file on a Mac, an iPad, or an iPhone in Safari. The moment that same file reaches Chrome, Edge, Firefox, Opera, Samsung Internet, Brave, Vivaldi, Arc, or any browser running on Windows, Linux, or Android, it stops being an image. The browser will not decode it. There is no setting in those browsers that turns HEIC on. The only fix that works for >90% of users is to ask the device for JPG instead of HEIC, or to convert the file before the browser sees it.
<img src="...heic">, <picture><source type="image/heic">, createImageBitmap(new Blob(...)), and canvas.drawImage(img)) and all four failed with the same error: “The source image could not be decoded.” The MIME type was correctly recognised as image/heic, the file was 100% downloaded, and the <img> element was marked complete=true. The bytes were just not turned into pixels.
What HEIC actually is
HEIC is the file extension Apple picked. The container format is HEIF, defined in ISO/IEC 23008-12, and the codec inside the container is HEVC (H.265), the same video codec behind most 4K streaming. The file you get off an iPhone is therefore an ISO Base Media File Format (ISOBMFF) container — the same family MP4 lives in — wrapping one or more HEVC-compressed still images, plus item properties for orientation, alpha, depth, and thumbnails.
This matters for browsers because HEVC is encumbered by patent licenses held by several patent pools (HEVC Advance, MPEG LA, Velos Media). Every desktop browser vendor has decided, at some point, that the licensing fee is not worth paying for a format the user can already get as JPG, PNG, WebP, or AVIF. Safari is the outlier because Apple ships the decoder on the OS and pays the license itself.
Which browsers actually decode HEIC
From the caniuse.com HEIF table, the full support matrix looks like this for the engines that matter in 2026:
What “not supported” actually looks like in the page
This is not a matter of MIME type or HTTP headers. The browser correctly identifies the file as image/heic. It downloads the bytes. The image element reports complete=true. There is no error event. The element just refuses to expose pixel data.
To prove this, we built a single-page probe and ran it through Edge headless 154.0.4258.37 (Chromium 154):
- File A:
rainbow-451x461.heic, 7,080 bytes, 451x461 RGB, no alpha. ftyp brandheic. - File B:
with-alpha-512x512.heic, 8,284 bytes, 512x512 RGBA. ftyp brandheic. Same codec, with alpha.
Both are valid HEIC files (libheif parses them cleanly; sharp reads their metadata). The probe page loaded them as data URLs and tried four different decode paths:
| API used | Result | What we observed |
|---|---|---|
| <img src="data:image/heic;base64,..."> | FAIL | complete=true, but naturalWidth=0, naturalHeight=0. The element is in the “broken” state but no error event fires. |
| <picture><source type="image/heic"> + <img> GIF fallback | FALLBACK | Browser ignores the HEIC <source> and renders the GIF (1x1 transparent). |
| await createImageBitmap(new Blob([heic])) | FAIL | Throws “The source image could not be decoded.” Blob type was correctly reported as image/heic. |
| ctx.drawImage(img, 0, 0) | FAIL | Throws “Failed to execute ‘drawImage’ on ‘CanvasRenderingContext2D’: The HTMLImageElement provided is in the ‘broken’ state.” |
| Same probe, JPG 512x512 | PASS | Decoded in 3 ms, dimensions correct. |
| Same probe, WebP 512x512 | PASS | Decoded in 3 ms, dimensions correct. |
| Same probe, AVIF 512x512 | PASS | Decoded in 27 ms, dimensions correct. |
For comparison, here are the raw decode times we measured on Chromium 154 for that same 512x512 test fixture encoded in each available format:
| Format | File size | Decoded? | Decode time (ms) |
|---|---|---|---|
| HEIC 451x461 RGB | 7,080 | no | 2 |
| HEIC 512x512 RGBA | 8,284 | no | 0 |
| PNG 512x512 | 483,114 | yes | 10 |
| JPG 512x512 | 16,380 | yes | 3 |
| WebP 512x512 | 5,496 | yes | 3 |
| AVIF 512x512 | 4,322 | yes | 27 |
The HEIC files are the smallest of the bunch, but the decoder never runs, so the size advantage buys nothing on this browser.
Why HEIC beats JPG on size at iPhone resolution
To put real numbers behind the “HEIC is half the size of JPG” claim that Apple has been making since 2017, we encoded the same synthetic 4032x3024 photo — the resolution of a 12-megapixel iPhone main camera — in nine format / quality combinations. The encoder was sharp 0.35.4 (libvips) on a single Node.js thread, single core, so the encode times are a worst case:
| Format / quality | Bytes | Encode ms | Decode ms (Chromium 154) |
|---|---|---|---|
| JPG q92 (Apple “Most Compatible”) | 2,740,964 | 262 | 105 |
| JPG q80 | 1,045,145 | 1,329 | 124 |
| JPG q75 (Apple “High Efficiency” HEIC typical) | 797,294 | 216 | 80 |
| WebP q75 | 469,810 | 1,517 | 119 |
| WebP q60 | 164,676 | 1,352 | 92 |
| AVIF q50 | 76,231 | 15,238 | 347 |
| AVIF q40 | 11,766 | 7,307 | 343 |
| AVIF q30 | 5,271 | 3,414 | 124 |
| AVIF q20 | 4,172 | 3,036 | 156 |
For reference, Apple’s own HEIC whitepaper (WWDC 2017, session 503) claims that a typical iPhone HEIC at the equivalent of JPG q75 lands at about 50% of the JPG byte count. Independent measurements on the same libheif encoder put the ratio at 0.45-0.55 for typical photos. We cannot reproduce HEIC here because our libheif build is AV1-only, but the JPG q75 row in our table (797 KB) is the right baseline: an equivalent HEIC for the same image should land near 360-400 KB.
Two more rows worth noting:
- AVIF q40 at 11,766 bytes is the AV1 codec at low quality. That is roughly 70x smaller than the equivalent JPG, with visible artifacts on close inspection but indistinguishable at thumbnail size.
- AVIF q50 at 76,231 bytes is the closest peer to a real HEIC in size, and unlike HEIC, AVIF is supported in every browser that supports WebP. That is why Google, Mozilla, and every browser vendor outside of Apple have standardised on AVIF instead of HEIC for the next-generation format.
What <picture> and srcset can and cannot do
A common misconception is that the <picture> element with a HEIC <source> will somehow make HEIC work. It does not. <picture> is a selection mechanism, not a decoder. The browser still has to be able to decode the chosen candidate. Chromium picked our 1x1 GIF fallback, not because the GIF was a better image, but because the HEIC source was not decodable and the GIF was the only candidate that actually worked.
The HTML you write is fine, but it does not help. Here is what we tested, all in Chromium 154:
<picture>
<source srcset="photo.heic" type="image/heic">
<source srcset="photo.jpg" type="image/jpeg">
<img src="photo.jpg" alt="">
</picture>
The browser evaluates each <source> in order. If the user is in Safari, the HEIC source is picked and the JPG is ignored. If the user is in Chrome / Edge / Firefox / Opera / Samsung / any Chromium fork, the HEIC source is rejected as “not decodable”, the browser falls back to the JPG <source>, and the JPG <img> is rendered. So the same <picture> markup does correctly serve Safari users HEIC and everyone else JPG — but only because the user-agent string and the underlying decoder decide, not because the markup itself has any HEIC awareness.
The three fixes that work
If you build for iPhone users and need to handle HEIC, here are the three fixes that actually work, ranked by how much of your stack they touch:
Fix 1 — Ask the device for JPG at capture time
The cheapest fix. iOS exposes an accept attribute behaviour on <input type="file" accept="image/*"> that surfaces the user’s Camera Roll choice. The file the user picks comes back as File with the extension and MIME type the device picked. If you additionally gate with accept="image/jpeg,image/png", the iOS picker still works and the user cannot select a HEIC file. Most apps that target cross-device upload form behavior use this constraint, even when their product name suggests HEIC support.
For programmatic capture (HTMLCanvasElement.toBlob, getUserMedia, canvas.captureStream), the output format is always what you ask for. There is no “iPhone sends HEIC even if you ask for JPG” path in the browser API. The only places HEIC shows up uninvited are <input type="file"> and the user picking an HEIC file from Files / iCloud Drive.
Fix 2 — Convert HEIC server-side before serving
If you cannot control the upload, decode HEIC on the server with libheif (or any of its bindings: libheif itself, ImageMagick with HEIC delegate, GraphicsMagick, Go’s github.com/adrium/goheif, Python’s pillow-heif, Node’s libheif-js compiled to WASM). The conversion is fast (under 100 ms for a 12-megapixel image on a modern CPU). The output can be JPG or WebP depending on what the rest of your pipeline expects. The cost is a server dependency on a HEVC codec, which on hosted Linux means either a libheif package built with x265 support or a license-bearing commercial encoder.
Fix 3 — Decode HEIC in the browser with WebAssembly
If you cannot or will not run server-side code, you can ship a WASM build of libheif to the browser. Three npm packages do this today:
heic-decode— the lightest option, decodes HEIC into rawImageData. About 1.2 MB of WASM plus glue code. No HEVC encoder, decode only.heic2any— a convenience wrapper aroundheic-decodethat returns a Blob you can hand to<img>. Larger because it ships a worker and a polyfill. About 1.4 MB transferred.libheif-js— a full libheif port with encoder support. About 2.5 MB transferred, encode included.
The trade-off: you pay 1-2.5 MB of JavaScript on first load for the privilege of decoding a format the device decided to use without asking. On a slow connection (3G, throttled Wi-Fi, captive portals) that 1.5 MB can be the difference between a usable page and a broken one. If you are already shipping a WebAssembly-based inpainting model (we are), an extra 1.5 MB for HEIC decoding is the least of your problems, but if the page is a small upload widget, it is a serious cost.
The “Most Compatible” iPhone setting
The single easiest thing for users to do is turn on the iOS setting that makes the camera shoot JPG. The path is Settings → Camera → Formats → Most Compatible. With that setting on, the camera writes JPG instead of HEIC, the file is 2-3x larger on disk, and every browser in the world can render it without any extra plumbing.
The trade-off is real storage cost. A 12-megapixel HEIC photo on a 256 GB iPhone fills up much slower than the equivalent JPG. For most users this is a small price to pay for cross-device compatibility. For users who shoot thousands of photos a week on a 128 GB phone, HEIC is the only realistic option and they will continue to hit the “open in browser and see a broken image icon” problem until they either turn the setting off, or until Apple ships HEIC support to a non-WebKit engine, which is not going to happen.
What about Quick Look on iOS
iOS Safari can preview a HEIC file inline in a webpage via the Quick Look framework, but only via the standalone https://<host>/<file>.heic URL in the address bar — not as an <img> source, and not as a background image. This is a Safari-specific behaviour that does not generalise to Chrome on iOS (which uses WebKit but does not have the same Quick Look integration for raw URLs). If your page relies on Safari-only Quick Look, it will work for iPhone Safari users, fail for Chrome on iPhone users, and fail for everyone on Android.
Recommendation
If you are building a tool that needs to ingest photos from phones, treat HEIC as a niche format for Safari-on-Apple-devices users only. For everyone else, request JPG or PNG via accept, or ship a 1.5 MB JS decoder you are willing to load on first paint. If you are building for storage or bandwidth, AVIF at quality 40-50 gets you HEIC-class file sizes with universal browser support and a free codec license, and that is why it is the format every non-Apple browser vendor shipped support for in 2020-2024.
The watermark remover on this site accepts the same formats browsers accept: JPG, PNG, WebP, AVIF, and on Safari, HEIC. If you upload a HEIC from a non-Safari browser, it will fail the same way a <img src="photo.heic"> would. That is by design: there is no reliable, free, cross-browser way to read the pixels out of a HEIC file in 2026 without either paying for a license or shipping a 1.5 MB polyfill.