Converting iPhone photos in the browser without leaking their location
How @su-engineering/heic 0.3.0 converts iPhone HEIC photos to JPEG in the browser, keeps useful EXIF, removes the GPS block and avoids double rotation.
An iPhone photo uploaded to a web application usually arrives as HEIC. Most teams convert it to JPEG somewhere along the way, and the conversion decides what happens to the photo's metadata. That metadata often includes the GPS coordinates of where the picture was taken, which for many photos is someone's home.
We maintain @su-engineering/heic, an MIT licensed library that decodes HEIC in the browser. Version 0.3.0, released on 6 November 2026, adds control over EXIF during conversion, a way to read the raw EXIF block, and fast previews from the thumbnail embedded in the file. This article explains the choices behind the EXIF option, because they apply to any photo upload pipeline, whatever library it uses.
two ways conversion goes wrong
The first failure is losing everything. When a browser decodes an image and encodes it again through a canvas, the result carries no EXIF at all. The capture date, camera and lens are gone. For a photo archive, an insurance claim or a property inspection, that information is part of the record, and the application has quietly removed it.
The second failure is keeping everything. Copying the original EXIF block into the JPEG preserves the capture date, and it also preserves the GPS coordinates. A user who shares a photo of a broken boiler does not expect to publish their address. If the image later becomes visible to other users, or is stored by a third party, the location travels with it.
A third, less obvious problem appears when EXIF is copied without changes. iPhones store the sensor image unrotated and record how to display it. A decoder that applies that rotation produces upright pixels. If the copied EXIF still says "rotate 90 degrees", image viewers apply the rotation a second time and the photo appears sideways.
what 0.3.0 does
convertHeic now accepts an exif option with three values:
import { convertHeic } from '@su-engineering/heic';
const converted = await convertHeic(file, {
type: 'image/jpeg',
exif: 'no-location', // 'none' (default), 'all' or 'no-location'
});
console.log(converted.exif); // what was actually written
console.log(converted.warnings); // e.g. exif-missing, exif-not-written
The default is 'none', so an application that upgrades without changing its code keeps the previous behavior: no source metadata in the output. Location data only leaves the device when a developer asks for it.
'no-location' keeps the capture time, camera and lens, and removes the GPS data. The pointer to the GPS section is removed, and every byte of that section, including values stored outside it, is overwritten with zeros. Removing only the pointer would hide the coordinates from ordinary EXIF readers while leaving them in the file for anyone who scans the bytes.
In both 'all' and 'no-location', the copied block is adjusted to match the new image. Orientation is set to 1, because the pixels are already upright, and the pixel dimensions are set to the output size, so a downscaled JPEG does not claim the original resolution.
The block is edited in place on a copy rather than rebuilt. EXIF contains vendor sections, such as Apple's MakerNote, whose internal layout is not public. Rebuilding the structure risks corrupting them. Editing in place keeps everything we do not deliberately change byte for byte identical.
fail closed
EXIF comes from the uploaded file, so it is untrusted input. When the library cannot walk the structure, for example because an offset points outside the block, it writes the JPEG without any EXIF and returns an exif-not-written warning.
This matters most for 'no-location'. If we cannot read a block completely, we cannot show that the GPS data is gone. Returning a JPEG with an unchecked block would turn a parser limitation into a location leak. Losing the capture date on a rare malformed file is the cheaper failure.
EXIF is written to JPEG output only. A PNG conversion still drops all metadata.
client-side removal is not a server control
Removing GPS data in the browser protects users of your own upload form. It does not protect your storage from a client that skips the form, an outdated app version, or a direct API call. If your service must never store location data, check and remove it on the server as well. Treat the browser step as a way to keep the data on the device by default, and the server step as the control you can rely on.
readExif(file) returns the raw EXIF block without converting the image, so the client can also show the user what a photo contains before upload. Pass the result to an EXIF parser such as exifr to display it.
faster previews
Many iPhone photos include a small HEVC thumbnail, around 320 pixels, beside the full image. decodeHeicThumbnail decodes it with WebCodecs, which is much cheaper than decoding a 12 to 48 megapixel photo for a gallery grid or an upload preview. Recent iPhones often omit the thumbnail, and some browsers cannot decode it. In both cases the library decodes the main image and scales it down, and preview.embedded tells you which path was used.
limits
HEVC decoding in the browser depends on the browser, operating system and hardware. The library tries the browser's own image decoder, then WebCodecs, then an optional WebAssembly fallback that you choose to load. Our automated tests use synthetic fixtures, and do not yet cover the full range of files that Apple cameras produce. Test with representative photos on your target devices before production use. The compatibility guide lists the known gaps.
The source, changelog and API reference are on GitHub. Bug reports with a redistributable sample file are the most useful contribution.
photo uploads are a privacy boundary
A photo upload looks like a file transfer, but it also decides which personal data enters your system. Location, capture times and device details are easy to collect by accident and hard to remove later from backups, caches and third-party services.
We work with teams on privacy engineering for data flows like this one. What shall we build together?
Working on something similar? Talk to us