Lossless EXIF / GPS / XMP / IPTC removal for JPEG, PNG and WebP — in the browser or in Node, with zero dependencies.
Most "remove EXIF" tools decode the image and re-save it, which silently re-encodes your pixels (quality loss on every pass) and often happens on someone else's server. This module does neither: it rewrites the container structure and copies the compressed image data across untouched. The output pixels are byte-identical to the input — and there is a test that fails if they ever aren't.
This is the exact engine behind the metadata remover in SolveKit's Image Batch Studio, where it runs client-side on unlimited batches.
JPEG — walks the segment list and drops:
APP1segments whose payload starts withExif\0\0(EXIF, including the GPS IFD) or the Adobe XMP namespace URIAPP13(Photoshop IRB, which is where IPTC lives)COMcomment segments
Everything else — DQT, DHT, SOF*, and critically the entropy-coded scan data after SOS — is copied verbatim.
PNG — filters out the metadata chunks tEXt, zTXt, iTXt, eXIf and tIME. Critical chunks (IHDR, PLTE, IDAT, IEND) pass through untouched with their CRCs intact.
WebP — drops the EXIF and XMP RIFF chunks, clears the corresponding VP8X feature flag bits (0x08 and 0x04), and patches the RIFF size field. The VP8 /VP8L bitstream is never touched.
Anything that doesn't identify as one of these three containers is refused (null) rather than risk a corrupting rewrite.
There is also an inspector (inspectMetadata) that reports what a file carries — camera make, capture date, orientation, whether GPS coordinates are present, and how many bytes stripping would remove — via a defensive TIFF/IFD walk that never throws on malformed input.
import { inspectMetadata, stripMetadata } from "./src/metadata.ts";
const bytes = new Uint8Array(await file.arrayBuffer());
const summary = inspectMetadata(bytes);
// { format: "jpeg", hasExif: true, hasGps: true, camera: "Apple iPhone 15",
// dateTaken: "2026:07:12 19:03:44", orientation: 6, metadataBytes: 12345, ... }
const result = stripMetadata(bytes);
if (result) {
// result.bytes → the clean file (same pixels, byte for byte)
// result.removedBytes → how much metadata was dropped
// result.lossless → always true when a result is returned
}No DOM, no Node APIs — plain typed-array code that runs anywhere.
node --test # requires Node >= 22.6 (runs TypeScript directly)The fixtures are built byte-by-byte in the tests (no binaries in the repo): a synthetic TIFF/EXIF payload with GPS pointer, a PNG with correct CRCs, and a WebP with VP8X flags. The JPEG test asserts the pixel tail of the output is byte-identical to the input.
- It does not remove metadata from formats it can't verify (TIFF, HEIC, AVIF → refused, not guessed).
- It does not touch color profiles (
APP2/ICC,iCCP) — stripping those changes how pixels render, which violates the whole point. - Compress-to-size, resizing and format conversion are different (lossy) jobs — this module only ever removes metadata.
MIT © SolveKit