How I Remove EXIF Data From Photos Without Re-Compressing Them
You’re about to post a photo of the sofa you’re selling, taken in your living room. The file very likely knows the GPS coordinates of that living room, the phone model and the minute you took it. Most “remove metadata” tools fix that by redrawing the photo and saving a new JPG, which works, but quietly costs you quality every time. For Remove EXIF data I wanted the picture to come out byte for byte the same, with only the metadata cut away.
Two ways to remove metadata
The easy way is a canvas. Draw the image, call toBlob, and the new file has no EXIF at all, because the browser writes only pixels. Several ToolOnTap tools work like that and drop metadata as a side effect. The price is a second round of JPEG compression, and the color profile is gone too.
The other way is to treat the file as what it is: a list of labelled blocks. Image formats keep the compressed pixels in some blocks and everything else (EXIF, XMP, comments, previews) in others. If you know which block is which, you can copy the pixel blocks unchanged and leave the rest out. Nothing is decoded and nothing is encoded again, so there’s nothing to lose. That’s what utils/metadata.ts does for JPEG, PNG and WebP.
It starts by reading the file’s signature, never its name: FF D8 FF for JPEG, the 8-byte \x89PNG\r\n\x1A\n for PNG, and RIFF followed by WEBP at byte 8 for WebP. A file named .jpg that is really a PNG is cleaned as a PNG.
JPEG: a chain of segments
A JPEG is a series of segments. Each starts with 0xFF, a marker byte, and a 2-byte length. The marker says what’s inside:
| Marker | What it usually holds | What the tool does |
|---|---|---|
APP1 (E1) |
EXIF, or XMP from Adobe apps | Removes it |
APP2 (E2) |
ICC color profile, or MPF (extra images) | Keeps the ICC profile, removes the rest |
APP13 (ED) |
IPTC captions, credits, keywords | Removes it |
APP14 (EE) |
Adobe color transform information | Keeps it |
COM (FE) |
A text comment | Removes it |
| APP3 to APP15 | Other apps’ data | Removes it |
SOS (DA) |
Start of the compressed image | Copies it to the end marker unchanged |
In code, each rule is one line:
let keep = true
if (marker === 0xE1) {
keep = false
if (startsWith(b, body, 'Exif\0')) readExif(b.subarray(body + 6, i + 2 + length), report)
else if (startsWith(b, body, 'http://ns.adobe.com/')) report.xmp = true
}
else if (marker === 0xE2) keep = startsWith(b, body, 'ICC_PROFILE\0') // drop MPF/FlashPix
else if (marker === 0xED) { keep = false; report.iptc = true }
else if (marker === 0xEE) keep = startsWith(b, body, 'Adobe') // colour transform info
else if (marker === 0xFE) { keep = false; report.comment = true }
else if (marker >= 0xE3 && marker <= 0xEF) keep = false // other apps' data
Before an EXIF segment is dropped, it’s read, so the page can tell you what it held: the location, the camera, a serial number, the date, the software.
The image data needs one bit of care. After the SOS marker, the compressed data has no length field; it runs until the end-of-image marker FF D9. Inside it, a 0xFF byte is always followed by 0x00 or a restart marker, never by D9, so scanning for FF D9 finds the real end. Anything after that end is dropped too. Phones and editors sometimes append extra data there, and the code’s own comment names depth maps and edit history as examples.
Keeping the two things you’d miss
Removing everything sounds safest, but two pieces of “metadata” change how the photo looks.
The color profile. A photo saved in a wide-gamut space such as Display P3 needs its ICC profile, or its colors are read as plain sRGB and come out dull or shifted. The profile says nothing about you, so it stays.
The orientation. Phones often store the pixels sideways and add an orientation tag telling viewers to turn them. Remove that tag and the photo appears on its side. So after stripping a JPEG or PNG, the tool writes back a new EXIF block that contains that one value and nothing else:
export function orientationOnlyExif(orientation: number): Uint8Array {
return Uint8Array.from([0x4D, 0x4D, 0, 42, 0, 0, 0, 8, 0, 1, 0x01, 0x12, 0, 3, 0, 0, 0, 1, 0, orientation, 0, 0, 0, 0, 0, 0])
}
Those 26 bytes are a complete, tiny TIFF file. MM means big-endian, then the number 42 and the offset of the first directory (8). The directory has 1 entry: tag 0x0112 (orientation), type 3 (a 16-bit number), count 1, and the value. The last 4 zero bytes say there is no next directory. It’s only added when the original orientation wasn’t 1 (upright), so an upright photo gets no EXIF at all.
Where it goes matters. If the file starts with a JFIF segment (APP0), that has to stay the first segment, so the new EXIF block is placed right after it.
PNG: chunks with checksums
PNG files are a signature followed by chunks. Each chunk has a 4-byte length, a 4-letter type, the data, and a CRC-32 checksum over the type and data. The tool drops five chunk types:
const PNG_DROP = new Set(['tEXt', 'zTXt', 'iTXt', 'eXIf', 'tIME'])
The three text chunks hold key-value pairs (the tool lists their keys, such as “Software” or “Creation Time”, and spots XMP by its key XML:com.adobe.xmp). eXIf is EXIF inside a PNG, and tIME is the last-modified time. Everything else is copied as it was, including iCCP, the color profile, and every IDAT chunk with the pixels.
Copied chunks keep their original checksums, which stay valid because their bytes don’t change. The one chunk the tool creates, an eXIf with only the orientation, needs a fresh CRC-32. The code computes it with the standard table-based CRC (polynomial 0xEDB88320), and puts the chunk right after IHDR, the header chunk. Anything after IEND is dropped.
WebP: RIFF chunks and two flags
WebP uses RIFF, a container where each chunk has a 4-letter type and a little-endian length, padded to an even size. The tool drops the original EXIF and XMP chunks (the space is part of the name) and keeps the rest, including ICCP.
Two more fixes keep the file valid. An extended WebP has a VP8X chunk with flags that announce which optional chunks exist. Left as they were, they would announce chunks the file no longer has, so the XMP bit (0x04) is cleared, and the EXIF bit (0x08) stays on only when the photo was rotated: then, as for JPEG and PNG, a new EXIF chunk holding just the orientation is added at the end. And the RIFF header at the top stores the total file size, which has to be rewritten because the file has changed.
The thumbnail you can’t see
This is the item people don’t expect. Many JPEGs carry a small preview inside the EXIF block, in a second directory (IFD1, marked by tag 0x0201). Cameras use it to show photos quickly.
Some editing apps update the main image when you crop it but leave that thumbnail alone. So a photo cropped to hide a house number, a face or a screen can still contain a small copy of the uncropped original. The tool reports “embedded thumbnail” when it finds one, and since the thumbnail lives in the EXIF segment, it goes when that segment goes.
How I check it
The tests build JPEG, PNG and WebP files in memory, with a fake iPhone EXIF block that includes Cairo coordinates, a serial number and a thumbnail. They check that:
- every sensitive string is gone from the output, and the ICC profile, the Adobe segment and the image data are still there;
- the compressed image bytes at the end of the JPEG are identical to the input;
- JFIF is still the first segment;
- stripping a cleaned file again changes nothing and finds nothing except the orientation;
- a file with no metadata comes back byte for byte the same, and the page says “No hidden data found. The file is unchanged.”;
- damaged EXIF data never throws, it only yields less. A directory can’t claim more than 512 entries.
The whole job runs in your browser, so the original photo never has to be uploaded to be cleaned.
Try it
Drop JPG, PNG or WebP photos on Remove EXIF data to see what each one contains and download clean copies. To see every detail a photo carries before you decide, open it in the EXIF viewer. iPhone HEIC photos are a different format built from boxes; reading HEIC EXIF by hand explains how they’re read, and HEIC to JPG converts them into a JPG without the location.