EXIF¶
Last Updated: April 21, 2026
EXIF (Exchangeable Image File Format) is a metadata standard for camera and image information โ capture date, exposure, GPS coordinates, orientation, camera make/model, and similar fields โ embedded directly in the image file. It is supported by virtually every camera manufacturer and image processor, and is by far the most important metadata source for PhotoPrism.
How PhotoPrism Reads EXIF¶
Unlike XMP, PhotoPrism reads EXIF through a two-stage pipeline that combines a native Go parser with an optional ExifTool overlay. Both stages run against the same media file and write into the same meta.Data struct; the second stage only fills fields that the first stage left empty.
Stage 1: Native EXIF Parser¶
For every supported container, internal/meta/exif.go extracts the raw EXIF block and assigns the values PhotoPrism cares about to meta.Data. The parser stack is built on Dustin Oprea's go-exif family (v3):
dsoprea/go-exif/v3โ the actual EXIF/IFD parser.dsoprea/go-jpeg-image-structure/v2โ locates the EXIF segment inside a JPEG.dsoprea/go-png-image-structure/v2โ PNGeXIfchunk extractor.dsoprea/go-heic-exif-extractor/v2โ HEIC / HEIF / AVIF container parser.dsoprea/go-tiff-image-structure/v2โ TIFF IFD walker.
When the file format is not one of the above, or when the format-specific parser fails, internal/meta/exif_parser.go:RawExif() falls back to a brute-force byte search via exif.SearchFileAndExtractExif. The brute-force path is also used when PHOTOPRISM_EXIF_BRUTE_FORCE is set (see below).
ExifSupported() (in internal/photoprism/mediafile.go) reports true for JPEG, RAW, HEIF (HEIC/AVIF), PNG, TIFF, and PSD โ the native parser stack either handles these directly or the brute-force fallback kicks in for RAW and PSD.
Stage 2: ExifTool Overlay (Optional)¶
When ExifTool support is enabled, internal/photoprism/mediafile_meta.go:MetaData() then runs ExifTool via Convert.ToJson() and merges its cached JSON output into the same meta.Data through internal/meta/json_exiftool.go:Exiftool(). This stage:
- Recognises far more tag variants and container formats than the native parser (video metadata, Maker Notes, vendor-specific IPTC/XMP blocks, Google Motion Photo, Samsung
MotionPhoto, etc.). - Does not overwrite non-zero values set by the native parser. The reflection loop in
json_exiftool.gochecks!fieldValue.IsZero()before assigning, so the native parser effectively wins for any field it populated. The overlay fills the gaps. - Is a precondition for embedded XMP support โ see Adobe XMP.
A regular <filename>.json sidecar (Google Photos export or similar) is read by meta.Data.JSON() between the two stages, and is also subject to the non-overwrite rule.
Configuration¶
| Variable | CLI Flag | Default | Effect |
|---|---|---|---|
PHOTOPRISM_DISABLE_EXIFTOOL |
--disable-exiftool |
false |
Skips Stage 2. Auto-forced to true when the sidecar path is not writable or ExifToolBin is empty. |
PHOTOPRISM_EXIFTOOL_BIN |
--exiftool-bin |
autodetected | Path to the exiftool binary used by Stage 2. |
PHOTOPRISM_EXIF_BRUTE_FORCE |
--exif-bruteforce |
false |
Forces the brute-force byte-search fallback in Stage 1 even when the format-specific parser succeeds. |
ExifTool JSON output is cached per file under ExifToolCacheName(hash) (under the configured cache directory, with the media file's SHA-1 as the cache key) so repeated indexing does not re-run exiftool on unchanged files.
Fields Extracted from EXIF¶
The table below lists every meta.Data field that EXIF and/or ExifTool can populate, along with the specific tag names consulted by each stage. The shape mirrors the XMP overview table so the two pages can be read side by side.
Column meanings:
- EXIF Tag(s) โ the tag names the native parser in
internal/meta/exif.golooks up indata.exif(the map populated bydsoprea/go-exiffrom the file's IFD0 / ExifIFD / GPSInfo block). Listed in priority order where the parser tries multiple aliases. - ExifTool JSON Key(s) โ the aliases listed in the
meta:"..."struct tag on that field ininternal/meta/data.go. Stage 2 iterates this list left-to-right and the first non-empty ExifTool JSON value wins. - Native (Stage 1) โ
โwhen the native EXIF parser assigns the field;โwhen it does not. Stage 1 only runs for file formats accepted byExifSupported()(JPEG, RAW, HEIF/HEIC/AVIF, PNG, TIFF, PSD). - ExifTool (Stage 2) โ
โwhen the ExifTool overlay can populate the field;โwhen it cannot (the field is not driven by ameta:"..."tag). Stage 2 only fills fields that Stage 1 left empty โ see How PhotoPrism Reads EXIF.
Data Field |
EXIF Tag(s) (IFD0 / ExifIFD / GPSInfo) | ExifTool JSON Key(s) | Native (Stage 1) | ExifTool (Stage 2) |
|---|---|---|---|---|
Artist |
Artist |
Artist, Creator, By-line, OwnerName, Owner |
โ | โ |
Copyright |
Copyright |
Rights, Copyright, CopyrightNotice, WebStatement |
โ | โ |
Title |
โ | Title, Headline |
โ | โ |
Caption |
ImageDescription |
Description, ImageDescription, Caption, Caption-Abstract |
โ (also auto-keyword) | โ |
Subject |
โ | Subject, PersonInImage, ObjectName, HierarchicalSubject, CatalogSets |
โ | โ |
Keywords |
โ | Keywords |
โ | โ |
Notes |
โ | Comment, UserComment |
โ | โ |
License |
โ | UsageTerms, License |
โ | โ |
Favorite |
โ | Favorite |
โ | โ |
Software |
Software |
Software, Producer, CreatorTool, Creator, CreatorSubTool, HistorySoftwareAgent, ProcessingSoftware |
โ | โ |
CameraMake |
CameraMake, then Make |
CameraMake, Make |
โ | โ |
CameraModel |
CameraModel, then Model, then UniqueCameraModel |
CameraModel, Model, CameraID, UniqueCameraModel |
โ | โ |
CameraOwner |
CameraOwnerName |
OwnerName |
โ | โ |
CameraSerial |
BodySerialNumber |
SerialNumber |
โ | โ |
LensMake |
LensMake |
LensMake |
โ | โ |
LensModel |
LensModel, then Lens |
LensModel, Lens, LensID |
โ | โ |
Exposure |
ExposureTime (N/M โ 1/โM/Nโ) |
ExposureTime, ShutterSpeedValue, ShutterSpeed, TargetExposureTime |
โ | โ |
FNumber |
FNumber (N/M โ float) |
FNumber |
โ | โ |
Aperture |
ApertureValue (N/M โ float) |
ApertureValue, Aperture |
โ | โ |
FocalLength |
FocalLengthIn35mmFilm, else FocalLength |
FocalLength, FocalLengthIn35mmFormat |
โ | โ |
FocalDistance |
โ | HyperfocalDistance |
โ | โ |
Iso |
ISOSpeedRatings |
ISO |
โ | โ |
Flash |
Flash (low bit == 1) |
FlashFired |
โ (also adds flash keyword) |
โ |
Width |
PixelXDimension, else ImageWidth |
ImageWidth, PixelXDimension, ExifImageWidth, SourceImageWidth |
โ | โ |
Height |
PixelYDimension, else ImageLength |
ImageHeight, ImageLength, PixelYDimension, ExifImageHeight, SourceImageHeight |
โ | โ |
Orientation |
Orientation (defaults to 1 when missing) |
Orientation / Rotation via Data.Rotation |
โ | โ (via Rotation) |
Rotation |
โ | Rotation |
โ | โ |
Projection |
ProjectionType (also adds panorama keyword) |
ProjectionType |
โ | โ |
ColorProfile |
โ | ICCProfileName, ProfileDescription |
โ | โ |
TakenAt |
DateTimeOriginal, then DateTimeCreated, CreateDate, DateTime, DateTimeDigitized |
SubSecDateTimeOriginal, SubSecDateTimeCreated, DateTimeOriginal, CreationTime, CreationDate, DateTimeCreated, DateTime, DateTimeDigitized |
โ | โ |
TakenAtLocal |
derived from TakenAt |
SubSecDateTimeOriginal, SubSecDateTimeCreated, DateTimeOriginal, CreationDate, DateTimeCreated, DateTime, DateTimeDigitized |
โ | โ |
TakenGps |
GPS sub-IFD timestamp | GPSDateTime, GPSDateStamp |
โ | โ |
TakenNs |
SubSecTimeOriginal, then SubSecTime, SubSecTimeDigitized |
(same sub-sec tags consumed by both stages) | โ | โ |
CreatedAt |
โ | SubSecCreateDate, CreationTime, CreationDate, CreateDate, MediaCreateDate, ContentCreateDate, TrackCreateDate |
โ | โ |
TimeOffset |
โ | OffsetTime, OffsetTimeOriginal, OffsetTimeDigitized |
โ | โ |
TimeZone |
derived from Lat/Lng via tz.Position |
derived from TakenAtLocal offset; normalised via tz.Name |
โ (via GPS) | โ |
Lat / Lng |
GPS sub-IFD โ GpsInfo.Decimal() โ NormalizeGPS |
GPSPosition or GPSLatitude + GPSLongitude (parsed via GpsToLatLng / GpsToDecimal) |
โ | โ |
GPSPosition |
โ | GPSPosition |
โ | โ |
GPSLatitude |
โ | GPSLatitude |
โ | โ |
GPSLongitude |
โ | GPSLongitude |
โ | โ |
Altitude |
GPS sub-IFD | GlobalAltitude, GPSAltitude |
โ | โ |
DocumentID |
ImageUniqueID (must pass rnd.SanitizeUUID) |
ContentIdentifier, MediaGroupUUID, BurstUUID, OriginalDocumentID, DocumentID, ImageUniqueID, DigitalImageGUID |
โ | โ |
InstanceID |
โ | InstanceID, DocumentID |
โ | โ |
ImageType |
โ | HDRImageType |
โ | โ |
HasThumbEmbedded |
โ | ThumbnailImage, PhotoshopThumbnail |
โ | โ |
HasVideoEmbedded |
โ | EmbeddedVideoFile, MotionPhoto, MotionPhotoVideo, MicroVideo |
โ | โ |
Duration |
โ | Duration, MediaDuration, TrackDuration, PreviewDuration |
โ | โ |
FPS |
โ | VideoFrameRate, VideoAvgFrameRate |
โ | โ |
Frames |
โ | FrameCount, AnimationFrames |
โ | โ |
Pages |
โ | PageCount, NPages, Pages |
โ | โ |
Codec |
โ | CompressorID, VideoCodecID, CodecID, OtherFormat, FileType |
โ | โ |
MimeType |
โ | MIMEType |
โ | โ |
FileName |
โ | FileName |
โ | โ |
Notes
- Native-reader tag names are not always identical to ExifTool's. In particular,
CameraOwnerName(native) โOwnerName(ExifTool),BodySerialNumber(native) โSerialNumber(ExifTool), andISOSpeedRatings(native) โISO(ExifTool). The native parser follows the EXIF 2.3 specification names; ExifTool uses its own normalised names. This is why the two columns show different strings for what is semantically the same value. - Purely numeric
CameraModel/CameraMake/LensMake/LensModelvalues are rejected by the native parser (txt.IsUIntcheck). Broken exporters occasionally write things like42as a camera model; these are dropped so Stage 2 can supply a real value instead. Orientationfalls back to1in Stage 1; the ExifTool path can also populate it viaRotation(xmp:Rotationor QuickTimeRotation), which is mapped back to an orientation value injson_exiftool.go.- GPS coordinates flow into
Lat/Lngdirectly from the native parser (viaexif.GpsInfo). TheGPSPosition/GPSLatitude/GPSLongitudestring fields inDataare only populated by Stage 2 and then parsed intoLat/Lngif they are still zero. - Sub-second tags are consulted by both stages but stored in
Data.TakenNsonly once; whichever stage writes first wins.
Notable Behaviours¶
These are the EXIF-reader quirks that most commonly surprise developers when debugging indexing issues:
- IFD0 wins over IFD1. When the same tag appears in both the primary IFD and the embedded-thumbnail IFD, PhotoPrism keeps the IFD0 value โ see issue #2231.
- Numeric model names are discarded.
CameraModel,CameraMake,LensMake, andLensModelare skipped when the value parses as an unsigned integer (seen in the wild from broken exporters). - Orientation defaults to
1when no tag is present; a missing orientation does not block indexing. - GPS derives the time zone even when the EXIF block contains an explicit offset tag (
OffsetTime,OffsetTimeOriginal,OffsetTimeDigitized) โ those offset tags are consumed later by the ExifTool path viaData.TimeOffset. - Sub-second precision is merged back into
TakenAtandTakenAtLocalafter time zone resolution. ImageUniqueIDbecomesDocumentID, which in turn governs file stacking โ if a RAW and a JPEG share anImageUniqueID, PhotoPrism stacks them.Flashkeyword is only added when the EXIF flash flag's low bit is1(flash actually fired), not just when the flag is present.- Per-file mutex.
Data.Exif()takes a package-levelsync.Mutex; concurrent calls serialise through it becausedsoprea/go-exif's tag index isn't goroutine-safe on write. ExifSupported()gate.mediafile_meta.go:MetaData()only runs Stage 1 for JPEG, RAW, HEIF, PNG, TIFF, and PSD; everything else relies entirely on Stage 2 (ExifTool).
References¶
- Official EXIF specification (v2.3)
dsoprea/go-exifโ native Go EXIF parser used in Stage 1.- ExifTool by Phil Harvey โ used in Stage 2.
- Dave Perrett โ EXIF Orientation Handling is a Ghetto โ still the clearest practical writeup on what the 8 orientation values actually mean.
- Editing & Debugging EXIF Data โ how to inspect and modify EXIF for testing.
Using ExifTool Locally¶
If you want to reproduce PhotoPrism's Stage 2 output on your laptop, install ExifTool via your package manager:
docker run --rm -v ${PWD}:/test -w /test -ti debian:bookworm bash
apt update && apt install -y exiftool libheif-examples
Then inspect a file the same way PhotoPrism does (-n disables human-readable formatting, -j emits JSON, -g groups tags by source โ helpful for disambiguating EXIF/XMP/IPTC aliases):
exiftool -n -j photo.jpg
exiftool -n -g -j photo.jpg | jq '.[0] | keys'
exiftool -n photo.jpg | grep -i orientation
For examples of how other applications display EXIF data, and for recipes to edit EXIF tags while testing, see the Editing & Debugging EXIF Data page.