Toolmingo
Guides8 min read

What Is EXIF Data? (How to Read, Write, and Strip Image Metadata)

Learn what EXIF data is, which metadata fields cameras embed in JPEG/PNG/TIFF files, and how to read, edit, or remove EXIF data with JavaScript, Python, Go, and PHP. Includes privacy risks and the orientation-tag trap.

What Is EXIF Data?

Every photo you take with a smartphone or camera contains hidden data embedded directly inside the image file — camera settings, GPS coordinates, timestamp, device model, and more. This metadata standard is called EXIF (Exchangeable Image File Format).

Understanding EXIF matters for three reasons: privacy (location data follows your photos), automation (scripts that skip orientation correction produce rotated images), and file size (EXIF bloats images unnecessarily when serving on the web).


What EXIF Stores

EXIF is a subset of the larger TIFF/Exif standard. It is embedded in JPEG, TIFF, WebP, HEIC, and some PNG files. Common fields:

Field Example value What it means
Make / Model Apple, iPhone 15 Pro Device brand and model
DateTime 2026-07-13 09:41:22 When the photo was taken
GPSLatitude / GPSLongitude 42.441472 N, 18.9636 E Location where shot
GPSAltitude 15 m above sea level Elevation
Orientation 6 (rotate 90° CW) How to display the image
ExposureTime 1/120 s Shutter speed
FNumber f/1.8 Aperture
ISO 200 Sensor sensitivity
FocalLength 24 mm Lens focal length
Flash 0 (off) Whether flash fired
Software Adobe Lightroom 7.0 Editing software used
Copyright © Teodor 2026 Copyright string
ImageWidth / ImageLength 4032 × 3024 Pixel dimensions

EXIF is separate from IPTC (caption, keywords, creator) and XMP (Adobe's extensible metadata). Tools that "strip all metadata" typically remove all three.


The Orientation Tag Trap

The most common EXIF bug in image processing:

Photo taken in portrait:
  Raw pixel data: 3024 × 4032  (landscape in memory)
  EXIF Orientation tag: 6      ("rotate 90° CW for display")
  
Photo viewers apply the tag → looks correct on screen.

Your script flips/resizes/grayscales without reading the tag →
result is sideways or upside-down.

Orientation values and their meaning:

Tag value Transformation needed
1 None (already correct)
2 Flip horizontal
3 Rotate 180°
4 Flip vertical
5 Flip horizontal, rotate 270° CW
6 Rotate 90° CW
7 Flip horizontal, rotate 90° CW
8 Rotate 270° CW (= 90° CCW)

Rule: always apply (or strip) EXIF orientation before any other image operation.


Reading EXIF Data

JavaScript — Browser

// Using the 'exifr' library (supports JPEG, TIFF, HEIC, WebP)
// npm install exifr
import exifr from 'exifr';

async function readExif(file) {
  const tags = await exifr.parse(file, {
    tiff: true,
    exif: true,
    gps: true,
    interop: false,
    ifd1: false,
  });

  if (!tags) return null;

  return {
    make:        tags.Make,
    model:       tags.Model,
    takenAt:     tags.DateTimeOriginal,
    lat:         tags.latitude,
    lon:         tags.longitude,
    orientation: tags.Orientation,
    width:       tags.ImageWidth,
    height:      tags.ImageHeight,
  };
}

// Usage with file input
document.querySelector('#file').addEventListener('change', async (e) => {
  const info = await readExif(e.target.files[0]);
  console.log(info);
});

JavaScript — Node.js

// npm install exifr
import exifr from 'exifr';
import { readFile } from 'fs/promises';

const buf = await readFile('photo.jpg');
const tags = await exifr.parse(buf);

console.log('Taken:', tags?.DateTimeOriginal);
console.log('GPS:',   tags?.latitude, tags?.longitude);
console.log('Orientation:', tags?.Orientation ?? 1);

Python

# pip install Pillow piexif
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
import piexif

def read_exif(path: str) -> dict:
    img = Image.open(path)
    raw = img._getexif()          # returns {tag_id: value} or None
    if not raw:
        return {}

    result = {}
    for tag_id, value in raw.items():
        tag_name = TAGS.get(tag_id, tag_id)
        result[tag_name] = value
    return result

def get_gps(path: str):
    """Return (lat, lon) or None."""
    img = Image.open(path)
    raw = img._getexif()
    if not raw:
        return None

    gps_data = raw.get(34853)     # GPSInfo tag ID
    if not gps_data:
        return None

    def dms_to_decimal(dms, ref):
        d, m, s = dms
        decimal = float(d) + float(m) / 60 + float(s) / 3600
        if ref in ('S', 'W'):
            decimal = -decimal
        return decimal

    lat = dms_to_decimal(gps_data[2], gps_data[1])
    lon = dms_to_decimal(gps_data[4], gps_data[3])
    return lat, lon

# Print all tags
for tag, value in read_exif('photo.jpg').items():
    print(f'{tag}: {value}')

Go

package main

import (
    "fmt"
    "os"

    "github.com/dsoprea/go-exif/v3"
    exifcommon "github.com/dsoprea/go-exif/v3/common"
)

func readExif(path string) {
    data, err := os.ReadFile(path)
    if err != nil {
        panic(err)
    }

    rawExif, err := exif.SearchAndExtractExif(data)
    if err != nil {
        fmt.Println("No EXIF found")
        return
    }

    entries, _, err := exif.GetFlatExifData(rawExif, &exif.ScanOptions{})
    if err != nil {
        panic(err)
    }

    for _, entry := range entries {
        fmt.Printf("%-30s %v\n", entry.TagName, entry.Formatted)
    }
}

// go get github.com/dsoprea/go-exif/v3
func main() {
    readExif("photo.jpg")
}

PHP

<?php
// Built-in — no extension needed for JPEG EXIF
function readExif(string $path): array {
    $data = exif_read_data($path, sections: 'ANY_TAG', arrays: true);
    if ($data === false) {
        return [];
    }
    return $data;
}

$tags = readExif('photo.jpg');

echo 'Camera: '  . ($tags['IFD0']['Make'] ?? 'unknown') . ' '
                 . ($tags['IFD0']['Model'] ?? '') . PHP_EOL;
echo 'Taken:  '  . ($tags['EXIF']['DateTimeOriginal'] ?? 'unknown') . PHP_EOL;
echo 'ISO:    '  . ($tags['EXIF']['ISOSpeedRatings'] ?? 'unknown') . PHP_EOL;

// GPS (stored as rational fractions)
if (isset($tags['GPS']['GPSLatitude'])) {
    function dmsToDecimal(array $dms, string $ref): float {
        [$d, $m, $s] = array_map(fn($v) => eval('return ' . str_replace('/', '/', $v) . ';'), $dms);
        $dec = $d + $m / 60 + $s / 3600;
        return in_array($ref, ['S', 'W'], true) ? -$dec : $dec;
    }
    $lat = dmsToDecimal($tags['GPS']['GPSLatitude'], $tags['GPS']['GPSLatitudeRef']);
    $lon = dmsToDecimal($tags['GPS']['GPSLongitude'], $tags['GPS']['GPSLongitudeRef']);
    echo "GPS: $lat, $lon" . PHP_EOL;
}

Stripping EXIF (Privacy + File Size)

Removing EXIF reduces file size by 5–50 KB per photo and prevents GPS coordinates from leaking when images are shared publicly.

JavaScript (Sharp)

import sharp from 'sharp';

// Strip all metadata (EXIF, IPTC, XMP, ICC profile)
await sharp('input.jpg')
  .withMetadata(false)   // default — metadata is stripped
  .toFile('output.jpg');

// Preserve ICC colour profile only (safe for colour-accurate display)
await sharp('input.jpg')
  .withMetadata({ icc: true })
  .toFile('output.jpg');

Python (Pillow)

from PIL import Image
import io

def strip_exif(input_path: str, output_path: str) -> None:
    img = Image.open(input_path)

    # Convert to RGB to drop alpha (needed for JPEG)
    if img.mode in ('RGBA', 'P'):
        img = img.convert('RGB')

    # Save WITHOUT passing exif= parameter → no EXIF in output
    img.save(output_path, 'JPEG', quality=90, optimize=True)

strip_exif('photo.jpg', 'clean.jpg')

Python (piexif — edit specific tags)

import piexif

# Load existing EXIF
exif_dict = piexif.load('photo.jpg')

# Remove GPS block entirely
exif_dict.pop('GPS', None)

# Update copyright
exif_dict['0th'][piexif.ImageIFD.Copyright] = b'\xc2\xa9 2026 Teodor'

# Write back
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, 'photo.jpg')

Go

// Using goexif/exif — strip by re-encoding without EXIF
import (
    "image/jpeg"
    "os"
)

func stripExif(src, dst string) error {
    in, _ := os.Open(src)
    defer in.Close()

    img, _ := jpeg.Decode(in)   // JPEG decoder discards EXIF

    out, _ := os.Create(dst)
    defer out.Close()

    return jpeg.Encode(out, img, &jpeg.Options{Quality: 90})
}

PHP (GD — re-encode without EXIF)

<?php
function stripExif(string $src, string $dst, int $quality = 90): void {
    $img = imagecreatefromjpeg($src); // GD drops EXIF on load
    imagejpeg($img, $dst, $quality);
    imagedestroy($img);
}

stripExif('photo.jpg', 'clean.jpg');

CLI

# ImageMagick — strip all profiles and metadata
convert input.jpg -strip output.jpg

# ExifTool — remove all EXIF
exiftool -all= input.jpg

# ExifTool — remove GPS only, keep other EXIF
exiftool -gps:all= input.jpg

Quick Reference

Task JS (Sharp) Python (Pillow/piexif) PHP
Read tags exifr.parse() img._getexif() exif_read_data()
Fix orientation .rotate() (no arg) ImageOps.exif_transpose() imagerotate() + tag
Strip all EXIF .withMetadata(false) save without exif= GD imagecreatefromjpeg
Remove GPS only piexif pop('GPS') piexif pop('GPS') exiftool -gps:all=
Keep ICC profile .withMetadata({icc:true}) img.info.get('icc_profile') imagick->stripImage() careful

Privacy Risks

Photos posted online with EXIF intact can expose:

  • Home address — GPS from photos taken at home
  • Daily routine — timestamps from regularly taken photos
  • Device fingerprint — exact phone model, firmware, lens serial number

When to strip EXIF:

  • User-generated content on public platforms (profile photos, product images)
  • Any image uploaded and served back to other users
  • Images in version control (screenshots often contain display metadata)

When to keep EXIF:

  • Professional photography archives (camera settings matter)
  • Legal evidence (timestamp and GPS chain of custody)
  • Internal asset management

6 Common Mistakes

  1. Skipping orientation correction — always call exif_transpose() / .rotate() / autoOrient() before any other operation or the image may be sideways.

  2. Assuming JPEG always has EXIF — screenshots, AI-generated images, and web-downloaded photos often have no EXIF at all. Always handle the null / false / error case.

  3. Reading EXIF on PNG — standard PNG does not support EXIF (only a non-standard chunk). Use XMP or iTXt chunks instead. exif_read_data() returns false for PNG.

  4. Losing ICC colour profile when stripping — stripping all metadata removes the ICC profile, which can cause colour shifts on wide-gamut displays. Preserve it with Sharp's {icc: true} option.

  5. Trusting DateTimeOriginal as the timezone — EXIF timestamps are local device time with no timezone offset. You cannot infer UTC without the OffsetTimeOriginal tag (rare) or external context.

  6. GPS stored as DMS fractions, not decimals — EXIF GPS is three rational numbers (degrees, minutes, seconds), each stored as a fraction (e.g., 48/1, 51/1, 235/100). Convert to decimal before using.


FAQ

Does PNG support EXIF? Not in the official spec. A non-standard eXIf chunk exists (PNG 1.6+), but support is inconsistent. Prefer JPEG, TIFF, WebP, or HEIC for EXIF-bearing photos.

Does stripping EXIF change image quality? No — EXIF is metadata stored separately from pixel data. Stripping it does not re-compress the image.

Does WebP support EXIF? Yes — WebP can carry EXIF, XMP, and ICC profile chunks. Sharp preserves them by default; pass .withMetadata(false) to strip.

Can I add custom EXIF tags? Yes, within limits. Standard tags have fixed IDs (defined in the EXIF spec). You can write any standard tag with piexif (Python) or ExifTool. Arbitrary custom data is better stored in XMP.

Does uploading to social media strip EXIF? Most platforms (Instagram, Twitter/X, Facebook) strip EXIF and GPS before storing. WhatsApp preserves EXIF on direct sends. Never rely on a platform to protect your privacy — strip before upload.

What tool reads EXIF without writing code? exiftool -a -u photo.jpg in the terminal shows every tag. On macOS: mdls photo.jpg. On Windows: right-click → Properties → Details tab.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools