Inspecting and Stripping EXIF, GPS, and C2PA Metadata in the Browser

By Bastien Sulyan · October 2026 · Binary Formats & Privacy

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.