如何在瀏覽器檢查並移除圖片中的 EXIF GPS 與 C2PA 內容憑證
Inspecting and Stripping EXIF GPS and C2PA Content Credentials in the Browser
指南示範在瀏覽器內檢查及移除 JPEG 圖片的 EXIF GPS 與 C2PA 中繼資料,並用 ArrayBuffer 和 DataView 解析 JPEG 標記段。
Every image captured on a modern smartphone or mirrorless camera carries structured binary metadata alongside its compressed pixel stream. While camera settings like aperture and focal length are benign, embedded GPS IFDs (latitude, longitude, altitude, and UTC timestamp) and cryptographic provenance manifests (C2PA / JUMBF) reveal exact physical locations, device serial numbers, and editing software chains.
Uploading sensitive photographs to server-side "metadata remover" websites defeats the purpose of privacy sanitization: the remote server receives the unredacted file before stripping it. Below is a byte-level guide to inspecting and stripping EXIF and C2PA metadata entirely inside the browser using ArrayBuffer and DataView.
1. Anatomy of JPEG APP1 (EXIF) and JUMBF (C2PA) Segments
In a baseline or progressive JPEG stream, data is organized into marker segments beginning with 0xFF:
-
0xFFD8(SOI): Start of Image. -
0xFFE1(APP1): Carries theExif\0\0header followed by a TIFF header (0x4949for little-endian or0x4D4Dfor big-endian) and linked Image File Directories (IFDs), or an XMP packet (http://ns.adobe.com/xap/1.0/). -
0xFFEB(APP11): Carries JPEG Universal Metadata Box Format (JUMBF, ISO/IEC 19566-5) boxes, which store C2PA (Coalition for Content Provenance and Authenticity) manifests, claim signatures, and asset hashes. -
0xFFDA(SOS): Start of Scan (compressed entropy-coded image data follows).
Parsing JPEG Marker Segments in JavaScript
Because all JPEG segment lengths (except SOI, EOI, and RSTn) are stored as big-endian 16-bit unsigned integers immediately following the 2-byte marker, you can enumerate every metadata segment in a few dozen lines of JavaScript without decoding a single pixel:
export function listJpegSegments(arrayBuffer) {
const view = new DataView(arrayBuffer);
if (view.byteLength < 4 || view.getUint16(0, false) !== 0xffd8) {
throw new Error('Not a valid JPEG stream');
}
const segments = [];
let offset = 2;
while (offset + 4 <= view.byteLength) {
if (view.getUint8(offset) !== 0xff) break;
const marker = view.getUint16(offset, false);
// Standalone markers without length payload
if (marker === 0xffd9 || marker === 0xffda) {
segments.push({ marker: marker.toString(16), offset, length: 0 });
break;
}
const segLength = view.getUint16(offset + 2, false);
if (segLength < 2 || offset + 2 + segLength > view.byteLength) break;
let signature = '';
const sigLen = Math.min(12, segLength - 2);
for (let i = 0; i < sigLen; i++) {
const ch = view.getUint8(offset + 4 + i);
signature += ch >= 32 && ch <= 126 ? String.fromCharCode(ch) : '.';
}
segments.push({
marker: `0x${marker.toString(16).toUpperCase()}`,
offset,
length: segLength + 2,
signature,
});
offset += 2 + segLength;
}
return segments;
}
2. Inspecting EXIF GPS Coordinates and C2PA Provenance Claims
Inside an APP1 segment starting with Exif\0\0, tag 0x8825 in IFD0 points to the GPS Info IFD. That sub-IFD stores coordinates as arrays of three RATIONAL values (degrees, minutes, seconds, each composed of two 32-bit unsigned integers numerator / denominator):
-
0x0001(GPSLatitudeRef):'N'or'S' -
0x0002(GPSLatitude):[degNum/degDen, minNum/minDen, secNum/secDen] -
0x0003(GPSLongitudeRef):'E'or'W' -
0x0004(GPSLongitude):[degNum/degDen, minNum/minDen, secNum/secDen]
To inspect these tags—along with camera shutter counts, lens serials, and ICC profiles—without uploading the photo, open it in the Sunasty EXIF Metadata Reader.
Verifying C2PA Content Credentials
Meanwhile, cameras from Leica, Sony, Nikon, and Canon, as well as generative AI pipelines and editing tools (Adobe Photoshop, OpenAI, Google), attach cryptographic C2PA manifests inside JUMBF superboxes (c2pa UUID d8fec3d6-1b0e-483c-9297-5828877ec481). A C2PA manifest contains:
-
Assertions (
c2pa.actions,stds.schema-org.CreativeWork,c2pa.hash.data). -
Claim (
c2pa.claim): Links assertions via SHA-256 digests. -
COSE Signature (
c2pa.signature): An X.509 certificate chain signing the claim structure.
You can inspect the full JUMBF box hierarchy, signer certificate details, and action history of any JPEG, PNG, WebP, or AVIF image locally using the Sunasty C2PA Content Credentials Reader.
3. Stripping Metadata Losslessly in Browser Memory
There are two ways to strip metadata in a browser environment:
-
Canvas Re-encoding (
canvas.toBlob): Drawing an image onto an HTML5<canvas>and exporting it discardsAPP1andAPP11segments automatically, but it re-quantizes DCT coefficients (causing generational quality loss on JPEG files). -
Lossless Binary Segment Filtering: Slicing out
APP1(0xFFE1),APP2,APP11(0xFFEB), andCOM(0xFFFE) segments while preservingSOI,DQT(quantization tables),DHT(Huffman tables),SOF0(frame header), and the compressedSOSbitstream produces a byte-identical image payload with zero recompression loss:
export function stripJpegMetadataLossless(arrayBuffer) {
const src = new Uint8Array(arrayBuffer);
const view = new DataView(arrayBuffer);
if (view.getUint16(0, false) !== 0xffd8) {
throw new Error('Invalid JPEG SOI');
}
const chunks = [src.subarray(0, 2)]; // Keep 0xFFD8 (SOI)
let offset = 2;
while (offset + 4 <= view.byteLength) {
if (view.getUint8(offset) !== 0xff) {
chunks.push(src.subarray(offset));
break;
}
const marker = view.getUint16(offset, false);
// Once we hit Start of Scan (0xFFDA), copy the rest of the entropy stream verbatim
if (marker === 0xffda) {
chunks.push(src.subarray(offset));
break;
}
const segLength = view.getUint16(offset + 2, false);
const totalLen = 2 + segLength;
// Strip APP1..APP15 (0xFFE1..0xFFEF) and COM (0xFFFE), keep APP0 (JFIF) and DQT/DHT/SOF
const isMetadataApp = marker >= 0xffe1 && marker <= 0xffef;
const isComment = marker === 0xfffe;
if (!isMetadataApp && !isComment) {
chunks.push(src.subarray(offset, offset + totalLen));
}
offset += totalLen;
}
const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0);
const out = new Uint8Array(totalBytes);
let pos = 0;
for (const chunk of chunks) {
out.set(chunk, pos);
pos += chunk.byteLength;
}
return out.buffer;
}
Notice one subtle consequence of C2PA: if an image contains a signed C2PA manifest and you strip or modify the APP1 EXIF block, the C2PA hard-binding data hash (c2pa.hash.data) will fail verification unless the APP11 JUMBF manifest is also removed.
For a complete step-by-step walkthrough across JPEG, PNG (eXIf and iTXt chunks), WebP, and HEIC files, read our guide on how to remove EXIF and GPS data from photos and our architectural audit of the privacy risks of server-side online file tools.
來源:DEV Community · dev.to