exiftool
CLI Tool to read and write image metadata for many kinds of images.
Tricks
Strip all tags
exiftool -all= -- "$filename"
Show tags in a format that you can use to rewrite them
exiftool -S -- "$filename"
For example
$ exiftool -S -- "$filename" | grep Daniel
Artist: Daniel Austin Hoherd
Copyright: ©Daniel Austin Hoherd
Creator: Daniel Austin Hoherd
Rights: ©Daniel Austin Hoherd
$ exiftool -Rights='All rights reserved' -- "$filename"
1 image files updated
$ exiftool -Rights -- "$filename"
Rights : All rights reserved
Expanded basic usage
This prints out a lot more information than normal usage, and indicates what type of metadata it is.
exiftool -a -u -G:1:2 -- "$filename"
Here is an example of each unique column 1 in a file
$ exiftool -a -u -G:1:2 -- "$filename" | sort -u -k1,1
[Adobe:Image] DCT Encode Version : 100
[Composite:Camera] Scale Factor To 35 mm Equivalent: 7.0
[Composite:Image] Aperture : 1.8
[Composite:Location] GPS Latitude : 37 deg 15' 53.04" N
[Composite:Time] Date/Time Created : 2019:01:08 15:59:06
[ExifIFD:Camera] Exposure Program : Program AE
[ExifIFD:Image] Exposure Time : 1/120
[ExifIFD:Time] Date/Time Original : 2019:01:08 15:59:06
[ExifTool:ExifTool] ExifTool Version Number : 11.11
[File:Image] File Type : JPEG
[GPS:Location] GPS Version ID : 2.2.0.0
[GPS:Time] GPS Time Stamp : 23:59:06
[ICC-header:Image] Profile CMM Type : Linotronic
[ICC-header:Time] Profile Date Time : 1998:02:09 06:49:00
[ICC-meas:Image] Measurement Observer : CIE 1931
[ICC-view:Image] Viewing Cond Illuminant : 19.6445 20.3718 16.8089
[ICC_Profile:Camera] Device Mfg Desc : IEC http://www.iec.ch
[ICC_Profile:Image] Profile Copyright : Copyright (c) 1998 Hewlett-Packard Company
[IFD0:Author] Artist : Daniel Austin Hoherd
[IFD0:Camera] Make : Apple
[IFD0:Image] X Resolution : 240
[IFD0:Time] Modify Date : 2019:01:09 13:50:29
[IFD1:Image] Compression : JPEG (old-style)
[IFD1:Preview] Thumbnail Image : (Binary data 12008 bytes, use -b option to extract)
[IPTC:Author] By-line : Daniel Austin Hoherd
[IPTC:Other] Coded Character Set : UTF8
[IPTC:Time] Date Created : 2019:01:08
[Photoshop:Author] Copyright Flag : True
[Photoshop:Image] X Resolution : 240
[Photoshop:Preview] Photoshop Thumbnail : (Binary data 12008 bytes, use -b option to extract)
[System:Image] File Name : 2019-01-08-15-59-06-46628465322_d1657e4c95_o.jpg
[System:Time] File Modification Date/Time : 2019:01:22 09:00:22-08:00
[XMP-aux:Camera] Distortion Correction Already Applied: True
[XMP-crs:Image] Already Applied : True
[XMP-dc:Author] Creator : Daniel Austin Hoherd
[XMP-dc:Image] Format : image/jpeg
[XMP-photoshop:Image] Headline : ljwZuD
[XMP-photoshop:Time] Date Created : 2019:01:08 15:59:06.448
[XMP-x:Document] XMP Toolkit : Image::ExifTool 11.11
[XMP-xmp:Image] Creator Tool : Adobe Photoshop Lightroom 6.14 (Macintosh)
[XMP-xmp:Time] Create Date : 2019:01:08 15:59:06.448
[XMP-xmpMM:Other] Derived From Document ID : 9880573B7AACBFC189C795E182E8A05D
[XMP-xmpMM:Time] History When : 2019:01:09 13:50:29-08:00
[XMP-xmpRights:Author] Marked : True
Add missing lens data on Rokinon 85mm
Rokinon 85mm is a mechanical lens with no electronics, so no data about photos taken with it are stored in the image. This adds some stock metadata describing characteristics of the lens that are always true, which helps these photos sort accurately, etc..
exiftool \
-overwrite_original \
-LensModel='Rokinon 85mm f/1.4' \
-FocalLength='85' \
-LongFocal='85' \
-ShortFocal='85' \
-- \
"$filename"
Correct EXIF time, for instance to sync with GPS time
The following example increases all metadata dates by 1 minute and 56 seconds.
# exiftool -AllDates-='Y:M:D H:M:S'
exiftool -AllDates+='0:0:0 0:1:56' -- "$filename"
Set all dates to something obviously wrong
This is useful when scanning or photographing film or prints where you do not want the current date associated with the image.
exiftool -alldates='1900:01:01 01:01:01' -- *.tif
Delete certain keywords from files
This example uses bash expansion to create multiple -keywords-=
statements from the words inside of the braces. Use echo exiftool
to see what command is actually being called when testing. Keywords can also be stored in the subject
tag, so we clean that too.
find ~/your/pictures/ -type f -name '*.jpg' |
xargs exiftool -overwrite_original -{keywords,subject}-={keyword1,"a keyword with spaces",keyword3,"another keyword with spaces"} --
A more readable way to do this is to use an array and loop over it to create args, then pass the args to exiftool. This technique is quite useful for use with a variety of tools. You could also change this logic to add tags instead of deleting them.
tags=(
"private tag 1"
"another private tag"
"some other tag that is private"
)
args=()
for tag in "${tags[@]}" ; do
args+=( "-subject-=$tag" "-keywords-=$tag" )
done
exiftool -overwrite_original "${args[@]}" -- "$@"
Append keywords to a file
When adding keywords, the default behavior allows duplicates. This case is covered in FAQ #17 and indicates that you must remove and re-add each keyword in one operation in order to prevent duplicates. A bash function to do that follows. Be careful to use it on only ONE FILE at a time, otherwise you will add filenames as keywords!
add_keyword_to_file(){
local args=()
[[ "$#" -ge 2 ]] || { echo "ERROR: Must have at least 2 args: <keyword> [keyword]... <file>" ; return 1 ;}
while [[ "$#" -gt 1 ]] ; do
args+=("-keywords-=${1}" "-keywords+=${1}")
shift
done
filename=$1
exiftool "${args[@]}" -- "${filename}"
}
Here it is in action:
$ exiftool -p '$keywords $filename' -- 20211016-21-25-03_450QaA.jpg # show there are no keywords
Warning: [Minor] Tag 'keywords' not defined - 20211016-21-25-03_450QaA.jpg
$ add_keyword_to_file "Sutro Heights" "San Francisco" 20211016-21-25-03_450QaA.jpg # add keywords
1 image files updated
$ exiftool -p '$keywords $filename' -- 20211016-21-25-03_450QaA.jpg # show that keywords were added
Sutro Heights, San Francisco 20211016-21-25-03_450QaA.jpg
$ add_keyword_to_file "Sutro Heights" "San Francisco" 20211016-21-25-03_450QaA.jpg # re-add existing keywords
1 image files updated
$ exiftool -p '$keywords $filename' -- 20211016-21-25-03_450QaA.jpg # show that duplicates were not added
Sutro Heights, San Francisco 20211016-21-25-03_450QaA.jpg
It even works to remove duplicates where they already exist, likely because the -=
matches all instances of the keyword.
$ exiftool -keywords+="San Francisco" -- 20211016-21-25-03_450QaA.jpg # add a duplicate
1 image files updated
$ exiftool -p '$keywords $filename' -- 20211016-21-25-03_450QaA.jpg # show that there are duplicates
Sutro Heights, San Francisco, San Francisco 20211016-21-25-03_450QaA.jpg
$ add_keyword_to_file "Sutro Heights" "San Francisco" 20211016-21-25-03_450QaA.jpg
1 image files updated
$ exiftool -p '$keywords $filename' -- 20211016-21-25-03_450QaA.jpg # show that duplicates have been removed
Sutro Heights, San Francisco 20211016-21-25-03_450QaA.jpg
To add the same keywords to many files, loop through the files one at a time using something like:
for file in *.jpg ; do add_keyword_to_file "Sutro Heights" "San Francisco" "${file}" ; done ;
Set file modify time to image capture time
Useful when you want to sort in your file browser by modification time and get a chronological order of files.
exiftool "-FileModifyDate<DateTimeOriginal" -- *.jpg
Generate a table of Filename, Camera Model and File Size in bytes, sorted by bytes
The -n
flag here tells exiftool not to convert numbers into human readable formats. This is somewhat ironic in some circumstances, such as with location where using -n
makes the GPS location show up as decimal, which IMHO is much more reaable.
$ find /src_dir/ -iname '*.dng' |
xargs exiftool -p '$filename,$Model,$FileSize#' -- 2>/dev/null |
sort -t, -k3 -n |
column -s, -t
2012-01-26-23-19-54-6795223065_2e771d1012_o.jpg iPhone 4S 1242739
2013-02-03-10-01-56-8441346635_df4404a1f6_o.jpg NIKON D5200 1646481
2012-01-22-15-16-38-6746574603_d52311264f_o.jpg Canon EOS REBEL T3i 1671734
2011-01-22-23-44-31-6271225963_f9b95b2d7a_o.jpg NIKON D3S 1773081
2010-01-27-13-07-00-4313649499_835a6649c2_o.jpg NIKON D300 1829578
2016-02-03-07-26-32-24522158414_4aaf116d2a_o.jpg iPhone 6 2319061
2018-10-24-13-39-09-44676649345_1de0f581cd_o.jpg iPhone XS Max 2971254
2015-02-02-19-17-09-24587486051_3032823e4e_o.jpg NIKON D800 3309696
2014-01-27-13-52-41-12951707465_79a8dd3827_o.jpg iPhone 5 3401479
2017-01-22-18-33-28-31693592473_40478df088_o.jpg ILCE-7 4230661
2018-12-23-22-33-40-45536007225_8fdd50691a_o.jpg NIKON D850 4924617
2017-02-06-08-04-18-44658317900_98e04997fb_o.jpg iPhone 6s 8712631
2018-12-28-16-56-42-39713091073_c57ec1a8a8_o.jpg Canon EOS 5D Mark II 8741601
2019-01-08-16-11-49-39716361093_479e6a2323_o.jpg iPhone 8 Plus 12041600
Generate rsync commands for files matching a string
Useful for reviewing commands before running them, the following example generates a command for every file, then uses awk to do a numeric comparison on the last field to sort out images under a certain ImageHeight. These rsync commands can be pasted into a terminal to run. (Generating a list of files for use with rsync --files-from
would be a better option for this specific use case, but this illustration could be adapted for commands that do not have such an option.)
$ exiftool -d "%s" -p 'rsync -aP $filename otherhost:~/Pictures/ # $ImageHeight' -- * 2>/dev/null | awk '$NF >= 2800 {print}'
rsync -aP 2017-02-06-08-04-18-44658317900_98e04997fb_o.jpg otherhost:~/Pictures/ # 2869
rsync -aP 2018-02-06-09-50-04-31514483967_a422a3e3aa_o.jpg otherhost:~/Pictures/ # 2880
rsync -aP 2018-02-06-15-04-43-45541501845_8dbdc3b208_o.jpg otherhost:~/Pictures/ # 2880
rsync -aP 2018-02-06-15-05-43-31514485997_e2551fdbbc_o.jpg otherhost:~/Pictures/ # 2880
rsync -aP 2018-12-19-10-53-27-45663859984_0f93ac24ec_o.jpg otherhost:~/Pictures/ # 2880
Print filenames that are missing a tag
This example creates a file with all full path names for jpg and dng files that do not have GPS Coordinates
find /some/dir -iname '*.jpg' -or -iname '*.dng' -print0 |
xargs -0 exiftool -p '${Directory}/${Filename}' -if 'not defined $GPSPosition' -- >> ~/no-geo.txt
Print filenames of photos that are older than 10 years
exiftool -if '$now ge ${DateTimeOriginal;ShiftTime($_,"10:0:0 0")}' -p '$FileName' *.jpg
Use TestName tag target to test what files would be renamed to
This block builds an array of possible tags to use as a filename, creates an exiftool argument string from that array, then tests what files would be named to. This is useful when dealing with files from various sources that don't all use the same tag to store the original media creation time. By using TestName
instead of FileName
as the target, we observe what would occur, essentially a dry-run, instead of actually renaming the files.
There is a funky behavior of %-c when you operate on a file that should ideally not be renamed. Exiftool will toggle back and forth each run appending and removing -1
.
This assumes GNU xargs for the -r
flag.
#!/usr/bin/env bash
set -x
# The last valid variable from this list is used as the filename source
create_date_sources=(
TrackCreateDate
RIFF:DateTimeOriginal
MediaCreateDate
FileModifyDate
DateTimeOriginal
CreateDate
)
for opt in "${create_date_sources[@]}" ; do
args+=( "-TestName<${opt}" ) ;
done ;
args+=( '-d' './%Y/%m/%Y%m%d-%H-%M-%S%%-c.%%le' )
find . -maxdepth 1 -type f ! -name '*.sh' -print0 | xargs -0 -r exiftool "${args[@]}" --
Rename files to their ShutterCount
Filenames will not be changed if ShutterCount field is not populated.
exiftool -P '-filename<${ShutterCount;}.%e' -- *.dng
Rename files based on a set of possible names
Exiftool will use the last parameter where all variables are present.
exiftool -P -d '%F-%H-%M-%S' \
'-filename<${DateTimeOriginal} - ${Make;}.%e' \
'-filename<${CreateDate} - ${Make;}.%e' \
'-filename<${DateTimeOriginal} - ${Make;} - ${Model;}.%e' \
'-filename<${CreateDate} - ${Make;} - ${Model;}.%e' \
'-filename<${DateTimeOriginal} - ${Make;} - ${Model;} - ${ShutterCount}.%e' \
'-filename<${CreateDate} - ${Make;} - ${Model;} - ${ShutterCount}.%e' \
-- \
*.dng
Rename GPX files based on the capture time
You will end up with a filename like 2013-09-30-23-35-40.gpx
based off of the first trkpt timestamp.
exiftool -d '%Y%m%d-%H-%M-%S' '-FileName<${GpxTrkTrksegTrkptTime;tr/ /-/;tr/:/-/;tr(/Z/)()d;}%-c.gpx' -- *.gpx
Rename files to their original date and time using a lower case file extension
# %le = lowercase extension
# %-c = unique filenames when the timestamp is exactly the same. EG: filename-1.jpg
exiftool "-FileName<CreateDate" -d "%Y%m%d-%H-%M-%S%%-c.%%le" -- *.jpg
Rename files using a combination of tags
Using the name of the tag as output by exiftool -S
, you can create complicated filenames by combining tags:
exiftool -d '%Y%m%d-%H-%M-%S' '-FileName<${CreateDate;}_${Headline;}%-c.%e'
Rename music files in a directory
If you use a semicolon inside of a tag that is used to generate a filename, it will have filename-invalid characters stripped. The invalid character list is: / \ ? * : | " < >
. See the next section for more examples of semicolon behavior.
exiftool \
'-FileName<${Artist;} - ${Title;}.%e' \
'-FileName<${Artist;} - ${Album;} - ${Title;}.%e' \
-- \
*.mp3 *.m4a
The way I solved this prior to knowing the semicolon behavior was to use a regex replace, which is included here because it could be useful in other circumstances:
exiftool \
'-FileName<${Artist;s/\//_/} - ${Title;s/\//_/}.%e' \
'-FileName<${Artist;s/\//_/} - ${Album;s/\//_/} - ${Title;s/\//_/}.%e' \
-- \
*.mp3 *.m4a
Rename files into directories with date components as directory names
Using the above technique, it's not possible to create directories using date components as parts of the directory structure.
$ exiftool -d '%Y/%m/%d/%F-%H-%M-%S' '-TestName<${DateTimeOriginal;}.%le' -- example.jpg
'example.jpg' --> '201803042018-03-04-00-01-29.jpg'
Notice how all the directory delimiters were left out. To work around this, you can use a date format string with DafeFmt
directly in the date tag instead of in -d
:
$ exiftool '-TestName<${DateTimeOriginal;DateFmt("%Y/%m/%d/%F-%H-%M-%S")}.%le' -- example.jpg
'example.jpg' --> '2018/03/04/2018-03-04-00-01-29.jpg'
Rename files into subdir based on multiple tags
Making sure not use put a semicolon into the tags, as described in the last section, you can use more than one tag to rename a file, so long as you format your date string correctly.
find ./ -type f -iname '*.jpg' -print0 |
xargs -0 exiftool -d "%Y/%m/%d/%Y%m%d-%H-%M-%S" '-FileName<${DateTimeOriginal}_${Headline}%-c.%le' --
EG:
$ find . -type f -iname '*.jpg' -print0 | xargs -0 exiftool -d "%Y/%m/%d/%Y%m%d-%H-%M-%S" '-TestName<${DateTimeOriginal}_${Headline}%-c.%le' --
'./20170406-17-11-59.jpg' --> '2017/04/06/20170406-17-11-59_qrWLGF.jpg'
'./20170401-22-20-56.jpg' --> '2017/04/01/20170401-22-20-56_907nMU.jpg'
'./20170403-07-14-18.jpg' --> '2017/04/03/20170403-07-14-18_JMPDVd.jpg'
0 image files updated
3 image files unchanged
But if we use a semicolon, the invalid characters are stripped, and thus directories are not created.
$ find . -type f -iname '*.jpg' -print0 | xargs -0 exiftool -d "%Y/%m/%d/%Y%m%d-%H-%M-%S" '-TestName<${DateTimeOriginal;}_${Headline}%-c.%le' --
'./20170406-17-11-59.jpg' --> './2017040620170406-17-11-59_qrWLGF.jpg'
'./20170401-22-20-56.jpg' --> './2017040120170401-22-20-56_907nMU.jpg'
'./20170403-07-14-18.jpg' --> './2017040320170403-07-14-18_JMPDVd.jpg'
0 image files updated
3 image files unchanged
Move short videos to one dir, long videos to another dir
In iOS, if you have Live Photo enabled it creates little movies each time you take a photo. While these can be very interesting context around photos, they can be quite irritating if you're playing through a collection of videos where these are mixed with videos of more moderate duration. The following code snip separates videos with a duration of more than 10 seconds from those with equal or lesser duration.
# -TestName is used here so it does not destroy data. Replace this with FileName to make this actually work.
# $Duration# has the # sign appended to make this tag machine readable so it can accurately be compared.
# We must use perl's numeric comparisons (>, <=), not string comparisons (gt, le)
# exiftool does not support if else syntax, so for the else condition you must run a second command.
long_args=( "-TestName<${opt}" '-d' "${working_path}/long/%Y/%m/%Y%m%d-%H-%M-%S%%-c.%%le" '-if' '${Duration#} > 10' )
short_args=( "-TestName<${opt}" '-d' "${working_path}/short/%Y/%m/%Y%m%d-%H-%M-%S%%-c.%%le" '-if' '${Duration#} <= 10' )
find "${PWD}" -maxdepth 1 -type f -print0 | xargs -0 -r exiftool "${long_args[@]}" --
find "${PWD}" -maxdepth 1 -type f -print0 | xargs -0 -r exiftool "${short_args[@]}" --
Add missing date metadata to Nintendo Switch screenshots
Nintendo Switch screenshots are named with the date, but do not contain this information in the EXIF, which makes this data fragile.
# Filename like: 2020041909511400-87C68A817A974473877AC288310226F6.jpg
for X in 202?????????????-????????????????????????????????.{jpg,mp4} ; do
echo "${X}" |
sed -E 's/^((....)(..)(..)(..)(..)(..).*)/\2 \3 \4 \5 \6 \7 \1/'
done | while read -r Y M D h m s f ; do
exiftool \
-overwrite_original \
"-alldates=$Y:$M:$D $h:$m:$s" \
'-FileName<DateTimeOriginal' \
-d '%Y%m%d-%H-%M-%S%%-c.%%le' \
-- "$f"
done
Copy all GPS location data from one file into other files
exiftool -tagsfromfile source-file.jpg '-gpsl*<gpsl*' -- dest-file-1.jpg dest-file-2.jpg
Review and delete all DJI photos that are looking at the sky
When taking panorama's with a DJI drone, you end up with a lot of photos of clouds and blue sky. These can be found by looking at GimbalPitchDegree
. Review them in macOS Preview.app with:
find PANORAMA -type f |
xargs exiftool -if '$GimbalPitchDegree > 40' -p '${Directory}/${Filename}' -- 2>/dev/null |
xargs -r open
Once you've verified that none of them are worth preserving, delete them with:
find PANORAMA -type f |
xargs exiftool -if '$GimbalPitchDegree > 40' -p '${Directory}/${Filename}' -- 2>/dev/null |
xargs -r rm -fv
If you want to filter out photos that are mostly sky but also contain a bit of the ground in the bottom third of the frame, use > 9
instead of > 40
.
Geotag non-geotagged files using a specific TZ
Timezones in photo images is kind of a mess. In order to be specific about what TZ you took photos in, you can override it using the syntax in the example below. For instance, I keep all my photos in UTC so I never have to wonder what TZ I took them in and I never have to worry about DST. This example also skips any files that have existing geotags.
find ~/Pictures/whatever -type f -iname '*.dng' -print0 |
xargs -0 exiftool -if 'not defined $GPSPosition' -geotag ~/gps_tracks.gpx '-Geotime<${createdate}+00:00' --
This page gives more examples: https://exiftool.org/geotag.html
Export exif data as JSON
You can use the -J/-json
flag to output JSON data, which is obviously really helpful.
$ exiftool -J -ExifToolVersion -LensFStops 20241027-20-44-44_177lgJ.dng | jq .
[
{
"SourceFile": "20241027-20-44-44_177lgJ.dng",
"ExifToolVersion": 12.5,
"LensFStops": 6
}
]
However, by default, all numeric looking values are not quoted, even if they are not numeric values, like version numbers. In the above example, the version number is 12.50
, not 12.5
, and the LensFSops is 6.00
, not 6
. To work around this, you can use -api StructFormat=JSONQ
, where JSONQ
is "JSON with quoted numbers". (See https://exiftool.org/ExifTool.html#StructFormat for more details.) You must be using exiftool >= 12.88 (2024-07-11) for this feature to be available, otherwise it will silently produce non-quoted numeric values.
$ exiftool -api StructFormat=JSONQ -json -ExifToolVersion -LensFStops 20241027-20-44-44_177lgJ.dng | jq .
[
{
"SourceFile": "20241027-20-44-44_177lgJ.dng",
"ExifToolVersion": "13.00",
"LensFStops": "6.00"
}
]