You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
7214 lines
316 KiB
7214 lines
316 KiB
#!/usr/bin/perl -w |
|
#------------------------------------------------------------------------------ |
|
# File: windows_exiftool |
|
# |
|
# Description: exiftool version for Windows EXE bundle |
|
# |
|
# Revisions: Nov. 12/03 - P. Harvey Created |
|
# (See html/history.html for revision history) |
|
#------------------------------------------------------------------------------ |
|
use strict; |
|
use warnings; |
|
require 5.004; |
|
|
|
my $version = '12.42'; |
|
|
|
# add our 'lib' directory to the include list BEFORE 'use Image::ExifTool' |
|
my $exePath; |
|
BEGIN { |
|
# (undocumented -xpath option added in 11.91, must come before other options) |
|
$exePath = @ARGV && lc($ARGV[0]) eq '-xpath' && shift() ? $^X : $0; |
|
# get exe directory |
|
$Image::ExifTool::exeDir = ($exePath =~ /(.*)[\\\/]/) ? $1 : '.'; |
|
# (no link following for Windows exe version) |
|
# add lib directory at start of include path |
|
unshift @INC, ($0 =~ /(.*)[\\\/]/) ? "$1/lib" : './lib'; |
|
# load or disable config file if specified |
|
if (@ARGV and lc($ARGV[0]) eq '-config') { |
|
shift; |
|
$Image::ExifTool::configFile = shift; |
|
} |
|
} |
|
use Image::ExifTool qw{:Public}; |
|
|
|
# function prototypes |
|
sub SigInt(); |
|
sub SigCont(); |
|
sub Cleanup(); |
|
sub GetImageInfo($$); |
|
sub SetImageInfo($$$); |
|
sub DoHardLink($$$$$); |
|
sub CleanXML($); |
|
sub EncodeXML($); |
|
sub FormatXML($$$); |
|
sub EscapeJSON($;$); |
|
sub FormatJSON($$$); |
|
sub PrintCSV(); |
|
sub AddGroups($$$$); |
|
sub ConvertBinary($); |
|
sub IsEqual($$); |
|
sub Infile($;$); |
|
sub AddSetTagsFile($;$); |
|
sub DoSetFromFile($$$); |
|
sub CleanFilename($); |
|
sub SetWindowTitle($); |
|
sub ProcessFiles($;$); |
|
sub ScanDir($$;$); |
|
sub FindFileWindows($$); |
|
sub FileNotFound($); |
|
sub PreserveTime(); |
|
sub AbsPath($); |
|
sub MyConvertFileName($$); |
|
sub SuggestedExtension($$$); |
|
sub LoadPrintFormat($); |
|
sub FilenameSPrintf($;$@); |
|
sub NextUnusedFilename($;$); |
|
sub CreateDirectory($); |
|
sub OpenOutputFile($;@); |
|
sub AcceptFile($); |
|
sub SlurpFile($$); |
|
sub FilterArgfileLine($); |
|
sub ReadStayOpen($); |
|
sub PrintTagList($@); |
|
sub PrintErrors($$$); |
|
sub Help(); |
|
|
|
$SIG{INT} = 'SigInt'; # do cleanup on Ctrl-C |
|
$SIG{CONT} = 'SigCont'; # (allows break-out of delays) |
|
END { |
|
Cleanup(); |
|
} |
|
|
|
# declare all static file-scope variables |
|
my @commonArgs; # arguments common to all commands |
|
my @condition; # conditional processing of files |
|
my @csvFiles; # list of files when reading with CSV option (in ExifTool Charset) |
|
my @csvTags; # order of tags for first file with CSV option (lower case) |
|
my @delFiles; # list of files to delete |
|
my @dynamicFiles; # list of -tagsFromFile files with dynamic names and -TAG<=FMT pairs |
|
my @efile; # files for writing list of error/fail/same file names |
|
my @exclude; # list of excluded tags |
|
my (@echo3, @echo4);# stdout and stderr echo after processing is complete |
|
my @files; # list of files and directories to scan |
|
my @moreArgs; # more arguments to process after -stay_open -@ |
|
my @newValues; # list of new tag values to set |
|
my @requestTags; # tags to request (for -p or -if option arguments) |
|
my @srcFmt; # source file name format strings |
|
my @tags; # list of tags to extract |
|
my %appended; # list of files appended to |
|
my %countLink; # count hard and symbolic links made |
|
my %created; # list of files we created |
|
my %csvTags; # lookup for all found tags with CSV option (lower case keys) |
|
my %database; # lookup for database information based on file name (in ExifTool Charset) |
|
my %filterExt; # lookup for filtered extensions |
|
my %ignore; # directory names to ignore |
|
my $ignoreHidden; # flag to ignore hidden files |
|
my %outComma; # flag that output text file needs a comma |
|
my %outTrailer; # trailer for output text file |
|
my %preserveTime; # preserved timestamps for files |
|
my %printFmt; # the contents of the print format file |
|
my %setTags; # hash of list references for tags to set from files |
|
my %setTagsList; # list of other tag lists for multiple -tagsFromFile from the same file |
|
my %usedFileName; # lookup for file names we already used in TestName feature |
|
my %utf8FileName; # lookup for file names that are UTF-8 encoded |
|
my %warnedOnce; # lookup for once-only warnings |
|
my %wext; # -W extensions to write |
|
my $allGroup; # show group name for all tags |
|
my $altEnc; # alternate character encoding if not UTF-8 |
|
my $argFormat; # use exiftool argument-format output |
|
my $binaryOutput; # flag for binary output (undef or 1, or 0 for binary XML/PHP) |
|
my $binaryStdout; # flag set if we output binary to stdout |
|
my $binSep; # separator used for list items in binary output |
|
my $binTerm; # terminator used for binary output |
|
my $comma; # flag set if we need a comma in JSON output |
|
my $count; # count of files scanned when reading or deleting originals |
|
my $countBad; # count of files with errors |
|
my $countBadCr; # count files not created due to errors |
|
my $countBadWr; # count write errors |
|
my $countCopyWr; # count of files copied without being changed |
|
my $countDir; # count of directories scanned |
|
my $countFailed; # count files that failed condition |
|
my $countGoodCr; # count files created OK |
|
my $countGoodWr; # count files written OK |
|
my $countNewDir; # count of directories created |
|
my $countSameWr; # count files written OK but not changed |
|
my $critical; # flag for critical operations (disable CTRL-C) |
|
my $csv; # flag for CSV option (set to "CSV", or maybe "JSON" when writing) |
|
my $csvAdd; # flag to add CSV information to existing lists |
|
my $csvDelim; # delimiter for CSV files |
|
my $csvSaveCount; # save counter for last CSV file loaded |
|
my $deleteOrig; # 0=restore original files, 1=delete originals, 2=delete w/o asking |
|
my $disableOutput; # flag to disable normal output |
|
my $doSetFileName; # flag set if FileName may be written |
|
my $doUnzip; # flag to extract info from .gz and .bz2 files |
|
my ($end,$endDir,%endDir); # flags to end processing |
|
my $escapeC; # C-style escape |
|
my $escapeHTML; # flag to escape printed values for html |
|
my $evalWarning; # warning from eval |
|
my $executeID; # -execute ID number |
|
my $failCondition; # flag to fail -if condition |
|
my $fastCondition; # flag for fast -if condition |
|
my $fileHeader; # header to print to output file (or console, once) |
|
my $fileTrailer; # trailer for output file |
|
my $filtered; # flag indicating file was filtered by name |
|
my $filterFlag; # file filter flag (0x01=deny extensions, 0x02=allow extensions, 0x04=add ext) |
|
my $fixLen; # flag to fix description lengths when writing alternate languages |
|
my $forcePrint; # string to use for missing tag values (undef to not print them) |
|
my $helped; # flag to avoid printing help if no tags specified |
|
my $html; # flag for html-formatted output (2=html dump) |
|
my $interrupted; # flag set if CTRL-C is pressed during a critical process |
|
my $isBinary; # true if value is a SCALAR ref |
|
my $isWriting; # flag set if we are writing tags |
|
my $joinLists; # flag set to join list values into a single string |
|
my $json; # flag for JSON/PHP output format (1=JSON, 2=PHP) |
|
my $langOpt; # language option |
|
my $listDir; # treat a directory as a regular file |
|
my $listItem; # item number for extracting single item from a list |
|
my $listSep; # list item separator (', ' by default) |
|
my $mt; # main ExifTool object |
|
my $multiFile; # non-zero if we are scanning multiple files |
|
my $noBinary; # flag set to ignore binary tags |
|
my $outFormat; # -1=Canon format, 0=same-line, 1=tag names, 2=values only |
|
my $outOpt; # output file or directory name |
|
my $overwriteOrig; # flag to overwrite original file (1=overwrite, 2=in place) |
|
my $pause; # pause before returning |
|
my $preserveTime; # flag to preserve times of updated files (2=preserve FileCreateDate only) |
|
my $progress; # flag to calculate total files to process (0=calculate but don't display) |
|
my $progressCount; # count of files processed |
|
my $progressMax; # total number of files to process |
|
my $progStr; # progress message string |
|
my $quiet; # flag to disable printing of informational messages / warnings |
|
my $rafStdin; # File::RandomAccess for stdin (if necessary to rewind) |
|
my $recurse; # recurse into subdirectories (2=also hidden directories) |
|
my $rtnVal; # command return value (0=success) |
|
my $rtnValPrev; # previous command return value (0=success) |
|
my $saveCount; # count the number of times we will/did call SaveNewValues() |
|
my $scanWritable; # flag to process only writable file types |
|
my $sectHeader; # current section header for -p option |
|
my $sectTrailer; # section trailer for -p option |
|
my $seqFileBase; # sequential file number at start of directory |
|
my $seqFileNum; # sequential file number used for %C |
|
my $setCharset; # character set setting ('default' if not set and -csv -b used) |
|
my $showGroup; # number of group to show (may be zero or '') |
|
my $showTagID; # non-zero to show tag ID's |
|
my $stayOpenBuff='';# buffer for -stay_open file |
|
my $stayOpenFile; # name of the current -stay_open argfile |
|
my $structOpt; # output structured XMP information (JSON and XML output only) |
|
my $tabFormat; # non-zero for tab output format |
|
my $tagOut; # flag for separate text output file for each tag |
|
my $textOut; # extension for text output file (or undef for no output) |
|
my $textOverwrite; # flag to overwrite existing text output file (2=append, 3=over+append) |
|
my $tmpFile; # temporary file to delete on exit |
|
my $tmpText; # temporary text file |
|
my $validFile; # flag indicating we processed a valid file |
|
my $verbose; # verbose setting |
|
my $vout; # verbose output file reference (\*STDOUT or \*STDERR) |
|
my $windowTitle; # title for console window |
|
my $xml; # flag for XML-formatted output |
|
|
|
# flag to keep the input -@ argfile open: |
|
# 0 = normal behaviour |
|
# 1 = received "-stay_open true" and waiting for argfile to keep open |
|
# 2 = currently reading from STAYOPEN argfile |
|
# 3 = waiting for -@ to switch to a new STAYOPEN argfile |
|
my $stayOpen = 0; |
|
|
|
my $rtnValApp = 0; # app return value (0=success) |
|
my $curTitle = ''; # current window title |
|
|
|
# lookup for O/S names which may use a backslash as a directory separator |
|
# (ref File::Spec of PathTools-3.2701) |
|
my %hasBackslash = ( MSWin32 => 1, os2 => 1, dos => 1, NetWare => 1, symbian => 1, cygwin => 1 ); |
|
|
|
# lookup for O/S names which use CR/LF newlines |
|
my $isCRLF = { MSWin32 => 1, os2 => 1, dos => 1 }->{$^O}; |
|
|
|
# lookup for JSON characters that we escape specially |
|
my %jsonChar = ( '"'=>'"', '\\'=>'\\', "\t"=>'t', "\n"=>'n', "\r"=>'r' ); |
|
|
|
# lookup for C-style escape sequences |
|
my %escC = ( "\n" => '\n', "\r" => '\r', "\t" => '\t', '\\' => '\\\\'); |
|
my %unescC = ( a => "\a", b => "\b", f => "\f", n => "\n", r => "\r", |
|
t => "\t", 0 => "\0", '\\' => '\\' ); |
|
|
|
# options requiring additional arguments |
|
# (used only to skip over these arguments when reading -stay_open ARGFILE) |
|
# (arg is converted to lower case then tested again unless an entry was found with the same case) |
|
my %optArgs = ( |
|
'-tagsfromfile' => 1, '-addtagsfromfile' => 1, '-alltagsfromfile' => 1, |
|
'-@' => 1, |
|
'-api' => 1, |
|
'-c' => 1, '-coordformat' => 1, |
|
'-charset' => 0, # (optional arg; OK because arg cannot begin with "-") |
|
'-config' => 1, |
|
'-csvdelim' => 1, |
|
'-d' => 1, '-dateformat' => 1, |
|
'-D' => 0, # necessary to avoid matching lower-case equivalent |
|
'-echo' => 1, '-echo1' => 1, '-echo2' => 1, '-echo3' => 1, '-echo4' => 1, |
|
'-efile' => 1, '-efile1' => 1, '-efile2' => 1, '-efile3' => 1, '-efile4' => 1, |
|
'-efile!' => 1, '-efile1!' => 1, '-efile2!' => 1, '-efile3!' => 1, '-efile4!' => 1, |
|
'-ext' => 1, '--ext' => 1, '-ext+' => 1, '--ext+' => 1, |
|
'-extension' => 1, '--extension' => 1, '-extension+' => 1, '--extension+' => 1, |
|
'-fileorder' => 1, '-fileorder0' => 1, '-fileorder1' => 1, '-fileorder2' => 1, |
|
'-fileorder3' => 1, '-fileorder4' => 1, '-fileorder5' => 1, |
|
'-geotag' => 1, |
|
'-globaltimeshift' => 1, |
|
'-i' => 1, '-ignore' => 1, |
|
'-if' => 1, '-if0' => 1, '-if1' => 1, '-if2' => 1, '-if3' => 1, '-if4' => 1, '-if5' => 1, |
|
'-lang' => 0, # (optional arg; cannot begin with "-") |
|
'-listitem' => 1, |
|
'-o' => 1, '-out' => 1, |
|
'-p' => 1, '-printformat' => 1, |
|
'-P' => 0, |
|
'-password' => 1, |
|
'-require' => 1, |
|
'-sep' => 1, '-separator' => 1, |
|
'-srcfile' => 1, |
|
'-stay_open' => 1, |
|
'-use' => 1, |
|
'-userparam' => 1, |
|
'-w' => 1, '-w!' => 1, '-w+' => 1, '-w+!' => 1, '-w!+' => 1, |
|
'-textout' => 1, '-textout!' => 1, '-textout+' => 1, '-textout+!' => 1, '-textout!+' => 1, |
|
'-tagout' => 1, '-tagout!' => 1, '-tagout+' => 1, '-tagout+!' => 1, '-tagout!+' => 1, |
|
'-wext' => 1, |
|
'-wm' => 1, '-writemode' => 1, |
|
'-x' => 1, '-exclude' => 1, |
|
'-X' => 0, |
|
); |
|
|
|
# recommended packages and alternatives |
|
my @recommends = qw( |
|
Archive::Zip |
|
Compress::Zlib |
|
Digest::MD5 |
|
Digest::SHA |
|
IO::Compress::Bzip2 |
|
POSIX::strptime |
|
Time::Local |
|
Unicode::LineBreak |
|
IO::Compress::RawDeflate |
|
IO::Uncompress::RawInflate |
|
Win32::API |
|
Win32::FindFile |
|
Win32API::File |
|
); |
|
my %altRecommends = ( |
|
'POSIX::strptime' => 'Time::Piece', # (can use Time::Piece instead of POSIX::strptime) |
|
); |
|
|
|
my %unescapeChar = ( 't'=>"\t", 'n'=>"\n", 'r'=>"\r" ); |
|
|
|
# special subroutines used in -if condition |
|
sub Image::ExifTool::EndDir() { return $endDir = 1 } |
|
sub Image::ExifTool::End() { return $end = 1 } |
|
|
|
# exit routine |
|
sub Exit { |
|
if ($pause) { |
|
if (eval { require Term::ReadKey }) { |
|
print STDERR "-- press any key --"; |
|
Term::ReadKey::ReadMode('cbreak'); |
|
Term::ReadKey::ReadKey(0); |
|
Term::ReadKey::ReadMode(0); |
|
print STDERR "\b \b" x 20; |
|
} else { |
|
print STDERR "-- press ENTER --\n"; |
|
<STDIN>; |
|
} |
|
} |
|
exit shift; |
|
} |
|
# my warning and error routines (NEVER say "die"!) |
|
sub Warn { |
|
if ($quiet < 2 or $_[0] =~ /^Error/) { |
|
my $oldWarn = $SIG{'__WARN__'}; |
|
delete $SIG{'__WARN__'}; |
|
warn(@_); |
|
$SIG{'__WARN__'} = $oldWarn if defined $oldWarn; |
|
} |
|
} |
|
sub Error { Warn @_; $rtnVal = 1; } |
|
sub WarnOnce($) { |
|
Warn(@_) and $warnedOnce{$_[0]} = 1 unless $warnedOnce{$_[0]}; |
|
} |
|
|
|
# define signal handlers and cleanup routine |
|
sub SigInt() { |
|
$critical and $interrupted = 1, return; |
|
Cleanup(); |
|
exit 1; |
|
} |
|
sub SigCont() { } |
|
sub Cleanup() { |
|
$mt->Unlink($tmpFile) if defined $tmpFile; |
|
$mt->Unlink($tmpText) if defined $tmpText; |
|
undef $tmpFile; |
|
undef $tmpText; |
|
PreserveTime() if %preserveTime; |
|
SetWindowTitle(''); |
|
} |
|
|
|
#------------------------------------------------------------------------------ |
|
# main script |
|
# |
|
|
|
# add arguments embedded in filename (Windows .exe version only) |
|
if ($exePath =~ /\(([^\\\/]+)\)(.exe|.pl)?$/i) { |
|
my $argstr = $1; |
|
# divide into separate quoted or whitespace-delineated arguments |
|
my (@args, $arg, $quote); |
|
while ($argstr =~ /(\s*)(\S+)/g) { |
|
$arg = $quote ? "$arg$1" : ''; # include quoted white space in arg |
|
my $a = $2; |
|
for (;;) { |
|
my $q = $quote || q{['"]}; # look for current (or any) quote |
|
$a =~ /(.*?)($q)/gs or last; # get string up to quote |
|
$quote = $quote ? undef : $2; # define next quote char for search |
|
$arg .= $1; # add to this argument |
|
$a = substr($a, pos($a)); # done parsing up to current position |
|
} |
|
$arg .= $a; # add unquoted part of string |
|
push @args, $arg unless $quote; # save in argument list |
|
} |
|
unshift @ARGV, @args; # add before other command-line arguments |
|
} |
|
|
|
# isolate arguments common to all commands |
|
if (grep /^-common_args$/i, @ARGV) { |
|
my (@newArgs, $common); |
|
foreach (@ARGV) { |
|
if (/^-common_args$/i) { |
|
$common = 1; |
|
} elsif ($common) { |
|
push @commonArgs, $_; |
|
} else { |
|
push @newArgs, $_; |
|
} |
|
} |
|
@ARGV = @newArgs if $common; |
|
} |
|
|
|
#.............................................................................. |
|
# loop over sets of command-line arguments separated by "-execute" |
|
Command: for (;;) { |
|
|
|
if (@echo3) { |
|
my $str = join("\n", @echo3) . "\n"; |
|
$str =~ s/\$\{status\}/$rtnVal/ig; |
|
print STDOUT $str; |
|
} |
|
if (@echo4) { |
|
my $str = join("\n", @echo4) . "\n"; |
|
$str =~ s/\$\{status\}/$rtnVal/ig; |
|
print STDERR $str; |
|
} |
|
|
|
$rafStdin->Close() if $rafStdin; |
|
undef $rafStdin; |
|
|
|
# save our previous return codes |
|
$rtnValPrev = $rtnVal; |
|
$rtnValApp = $rtnVal if $rtnVal; |
|
|
|
# exit Command loop now if we are all done processing commands |
|
last unless @ARGV or not defined $rtnVal or $stayOpen >= 2 or @commonArgs; |
|
|
|
# attempt to restore text mode for STDOUT if necessary |
|
if ($binaryStdout) { |
|
binmode(STDOUT,':crlf') if $] >= 5.006 and $isCRLF; |
|
$binaryStdout = 0; |
|
} |
|
|
|
# flush console and print "{ready}" message if -stay_open is in effect |
|
if ($stayOpen >= 2) { |
|
if ($quiet and not defined $executeID) { |
|
# flush output if possible |
|
eval { require IO::Handle } and STDERR->flush(), STDOUT->flush(); |
|
} else { |
|
eval { require IO::Handle } and STDERR->flush(); |
|
my $id = defined $executeID ? $executeID : ''; |
|
my $save = $|; |
|
$| = 1; # turn on output autoflush for stdout |
|
print "{ready$id}\n"; |
|
$| = $save; # restore original autoflush setting |
|
} |
|
} |
|
|
|
# initialize necessary static file-scope variables |
|
# (not done: @commonArgs, @moreArgs, $critical, $binaryStdout, $helped, |
|
# $interrupted, $mt, $pause, $rtnValApp, $rtnValPrev, $stayOpen, $stayOpenBuff, $stayOpenFile) |
|
undef @condition; |
|
undef @csvFiles; |
|
undef @csvTags; |
|
undef @delFiles; |
|
undef @dynamicFiles; |
|
undef @echo3; |
|
undef @echo4; |
|
undef @efile; |
|
undef @exclude; |
|
undef @files; |
|
undef @newValues; |
|
undef @srcFmt; |
|
undef @tags; |
|
undef %appended; |
|
undef %countLink; |
|
undef %created; |
|
undef %csvTags; |
|
undef %database; |
|
undef %endDir; |
|
undef %filterExt; |
|
undef %ignore; |
|
undef %outComma; |
|
undef %outTrailer; |
|
undef %printFmt; |
|
undef %preserveTime; |
|
undef %setTags; |
|
undef %setTagsList; |
|
undef %usedFileName; |
|
undef %utf8FileName; |
|
undef %warnedOnce; |
|
undef %wext; |
|
undef $allGroup; |
|
undef $altEnc; |
|
undef $argFormat; |
|
undef $binaryOutput; |
|
undef $binSep; |
|
undef $binTerm; |
|
undef $comma; |
|
undef $csv; |
|
undef $csvAdd; |
|
undef $deleteOrig; |
|
undef $disableOutput; |
|
undef $doSetFileName; |
|
undef $doUnzip; |
|
undef $end; |
|
undef $endDir; |
|
undef $escapeHTML; |
|
undef $escapeC; |
|
undef $evalWarning; |
|
undef $executeID; |
|
undef $failCondition; |
|
undef $fastCondition; |
|
undef $fileHeader; |
|
undef $filtered; |
|
undef $fixLen; |
|
undef $forcePrint; |
|
undef $ignoreHidden; |
|
undef $joinLists; |
|
undef $langOpt; |
|
undef $listItem; |
|
undef $multiFile; |
|
undef $noBinary; |
|
undef $outOpt; |
|
undef $preserveTime; |
|
undef $progress; |
|
undef $progressCount; |
|
undef $progressMax; |
|
undef $recurse; |
|
undef $scanWritable; |
|
undef $sectHeader; |
|
undef $setCharset; |
|
undef $showGroup; |
|
undef $showTagID; |
|
undef $structOpt; |
|
undef $tagOut; |
|
undef $textOut; |
|
undef $textOverwrite; |
|
undef $tmpFile; |
|
undef $tmpText; |
|
undef $validFile; |
|
undef $verbose; |
|
undef $windowTitle; |
|
|
|
$count = 0; |
|
$countBad = 0; |
|
$countBadCr = 0; |
|
$countBadWr = 0; |
|
$countCopyWr = 0; |
|
$countDir = 0; |
|
$countFailed = 0; |
|
$countGoodCr = 0; |
|
$countGoodWr = 0; |
|
$countNewDir = 0; |
|
$countSameWr = 0; |
|
$csvDelim = ','; |
|
$csvSaveCount = 0; |
|
$fileTrailer = ''; |
|
$filterFlag = 0; |
|
$html = 0; |
|
$isWriting = 0; |
|
$json = 0; |
|
$listSep = ', '; |
|
$outFormat = 0; |
|
$overwriteOrig = 0; |
|
$progStr = ''; |
|
$quiet = 0; |
|
$rtnVal = 0; |
|
$saveCount = 0; |
|
$sectTrailer = ''; |
|
$seqFileBase = 0; |
|
$seqFileNum = 0; |
|
$tabFormat = 0; |
|
$vout = \*STDOUT; |
|
$xml = 0; |
|
|
|
# define local variables used only in this command loop |
|
my @fileOrder; # tags to use for ordering of input files |
|
my $fileOrderFast; # -fast level for -fileOrder option |
|
my $addGeotime; # automatically added geotime argument |
|
my $doGlob; # flag set to do filename wildcard expansion |
|
my $endOfOpts; # flag set if "--" option encountered |
|
my $escapeXML; # flag to escape printed values for xml |
|
my $setTagsFile; # filename for last TagsFromFile option |
|
my $sortOpt; # sort option is used |
|
my $srcStdin; # one of the source files is STDIN |
|
my $useMWG; # flag set if we are using any MWG tag |
|
|
|
my ($argsLeft, @nextPass, $badCmd); |
|
my $pass = 0; |
|
|
|
# for Windows, use globbing for wildcard expansion if available - MK/20061010 |
|
if ($^O eq 'MSWin32' and eval { require File::Glob }) { |
|
# override the core glob forcing case insensitivity |
|
import File::Glob qw(:globally :nocase); |
|
$doGlob = 1; |
|
} |
|
|
|
$mt = new Image::ExifTool; # create ExifTool object |
|
|
|
# don't extract duplicates by default unless set by UserDefined::Options |
|
$mt->Options(Duplicates => 0) unless %Image::ExifTool::UserDefined::Options |
|
and defined $Image::ExifTool::UserDefined::Options{Duplicates}; |
|
|
|
# default is to join lists if the List option was set to zero in the config file |
|
$joinLists = 1 if defined $mt->Options('List') and not $mt->Options('List'); |
|
|
|
# preserve FileCreateDate if possible |
|
if (not $preserveTime and $^O eq 'MSWin32') { |
|
$preserveTime = 2 if eval { require Win32::API } and eval { require Win32API::File }; |
|
} |
|
|
|
# parse command-line options in 2 passes... |
|
# pass 1: set all of our ExifTool options |
|
# pass 2: print all of our help and informational output (-list, -ver, etc) |
|
for (;;) { |
|
|
|
# execute the command now if no more arguments or -execute is used |
|
if (not @ARGV or ($ARGV[0] =~ /^(-|\xe2\x88\x92)execute(\d+)?$/i and not $endOfOpts)) { |
|
if (@ARGV) { |
|
$executeID = $2; # save -execute number for "{ready}" response |
|
$helped = 1; # don't show help if we used -execute |
|
$badCmd and shift, $rtnVal=1, next Command; |
|
} elsif ($stayOpen >= 2) { |
|
ReadStayOpen(\@ARGV); # read more arguments from -stay_open file |
|
next; |
|
} elsif ($badCmd) { |
|
undef @commonArgs; # all done. Flush common arguments |
|
$rtnVal = 1; |
|
next Command; |
|
} |
|
if ($pass == 0) { |
|
# insert common arguments now if not done already |
|
if (@commonArgs and not defined $argsLeft) { |
|
# count the number of arguments remaining for subsequent commands |
|
$argsLeft = scalar(@ARGV) + scalar(@moreArgs); |
|
unshift @ARGV, @commonArgs; |
|
# all done with commonArgs if this is the end of the command |
|
undef @commonArgs unless $argsLeft; |
|
next; |
|
} |
|
# check if we have more arguments now than we did before we processed |
|
# the common arguments. If so, then we have an infinite processing loop |
|
if (defined $argsLeft and $argsLeft < scalar(@ARGV) + scalar(@moreArgs)) { |
|
Warn "Ignoring -common_args from $ARGV[0] onwards to avoid infinite recursion\n"; |
|
while ($argsLeft < scalar(@ARGV) + scalar(@moreArgs)) { |
|
@ARGV and shift(@ARGV), next; |
|
shift @moreArgs; |
|
} |
|
} |
|
# require MWG module if used in any argument |
|
# (note: doesn't cover the -p option because these tags will be parsed on the 2nd pass) |
|
$useMWG = 1 if not $useMWG and grep /^mwg:/i, @tags, @requestTags; |
|
if ($useMWG) { |
|
require Image::ExifTool::MWG; |
|
Image::ExifTool::MWG::Load(); |
|
} |
|
# update necessary variables for 2nd pass |
|
if (defined $forcePrint) { |
|
unless (defined $mt->Options('MissingTagValue')) { |
|
$mt->Options(MissingTagValue => '-'); |
|
} |
|
$forcePrint = $mt->Options('MissingTagValue'); |
|
} |
|
} |
|
if (@nextPass) { |
|
# process arguments which were deferred to the next pass |
|
unshift @ARGV, @nextPass; |
|
undef @nextPass; |
|
undef $endOfOpts; |
|
++$pass; |
|
next; |
|
} |
|
@ARGV and shift; # remove -execute from argument list |
|
last; # process the command now |
|
} |
|
$_ = shift; |
|
next if $badCmd; # flush remaining arguments if aborting this command |
|
|
|
# allow funny dashes (nroff dash bug for cut-n-paste from pod) |
|
if (not $endOfOpts and s/^(-|\xe2\x88\x92)//) { |
|
s/^\xe2\x88\x92/-/; # translate double-dash too |
|
if ($_ eq '-') { |
|
$pass or push @nextPass, '--'; |
|
$endOfOpts = 1; |
|
next; |
|
} |
|
my $a = lc $_; |
|
if (/^list([wfrdx]|wf|g(\d*))?$/i) { |
|
$pass or push @nextPass, "-$_"; |
|
my $type = lc($1 || ''); |
|
if (not $type or $type eq 'w' or $type eq 'x') { |
|
my $group; |
|
if ($ARGV[0] and $ARGV[0] =~ /^(-|\xe2\x88\x92)(.+):(all|\*)$/i) { |
|
if ($pass == 0) { |
|
$useMWG = 1 if lc($2) eq 'mwg'; |
|
push @nextPass, shift; |
|
next; |
|
} |
|
$group = $2; |
|
shift; |
|
$group =~ /IFD/i and Warn("Can't list tags for specific IFD\n"), next; |
|
$group =~ /^(all|\*)$/ and undef $group; |
|
} else { |
|
$pass or next; |
|
} |
|
$helped = 1; |
|
if ($type eq 'x') { |
|
require Image::ExifTool::TagInfoXML; |
|
my %opts; |
|
$opts{Flags} = 1 if defined $forcePrint; |
|
$opts{NoDesc} = 1 if $outFormat > 0; |
|
$opts{Lang} = $langOpt; |
|
Image::ExifTool::TagInfoXML::Write(undef, $group, %opts); |
|
next; |
|
} |
|
my $wr = ($type eq 'w'); |
|
my $msg = ($wr ? 'Writable' : 'Available') . ($group ? " $group" : '') . ' tags'; |
|
PrintTagList($msg, $wr ? GetWritableTags($group) : GetAllTags($group)); |
|
# also print shortcuts if listing all tags |
|
next if $group or $wr; |
|
my @tagList = GetShortcuts(); |
|
PrintTagList('Command-line shortcuts', @tagList) if @tagList; |
|
next; |
|
} |
|
$pass or next; |
|
$helped = 1; |
|
if ($type eq 'wf') { |
|
my @wf; |
|
CanWrite($_) and push @wf, $_ foreach GetFileType(); |
|
PrintTagList('Writable file extensions', @wf); |
|
} elsif ($type eq 'f') { |
|
PrintTagList('Supported file extensions', GetFileType()); |
|
} elsif ($type eq 'r') { |
|
PrintTagList('Recognized file extensions', GetFileType(undef, 0)); |
|
} elsif ($type eq 'd') { |
|
PrintTagList('Deletable groups', GetDeleteGroups()); |
|
} else { # 'g(\d*)' |
|
# list all groups in specified family |
|
my $family = $2 || 0; |
|
PrintTagList("Groups in family $family", $mt->GetAllGroups($family)); |
|
} |
|
next; |
|
} |
|
if ($a eq 'ver') { |
|
$pass or push(@nextPass,'-ver'), next; |
|
my $libVer = $Image::ExifTool::VERSION; |
|
my $str = $libVer eq $version ? '' : " [Warning: Library version is $libVer]"; |
|
if ($verbose) { |
|
print "ExifTool version $version$str$Image::ExifTool::RELEASE\n"; |
|
printf "Perl version %s%s\n", $], (defined ${^UNICODE} ? " (-C${^UNICODE})" : ''); |
|
print "Platform: $^O\n"; |
|
print "Optional libraries:\n"; |
|
foreach (@recommends) { |
|
next if /^Win32/ and $^O ne 'MSWin32'; |
|
my $ver = eval "require $_ and \$${_}::VERSION"; |
|
my $alt = $altRecommends{$_}; |
|
# check for alternative if primary not available |
|
$ver = eval "require $alt and \$${alt}::VERSION" and $_ = $alt if not $ver and $alt; |
|
printf " %-28s %s\n", $_, $ver || '(not installed)'; |
|
} |
|
if ($verbose > 1) { |
|
print "Include directories:\n"; |
|
print " $_\n" foreach @INC; |
|
} |
|
} else { |
|
print "$version$str$Image::ExifTool::RELEASE\n"; |
|
} |
|
$helped = 1; |
|
next; |
|
} |
|
if (/^(all|add)?tagsfromfile(=.*)?$/i) { |
|
$setTagsFile = $2 ? substr($2,1) : (@ARGV ? shift : ''); |
|
if ($setTagsFile eq '') { |
|
Error("File must be specified for -tagsFromFile option\n"); |
|
$badCmd = 1; |
|
next; |
|
} |
|
# create necessary lists, etc for this new -tagsFromFile file |
|
AddSetTagsFile($setTagsFile, { Replace => ($1 and lc($1) eq 'add') ? 0 : 1 } ); |
|
next; |
|
} |
|
if ($a eq '@') { |
|
my $argFile = shift or Error("Expecting filename for -\@ option\n"), $badCmd=1, next; |
|
# switch to new ARGFILE if using chained -stay_open options |
|
if ($stayOpen == 1) { |
|
# defer remaining arguments until we close this argfile |
|
@moreArgs = @ARGV; |
|
undef @ARGV; |
|
} elsif ($stayOpen == 3) { |
|
if ($stayOpenFile and $stayOpenFile ne '-' and $argFile eq $stayOpenFile) { |
|
# don't allow user to switch to the same -stay_open argfile |
|
# because it will result in endless recursion |
|
$stayOpen = 2; |
|
Warn "Ignoring request to switch to the same -stay_open ARGFILE ($argFile)\n"; |
|
next; |
|
} |
|
close STAYOPEN; |
|
$stayOpen = 1; # switch to this -stay_open file |
|
} |
|
my $fp = ($stayOpen == 1 ? \*STAYOPEN : \*ARGFILE); |
|
unless ($mt->Open($fp, $argFile)) { |
|
unless ($argFile !~ /^\// and $mt->Open($fp, "$Image::ExifTool::exeDir/$argFile")) { |
|
Error "Error opening arg file $argFile\n"; |
|
$badCmd = 1; |
|
next |
|
} |
|
} |
|
if ($stayOpen == 1) { |
|
$stayOpenFile = $argFile; # remember the name of the file we have open |
|
$stayOpenBuff = ''; # initialize buffer for reading this file |
|
$stayOpen = 2; |
|
$helped = 1; |
|
ReadStayOpen(\@ARGV); |
|
next; |
|
} |
|
my (@newArgs, $didBOM); |
|
foreach (<ARGFILE>) { |
|
# filter Byte Order Mark if it exists from start of UTF-8 text file |
|
unless ($didBOM) { |
|
s/^\xef\xbb\xbf//; |
|
$didBOM = 1; |
|
} |
|
$_ = FilterArgfileLine($_); |
|
push @newArgs, $_ if defined $_; |
|
} |
|
close ARGFILE; |
|
unshift @ARGV, @newArgs; |
|
next; |
|
} |
|
/^(-?)(a|duplicates)$/i and $mt->Options(Duplicates => ($1 ? 0 : 1)), next; |
|
if ($a eq 'api') { |
|
my $opt = shift; |
|
defined $opt or Error("Expected OPT[=VAL] argument for -api option\n"), $badCmd=1, next; |
|
my $val = ($opt =~ s/=(.*)//s) ? $1 : 1; |
|
# empty string means an undefined value unless ^= is used |
|
$val = undef unless $opt =~ s/\^$// or length $val; |
|
$mt->Options($opt => $val); |
|
next; |
|
} |
|
/^arg(s|format)$/i and $argFormat = 1, next; |
|
if (/^(-?)b(inary)?$/i) { |
|
($binaryOutput, $noBinary) = $1 ? (undef, 1) : (1, undef); |
|
$mt->Options(Binary => $binaryOutput, NoPDFList => $binaryOutput); |
|
next; |
|
} |
|
if (/^c(oordFormat)?$/i) { |
|
my $fmt = shift; |
|
$fmt or Error("Expecting coordinate format for -c option\n"), $badCmd=1, next; |
|
$mt->Options('CoordFormat', $fmt); |
|
next; |
|
} |
|
if ($a eq 'charset') { |
|
my $charset = (@ARGV and $ARGV[0] !~ /^(-|\xe2\x88\x92)/) ? shift : undef; |
|
if (not $charset) { |
|
$pass or push(@nextPass, '-charset'), next; |
|
my %charsets; |
|
$charsets{$_} = 1 foreach values %Image::ExifTool::charsetName; |
|
PrintTagList('Available character sets', sort keys %charsets); |
|
$helped = 1; |
|
} elsif ($charset !~ s/^(\w+)=// or lc($1) eq 'exiftool') { |
|
{ |
|
local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] }; |
|
undef $evalWarning; |
|
$mt->Options(Charset => $charset); |
|
} |
|
if ($evalWarning) { |
|
warn $evalWarning; |
|
} else { |
|
$setCharset = $mt->Options('Charset'); |
|
} |
|
} else { |
|
# set internal encoding of specified metadata type |
|
my $type = { id3 => 'ID3', iptc => 'IPTC', exif => 'EXIF', filename => 'FileName', |
|
photoshop => 'Photoshop', quicktime => 'QuickTime', riff=>'RIFF' }->{lc $1}; |
|
$type or Warn("Unknown type for -charset option: $1\n"), next; |
|
$mt->Options("Charset$type" => $charset); |
|
} |
|
next; |
|
} |
|
/^config$/i and Warn("Ignored -config option (not first on command line)\n"), shift, next; |
|
if (/^csv(\+?=.*)?$/i) { |
|
my $csvFile = $1; |
|
# must process on 2nd pass so -f and -charset options are available |
|
unless ($pass) { |
|
push @nextPass, "-$_"; |
|
if ($csvFile) { |
|
push @newValues, { SaveCount => ++$saveCount }; # marker to save new values now |
|
$csvSaveCount = $saveCount; |
|
} |
|
next; |
|
} |
|
if ($csvFile) { |
|
$csvFile =~ s/^(\+?=)//; |
|
$csvAdd = 2 if $1 eq '+='; |
|
$vout = \*STDERR if $srcStdin; |
|
$verbose and print $vout "Reading CSV file $csvFile\n"; |
|
my $msg; |
|
if ($mt->Open(\*CSVFILE, $csvFile)) { |
|
binmode CSVFILE; |
|
require Image::ExifTool::Import; |
|
$msg = Image::ExifTool::Import::ReadCSV(\*CSVFILE, \%database, $forcePrint, $csvDelim); |
|
close(CSVFILE); |
|
} else { |
|
$msg = "Error opening CSV file '${csvFile}'"; |
|
} |
|
$msg and Warn("$msg\n"); |
|
$isWriting = 1; |
|
} |
|
$csv = 'CSV'; |
|
next; |
|
} |
|
if (/^csvdelim$/i) { |
|
$csvDelim = shift; |
|
defined $csvDelim or Error("Expecting argument for -csvDelim option\n"), $badCmd=1, next; |
|
$csvDelim =~ /"/ and Error("CSV delimiter can not contain a double quote\n"), $badCmd=1, next; |
|
my %unescape = ( 't'=>"\t", 'n'=>"\n", 'r'=>"\r", '\\' => '\\' ); |
|
$csvDelim =~ s/\\(.)/$unescape{$1}||"\\$1"/sge; |
|
$mt->Options(CSVDelim => $csvDelim); |
|
next; |
|
} |
|
if (/^d$/ or $a eq 'dateformat') { |
|
my $fmt = shift; |
|
$fmt or Error("Expecting date format for -d option\n"), $badCmd=1, next; |
|
$mt->Options('DateFormat', $fmt); |
|
next; |
|
} |
|
(/^D$/ or $a eq 'decimal') and $showTagID = 'D', next; |
|
/^delete_original(!?)$/i and $deleteOrig = ($1 ? 2 : 1), next; |
|
/^list_dir$/i and $listDir = 1, next; |
|
(/^e$/ or $a eq '-composite') and $mt->Options(Composite => 0), next; |
|
(/^-e$/ or $a eq 'composite') and $mt->Options(Composite => 1), next; |
|
(/^E$/ or $a eq 'escapehtml') and require Image::ExifTool::HTML and $escapeHTML = 1, next; |
|
($a eq 'ec' or $a eq 'escapec') and $escapeC = 1, next; |
|
($a eq 'ex' or $a eq 'escapexml') and $escapeXML = 1, next; |
|
if (/^echo(\d)?$/i) { |
|
my $n = $1 || 1; |
|
my $arg = shift; |
|
next unless defined $arg; |
|
$n > 4 and Warn("Invalid -echo number\n"), next; |
|
if ($n > 2) { |
|
$n == 3 ? push(@echo3, $arg) : push(@echo4, $arg); |
|
} else { |
|
print {$n==2 ? \*STDERR : \*STDOUT} $arg, "\n"; |
|
} |
|
$helped = 1; |
|
next; |
|
} |
|
if (/^(ee|extractembedded)(\d*)$/i) { |
|
$mt->Options(ExtractEmbedded => $2 || 1); |
|
$mt->Options(Duplicates => 1); |
|
next; |
|
} |
|
if (/^efile(\d)?(!)?$/i) { |
|
my $arg = shift; |
|
defined $arg or Error("Expecting file name for -$_ option\n"), $badCmd=1, next; |
|
$efile[0] = $arg if not $1 or $1 & 0x01; |
|
$efile[1] = $arg if $1 and $1 & 0x02; |
|
$efile[2] = $arg if $1 and $1 & 0x04; |
|
unlink $arg if $2; |
|
next; |
|
} |
|
# (-execute handled at top of loop) |
|
if (/^-?ext(ension)?(\+)?$/i) { |
|
my $ext = shift; |
|
defined $ext or Error("Expecting extension for -ext option\n"), $badCmd=1, next; |
|
my $flag = /^-/ ? 0 : ($2 ? 2 : 1); |
|
$filterFlag |= (0x01 << $flag); |
|
$ext =~ s/^\.//; # remove leading '.' if it exists |
|
$filterExt{uc($ext)} = $flag ? 1 : 0; |
|
next; |
|
} |
|
if (/^f$/ or $a eq 'forceprint') { |
|
$forcePrint = 1; |
|
next; |
|
} |
|
if (/^F([-+]?\d*)$/ or /^fixbase([-+]?\d*)$/i) { |
|
$mt->Options(FixBase => $1); |
|
next; |
|
} |
|
if (/^fast(\d*)$/i) { |
|
$mt->Options(FastScan => (length $1 ? $1 : 1)); |
|
next; |
|
} |
|
if (/^fileorder(\d*)$/i) { |
|
push @fileOrder, shift if @ARGV; |
|
my $num = $1 || 0; |
|
$fileOrderFast = $num if not defined $fileOrderFast or $fileOrderFast > $num; |
|
next; |
|
} |
|
$a eq 'globaltimeshift' and $mt->Options(GlobalTimeShift => shift), next; |
|
if (/^(g)(roupHeadings|roupNames)?([\d:]*)$/i) { |
|
$showGroup = $3 || 0; |
|
$allGroup = ($2 ? lc($2) eq 'roupnames' : $1 eq 'G'); |
|
$mt->Options(SavePath => 1) if $showGroup =~ /\b5\b/; |
|
$mt->Options(SaveFormat => 1) if $showGroup =~ /\b6\b/; |
|
next; |
|
} |
|
if ($a eq 'geotag') { |
|
my $trkfile = shift; |
|
unless ($pass) { |
|
# defer to next pass so the filename charset is available |
|
push @nextPass, '-geotag', $trkfile; |
|
next; |
|
} |
|
$trkfile or Error("Expecting file name for -geotag option\n"), $badCmd=1, next; |
|
# allow wildcards in filename |
|
if ($trkfile =~ /[*?]/) { |
|
# CORE::glob() splits on white space, so use File::Glob if possible |
|
my @trks; |
|
if ($^O eq 'MSWin32' and eval { require Win32::FindFile }) { |
|
# ("-charset filename=UTF8" must be set for this to work with Unicode file names) |
|
@trks = FindFileWindows($mt, $trkfile); |
|
} elsif (eval { require File::Glob }) { |
|
@trks = File::Glob::bsd_glob($trkfile); |
|
} else { |
|
@trks = glob($trkfile); |
|
} |
|
@trks or Error("No matching file found for -geotag option\n"), $badCmd=1, next; |
|
push @newValues, 'geotag='.shift(@trks) while @trks > 1; |
|
$trkfile = pop(@trks); |
|
} |
|
$_ = "geotag=$trkfile"; |
|
# (fall through!) |
|
} |
|
if (/^h$/ or $a eq 'htmlformat') { |
|
require Image::ExifTool::HTML; |
|
$html = $escapeHTML = 1; |
|
$json = $xml = 0; |
|
next; |
|
} |
|
(/^H$/ or $a eq 'hex') and $showTagID = 'H', next; |
|
if (/^htmldump([-+]?\d+)?$/i) { |
|
$verbose = ($verbose || 0) + 1; |
|
$html = 2; |
|
$mt->Options(HtmlDumpBase => $1) if defined $1; |
|
next; |
|
} |
|
if (/^i(gnore)?$/i) { |
|
my $dir = shift; |
|
defined $dir or Error("Expecting directory name for -i option\n"), $badCmd=1, next; |
|
$ignore{$dir} = 1; |
|
$dir eq 'HIDDEN' and $ignoreHidden = 1; |
|
next; |
|
} |
|
if (/^if(\d*)$/i) { |
|
my $cond = shift; |
|
$fastCondition = $1 if length $1; |
|
defined $cond or Error("Expecting expression for -if option\n"), $badCmd=1, next; |
|
# prevent processing file unnecessarily for simple case of failed '$ok' or 'not $ok' |
|
$cond =~ /^\s*(not\s*)\$ok\s*$/i and ($1 xor $rtnValPrev) and $failCondition=1; |
|
# add to list of requested tags |
|
push @requestTags, $cond =~ /\$\{?((?:[-\w]+:)*[-\w?*]+)/g; |
|
push @condition, $cond; |
|
next; |
|
} |
|
if (/^j(son)?(\+?=.*)?$/i) { |
|
if ($2) { |
|
# must process on 2nd pass because we need -f and -charset options |
|
unless ($pass) { |
|
push @nextPass, "-$_"; |
|
push @newValues, { SaveCount => ++$saveCount }; # marker to save new values now |
|
$csvSaveCount = $saveCount; |
|
next; |
|
} |
|
my $jsonFile = $2; |
|
$jsonFile =~ s/^(\+?=)//; |
|
$csvAdd = 2 if $1 eq '+='; |
|
$vout = \*STDERR if $srcStdin; |
|
$verbose and print $vout "Reading JSON file $jsonFile\n"; |
|
my $chset = $mt->Options('Charset'); |
|
my $msg; |
|
if ($mt->Open(\*JSONFILE, $jsonFile)) { |
|
binmode JSONFILE; |
|
require Image::ExifTool::Import; |
|
$msg = Image::ExifTool::Import::ReadJSON(\*JSONFILE, \%database, $forcePrint, $chset); |
|
close(JSONFILE); |
|
} else { |
|
$msg = "Error opening JSON file '${jsonFile}'"; |
|
} |
|
$msg and Warn("$msg\n"); |
|
$isWriting = 1; |
|
$csv = 'JSON'; |
|
} else { |
|
$json = 1; |
|
$html = $xml = 0; |
|
$mt->Options(Duplicates => 1); |
|
require Image::ExifTool::XMP; # for FixUTF8() |
|
} |
|
next; |
|
} |
|
/^(k|pause)$/i and $pause = 1, next; |
|
(/^l$/ or $a eq 'long') and --$outFormat, next; |
|
(/^L$/ or $a eq 'latin') and $mt->Options(Charset => 'Latin'), next; |
|
if ($a eq 'lang') { |
|
$langOpt = (@ARGV and $ARGV[0] !~ /^(-|\xe2\x88\x92)/) ? shift : undef; |
|
if ($langOpt) { |
|
# make lower case and use underline as a separator (eg. 'en_ca') |
|
$langOpt =~ tr/-A-Z/_a-z/; |
|
$mt->Options(Lang => $langOpt); |
|
next if $langOpt eq $mt->Options('Lang'); |
|
} else { |
|
$pass or push(@nextPass, '-lang'), next; |
|
} |
|
my $langs = "Available languages:\n"; |
|
$langs .= " $_ - $Image::ExifTool::langName{$_}\n" foreach @Image::ExifTool::langs; |
|
$langs =~ tr/_/-/; # display dashes instead of underlines in language codes |
|
$langs = Image::ExifTool::HTML::EscapeHTML($langs) if $escapeHTML; |
|
$langs = $mt->Decode($langs, 'UTF8'); |
|
$langOpt and Error("Invalid or unsupported language '${langOpt}'.\n$langs"), $badCmd=1, next; |
|
print $langs; |
|
$helped = 1; |
|
next; |
|
} |
|
if ($a eq 'listitem') { |
|
my $li = shift; |
|
defined $li and Image::ExifTool::IsInt($li) or Warn("Expecting integer for -listItem option\n"), next; |
|
$mt->Options(ListItem => $li); |
|
$listItem = $li; |
|
next; |
|
} |
|
/^(m|ignoreminorerrors)$/i and $mt->Options(IgnoreMinorErrors => 1), next; |
|
/^(n|-printconv)$/i and $mt->Options(PrintConv => 0), next; |
|
/^(-n|printconv)$/i and $mt->Options(PrintConv => 1), next; |
|
$a eq 'nop' and $helped=1, next; # (undocumented) no operation, added in 11.25 |
|
if (/^o(ut)?$/i) { |
|
$outOpt = shift; |
|
defined $outOpt or Error("Expected output file or directory name for -o option\n"), $badCmd=1, next; |
|
CleanFilename($outOpt); |
|
# verbose messages go to STDERR of output is to console |
|
$vout = \*STDERR if $vout =~ /^-(\.\w+)?$/; |
|
next; |
|
} |
|
/^overwrite_original$/i and $overwriteOrig = 1, next; |
|
/^overwrite_original_in_place$/i and $overwriteOrig = 2, next; |
|
if (/^p$/ or $a eq 'printformat') { |
|
my $fmt = shift; |
|
if ($pass) { |
|
LoadPrintFormat($fmt); |
|
# load MWG module now if necessary |
|
if (not $useMWG and grep /^mwg:/i, @requestTags) { |
|
$useMWG = 1; |
|
require Image::ExifTool::MWG; |
|
Image::ExifTool::MWG::Load(); |
|
} |
|
} else { |
|
# defer to next pass so the filename charset is available |
|
push @nextPass, '-p', $fmt; |
|
} |
|
next; |
|
} |
|
(/^P$/ or $a eq 'preserve') and $preserveTime = 1, next; |
|
/^password$/i and $mt->Options(Password => shift), next; |
|
if (/^progress(:.*)?$/i) { |
|
if ($1) { |
|
$windowTitle = substr $1, 1; |
|
$windowTitle = 'ExifTool %p%%' unless length $windowTitle; |
|
$windowTitle =~ /%\d*[bpr]/ and $progress = 0 unless defined $progress; |
|
} else { |
|
$progress = 1; |
|
$verbose = 0 unless defined $verbose; |
|
} |
|
$progressCount = 0; |
|
next; |
|
} |
|
/^q(uiet)?$/i and ++$quiet, next; |
|
/^r(ecurse)?(\.?)$/i and $recurse = ($2 ? 2 : 1), next; |
|
if ($a eq 'require') { # (undocumented) added in version 8.65 |
|
my $ver = shift; |
|
unless (defined $ver and Image::ExifTool::IsFloat($ver)) { |
|
Error("Expecting version number for -require option\n"); |
|
$badCmd = 1; |
|
next; |
|
} |
|
unless ($Image::ExifTool::VERSION >= $ver) { |
|
Error("Requires ExifTool version $ver or later\n"); |
|
$badCmd = 1; |
|
} |
|
next; |
|
} |
|
/^restore_original$/i and $deleteOrig = 0, next; |
|
(/^S$/ or $a eq 'veryshort') and $outFormat+=2, next; |
|
/^s(hort)?(\d*)$/i and $outFormat = $2 eq '' ? $outFormat + 1 : $2, next; |
|
/^scanforxmp$/i and $mt->Options(ScanForXMP => 1), next; |
|
if (/^sep(arator)?$/i) { |
|
my $sep = $listSep = shift; |
|
defined $listSep or Error("Expecting list item separator for -sep option\n"), $badCmd=1, next; |
|
$sep =~ s/\\(.)/$unescapeChar{$1}||$1/sge; # translate escape sequences |
|
(defined $binSep ? $binTerm : $binSep) = $sep; |
|
$mt->Options(ListSep => $listSep); |
|
$joinLists = 1; |
|
# also split when writing values |
|
my $listSplit = quotemeta $listSep; |
|
# a space in the string matches zero or more whitespace characters |
|
$listSplit =~ s/(\\ )+/\\s\*/g; |
|
# but a single space alone matches one or more whitespace characters |
|
$listSplit = '\\s+' if $listSplit eq '\\s*'; |
|
$mt->Options(ListSplit => $listSplit); |
|
next; |
|
} |
|
/^(-)?sort$/i and $sortOpt = $1 ? 0 : 1, next; |
|
if ($a eq 'srcfile') { |
|
@ARGV or Warn("Expecting FMT for -srcfile option\n"), next; |
|
push @srcFmt, shift; |
|
next; |
|
} |
|
if ($a eq 'stay_open') { |
|
my $arg = shift; |
|
defined $arg or Warn("Expecting argument for -stay_open option\n"), next; |
|
if ($arg =~ /^(1|true)$/i) { |
|
if (not $stayOpen) { |
|
$stayOpen = 1; |
|
} elsif ($stayOpen == 2) { |
|
$stayOpen = 3; # chained -stay_open options |
|
} else { |
|
Warn "-stay_open already active\n"; |
|
} |
|
} elsif ($arg =~ /^(0|false)$/i) { |
|
if ($stayOpen >= 2) { |
|
# close -stay_open argfile and process arguments up to this point |
|
close STAYOPEN; |
|
push @ARGV, @moreArgs; |
|
undef @moreArgs; |
|
} elsif (not $stayOpen) { |
|
Warn("-stay_open wasn't active\n"); |
|
} |
|
$stayOpen = 0; |
|
} else { |
|
Warn "Invalid argument for -stay_open\n"; |
|
} |
|
next; |
|
} |
|
if (/^(-)?struct$/i) { |
|
$mt->Options(Struct => $1 ? 0 : 1); |
|
next; |
|
} |
|
/^t(ab)?$/ and $tabFormat = 1, next; |
|
if (/^T$/ or $a eq 'table') { |
|
$tabFormat = $forcePrint = 1; $outFormat+=2; ++$quiet; |
|
next; |
|
} |
|
if (/^(u)(nknown(2)?)?$/i) { |
|
my $inc = ($3 or (not $2 and $1 eq 'U')) ? 2 : 1; |
|
$mt->Options(Unknown => $mt->Options('Unknown') + $inc); |
|
next; |
|
} |
|
if ($a eq 'use') { |
|
my $module = shift; |
|
$module or Error("Expecting module name for -use option\n"), $badCmd=1, next; |
|
lc $module eq 'mwg' and $useMWG = 1, next; |
|
$module =~ /[^\w:]/ and Error("Invalid module name: $module\n"), $badCmd=1, next; |
|
local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] }; |
|
unless (eval "require Image::ExifTool::$module" or |
|
eval "require $module" or |
|
eval "require '${module}'") |
|
{ |
|
Error("Error using module $module\n"); |
|
$badCmd = 1; |
|
} |
|
next; |
|
} |
|
if ($a eq 'userparam') { |
|
my $opt = shift; |
|
defined $opt or Error("Expected parameter for -userParam option\n"), $badCmd=1, next; |
|
$opt =~ /=/ or $opt .= '=1'; |
|
$mt->Options(UserParam => $opt); |
|
next; |
|
} |
|
if (/^v(erbose)?(\d*)$/i) { |
|
$verbose = ($2 eq '') ? ($verbose || 0) + 1 : $2; |
|
next; |
|
} |
|
if (/^(w|textout|tagout)([!+]*)$/i) { |
|
$textOut = shift || Warn("Expecting output extension for -$_ option\n"); |
|
my ($t1, $t2) = ($1, $2); |
|
$textOverwrite = 0; |
|
$textOverwrite += 1 if $t2 =~ /!/; # overwrite |
|
$textOverwrite += 2 if $t2 =~ /\+/; # append |
|
if ($t1 ne 'W' and lc($t1) ne 'tagout') { |
|
undef $tagOut; |
|
} elsif ($textOverwrite >= 2 and $textOut !~ /%[-+]?\d*[.:]?\d*[lu]?[tgso]/) { |
|
$tagOut = 0; # append tags to one file |
|
} else { |
|
$tagOut = 1; # separate file for each tag |
|
} |
|
next; |
|
} |
|
if (/^(-?)(wext|tagoutext)$/i) { |
|
my $ext = shift; |
|
defined $ext or Error("Expecting extension for -wext option\n"), $badCmd=1, next; |
|
my $flag = 1; |
|
$1 and $wext{'*'} = 1, $flag = -1; |
|
$ext =~ s/^\.//; |
|
$wext{lc $ext} = $flag; |
|
next; |
|
} |
|
if ($a eq 'wm' or $a eq 'writemode') { |
|
my $wm = shift; |
|
defined $wm or Error("Expecting argument for -$_ option\n"), $badCmd=1, next; |
|
$wm =~ /^[wcg]*$/i or Error("Invalid argument for -$_ option\n"), $badCmd=1, next; |
|
$mt->Options(WriteMode => $wm); |
|
next; |
|
} |
|
if (/^x$/ or $a eq 'exclude') { |
|
my $tag = shift; |
|
defined $tag or Error("Expecting tag name for -x option\n"), $badCmd=1, next; |
|
$tag =~ s/\ball\b/\*/ig; # replace 'all' with '*' in tag names |
|
if ($setTagsFile) { |
|
push @{$setTags{$setTagsFile}}, "-$tag"; |
|
} else { |
|
push @exclude, $tag; |
|
} |
|
next; |
|
} |
|
(/^X$/ or $a eq 'xmlformat') and $xml = 1, $html = $json = 0, $mt->Options(Duplicates => 1), next; |
|
if (/^php$/i) { |
|
$json = 2; |
|
$html = $xml = 0; |
|
$mt->Options(Duplicates => 1); |
|
next; |
|
} |
|
if (/^z(ip)?$/i) { |
|
$doUnzip = 1; |
|
$mt->Options(Compress => 1, XMPShorthand => 1); |
|
$mt->Options(Compact => 1) unless $mt->Options('Compact'); |
|
next; |
|
} |
|
$_ eq '' and push(@files, '-'), $srcStdin = 1, next; # read STDIN |
|
length $_ eq 1 and $_ ne '*' and Error("Unknown option -$_\n"), $badCmd=1, next; |
|
if (/^[^<]+(<?)=(.*)/s) { |
|
my $val = $2; |
|
if ($1 and length($val) and ($val eq '@' or not defined FilenameSPrintf($val))) { |
|
# save count of new values before a dynamic value |
|
push @newValues, { SaveCount => ++$saveCount }; |
|
} |
|
push @newValues, $_; |
|
if (/^mwg:/i) { |
|
$useMWG = 1; |
|
} elsif (/^([-\w]+:)*(filename|directory|testname)\b/i) { |
|
$doSetFileName = 1; |
|
} elsif (/^([-\w]+:)*(geotag|geotime|geosync)\b/i) { |
|
if (lc $2 eq 'geotime') { |
|
$addGeotime = ''; |
|
} else { |
|
# add geotag/geosync commands first |
|
unshift @newValues, pop @newValues; |
|
if (lc $2 eq 'geotag' and (not defined $addGeotime or $addGeotime) and length $val) { |
|
$addGeotime = ($1 || '') . 'Geotime<DateTimeOriginal#'; |
|
} |
|
} |
|
} |
|
} else { |
|
# assume '-tagsFromFile @' if tags are being redirected |
|
# and -tagsFromFile hasn't already been specified |
|
AddSetTagsFile($setTagsFile = '@') if not $setTagsFile and /(<|>)/; |
|
if ($setTagsFile) { |
|
push @{$setTags{$setTagsFile}}, $_; |
|
if (/>/) { |
|
$useMWG = 1 if /^(.*>\s*)?mwg:/si; |
|
if (/\b(filename|directory|testname)#?$/i) { |
|
$doSetFileName = 1; |
|
} elsif (/\bgeotime#?$/i) { |
|
$addGeotime = ''; |
|
} |
|
} else { |
|
$useMWG = 1 if /^([^<]+<\s*(.*\$\{?)?)?mwg:/si; |
|
if (/^([-\w]+:)*(filename|directory|testname)\b/i) { |
|
$doSetFileName = 1; |
|
} elsif (/^([-\w]+:)*geotime\b/i) { |
|
$addGeotime = ''; |
|
} |
|
} |
|
} else { |
|
my $lst = s/^-// ? \@exclude : \@tags; |
|
unless (/^([-\w*]+:)*([-\w*?]+)#?$/) { |
|
Warn(qq(Invalid TAG name: "$_"\n)); |
|
} |
|
push @$lst, $_; # (push everything for backward compatibility) |
|
} |
|
} |
|
} else { |
|
unless ($pass) { |
|
# defer to next pass so the filename charset is available |
|
push @nextPass, $_; |
|
next; |
|
} |
|
if ($doGlob and /[*?]/) { |
|
if ($^O eq 'MSWin32' and eval { require Win32::FindFile }) { |
|
push @files, FindFileWindows($mt, $_); |
|
} else { |
|
# glob each filespec if necessary - MK/20061010 |
|
push @files, File::Glob::bsd_glob($_); |
|
} |
|
$doGlob = 2; |
|
} else { |
|
push @files, $_; |
|
$srcStdin = 1 if $_ eq '-'; |
|
} |
|
} |
|
} |
|
|
|
# set "OK" UserParam based on result of last command |
|
$mt->Options(UserParam => 'OK=' . (not $rtnValPrev)); |
|
|
|
# set verbose output to STDERR if output could be to console |
|
$vout = \*STDERR if $srcStdin and ($isWriting or @newValues); |
|
$mt->Options(TextOut => $vout) if $vout eq \*STDERR; |
|
|
|
# change default EXIF string encoding if MWG used |
|
if ($useMWG and not defined $mt->Options('CharsetEXIF')) { |
|
$mt->Options(CharsetEXIF => 'UTF8'); |
|
} |
|
|
|
# print help |
|
unless ((@tags and not $outOpt) or @files or @newValues) { |
|
if ($doGlob and $doGlob == 2) { |
|
Warn "No matching files\n"; |
|
$rtnVal = 1; |
|
next; |
|
} |
|
if ($outOpt) { |
|
Warn "Nothing to write\n"; |
|
$rtnVal = 1; |
|
next; |
|
} |
|
Help() unless $helped; |
|
next; |
|
} |
|
|
|
# do sanity check on -delete_original and -restore_original |
|
if (defined $deleteOrig and (@newValues or @tags)) { |
|
if (not @newValues) { |
|
my $verb = $deleteOrig ? 'deleting' : 'restoring from'; |
|
Warn "Can't specify tags when $verb originals\n"; |
|
} elsif ($deleteOrig) { |
|
Warn "Can't use -delete_original when writing.\n"; |
|
Warn "Maybe you meant -overwrite_original ?\n"; |
|
} else { |
|
Warn "It makes no sense to use -restore_original when writing\n"; |
|
} |
|
$rtnVal = 1; |
|
next; |
|
} |
|
|
|
if ($overwriteOrig > 1 and $outOpt) { |
|
Warn "Can't overwrite in place when -o option is used\n"; |
|
$rtnVal = 1; |
|
next; |
|
} |
|
|
|
if ($tagOut and ($csv or %printFmt or $tabFormat or $xml or ($verbose and $html))) { |
|
Warn "Sorry, -W may not be combined with -csv, -htmlDump, -j, -p, -t or -X\n"; |
|
$rtnVal = 1; |
|
next; |
|
} |
|
|
|
if ($csv and $csv eq 'CSV' and not $isWriting) { |
|
if ($textOut) { |
|
Warn "Sorry, -w may not be combined with -csv\n"; |
|
$rtnVal = 1; |
|
next; |
|
} |
|
if ($binaryOutput) { |
|
$binaryOutput = 0; |
|
$setCharset = 'default' unless defined $setCharset; |
|
} |
|
require Image::ExifTool::XMP if $setCharset; |
|
} |
|
|
|
if ($escapeHTML or $json) { |
|
# must be UTF8 for HTML conversion and JSON output |
|
$mt->Options(Charset => 'UTF8') if $json; |
|
# use Escape option to do our HTML escaping unless XML output |
|
$mt->Options(Escape => 'HTML') if $escapeHTML and not $xml; |
|
} elsif ($escapeXML and not $xml) { |
|
$mt->Options(Escape => 'XML'); |
|
} |
|
|
|
# set sort option |
|
if ($sortOpt) { |
|
# (note that -csv sorts alphabetically by default anyway if more than 1 file) |
|
my $sort = ($outFormat > 0 or $xml or $json or $csv) ? 'Tag' : 'Descr'; |
|
$mt->Options(Sort => $sort, Sort2 => $sort); |
|
} |
|
|
|
# set $structOpt in case set by API option |
|
if ($mt->Options('Struct') and not $structOpt) { |
|
$structOpt = $mt->Options('Struct'); |
|
require 'Image/ExifTool/XMPStruct.pl'; |
|
} |
|
|
|
# set up for RDF/XML, JSON and PHP output formats |
|
if ($xml) { |
|
require Image::ExifTool::XMP; # for EscapeXML() |
|
my $charset = $mt->Options('Charset'); |
|
# standard XML encoding names for supported Charset settings |
|
# (ref http://www.iana.org/assignments/character-sets) |
|
my %encoding = ( |
|
UTF8 => 'UTF-8', |
|
Latin => 'windows-1252', |
|
Latin2 => 'windows-1250', |
|
Cyrillic => 'windows-1251', |
|
Greek => 'windows-1253', |
|
Turkish => 'windows-1254', |
|
Hebrew => 'windows-1255', |
|
Arabic => 'windows-1256', |
|
Baltic => 'windows-1257', |
|
Vietnam => 'windows-1258', |
|
MacRoman => 'macintosh', |
|
); |
|
# switch to UTF-8 if we don't have a standard encoding name |
|
unless ($encoding{$charset}) { |
|
$charset = 'UTF8'; |
|
$mt->Options(Charset => $charset); |
|
} |
|
# set file header/trailer for XML output |
|
$fileHeader = "<?xml version='1.0' encoding='$encoding{$charset}'?>\n" . |
|
"<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n"; |
|
$fileTrailer = "</rdf:RDF>\n"; |
|
# extract as a list unless short output format |
|
$joinLists = 1 if $outFormat > 0; |
|
$mt->Options(List => 1) unless $joinLists; |
|
$showGroup = $allGroup = 1; # always show group 1 |
|
# set binaryOutput flag to 0 or undef (0 = output encoded binary in XML) |
|
$binaryOutput = ($outFormat > 0 ? undef : 0) if $binaryOutput; |
|
$showTagID = 'D' if $tabFormat and not $showTagID; |
|
} elsif ($json) { |
|
if ($json == 1) { # JSON |
|
$fileHeader = '['; |
|
$fileTrailer = "]\n"; |
|
} else { # PHP |
|
$fileHeader = 'Array('; |
|
$fileTrailer = ");\n"; |
|
} |
|
# allow binary output in a text-mode file when -php/-json and -b used together |
|
# (this works because PHP strings are simple arrays of bytes, and CR/LF |
|
# won't be messed up in the text mode output because they are converted |
|
# to escape sequences in the strings) |
|
if ($binaryOutput) { |
|
$binaryOutput = 0; |
|
require Image::ExifTool::XMP if $json == 1; # (for EncodeBase64) |
|
} |
|
$mt->Options(List => 1) unless $joinLists; |
|
$showTagID = 'D' if $tabFormat and not $showTagID; |
|
} elsif ($structOpt) { |
|
$mt->Options(List => 1); |
|
} else { |
|
$joinLists = 1; # join lists for all other unstructured output formats |
|
} |
|
|
|
if ($argFormat) { |
|
$outFormat = 3; |
|
$allGroup = 1 if defined $showGroup; |
|
} |
|
|
|
# change to forward slashes if necessary in all filenames (like CleanFilename) |
|
if ($hasBackslash{$^O}) { |
|
tr/\\/\// foreach @files; |
|
} |
|
|
|
# can't do anything if no file specified |
|
unless (@files) { |
|
unless ($outOpt) { |
|
if ($doGlob and $doGlob == 2) { |
|
Warn "No matching files\n"; |
|
} else { |
|
Warn "No file specified\n"; |
|
} |
|
$rtnVal = 1; |
|
next; |
|
} |
|
push @files, ''; # create file from nothing |
|
} |
|
|
|
# set Verbose and HtmlDump options |
|
if ($verbose) { |
|
$disableOutput = 1 unless @tags or @exclude or $tagOut; |
|
undef $binaryOutput unless $tagOut; # disable conflicting option |
|
if ($html) { |
|
$html = 2; # flag for html dump |
|
$mt->Options(HtmlDump => $verbose); |
|
} else { |
|
$mt->Options(Verbose => $verbose) unless $tagOut; |
|
} |
|
} elsif (defined $verbose) { |
|
# auto-flush output when -v0 is used |
|
require FileHandle; |
|
STDOUT->autoflush(1); |
|
STDERR->autoflush(1); |
|
} |
|
|
|
# validate all tags we're writing |
|
my $needSave = 1; |
|
if (@newValues) { |
|
# assume -geotime value if -geotag specified without -geotime |
|
if ($addGeotime) { |
|
AddSetTagsFile($setTagsFile = '@') unless $setTagsFile and $setTagsFile eq '@'; |
|
push @{$setTags{$setTagsFile}}, $addGeotime; |
|
$verbose and print $vout qq{Argument "-$addGeotime" is assumed\n}; |
|
} |
|
my %setTagsIndex; |
|
# add/delete option lookup |
|
my %addDelOpt = ( '+' => 'AddValue', '-' => 'DelValue', "\xe2\x88\x92" => 'DelValue' ); |
|
$saveCount = 0; |
|
foreach (@newValues) { |
|
if (ref $_ eq 'HASH') { |
|
# save new values now if we stored a "SaveCount" marker |
|
if ($$_{SaveCount}) { |
|
$saveCount = $mt->SaveNewValues(); |
|
$needSave = 0; |
|
# insert marker to load values from CSV file now if this was the CSV file |
|
push @dynamicFiles, \$csv if $$_{SaveCount} == $csvSaveCount; |
|
} |
|
next; |
|
} |
|
/(.*?)=(.*)/s or next; |
|
my ($tag, $newVal) = ($1, $2); |
|
$tag =~ s/\ball\b/\*/ig; # replace 'all' with '*' in tag names |
|
$newVal eq '' and undef $newVal unless $tag =~ s/\^([-+]*)$/$1/; # undefined to delete tag |
|
if ($tag =~ /^(All)?TagsFromFile$/i) { |
|
defined $newVal or Error("Need file name for -tagsFromFile\n"), next Command; |
|
++$isWriting; |
|
if ($newVal eq '@' or not defined FilenameSPrintf($newVal)) { |
|
push @dynamicFiles, $newVal; |
|
next; # set tags from dynamic file later |
|
} |
|
unless ($mt->Exists($newVal) or $newVal eq '-') { |
|
Warn "File '${newVal}' does not exist for -tagsFromFile option\n"; |
|
$rtnVal = 1; |
|
next Command; |
|
} |
|
my $setTags = $setTags{$newVal}; |
|
# do we have multiple -tagsFromFile options with this file? |
|
if ($setTagsList{$newVal}) { |
|
# use the tags set in the i-th occurrence |
|
my $i = $setTagsIndex{$newVal} || 0; |
|
$setTagsIndex{$newVal} = $i + 1; |
|
$setTags = $setTagsList{$newVal}[$i] if $setTagsList{$newVal}[$i]; |
|
} |
|
# set specified tags from this file |
|
unless (DoSetFromFile($mt, $newVal, $setTags)) { |
|
$rtnVal = 1; |
|
next Command; |
|
} |
|
$needSave = 1; |
|
next; |
|
} |
|
my %opts = ( Shift => 0 ); # shift values if possible instead of adding/deleting |
|
# allow writing of 'Unsafe' tags unless specified by wildcard |
|
$opts{Protected} = 1 unless $tag =~ /[?*]/; |
|
|
|
if ($tag =~ s/<// and defined $newVal) { |
|
if (defined FilenameSPrintf($newVal)) { |
|
SlurpFile($newVal, \$newVal) or next; # read file data into $newVal |
|
} else { |
|
$tag =~ s/([-+]|\xe2\x88\x92)$// and $opts{$addDelOpt{$1}} = 1; |
|
# verify that this tag can be written |
|
my $result = Image::ExifTool::IsWritable($tag); |
|
if ($result) { |
|
$opts{ProtectSaved} = $saveCount; # protect new values set after this |
|
# add to list of dynamic tag values |
|
push @dynamicFiles, [ $tag, $newVal, \%opts ]; |
|
++$isWriting; |
|
} elsif (defined $result) { |
|
Warn "Tag '${tag}' is not writable\n"; |
|
} else { |
|
Warn "Tag '${tag}' does not exist\n"; |
|
} |
|
next; |
|
} |
|
} |
|
if ($tag =~ s/([-+]|\xe2\x88\x92)$//) { |
|
$opts{$addDelOpt{$1}} = 1; # set AddValue or DelValue option |
|
# set $newVal to '' if deleting nothing |
|
$newVal = '' if $1 eq '-' and not defined $newVal; |
|
} |
|
if ($escapeC and defined $newVal) { |
|
$newVal =~ s/\\(x([0-9a-fA-F]{2})|.)/$2 ? chr(hex($2)) : $unescC{$1} || $1/seg; |
|
} |
|
my ($rtn, $wrn) = $mt->SetNewValue($tag, $newVal, %opts); |
|
$needSave = 1; |
|
++$isWriting if $rtn; |
|
$wrn and Warn "Warning: $wrn\n"; |
|
} |
|
# exclude specified tags |
|
foreach (@exclude) { |
|
$mt->SetNewValue($_, undef, Replace => 2); |
|
$needSave = 1; |
|
} |
|
unless ($isWriting or $outOpt or @tags) { |
|
Warn "Nothing to do.\n"; |
|
$rtnVal = 1; |
|
next; |
|
} |
|
} elsif (grep /^(\*:)?\*$/, @exclude) { |
|
Warn "All tags excluded -- nothing to do.\n"; |
|
$rtnVal = 1; |
|
next; |
|
} |
|
if ($isWriting and @tags and not $outOpt) { |
|
my ($tg, $s) = @tags > 1 ? ("$tags[0] ...", 's') : ($tags[0], ''); |
|
Warn "Ignored superfluous tag name$s or invalid option$s: -$tg\n"; |
|
} |
|
# save current state of new values if setting values from target file |
|
# or if we may be translating to a different format |
|
$mt->SaveNewValues() if $outOpt or (@dynamicFiles and $needSave); |
|
|
|
$multiFile = 1 if @files > 1; |
|
@exclude and $mt->Options(Exclude => \@exclude); |
|
|
|
undef $binaryOutput if $html; |
|
|
|
if ($binaryOutput) { |
|
$outFormat = 99; # shortest possible output format |
|
$mt->Options(PrintConv => 0); |
|
unless ($textOut or $binaryStdout) { |
|
binmode(STDOUT); |
|
$binaryStdout = 1; |
|
$mt->Options(TextOut => ($vout = \*STDERR)); |
|
} |
|
# disable conflicting options |
|
undef $showGroup; |
|
} |
|
|
|
# sort by groups to look nicer depending on options |
|
if (defined $showGroup and not (@tags and $allGroup) and ($sortOpt or not defined $sortOpt)) { |
|
$mt->Options(Sort => "Group$showGroup"); |
|
} |
|
|
|
if (defined $textOut) { |
|
CleanFilename($textOut); # make all forward slashes |
|
# add '.' before output extension if necessary |
|
$textOut = ".$textOut" unless $textOut =~ /[.%]/ or defined $tagOut; |
|
} |
|
|
|
# determine if we should scan for only writable files |
|
if ($outOpt) { |
|
my $type = GetFileType($outOpt); |
|
if ($type) { |
|
unless (CanWrite($type)) { |
|
Warn "Can't write $type files\n"; |
|
$rtnVal = 1; |
|
next; |
|
} |
|
$scanWritable = $type unless CanCreate($type); |
|
} else { |
|
$scanWritable = 1; |
|
} |
|
$isWriting = 1; # set writing flag |
|
} elsif ($isWriting or defined $deleteOrig) { |
|
$scanWritable = 1; |
|
} |
|
|
|
# initialize alternate encoding flag |
|
$altEnc = $mt->Options('Charset'); |
|
undef $altEnc if $altEnc eq 'UTF8'; |
|
|
|
# set flag to fix description lengths if necessary |
|
if (not $altEnc and $mt->Options('Lang') ne 'en' and eval { require Encode }) { |
|
# (note that Unicode::GCString is part of the Unicode::LineBreak package) |
|
$fixLen = eval { require Unicode::GCString } ? 2 : 1; |
|
} |
|
|
|
# sort input files if specified |
|
if (@fileOrder) { |
|
my @allFiles; |
|
ProcessFiles($mt, \@allFiles); |
|
my $sortTool = new Image::ExifTool; |
|
$sortTool->Options(FastScan => $fileOrderFast) if $fileOrderFast; |
|
$sortTool->Options(PrintConv => $mt->Options('PrintConv')); |
|
$sortTool->Options(Duplicates => 0); |
|
my (%sortBy, %isFloat, @rev, $file); |
|
# save reverse sort flags |
|
push @rev, (s/^-// ? 1 : 0) foreach @fileOrder; |
|
foreach $file (@allFiles) { |
|
my @tags; |
|
my $info = $sortTool->ImageInfo(Infile($file,1), @fileOrder, \@tags); |
|
# get values of all tags (or '~' to sort last if not defined) |
|
foreach (@tags) { |
|
$_ = $$info{$_}; # put tag value into @tag list |
|
defined $_ or $_ = '~', next; |
|
$isFloat{$_} = Image::ExifTool::IsFloat($_); |
|
# pad numbers to 12 digits to keep them sequential |
|
s/(\d+)/(length($1) < 12 ? '0'x(12-length($1)) : '') . $1/eg unless $isFloat{$_}; |
|
} |
|
$sortBy{$file} = \@tags; # save tag values for each file |
|
} |
|
# sort in specified order |
|
@files = sort { |
|
my ($i, $cmp); |
|
for ($i=0; $i<@rev; ++$i) { |
|
my $u = $sortBy{$a}[$i]; |
|
my $v = $sortBy{$b}[$i]; |
|
if (not $isFloat{$u} and not $isFloat{$v}) { |
|
$cmp = $u cmp $v; # alphabetically |
|
} elsif ($isFloat{$u} and $isFloat{$v}) { |
|
$cmp = $u <=> $v; # numerically |
|
} else { |
|
$cmp = $isFloat{$u} ? -1 : 1; # numbers first |
|
} |
|
return $rev[$i] ? -$cmp : $cmp if $cmp; |
|
} |
|
return $a cmp $b; # default to sort by name |
|
} @allFiles; |
|
} elsif (defined $progress) { |
|
# expand FILE argument to count the number of files to process |
|
my @allFiles; |
|
ProcessFiles($mt, \@allFiles); |
|
@files = @allFiles; |
|
} |
|
# set file count for progress message |
|
$progressMax = scalar @files if defined $progress; |
|
|
|
# store duplicate database information under absolute path |
|
my @dbKeys = keys %database; |
|
if (@dbKeys) { |
|
if (eval { require Cwd }) { |
|
undef $evalWarning; |
|
local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] }; |
|
foreach (@dbKeys) { |
|
my $db = $database{$_}; |
|
tr/\\/\// and $database{$_} = $db; # allow for backslashes in SourceFile |
|
# (punt on using ConvertFileName here, so $absPath may be a mix of encodings) |
|
my $absPath = AbsPath($_); |
|
if (defined $absPath) { |
|
$database{$absPath} = $db unless $database{$absPath}; |
|
if ($verbose and $verbose > 1) { |
|
print $vout "Imported entry for '${_}' (full path: '${absPath}')\n"; |
|
} |
|
} elsif ($verbose and $verbose > 1) { |
|
print $vout "Imported entry for '${_}' (non-existent file)\n"; |
|
} |
|
} |
|
} |
|
} |
|
|
|
# process all specified files |
|
ProcessFiles($mt); |
|
|
|
if ($filtered and not $validFile) { |
|
Warn "No file with specified extension\n"; |
|
$rtnVal = 1; |
|
} |
|
|
|
# print CSV information if necessary |
|
PrintCSV() if $csv and not $isWriting; |
|
|
|
# print folder/file trailer if necessary |
|
if ($textOut) { |
|
foreach (keys %outTrailer) { |
|
next unless $outTrailer{$_}; |
|
if ($mt->Open(\*OUTTRAIL, $_, '>>')) { |
|
my $fp = \*OUTTRAIL; |
|
print $fp $outTrailer{$_}; |
|
close $fp; |
|
} else { |
|
Error("Error appending to $_\n"); |
|
} |
|
} |
|
} else { |
|
print $sectTrailer if $sectTrailer; |
|
print $fileTrailer if $fileTrailer and not $fileHeader; |
|
} |
|
|
|
my $totWr = $countGoodWr + $countBadWr + $countSameWr + $countCopyWr + |
|
$countGoodCr + $countBadCr; |
|
|
|
if (defined $deleteOrig) { |
|
|
|
# print summary and delete requested files |
|
unless ($quiet) { |
|
printf "%5d directories scanned\n", $countDir if $countDir; |
|
printf "%5d directories created\n", $countNewDir if $countNewDir; |
|
printf "%5d files failed condition\n", $countFailed if $countFailed; |
|
printf "%5d image files found\n", $count; |
|
} |
|
if (@delFiles) { |
|
# verify deletion unless "-delete_original!" was specified |
|
if ($deleteOrig == 1) { |
|
printf '%5d originals will be deleted! Are you sure [y/n]? ', scalar(@delFiles); |
|
my $response = <STDIN>; |
|
unless ($response =~ /^(y|yes)\s*$/i) { |
|
Warn "Originals not deleted.\n"; |
|
next; |
|
} |
|
} |
|
$countGoodWr = $mt->Unlink(@delFiles); |
|
$countBad = scalar(@delFiles) - $countGoodWr; |
|
} |
|
if ($quiet) { |
|
# no more messages |
|
} elsif ($count and not $countGoodWr and not $countBad) { |
|
printf "%5d original files found\n", $countGoodWr; # (this will be 0) |
|
} elsif ($deleteOrig) { |
|
printf "%5d original files deleted\n", $countGoodWr if $count; |
|
printf "%5d originals not deleted due to errors\n", $countBad if $countBad; |
|
} else { |
|
printf "%5d image files restored from original\n", $countGoodWr if $count; |
|
printf "%5d files not restored due to errors\n", $countBad if $countBad; |
|
} |
|
|
|
} elsif ((not $binaryStdout or $verbose) and not $quiet) { |
|
|
|
# print summary |
|
my $tot = $count + $countBad; |
|
if ($countDir or $totWr or $countFailed or $tot > 1 or $textOut or %countLink) { |
|
my $o = (($html or $json or $xml or %printFmt or $csv) and not $textOut) ? \*STDERR : $vout; |
|
printf($o "%5d directories scanned\n", $countDir) if $countDir; |
|
printf($o "%5d directories created\n", $countNewDir) if $countNewDir; |
|
printf($o "%5d files failed condition\n", $countFailed) if $countFailed; |
|
printf($o "%5d image files created\n", $countGoodCr) if $countGoodCr; |
|
printf($o "%5d image files updated\n", $countGoodWr) if $totWr - $countGoodCr - $countBadCr - $countCopyWr; |
|
printf($o "%5d image files unchanged\n", $countSameWr) if $countSameWr; |
|
printf($o "%5d image files %s\n", $countCopyWr, $overwriteOrig ? 'moved' : 'copied') if $countCopyWr; |
|
printf($o "%5d files weren't updated due to errors\n", $countBadWr) if $countBadWr; |
|
printf($o "%5d files weren't created due to errors\n", $countBadCr) if $countBadCr; |
|
printf($o "%5d image files read\n", $count) if ($tot+$countFailed)>1 or ($countDir and not $totWr); |
|
printf($o "%5d files could not be read\n", $countBad) if $countBad; |
|
printf($o "%5d output files created\n", scalar(keys %created)) if $textOut; |
|
printf($o "%5d output files appended\n", scalar(keys %appended)) if %appended; |
|
printf($o "%5d hard links created\n", $countLink{Hard} || 0) if $countLink{Hard} or $countLink{BadHard}; |
|
printf($o "%5d hard links could not be created\n", $countLink{BadHard}) if $countLink{BadHard}; |
|
printf($o "%5d symbolic links created\n", $countLink{Sym} || 0) if $countLink{Sym} or $countLink{BadSym}; |
|
printf($o "%5d symbolic links could not be created\n", $countLink{BadSym}) if $countLink{BadSym}; |
|
} |
|
} |
|
|
|
# set error status if we had any errors or if all files failed the "-if" condition |
|
if ($countBadWr or $countBadCr or $countBad) { |
|
$rtnVal = 1; |
|
} elsif ($countFailed and not ($count or $totWr) and not $rtnVal) { |
|
$rtnVal = 2; |
|
} |
|
|
|
# clean up after each command |
|
Cleanup(); |
|
|
|
} # end "Command" loop ........................................................ |
|
|
|
close STAYOPEN if $stayOpen >= 2; |
|
|
|
Exit $rtnValApp; # all done |
|
|
|
|
|
#------------------------------------------------------------------------------ |
|
# Get image information from EXIF data in file (or write file if writing) |
|
# Inputs: 0) ExifTool object reference, 1) file name |
|
sub GetImageInfo($$) |
|
{ |
|
my ($et, $orig) = @_; |
|
my (@foundTags, $info, $file, $ind); |
|
|
|
# set window title for this file if necessary |
|
if (defined $windowTitle) { |
|
my $prog = $progressMax ? "$progressCount/$progressMax" : '0/0'; |
|
my $title = $windowTitle; |
|
my ($num, $denom) = split '/', $prog; |
|
my $frac = $num / ($denom || 1); |
|
my $n = $title =~ s/%(\d+)b/%b/ ? $1 : 20; # length of bar |
|
my $bar = int($frac * $n + 0.5); |
|
my %lkup = ( |
|
b => ('I' x $bar) . ('.' x ($n - $bar)), # (undocumented) |
|
f => $orig, |
|
p => int(100 * $frac + 0.5), |
|
r => $prog, |
|
'%'=> '%', |
|
); |
|
$title =~ s/%([%bfpr])/$lkup{$1}/eg; |
|
SetWindowTitle($title); |
|
} |
|
unless (length $orig or $outOpt) { |
|
Warn qq(Error: Zero-length file name - ""\n); |
|
++$countBad; |
|
return; |
|
} |
|
# determine the name of the source file based on the original input file name |
|
if (@srcFmt) { |
|
my ($fmt, $first); |
|
foreach $fmt (@srcFmt) { |
|
$file = $fmt eq '@' ? $orig : FilenameSPrintf($fmt, $orig); |
|
# use this file if it exists |
|
$et->Exists($file) and undef($first), last; |
|
$verbose and print $vout "Source file $file does not exist\n"; |
|
$first = $file unless defined $first; |
|
} |
|
$file = $first if defined $first; |
|
my ($d, $f) = Image::ExifTool::SplitFileName($orig); |
|
$et->Options(UserParam => "OriginalDirectory#=$d"); |
|
$et->Options(UserParam => "OriginalFileName#=$f"); |
|
} else { |
|
$file = $orig; |
|
} |
|
|
|
my $pipe = $file; |
|
if ($doUnzip) { |
|
# pipe through gzip or bzip2 if necessary |
|
if ($file =~ /\.(gz|bz2)$/i) { |
|
my $type = lc $1; |
|
if ($file =~ /[^-_.'A-Za-z0-9\/\\]/) { |
|
Warn "Error: Insecure zip file name. Skipped\n"; |
|
EFile($file); |
|
++$countBad; |
|
return; |
|
} |
|
if ($type eq 'gz') { |
|
$pipe = qq{gzip -dc "$file" |}; |
|
} else { |
|
$pipe = qq{bzip2 -dc "$file" |}; |
|
} |
|
$$et{TRUST_PIPE} = 1; |
|
} |
|
} |
|
# evaluate -if expression for conditional processing |
|
if (@condition) { |
|
unless ($file eq '-' or $et->Exists($file)) { |
|
Warn "Error: File not found - $file\n"; |
|
EFile($file); |
|
FileNotFound($file); |
|
++$countBad; |
|
return; |
|
} |
|
my $result; |
|
|
|
unless ($failCondition) { |
|
# catch run time errors as well as compile errors |
|
undef $evalWarning; |
|
local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] }; |
|
|
|
my (%info, $condition); |
|
# extract information and build expression for evaluation |
|
my $opts = { Duplicates => 1, RequestTags => \@requestTags, Verbose => 0, HtmlDump => 0 }; |
|
$$opts{FastScan} = $fastCondition if defined $fastCondition; |
|
# return all tags but explicitly mention tags on command line so |
|
# requested images will generate the appropriate warnings |
|
@foundTags = ('*', @tags) if @tags; |
|
$info = $et->ImageInfo(Infile($pipe,$isWriting), \@foundTags, $opts); |
|
foreach $condition (@condition) { |
|
my $cond = $et->InsertTagValues(\@foundTags, $condition, \%info); |
|
{ |
|
# set package so eval'd functions are in Image::ExifTool namespace |
|
package Image::ExifTool; |
|
|
|
my $self = $et; |
|
#### eval "-if" condition (%info, $self) |
|
$result = eval $cond; |
|
|
|
$@ and $evalWarning = $@; |
|
} |
|
if ($evalWarning) { |
|
# fail condition if warning is issued |
|
undef $result; |
|
if ($verbose) { |
|
chomp $evalWarning; |
|
$evalWarning =~ s/ at \(eval .*//s; |
|
Warn "Condition: $evalWarning - $file\n"; |
|
} |
|
} |
|
last unless $result; |
|
} |
|
undef @foundTags if $fastCondition; # ignore if we didn't get all tags |
|
} |
|
unless ($result) { |
|
$verbose and print $vout "-------- $file (failed condition)$progStr\n"; |
|
EFile($file, 2); |
|
++$countFailed; |
|
return; |
|
} |
|
# can't make use of $info if verbose because we must reprocess |
|
# the file anyway to generate the verbose output |
|
undef $info if $verbose or defined $fastCondition; |
|
} |
|
if (defined $deleteOrig) { |
|
print $vout "======== $file$progStr\n" if defined $verbose; |
|
++$count; |
|
my $original = "${file}_original"; |
|
$et->Exists($original) or return; |
|
if ($deleteOrig) { |
|
$verbose and print $vout "Scheduled for deletion: $original\n"; |
|
push @delFiles, $original; |
|
} elsif ($et->Rename($original, $file)) { |
|
$verbose and print $vout "Restored from $original\n"; |
|
++$countGoodWr; |
|
} else { |
|
Warn "Error renaming $original\n"; |
|
EFile($file); |
|
++$countBad; |
|
} |
|
return; |
|
} |
|
++$seqFileNum; # increment our file counter |
|
|
|
my $lineCount = 0; |
|
my ($fp, $outfile, $append); |
|
if ($textOut and $verbose and not $tagOut) { |
|
($fp, $outfile, $append) = OpenOutputFile($orig); |
|
$fp or EFile($file), ++$countBad, return; |
|
# delete file if we exit prematurely (unless appending) |
|
$tmpText = $outfile unless $append; |
|
$et->Options(TextOut => $fp); |
|
} |
|
|
|
if ($isWriting) { |
|
print $vout "======== $file$progStr\n" if defined $verbose; |
|
SetImageInfo($et, $file, $orig); |
|
$info = $et->GetInfo('Warning', 'Error'); |
|
PrintErrors($et, $info, $file); |
|
# close output text file if necessary |
|
if ($outfile) { |
|
undef $tmpText; |
|
close($fp); |
|
$et->Options(TextOut => $vout); |
|
if ($info->{Error}) { |
|
$et->Unlink($outfile); # erase bad file |
|
} elsif ($append) { |
|
$appended{$outfile} = 1 unless $created{$outfile}; |
|
} else { |
|
$created{$outfile} = 1; |
|
} |
|
} |
|
return; |
|
} |
|
|
|
# extract information from this file |
|
unless ($file eq '-' or $et->Exists($file)) { |
|
Warn "Error: File not found - $file\n"; |
|
FileNotFound($file); |
|
$outfile and close($fp), undef($tmpText), $et->Unlink($outfile); |
|
EFile($file); |
|
++$countBad; |
|
return; |
|
} |
|
# print file/progress message |
|
my $o; |
|
unless ($binaryOutput or $textOut or %printFmt or $html > 1 or $csv) { |
|
if ($html) { |
|
require Image::ExifTool::HTML; |
|
my $f = Image::ExifTool::HTML::EscapeHTML($file); |
|
print "<!-- $f -->\n"; |
|
} elsif (not ($json or $xml)) { |
|
$o = \*STDOUT if ($multiFile and not $quiet) or $progress; |
|
} |
|
} |
|
$o = \*STDERR if $progress and not $o; |
|
$o and print $o "======== $file$progStr\n"; |
|
if ($info) { |
|
# get the information we wanted |
|
if (@tags and not %printFmt) { |
|
@foundTags = @tags; |
|
$info = $et->GetInfo(\@foundTags); |
|
} |
|
} else { |
|
# request specified tags unless using print format option |
|
my $oldDups = $et->Options('Duplicates'); |
|
if (%printFmt) { |
|
$et->Options(Duplicates => 1); |
|
$et->Options(RequestTags => \@requestTags); |
|
} else { |
|
@foundTags = @tags; |
|
} |
|
# extract the information |
|
$info = $et->ImageInfo(Infile($pipe), \@foundTags); |
|
$et->Options(Duplicates => $oldDups); |
|
} |
|
# all done now if we already wrote output text file (eg. verbose option) |
|
if ($fp) { |
|
if ($outfile) { |
|
$et->Options(TextOut => \*STDOUT); |
|
undef $tmpText; |
|
if ($info->{Error}) { |
|
close($fp); |
|
$et->Unlink($outfile); # erase bad file |
|
} else { |
|
++$lineCount; # output text file (likely) is not empty |
|
} |
|
} |
|
if ($info->{Error}) { |
|
Warn "Error: $info->{Error} - $file\n"; |
|
EFile($file); |
|
++$countBad; |
|
return; |
|
|