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
Skipping orientation correction — always call
exif_transpose()/.rotate()/autoOrient()before any other operation or the image may be sideways.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.Reading EXIF on PNG — standard PNG does not support EXIF (only a non-standard chunk). Use XMP or iTXt chunks instead.
exif_read_data()returnsfalsefor PNG.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.Trusting
DateTimeOriginalas the timezone — EXIF timestamps are local device time with no timezone offset. You cannot infer UTC without theOffsetTimeOriginaltag (rare) or external context.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.