Read and strip image EXIF data with JavaScript: camera, date and GPS in the browser and Node.js
A photo straight from a phone usually contains EXIF metadata: the make and model of the camera, when it was taken, the orientation, and often the GPS coordinates of where it was taken. That's useful if you're building a photo gallery, and a privacy problem if you're publishing user uploads.
This post reads EXIF with the exifreader package (v4.46.0) in the browser and in Node.js, then shows two ways to strip it. The test image is a real JPEG tagged with exiftool: camera "Google Pixel 9", taken 2026-08-14 09:41:27, GPS 48.858370 N 2.294481 E (the Eiffel Tower), altitude 35 m, and Orientation = "Rotate 90 CW", like a portrait shot from a phone. The browser code ran in headless Chromium 153, Firefox 155 and WebKit 26.6 using the official Playwright image. The Node.js code ran on node:24-slim and node:26-slim.
Reading EXIF in the browser
No build step is needed. ExifReader ships a browser bundle that defines a global ExifReader:
<label>Photo: <input type="file" id="photo" accept="image/jpeg,image/png,image/heic,image/webp"></label>
<pre id="output"></pre>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/exif-reader.js"></script>
<script src="browser.js"></script>// Read EXIF from the picked file
const input = document.querySelector('#photo');
const output = document.querySelector('#output');
input.addEventListener('change', async () => {
const file = input.files[0];
if (!file) return;
const tags = await ExifReader.load(file, { expanded: true });
const { exif = {}, gps } = tags;
output.textContent = JSON.stringify({
camera: [exif.Make?.description, exif.Model?.description].filter(Boolean).join(' ') || null,
taken: exif.DateTimeOriginal?.description ?? null,
gps: gps ? { lat: gps.Latitude, lon: gps.Longitude } : null,
}, null, 2);
});With { expanded: true }, tags are grouped by standard (exif, gps, iptc, xmp, …) so they can't overwrite each other, and gps gives ready-to-use decimal degrees. Coordinates south of the equator or west of Greenwich are negative. Each tag has a raw value and a human-readable description. All three engines produced the same result:
{"camera":"Google Pixel 9","taken":"2026:08:14 09:41:27","gps":{"lat":48.85837,"lon":2.294481000090261}}Note the accept attribute. ExifReader's README warns that accept="image/*" can make Android (and possibly iOS) strip the GPS data from the picked file before your code ever sees it. Listing explicit types, as above, avoids that. The file never leaves the browser, either: ExifReader reads it from the File object locally.
Reading EXIF in Node.js
The same API takes a file path (or a Buffer) in Node.js:
npm install exifreaderimport ExifReader from 'exifreader';
const tags = await ExifReader.load(process.argv[2] ?? 'photo.jpg', { expanded: true });
console.log('Camera: ', tags.exif?.Make?.description, tags.exif?.Model?.description);
console.log('Taken: ', tags.exif?.DateTimeOriginal?.description);
console.log('Orientation:', tags.exif?.Orientation?.description);
console.log('GPS: ', tags.gps ?? 'none');$ node read.mjs photo.jpg
Camera: Google Pixel 9
Taken: 2026:08:14 09:41:27
Orientation: right-top
GPS: { Latitude: 48.85837, Longitude: 2.294481000090261, Altitude: 35 }Node 26.10 printed the same. right-top is ExifReader's description of orientation 6, "rotate 90° clockwise to display". The pixels are stored landscape (1200×539), and viewers are expected to rotate them.
Stripping metadata in the browser before upload
If users upload photos that other people will see, remove the location before it leaves their device. Redrawing the image onto a canvas and re-encoding it writes a new file with no metadata at all:
// Re-encode through a canvas: the result carries no EXIF at all
async function withoutMetadata(file, type = 'image/jpeg', quality = 0.9) {
// createImageBitmap applies the EXIF orientation by default,
// so the pixels are already the right way up before the tag is dropped.
const bitmap = await createImageBitmap(file);
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
canvas.getContext('2d').drawImage(bitmap, 0, 0);
bitmap.close();
return canvas.convertToBlob({ type, quality });
}Checking the result with ExifReader in each engine:
chromium 153 {"type":"image/jpeg","size":187288,"gps":null,"exifKeys":0,"w":539,"h":1200}
firefox 155 {"type":"image/jpeg","size":228156,"gps":null,"exifKeys":0,"w":539,"h":1200}
webkit 26.6 {"type":"image/jpeg","size":187288,"gps":null,"exifKeys":0,"w":539,"h":1200}No EXIF tags at all, no GPS, and the image is 539×1200, upright. That last part matters: the orientation tag is gone too, so if the pixels hadn't been rotated first, the portrait photo would now display sideways. createImageBitmap() applies the EXIF orientation by default, so the rotation is baked in before the tag disappears. (The file size differs between engines because each uses its own JPEG encoder. The original was 148 kB.) OffscreenCanvas.convertToBlob() is available in Chrome 69, Firefox 105 and Safari 16.4. With a regular <canvas>, use canvas.toBlob() instead.
Re-encoding also recompresses the image. If you'd rather not touch the pixels, strip it on the server instead, where you control the encoder.
Stripping metadata in Node.js with sharp
On the server, sharp drops metadata by default. That's exactly what you want for GPS, and exactly what breaks orientation:
import sharp from 'sharp';
// Default: sharp writes no EXIF at all, so GPS is gone, and so is Orientation.
await sharp('photo.jpg').toFile('out-default.jpg');
// autoOrient bakes the rotation into the pixels first, so dropping the tag is safe.
await sharp('photo.jpg', { autoOrient: true }).toFile('out-oriented.jpg');
// keepExif() copies everything, GPS included.
await sharp('photo.jpg').keepExif().toFile('out-keepexif.jpg');
for (const f of ['photo.jpg', 'out-default.jpg', 'out-oriented.jpg', 'out-keepexif.jpg']) {
const m = await sharp(f).metadata();
console.log(`${f.padEnd(17)} ${m.width}×${m.height} orientation: ${m.orientation ?? '-'} exif: ${m.exif ? m.exif.length + ' bytes' : 'none'}`);
}$ node strip.mjs
photo.jpg 1200×539 orientation: 6 exif: 328 bytes
out-default.jpg 1200×539 orientation: - exif: none
out-oriented.jpg 539×1200 orientation: - exif: none
out-keepexif.jpg 1200×539 orientation: 6 exif: 416 bytesout-default.jpg: GPS gone, but so is the orientation tag. The pixels are still landscape, so the portrait photo now shows sideways. This is the classic "my uploads are rotated" bug.out-oriented.jpg:autoOrient: truerotates the pixels first (539×1200), then the metadata is dropped. Use this for uploads.out-keepexif.jpg:keepExif()preserves everything, including GPS. ExifReader still foundLatitude: 48.85837in it.
If you're also converting uploads to WebP or AVIF, the same autoOrient option works in the batch script from Batch convert a folder of images to WebP and AVIF.
Sources & further reading
- ExifReader on GitHub (API,
expanded, GPS, and theaccept="image/*"note) - sharp: output options (metadata is removed by default;
keepExif) and constructor options (autoOrient) - MDN: createImageBitmap() (
imageOrientation) - MDN: OffscreenCanvas.convertToBlob()
- MDN: image-orientation (browsers honour EXIF orientation by default)
- ExifTool, used to tag the test image