Every photo from a phone carries a small hidden file inside it: EXIF metadata. Camera model, lens, timestamps, sometimes the owner's name, and, if location was on, GPS coordinates precise enough to find a house.
Some platforms strip this when you upload. Many paths don't: sending a photo "as a file" in a messenger, email attachments, shared cloud links, forums, marketplaces, your own website. If you're not sure, assume the metadata travels with the file.
In a previous post I showed that drawing an image to a canvas drops metadata as a side effect. That works, but it re-encodes the JPEG and loses a bit of quality every time. Here's the other approach: cut the metadata out of the file byte by byte, and leave the image data untouched.
How a JPEG is laid out
A JPEG is a sequence of segments. Each one starts with a marker: the byte 0xFF followed by a type byte. Most segments then have a 2-byte big-endian length (which includes those 2 bytes).
FF D8 SOI start of image
FF E0 len ... APP0 JFIF header
FF E1 len "Exif" ... APP1 EXIF (camera, dates, GPS...)
FF E1 len "http://ns.adobe.com/xap/1.0/" ... APP1 XMP
FF E2 len ... APP2 ICC colour profile
FF ED len ... APP13 Photoshop / IPTC
FF DB len ... DQT quantization tables
FF C0 len ... SOF frame header (size, components)
FF C4 len ... DHT Huffman tables
FF DA len ... SOS start of scan, then compressed pixels
FF D9 EOI end of image
The metadata lives in the APPn segments before the image data. The pixels start after SOS. So stripping metadata means: walk the segments, drop the ones you don't want, copy the rest unchanged.
Step 1: see what's inside
async function listSegments(file) {
const b = new Uint8Array(await file.arrayBuffer());
if (b[0] !== 0xff || b[1] !== 0xd8) throw new Error('Not a JPEG');
const segments = [];
let i = 2;
while (i < b.length && b[i] === 0xff) {
const marker = b[i + 1];
if (marker === 0xda) break; // SOS: compressed data follows
const len = (b[i + 2] << 8) | b[i + 3];
const id = String.fromCharCode(...b.subarray(i + 4, i + 8));
segments.push({ marker: marker.toString(16), bytes: len + 2, id });
i += 2 + len;
}
return segments;
}
On a typical phone photo you'll see an e1 segment starting with Exif that's several kilobytes, sometimes much more, because it also holds an embedded thumbnail.
Step 2: drop the metadata segments
const DROP = new Set([
0xe1, // APP1: EXIF and XMP
0xed, // APP13: Photoshop / IPTC
0xfe, // COM: comments
]);
async function stripJpegMetadata(file) {
const b = new Uint8Array(await file.arrayBuffer());
if (b[0] !== 0xff || b[1] !== 0xd8) throw new Error('Not a JPEG');
const parts = [b.subarray(0, 2)]; // SOI
let i = 2;
while (i < b.length) {
if (b[i] !== 0xff) throw new Error('Unexpected byte at ' + i);
const marker = b[i + 1];
if (marker === 0xda) {
parts.push(b.subarray(i)); // SOS + image data + EOI, untouched
break;
}
const len = (b[i + 2] << 8) | b[i + 3];
if (!DROP.has(marker)) parts.push(b.subarray(i, i + 2 + len));
i += 2 + len;
}
return new Blob(parts, { type: 'image/jpeg' });
}
That's it. No canvas, no quality loss, and the output is usually smaller. Everything happens in memory; nothing is uploaded.
Note what we keep: APP0 (JFIF) and APP2, because APP2 usually holds the ICC colour profile. Drop it and wide-gamut photos can look dull. This is a minimal version: a production parser should also handle padding 0xFF bytes between segments and check that lengths don't run past the end of the file.
The catch: orientation
Phones often save the pixels sideways and add an EXIF flag that says "rotate 90° when displaying". Remove EXIF and that flag goes too, so a portrait photo can suddenly show up lying on its side.
Two fixes:
-
Keep a minimal EXIF with only the orientation tag. Libraries like
piexifjscan build one. -
Rotate the pixels for those images: decode with
createImageBitmap()(which applies orientation), draw to a canvas and re-encode. You lose the "no re-encoding" benefit, but only for rotated photos.
Read the orientation before you strip, then decide per file.
Verify the result
Don't trust your own code with privacy, test it:
- Run
listSegments()on the output. There should be noe1segment. - Open the file in an EXIF viewer and confirm there's no GPS block.
- Compare the image visually with the original. Since compressed data is copied byte for byte, it should be identical.
What about HEIC, PNG and others?
Same idea, different containers. PNG stores metadata in chunks (eXIf, tEXt, iTXt), which you can drop the same way. HEIC keeps EXIF inside its box structure and is harder to edit safely; converting to JPEG is often the simpler route.
No-code version
If you just need to clean a few photos, FormatMath has a metadata viewer that shows what a file reveals and a remove metadata from photo tool. Both run entirely in your browser, so the photo never leaves your device.
Have you ever checked what your own photos give away? The GPS block is usually the part that surprises people.
Top comments (0)