Easy to use Python module to extract Exif metadata from digital image files. Supports multiple image formats including TIFF, JPEG, JPEG XL, PNG, Webp, HEIC, and RAW files with zero external dependencies.
pip install exifreadimport exifreadFor specific components:
from exifread import process_file
from exifread.core.exceptions import ExifError, InvalidExif, ExifNotFound
from exifread.core.ifd_tag import IfdTag
from exifread.utils import get_gps_coords, Ratio
from exifread.serialize import convert_typesExifRead uses a multi-layered architecture for EXIF metadata extraction:
The core process_file() function orchestrates this pipeline with configurable options for performance optimization (quick mode, early termination) and output format control (builtin types, thumbnail extraction). The architecture supports both lenient processing (warnings for errors) and strict processing (exceptions on errors) to accommodate various use cases from casual metadata viewing to production data processing.
import exifread
# Basic EXIF extraction
with open("image.jpg", "rb") as f:
tags = exifread.process_file(f)
# Iterate through tags (skip long/boring ones)
for tag, value in tags.items():
if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'EXIF MakerNote'):
print(f"Key: {tag}, value: {value}")
# Quick processing (skip MakerNotes and thumbnails)
with open("image.jpg", "rb") as f:
tags = exifread.process_file(f, details=False, extract_thumbnail=False)
# Convert to built-in Python types for JSON serialization
with open("image.jpg", "rb") as f:
tags = exifread.process_file(f, builtin_types=True)
import json
json_data = json.dumps(tags)
# Extract GPS coordinates
from exifread.utils import get_gps_coords
coords = get_gps_coords(tags) # Returns (latitude, longitude) or None
if coords:
lat, lng = coords
print(f"GPS: {lat}, {lng}")Extract comprehensive EXIF metadata from digital image files with configurable processing options for performance optimization and data format conversion.
def process_file(
fh,
stop_tag="UNDEF",
details=True,
strict=False,
debug=False,
truncate_tags=True,
auto_seek=True,
extract_thumbnail=True,
builtin_types=False
) -> dict:
"""
Process an image file to extract EXIF metadata.
Parameters:
- fh: Binary file handle opened in 'rb' mode
- stop_tag: Stop processing when this tag is retrieved (default: "UNDEF")
- details: If True, process MakerNotes for camera-specific metadata (default: True)
- strict: If True, raise exceptions on errors instead of logging warnings (default: False)
- debug: Output detailed debug information during processing (default: False)
- truncate_tags: If True, truncate printable tag output for readability (default: True)
- auto_seek: If True, automatically seek to start of file (default: True)
- extract_thumbnail: If True, extract JPEG thumbnail if present (default: True)
- builtin_types: If True, convert tags to standard Python types for serialization (default: False)
Returns:
Dictionary mapping tag names to values. Keys are formatted as "IFD_NAME TAG_NAME".
Values are IfdTag objects (builtin_types=False) or Python built-in types (builtin_types=True).
Raises:
- InvalidExif: When EXIF data is malformed (strict=True only)
- ExifNotFound: When no EXIF data is found (strict=True only)
"""Extract latitude and longitude coordinates from EXIF GPS tags with automatic conversion to decimal degrees format.
def get_gps_coords(tags: dict) -> tuple[float, float] | None:
"""
Extract GPS coordinates from EXIF tags.
Parameters:
- tags: Dictionary of EXIF tags from process_file()
Returns:
Tuple of (latitude, longitude) in decimal degrees, or None if GPS data not found.
Handles both IfdTag objects and serialized tags.
"""Convert EXIF tag objects to standard Python types for easier programmatic use and JSON serialization.
def convert_types(exif_tags: dict) -> dict:
"""
Convert Exif IfdTags to built-in Python types.
Parameters:
- exif_tags: Dictionary of IfdTag objects from process_file()
Returns:
Dictionary with same keys but values converted to int, float, str, bytes, list, or None.
Single-element lists are unpacked to individual values.
"""Process image files from the command line with various output and processing options.
def main() -> None:
"""Main CLI entry point. Processes command line arguments and extracts EXIF data."""
def run_cli(args: argparse.Namespace) -> None:
"""
Execute CLI processing with parsed arguments.
Parameters:
- args: Parsed command line arguments from get_args()
"""
def get_args() -> argparse.Namespace:
"""
Parse command line arguments.
Returns:
argparse.Namespace with parsed CLI options
"""CLI Usage:
# Basic usage
EXIF.py image.jpg
# Multiple files with quick processing and builtin types
EXIF.py -q -b image1.jpg image2.tiff
# Stop at specific tag with strict error handling
EXIF.py -t DateTimeOriginal -s image.jpg
# Debug mode with color output
EXIF.py -d -c image.jpg
# Module execution
python -m exifread image.jpgCLI Options:
-v, --version: Display version information-q, --quick: Skip MakerNotes and thumbnails for faster processing-t TAG, --tag TAG: Stop processing when specified tag is retrieved-s, --strict: Run in strict mode (raise exceptions on errors)-b, --builtin: Convert IfdTag values to built-in Python types-d, --debug: Run in debug mode with detailed information-c, --color: Enable colored output (POSIX systems only)Configure logging output with debug information and colored formatting for development and troubleshooting.
def get_logger():
"""
Get the exifread logger instance.
Returns:
logging.Logger configured for exifread
"""
def setup_logger(debug: bool, color: bool) -> None:
"""
Configure logger with debug and color settings.
Parameters:
- debug: Enable debug level logging
- color: Enable colored console output
"""class IfdTag:
"""
Represents an IFD (Image File Directory) tag containing EXIF metadata.
Attributes:
- printable: Human-readable version of the tag data (str)
- tag: Numeric tag identifier (int)
- field_type: EXIF field type from FieldType enum
- values: Raw tag values (various types: str, bytes, list)
- field_offset: Byte offset of field start in IFD (int)
- field_length: Length of data field in bytes (int)
- prefer_printable: Whether to prefer printable over raw values for serialization (bool)
"""
def __init__(
self,
printable: str,
tag: int,
field_type: FieldType,
values,
field_offset: int,
field_length: int,
prefer_printable: bool = True
) -> None: ...
def __str__(self) -> str:
"""Return printable representation of tag."""
def __repr__(self) -> str:
"""Return detailed representation with field type and offset information."""class Ratio:
"""
Represents EXIF ratio values, extending fractions.Fraction with additional methods.
Used for GPS coordinates, exposure times, and other fractional EXIF data.
"""
def __new__(cls, numerator: int = 0, denominator: int | None = None): ...
@property
def num(self) -> int:
"""Get numerator value."""
@property
def den(self) -> int:
"""Get denominator value."""
def decimal(self) -> float:
"""Convert ratio to decimal floating point value."""class FieldType:
"""
EXIF field type enumeration defining data storage formats.
"""
PROPRIETARY = 0
BYTE = 1
ASCII = 2
SHORT = 3
LONG = 4
RATIO = 5
SIGNED_BYTE = 6
UNDEFINED = 7
SIGNED_SHORT = 8
SIGNED_LONG = 9
SIGNED_RATIO = 10
FLOAT_32 = 11
FLOAT_64 = 12
IFD = 13class ExifError(Exception):
"""Base exception class for all ExifRead errors."""
class InvalidExif(ExifError):
"""Raised when EXIF data is malformed or corrupted."""
class ExifNotFound(ExifError):
"""Raised when no EXIF data is found in the image file."""__version__: str = "3.5.1"
"""Package version string."""
DEFAULT_STOP_TAG: str = "UNDEF"
"""Default tag name to stop processing at."""
IGNORE_TAGS: list[int] = [0x02BC, 0x927C, 0x9286]
"""Tag IDs ignored during quick processing mode."""
FIELD_DEFINITIONS: dict
"""Maps FieldType values to (byte_length, description) tuples."""import exifread
import os
from pathlib import Path
def process_directory(directory_path):
"""Process all JPEG images in a directory."""
image_extensions = {'.jpg', '.jpeg', '.tiff', '.tif'}
for file_path in Path(directory_path).iterdir():
if file_path.suffix.lower() in image_extensions:
try:
with open(file_path, 'rb') as f:
tags = exifread.process_file(f, builtin_types=True)
if tags:
print(f"\n{file_path.name}:")
# Print camera info
camera = tags.get('Image Make', 'Unknown')
model = tags.get('Image Model', 'Unknown')
print(f" Camera: {camera} {model}")
# Print capture date
date_taken = tags.get('EXIF DateTimeOriginal', 'Unknown')
print(f" Date: {date_taken}")
# Print GPS if available
from exifread.utils import get_gps_coords
coords = get_gps_coords(tags)
if coords:
lat, lng = coords
print(f" GPS: {lat:.6f}, {lng:.6f}")
except Exception as e:
print(f"Error processing {file_path.name}: {e}")
# Usage
process_directory("/path/to/photos")import exifread
def extract_specific_tags(file_path, target_tags):
"""Extract only specific EXIF tags efficiently."""
with open(file_path, 'rb') as f:
# Use quick mode and stop early for efficiency
tags = exifread.process_file(
f,
details=False, # Skip MakerNotes
extract_thumbnail=False, # Skip thumbnails
builtin_types=True # Get Python types
)
result = {}
for tag_name in target_tags:
if tag_name in tags:
result[tag_name] = tags[tag_name]
return result
# Extract specific metadata
important_tags = [
'Image Make',
'Image Model',
'EXIF DateTimeOriginal',
'EXIF ExposureTime',
'EXIF FNumber',
'EXIF ISOSpeedRatings'
]
metadata = extract_specific_tags('photo.jpg', important_tags)
print(metadata)import exifread
from PIL import Image
def correct_image_orientation(image_path):
"""Correct image orientation based on EXIF data."""
# Read EXIF orientation
with open(image_path, 'rb') as f:
tags = exifread.process_file(f, details=False)
# Open image
img = Image.open(image_path)
# Check for orientation tag
orientation = tags.get('Image Orientation')
if orientation:
orientation_value = orientation.values[0] if hasattr(orientation, 'values') else orientation
# Apply rotation based on orientation
if orientation_value == 3:
img = img.transpose(Image.ROTATE_180)
elif orientation_value == 6:
img = img.transpose(Image.ROTATE_270)
elif orientation_value == 8:
img = img.transpose(Image.ROTATE_90)
return img
# Usage
corrected_image = correct_image_orientation('photo.jpg')
corrected_image.save('corrected_photo.jpg')The library provides different error handling approaches based on the strict parameter:
Lenient Mode (default):
# Warnings logged, empty dict returned on errors
with open('corrupted.jpg', 'rb') as f:
tags = exifread.process_file(f) # Returns {} if no EXIF foundStrict Mode:
from exifread.core.exceptions import ExifNotFound, InvalidExif
try:
with open('image.jpg', 'rb') as f:
tags = exifread.process_file(f, strict=True)
except ExifNotFound:
print("No EXIF data found in image")
except InvalidExif:
print("EXIF data is corrupted or invalid")
except Exception as e:
print(f"Unexpected error: {e}")Quick Processing: Use details=False and extract_thumbnail=False for faster processing:
tags = exifread.process_file(f, details=False, extract_thumbnail=False)Early Termination: Stop processing at specific tags:
tags = exifread.process_file(f, stop_tag='DateTimeOriginal')Memory Efficiency: Use built-in types to avoid keeping IfdTag objects:
tags = exifread.process_file(f, builtin_types=True)