Back to News & Insights
JavaScript October 15, 2026 · 4 min read

Stripping EXIF and GPS from photos in the browser, losslessly, in ~500 lines of vanilla JS

Photos from phones carry more than pixels. Open the file and there's often the exact GPS position it...

Stripping EXIF and GPS from photos in the browser, losslessly, in ~500 lines of vanilla JS

Photos from phones carry more than pixels. Open the file and there's often the exact GPS position it was taken at, the phone model, lens and settings, the date and time, sometimes the owner's name or a camera serial number, and, increasingly, labels saying whether an image was edited with generative AI.

I make a small Android camera app, and people kept asking whether the photos they share give away where they live. So I built a checker that shows what a photo file carries and saves a clean copy, without uploading anything:

No libraries, no server, no WebAssembly: a DataView over the file's bytes. Here's how it works.

From there it's container walking. The first bytes tell you the format: FF D8 is a JPEG, 89 PNG is a PNG, RIFF....WEBP is a WebP, and II/MM at the start is a TIFF (which is also what a DNG raw file is).

A JPEG is a list of segments. Each starts with 0xFF, a marker byte, then a two-byte big-endian length:

The fiddly part is the image data itself. After a Start of Scan (FF DA) segment comes entropy-coded data with no length field. Inside it, a literal 0xFF byte is "stuffed" as FF 00, and restart markers FF D0–FF D7 can appear. So to find the next real segment (progressive JPEGs have several scans, with tables in between) you scan forward for an 0xFF that is followed by something other than 00 or a restart marker.

The EXIF block is a TIFF structure: a byte-order mark (II little-endian, MM big-endian), the number 42, and an offset to the first IFD (image file directory). Each IFD entry is 12 bytes: tag, type, count, and either the value or an offset to it.

GPS coordinates are three rationals (degrees, minutes, seconds), each a pair of 32-bit integers, plus a reference letter:

The IFD after the first one (IFD1) is the embedded thumbnail. It's easy to forget, and it can still show the uncropped original after you've cropped the photo.

PNG is a list of chunks (length, type, data, CRC). Metadata lives in eXIf (the same TIFF structure), tEXt/iTXt/zTXt text chunks and tIME. Some image generators write their whole prompt into a tEXt chunk called parameters, so the tool shows text chunks verbatim.

WebP is RIFF: four-character chunk IDs with little-endian sizes, padded to even lengths. EXIF and XMP are separate chunks, and the VP8X header has flag bits saying they're present.

In XMP, the interesting fields for 2026 are the IPTC DigitalSourceType values (trainedAlgorithmicMedia means made with generative AI, compositeWithTrainedAlgorithmicMedia edited with it) and whether a Content Credentials (C2PA) manifest is embedded. The tool only detects C2PA; it doesn't validate the signature.

The easy way to strip metadata is to draw the image on a canvas and export it. That re-encodes the JPEG and loses quality. Instead, for JPEG, PNG and WebP the tool rebuilds the file from the parts it keeps: JPEG: keep everything except APP1–APP15 and COM segments, with two exceptions: the ICC colour profile (APP2 "ICC_PROFILE") and Adobe's APP14, because dropping those changes how colours display. The image data is copied byte for byte. PNG: drop eXIf, tEXt, iTXt, zTXt, tIME and the C2PA caBX chunk. WebP: drop EXIF and XMP chunks, clear their bits in VP8X, and rewrite the RIFF size.

One catch with JPEGs: phones often store the image sideways and add an Orientation tag saying how to display it. Strip all EXIF and the photo shows up rotated. So the clean JPEG gets a brand-new 36-byte EXIF segment containing exactly one tag, Orientation, and nothing else.

Other formats the browser can decode (AVIF, for example) fall back to a canvas re-encode as JPEG at quality 0.95. DNG is read-only, and HEIC can only be cleaned in browsers that can decode it.

Before offering the download, the page runs the same parser on the clean copy and reports anything left. It's a small step, but it turns "trust me" into "here's what the output contains":

The page runs under a strict Content Security Policy (no inline scripts, no third-party scripts), and the photo never leaves the tab: load the page, switch off your network, and it still works. The script isn't minified, so you can read it at /js/photo-check.js. The only request it makes is to my own page counter, saying that a photo was checked or cleaned. There are no cookies and no photo data in it.

If you find a file it misreads, I'd love to hear about it in the comments: the metadata, not the photo, or a test file with fake details.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation