sharp's prebuilt binary reads HEIC metadata, then can't decode it
I wanted to know whether a small store backend could take HEIC photos from sellers and turn them into JPEG with nothing but sharp. On 2026-11-06 I tested the prebuilt sharp 0.34.5 from npm on an Apple M4 Mac, and the ans
I wanted to know whether a small store backend could take HEIC photos from sellers and turn them into JPEG with nothing but sharp. On 2026-11-06 I tested the prebuilt sharp 0.34.5 from npm on an Apple M4 Mac, and the answer was half yes. metadata() read a HEIC file without complaint: format heif, 2016×1512, a 536-byte ICC profile, EXIF present, compression hevc. Then the decode failed with heif: Error while loading plugin: Support for this compression format has not been built in (11.6003). The libheif bundled in the prebuilt package only decodes AVIF, so HEVC-coded HEIC stops right there.
Scope first. The test files were HEIC images I encoded with macOS sips from a synthetic color chart and a CC0 Wikimedia Commons photo, tagged Display P3. They are not iPhone originals, so HDR gain maps, 10-bit data and nclx color descriptions are not covered. One machine, single runs. The pipeline part is a plan with assumptions, not something I have run in production.
A metadata check passes files the decoder cannot open
This is the part that could eat an afternoon. A typical small upload handler validates a file with metadata(), checks width, height and format, then hands the job to a worker. With this build a HEIC upload would pass validation and fail later in the worker, after the seller has already seen "upload complete". The same call already gives you the routing key, though. The compression field said hevc, so I would send heif plus hevc to a separate decode path. I have not checked what this build reports for an AVIF-coded HEIF, so the "AVIF goes straight through" branch is an assumption.
Four places the decode step could live
Option one is the seller's browser. WebKit 26.5 on macOS decoded the HEIC directly, while Chromium 149 could not decode it at all and createImageBitmap threw InvalidStateError. Going by these two engines, a canvas converter only covers the WebKit side (I didn't test retail Chrome or Safari), and the canvas export drops EXIF and GPS. For a storefront losing GPS is almost a feature. Losing the Chromium side is not.
Option two is a WASM build of libheif in the browser. I haven't built one, so this is the assumption: it fixes coverage but hands you the color management. In an earlier test on 2026-09-25, libheif returned raw P3 numbers with no color management, and writing them into a JPEG without the profile gave CIEDE2000 2.36 to 3.33 on colors sRGB can hold. This run hit the same mistake through Pillow's default save: 2.32 to 3.33 on those colors (skin tone 3.24) and up to 6.98 on saturated P3 red. As a rough rule, not a measurement, 2 to 3 shows up side by side.
For one-off files I used ImgIng https://imging.ai/ in a desktop browser on the same Mac: three conversions, no upload request in this run, one file at a time, and the JPEG kept the Display P3 profile and the EXIF, location included.
Option three is a Mac. sips -s format jpeg converted two files in one command and kept the P3 profile, capture time and GPS. sips only exists on macOS, so on a Linux box this only fits sellers who convert before uploading.
Option four is a hosted converter, and this is where I start counting. CloudConvert's pricing page, read on 2026-11-06, lists 10 free conversions per day, 1 GB per file and 5 concurrent jobs, with paid plans from 9 USD a month. Convertio lists a 1 GB maximum and free basic conversion, but I found no daily free limit on its site. Both process files on their servers. Ten a day covers my test traffic and nothing else.
A fifth path I have not tried is building libvips against a libheif with an HEVC decoder. Probably the cleanest server answer, and also a native build I would have to maintain myself.
After decode the sharp defaults mostly suit a store
Once sharp has pixels it can read (I fed it the PNG that sips decoded), the output option is the real choice. Default sharp converted to sRGB and wrote no ICC and no EXIF: in-gamut colors stayed within 0.37, saturated P3 got clipped, GPS was gone. keepIccProfile() kept the P3 profile and still dropped EXIF, which is what I would pick for product photos. keepMetadata() kept P3, EXIF and GPS, the one to avoid for seller uploads.
The three ready-made converters here, sips, Convertio and ImgIng, all kept the location. Stripping it is my job, wherever decoding ends up.
What I still don't know is how a real iPhone file behaves, since those often use nclx instead of an embedded ICC. Until I have one, the plan is: validate with metadata(), route hevc elsewhere, re-encode with keepIccProfile(), and test one real phone photo first.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.