#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# File:         exiftool
#
# Description:  Extract EXIF information from image files
#
# Revisions:    Nov. 12/03 - P. Harvey Created
#               (See html/history.html for revision history)
#------------------------------------------------------------------------------
use strict;
require 5.002;
require Image::ExifTool;

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

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

BEGIN {
    # get exe directory
    my $exeDir = $0;        # get exe directory from command
    $exeDir =~ 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";
}

END {
    Cleanup();
}

my @files;          # list of files and directories to scan
my @tags;           # list of EXIF 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 $countDir = 0;   # count of directories scanned
my $countGoodWr = 0;# count files written OK
my $countSameWr = 0;# count files written OK but not changed
my $countBadWr = 0; # count write errors
my $outputExt;      # extension for output file (or undef for no output)
my $listTags;       # flag to list all tags (=2 for writable tags)
my $listGroups;     # flag to list all groups
my $forcePrint;     # force printing of tags whose values weren't found
my $htmlOutput = 0; # flag for html-formatted output
my $escapeHTML;     # flag to escape printed values for html
my $binaryOutput;   # flag for binary output
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 $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

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

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

my $exifTool = new Image::ExifTool; # create ExifTool object
my @exclude;
my %excludeGrp;     # hash of tags excluded by group

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

# parse command-line options
while ($_ = shift) {
    if (s/^-//) {
        /^list$/i and $listTags = 1, next;
        /^listw$/i and $listTags = 2, next;
        /^group(\d*)$/i and $listGroups = $1, next;
        /^ver$/i and print("ExifTool version $Image::ExifTool::VERSION\n"), exit 0;
        /^alltagsfromfile$/i and push(@newValues, "$_=" . (@ARGV ? shift : '')), next;
        /^a$/i and $exifTool->Options(Duplicates => 1), next;
        /^b$/i and $binaryOutput = 1, next;
        /^d$/  and $exifTool->Options('DateFormat', shift || die "Expecting date format\n"), next;
        /^D$/  and $showTagID = 'D', next;
        /^e$/  and $exifTool->Options(Composite => 0), next;
        /^E$/  and $escapeHTML = 1, next;
        /^f$/i and $forcePrint = 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;
        /^i$/i and push(@ignore,shift || die "Expecting directory name\n"), next;
        /^l$/  and --$outFormat, next;
        /^L$/  and $exifTool->Options(Charset => 'Latin'), next;
        /^m$/i and $exifTool->Options(IgnoreMinorErrors => 1), next;
        /^n$/i and $exifTool->Options(PrintConv => 0), next;
        /^o$/i and $outOpt = shift || die("Expected output file or directory name\n"), next;
        /^p$/  and LoadPrintFormat(shift || die "Expecting file name\n"), next;
        /^P$/  and $preserveTime = 1, next;
        /^r$/i and $recurse = 1, next;
        /^s$/  and ++$outFormat, next;
        /^S$/  and $outFormat+=2, next;
        /^t$/i and $tabFormat = 1, next;
        /^u$/  and $exifTool->Options(Unknown => $exifTool->Options('Unknown')+1), next;
        /^U$/  and $exifTool->Options(Unknown => 2), next;
        if (/^v(\d*)$/i) {
            my $ver = $1;
            # -v with no # increments the verbosity
            $ver eq '' and $ver = $exifTool->Options('Verbose') + 1;
            $exifTool->Options(Verbose => $ver);
            next;
        }
        /^w$/i and $outputExt = shift || die("Expecting output extension\n"), next;
        /^x$/i and AddExclude(shift), next;
        /^z$/i and $doUnzip = 1, next;
        $_ eq '' and push(@files, '-'), next;   # read STDIN
        if (/=/) {
            push @newValues, $_;
        } elsif (/^-(.*)/) {
            AddExclude($1);
        } else {
            push @tags, $_;
        }
    } else {
        push @files, $_;
    }
}

# handle '-list' command-line option
if ($listTags) {
    # load all TagTables to get all descriptions
    my @tagList;
    if ($listTags == 2) {
        @tagList = Image::ExifTool::GetWritableTags();
        print "Writable tags:\n";
        PrintTagList(@tagList);
    } else {
        @tagList = Image::ExifTool::GetAllTags();
        print "Available tags:\n";
        PrintTagList(@tagList);
        # also print shortcuts
        @tagList = Image::ExifTool::GetShortcuts();
        if (@tagList) {
            print "\nCommand-line shortcuts:\n";
            PrintTagList(@tagList);
        }
    }
    exit 0;
}

# handle '-group#' command-line option
if (defined $listGroups) {
    $listGroups = 0 unless $listGroups;
    # load all TagTables to get all descriptions
    my @groupList = Image::ExifTool::GetAllGroups($listGroups);
    print "Groups in family $listGroups:\n";
    PrintTagList(@groupList);
    exit 0;
}

# print help
unless (@tags or @files or @newValues) {
    if (system "perldoc '$0'") {
        print "Run 'perldoc exiftool' for help on exiftool.\n";
    }
    exit 0;
}

# can't do anything if no file specified
die "No file specified\n" unless @files or $listTags;

# validate all tags we're writing
if (@newValues) {
    foreach (@newValues) {
        /(.*?)=(.*)/s or next;
        my ($tag, $newVal) = ($1, $2);
        $newVal eq '' and undef $newVal;    # undefined to delete tag
        if ($tag =~ /^AllTagsFromFile$/i) {
            die "Need file name for -AllTagsFromFile\n" unless defined $newVal;
            my $info = $exifTool->SetNewValuesFromFile($newVal);
            $info->{Error} and die "Error: $info->{Error} - $newVal\n";
            if ($info->{Warning}) {
                warn "Warning: $info->{Warning} - $newVal\n";
                delete $info->{Warning};
            }
            ++$isWriting;
            %$info or warn "No writable tags found - $newVal\n";
            next;
        }
        my %opts = ( Protected => 1 );      # allow writing of 'unsafe' tags
        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);
            read(INFILE,$newVal,16000000) or die "Error reading $file\n";
            close(INFILE);
        }
        $tag =~ s/\+// and $opts{AddValue} = 1;
        if ($tag =~ s/-$//) {
            $opts{DelValue} = 1;
            # set $newVal to '' if deleting nothing
            $newVal = '' unless defined $newVal;
        }
        if ($tag =~ /(.*?):(.*)/) {
            $opts{Group} = $1;
            $tag = $2;
        }
        $exifTool->SetNewValue($tag, $newVal, %opts) and ++$isWriting;
    }
    die "Nothing to do.\n" unless $isWriting or @tags;
}

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

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

if ($outputExt) {
    # add '.' before output extension if it doesn't contain one already
    $outputExt = ".$outputExt" unless $outputExt =~ /\./;
}

if ($binaryOutput) {
    $outFormat = 99;    # shortest possible output format
    $exifTool->Options(PrintConv => 0);
    binmode(STDOUT);
    $binaryStdout = 1;
}

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

# print summary and exit
my $tot = $count + $countBad;
my $totWr = $countGoodWr + $countBadWr + $countSameWr;
if (($countDir or $totWr or $tot > 1) and not $binaryStdout) {
    printf("%5d directories scanned\n", $countDir) if $countDir;
    printf("%5d image files updated\n", $countGoodWr) if $totWr;
    printf("%5d image files unchanged\n", $countSameWr) if $countSameWr;
    printf("%5d files weren't updated due to errors\n", $countBadWr) if $countBadWr;
    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", $count-$countBad) if $outputExt;
}
exit 0;     # all done

#------------------------------------------------------------------------------
# Set information in file
# Inputs: 0) ExifTool object reference, 1) file name
# Returns: true on success
sub SetImageInfo($$)
{
    my ($exifTool, $file) = @_;
    # only need to rewrite file if we set a valid tag
    my $outfile;
    # get output file name
    if (defined $outOpt and $outOpt ne '-') {
        if (-d $outOpt) {
            ($outfile = $outOpt) =~ tr/\\/\//;
            $outfile .= '/' unless $outfile =~ /\/$/;
            my $name = $file;
            $name =~ tr/\\/\//;
            $name =~ s/.*\///;  # remove directory name
            $outfile .= $name;
        } else {
            $outfile = $outOpt;
        }
        if (-e $outfile) {
            warn "Error: File already exists: $outfile\n";
            return;
        }
        $tmpFile = $outfile;
    } elsif ($file eq '-' or $outOpt) {
        # write to STDOUT
        $outfile = \*STDOUT;
        binmode(STDOUT);
        $binaryStdout = 1;
        undef $tmpFile;
    } else {
        $outfile = "${file}_exiftool_tmp";  # write to temporary file
        $tmpFile = $outfile;
    }
    my $success = $exifTool->WriteInfo($file, $outfile);
    # fail on minor errors too unless directed otherwise
    if ($exifTool->GetValue('Error') and not $exifTool->Options('IgnoreMinorErrors')) {
        $success = 0;
    }
    if ($success == 1) {
        ++$countGoodWr;
        # preserve the original file times
        if (defined $tmpFile) {
            if ($preserveTime) {
                my $modTime = $^T - (-M $file) * (24 * 3600);
                my $accTime = $^T - (-A $file) * (24 * 3600);
                utime($accTime, $modTime, $tmpFile);
            }
            unless (defined $outOpt) {
                # move original out of the way
                my $original = "${file}_original";
                unless (-e $original) {
                    rename($file, $original) or die "Error renaming $file\n";
                }
                unless (rename($tmpFile, $file)) {
                    warn "Error renaming temporary file\n";
                    unlink $tmpFile;
                }
            }
        }
    } elsif ($success) {
        ++$countSameWr;
        # just erase the temporary file since no changes were made
        unlink $tmpFile if defined $tmpFile and not defined $outOpt;
    } else {
        ++$countBadWr;
        unlink $tmpFile if defined $tmpFile;
    }
    undef $tmpFile;
    return $success;
}

#------------------------------------------------------------------------------
# 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;

    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" |};
        }
    }
    if ($isWriting and not @tags) {
        my $success = SetImageInfo($exifTool, $file);
        unless ($success and $exifTool->Options('IgnoreMinorErrors')) {
            $info = $exifTool->GetInfo('Warning', 'Error');
            $info->{Warning} and warn "Warning: $info->{Warning} - $file\n";
            $info->{Error} and warn "Error: $info->{Error} - $file\n";
        }
        return;
    } else {
        my ($tag, $doGroup, %options);
        # don't request specific tags if using print format option
        unless (@printFmt) {
            # copy over tags and strip off group names
            foreach $tag (@tags) {
                if ($tag =~ /.+?:(.+)/) {
                    push @foundTags, $1;
                    $doGroup = 1;
                } else {
                    push @foundTags, $tag;
                }
            }
        }
        # temporarily change options to make it possible to
        # weed out specific groups later
        $doGroup = 1 if %excludeGrp;
        $options{'Duplicates'} = 1 if $doGroup or @printFmt;
        $options{'Sort'} = 'Input' if $doGroup;

        # extract EXIF information from this file
        unless ($file eq '-' or -e $file) {
            warn "File not found: $file\n";
            return;
        }
        unless ($binaryOutput or $outputExt or @printFmt) {
            if ($htmlOutput) {
                print "<!-- $file -->\n";
            } else {
                print "======== $file\n" if $multiFile;
            }
        }
        # extract the information!
        $info = $exifTool->ImageInfo($pipe, \@foundTags, \%options);

        # get tags for the specified groups if required
        if ($doGroup) {
            FilterGroups(\@foundTags);
            # re-sort tags if necessary
            if (not defined $exifTool->Options('Sort') or
                $options{Sort} ne $exifTool->Options('Sort'))
            {
                @foundTags = $exifTool->GetTagList(\@foundTags);
            }
        }
    }
    # 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) {
            warn "$_: $$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
    my $fp;
    my $outfile;
    if ($outputExt) {
        $outfile = $file;
        $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;
    }

    # print the results for this file
    my $lineCount = 0;
    if (@printFmt) {
        # output using print format file (-p) option
        foreach (@printFmt) {
            my $line = $_;  # copy the print format line
            while ($line =~ /(.*?)\$([-a-zA-Z_0-9]+)(.*)/s) {
                my ($pre, $tag, $group);
                ($pre, $tag, $line) = ($1, $2, $3);
                # check to see if this is a group name
                if ($line =~ /^:([-a-zA-Z_0-9]+)(.*)/s) {
                    $group = lc($tag);
                    ($tag, $line) = ($1, $2);
                }
                my $val;
                if ($group) {
                    # find the specified tag
                    my @matches = grep /^$tag(\s|$)/i, @foundTags;
                    foreach $tag (@matches) {
                        next unless ($group eq lc($exifTool->GetGroup($tag, 0)) or
                                     $group eq lc($exifTool->GetGroup($tag, 1)));
                        $val = $info->{$tag};
                    }
                } else {
                    $val = $info->{$tag};
                    unless (defined $val) {
                        # check for tag name with different case
                        ($tag) = grep /^$tag$/i, @foundTags;
                        $val = $info->{$tag} if defined $tag;
                    }
                }
                $val = '-' unless defined $val;
                print $fp $pre, $val;
            }
            ++$lineCount;
            print $fp $line;
        }
    } else {
        print $fp "<table>\n" if $htmlOutput;
        my $lastGroup = '';
        my $tag;
        foreach $tag (@foundTags) {
            my $tagName = Image::ExifTool::GetTagName($tag);
            my $group;
            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
                }
            }
            my $val = $info->{$tag};

            my $description = $exifTool->GetDescription($tag);
            if (not defined $val) {
                # ignore tags that weren't found unless necessary
                next if $binaryOutput;
                next unless $forcePrint or $outFormat+$htmlOutput>=3;
                $val = '-';     # forced to print all tag values
            }
            ++$lineCount;

            my $id;
            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 ($binaryOutput) {
                # translate scalar reference to actual binary data
                $val = $$val if ref $val eq 'SCALAR';
                print $fp "$id " if $showTagID;
                print $fp $val;
                next;
            }
            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
            $val =~ tr/\x01-\x1f\x7f/./;
            $val =~ s/\x00//g;

            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 >= 2) {
                        print $fp "$tagName\t$val\n";
                    } else {
                        print $fp "$description\t$val\n";
                    }
                } 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 "%-12s ", $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 == 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";
                }
            }
        }
        print $fp "</table>\n" if $htmlOutput;
    }
    if ($outfile) {
        close(OUTFILE);
        $lineCount or unlink $outfile; # don't keep empty output files
    }
    SetImageInfo($exifTool, $file) if $isWriting and @tags;
    ++$count;
}

#------------------------------------------------------------------------------
# filter out specific groups from tag list
# Inputs: 0) reference to tag list
# Note: this logic relies on the order of the found tags
sub FilterGroups($)
{
    my ($foundTags, $reSort) = @_;
    my ($groupTag, @newFoundTags);
    my $dups = $exifTool->Options('Duplicates');
    my $inputTags = \@tags;
    unless (@tags) {
        my @allInput = grep !/ /, @$foundTags;
        $inputTags = \@allInput;
    }
    my $tag = shift @$foundTags;
    foreach $groupTag (@$inputTags) {
        my $foundTag = $tag;
        my $numFound = scalar @newFoundTags;
        if ($groupTag =~ /(.+?):.+/) {
            # only allow the specific GROUP:TAG requested
            my $group = lc($1);
            for (;;) {
                if ($group eq lc($exifTool->GetGroup($tag, 0)) or
                    $group eq lc($exifTool->GetGroup($tag, 1)))
                {
                    push @newFoundTags, $tag;
                }
                $tag = shift @$foundTags;
                last unless $tag and $tag =~ / /;
            }
        } else {
            # exclude specified GROUP:TAG and duplicates unless requested
            unless (IsExcluded($tag)) {
                push @newFoundTags, $tag;
            }
            for (;;) {
                $tag = shift @$foundTags;
                last unless $tag and $tag =~ / /;
                if ($dups or $numFound == scalar @newFoundTags) {
                    next if IsExcluded($tag);
                    push @newFoundTags, $tag;
                }
            }
        }
        # push invalid tag as placeholder in list if necessary
        if ($numFound == scalar @newFoundTags) {
            my $bogusTag = "$foundTag (x)";
            push @newFoundTags, $bogusTag;
        }
    }
    # return new tag list in original array
    @$foundTags = @newFoundTags;
}

#------------------------------------------------------------------------------
# Add tag to exclude list
# Inputs: 0) tag name
sub AddExclude($)
{
    my $tag = shift or die "Expecting tag name";
    if ($tag =~ /(.+?):(.+)/) {
        # convert group and tag to lower case for case-insensitive lookups
        my $group = lc($1);
        $tag = lc($2);
        $excludeGrp{$tag} or $excludeGrp{$tag} = [ ];
        # save in list of excluded groups for this tag
        push @{$excludeGrp{$tag}}, $group;
    } else {
        push @exclude, $tag;
    }
}

#------------------------------------------------------------------------------
# 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 $tag = shift;
    # exclude specific GROUP:TAG
    my $lcTag = lc(Image::ExifTool::GetTagName($tag));
    my $groupList = $excludeGrp{$lcTag};
    return 0 unless $groupList;
    my $group;
    foreach $group (@$groupList) {
        return 1 if $group eq lc($exifTool->GetGroup($tag, 0)) or
                    $group eq lc($exifTool->GetGroup($tag, 1));
    }
    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-Z_0-9]+)/g;
    }
    close(FMT_FILE);
    @tags or die "Print format file doesn't contain any tags 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;
    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;
        }
        # get information for this file if it is a recognized type
        unless (Image::ExifTool::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;
    }
    $len and print "\n";
}

__END__

=head1 NAME

exiftool - Read/write meta information in image files

=head1 SYNOPSIS

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

=head1 DESCRIPTION

Read or write meta information in image 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 in an image file, specify new values using either the
C<-TAG=VALUE> syntax or the C<-AllTagsFromFile> 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 meta information formats and file types supported by
exiftool (r = read support, w = write support):

     Meta Information                   File Types
    ------------------          --------------------------
    EXIF           r/w          JPEG   r/w      NEF    r/w
    GPS            r/w          JP2    r        ORF    r
    IPTC           r/w          TIFF   r/w      PEF    r/w
    XMP            r/w          GIF    r/w
    MakerNotes     r/w          THM    r/w
    GeoTIFF        r            CRW    r/w
    ICC Profile    r            CR2    r/w
    Photoshop IRB  r            DNG    r/w
    PrintIM        r            MRW    r

=head1 OPTIONS

Note:  Case is not significant for any command-line option (including tag
and group names), except for certain single-character options where the
corresponding upper case option is defined.

=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.  Use the C<-group> option to list valid group names.)  If
no tags are specified, all available information is extracted.

=item B<-->I<TAG>

Exclude specified tag from extracted information.  Same as the C<-x> option.

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

Writes a new value for the specified tag, or deletes the tag if C<VALUE> is
not specified.  Use C<+=> to add a value to a list without replacing
existing values, and C<-=> to delete a specific value from a list.  Use
C<E<lt>=> to set 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.)

=item B<-a>

B<A>llow duplicate tag names in the output (otherwise duplicates are
suppressed).

=item B<-AllTagsFromFile> I<SRCFILE>

Set the value of all writable tags from information in the specified source
file.

=item B<-b>

Output requested data in B<b>inary format.  Mainly used for extracting
embedded images.

=item B<-d> I<FMT>

Set B<d>ate/time format (consult strftime man page for FMT syntax).

=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.

=item B<-f>

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

=item B<-g>[#]

Organize output by tag B<g>roup (-g0 assumed if # not specified).

=item B<-G>[#]

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

=item B<-H>

Show tag ID number in B<H>exadecimal.

=item B<-group>[#]

List all tag groups for family #.  Family 0 assumed if # not specified.

=item B<-h>

Use B<H>TML formatting for output (implies -E option).

=item B<-i> I<DIR>

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

=item B<-l>

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

=item B<-L>

Convert Unicode characters in output to Windows B<L>atin1 (cp1252) instead
of the default UTF8.

=item B<-list>

List all valid tag names.

=item B<-listw>

List all writable tag names.

=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>

Do B<n>ot apply print conversion to displayed tag values.

=item B<-o> I<OUTFILE>

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>).

=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.  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 sec, ISO $EXIF:ISO)

produces output like this:

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

=item B<-P>

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

=item B<-r>

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

=item B<-s>

Use B<s>hort output format (add up to 3 -s options for even shorter
formats).

=item B<-S>

Print tag names instead of descriptions (very B<s>hort format, same as two
-s options).

=item B<-t>

Output a B<t>ab-delimited list of description/values (useful for database
import).

=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 (same as two C<-u> options).

=item B<-v>[#]

Print B<v>erbose messages (# may be 1-4, higher is more verbose).

=item B<-ver>

Print version number and exit.

=item B<-w> I<EXT>

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.

=item B<-x> I<TAG>

EB<x>clude specified tag (may be many -x options).  Same as C<--TAG>.

=item B<-z>

Extract information from .gB<z> and .bB<z>2 compressed images.

=back

=head1 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 '_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.

=back

=head1 WRITING EXAMPLES

=over 5

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

Set comment in file (replaces any existing comment).

=item exiftool -comment= *.jpg

Remove comment from all JPG files.

=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 dst.jpg

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 -AllTagsFromFile src.crw dst.jpg

Set the values of all writable tags from information in C<src.crw>, and
update C<dst.jpg> with this new information.

=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 -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>.

=back

=head1 PIPING EXAMPLES

=over 5

=item cat a.jpg | exiftool -

Extract information from stdin.

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

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

=back

=head1 AUTHOR

Copyright 2003-2005, Phil Harvey

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

=head1 SEE ALSO

L<Image::ExifTool|Image::ExifTool>,
L<ExifTool Tag Names|Image::ExifTool::TagNames>,
L<Shortcut Tags|Image::ExifTool::Shortcuts>

=cut

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