Skip to content

Latest commit

 

History

History
executable file
·
6301 lines (5559 loc) · 257 KB

exiftool

File metadata and controls

executable file
·
6301 lines (5559 loc) · 257 KB
 
Jun 21, 2016
Jun 21, 2016
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# File: exiftool
#
# Description: Read/write meta information
#
# Revisions: Nov. 12/03 - P. Harvey Created
# (See html/history.html for revision history)
#
# References: ATV - Alexander Vonk, private communication
#------------------------------------------------------------------------------
use strict;
require 5.004;
my $version = '10.20';
# add our 'lib' directory to the include list BEFORE 'use Image::ExifTool'
my $exeDir;
BEGIN {
# get exe directory
$exeDir = ($0 =~ /(.*)[\\\/]/) ? $1 : '.';
# add lib directory at start of include path
unshift @INC, "$exeDir/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 ProcessFiles($;$);
sub ScanDir($$;$);
sub FindFileWindows($$);
sub FileNotFound($);
sub PreserveTime();
sub AbsPath($);
sub SuggestedExtension($$$);
sub LoadPrintFormat($);
sub FilenameSPrintf($;$@);
sub NextUnusedFilename($;$$);
sub CreateDirectory($);
sub OpenOutputFile($;@);
sub AcceptFile($);
sub SlurpFile($$);
sub ReadStayOpen($);
sub PrintTagList($@);
sub PrintErrors($$$);
$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 @csvFiles; # list of files when reading with CSV option
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 @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 %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
my %filterExt; # lookup for filtered extensions
my %ignore; # directory names to ignore
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 $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 $comma; # flag set if we need a comma in JSON output
my $condition; # conditional processing of files
my $count; # count of files scanned
my $countBad; # count of files with errors
my $countBadCr; # count files not created due to errors
my $countBadLink; # count bad links
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 $countLink; # count number of links created
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 $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 $escapeHTML; # flag to escape printed values for html
my $evalWarning; # warning from eval
my $executeID; # -execute ID number
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)
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 $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 $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 $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; # progress count
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 $saveCount; # count the number of times we will/did call SaveNewValues()
my $scanWritable; # flag to process only writable file types
my $seqFileNum; # sequential file number used for %C
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 $utf8; # flag set if we are using UTF-8 encoding
my $validFile; # flag indicating we processed a valid file
my $verbose; # verbose setting
my $vout; # verbose output file reference (\*STDOUT or \*STDERR)
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;
# 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' );
# options requiring additional arguments
# (used only to skip over these arguments when reading -stay_open ARGFILE)
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,
'-d' => 1, '-dateformat' => 1,
'-D' => 0, # necessary to avoid matching lower-case equivalent
'-echo' => 1, '-echo1' => 1, '-echo2' => 1, '-echo3' => 1, '-echo4' => 1,
'-ext' => 1, '--ext' => 1, '-extension' => 1, '--extension' => 1,
'-fileorder' => 1,
'-geotag' => 1,
'-globaltimeshift' => 1,
'-i' => 1, '-ignore' => 1,
'-if' => 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,
'-x' => 1, '-exclude' => 1,
'-X' => 0,
);
# 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 RETURN --\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;
}
#------------------------------------------------------------------------------
# main script
#
# 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 (;;) {
@echo3 and print STDOUT join("\n", @echo3), "\n";
@echo4 and print STDERR join("\n", @echo4), "\n";
$rafStdin->Close() if $rafStdin;
undef $rafStdin;
# 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) {
# 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
}
}
$rtnVal = 0 unless defined $rtnVal;
# initialize necessary static file-scope variables
# (not done: @commonArgs, @moreArgs, $critical, $binaryStdout, $helped,
# $interrupted, $mt, $pause, $rtnVal, $stayOpen, $stayOpenBuff, $stayOpenFile)
undef @csvFiles;
undef @csvTags;
undef @delFiles;
undef @dynamicFiles;
undef @echo3;
undef @echo4;
undef @exclude;
undef @files;
undef @newValues;
undef @srcFmt;
undef @tags;
undef %appended;
undef %created;
undef %csvTags;
undef %database;
undef %filterExt;
undef %ignore;
undef %printFmt;
undef %preserveTime;
undef %setTags;
undef %setTagsList;
undef %usedFileName;
undef %utf8FileName;
undef %warnedOnce;
undef %wext;
undef $allGroup;
undef $argFormat;
undef $binaryOutput;
undef $comma;
undef $condition;
undef $csv;
undef $csvAdd;
undef $deleteOrig;
undef $disableOutput;
undef $doSetFileName;
undef $doUnzip;
undef $escapeHTML;
undef $evalWarning;
undef $executeID;
undef $fileHeader;
undef $fileTrailer;
undef $filtered;
undef $fixLen;
undef $forcePrint;
undef $joinLists;
undef $langOpt;
undef $listItem;
undef $multiFile;
undef $outOpt;
undef $preserveTime;
undef $progress;
undef $progressMax;
undef $recurse;
undef $scanWritable;
undef $showGroup;
undef $showTagID;
undef $structOpt;
undef $tagOut;
undef $textOut;
undef $textOverwrite;
undef $tmpFile;
undef $tmpText;
undef $validFile;
undef $verbose;
$count = 0;
$countBad = 0;
$countBadCr = 0;
$countBadLink = 0;
$countBadWr = 0;
$countCopyWr = 0;
$countDir = 0;
$countFailed = 0;
$countGoodCr = 0;
$countGoodWr = 0;
$countLink = 0;
$countNewDir = 0;
$countSameWr = 0;
$csvSaveCount = 0;
$filterFlag = 0;
$html = 0;
$isWriting = 0;
$json = 0;
$listSep = ', ';
$outFormat = 0;
$overwriteOrig = 0;
$progStr = '';
$quiet = 0;
$saveCount = 0;
$seqFileNum = 0;
$tabFormat = 0;
$utf8 = 1;
$vout = \*STDOUT;
$xml = 0;
# define local variables used only in this command loop
my @fileOrder; # tags to use for ordering of input files
my %excludeGrp; # hash of tags excluded by group
my $addGeotime; # automatically added geotime argument
my $allInGroup; # flag to show all tags in a group
my $doGlob; # flag set to do filename wildcard expansion
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 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] =~ /^-execute(\d*)$/i) {
if (@ARGV) {
$executeID = $1; # save -execute number for "{ready}" response
$helped = 1; # don't show help if we used -execute
$badCmd and shift, next Command;
} elsif ($stayOpen >= 2) {
ReadStayOpen(\@ARGV); # read more arguments from -stay_open file
next;
} elsif ($badCmd) {
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();
}
}
if (@nextPass) {
# process arguments which were deferred to the next pass
unshift @ARGV, @nextPass;
undef @nextPass;
++$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
if (s/^(-|\xe2\x88\x92)//) { # allow funny dashes (nroff dash bug for cut-n-paste from pod)
s/^\xe2\x88\x92/-/; # translate double-dash too
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", 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]";
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, "$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;
}
s/^\s+//; s/[\x0d\x0a]+$//s; # remove leading white space and trailing newline
# remove white space before, and single space after '=', '+=', '-=' or '<='
s/^(-[-:\w]+#?)\s*([-+<]?=) ?/$1$2/;
push @newArgs, $_ unless $_ eq '' or /^#/;
}
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;
$mt->Options($opt => (length($val) ? $val : undef));
# update $forcePrint in case MissingTagValue was changed
$forcePrint = $mt->Options('MissingTagValue') if defined $forcePrint;
next;
}
/^arg(s|format)$/i and $argFormat = 1, next;
/^b(inary)?$/i and $mt->Options(Binary => 1, NoPDFList => 1), $binaryOutput = 1, 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') {
$mt->Options(Charset => $charset);
$utf8 = ($mt->Options('Charset') eq 'UTF8');
} else {
# set internal encoding of specified metadata type
my $type = { id3 => 'ID3', iptc => 'IPTC', exif => 'EXIF', filename => 'FileName',
photoshop => 'Photoshop', quicktime => 'QuickTime' }->{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);
close(CSVFILE);
} else {
$msg = "Error opening CSV file '$csvFile'";
}
$msg and Warn("$msg\n");
$isWriting = 1;
}
$csv = 'CSV';
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;
(/^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 '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)$/i) {
$mt->Options(ExtractEmbedded => 1);
$mt->Options(Duplicates => 1);
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;
$ext =~ s/^\.//; # remove leading '.' if it exists
my $flag = /^-/ ? 0 : 1;
$filterFlag |= (0x01 << $flag);
$filterExt{uc($ext)} = $flag;
next;
}
if (/^f$/ or $a eq 'forceprint') {
$forcePrint = $mt->Options('MissingTagValue');
unless (defined $forcePrint) {
$forcePrint = '-';
$mt->Options(MissingTagValue => '-');
}
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 ($a eq 'fileorder') {
push @fileOrder, shift if @ARGV;
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/;
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;
next;
}
if ($a eq 'if') {
my $cond = shift;
defined $cond or Error("Expecting expression for -if option\n"), $badCmd=1, next;
# add to list of requested tags
push @requestTags, $cond =~ /\$\{?((?:[-\w]+:)*[-\w?*]+)/g;
if (defined $condition) {
$condition .= " and ($cond)";
} else {
$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 $utf8 = 0, $mt->Options(Charset => 'Latin'), next;
if ($a eq 'lang') {
$langOpt = (@ARGV and $ARGV[0] !~ /^-/) ? 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 = $mt->Decode($langs, 'UTF8');
$langs = Image::ExifTool::HTML::EscapeHTML($langs) if $escapeHTML;
$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;
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 ($a eq 'progress') {
$progress = 0;
$verbose = 0 unless defined $verbose;
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) {
$listSep = shift;
defined $listSep or Error("Expecting list item separator for -sep option\n"), $badCmd=1, next;
$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