#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# File:         exiftool
#
# Description:  Read/write meta information
#
# Revisions:    Nov. 12/03 - P. Harvey Created
#               (See html/history.html for revision history)
#------------------------------------------------------------------------------
use strict;
require 5.004;

# add our 'lib' directory to the include list BEFORE 'use Image::ExifTool'
my $exeDir;
BEGIN {
    # get exe directory
    ($exeDir = $0) =~ tr/\\/\//;    # for systems that use backslashes
    # isolate directory specification
    $exeDir =~ s/(.*)\/.*/$1/ or $exeDir = '.';
    # add lib directory at start of include path
    unshift @INC, "$exeDir/lib";
}
use Image::ExifTool qw{:Public};

sub ScanDir($$);
sub GetImageInfo($$);
sub PrintTagList(@);
sub LoadPrintFormat($);
sub FilenameSPrintf($;$);
sub IsExcluded($$);
sub Cleanup();
sub SigInt();

# do cleanup on Ctrl-C
$SIG{INT} = 'SigInt';

END {
    Cleanup();
}

my @files;          # list of files and directories to scan
my @tags;           # list of tags to extract
my @newValues;      # list of new tag values to set
my $outFormat = 0;  # 0=Canon format, 1=same-line, 2=tag names, 3=values only
my $tabFormat = 0;  # non-zero for tab output format
my $recurse;        # recurse into subdirectories
my @ignore;         # directory names to ignore
my $count = 0;      # count of files scanned
my $countBad = 0;   # count of files with errors
my $countCreated=0; # count output files created
my $countDir = 0;   # count of directories scanned
my $countGoodWr = 0;# count files written OK
my $countGoodCr = 0;# count files created OK
my $countSameWr = 0;# count files written OK but not changed
my $countBadWr = 0; # count write errors
my $countBadCr = 0; # count files not created due to errors
my $countNewDir = 0;# count of directories created
my $outputExt;      # extension for output file (or undef for no output)
my $forcePrint;     # force printing of tags whose values weren't found
my $helped;         # flag to avoid printing help if no tags specified
my $htmlOutput = 0; # flag for html-formatted output
my $escapeHTML;     # flag to escape printed values for html
my $binaryOutput;   # flag for binary output
my $doSetFileName;  # flag set if FileName may be written
my $showGroup;      # number of group to show (may be zero or '')
my $allGroup;       # show group name for all tags
my $preserveTime;   # flag to preserve times of updated files
my $multiFile;      # non-zero if we are scanning multiple files
my $showTagID;      # non-zero to show tag ID's
my @printFmt;       # the contents of the print format file
my $tmpFile;        # temporary file to delete on exit
my $tmpText;        # temporary text file
my $binaryStdout;   # flag set if we output binary to stdout
my $isWriting = 0;  # flag set if we are writing tags
my $outOpt;         # output file or directory name
my $doUnzip;        # flag to extract info from .gz and .bz2 files
my %setTags;        # hash of list references for tags to set from files
my @dynamicFiles;   # list of files with dynamic names
my $setTagsFile;    # filename for last TagsFromFile option
my @exclude;        # list of excluded tags
my %excludeGrp;     # hash of tags excluded by group
my $allInGroup;     # flag to show all tags in a group
my $disableOutput;  # flag to disable normal output
my $quiet = 0;      # flag to disable printing of informational messages / warnings
my $overwriteOrig=0;# flag to overwrite original file
my $filterFlag = 0; # file filter flag (0x01=allow extensions, 0x02=deny extensions)
my %filterExt;      # lookup for filtered extensions
my $verbose = 0;    # verbose setting
my %warnedOnce;     # lookup for once-only warnings
my $scanWritable;   # flag to process only writable file types

# my warn and die routines
sub Warn { warn(@_) if $quiet < 2 or $_[0] =~ /^Error/; }
sub Die  { Warn @_; exit 1; }
sub WarnOnce($) {
    Warn(@_) and $warnedOnce{$_[0]} = 1 unless $warnedOnce{$_[0]};
}

# define Cleanup and SigInt routines
sub Cleanup() {
    unlink $tmpFile if defined $tmpFile;
    unlink $tmpText if defined $tmpText;
}
sub SigInt()  { Cleanup(); exit 1; }

#------------------------------------------------------------------------------
# main script
#

my $mainTool = new Image::ExifTool;     # create ExifTool object

$mainTool->Options(Duplicates => 0);    # don't save duplicates by default

# parse command-line options
while ($_ = shift) {
    if (s/^-//) {
        if (/^listw?$/i) {
            $helped = 1;
            my $wr = ($_ eq 'listw');
            my $group;
            if ($ARGV[0] and $ARGV[0] =~ /^-(.+):(all|\*)$/i) {
                $group = $1;
                shift;
                $group =~ /IFD/i and warn("Can't list tags for specific IFD\n"), next;
            }
            print $wr ? 'Writable' : 'Available', $group ? " $group" : '', " tags:\n";
            PrintTagList($wr ? GetWritableTags($group) : GetAllTags($group));
            # also print shortcuts if listing all tags
            unless ($group or $wr) {
                my @tagList = GetShortcuts();
                if (@tagList) {
                    print "Command-line shortcuts:\n";
                    PrintTagList(@tagList);
                }
            }
            next;
        }
        if (/^listf$/i) {
            $helped = 1;
            print "Recognized file types:\n";
            PrintTagList(GetFileType());
            next;
        }
        if (/^(listg|group)(\d*)$/i) {
            $helped = 1;
            # list all groups in specified family
            my $family = $2 || 0;
            print "Groups in family $family:\n";
            PrintTagList(GetAllGroups($family));
            next;
        }
        /^ver$/i and print("ExifTool version $Image::ExifTool::VERSION\n"), exit 0;
        if (/^(all)?tagsfromfile(=.*)?$/i) {
            $setTagsFile = $2 ? substr($2,1) : (@ARGV ? shift : '');
            $setTagsFile eq '' and Die "File must be specified for -TagsFromFile option\n";
            push(@newValues, "TagsFromFile=$setTagsFile");
            $setTags{$setTagsFile} or $setTags{$setTagsFile} = [];
            next;
        }
        if (/^\@$/) {
            my $argFile = shift or Die "Expecting filename for -\@ option\n";
            unless (open(ARGFILE,$argFile)) {
                unless ($argFile !~ /^\// and open(ARGFILE, "$exeDir/$argFile")) {
                    Die "Error opening arg file $argFile\n";
                }
            }
            foreach (<ARGFILE>) {
                s/^\s+//; s/\s+$//s; # remove leading/trailing white space
                s/\s*=\s*/=/;        # remove white space around '=' sign
                s/('|")(.*?)\1/$2/g; # remove quotes
                push @ARGV, $_ unless $_ eq '' or /^#/;
            }
            close(ARGFILE);
            next;
        }
        /^a$/i and $mainTool->Options(Duplicates => 1), next;
        /^b$/i and $binaryOutput = 1, next;
        /^c$/  and $mainTool->Options('CoordFormat', shift || Die "Expecting coordinate format for -c option\n"), next;
        /^d$/  and $mainTool->Options('DateFormat', shift || Die "Expecting date format for -d option\n"), next;
        /^D$/  and $showTagID = 'D', next;
        /^e$/  and $mainTool->Options(Composite => 0), next;
        if (/^-?ext$/i) {
            my $ext = shift;
            defined $ext or Die "Expecting extension for -ext option\n";
            $ext =~ s/^\.//;    # remove leading '.' if it exists
            my $flag = /^-/ ? 0 : 1;
            $filterFlag |= (0x01 << $flag);
            $filterExt{uc($ext)} = $flag;
            next;
        }
        /^E$/  and $escapeHTML = 1, next;
        /^f$/  and $forcePrint = 1, next;
        /^F([-+]?\d*)$/ and $mainTool->Options(FixBase => $1), next;
        /^fast$/i and $mainTool->Options(FastScan => 1), next;
        /^g(\d*)$/ and $showGroup = $1, next;
        /^G(\d*)$/ and $showGroup = $1, $allGroup=1, next;
        /^h$/  and $htmlOutput = 1, $escapeHTML = 1, next;
        /^H$/  and $showTagID = 'H', next;
        /^htmldump$/i and ++$verbose, $htmlOutput = 2, next;
        /^i$/i and push(@ignore,shift || Die "Expecting directory name for -i option\n"), next;
        /^l$/  and --$outFormat, next;
        /^L$/  and $mainTool->Options(Charset => 'Latin'), next;
        /^m$/i and $mainTool->Options(IgnoreMinorErrors => 1), next;
        /^n$/i and $mainTool->Options(PrintConv => 0), next;
        if (/^o$/i) {
            $outOpt = shift || Die("Expected output file or directory name for -o option\n");
            $outOpt =~ tr/\\/\//;
            next;
        }
        /^overwrite_original$/i and $overwriteOrig = 1, next;
        /^overwrite_original_in_place$/i and $overwriteOrig = 2, next;
        /^p$/  and LoadPrintFormat(shift), next;
        /^P$/  and $preserveTime = 1, next;
        /^q$/i and ++$quiet, next;
        /^r$/i and $recurse = 1, next;
        /^s$/  and ++$outFormat, next;
        /^S$/  and $outFormat+=2, next;
        /^t$/i and $tabFormat = 1, next;
        /^u$/  and $mainTool->Options(Unknown => $mainTool->Options('Unknown')+1), next;
        /^U$/  and $mainTool->Options(Unknown => 2), next;
        if (/^v(\d*)$/i) {
            $verbose = $1 eq '' ? $verbose + 1 : $1;
            next;
        }
        /^w$/i and $outputExt = shift || Die("Expecting output extension for -w option\n"), next;
        if (/^x$/i) {
            my $tag = shift;
            defined $tag or Die "Expecting tag name for -x option\n";
            $tag =~ s/\ball\b/\*/ig;    # replace 'all' with '*' in tag names
            if ($setTagsFile) {
                push @{$setTags{$setTagsFile}}, "-$tag";
            } else {
                push @exclude, $tag;
            }
            next;
        }
        /^z$/i and $doUnzip = 1, $mainTool->Options(Compress => 1), next;
        $_ eq '' and push(@files, '-'), next;   # read STDIN
        length $_ eq 1 and Die "Unknown option -$_\n";
        if (/^[^<]+<?=/) {
            push @newValues, $_;
            $doSetFileName = 1 if /^(filename|directory)\b/i;
        } else {
            s/\ball\b/\*/ig;    # replace 'all' with '*' in tag names
            if (not $setTagsFile and /(<|>)/) {
                # assume '-tagsFromFile @' if tags are being redirected
                # and -tagsFromFile hasn't already been specified
                $setTagsFile = '@';
                push(@newValues, "TagsFromFile=@");
                $setTags{$setTagsFile} or $setTags{$setTagsFile} = [];
            }
            if ($setTagsFile) {
                push @{$setTags{$setTagsFile}}, $_;
                $doSetFileName = 1 if /(^|>)(filename|directory)\b/i;
            } elsif (/^-(.*)/) {
                push @exclude, $1;
            } else {
                push @tags, $_;
            }
        }
    } else {
        push @files, $_;
    }
}

# print help
unless ((@tags and not $outOpt) or @files or @newValues) {
    Die "Nothing to write\n" if $outOpt;
    unless ($helped or not system qq{perldoc "$0"}) {
        print "Run 'perldoc exiftool' for help on exiftool.\n";
    }
    exit 0;
}

if ($overwriteOrig > 1 and $outOpt) {
    Die "Can't overwrite in place when -o option is used\n";
}

# can't do anything if no file specified
unless (@files) {
    Die "No file specified\n" unless $outOpt;
    push @files, '';    # create file from nothing
}

# set Verbose and HtmlDump options
if ($verbose) {
    $disableOutput = 1 unless @tags or @exclude;
    undef $binaryOutput;    # disable conflicting option
    if ($htmlOutput) {
        $htmlOutput = 2;    # flag for html dump
        $mainTool->Options(HtmlDump => $verbose);
    } else {
        $mainTool->Options(Verbose => $verbose);
    }
}

# validate all tags we're writing
if (@newValues) {
    foreach (@newValues) {
        /(.*?)=(.*)/s or next;
        my ($tag, $newVal) = ($1, $2);
        $tag =~ s/\ball\b/\*/ig;    # replace 'all' with '*' in tag names
        $newVal eq '' and undef $newVal;    # undefined to delete tag
        if ($tag =~ /^(All)?TagsFromFile$/i) {
            Die "Need file name for -TagsFromFile\n" unless defined $newVal;
            ++$isWriting;
            if ($newVal eq '@' or not defined FilenameSPrintf($newVal)) {
                push @dynamicFiles, $newVal;
                next;   # set tags from dynamic file later
            }
            -e $newVal or Die "File '$newVal' does not exist for -TagsFromFile option\n";
            # set specified tags from this file
            $verbose and print("Setting new values from $newVal\n");
            my $info = $mainTool->SetNewValuesFromFile($newVal, @{$setTags{$newVal}});
            $info->{Error} and Die "Error: $info->{Error} - $newVal\n";
            if ($info->{Warning}) {
                Warn "Warning: $info->{Warning} - $newVal\n";
                delete $info->{Warning};
            }
            %$info or Warn "No writable tags found - $newVal\n";
            next;
        } elsif ($tag =~ /^PreviewImage$/i) {
            # can't delete preview image, so we can set it to ''
            $newVal = '' unless defined $newVal;
        }
        my %opts = (
            Protected => 1, # allow writing of 'unsafe' tags
            Shift => 0,     # shift values if possible instead of adding/deleting
        );
        if ($tag =~ s/<// and defined $newVal) {
            # read new value from file
            my $file = $newVal;
            open(INFILE,$file) or Die "Error opening file $file\n";
            binmode(INFILE);
            my $maxSize = 16000000;
            my $num = read(INFILE,$newVal,$maxSize);
            close(INFILE);
            $num or Die "Error reading $file\n";
            $num < $maxSize or Die "File exceeds size limit: $file\n";
        }
        $tag =~ s/\+// and $opts{AddValue} = 1;
        if ($tag =~ s/-$//) {
            $opts{DelValue} = 1;
            # set $newVal to '' if deleting nothing
            $newVal = '' unless defined $newVal;
        }
        my ($rtn, $wrn) = $mainTool->SetNewValue($tag, $newVal, %opts);
        ++$isWriting if $rtn;
        $wrn and Warn "$wrn\n";
    }
    if ($isWriting) {
        # create input file from scratch possible and it doesn't exist
        # (by treating it as if we had used the -o option)
        if (not $outOpt and @files == 1 and not -e $files[0] and CanCreate($files[0])) {
            $outOpt = $files[0];
            $files[0] = '';
        }
    } else {
        unless ($isWriting or $outOpt or @tags) {
            Warn "Nothing to do.\n";
            exit 1;
        }
    }
} elsif (grep /^(\*:)?\*$/, @exclude) {
    Die "All tags excluded -- nothing to do.\n";
}
# save current state of new values if setting values from target file
# or if we may be translating to a different format
$mainTool->SaveNewValues() if $setTags{'@'} or $outOpt;

$multiFile = 1 if @files > 1;
$showGroup = 0 if defined $showGroup and not $showGroup;
@exclude and $mainTool->Options(Exclude => \@exclude);

if ($binaryOutput) {
    $outFormat = 99;    # shortest possible output format
    $mainTool->Options(PrintConv => 0);
    binmode(STDOUT);
    $binaryStdout = 1;
    # disable conflicting options
    undef $showGroup;
    $htmlOutput = 0;
}

# sort by groups to look nicer depending on options
if (defined $showGroup and not (@tags and $allGroup)) {
    $mainTool->Options(Sort => "Group$showGroup"),
}

if ($outputExt) {
    $outputExt =~ tr/\\/\//;    # make all forward slashes
    # add '.' before output extension if necessary
    $outputExt = ".$outputExt" unless $outputExt =~ /[.%]/;
}

# determine if we should scan for only writable files
if ($outOpt) {
    my $type = GetFileType($outOpt);
    if ($type) {
        CanWrite($type) or Die "Can't write $type files\n";
        $scanWritable = $type unless CanCreate($type);
    } else {
        $scanWritable = 1;
    }
    $isWriting = 1;     # set writing flag
} elsif ($isWriting) {
    $scanWritable = 1;
}

# scan through all specified files
my $file;
foreach $file (@files) {
    if (-d $file) {
        $multiFile = 1;
        ScanDir($mainTool, $file);
    } else {
        GetImageInfo($mainTool, $file);
    }
}

# print summary and exit
my $tot = $count + $countBad;
my $totWr = $countGoodWr + $countBadWr + $countSameWr + $countGoodCr + $countBadCr;
if (($countDir or $totWr or $tot > 1 or $outputExt) and not ($binaryStdout or $quiet)) {
    printf("%5d directories scanned\n", $countDir) if $countDir;
    printf("%5d directories created\n", $countNewDir) if $countNewDir;
    printf("%5d image files created\n", $countGoodCr) if $countGoodCr;
    printf("%5d image files updated\n", $countGoodWr) if $totWr - $countGoodCr - $countBadCr;
    printf("%5d image files unchanged\n", $countSameWr) if $countSameWr;
    printf("%5d files weren't updated due to errors\n", $countBadWr) if $countBadWr;
    printf("%5d files weren't created due to errors\n", $countBadCr) if $countBadCr;
    printf("%5d image files read\n", $count) if $tot>1 or ($countDir and not $totWr);
    printf("%5d files could not be read\n", $countBad) if $countBad;
    printf("%5d output files created\n", $countCreated) if $outputExt;
}
# return error status if we had any errors
exit 1 if $countBadWr or $countBadCr or $countBad;

exit 0;     # all done

#------------------------------------------------------------------------------
# Set new values from file
# Inputs: 0) exiftool ref, 1) filename, 2) reference to list of values to set
# Returns: 0 on error (and increments $countBadWr)
sub DoSetFromFile($$$)
{
    my ($exifTool, $file, $setTags) = @_;
    $verbose and print "Setting new values from $file\n";
    my $info = $exifTool->SetNewValuesFromFile($file, @$setTags);
    if ($info->{Error}) {
        Warn("Error: $info->{Error} - $file\n");
        ++$countBadWr;
        return 0;
    }
    $info->{Warning} and Warn "Warning: $info->{Warning} - $file\n";
    return 1;
}

#------------------------------------------------------------------------------
# Create directory for specified file
# Inputs: 0) complete file name including path
# Returns: true if a directory was created (dies if dir can't be created)
sub CreateDirectory($)
{
    my $file = shift;
    my ($dir, $created);
    ($dir = $file) =~ s/[^\/]*$//;  # remove filename from path specification
    if ($dir and not -d $dir) {
        my @parts = split /\//, $dir;
        $dir = '';
        foreach (@parts) {
            $dir .= $_;
            if (length $dir and not -d $dir) {
                # create directory since it doesn't exist
                mkdir($dir, 0777) or Die "Error creating directory $dir\n";
                $verbose and print "Created directory $dir\n";
                $created = 1;
            }
            $dir .= '/';
        }
    }
    ++$countNewDir if $created;
    return $created;
}

#------------------------------------------------------------------------------
# Set information in file
# Inputs: 0) ExifTool object reference
#         1) source file name ('' to create from scratch)
# Returns: true on success
sub SetImageInfo($$)
{
    my ($exifTool, $file) = @_;
    my ($outfile, $restored, $isTemporary, $isStdout);
    my $infile = $file;    # save original file argument

    undef $tmpFile; # make sure this isn't defined

    # first, try to determine our output file name so we can return quickly
    # if it already exists (note: this test must be delayed until after we
    # set tags from dynamic files if writing FileName or Directory)
    if (defined $outOpt) {
        if ($outOpt eq '-') {
            $isStdout = 1;
            last;
        } else {
            $outfile = FilenameSPrintf($outOpt, $file);
            if ($outfile eq '') {
                Warn "Can't create file with zero-length name from $file\n";
                ++$countBadCr;
                return 0;
            }
            if (-d $outfile or $outfile =~ /\/$/) {
                $outfile .= '/' unless $outfile =~ /\/$/;
                my $name = $file;
                $name =~ tr/\\/\//;
                $name =~ s/.*\///;  # remove directory name
                $outfile .= $name;
            } else {
                my $srcType = GetFileType($file) || '';
                my $outType = GetFileType($outfile);
                if ($outType and ($srcType ne $outType or $outType eq 'ICC')) {
                    if (CanCreate($outfile)) {
                        if ($file ne '') {
                            # restore previous new values unless done already
                            $exifTool->RestoreNewValues();
                            $restored = 1;
                            # translate to this type by setting specified tags from file
                            my @setTags = @tags;
                            foreach (@exclude) {
                                push @setTags, "-$_";
                            }
                            # force ICC_Profile to be set if this is an ICC output file
                            push @setTags, 'ICC_Profile' if $outType eq 'ICC';
                            return 0 unless DoSetFromFile($exifTool, $file, \@setTags);
                            # all done with source file -- create from meta information alone
                            $file = '';
                        }
                    } else {
                        my $what = $srcType ? 'other types' : 'scratch';
                        WarnOnce "Error: Can't create $outType files from $what\n";
                        ++$countBadCr;
                        return 0;
                    }
                }
            }
            if (-e $outfile and not $doSetFileName) {
                Warn "Error: '$outfile' already exists - $infile\n";
                ++$countBadWr;
                return 0;
            }
        }
    } elsif ($file eq '-') {
        $isStdout = 1;
    }
    # set tags from destination file if required
    if (@dynamicFiles) {
        # restore previous values if necessary
        $exifTool->RestoreNewValues() unless $restored;
        my $dyFile;
        foreach $dyFile (@dynamicFiles) {
            my $fromFile;
            if ($dyFile eq '@') {
                $fromFile = $infile;
            } else {
                $fromFile = FilenameSPrintf($dyFile, $infile);
                ++$countBadWr, return 0 unless defined $fromFile;
            }
            # set new values values from file
            return 0 unless DoSetFromFile($exifTool, $fromFile, $setTags{$dyFile});
        }
    }
    if ($isStdout) {
        # write to STDOUT
        $outfile = \*STDOUT;
        binmode(STDOUT);
        $binaryStdout = 1;
    } else {
        # determine what our output file name should be
        my $newFileName = $exifTool->GetNewValues('FileName');
        my $newDir = $exifTool->GetNewValues('Directory');
        if ($newFileName or $newDir or ($doSetFileName and defined $outfile)) {
            if ($newFileName) {
                $newFileName = FilenameSPrintf($newFileName, $infile);
                if (defined $outfile) {
                    $outfile = Image::ExifTool::GetNewFileName($infile, $outfile) if $infile ne '';
                    $outfile = Image::ExifTool::GetNewFileName($outfile, $newFileName);
                } elsif ($infile ne '') {
                    $outfile = Image::ExifTool::GetNewFileName($infile, $newFileName);
                }
            }
            if ($newDir) {
                $newDir = FilenameSPrintf($newDir, $infile);
                $outfile = Image::ExifTool::GetNewFileName(defined $outfile ? $outfile : $infile, $newDir);
            }
            if (-e $outfile) {
                if ($infile ne $outfile) {
                    Warn "Error: '$outfile' already exists - $infile\n";
                    ++$countBadWr;
                    return 0;
                }
                undef $outfile; # not changing the file name after all
            }
        }
        if (defined $outfile) {
            $verbose and print "'$infile' --> '$outfile'\n";
            # create output directory if necessary
            CreateDirectory($outfile);
            # set temporary file (automatically erased on abnormal exit)
            $tmpFile = $outfile if defined $outOpt;
        }
        unless (defined $tmpFile) {
            # only need to rewrite file if we set a valid tag
            my ($numSet, $numQuick) = $exifTool->CountNewValues();
            unless ($numSet) {
                if (-e $file) {
                    ++$countSameWr;
                    return 1;
                } else {
                    Warn("Warning: File not found - $file\n");
                    ++$countBadWr;
                    return 0;
                }
            }
            # quickly rename file and/or set file date if this is all we are doing
            if ($numSet == $numQuick) {
                my $r1 = $exifTool->SetFileModifyDate($file);
                my $r2 = 0;
                $r2 = $exifTool->SetFileName($file, $outfile) if defined $outfile;
                if ($r1 > 0 or $r2 > 0) {
                    ++$countGoodWr;
                } elsif ($r1 < 0 or $r2 < 0) {
                    ++$countBadWr;
                    return 0;
                } else {
                    ++$countSameWr;
                }
                return 1;
            }
            unless (defined $outfile) {
                # write to a truly temporary file
                $outfile = "${file}_exiftool_tmp";
                $isTemporary = 1;
            }
            $tmpFile = $outfile;
        }
    }
    # rewrite the file
    my $success = $exifTool->WriteInfo($file, $outfile);

    if ($success == 1) {
        ++$countGoodWr;
        # preserve the original file times
        if (defined $tmpFile) {
            if (-e $file) {
                my ($modTime, $accTime);
                if ($preserveTime) {
                    $modTime = $^T - (-M $file) * (24 * 3600);
                    $accTime = $^T - (-A $file) * (24 * 3600);
                    unless (utime($accTime, $modTime, $tmpFile)) {
                        Warn "Error setting file time - $file\n";
                    }
                }
                if ($isTemporary) {
                    # move original out of the way
                    my $original = "${file}_original";
                    if (not $overwriteOrig and not -e $original) {
                        # rename the file and check again to be sure the file doesn't exist
                        # (in case, say, the filesystem truncated the file extension)
                        if (not rename($file, $original) or -e $file) {
                            Die "Error renaming $file\n";
                        }
                    }
                    if ($overwriteOrig > 1) {
                        # copy temporary file over top of original to preserve attributes
                        my ($err, $buff);
                        open(TMP_FILE, $tmpFile) or die "Error opening $tmpFile\n";
                        binmode(TMP_FILE);
                        if (open(ORIG_FILE, ">$file")) {
                            binmode(ORIG_FILE);
                            while (read(TMP_FILE, $buff, 65536)) {
                                print ORIG_FILE $buff or $err = 1;
                            }
                            close(TMP_FILE);
                            close(ORIG_FILE) or $err = 1;
                            if ($err) {
                                warn "Error preserving creator for $file\n";
                                rename($tmpFile, $file) or die "Error renaming $tmpFile to $file\n";
                            } else {
                                unlink $tmpFile;
                                if ($preserveTime and not utime($accTime, $modTime, $file)) {
                                    Warn "Error setting file time - $file\n";
                                }
                            }
                        } else {
                            close(TMP_FILE);
                            warn "Error opening $file for writing\n";
                            unlink $tmpFile;
                        }
                    } else {
                        # simply rename temporary file to replace original
                        unless (rename($tmpFile, $file)) {
                            Warn "Error renaming temporary file to $file\n";
                            unlink $tmpFile;
                        }
                    }
                } elsif ($overwriteOrig) {
                    # erase original file
                    unlink $file or Warn "Error erasing original $file\n";
                }
            } else {
                # this file was created from scratch, not edited
                ++$countGoodCr;
                --$countGoodWr;
            }
        }
    } elsif ($success) {
        if ($isTemporary) {
            # just erase the temporary file since no changes were made
            ++$countSameWr;
            unlink $tmpFile;
        } else {
            if ($overwriteOrig) {
                unlink $file or Warn "Error erasing original $file\n";
            }
            ++$countGoodWr;
        }
    } else {
        ++$countBadWr;
        unlink $tmpFile if defined $tmpFile;
    }
    undef $tmpFile;
    return $success;
}

#------------------------------------------------------------------------------
# A sort of sprintf for filenames
# Inputs: 0) format string (%d=dir, %f=file name, %e=ext),
#         1) source filename or undef to test format string
# Returns: new filename or undef on error (or if no file and fmt contains token)
sub FilenameSPrintf($;$)
{
    my ($fmt, $file) = @_;
    # return format string straight away if no tokens
    return $fmt unless $fmt =~ /%[-+]?\d*\.?\d*(d|f|e)/;
    return undef unless defined $file;
    $file =~ s/\\/\//g; # make sure we are using forward slashes
    # split filename into directory, file, extension
    my @parts = ($file =~ /^(.*?)([^\/]*?)[.]?([^.\/]*)$/);
    @parts or Warn("Error: Bad pattern match for file $file\n"), return undef;
    my %part;
    foreach ('d','f','e') {
        $part{$_} = shift @parts;
    }
    my ($filename, $pos) = ('', 0);
    while ($fmt =~ /(%([-+]?)(\d*)\.?(\d*)(d|f|e))/g) {
        $filename .= substr($fmt, $pos, pos($fmt) - $pos - length($1));
        $pos = pos($fmt);
        my ($wid, $skip, $code) = ($3, $4 || 0, $5);
        my $len = length $part{$code};
        next unless $skip < $len;
        $wid = $len - $skip if $wid eq '' or $wid + $skip > $len;
        $skip = $len - $wid - $skip if $2 eq '-';
        $filename .= substr($part{$code}, $skip, $wid);
    }
    $filename .= substr($fmt, $pos); # add rest of file name
    $filename =~ s{//}{/}g; # remove double slashes
    return $filename;
}

#------------------------------------------------------------------------------
# Open output text file
# Inputs: 0) file name format string
# Returns: 0) file reference (or undef on error), 1) file name if opened
sub OpenOutputFile($)
{
    my $file = shift;
    my ($fp, $outfile);
    if ($outputExt) {
        ($outfile = $file) =~ tr/\\/\//;
        if (not defined FilenameSPrintf($outputExt)) {
            # make filename from printf-like $outputExt
            $outfile = FilenameSPrintf($outputExt, $file);
            return () unless defined $outfile;
            CreateDirectory($outfile);  # create directory if necessary
        } else {
            $outfile =~ s/\.[^.\/]*$//; # remove extension if it exists
            $outfile .= $outputExt;
        }
        if (-e $outfile) {
            Warn "Output file $outfile already exists for $file\n";
            return ();
        }
        open(OUTFILE, ">$outfile") or Die "Error creating $outfile\n";
        binmode(OUTFILE) if $binaryOutput;
        $fp = \*OUTFILE;
    } else {
        $fp = \*STDOUT;
    }
    return($fp, $outfile);
}

#------------------------------------------------------------------------------
# Get image information from EXIF data in file
# Inputs: 0) ExifTool object reference, 1) file name
sub GetImageInfo($$)
{
    my ($exifTool, $file) = @_;
    my (@foundTags, $info, $writeOnly);
    my $pipe = $file;

    # filter out unwanted file extensions
    if ($filterFlag) {
        my $ext = ($file =~ /.*\.(.+)$/) ? uc($1) : '';
        if ($filterFlag & 0x02 or defined $filterExt{$ext}) {
            return unless $filterExt{$ext};
        }
    }
    if ($doUnzip) {
        # pipe through gzip or bzip2 if necessary
        if ($file =~ /\.gz$/i) {
            $pipe = qq{gzip -dc "$file" |};
        } elsif ($file =~ /\.bz2$/i) {
            $pipe = qq{bzip2 -dc "$file" |};
        }
    }
    my ($fp, $outfile);
    if ($outputExt and $verbose) {
        ($fp, $outfile) = OpenOutputFile($file);
        $fp or return;
        $tmpText = $outfile;    # deletes file if we exit prematurely
        $exifTool->Options(TextOut => $fp);
    }

    if ($isWriting) {
        my $success = SetImageInfo($exifTool, $file);
        $info = $exifTool->GetInfo('Warning', 'Error');
        if ($info->{Warning}) {
            my @warns = qw(Warning);
            push @warns, sort(grep /Warning /, keys %$info) if $exifTool->Options('Duplicates');
            foreach (@warns) {
                Warn "Warning: $info->{$_} - $file\n";
            }
        }
        $info->{Error} and Warn "Error: $info->{Error} - $file\n";
        # close output text file if necessary
        if ($outfile) {
            undef $tmpText;
            close($fp);
            $exifTool->Options(TextOut => \*STDOUT);
            if ($info->{Error}) {
                unlink $outfile;    # erase bad file
            } else {
                ++$countCreated;
            }
        }
        return;
    } else {
        my $tag;
        # request specified tags unless using print format option
        @foundTags = @tags unless @printFmt;
        # extract EXIF information from this file
        unless ($file eq '-' or -e $file) {
            Warn "File not found: $file\n";
            $outfile and close($fp), unlink $outfile;
            return;
        }
        unless ($binaryOutput or $outputExt or @printFmt or $htmlOutput > 1) {
            if ($htmlOutput) {
                print "<!-- $file -->\n";
            } else {
                print "======== $file\n" if $multiFile and not $quiet;
            }
        }

        # extract the information!
        $info = $exifTool->ImageInfo($pipe, \@foundTags);

        # all done now if we already wrote output file
        if ($fp) {
            if ($outfile) {
                undef $tmpText;
                close($fp);
                $exifTool->Options(TextOut => \*STDOUT);
                if ($info->{Error}) {
                    unlink $outfile;    # erase bad file
                } else {
                    ++$countCreated;
                }
            }
            if ($info->{Error}) {
                Warn "Error: $info->{Error} - $file\n";
                ++$countBad;
            }
            return;
        }
    }
    # check for file error
    if ($info->{Error}) {
        Warn "Error: $info->{Error} - $file\n";
        ++$countBad;
        return;
    }
    # print warnings to stderr if using binary output
    # (because we are likely ignoring them and piping stdout to file)
    # or if there is none of the requested information available
    if ($binaryOutput or not %$info) {
        my $warns = $exifTool->GetInfo('Warning', 'Error');
        foreach (sort keys %$warns) {
            my $type = $_;
            $type =~ s/ .*//;
            Warn "$type: $$warns{$_} - $file\n";
        }
    }
    # escape characters for html if requested
    if ($escapeHTML) {
        require Image::ExifTool::XMP;
        foreach (keys %$info) {
            $$info{$_} = Image::ExifTool::XMP::EscapeHTML($$info{$_});
        }
    }

    # open output file (or stdout if no output file)
    ($fp, $outfile) = OpenOutputFile($file);
    return unless $fp;

    # print the results for this file
    my $lineCount = 0;
    if (@printFmt) {
        # output using print format file (-p) option
        foreach (@printFmt) {
            print $fp $exifTool->InsertTagValues(\@foundTags, $_);
            ++$lineCount;
        }
    } elsif (not $disableOutput) {
        print $fp "<table>\n" if $htmlOutput;
        my $lastGroup = '';
        my ($tag, $line);
        foreach $tag (@foundTags) {
            my $tagName = GetTagName($tag);
            my $group;
            # make sure this tag has a value
            my $val = $info->{$tag};
            if (not defined $val) {
                # ignore tags that weren't found unless necessary
                next if $binaryOutput;
                next unless $forcePrint;
                $val = '-';     # forced to print all tag values
            }
            if (defined $showGroup) {
                $group = $exifTool->GetGroup($tag, $showGroup);
                unless ($allGroup) {
                    if ($lastGroup ne $group) {
                        if ($htmlOutput) {
                            my $cols = 1;
                            ++$cols if $outFormat==0 or $outFormat==1;
                            ++$cols if $showTagID;
                            print $fp "<tr><td colspan=$cols bgcolor='#dddddd'>$group</td></tr>\n";
                        } else {
                            print $fp "---- $group ----\n";
                        }
                        $lastGroup = $group;
                    }
                    undef $group;   # undefine so we don't print it below
                }
            }

            ++$lineCount;           # we are printing something meaningful

            my $id;
            if ($binaryOutput) {
                # translate scalar reference to actual binary data
                $val = $$val if ref $val eq 'SCALAR';
                print $fp $val;
                next;
            }
            if ($showTagID) {
                $id = $exifTool->GetTagID($tag);
                if ($id =~ /^\d+$/) {    # only print numeric ID's
                    $id = sprintf("0x%.4x", $id) if $showTagID eq 'H';
                } else {
                    $id = '-';
                }
            }
            if (ref $val eq 'SCALAR') {
                my $msg;
                if ($$val =~ /^Binary data/) {
                    $msg = $$val;
                } else {
                    $msg = 'Binary data ' . length($$val) . ' bytes';
                }
                $val = "($msg, use -b option to extract)";
            } elsif (ref $val eq 'ARRAY') {
                $val = join(', ',@$val);
            }
            # translate unprintable chars in value and remove trailing spaces
            $val =~ tr/\x01-\x1f\x7f/./;
            $val =~ s/\x00//g;
            $val =~ s/\s+$//;

            # get description in case we need it
            my $description = $exifTool->GetDescription($tag);

            if ($htmlOutput) {
                print $fp "<tr>";
                print $fp "<td>$group</td>" if defined $group;
                print $fp "<td>$id</td>" if $showTagID;
                if ($outFormat <= 0) {
                    print $fp "<td>$description</td><td>$val</td></tr>\n";
                } elsif ($outFormat == 1) {
                    print $fp "<td>$tagName</td><td>$val</td></tr>\n";
                } else {
                    # make value html-friendly
                    $val =~ s/&/&amp;/g;
                    $val =~ s/</&lt;/g;
                    $val =~ s/>/&gt;/g;
                    print $fp "<td>$val</td></tr>\n";
                }
            } else {
                if ($tabFormat) {
                    print $fp "$group\t" if defined $group;
                    print $fp "$id\t" if $showTagID;
                    if ($outFormat <= 0) {
                        print $fp "$description\t$val\n";
                    } elsif ($outFormat <= 1) {
                        print $fp "$tagName\t$val\n";
                    } elsif (defined $line) {
                        $line .= "\t$val";
                    } else {
                        $line = $val;
                    }
                } elsif ($outFormat < 0) {    # long format
                    print $fp "[$group] " if defined $group;
                    print $fp "$id " if $showTagID;
                    print $fp "$description\n      $val\n";
                } elsif ($outFormat == 0) {
                    printf $fp "%-15s ","[$group]" if defined $group;
                    if ($showTagID) {
                        my $wid = ($showTagID eq 'D') ? 5 : 6;
                        printf $fp "%${wid}s ", $id;
                    }
                    printf $fp "%-32s: %s\n",$description,$val;
                } elsif ($outFormat == 1) {
                    printf $fp "%-15s ", "[$group]" if defined $group;
                     if ($showTagID) {
                        my $wid = ($showTagID eq 'D') ? 5 : 6;
                        printf $fp "%${wid}s ", $id;
                    }
                    printf $fp "%-32s: %s\n",$tagName,$val;
                } elsif ($outFormat == 2) {
                    print $fp "[$group] " if defined $group;
                    print $fp "$id " if $showTagID;
                    print $fp "$tagName: $val\n";
                } else {
                    print $fp "$group " if defined $group;
                    print $fp "$id " if $showTagID;
                    print $fp "$val\n";
                }
            }
        }
        if ($htmlOutput) {
            print $fp "</table>\n";
        } elsif ($tabFormat and $outFormat > 1) {
            print $fp "$line\n" if defined $line;
        }
    }
    if ($outfile) {
        close($fp);
        if ($lineCount) {
            ++$countCreated;
        } else {
            unlink $outfile; # don't keep empty output files
        }
    }
    ++$count;
}

#------------------------------------------------------------------------------
# Is specified tag excluded by group?
# Inputs: 1) tag key
# Returns: true if tag is excluded by group
sub IsExcluded($$)
{
    return 0 unless %excludeGrp;
    my ($exifTool, $tag) = @_;
    # exclude all in group or specific GROUP:TAG
    my $tok;
    foreach $tok ('*', lc(GetTagName($tag))) {
        my $groupList = $excludeGrp{$tok} or next;
        my $grp0 = $exifTool->GetGroup($tag, 0);
        my $grp1 = $exifTool->GetGroup($tag, 1);
        return 1 if grep /^($grp0|$grp1)$/i, @$groupList;
    }
    return 0;
}

#------------------------------------------------------------------------------
# Load print format file
# Inputs: 0) file name
# - saves lines of file to @printFmt list
# - adds tag names to @tag list
sub LoadPrintFormat($)
{
    my $file = shift || Die "Must specify file for -p option\n";
    open(FMT_FILE, $file) or Die "Can't open file: $file\n";
    foreach (<FMT_FILE>) {
        /^#/ and next;  # ignore comments
        push @printFmt, $_;
        push @tags, /\$((?:[a-zA-Z0-9]+:)?[-a-zA-Z_0-9]+)/g;
    }
    close(FMT_FILE);
    @tags or Die "Print format file doesn't contain any tag names!\n";
}

#------------------------------------------------------------------------------
# Scan directory for image files
# Inputs: 0) ExifTool object reference, 1) directory name
sub ScanDir($$)
{
    my $exifTool = shift;
    my $dir = shift;
    opendir(DIR_HANDLE, $dir) or Die "Error opening directory $dir\n";
    my @fileList = readdir(DIR_HANDLE);
    closedir(DIR_HANDLE);

    my $file;
    $dir =~ /\/$/ or $dir .= '/';
    foreach $file (@fileList) {
        my $path = "$dir$file";
        if (-d $path) {
            next if $file =~ /^\./; # ignore dirs starting with "."
            next if grep /^$file$/, @ignore;
            $recurse and ScanDir($exifTool, $path);
            next;
        }
        # read/write this file if it is a recognized type
        if ($scanWritable) {
            if ($scanWritable eq '1') {
                next unless CanWrite($file);
            } else {
                next unless GetFileType($file) eq $scanWritable;
            }
        } elsif (not GetFileType($file)) {
            next unless $doUnzip;
            next unless $file =~ /\.(gz|bz2)$/i;
        }
        GetImageInfo($exifTool, $path);
    }
    ++$countDir;
}

#------------------------------------------------------------------------------
# Print list of tags
# Inputs: 0) Reference to hash whose keys are the tags to print
sub PrintTagList(@)
{
    my $len = 1;
    my $tag;
    print ' ';
    foreach $tag (@_) {
        my $taglen = length($tag);
        if ($len + $taglen > 78) {
            print "\n ";
            $len = 1;
        }
        print " $tag";
        $len += $taglen + 1;
    }
    @_ or print ' [empty list]';
    print "\n";
}

__END__

=head1 NAME

exiftool - Read/write meta information

=head1 SYNOPSIS

B<exiftool> [I<OPTIONS>] [-I<TAG>[[+-E<lt>]=[I<VALUE>]] or --I<TAG> ...] I<FILE> ...

=head1 DESCRIPTION

A command-line interface to L<Image::ExifTool|Image::ExifTool> used for
reading and writing meta information in image, audio and video files.
C<FILE> may be an image file name, a directory name, or C<-> for the
standard input. Information is read from the specified file and output in
readable form to the console (or written to an output text file with the
C<-w> option).

To write information to a file, specify new values using either the
C<-TAG=[VALUE]> syntax or the C<-TagsFromFile> option.  This causes exiftool
to rewrite C<FILE> with the specified information, preserving the original
file by renaming it to C<FILE_original>.  (Note: Be sure to verify that the
new file is OK before erasing the original.)

Below is a list of file types and meta information formats currently
supported by exiftool (r = read support, w = write support):

                 File Type                      Meta Information
    -----------------------------------        ------------------
    JPEG  r/w     ICC   r/w     MIFF  r        EXIF           r/w
    TIFF  r/w     MIE   r/w     PICT  r        GPS            r/w
    GIF   r/w     PPM   r/w     QTIF  r        IPTC           r/w
    CRW   r/w     PGM   r/w     RIFF  r        XMP            r/w
    CR2   r/w     PBM   r/w     AIFF  r        MakerNotes     r/w
    ERF   r/w     WDP   r/w     AVI   r        Photoshop IRB  r/w
    NEF   r/w     JP2   r       WAV   r        AFCP           r/w
    PEF   r/w     BMP   r       MPG   r        JFIF           r/w
    MRW   r/w     FPX   r       MP3   r        ICC Profile    r/w
    MOS   r/w     ORF   r       MP4   r        MIE            r/w
    DNG   r/w     RAF   r       MOV   r        FlashPix       r
    PNG   r/w     RAW   r       ASF   r        GeoTIFF        r
    MNG   r/w     SRF   r       WMA   r        PrintIM        r
    JNG   r/w     SR2   r       WMV   r        ID3            r
    XMP   r/w     X3F   r       RA    r
    THM   r/w     DCM   r       RM    r
    PSD   r/w     ACR   r       RAM   r
    EPS   r/w     AI    r       SWF   r
    PS    r/w     PDF   r

=head1 OPTIONS

Note:  Case is not significant for any command-line option (including tag
and group names), except for single-character options if the corresponding
upper case option is defined.  Multiple options may NOT be combined into a
single argument, because that would be interpreted as a tag name.

=over 5

=item B<->I<TAG>

Extract information for specified tag.  See
L<Image::ExifTool::TagNames|Image::ExifTool::TagNames> for documentation on
available tag names.  The tag name may begin with an optional group name
followed by a colon.  (ie. C<-TAG:GROUP>, where C<GROUP> is any valid family
0 or 1 group name optionally prefixed by a family number.  Use the C<-listg>
option to list valid group names.)  If no tags are specified, all available
information is extracted.

A special tag name of C<All> may be used to indicate all meta information.
This is particularly useful when a group name is specified to extract all
information in a group.  (C<*> is a synonym for C<All>, but must be quoted
if used on the command line to prevent shell globbing.)

=item B<-->I<TAG>

Exclude specified tag from extracted information.  Same as the C<-x> option.
May also be used following a C<-TagsFromFile> option to exclude tags from
being extracted from the source file.

=item B<->I<TAG>[+-E<lt>]B<=>[I<VALUE>]

Write a new value for the specified tag (with C<-TAG=VALUE>), or delete the
tag if C<VALUE> is not specified.  C<+=> and C<-=> add or remove C<VALUE>
from a list, or shift date/time values (see
L<Image::ExifTool::Shift.pl|Image::ExifTool::Shift.pl> for shift formats).
C<E<lt>=> sets the value of a tag from the contents of a file with name
C<VALUE>.  (Note: Quotes are required around the argument in this case to
prevent shell redirection.)

If a group name is not specified for C<TAG>, then the information is written
to the preferred group, which is the first group in the following list where
C<TAG> is valid:  1) EXIF, 2) GPS, 3) IPTC, 4) XMP, 5) MakerNotes.

The special C<All> tag may be used in this syntax only if a C<VALUE> is NOT
given.  This causes all meta information to be deleted (or all information
in a group if C<-GROUP:All=> is used).  Note that not all groups are
deletable.  Also, within an image some groups may be contained within
others, and these groups are removed if the super group is deleted.  Below
are lists of these group dependencies:

  JPEG Image:
  - Deleting EXIF or IFD0 also deletes ExifIFD, GlobParamIFD,
    GPS, IFD1, InteropIFD, MakerNotes, PrintIM and SubIFD.
  - Deleting ExifIFD also deletes InteropIFD and MakerNotes.
  - Deleting Photoshop also deletes IPTC.

  TIFF Image:
  - Deleting EXIF only removes ExifIFD which also deletes
    InteropIFD and MakerNotes.

=item B<-@> I<ARGFILE>

Read command-line arguments from the specified file.  The file contains one
argument per line.  Blank lines and lines beginning with C<#> and are
ignored.  C<ARGFILE> may exist relative to either the current directory or
the exiftool directory unless an absolute pathname is given.

=item B<-a>

B<A>llow duplicate tag names in the output.  Without this option, duplicates
are suppressed.

=item B<-b>

Output requested data in B<b>inary format.  Mainly used for extracting
embedded images.  Suppresses output of tag names and descriptions.

=item B<-c> I<FMT>

Set the print format for GPS B<c>oordinates.  C<FMT> uses the same syntax as
the C<printf> format string.  The specifiers correspond to degrees, minutes
and seconds in that order, but minutes and seconds are optional.  For
example, the following table gives the output for the same coordinate using
various formats:

            FMT                  Output
    -------------------    ------------------
    "%d deg %d' %.2f"\"    54 deg 59' 22.80"   (the default)
    "%d deg %.4f min"      54 deg 59.3800 min
    "%.6f degrees"         54.989667 degrees

=item B<-d> I<FMT>

Set the format for B<d>ate/time tag values.  Consult C<strftime> man page
for C<FMT> syntax. The default format is equivalent to "%Y:%m:%d %H:%M:%S".
This option has no effect on date-only or time-only tags.

=item B<-D>

Show tag ID number in B<D>ecimal.

=item B<-e>

Print B<e>xisting tags only -- don't calculate composite tags.

=item B<-E>

B<E>scape characters in output values for HTML.  Implied with the C<-h>
option.

=item B<-ext> I<EXT> (or B<--ext> I<EXT>)

Process only files with the specified extension, or use C<--ext> to exclude
files.  There may be multiple C<-ext> or C<--ext> options. Extensions may
begin with a leading '.', and case is not significant.  For example:

    exiftool -ext .JPG *            # process only JPG files
    exiftool --ext crw --ext dng *  # process all but CRW and DNG
    exiftool --ext . *              # ignore if no extension

=item B<-f>

B<F>orce printing of tags even if their values are not found.

=item B<-F>[VALUE]

Fix the base for maker notes offsets.  A common problem with some image
editors is that offsets in the maker notes are not adjusted properly when
the file is modified.  This may cause the wrong values to be extracted for
some maker note entries when reading the edited file.  This option allows an
integer value to be specified for adjusting the maker notes base offset.  If
no value is given, ExifTool will take its best guess at the correct base.

=item B<-fast>

Increases speed of extracting information from JPEG images.  With this
option, ExifTool will not scan to the end of a JPEG image to check for an
AFCP or PreviewImage trailer.  The speed benefits are small when reading
images directly from disk, but can be substantial if piping images through a
network connection.

=item B<-g>[#]

Organize output by tag B<g>roup.  C<#> specifies the group family number,
and may be 0 (general location), 1 (specific location) or 2 (category).  If
not specified, C<-g0> is assumed.  Use the C<-listg> option to list all
group names for a specified family.

=item B<-G>[#]

Same as C<-g> but print B<G>roup name for each tag.

=item B<-h>

Use B<H>TML formatting for output.  Implies C<-E> option.

=item B<-H>

Show tag ID number in B<H>exadecimal.

=item B<-htmlDump>

Generates a dynamic web page containing a hex dump of the EXIF information.
This can be a very powerful tool for low-level analysis of EXIF information.
The C<-htmlDump> option is also involked if the C<-v> and C<-h> options are
used together.  The verbose level controls the maximum length of the blocks
dumped.  Currently only works for EXIF and TIFF information.

=item B<-i> I<DIR>

B<I>gnore specified directory name.  May be multiple C<-i> options.

=item B<-l>

Use B<l>ong 2-line Canon-style output format.

=item B<-L>

Convert 16-bit Unicode characters in output to Windows B<L>atin1 (cp1252)
instead of the default UTF-8.

=item B<-list>, B<-listw>, B<-listf>, B<-listg>[#]

Print a B<list> of all valid tag names (C<-list>), all B<w>ritable tag names
(C<-listw>), all recognized B<f>ile types (C<-listf>), or all tag B<g>roups
in a specified family (C<-listg>).  The C<-list> and C<-listw> options may
be followed by an additional argument of the form C<-GROUP:All> to list all
tags in a specific group.  With C<-listg>, a number may be given to specify
the group family, otherwise family 0 is assumed.  For example:

    -list               # list all tag names
    -list -EXIF:All     # list all EXIF tags
    -listw -XMP-dc:All  # list all writable XMP-dc tags
    -listf              # list all recognized file types
    -listg1             # list all groups in family 1

=item B<-m>

Ignore B<m>inor errors.  Allows writing if some minor errors occur, or
extraction of embedded images that aren't in standard JPG format.

=item B<-n>

Read and write values as B<n>umbers instead of words.  This option disables
the print conversion that is applied when extracting values to make them
more readable, and the inverse print conversion when writing.  For example:

    > exiftool -Orientation -S a.jpg
    Orientation: Rotate 90 CW
    > exiftool -Orientation -S -n a.jpg
    Orientation: 6

and the following two writing commands have the same effect

    > exiftool -Orientation='Rotate 90 CW' a.jpg
    > exiftool -Orientation=6 -n a.jpg

=item B<-o> I<OUTFILE> or I<FMT>

Set B<o>utput file or directory name when writing information (otherwise the
source file is renamed to C<FILE_original> and the output file is C<FILE> in
the original directory).  The output file name may also be specified using a
C<FMT> string in which %d, %f and %e represent the directory, file name and
extension of C<FILE>.  See the C<-w> option for C<FMT> string examples.

The output file is taken to be a directory name if it already exists as a
directory or if the name ends with '/'.  Output directories are created if
necessary.  Existing files will not be overwritten.  Combining the
C<-overwrite_original> option with C<-o> causes the original source file to
be erased after the output file is successfully written.

A special feature of this option allows it to be used to create certain types
of files from scratch.  Currently, this can only be done with XMP and
ICC/ICM files.  The file is created from a combination of information in
C<FILE> and tag values assigned on the command line.  This is done by
specifying a file extension of '.XMP', '.ICC' or '.ICM' for C<OUTFILE>.  The
output file may be created even if no C<FILE> is specified, provided some
appropriate tag values are specified on the command line.

=item B<-overwrite_original>

Overwrite the original file instead of renaming it to C<FILE_original> when
writing information to an image.  Caution:  This option should only be used
if you already have separate backup copies of your image files.

=item B<-overwrite_original_in_place>

Similar to the C<-overwrite_original> option except that an extra step is
added to allow the original file attributes to be preserved.  On a Macintosh
for example, this preserves the original file type, creator and icon.

=item B<-p> I<FMTFILE>

B<P>rint output in the format specified by the given file (and ignore other
format options).  Tag names in the format file begin with a C<$> symbol and
may contain an optional group name.  Case is not significant.  Braces C<{}>
may be used around the tag name to separate it from subsequent text.  Use
C<$$> to represent a C<$> symbol.  Lines beginning with C<#> are ignored.
For example, this format file:

    # this is a comment line
    File $FileName was created on $DateTimeOriginal
    (f/$Aperture, ${ShutterSpeed}s, ISO $EXIF:ISO)

produces output like this:

    File test.jpg was created on 2003:10:31 15:44:19
    (f/5.6, 1/60s, ISO 100)

If a tag does not exist, the value is set to '-' in the output.

=item B<-P>

B<P>reserve date/time of original file when writing.

=item B<-q>

B<Q>uiet processing.  One C<-q> suppresses normal informational messages,
and a second C<-q> suppresses warnings as well.  Error messages can not be
suppressed, although minor errors may be downgraded to warnings with the
C<-m> option.

=item B<-r>

B<R>ecursively scan subdirectories.  Only meaningful if C<FILE> is a
directory name.

=item B<-s>

Print tag names instead of descriptions.  This is the B<s>hort output
format.  Add up to 3 C<-s> options for even shorter formats.  Also effective
when combined with C<-t> or C<-h> options.

=item B<-S>

Very B<s>hort format.  The same as two C<-s> options.  Extra spaces used
to column-align values are not printed.

=item B<-t>

Output a B<t>ab-delimited list of description/values (useful for database
import).  May be combined with C<-s> to print tag names instead of
descriptions, or C<-S> to print tag values only, tab-delimited on a single
line.

=item B<-TagsFromFile> I<SRCFILE> or I<FMT>

Set the value of writable tags from information in the specified source
file.  Tag names on the command line after this option specify information
to be extracted (or excluded) from the source file.  If no tags are
specified, then all tags found in the source file are used.  More than one
C<-TagsFromFile> option may be specified to set tag values from information
in different files.

By default, this option will commute information between same-named tags in
different groups, allowing information to be translated between images with
different formats.  This behaviour may be modified by specifying a group
name for extracted tags (even if C<All> is used as a group name), in which
case the information is written to the original group, unless redirected to
a different group.

C<SRCFILE> may be the same as C<FILE> to move information around within a
file.  C<@> may be used to represent C<FILE> (ie. C<-TagsFromFile @>),
permitting this feature to be used when batch processing multiple files.
Specified tags are then copied from file in turn as it is rewritten.  As a
convenience, C<-TagsFromFile @> is assumed for any redirected tags which are
specified without a prior C<-TagsFromFile> option.

A powerful information redirection feature allows a destination tag to be
specified for each extracted tag.  With this feature, information may be
written to a tag with a different name or group.  This is done using
E<quot>C<'-SRCTAGE<gt>DSTTAG'>E<quot> on the command line after
C<-TagsFromFile> (E<quot>C<'-DSTTAGE<lt>SRCTAG'>E<quot> also works).  Note
that this argument must be quoted to prevent shell redirection, and there is
no C<=> sign as there is when setting new values.  Both source and
destination tags may be prefixed by a group name, and C<All> or C<*> may be
used as a tag or group name.  If no destination group is specified, then the
information is written to the preferred group.

An extension of this redirection feature allows expressions involving tag
names to be used on the right hand side of the C<E<lt>> symbol with the
syntax E<quot>C<'-DSTTAGE<lt>EXP'>E<quot>, where C<EXP> is an expression
containing tag names with leading C<$> symbols.  See the C<-p> option for
more details about this syntax.  Expressions starting with a C<=> sign must
insert a single space after the C<E<lt>> to avoid confusion with the
C<E<lt>=> syntax which would otherwise attempt to set the tag value from the
contents of a file.  A single space at the start of an expression is removed
if it exists, but all other whitespace is preserved.

See L</COPYING EXAMPLES> for examples using C<-TagsFromFile>.

For advanced batch use, the source file name may also be specified using a
C<FMT> string in which %d, %f and %e represent the directory, file name and
extension of C<FILE>.  See C<-w> option for C<FMT> string examples.

Be aware of the difference between excluding a tag from being copied
(C<--TAG>), and deleting a tag (C<-TAG=>).  Excluding a tag will prevent it
from being copied to the destination image, but deleting a tag will remove
it if it already exists.

Note that the maker note information is set as a block, so it isn't affected
like other information by subsequent tag assignments on the command line.
For example, to copy all information but the thumbnail image, use
C<-ThumbnailImage=> after C<-TagsFromFile> on the command line.  Since the
preview image is referenced from the maker notes and may be rather large, it
is not copied.  Instead, the preview image must be transferred separately if
desired.

=item B<-u>

Extract values of B<u>nknown tags.  Add another C<-u> to also extract
unknown information from binary data blocks.

=item B<-U>

Extract values of B<u>nknown tags as well as unknown information from binary
data blocks.  This is the same as two C<-u> options.

=item B<-v>[#]

Print B<v>erbose messages.  A C<#> in the range 1-5 may be specified to
indicate the level of verbosity -- higher is more verbose.  This option
suppresses normal console output unless specific tags are being extracted.
Verbose output goes to the console, and is not affected by the C<-w> option.

=item B<-ver>

Print version number and exit.

=item B<-w> I<EXT> or I<FMT>

B<W>rite console output to a file with name ending in C<EXT> for each source
file.  The output file name is obtained by replacing the source file
extension (including the C<.>) with the specified extension.  Alternatively,
a format statement may be used to give more control over the output file
name and directory.  In this case, C<FMT> is a string specifying the output
file name.  In this string, %d, %f and %e represent the directory, filename
and extension of the source file.  (%d includes the trailing '/' if
necessary, but %e does not include the leading '.')  For example:

    -w %d%f.txt       # same effect as "-w txt"
    -w dir/%f_%e.out  # writes files to "dir" as "FILE_EXT.out"
    -w dir2/%d%f.txt  # writes to "dir2", keeping dir structure

Existing files will not be overwritten.  Output directories are created
automatically if necessary.

Advanced feature:  A substring of the original file name, directory or
extension may be taken by specifying a string length immediately following
the % character.  If the length is negative, the substring is taken from the
end.  The substring position (characters to ignore at the start or end of
the string) may be given by a second optional value after a decimal point.
For example:

    Input File Name     Format Specifier    Output File Name
    ----------------    ----------------    ----------------
    Picture-123.jpg     %7f.txt             Picture.txt
    Picture-123.jpg     %-.4f.out           Picture.out
    Picture-123.jpg     %7f.%-3f            Picture.123
    Picture-123a.jpg    Meta%-3.1f.txt      Meta123.txt

This same C<FMT> syntax is used with the C<-o> and C<-TagsFromFile> options.

=item B<-x> I<TAG>

EB<x>clude the specified tag.  There may be multiple C<-x> options.  This
has the same effect as C<--TAG> on the command line.  May also be used
following a C<-TagsFromFile> option to exclude tags from being extracted
from the source file.

=item B<-z>

When reading, causes information to be extracted from .gB<z> and .bB<z>2
compressed images.  Must be only one image in the compressed archive.  When
writing, causes compressed information to be written if supported in the
particular information format.

=back

=head1 READING EXAMPLES

=over 5

=item exiftool -g a.jpg

Print all EXIF information sorted by group (for family 0).

=item exiftool -common dir

Print common EXIF information for all images in C<dir>.

=item exiftool -s -ImageSize -ExposureTime b.jpg

Print ImageSize and ExposureTime tag names and values.

=item exiftool -l -canon c.jpg d.jpg

Print standard Canon information from 2 image files.

=item exiftool -r -w .txt -common pictures

Recursively save common EXIF information for files in C<pictures> directory
into files with the same names as the images but with a C<.txt> extension.

=item exiftool -b -ThumbnailImage image.jpg > thumbnail.jpg

Save thumbnail image from C<image.jpg> to a file called C<thumbnail.jpg>.

=item exiftool -b -JpgFromRaw -w _JFR.JPG -r .

Recursively extract JPG image from all Canon RAW files in the current
directory, adding C<_JFR.JPG> for the name of the output JPG files.

=item exiftool -b -PreviewImage 118_1834.JPG > preview.jpg

Extract preview image from JPG file and write it to C<preview.jpg>.

=item exiftool -d '%r %a, %B %e, %Y' -DateTimeOriginal -S -s *.jpg

Print formatted date/time for all JPG files in a directory.

=item exiftool -IFD1:XResolution -IFD1:YResolution

Extract image resolution from IFD1.

=item exiftool -xmp -b a.jpg > xmp.out

Extract complete XMP data record intact from C<a.jpg> and write it to
C<xmp.out> using the special C<XMP> tag (see the Extra tags in
L<Image::ExifTool::TagNames|Image::ExifTool::TagNames>).

=item exiftool -htmldump -w tmp/%f_%e.html t/images

Generate HTML pages from a hex dump of EXIF information in all images from
the C<t/images> directory.  The output HTML files are written to the C<tmp>
directory (which is created if it didn't exist), with names of the form
'FILENAME_EXT.html'.

=back

=head1 WRITING EXAMPLES

Note that quotes are necessary around arguments which contain certain
special characters such as E<gt>, E<lt> or any white space.  These quoting
techniques are shell dependent, but the examples below will work for most
Unix shells.  With the Windows cmd shell however, double quotes should be
used around the entire argument (ie. "-Comment=This is a new comment")

=over 5

=item exiftool -Comment='This is a new comment' dst.jpg

Write new comment to a JPG image (replaces any existing comment).

=item exiftool -comment= -o newdir *.jpg

Remove comment from all JPG images in the current directory, writing the
modified images to a new directory.

=item exiftool -keywords=EXIF -keywords=editor dst.jpg

Replace existing keyword list with two new keywords (C<EXIF> and C<editor>).

=item exiftool -Keywords+=word -o newfile.jpg src.jpg

Copy a source image to a new file, and add a keyword (C<word>) to the
current list of keywords.

=item exiftool -category-=xxx dir

Delete only the specified category (C<xxx>) from all files in directory.

=item exiftool -all= dst.jpg

Delete all meta information from an image.

=item exiftool -Photoshop:All= dst.jpg

Delete Photoshop meta information from an image (note that the Photoshop
information also includes IPTC).

=item exiftool '-ThumbnailImageE<lt>=thumb.jpg' dst.jpg

Set the thumbnail image from specified file (Note: The quotes are neccessary
to prevent shell redirection).

=item exiftool -DateTimeOriginal-='0:0:0 1:30:0' dir

Adjust original date/time of all images in directory C<dir> by subtracting
one hour and 30 minutes.  (This is equivalent to C<-DateTimeOriginal-=1.5>.
See L<Image::ExifTool::Shift.pl|Image::ExifTool::Shift.pl> for details.)

=item exiftool -createdate+=3 -modifydate+=3 a.jpg b.jpg

Add 3 hours to the CreateDate and ModifyDate timestamps of two images.

=item exiftool -AllDates+=1:30 image.jpg

When shifting date/time values in JPEG images, typically three date/time
tags must be updated:  DateTimeOriginal, CreateDate and ModifyDate.  The
AllDates tag is provided as a shortcut for these tags, allowing all three
values to be shifted at once.  The command above shifts these values forward
by 1 hour and 30 minutes.

=item exiftool -xmp:city=Kingston dst.jpg

Write a tag to the XMP group (otherwise in this case the tag would get
written to the IPTC group since C<City> exists in both, and IPTC has
priority).

=item exiftool -Canon:ISO=100 dst.jpg

Set C<ISO> only in the Canon maker notes.

=item exiftool -LightSource-='Unknown (0)' dst.tiff

Delete C<LightSource> tag only if it is unknown with a value of 0.

=item exiftool -whitebalance-=auto -WhiteBalance=tung dst.jpg

Set C<WhiteBalance> to C<Tungsten> only if it was previously C<Auto>.

=item exiftool -o %d%f.xmp dir

Create XMP meta information data files for all images in C<dir>.

=item exiftool -o test.xmp -owner=Phil -title='XMP File'

Create an XMP data file only from tags defined on the command line.

=back

=head1 COPYING EXAMPLES

These examples demonstrate the ability to copy tag values between files.

=over 5

=item exiftool -TagsFromFile src.crw dst.jpg

Copy the values of all writable tags from C<src.crw> to C<dst.jpg>, writing
the information to the preferred groups.

=item exiftool -TagsFromFile src.jpg -all:all dst.jpg

Copy the values of all writable tags from C<src.jpg> to C<dst.jpg>,
preserving the original tag groups.

=item exiftool -tagsfromfile a.jpg out.xmp

Copy meta information C<a.jpg> to an XMP data file.  If the XMP data file
C<out.xmp> already exists, it will be updated with the new information.
Otherwise the XMP data file will be created.  Only XMP and ICC files may be
created like this (other file types may be edited but not created).

=item exiftool -tagsFromFile a.jpg -XMP:All= -ThumbnailImage= -m b.jpg

Copy all meta information from C<a.jpg> to C<b.jpg>, deleting all XMP
information and the thumbnail image from the destination.

=item exiftool -TagsFromFile src.jpg -title -author=Phil dst.jpg

Copy title from one image to another and set a new author name.

=item exiftool -TagsFromFile a.jpg -ISO -TagsFromFile b.jpg -comment
dst.jpg

Copy ISO from one image and Comment from another image to a destination
image.

=item exiftool -tagsfromfile src.jpg -exif:all --subifd:all dst.jpg

Copy only the EXIF information from one image to another, excluding SubIFD
tags.

=item exiftool '-DateTimeOriginal>FileModifyDate' dir

Use the original date from the meta information to set the same file's
filesystem modification date for all images in a directory.  (Note that
C<-TagsFromFile @> is assumed if no other C<-TagsFromFile> is specified when
redirecting information as in this example.)

=item exiftool -TagsFromFile src.jpg '-all>xmp:all' dst.jpg

Copy all possible information from C<src.jpg> and write in XMP format to
C<dst.jpg>.

=item exiftool -tagsFromFile a.jpg -@ iptc2xmp.args -iptc:all= a.jpg

Translate IPTC information to XMP with appropriate tag name conversions, and
delete the original IPTC information from an image.  This example uses
iptc2xmp.args, which is a file included with the ExifTool distribution that
contains the required arguments to convert IPTC information to XMP format.
Also included with the distribution is xmp2iptc.args, which performs the
inverse conversion.

=item exiftool -tagsfromfile %d%f.CRW -r -ext JPG dir

Recursively rewrite all C<JPG> images in C<dir> with information copied from
the corresponding C<CRW> images in the same directories.

=item exiftool '-comment<ISO=$exif:iso Exposure=${shutterspeed}' dir

Set the Comment tag of all images in C<dir> from the values of the EXIF:ISO
and ShutterSpeed tags.  The resulting comment will be in the form "ISO=100
Exposure=1/60".

=back

=head1 RENAMING EXAMPLES

By writing the C<FileName> and C<Directory> tags, files are renamed and/or
moved to new directories.  This can be particularly useful and powerful for
organizing files by date when combined with the C<-d> option.  New
directories are created as necessary, but existing files will not be
overwritten.  The file name format codes %d, %f and %e (see the C<-w> option
for details) may be used to represent the directory, name and extension of
the original file, but note that if used within a date format string, an
extra '%' must be added to pass these codes through the date/time parser.

=over 5

=item exiftool -filename=new.jpg dir/old.jpg

Rename C<old.jpg> to C<new.jpg> in directory C<dir>.

=item exiftool -directory=%e dir

Move all files from directory C<dir> into directories named by the original
file extensions.

=item exiftool '-Directory<DateTimeOriginal' -d %Y/%m/%d dir

Move all files in C<dir> into a directory hierarchy based on year, month and
day of C<DateTimeOriginal>.  ie) This command would move the file
C<dir/image.jpg> with a C<DateTimeOriginal> of C<2005:10:12 16:05:56> to
C<2005/10/12/image.jpg>.

=item exiftool '-FileName<CreateDate' -d %Y%m%d_%H%M%S.%%e dir

Rename all images in C<dir> according to the C<CreateDate> date and time,
preserving the original file extensions.  Note the extra '%' added to the
file extension code (C<%%e>) since it is used in a date format string.

=item exiftool -r '-FileName<CreateDate' -d %Y-%m-%d/%H%M_%%f.%%e dir

Both the directory and the filename may be changed together via the
C<FileName> tag if the new C<FileName> contains a '/'.  The example above
recursively renames all images in a directory by adding a C<CreateDate>
timestamp to the start of the filename, then moves them into new directories
named by date.

=item exiftool '-FileName<${CreateDate}_$filenumber.jpg' -d %Y%m%d dir/*.jpg

Set the filename of all JPG images in the current directory from the
CreateDate and FileNumber tags, in the form "20060507_118-1861.jpg".

=back

=head1 PIPING EXAMPLES

=over 5

=item cat a.jpg | exiftool -

Extract information from stdin.

=item exiftool image.jpg -thumbnailimage -b | exiftool -

Extract information from an embedded thumbnail image.

=item cat a.jpg | exiftool -iptc:keywords+=fantastic - > b.jpg

Add an IPTC keyword in a pipeline, saving output to a new file.

=item exiftool a.jpg -thumbnailimage -b | exiftool -comment=wow - |
      exiftool a.jpg -thumbnailimage'<=-'

Add a comment to an embedded thumbnail image.  (Why anyone would want to do
this I don't know, but I've included this as an example to illustrate the
flexibility of ExifTool.)

=back

=head1 AUTHOR

Copyright 2003-2006, Phil Harvey

This is free software; you can redistribute it and/or modify it under the
same terms as Perl itself.

=head1 SEE ALSO

L<Image::ExifTool(3pm)|Image::ExifTool>,
L<Image::ExifTool::TagNames(3pm)|Image::ExifTool::TagNames>,
L<Image::ExifTool::Shortcuts(3pm)|Image::ExifTool::Shortcuts>,
L<Image::ExifTool::Shift.pl|Image::ExifTool::Shift.pl>

=cut

#------------------------------------------------------------------------------
# end
