Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 3 min read

Canvas-exporting a P3 photo: the colors survive, EXIF and GPS don't

The usual browser recipe for HEIC to JPEG is short: decode the file, draw it on a canvas, export it as image/jpeg. Two worries come with it: that a Display P3 photo comes out washed out because the canvas is sRGB, and th

The usual browser recipe for HEIC to JPEG is short: decode the file, draw it on a canvas, export it as image/jpeg. Two worries come with it: that a Display P3 photo comes out washed out because the canvas is sRGB, and that location data goes missing on the way. I came to scripting from content work, not CS, so instead of guessing I ran the recipe in two engines on 2026-11-06 and read what was inside the files.

The setup, so you know how far to trust it: one Apple M4 Mac, a single run each, Chromium 149 and WebKit 26.5 engines rather than retail Chrome and Safari. The main sample is a synthetic 2016Γ—1512 image with 12 color patches: eight saturated P3 colors outside sRGB and four inside it, like a skin tone. I encoded it to HEIC with macOS sips, with a 536-byte Display P3 profile, a capture time and a made-up GPS point in the Pacific. The second is the CC0 Wikimedia Commons photo "Avions Γ  l'aΓ©roport de Madrid - 2015", converted to P3. Neither is a straight-from-iPhone file, so HDR, 10-bit and nclx color descriptions are not covered. Color differences are CIEDE2000; by rule of thumb, not measurement, under 1 is hard to see and 2 to 3 shows side by side.

Chromium 149 could not decode the HEIC

The first surprise came before any color question: Chromium refused the file outright, with InvalidStateError and the message "The source image could not be decoded." WebKit 26.5 decoded the same HEIC directly, presumably through the macOS system decoder. For Chromium I cheated and fed it a P3 PNG that sips had decoded from the HEIC, profile intact. If your page accepts HEIC uploads, that alone decides whether a canvas pipeline works for Chrome users.

Both engines converted to sRGB on decode

With a default canvas, both engines converted the pixels to sRGB on decode, and the exported JPEG looked right. The in-gamut patches landed within Ξ”E2000 0.37 in Chromium and 0.51 in WebKit reading the HEIC directly; I didn't dig into why WebKit's skin patch was a little further off. The tagging differs: Chromium wrote an sRGB ICC profile into the JPEG, WebKit wrote none, which still displays correctly because viewers assume sRGB for untagged JPEGs. I went in expecting the washed-out look and didn't get it.

The fade is real, it just lives on a different path: raw P3 numbers written into a JPEG with no profile, so a viewer reads them as sRGB. Pillow's default save produced that file. The in-gamut patches drifted Ξ”E2000 2.32 to 3.33 and saturated P3 colors up to 6.98. So a JPEG without a profile proves nothing by itself. WebKit's correct file and Pillow's faded one both lack an ICC, and only the pixels tell them apart.

Both engines dropped EXIF and GPS

Metadata went the other way. Both exports came out with no capture time and no GPS, in default mode and in P3 mode. For a product shot on a public page, losing the location is a plus. For someone converting a camera roll and sorting by date afterwards, losing every capture time is a much worse surprise than a slight color shift.

Setting the canvas to Display P3 kept the profile

Passing colorSpace: 'display-p3' when creating the canvas context changed the tagging in both engines. Chromium embedded an ICC described as "Display P3 Gamut with sRGB Transfer", WebKit embedded Display P3, and the P3 patches stayed within Ξ”E2000 0.40. It's the only canvas setup here that keeps the saturated colors. Any sRGB conversion clips them: through Pillow's conversion and sips they ended up as much as 7.55 off, which is sRGB running out of room, not a bug. The default canvas exports clipped them the same way: up to 7.55 in Chromium and 7.91 in WebKit.

For comparison I also ran the samples through the format converter on ImgIng https://imging.ai/ in a desktop browser. HEIC to JPG ran locally, there was no upload request during those three conversions, and the JPGs still had the Display P3 profile, the capture time and the GPS point, with patches within Ξ”E2000 0.31. It converts one file at a time. My fake Pacific coordinates surviving was a reminder that converting a format doesn't strip location by itself.

So checking a browser conversion takes two looks: a metadata viewer for the ICC profile, capture time and GPS, and a pixel value or two against the source, because a missing profile can mean correct sRGB or faded P3. Real iPhone HEIC with nclx and HDR is still the part I haven't tested, and retail Chrome and Safari may not behave exactly like the engines I used.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.