Pure Python library for EXIF metadata manipulation in JPEG and WebP image files.
npx @tessl/cli install tessl/pypi-piexif@1.1.0Pure Python library for EXIF (Exchangeable Image File Format) metadata manipulation in JPEG and WebP image files. Piexif provides a simple and comprehensive API with five core functions for loading, dumping, inserting, removing, and transplanting EXIF data without external dependencies.
pip install piexifimport piexifFor specific functionality:
from piexif import load, dump, insert, remove, transplant
from piexif import ImageIFD, ExifIFD, GPSIFD, InteropIFD, TYPES, TAGS
from piexif.helper import UserCommentimport piexif
# Load EXIF data from an image
exif_dict = piexif.load("image.jpg")
# Examine EXIF data structure
for ifd in ("0th", "Exif", "GPS", "1st"):
for tag in exif_dict[ifd]:
tag_name = piexif.TAGS[ifd][tag]["name"]
print(f"{tag_name}: {exif_dict[ifd][tag]}")
# Modify EXIF data
exif_dict["0th"][piexif.ImageIFD.Software] = b"My Software"
exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal] = b"2023:12:25 10:30:00"
# Convert back to bytes and insert into a new image
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, "input.jpg", "output.jpg")Piexif operates on the EXIF (Exchangeable Image File Format) data structure, which is embedded within image files as metadata. The EXIF format organizes metadata into multiple Image File Directories (IFDs), each containing related sets of tags:
This hierarchical structure allows piexif to preserve all metadata relationships while providing simple dictionary-based access to individual tags and values.
Load EXIF metadata from image files into a structured dictionary format.
def load(input_data, key_is_name=False):
"""
Load EXIF data from JPEG, TIFF, or WebP files.
Parameters:
- input_data: str (file path) or bytes (image data)
- key_is_name: bool, optional - Use tag names as keys instead of tag numbers (default: False)
Returns:
dict: EXIF data with IFD structure
{
"0th": dict, # Main image metadata (ImageIFD tags)
"Exif": dict, # Camera-specific metadata (ExifIFD tags)
"GPS": dict, # GPS location data (GPSIFD tags)
"Interop": dict, # Interoperability data (InteropIFD tags)
"1st": dict, # Thumbnail image metadata (ImageIFD tags)
"thumbnail": bytes # Thumbnail JPEG data or None
}
Raises:
InvalidImageDataError: If image data is corrupted or invalid
"""Convert EXIF dictionary data back to binary format for storage in image files.
def dump(exif_dict):
"""
Convert EXIF dictionary to bytes format.
Parameters:
- exif_dict: dict - EXIF data dictionary with IFD structure
Returns:
bytes: EXIF data in binary format with proper headers
Raises:
ValueError: If EXIF dictionary structure is invalid
"""Insert EXIF metadata into image files, supporting both file paths and binary data.
def insert(exif, image, new_file=None):
"""
Insert EXIF data into JPEG or WebP image.
Parameters:
- exif: bytes - EXIF data in binary format (from dump())
- image: str (file path) or bytes (image data) - Target image
- new_file: str, optional - Output file path; if None, modifies original
Returns:
None: Modifies file in place or saves to new_file
Raises:
ValueError: If exif data is not valid EXIF data
InvalidImageDataError: If image format is unsupported
"""Remove all EXIF metadata from image files while preserving image quality.
def remove(src, new_file=None):
"""
Remove EXIF data from JPEG or WebP image.
Parameters:
- src: str (file path) or bytes (image data) - Source image
- new_file: str, optional - Output file path; if None, modifies original
Returns:
None: Modifies file in place or saves to new_file
Raises:
InvalidImageDataError: If image format is unsupported
"""Copy EXIF metadata from one JPEG image to another, useful for preserving metadata during image processing.
def transplant(exif_src, image, new_file=None):
"""
Copy EXIF data from one JPEG to another JPEG.
Parameters:
- exif_src: str (file path) or bytes (JPEG data) - Source JPEG with EXIF
- image: str (file path) or bytes (JPEG data) - Target JPEG image
- new_file: str, optional - Output file path; if None, modifies target
Returns:
None: Modifies file in place or saves to new_file
Raises:
ValueError: If source has no EXIF data or new_file not provided for bytes input
InvalidImageDataError: If image format is not JPEG
Note: Only supports JPEG format, not WebP or TIFF.
"""class TYPES:
"""EXIF data type constants."""
Byte = 1
Ascii = 2
Short = 3
Long = 4
Rational = 5
SByte = 6
Undefined = 7
SShort = 8
SLong = 9
SRational = 10
Float = 11
DFloat = 12TAGS: dict
"""
Tag information dictionary mapping IFD names to tag definitions.
Structure:
{
"0th": dict, # Main image tags (same as "Image")
"1st": dict, # Thumbnail image tags (same as "Image")
"Exif": dict, # Camera-specific tags
"GPS": dict, # GPS location tags
"Interop": dict, # Interoperability tags
"Image": dict # Standard TIFF/image tags
}
Each tag entry contains:
{
tag_number: {
"name": str, # Human-readable tag name
"type": int # EXIF data type (from TYPES class)
}
}
"""class ImageIFD:
"""Tag constants for main image metadata (0th and 1st IFD)."""
# Core image properties
ImageWidth = 256
ImageLength = 257
BitsPerSample = 258
Compression = 259
PhotometricInterpretation = 262
Orientation = 274
SamplesPerPixel = 277
XResolution = 282
YResolution = 283
ResolutionUnit = 296
# Image description
ImageDescription = 270
Make = 271
Model = 272
Software = 305
DateTime = 306
Artist = 315
Copyright = 33432
# Technical metadata
WhitePoint = 318
PrimaryChromaticities = 319
YCbCrCoefficients = 529
YCbCrSubSampling = 530
YCbCrPositioning = 531
ReferenceBlackWhite = 532
# Pointers to other IFDs
ExifTag = 34665 # Pointer to Exif IFD
GPSTag = 34853 # Pointer to GPS IFD
# Additional core tags
ProcessingSoftware = 11
NewSubfileType = 254
SubfileType = 255
Threshholding = 263
CellWidth = 264
CellLength = 265
FillOrder = 266
DocumentName = 269
StripOffsets = 273
RowsPerStrip = 278
StripByteCounts = 279
PlanarConfiguration = 284
GrayResponseUnit = 290
GrayResponseCurve = 291
T4Options = 292
T6Options = 293
TransferFunction = 301
HostComputer = 316
Predictor = 317
ColorMap = 320
HalftoneHints = 321
TileWidth = 322
TileLength = 323
TileOffsets = 324
TileByteCounts = 325
SubIFDs = 330
InkSet = 332
InkNames = 333
NumberOfInks = 334
DotRange = 336
TargetPrinter = 337
ExtraSamples = 338
SampleFormat = 339
SMinSampleValue = 340
SMaxSampleValue = 341
TransferRange = 342
ClipPath = 343
XClipPathUnits = 344
YClipPathUnits = 345
Indexed = 346
JPEGTables = 347
OPIProxy = 351
# JPEG-specific tags
JPEGProc = 512
JPEGInterchangeFormat = 513
JPEGInterchangeFormatLength = 514
JPEGRestartInterval = 515
JPEGLosslessPredictors = 517
JPEGPointTransforms = 518
JPEGQTables = 519
JPEGDCTables = 520
JPEGACTables = 521
# Metadata and extensions
XMLPacket = 700
Rating = 18246
RatingPercent = 18249
ImageID = 32781
CFARepeatPatternDim = 33421
CFAPattern = 33422
BatteryLevel = 33423
ImageResources = 34377class ExifIFD:
"""Tag constants for camera-specific metadata (Exif IFD)."""
# Exposure settings
ExposureTime = 33434
FNumber = 33437
ExposureProgram = 34850
ISOSpeedRatings = 34855
ShutterSpeedValue = 37377
ApertureValue = 37378
BrightnessValue = 37379
ExposureBiasValue = 37380
MaxApertureValue = 37381
# Date and time
ExifVersion = 36864
DateTimeOriginal = 36867
DateTimeDigitized = 36868
OffsetTime = 36880
OffsetTimeOriginal = 36881
OffsetTimeDigitized = 36882
SubSecTime = 37520
SubSecTimeOriginal = 37521
SubSecTimeDigitized = 37522
# Camera settings
MeteringMode = 37383
LightSource = 37384
Flash = 37385
FocalLength = 37386
SubjectDistance = 37382
SubjectArea = 37396
# Image properties
ColorSpace = 40961
PixelXDimension = 40962
PixelYDimension = 40963
ComponentsConfiguration = 37121
CompressedBitsPerPixel = 37122
# Additional metadata
UserComment = 37510
MakerNote = 37500
FlashpixVersion = 40960
RelatedSoundFile = 40964
# Advanced technical settings
SpectralSensitivity = 34852
OECF = 34856
SensitivityType = 34864
StandardOutputSensitivity = 34865
RecommendedExposureIndex = 34866
ISOSpeed = 34867
ISOSpeedLatitudeyyy = 34868
ISOSpeedLatitudezzz = 34869
Temperature = 37888
Humidity = 37889
Pressure = 37890
WaterDepth = 37891
Acceleration = 37892
CameraElevationAngle = 37893
# Image capture details
FlashEnergy = 41483
SpatialFrequencyResponse = 41484
FocalPlaneXResolution = 41486
FocalPlaneYResolution = 41487
FocalPlaneResolutionUnit = 41488
SubjectLocation = 41492
ExposureIndex = 41493
SensingMethod = 41495
FileSource = 41728
SceneType = 41729
CFAPattern = 41730
# Processing and quality
CustomRendered = 41985
ExposureMode = 41986
WhiteBalance = 41987
DigitalZoomRatio = 41988
FocalLengthIn35mmFilm = 41989
SceneCaptureType = 41990
GainControl = 41991
Contrast = 41992
Saturation = 41993
Sharpness = 41994
DeviceSettingDescription = 41995
SubjectDistanceRange = 41996
ImageUniqueID = 42016
# Camera and lens identification
CameraOwnerName = 42032
BodySerialNumber = 42033
LensSpecification = 42034
LensMake = 42035
LensModel = 42036
LensSerialNumber = 42037
Gamma = 42240
# Interoperability pointer
InteroperabilityTag = 40965class GPSIFD:
"""Tag constants for GPS location metadata (GPS IFD)."""
GPSVersionID = 0
GPSLatitudeRef = 1 # 'N' or 'S'
GPSLatitude = 2 # Degrees, minutes, seconds
GPSLongitudeRef = 3 # 'E' or 'W'
GPSLongitude = 4 # Degrees, minutes, seconds
GPSAltitudeRef = 5 # Above/below sea level
GPSAltitude = 6 # Altitude in meters
GPSTimeStamp = 7 # UTC time as hours, minutes, seconds
GPSSatellites = 8 # Satellites used for measurement
GPSStatus = 9 # Receiver status
GPSMeasureMode = 10 # Measurement mode
GPSDOP = 11 # Measurement precision
GPSSpeedRef = 12 # Speed unit
GPSSpeed = 13 # Speed of GPS receiver
GPSTrackRef = 14 # Reference for direction of movement
GPSTrack = 15 # Direction of movement
GPSImgDirectionRef = 16 # Reference for direction of image
GPSImgDirection = 17 # Direction of image when captured
GPSMapDatum = 18 # Geodetic survey data
GPSDestLatitudeRef = 19 # Reference for destination latitude
GPSDestLatitude = 20 # Destination latitude
GPSDestLongitudeRef = 21 # Reference for destination longitude
GPSDestLongitude = 22 # Destination longitude
GPSDestBearingRef = 23 # Reference for destination bearing
GPSDestBearing = 24 # Destination bearing
GPSDestDistanceRef = 25 # Reference for destination distance
GPSDestDistance = 26 # Destination distance
GPSProcessingMethod = 27 # GPS processing method
GPSAreaInformation = 28 # GPS area information
GPSDateStamp = 29 # GPS date
GPSDifferential = 30 # Differential correction
GPSHPositioningError = 31 # Horizontal positioning errorclass InteropIFD:
"""Tag constants for interoperability metadata (Interop IFD)."""
InteroperabilityIndex = 1Utility class for handling the UserComment EXIF field, which requires special encoding.
class UserComment:
"""Helper for UserComment EXIF field encoding/decoding."""
# Supported encodings
ASCII = 'ascii'
JIS = 'jis'
UNICODE = 'unicode'
ENCODINGS = (ASCII, JIS, UNICODE)
@classmethod
def load(cls, data):
"""
Convert UserComment EXIF field to string.
Parameters:
- data: bytes - UserComment field data from EXIF
Returns:
str: Decoded comment text
Raises:
ValueError: If data is invalid or encoding unsupported
"""
@classmethod
def dump(cls, data, encoding="ascii"):
"""
Convert string to UserComment EXIF field format.
Parameters:
- data: str - Comment text to encode
- encoding: str - Encoding to use ('ascii', 'jis', 'unicode')
Returns:
bytes: Encoded UserComment field data
Raises:
ValueError: If encoding is unsupported
"""class InvalidImageDataError(ValueError):
"""Raised when image data is corrupted or in unsupported format."""
passimport piexif
# Load and examine EXIF data
exif_dict = piexif.load("photo.jpg")
print(f"Camera: {exif_dict['0th'].get(piexif.ImageIFD.Make, b'Unknown').decode()}")
print(f"Date: {exif_dict['Exif'].get(piexif.ExifIFD.DateTimeOriginal, b'Unknown').decode()}")
# Modify and save EXIF data
exif_dict["0th"][piexif.ImageIFD.Software] = b"Python Piexif"
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, "photo.jpg", "photo_modified.jpg")import piexif
# Add GPS coordinates to an image
exif_dict = piexif.load("photo.jpg")
# GPS coordinates for New York City (40.7128° N, 74.0060° W)
exif_dict["GPS"] = {
piexif.GPSIFD.GPSVersionID: (2, 0, 0, 0),
piexif.GPSIFD.GPSLatitudeRef: b'N',
piexif.GPSIFD.GPSLatitude: ((40, 1), (42, 1), (46, 1)), # 40°42'46"
piexif.GPSIFD.GPSLongitudeRef: b'W',
piexif.GPSIFD.GPSLongitude: ((74, 1), (0, 1), (22, 1)), # 74°0'22"
}
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, "photo.jpg", "photo_with_gps.jpg")import piexif
from piexif.helper import UserComment
# Read UserComment from EXIF
exif_dict = piexif.load("photo.jpg")
user_comment_bytes = exif_dict["Exif"].get(piexif.ExifIFD.UserComment)
if user_comment_bytes:
comment = UserComment.load(user_comment_bytes)
print(f"User comment: {comment}")
# Set UserComment in EXIF
new_comment = "Processed with Python"
exif_dict["Exif"][piexif.ExifIFD.UserComment] = UserComment.dump(new_comment, "unicode")
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, "photo.jpg", "photo_with_comment.jpg")from PIL import Image
import piexif
# Load image with PIL and extract EXIF
im = Image.open("photo.jpg")
if "exif" in im.info:
exif_dict = piexif.load(im.info["exif"])
# Modify EXIF data
w, h = im.size
exif_dict["0th"][piexif.ImageIFD.XResolution] = (w, 1)
exif_dict["0th"][piexif.ImageIFD.YResolution] = (h, 1)
# Save with modified EXIF
exif_bytes = piexif.dump(exif_dict)
im.save("photo_resized.jpg", "jpeg", exif=exif_bytes)