Inspecting and Stripping EXIF, GPS, and C2PA Metadata in the Browser
Every smartphone photo embeds a surprising amount of telemetry outside the visible pixel matrix: GPS latitude, longitude, altitude, compass bearing, lens serial number, shutter timestamp, and often an embedded JPEG thumbnail inside the EXIF header that persists even if the main image was cropped in a poorly implemented editor. Newer cameras and generative pipelines also attach cryptographic provenance manifests in C2PA (Coalition for Content Provenance and Authenticity) JUMBF containers.
Uploading original photos to a third-party server just to inspect or remove those headers defeats the privacy objective, as discussed in our analysis of privacy risks in online file tools. Below is how we parse and sanitize image containers locally across our browser image tools and file privacy utilities.
1. Anatomy of JPEG Application Markers
A baseline or progressive JPEG stream begins with the Start of Image marker (0xFFD8) followed by a sequence of framed segments before the Start of Scan (0xFFDA) compressed entropy data begins. Each marker consists of 0xFF, a marker type byte, and a big-endian 16-bit length field (which includes the two length bytes themselves):
| Marker | Hex | Payload Identifier / Purpose |
|---|---|---|
APP0 |
0xFFE0 |
JFIF\0 — pixel density and aspect ratio |
APP1 |
0xFFE1 |
Exif\0\0 (TIFF IFD, GPS IFD) or http://ns.adobe.com/xap/1.0/\0 (XMP packet) |
APP2 |
0xFFE2 |
ICC_PROFILE\0 — color management profile (worth keeping for wide-gamut P3 photos) |
APP11 |
0xFFEB |
ISO/IEC 19566-5 JUMBF boxes carrying C2PA Content Credentials (c2pa) |
APP13 |
0xFFED |
Photoshop 3.0\0 — IPTC press metadata and 8BIM resources |
COM |
0xFFFE |
Free-form ASCII/UTF-8 comment string |
2. Lossless JPEG Metadata Stripping via DataView
Many tutorials suggest drawing the image onto an HTML5 <canvas> and calling canvas.toBlob('image/jpeg', 0.92) to strip EXIF. While effective, canvas re-encoding decodes and re-quantizes the DCT coefficients, introducing generation loss and discarding the embedded ICC color profile.
For JPEG files, we can instead scan the segment headers with a DataView and copy only the structural markers, quantization/Huffman tables, optional APP2 ICC profile, and raw entropy scan bytes into a new Uint8Array—achieving zero pixel degradation in under 5 milliseconds. This lossless surgery powers the EXIF Reader and Remover and is covered step by step in our guide on removing EXIF and GPS location data from photos.
/**
* Strips APP1 (EXIF/XMP), APP11 (C2PA), APP13 (IPTC), and COM segments from a JPEG
* without decoding or recompressing the DCT image data.
*/
export function stripJpegMetadataLossless(arrayBuffer, keepIccProfile = true) {
const view = new DataView(arrayBuffer);
const bytes = new Uint8Array(arrayBuffer);
if (view.getUint16(0, false) !== 0xffd8) {
throw new Error('Not a valid JPEG file (missing SOI 0xFFD8)');
}
const chunks = [bytes.subarray(0, 2)]; // Keep SOI
let offset = 2;
while (offset + 4 <= bytes.length) {
if (bytes[offset] !== 0xff) {
break;
}
const marker = bytes[offset + 1];
// SOS (Start of Scan, 0xDA) or EOI (End of Image, 0xD9):
// Copy the remainder of the compressed bitstream untouched
if (marker === 0xda || marker === 0xd9) {
chunks.push(bytes.subarray(offset));
break;
}
// Standalone markers without a length payload
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
chunks.push(bytes.subarray(offset, offset + 2));
offset += 2;
continue;
}
const segmentLength = view.getUint16(offset + 2, false);
const segmentEnd = offset + 2 + segmentLength;
const isApp0 = marker === 0xe0;
const isApp2Icc = marker === 0xe2 && keepIccProfile;
const isAppMarker = marker >= 0xe0 && marker <= 0xef;
const isComment = marker === 0xfe;
if ((!isAppMarker && !isComment) || isApp0 || isApp2Icc) {
chunks.push(bytes.subarray(offset, segmentEnd));
}
offset = segmentEnd;
}
const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
const out = new Uint8Array(totalLength);
let writePos = 0;
for (const chunk of chunks) {
out.set(chunk, writePos);
writePos += chunk.byteLength;
}
return out.buffer;
}
3. Reading C2PA Provenance and JUMBF Boxes
C2PA Content Credentials store a signed manifest store inside ISO BMFF / JUMBF boxes (UUID d8fec3d6-1b0e-483c-9297-5828877ec481). In JPEG files, large manifests are split across multiple extended APP11 segments that share a sequence number and box instance number, reassembled in memory, and decoded as CBOR structures containing claim signatures, software agent names (e.g., camera firmware or AI model identifiers), and hard/soft ingredient assertions.
You can inspect those CBOR claim graphs and verify what actions were recorded on a media file without uploading it using the Sunasty C2PA Content Credentials Reader.
4. When Re-Encoding Is Useful, and Beyond Image Containers
If the goal is both metadata removal and file size reduction for web publishing, re-encoding through WebP or AVIF via the browser image compressor discards all original marker segments while cutting payload byte size by 40% to 70%.
Note that office documents (.docx, .xlsx, .pptx) carry a parallel privacy surface: because OpenXML files are ZIP archives, inspecting and sanitizing docProps/core.xml and docProps/app.xml in memory removes author names, revision counts, and company identifiers just as cleanly. We apply the same local ZIP-stream approach in the Office Document Metadata Inspector.