Skip to content

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

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
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"
  }
]

See Also