Strip Location Data From Your Photos Before Posting — Here's the Browser Tool That Does It
Every photo you take carries a hidden backpack of metadata. Camera model, lens settings, GPS coordinates, timestamps — it's all buried in the EXIF data. Most people don't know it's there. Some people really don't want it there. I built an EXIF editor that runs entirely in your browser. Drop a JPEG, PNG, WebP, or TIFF file in, see every metadata field, edit what you want, delete what you don't, and download a clean copy. No uploads, no servers, no "we'll process it for you." Your image never leaves your device. You can try it right now on our free online EXIF editor. EXIF data often contains sensitive information. GPS coordinates can reveal your home address. Timestamps can expose your daily routine. Camera serial numbers can track your gear. Sending all of that to a third-party server just to strip a few fields is absurd. When everything runs client-side, your image bytes stay on your machine. The tool doesn't even have a backend to leak data to. It's physically impossible for us to see your photos because we never receive them. No upload queue, no processing delay. A 10 MB image gets parsed, edited, and re-encoded in milliseconds. The bottleneck is your disk read speed, not network bandwidth. Load the page once and you can scrub metadata from images even without internet. Useful when you're traveling, on metered connections, or just paranoid about network traffic. No sign-up forms, no daily quotas, no watermarks. Process as many images as you want. The only limit is your browser's memory. Here's what happens under the hood when you drop an image into the editor: The entire engine is a from-scratch JavaScript implementation inspired by Perl's Image::ExifTool. Let's walk through the interesting parts. Before we can parse anything, we need to know what we're looking at. File extensions lie. Magic numbers don't. function extractMetadata(input, options = {}) { let buffer; if (input instanceof ArrayBuffer) { buffer = Buffer.from(input); } else if (input instanceof Uint8Array) { buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength); } const sig2 = buffer.toString('ascii', 0, 2); const sig4 = buffer.toString('ascii', 0, 4); const sig8 = buffer.length >= 12 ? buffer.toString('ascii', 8, 12) : ''; const magic16 = (buffer[0] = 0xD0 && marker 0) { processIFD(state, nextOffset, 'IFD1', result); // thumbnail } return result; } The processIFD function reads the number of entries, loops through each 12-byte tag record, and dispatches to the appropriate value reader based on the tag's data format (BYTE, ASCII, SHORT, LONG, RATIONAL, etc.). EXIF isn't flat. IFD0 can contain offsets to sub-IFDs: ExifIFD (tag 0x8769): camera settings like ISO, aperture, shutter speed GPSInfo (tag 0x8825): latitude, longitude, altitude InteropIFD (tag 0xA005): interoperability info IFD1: thumbnail image The parser recursively follows these offsets, tracking visited locations to prevent infinite loops from malformed files. Raw TIFF values are often meaningless without context. A GPS latitude isn't a single number — it's three rationals representing degrees, minutes, and seconds. The ValueConverter.js module handles these translations: // GPS coordinate: [deg/1, min/1, sec/100] → "40.7128° N" function printGPSCoord(value, ref) { const deg = value[0][0] / value[0][1]; const min = value[1][0] / value[1][1]; const sec = value[2][0] / value[2][1]; const decimal = deg + min / 60 + sec / 3600; return `${decimal.toFixed(4)}° ${ref}`; } Parsing is half the battle. The other half is rewriting the file with your changes applied. The ExifEditor class provides a simple API modeled after Perl's Image::ExifTool: class ExifEditor { constructor() { this.newValues = {}; this.deletedTags = new Set(); this.byteOrder = 'II'; } setNewValue(tagName, value) { this.newValues[tagName] = value; this.deletedTags.delete(tagName); } deleteTag(tagName) { this.deletedTags.add(tagName); delete this.newValues[tagName]; } } When you hit "Process EXIF," the editor: Reads the current metadata from the image Gathers all tags organized by IFD Removes tags in the deletion set Overwrites tags with new values Rebuilds the TIFF EXIF block from scratch Injects it back into the original file format The buildTIFF function in ExifWriter.js is essentially the inverse of the parser. It takes tags grouped by IFD, resolves tag names to numeric IDs, determines the correct binary format for each value, and lays out the entire directory structure: function buildTIFF(tagsByIFD, tagTable, byteOrder = 'II', gpsTagTable = null) { const isLE = byteOrder === 'II'; // Resolve tag names to IDs and formats const ifdData = {}; for (const [ifdName, tags] of Object.entries(tagsByIFD)) { const entries = []; const activeTable = (ifdName === 'GPS') ? gpsTagTable : tagTable; for (const [tagName, value] of Object.entries(tags)) { const tagID = findTagID(activeTable, tagName); const format = resolveFormat(value, tagInfo); entries.push({ tagID, name: tagName, value, format }); } entries.sort((a, b) => a.tagID - b.tagID); ifdData[ifdName] = entries; } // ...layout calculation and binary serialization } Tags are sorted by ID (TIFF spec requirement). Values larger than 4 bytes get stored in a data area after the directory entries, with the directory entry containing an offset pointer. Smaller values fit directly into the 4-byte "value/offset" field of the entry. Different image formats embed EXIF data differently, so the editor handles each one separately. _writeJPEG(buffer, currentMeta) { const exifBlock = this._buildExifBlock(currentMeta); // Find existing APP1 segment let app1Offset = -1, app1Length = 0, pos = 2; while (pos = 0) { // Replace existing chunk for (let i = 0; i c.type === 'IDAT'); if (idatIndex = 0) { for (let i = 0; i < webpInfo.chunks.length; i++) { if (i === exifChunkIndex) chunkBufs.push(makeRiffChunk('EXIF', exifBlock)); else chunkBufs.push(makeRiffChunk(webpInfo.chunks[i].type, webpInfo.chunks[i].data)); } } else { for (const chunk of webpInfo.chunks) { chunkBufs.push(makeRiffChunk(chunk.type, chunk.data)); } chunkBufs.push(makeRiffChunk('EXIF', exifBlock)); } const allData = Buffer.concat(chunkBufs); const fileSize = 4 + allData.length; const riffHeader = Buffer.concat([ Buffer.from('RIFF'), writeUInt32LE(fileSize), Buffer.from('WEBP') ]); return Buffer.concat([riffHeader, allData]); } For TIFF files (and TIFF-based RAW files), the EXIF data is the file structure. The editor rebuilds the entire IFD structure and returns it as the new file buffer. The frontend is a React client component that bridges raw binary metadata and human-friendly editing. Key design decisions: With over 100 possible EXIF tags, a flat list is overwhelming. Fields are grouped into categories: const categories = [ { key: "camera", label: "Camera", emoji: "📷" }, { key: "datetime", label: "Date/Time", emoji: "🕐" }, { key: "exposure", label: "Exposure", emoji: "☀️" }, { key: "lens", label: "Lens", emoji: "🔭" }, { key: "color", label: "Color", emoji: "🎨" }, { key: "author", label: "Author", emoji: "👤" }, { key: "location", label: "Location", emoji: "📍" }, ]; Each field shows a checkbox (to keep or remove), a label, the current value, and an edit button. Modified fields get a green badge. New fields get a blue badge. GPS coordinates get degree symbols and hemisphere labels. Exposure times like "1/250" stay as fractions. ISO values stay plain numbers. The formatExifValue function handles the messiness: function formatExifValue(key, value) { if (typeof value === 'number') { if (key.toLowerCase().includes('latitude') || key.toLowerCase().includes('longitude')) { return value.toFixed(6) + '°'; } if (key.toLowerCase().includes('altitude')) { return value.toFixed(1) + ' m'; } } if (value instanceof Date) return value.toLocaleString(); if (Array.isArray(value)) return value.join(', '); return String(value); } One tricky detail: the core parser was originally written for Node.js, which has a native Buffer class. Browsers don't. The browser entry point fixes this by polyfilling Buffer into globalThis before loading any internal modules: import { Buffer } from "buffer"; if (typeof globalThis !== "undefined" && !globalThis.Buffer) { globalThis.Buffer = Buffer; } import { extractMetadata } from "./src/ExifTool.js"; import { ExifEditor } from "./src/ExifEditor.js"; This lets us reuse the same parsing and writing code across Node.js CLI tools and the browser UI without forking the logic. There are existing JavaScript EXIF libraries. Most of them only read metadata. The ones that write often only support JPEG, or they require WASM binaries, or they don't handle PNG/WebP at all. Building our own gave us: Full read/write support for JPEG, PNG, WebP, and TIFF Pure JavaScript — no WASM, no native modules, no service workers Complete control over which tags get preserved, modified, or stripped Small bundle size — we only ship the tag tables we actually use Got a photo with questionable metadata? Want to strip GPS before posting to social media? Need to batch-edit copyright info on a folder of images? Head over to our free online EXIF editor. Upload your image, check the fields you want to keep, edit or delete the rest, and download a clean copy. Everything happens in your browser — your photos never touch our servers.
