When a user uploads a photo, its EXIF data goes with it: often the GPS position of their home, their phone model, and the time it was taken. You should remove it on the server anyway, since you can't trust what a browser sends. Removing it in the browser as well means the location never leaves the user's device at all.
This guide gives you a small, dependency-free function that does it, and explains the traps in the usual answers. We ran every piece of code here in Chrome on real test files.
What's wrong with the usual ways to remove EXIF in JavaScript?
1. Draw the image on a canvas and export it. This works, and the result carries no metadata. But it re-encodes the photo: JPEG quality drops a little every time, the file size changes, and the wide colour range of an iPhone photo is squeezed to standard sRGB. Fine for thumbnails; not what you want for the original.
2. Delete the EXIF block from the bytes. This is lossless, and it's what most small libraries do. But phones don't rotate portrait photos; they store the pixels on their side and add an Orientation tag telling viewers to turn them. Delete the EXIF and the tag goes too, so the photo appears sideways. Most of these snippets also only remove EXIF, leaving XMP (which can hold the author and the editing history), comments and Content Credentials.
The function below takes the lossless route and fixes both problems:
- It keeps the picture data byte-for-byte.
- It keeps the colour profile, which isn't personal information.
- It writes back a tiny EXIF block holding only the Orientation.
- It drops everything else: EXIF, GPS, XMP, IPTC, comments and C2PA Content Credentials. That includes the multi-picture blocks some phones use to hide a second full image with its own GPS.
How to remove EXIF data in JavaScript without a library
// Removes metadata from a JPEG without touching the picture data.
// Keeps the colour profile, and writes back only the Orientation so phone photos stay upright.
function stripJpeg(bytes) {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
if (view.getUint16(0) !== 0xffd8) throw new Error('Not a JPEG');
const text = (seg, at, len) => String.fromCharCode(...seg.subarray(at, at + len));
const parts = [];
let orientation = 1;
let pos = 2;
while (pos + 4 <= bytes.length) {
const marker = view.getUint16(pos);
if (marker === 0xffda) { parts.push(bytes.subarray(pos)); break; } // image data: keep the rest
const seg = bytes.subarray(pos, pos + 2 + view.getUint16(pos + 2));
if (marker === 0xffe1 && text(seg, 4, 6) === 'Exif\0\0') {
try { orientation = readOrientation(seg); } catch { /* unreadable EXIF: leave upright as stored */ }
}
const isMeta = (marker >= 0xffe0 && marker <= 0xffef) || marker === 0xfffe;
const keep = !isMeta
|| (marker === 0xffe0 && text(seg, 4, 5) === 'JFIF\0')
|| (marker === 0xffe2 && text(seg, 4, 12) === 'ICC_PROFILE\0');
if (keep) parts.push(seg);
pos += seg.length;
}
if (orientation > 1) parts.splice(parts[0]?.[1] === 0xe0 ? 1 : 0, 0, orientationSegment(orientation)); // after JFIF
return new Blob([new Uint8Array([0xff, 0xd8]), ...parts], { type: 'image/jpeg' });
}
function readOrientation(seg) {
const tiff = new DataView(seg.buffer, seg.byteOffset + 10, seg.length - 10);
const le = tiff.getUint16(0) === 0x4949;
const ifd = tiff.getUint32(4, le);
const count = tiff.getUint16(ifd, le);
for (let i = 0; i < count; i++) {
const entry = ifd + 2 + i * 12;
if (tiff.getUint16(entry, le) === 0x0112) return tiff.getUint16(entry + 8, le);
}
return 1;
}
function orientationSegment(value) {
// A minimal EXIF block holding nothing but the Orientation tag.
return new Uint8Array([
0xff, 0xe1, 0x00, 0x22, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, // APP1, length 34, "Exif\0\0"
0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, // big-endian TIFF header
0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, // 1 entry: Orientation, SHORT, 1
0x00, value, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // the value; no next IFD
]);
}
// For PNG and WebP: redraw the picture, which leaves every piece of metadata behind.
async function redraw(file, quality = 0.95) {
const bitmap = await createImageBitmap(file); // applies the photo's rotation
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
canvas.getContext('2d').drawImage(bitmap, 0, 0);
const blob = await canvas.convertToBlob({ type: file.type, quality });
return new File([blob], file.name, { type: blob.type });
}
async function removeMetadata(file) {
if (file.type === 'image/jpeg') {
const clean = stripJpeg(new Uint8Array(await file.arrayBuffer()));
return new File([clean], file.name, { type: 'image/jpeg' });
}
return redraw(file);
}
JPEG goes through the lossless path. PNG and WebP are redrawn on a canvas, which carries no metadata over. Other types pass through redraw too, as long as the browser can open them.
How to strip EXIF before upload with a file input
<input type="file" id="photo" accept="image/jpeg,image/png,image/webp">
<script>
const input = document.querySelector('#photo');
input.addEventListener('change', async () => {
const file = input.files[0];
if (!file) return;
const clean = await removeMetadata(file);
const form = new FormData();
form.append('photo', clean);
await fetch('/upload', { method: 'POST', body: form });
});
</script>
We ran this exact page in Chrome with a phone-style photo carrying GPS, a camera model, an author and a Display P3 profile, and captured what reached the server. It held the Orientation tag and the colour profile, and nothing else. The pixels were identical to the original.
How to strip EXIF from uploads in React
The same function, called from an onChange handler:
function PhotoUpload() {
async function handleChange(event) {
const file = event.target.files[0];
if (!file) return;
const clean = await removeMetadata(file);
const form = new FormData();
form.append('photo', clean);
await fetch('/api/upload', { method: 'POST', body: form });
}
return <input type="file" accept="image/jpeg,image/png,image/webp" />;
}
If you show a preview, make it from clean (URL.createObjectURL(clean)), so the preview is exactly what gets uploaded.
Does it keep photos upright and pixels identical?
| File | Result in Chrome |
|---|---|
| Phone-style JPEG, big-endian EXIF, Display P3 | Metadata gone, upright, colour profile kept, pixels identical |
| JPEG with little-endian EXIF (as many Android phones write it) | Metadata gone, upright, pixels identical |
| JPEG with EXIF but no JFIF header (as cameras write it) | Metadata gone, upright, pixels identical |
| JPEG with C2PA Content Credentials | Credentials gone, pixels identical |
| Stable Diffusion PNG with its prompt, GPS and author | All gone (redrawn) |
| WebP with GPS, camera and author | All gone (redrawn) |
Does it work with HEIC, Safari and older browsers?
- HEIC: Chrome and Firefox can't open iPhone HEIC photos, so they can't clean them either. iPhones usually hand the browser a JPEG instead when a file input doesn't ask for HEIC, which the
acceptlist above doesn't. - Redrawn PNG and WebP are re-encoded. PNG stays lossless. WebP uses quality 0.95. Safari can't encode WebP, and there
convertToBlobreturns a PNG instead. OffscreenCanvasneeds Safari 16.4 or newer. For older browsers, use a regular<canvas>element andcanvas.toBlob().- Video and PDF uploads carry the same kind of details, and browsers can't clean them.
Should you also clean uploads on the server?
A browser can be bypassed, so treat this as protection for your users, not your only defence. On the server, clean every upload again. See our guides for Node.js and Python. To cover video, PDF and RAW uploads as well, the metadata removal API cleans them all with one call and reports what it removed.
Frequently asked questions
Why is my uploaded photo sideways after removing EXIF?
The Orientation tag was removed along with the rest of the EXIF. Phone photos rely on it. Keep that one tag, as the function above does, or draw the image on a canvas, which applies the rotation.
Does drawing an image on a canvas remove metadata?
Yes. A canvas holds only pixels, so the exported file carries no EXIF, GPS or XMP. Current browsers apply the photo's rotation when drawing it. The downside is re-encoding: some quality loss for JPEG, and colours converted to sRGB.
Can I strip EXIF before upload without a library?
Yes. The code above has no dependencies and runs in any current browser.
Is stripping EXIF in the browser enough?
No. Anyone can send your server a file directly, so clean uploads on the server as well. Cleaning in the browser is still worth doing: it keeps the user's location from ever leaving their device.
