Building a metadata remover that does not touch your pixels
Photographs carry more than they look like they do: the camera, the lens, the timestamp and very often the exact coordinates where the shutter opened. Removing that is easy to do badly. The naive approach is to decode the image and re-encode it, which does strip the metadata and also quietly degrades every pixel in the photo. This tool does not decode anything.
What it is built with
- No image library at all: plain `Uint8Array` walking
- The JPEG segment structure (JFIF markers) and the PNG chunk structure
- Web Workers, so a large batch does not freeze the page
Step 1: reject the obvious implementation
Draw the photo to a canvas and export it, and the metadata is gone. It is three lines of code and it is the wrong answer. Canvas export re-encodes the image, so a JPEG that was already lossy is now lossy twice, with visible degradation on gradients and edges. The user asked to remove information about the photo, not to alter the photo.
The correct implementation treats the file as a container: the compressed pixel data is one part of it, the metadata is another, and the job is to copy the first while dropping the second. The pixels are never decoded, so they cannot be damaged.
Step 2: walk the JPEG segment list
A JPEG is a sequence of segments, each introduced by a two-byte marker beginning `0xFF`. Most segments declare their own length, so you can step through them without understanding any of them. EXIF lives in `APP1`, XMP in another `APP1`, IPTC and Photoshop data in `APP13`, and comments in `COM`.
The whole strip is therefore: copy segments through, skip the ones on the list, and stop walking at the start-of-scan marker because everything after it is entropy-coded pixel data that must be copied verbatim.
That last detail matters. `0xFF` bytes occur inside compressed scan data, so continuing to parse for markers past the start of scan would find markers that are not there.
// Segments to drop: EXIF/XMP (APP1), IPTC/Photoshop (APP13),
// other APPn blocks, and comments.
while (i < bytes.length) {
if (bytes[i] !== 0xff) break;
const marker = bytes[i + 1];
// Start of scan: everything from here is compressed pixel data.
// Copy the rest verbatim and stop parsing.
if (marker === 0xda) { copyRest(i); break; }
const length = (bytes[i + 2] << 8) | bytes[i + 3];
if (!DROP.has(marker)) copy(i, i + 2 + length);
i += 2 + length;
}
Step 3: PNG is easier, and stricter
PNG is a cleaner format for this: a signature followed by length-prefixed chunks, each with a four-character type and a CRC. Metadata lives in `tEXt`, `iTXt`, `zTXt`, `eXIf` and `tIME`, and the image data lives in `IDAT`.
Because every chunk carries its own length and checksum, dropping one is genuinely just omitting it from the output. No CRC needs recomputing, because the CRCs are per chunk rather than over the file.
Step 4: say so when you cannot help
A format the stripper does not understand is not something to guess at. Silently returning the input unchanged would tell the user their photo had been cleaned when it had not, which for this particular tool could matter a great deal.
So the result carries whether the format was supported and how many bytes were actually removed. An unsupported file is reported as unsupported, and a file that genuinely had no metadata is reported as having had none, which are different statements and should not look the same.
What went wrong
The most useful part of any build write-up, and the part usually left out.
- Re-encoding to strip metadata is the common implementation and it silently degrades the photo. If the output is a different size than "input minus the metadata", something has decoded your pixels.
- Parsing for markers past the JPEG start-of-scan finds false markers inside compressed data. Copy the remainder verbatim instead.
- Reporting "cleaned" for a format you did not understand is worse than reporting failure, because the user acts on it.
- Removing metadata does not anonymise a photograph. The image itself can still identify a place or a person, and this tool says so rather than implying otherwise.
Use the tool: Remove metadata from a photo · How YappyKit works