rjhead 0.2.88

Sign up to get free protection for your applications and to get access to all the features.
data/ext/jhead.c ADDED
@@ -0,0 +1,1697 @@
1
+ //--------------------------------------------------------------------------
2
+ // Program to pull the information out of various types of EXIF digital
3
+ // camera files and show it in a reasonably consistent way
4
+ //
5
+ // Version 2.88
6
+ //
7
+ // Compiling under Windows:
8
+ // Make sure you have Microsoft's compiler on the path, then run make.bat
9
+ //
10
+ // Dec 1999 - Nov 2009
11
+ //
12
+ // by Matthias Wandel www.sentex.net/~mwandel
13
+ //--------------------------------------------------------------------------
14
+ #include "jhead.h"
15
+
16
+ #include <sys/stat.h>
17
+
18
+ #define JHEAD_VERSION "2.88"
19
+
20
+ // This #define turns on features that are too very specific to
21
+ // how I organize my photos. Best to ignore everything inside #ifdef MATTHIAS
22
+ //#define MATTHIAS
23
+
24
+ #ifdef _WIN32
25
+ #include <io.h>
26
+ #endif
27
+
28
+ static int FilesMatched;
29
+ static int FileSequence;
30
+
31
+ static const char * CurrentFile;
32
+
33
+ static const char * progname; // program name for error messages
34
+
35
+ //--------------------------------------------------------------------------
36
+ // Command line options flags
37
+ static int TrimExif = FALSE; // Cut off exif beyond interesting data.
38
+ static int RenameToDate = 0; // 1=rename, 2=rename all.
39
+ #ifdef _WIN32
40
+ static int RenameAssociatedFiles = FALSE;
41
+ #endif
42
+ static char * strftime_args = NULL; // Format for new file name.
43
+ static int Exif2FileTime = FALSE;
44
+ static int DoModify = FALSE;
45
+ static int DoReadAction = FALSE;
46
+ int ShowTags = FALSE; // Do not show raw by default.
47
+ static int Quiet = FALSE; // Be quiet on success (like unix programs)
48
+ int DumpExifMap = FALSE;
49
+ static int ShowConcise = FALSE;
50
+ static int CreateExifSection = FALSE;
51
+ static char * ApplyCommand = NULL; // Apply this command to all images.
52
+ static char * FilterModel = NULL;
53
+ static int ExifOnly = FALSE;
54
+ static int PortraitOnly = FALSE;
55
+ static time_t ExifTimeAdjust = 0; // Timezone adjust
56
+ static time_t ExifTimeSet = 0; // Set exif time to a value.
57
+ static char DateSet[11];
58
+ static unsigned DateSetChars = 0;
59
+ static unsigned FileTimeToExif = FALSE;
60
+
61
+ static int DeleteComments = FALSE;
62
+ static int DeleteExif = FALSE;
63
+ static int DeleteIptc = FALSE;
64
+ static int DeleteXmp = FALSE;
65
+ static int DeleteUnknown = FALSE;
66
+ static char * ThumbSaveName = NULL; // If not NULL, use this string to make up
67
+ // the filename to store the thumbnail to.
68
+
69
+ static char * ThumbInsertName = NULL; // If not NULL, use this string to make up
70
+ // the filename to retrieve the thumbnail from.
71
+
72
+ static int RegenThumbnail = FALSE;
73
+
74
+ static char * ExifXferScrFile = NULL;// Extract Exif header from this file, and
75
+ // put it into the Jpegs processed.
76
+
77
+ static int EditComment = FALSE; // Invoke an editor for editing the comment
78
+ static int SupressNonFatalErrors = FALSE; // Wether or not to pint warnings on recoverable errors
79
+
80
+ static char * CommentSavefileName = NULL; // Save comment to this file.
81
+ static char * CommentInsertfileName = NULL; // Insert comment from this file.
82
+ static char * CommentInsertLiteral = NULL; // Insert this comment (from command line)
83
+
84
+ static int AutoRotate = FALSE;
85
+ static int ZeroRotateTagOnly = FALSE;
86
+
87
+ static int ShowFileInfo = TRUE; // Indicates to show standard file info
88
+ // (file name, file size, file date)
89
+
90
+
91
+ #ifdef MATTHIAS
92
+ // This #ifdef to take out less than elegant stuff for editing
93
+ // the comments in a JPEG. The programs rdjpgcom and wrjpgcom
94
+ // included with Linux distributions do a better job.
95
+
96
+ static char * AddComment = NULL; // Add this tag.
97
+ static char * RemComment = NULL; // Remove this tag
98
+ static int AutoResize = FALSE;
99
+ #endif // MATTHIAS
100
+
101
+ //--------------------------------------------------------------------------
102
+ // Error exit handler
103
+ //--------------------------------------------------------------------------
104
+ void ErrFatal(char * msg)
105
+ {
106
+ fprintf(stderr,"\nError : %s\n", msg);
107
+ if (CurrentFile) fprintf(stderr,"in file '%s'\n",CurrentFile);
108
+ exit(EXIT_FAILURE);
109
+ }
110
+
111
+ //--------------------------------------------------------------------------
112
+ // Report non fatal errors. Now that microsoft.net modifies exif headers,
113
+ // there's corrupted ones, and there could be more in the future.
114
+ //--------------------------------------------------------------------------
115
+ void ErrNonfatal(char * msg, int a1, int a2)
116
+ {
117
+ if (SupressNonFatalErrors) return;
118
+
119
+ fprintf(stderr,"\nNonfatal Error : ");
120
+ if (CurrentFile) fprintf(stderr,"'%s' ",CurrentFile);
121
+ fprintf(stderr, msg, a1, a2);
122
+ fprintf(stderr, "\n");
123
+ }
124
+
125
+
126
+ //--------------------------------------------------------------------------
127
+ // Invoke an editor for editing a string.
128
+ //--------------------------------------------------------------------------
129
+ static int FileEditComment(char * TempFileName, char * Comment, int CommentSize)
130
+ {
131
+ FILE * file;
132
+ int a;
133
+ char QuotedPath[PATH_MAX+10];
134
+
135
+ file = fopen(TempFileName, "w");
136
+ if (file == NULL){
137
+ fprintf(stderr, "Can't create file '%s'\n",TempFileName);
138
+ ErrFatal("could not create temporary file");
139
+ }
140
+ fwrite(Comment, CommentSize, 1, file);
141
+
142
+ fclose(file);
143
+
144
+ fflush(stdout); // So logs are contiguous.
145
+
146
+ {
147
+ char * Editor;
148
+ Editor = getenv("EDITOR");
149
+ if (Editor == NULL){
150
+ #ifdef _WIN32
151
+ Editor = "notepad";
152
+ #else
153
+ Editor = "vi";
154
+ #endif
155
+ }
156
+ if (strlen(Editor) > PATH_MAX) ErrFatal("env too long");
157
+
158
+ sprintf(QuotedPath, "%s \"%s\"",Editor, TempFileName);
159
+ a = system(QuotedPath);
160
+ }
161
+
162
+ if (a != 0){
163
+ perror("Editor failed to launch");
164
+ exit(-1);
165
+ }
166
+
167
+ file = fopen(TempFileName, "r");
168
+ if (file == NULL){
169
+ ErrFatal("could not open temp file for read");
170
+ }
171
+
172
+ // Read the file back in.
173
+ CommentSize = fread(Comment, 1, 999, file);
174
+
175
+ fclose(file);
176
+
177
+ unlink(TempFileName);
178
+
179
+ return CommentSize;
180
+ }
181
+
182
+ #ifdef MATTHIAS
183
+ //--------------------------------------------------------------------------
184
+ // Modify one of the lines in the comment field.
185
+ // This very specific to the photo album program stuff.
186
+ //--------------------------------------------------------------------------
187
+ static char KnownTags[][10] = {"date", "desc","scan_date","author",
188
+ "keyword","videograb",
189
+ "show_raw","panorama","titlepix",""};
190
+
191
+ static int ModifyDescriptComment(char * OutComment, char * SrcComment)
192
+ {
193
+ char Line[500];
194
+ int Len;
195
+ int a,i;
196
+ unsigned l;
197
+ int HasScandate = FALSE;
198
+ int TagExists = FALSE;
199
+ int Modified = FALSE;
200
+ Len = 0;
201
+
202
+ OutComment[0] = 0;
203
+
204
+
205
+ for (i=0;;i++){
206
+ if (SrcComment[i] == '\r' || SrcComment[i] == '\n' || SrcComment[i] == 0 || Len >= 199){
207
+ // Process the line.
208
+ if (Len > 0){
209
+ Line[Len] = 0;
210
+ //printf("Line: '%s'\n",Line);
211
+ for (a=0;;a++){
212
+ l = strlen(KnownTags[a]);
213
+ if (!l){
214
+ // Unknown tag. Discard it.
215
+ printf("Error: Unknown tag '%s'\n", Line); // Deletes the tag.
216
+ Modified = TRUE;
217
+ break;
218
+ }
219
+ if (memcmp(Line, KnownTags[a], l) == 0){
220
+ if (Line[l] == ' ' || Line[l] == '=' || Line[l] == 0){
221
+ // Its a good tag.
222
+ if (Line[l] == ' ') Line[l] = '='; // Use equal sign for clarity.
223
+ if (a == 2) break; // Delete 'orig_path' tag.
224
+ if (a == 3) HasScandate = TRUE;
225
+ if (RemComment){
226
+ if (strlen(RemComment) == l){
227
+ if (!memcmp(Line, RemComment, l)){
228
+ Modified = TRUE;
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ if (AddComment){
234
+ // Overwrite old comment of same tag with new one.
235
+ if (!memcmp(Line, AddComment, l+1)){
236
+ TagExists = TRUE;
237
+ strncpy(Line, AddComment, sizeof(Line));
238
+ Modified = TRUE;
239
+ }
240
+ }
241
+ strncat(OutComment, Line, MAX_COMMENT_SIZE-5-strlen(OutComment));
242
+ strcat(OutComment, "\n");
243
+ break;
244
+ }
245
+ }
246
+ }
247
+ }
248
+ Line[Len = 0] = 0;
249
+ if (SrcComment[i] == 0) break;
250
+ }else{
251
+ Line[Len++] = SrcComment[i];
252
+ }
253
+ }
254
+
255
+ if (AddComment && TagExists == FALSE){
256
+ strncat(OutComment, AddComment, MAX_COMMENT_SIZE-5-strlen(OutComment));
257
+ strcat(OutComment, "\n");
258
+ Modified = TRUE;
259
+ }
260
+
261
+ if (!HasScandate && !ImageInfo.DateTime[0]){
262
+ // Scan date is not in the file yet, and it doesn't have one built in. Add it.
263
+ char Temp[30];
264
+ sprintf(Temp, "scan_date=%s", ctime(&ImageInfo.FileDateTime));
265
+ strncat(OutComment, Temp, MAX_COMMENT_SIZE-5-strlen(OutComment));
266
+ Modified = TRUE;
267
+ }
268
+ return Modified;
269
+ }
270
+ //--------------------------------------------------------------------------
271
+ // Automatic make smaller command stuff
272
+ //--------------------------------------------------------------------------
273
+ static int AutoResizeCmdStuff(void)
274
+ {
275
+ static char CommandString[PATH_MAX+1];
276
+ double scale;
277
+
278
+ ApplyCommand = CommandString;
279
+
280
+ if (ImageInfo.Height <= 1280 && ImageInfo.Width <= 1280){
281
+ printf("not resizing %dx%x '%s'\n",ImageInfo.Height, ImageInfo.Width, ImageInfo.FileName);
282
+ return FALSE;
283
+ }
284
+
285
+ scale = 1024.0 / ImageInfo.Height;
286
+ if (1024.0 / ImageInfo.Width < scale) scale = 1024.0 / ImageInfo.Width;
287
+
288
+ if (scale < 0.5) scale = 0.5; // Don't scale down by more than a factor of two.
289
+
290
+ sprintf(CommandString, "mogrify -geometry %dx%d -quality 85 &i",(int)(ImageInfo.Width*scale), (int)(ImageInfo.Height*scale));
291
+ return TRUE;
292
+ }
293
+
294
+
295
+ #endif // MATTHIAS
296
+
297
+
298
+ //--------------------------------------------------------------------------
299
+ // Escape an argument such that it is interpreted literally by the shell
300
+ // (returns the number of written characters)
301
+ //--------------------------------------------------------------------------
302
+ static int shellescape(char* to, const char* from)
303
+ {
304
+ int i, j;
305
+ i = j = 0;
306
+
307
+ // Enclosing characters in double quotes preserves the literal value of
308
+ // all characters within the quotes, with the exception of $, `, and \.
309
+ to[j++] = '"';
310
+ while(from[i])
311
+ {
312
+ #ifdef _WIN32
313
+ // Under WIN32, there isn't really anything dangerous you can do with
314
+ // escape characters, plus windows users aren't as sercurity paranoid.
315
+ // Hence, no need to do fancy escaping.
316
+ to[j++] = from[i++];
317
+ #else
318
+ switch(from[i]) {
319
+ case '"':
320
+ case '$':
321
+ case '`':
322
+ case '\\':
323
+ to[j++] = '\\';
324
+ // Fallthru...
325
+ default:
326
+ to[j++] = from[i++];
327
+ }
328
+ #endif
329
+ if (j >= PATH_MAX) ErrFatal("max path exceeded");
330
+ }
331
+ to[j++] = '"';
332
+ return j;
333
+ }
334
+
335
+
336
+ //--------------------------------------------------------------------------
337
+ // Apply the specified command to the JPEG file.
338
+ //--------------------------------------------------------------------------
339
+ static void DoCommand(const char * FileName, int ShowIt)
340
+ {
341
+ int a,e;
342
+ char ExecString[PATH_MAX*3];
343
+ char TempName[PATH_MAX+10];
344
+ int TempUsed = FALSE;
345
+
346
+ e = 0;
347
+
348
+ // Generate an unused temporary file name in the destination directory
349
+ // (a is the number of characters to copy from FileName)
350
+ a = strlen(FileName)-1;
351
+ while(a > 0 && FileName[a-1] != SLASH) a--;
352
+ memcpy(TempName, FileName, a);
353
+ strcpy(TempName+a, "XXXXXX");
354
+
355
+ // Note: Compiler will warn about mkstemp. but I need a filename, not a file.
356
+ // I could just then get the fiel name from what mkstemp made, and pass that
357
+ // to the executable, but that would make for the exact same vulnerability
358
+ // as mktemp - that is, that between getting the random name, and making the file
359
+ // some other program could snatch that exact same name!
360
+ // also, not all pltforms support mkstemp.
361
+ mktemp(TempName);
362
+
363
+
364
+ if(!TempName[0]) {
365
+ ErrFatal("Cannot find available temporary file name");
366
+ }
367
+
368
+
369
+ // Build the exec string. &i and &o in the exec string get replaced by input and output files.
370
+ for (a=0;;a++){
371
+ if (ApplyCommand[a] == '&'){
372
+ if (ApplyCommand[a+1] == 'i'){
373
+ // Input file.
374
+ e += shellescape(ExecString+e, FileName);
375
+ a += 1;
376
+ continue;
377
+ }
378
+ if (ApplyCommand[a+1] == 'o'){
379
+ // Needs an output file distinct from the input file.
380
+ e += shellescape(ExecString+e, TempName);
381
+ a += 1;
382
+ TempUsed = TRUE;
383
+ continue;
384
+ }
385
+ }
386
+ ExecString[e++] = ApplyCommand[a];
387
+ if (ApplyCommand[a] == 0) break;
388
+ }
389
+
390
+ if (ShowIt) printf("Cmd:%s\n",ExecString);
391
+
392
+ errno = 0;
393
+ a = system(ExecString);
394
+
395
+ if (a || errno){
396
+ // A command can however fail without errno getting set or system returning an error.
397
+ if (errno) perror("system");
398
+ ErrFatal("Problem executing specified command");
399
+ }
400
+
401
+ if (TempUsed){
402
+ // Don't delete original file until we know a new one was created by the command.
403
+ struct stat dummy;
404
+ if (stat(TempName, &dummy) == 0){
405
+ unlink(FileName);
406
+ rename(TempName, FileName);
407
+ }else{
408
+ ErrFatal("specified command did not produce expected output file");
409
+ }
410
+ }
411
+ }
412
+
413
+ //--------------------------------------------------------------------------
414
+ // check if this file should be skipped based on contents.
415
+ //--------------------------------------------------------------------------
416
+ static int CheckFileSkip(void)
417
+ {
418
+ // I sometimes add code here to only process images based on certain
419
+ // criteria - for example, only to convert non progressive Jpegs to progressives, etc..
420
+
421
+ if (FilterModel){
422
+ // Filtering processing by camera model.
423
+ // This feature is useful when pictures from multiple cameras are colated,
424
+ // the its found that one of the cameras has the time set incorrectly.
425
+ if (strstr(ImageInfo.CameraModel, FilterModel) == NULL){
426
+ // Skip.
427
+ return TRUE;
428
+ }
429
+ }
430
+
431
+ if (ExifOnly){
432
+ // Filtering by EXIF only. Skip all files that have no Exif.
433
+ if (FindSection(M_EXIF) == NULL){
434
+ return TRUE;
435
+ }
436
+ }
437
+
438
+ if (PortraitOnly == 1){
439
+ if (ImageInfo.Width > ImageInfo.Height) return TRUE;
440
+ }
441
+
442
+ if (PortraitOnly == -1){
443
+ if (ImageInfo.Width < ImageInfo.Height) return TRUE;
444
+ }
445
+
446
+ return FALSE;
447
+ }
448
+
449
+ //--------------------------------------------------------------------------
450
+ // Subsititute original name for '&i' if present in specified name.
451
+ // This to allow specifying relative names when manipulating multiple files.
452
+ //--------------------------------------------------------------------------
453
+ static void RelativeName(char * OutFileName, const char * NamePattern, const char * OrigName)
454
+ {
455
+ // If the filename contains substring "&i", then substitute the
456
+ // filename for that. This gives flexibility in terms of processing
457
+ // multiple files at a time.
458
+ char * Subst;
459
+ Subst = strstr(NamePattern, "&i");
460
+ if (Subst){
461
+ strncpy(OutFileName, NamePattern, Subst-NamePattern);
462
+ OutFileName[Subst-NamePattern] = 0;
463
+ strncat(OutFileName, OrigName, PATH_MAX);
464
+ strncat(OutFileName, Subst+2, PATH_MAX);
465
+ }else{
466
+ strncpy(OutFileName, NamePattern, PATH_MAX);
467
+ }
468
+ }
469
+
470
+
471
+ #ifdef _WIN32
472
+ //--------------------------------------------------------------------------
473
+ // Rename associated files
474
+ //--------------------------------------------------------------------------
475
+ void RenameAssociated(const char * FileName, char * NewBaseName)
476
+ {
477
+ int a;
478
+ int PathLen;
479
+ int ExtPos;
480
+ char FilePattern[_MAX_PATH+1];
481
+ char NewName[_MAX_PATH+1];
482
+ struct _finddata_t finddata;
483
+ long find_handle;
484
+
485
+ for(ExtPos = strlen(FileName);FileName[ExtPos-1] != '.';){
486
+ if (--ExtPos == 0) return; // No extension!
487
+ }
488
+
489
+ memcpy(FilePattern, FileName, ExtPos);
490
+ FilePattern[ExtPos] = '*';
491
+ FilePattern[ExtPos+1] = '\0';
492
+
493
+ for(PathLen = strlen(FileName);FileName[PathLen-1] != SLASH;){
494
+ if (--PathLen == 0) break;
495
+ }
496
+
497
+ find_handle = _findfirst(FilePattern, &finddata);
498
+
499
+ for (;;){
500
+ if (find_handle == -1) break;
501
+
502
+ // Eliminate the obvious patterns.
503
+ if (!memcmp(finddata.name, ".",2)) goto next_file;
504
+ if (!memcmp(finddata.name, "..",3)) goto next_file;
505
+ if (finddata.attrib & _A_SUBDIR) goto next_file;
506
+
507
+ strncpy(FilePattern+PathLen, finddata.name, PATH_MAX-PathLen); // full name with path
508
+
509
+ strcpy(NewName, NewBaseName);
510
+ for(a = strlen(finddata.name);finddata.name[a] != '.';){
511
+ if (--a == 0) goto next_file;
512
+ }
513
+ strncat(NewName, finddata.name+a, _MAX_PATH-strlen(NewName)); // add extension to new name
514
+
515
+ if (rename(FilePattern, NewName) == 0){
516
+ if (!Quiet){
517
+ printf("%s --> %s\n",FilePattern, NewName);
518
+ }
519
+ }
520
+
521
+ next_file:
522
+ if (_findnext(find_handle, &finddata) != 0) break;
523
+ }
524
+ _findclose(find_handle);
525
+ }
526
+ #endif
527
+
528
+ //--------------------------------------------------------------------------
529
+ // Handle renaming of files by date.
530
+ //--------------------------------------------------------------------------
531
+ static void DoFileRenaming(const char * FileName)
532
+ {
533
+ int NumAlpha = 0;
534
+ int NumDigit = 0;
535
+ int PrefixPart = 0; // Where the actual filename starts.
536
+ int ExtensionPart; // Where the file extension starts.
537
+ int a;
538
+ struct tm tm;
539
+ char NewBaseName[PATH_MAX*2];
540
+ int AddLetter = 0;
541
+ char NewName[PATH_MAX+2];
542
+
543
+ ExtensionPart = strlen(FileName);
544
+ for (a=0;FileName[a];a++){
545
+ if (FileName[a] == SLASH){
546
+ // Don't count path component.
547
+ NumAlpha = 0;
548
+ NumDigit = 0;
549
+ PrefixPart = a+1;
550
+ }
551
+
552
+ if (FileName[a] == '.') ExtensionPart = a; // Remember where extension starts.
553
+
554
+ if (isalpha(FileName[a])) NumAlpha += 1; // Tally up alpha vs. digits to judge wether to rename.
555
+ if (isdigit(FileName[a])) NumDigit += 1;
556
+ }
557
+
558
+ if (RenameToDate <= 1){
559
+ // If naming isn't forced, ensure name is mostly digits, or leave it alone.
560
+ if (NumAlpha > 8 || NumDigit < 4){
561
+ return;
562
+ }
563
+ }
564
+
565
+ if (!Exif2tm(&tm, ImageInfo.DateTime)){
566
+ printf("File '%s' contains no exif date stamp. Using file date\n",FileName);
567
+ // Use file date/time instead.
568
+ tm = *localtime(&ImageInfo.FileDateTime);
569
+ }
570
+
571
+
572
+ strncpy(NewBaseName, FileName, PATH_MAX); // Get path component of name.
573
+
574
+ if (strftime_args){
575
+ // Complicated scheme for flexibility. Just pass the args to strftime.
576
+ time_t UnixTime;
577
+
578
+ char *s;
579
+ char pattern[PATH_MAX+20];
580
+ int n = ExtensionPart - PrefixPart;
581
+
582
+ // Call mktime to get weekday and such filled in.
583
+ UnixTime = mktime(&tm);
584
+ if ((int)UnixTime == -1){
585
+ printf("Could not convert %s to unix time",ImageInfo.DateTime);
586
+ return;
587
+ }
588
+
589
+ // Substitute "%f" for the original name (minus path & extension)
590
+ pattern[PATH_MAX-1]=0;
591
+ strncpy(pattern, strftime_args, PATH_MAX-1);
592
+ while ((s = strstr(pattern, "%f")) && strlen(pattern) + n < PATH_MAX-1){
593
+ memmove(s + n, s + 2, strlen(s+2) + 1);
594
+ memmove(s, FileName + PrefixPart, n);
595
+ }
596
+
597
+ {
598
+ // Sequential number renaming part.
599
+ // '%i' type pattern becomes sequence number.
600
+ int ppos = -1;
601
+ for (a=0;pattern[a];a++){
602
+ if (pattern[a] == '%'){
603
+ ppos = a;
604
+ }else if (pattern[a] == 'i'){
605
+ if (ppos >= 0 && a<ppos+4){
606
+ // Replace this part with a number.
607
+ char pat[8], num[16];
608
+ int l,nl;
609
+ memcpy(pat, pattern+ppos, 4);
610
+ pat[a-ppos] = 'd'; // Replace 'i' with 'd' for '%d'
611
+ pat[a-ppos+1] = '\0';
612
+ sprintf(num, pat, FileSequence); // let printf do the number formatting.
613
+ nl = strlen(num);
614
+ l = strlen(pattern+a+1);
615
+ if (ppos+nl+l+1 >= PATH_MAX) ErrFatal("str overflow");
616
+ memmove(pattern+ppos+nl, pattern+a+1, l+1);
617
+ memcpy(pattern+ppos, num, nl);
618
+ break;
619
+ }
620
+ }else if (!isdigit(pattern[a])){
621
+ ppos = -1;
622
+ }
623
+ }
624
+ }
625
+ strftime(NewName, PATH_MAX, pattern, &tm);
626
+ }else{
627
+ // My favourite scheme.
628
+ sprintf(NewName, "%02d%02d-%02d%02d%02d",
629
+ tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
630
+ }
631
+
632
+ NewBaseName[PrefixPart] = 0;
633
+ CatPath(NewBaseName, NewName);
634
+
635
+ AddLetter = isdigit(NewBaseName[strlen(NewBaseName)-1]);
636
+ for (a=0;;a++){
637
+ char NewName[PATH_MAX+10];
638
+ char NameExtra[3];
639
+ struct stat dummy;
640
+
641
+ if (a){
642
+ // Generate a suffix for the file name if previous choice of names is taken.
643
+ // depending on wether the name ends in a letter or digit, pick the opposite to separate
644
+ // it. This to avoid using a separator character - this because any good separator
645
+ // is before the '.' in ascii, and so sorting the names would put the later name before
646
+ // the name without suffix, causing the pictures to more likely be out of order.
647
+ if (AddLetter){
648
+ NameExtra[0] = (char)('a'-1+a); // Try a,b,c,d... for suffix if it ends in a number.
649
+ }else{
650
+ NameExtra[0] = (char)('0'-1+a); // Try 0,1,2,3... for suffix if it ends in a latter.
651
+ }
652
+ NameExtra[1] = 0;
653
+ }else{
654
+ NameExtra[0] = 0;
655
+ }
656
+
657
+ sprintf(NewName, "%s%s.jpg", NewBaseName, NameExtra);
658
+
659
+ if (!strcmp(FileName, NewName)) break; // Skip if its already this name.
660
+
661
+ if (!EnsurePathExists(NewBaseName)){
662
+ break;
663
+ }
664
+
665
+
666
+ if (stat(NewName, &dummy)){
667
+ // This name does not pre-exist.
668
+ if (rename(FileName, NewName) == 0){
669
+ printf("%s --> %s\n",FileName, NewName);
670
+ #ifdef _WIN32
671
+ if (RenameAssociatedFiles){
672
+ sprintf(NewName, "%s%s", NewBaseName, NameExtra);
673
+ RenameAssociated(FileName, NewName);
674
+ }
675
+ #endif
676
+ }else{
677
+ printf("Error: Couldn't rename '%s' to '%s'\n",FileName, NewName);
678
+ }
679
+ break;
680
+
681
+ }
682
+
683
+ if (a > 25 || (!AddLetter && a > 9)){
684
+ printf("Possible new names for for '%s' already exist\n",FileName);
685
+ break;
686
+ }
687
+ }
688
+ }
689
+
690
+ //--------------------------------------------------------------------------
691
+ // Rotate the image and its thumbnail
692
+ //--------------------------------------------------------------------------
693
+ static int DoAutoRotate(const char * FileName)
694
+ {
695
+ if (ImageInfo.Orientation >= 2 && ImageInfo.Orientation <= 8){
696
+ const char * Argument;
697
+ Argument = ClearOrientation();
698
+
699
+ if (!ZeroRotateTagOnly){
700
+ char RotateCommand[PATH_MAX*2+50];
701
+ if (Argument == NULL){
702
+ ErrFatal("Orientation screwup");
703
+ }
704
+
705
+ sprintf(RotateCommand, "jpegtran -trim -%s -outfile &o &i", Argument);
706
+ ApplyCommand = RotateCommand;
707
+ DoCommand(FileName, FALSE);
708
+ ApplyCommand = NULL;
709
+
710
+ // Now rotate the thumbnail, if there is one.
711
+ if (ImageInfo.ThumbnailOffset &&
712
+ ImageInfo.ThumbnailSize &&
713
+ ImageInfo.ThumbnailAtEnd){
714
+ // Must have a thumbnail that exists and is modifieable.
715
+
716
+ char ThumbTempName_in[PATH_MAX+5];
717
+ char ThumbTempName_out[PATH_MAX+5];
718
+
719
+ strcpy(ThumbTempName_in, FileName);
720
+ strcat(ThumbTempName_in, ".thi");
721
+ strcpy(ThumbTempName_out, FileName);
722
+ strcat(ThumbTempName_out, ".tho");
723
+ SaveThumbnail(ThumbTempName_in);
724
+ sprintf(RotateCommand,"jpegtran -trim -%s -outfile \"%s\" \"%s\"",
725
+ Argument, ThumbTempName_out, ThumbTempName_in);
726
+
727
+ if (system(RotateCommand) == 0){
728
+ // Put the thumbnail back in the header
729
+ ReplaceThumbnail(ThumbTempName_out);
730
+ }
731
+
732
+ unlink(ThumbTempName_in);
733
+ unlink(ThumbTempName_out);
734
+ }
735
+ }
736
+ return TRUE;
737
+ }
738
+ return FALSE;
739
+ }
740
+
741
+ //--------------------------------------------------------------------------
742
+ // Regenerate the thumbnail using mogrify
743
+ //--------------------------------------------------------------------------
744
+ static int RegenerateThumbnail(const char * FileName)
745
+ {
746
+ char ThumbnailGenCommand[PATH_MAX*2+50];
747
+ if (ImageInfo.ThumbnailOffset == 0 || ImageInfo.ThumbnailAtEnd == FALSE){
748
+ // There is no thumbnail, or the thumbnail is not at the end.
749
+ return FALSE;
750
+ }
751
+
752
+ sprintf(ThumbnailGenCommand, "mogrify -thumbnail %dx%d \"%s\"",
753
+ RegenThumbnail, RegenThumbnail, FileName);
754
+
755
+ if (system(ThumbnailGenCommand) == 0){
756
+ // Put the thumbnail back in the header
757
+ return ReplaceThumbnail(FileName);
758
+ }else{
759
+ ErrFatal("Unable to run 'mogrify' command");
760
+ return FALSE;
761
+ }
762
+ }
763
+
764
+ //--------------------------------------------------------------------------
765
+ // Set file time as exif time.
766
+ //--------------------------------------------------------------------------
767
+ void FileTimeAsString(char * TimeStr)
768
+ {
769
+ struct tm ts;
770
+ ts = *localtime(&ImageInfo.FileDateTime);
771
+ strftime(TimeStr, 20, "%Y:%m:%d %H:%M:%S", &ts);
772
+ }
773
+
774
+ //--------------------------------------------------------------------------
775
+ // Do selected operations to one file at a time.
776
+ //--------------------------------------------------------------------------
777
+ void ProcessFile(const char * FileName)
778
+ {
779
+ int Modified = FALSE;
780
+ ReadMode_t ReadMode;
781
+
782
+ if (strlen(FileName) >= PATH_MAX-1){
783
+ // Protect against buffer overruns in strcpy / strcat's on filename
784
+ ErrFatal("filename too long");
785
+ }
786
+
787
+ ReadMode = READ_METADATA;
788
+ CurrentFile = FileName;
789
+ FilesMatched = 1;
790
+
791
+ ResetJpgfile();
792
+
793
+ // Start with an empty image information structure.
794
+ memset(&ImageInfo, 0, sizeof(ImageInfo));
795
+ ImageInfo.FlashUsed = -1;
796
+ ImageInfo.MeteringMode = -1;
797
+ ImageInfo.Whitebalance = -1;
798
+
799
+ // Store file date/time.
800
+ {
801
+ struct stat st;
802
+ if (stat(FileName, &st) >= 0){
803
+ ImageInfo.FileDateTime = st.st_mtime;
804
+ ImageInfo.FileSize = st.st_size;
805
+ }else{
806
+ ErrFatal("No such file");
807
+ }
808
+ }
809
+
810
+ if (DoModify || RenameToDate || Exif2FileTime){
811
+ if (access(FileName, 2 /*W_OK*/)){
812
+ printf("Skipping readonly file '%s'\n",FileName);
813
+ return;
814
+ }
815
+ }
816
+
817
+ strncpy(ImageInfo.FileName, FileName, PATH_MAX);
818
+
819
+
820
+ if (ApplyCommand || AutoRotate){
821
+ // Applying a command is special - the headers from the file have to be
822
+ // pre-read, then the command executed, and then the image part of the file read.
823
+
824
+ if (!ReadJpegFile(FileName, READ_METADATA)) return;
825
+
826
+ #ifdef MATTHIAS
827
+ if (AutoResize){
828
+ // Automatic resize computation - to customize for each run...
829
+ if (AutoResizeCmdStuff() == 0){
830
+ DiscardData();
831
+ return;
832
+ }
833
+ }
834
+ #endif // MATTHIAS
835
+
836
+
837
+ if (CheckFileSkip()){
838
+ DiscardData();
839
+ return;
840
+ }
841
+
842
+ DiscardAllButExif();
843
+
844
+ if (AutoRotate){
845
+ if (DoAutoRotate(FileName)){
846
+ Modified = TRUE;
847
+ }
848
+ }else{
849
+ struct stat dummy;
850
+ DoCommand(FileName, Quiet ? FALSE : TRUE);
851
+
852
+ if (stat(FileName, &dummy)){
853
+ // The file is not there anymore. Perhaps the command
854
+ // was a delete or a move. So we are all done.
855
+ return;
856
+ }
857
+ Modified = TRUE;
858
+ }
859
+ ReadMode = READ_IMAGE; // Don't re-read exif section again on next read.
860
+
861
+ }else if (ExifXferScrFile){
862
+ char RelativeExifName[PATH_MAX+1];
863
+
864
+ // Make a relative name.
865
+ RelativeName(RelativeExifName, ExifXferScrFile, FileName);
866
+
867
+ if(!ReadJpegFile(RelativeExifName, READ_METADATA)) return;
868
+
869
+ DiscardAllButExif(); // Don't re-read exif section again on next read.
870
+
871
+ Modified = TRUE;
872
+ ReadMode = READ_IMAGE;
873
+ }
874
+
875
+ if (DoModify){
876
+ ReadMode |= READ_IMAGE;
877
+ }
878
+
879
+ if (!ReadJpegFile(FileName, ReadMode)) return;
880
+
881
+ if (CheckFileSkip()){
882
+ DiscardData();
883
+ return;
884
+ }
885
+
886
+ FileSequence += 1; // Count files processed.
887
+
888
+ if (ShowConcise){
889
+ ShowConciseImageInfo();
890
+ }else{
891
+ if (!(DoModify || DoReadAction) || ShowTags){
892
+ ShowImageInfo(ShowFileInfo);
893
+
894
+ {
895
+ // if IPTC section is present, show it also.
896
+ Section_t * IptcSection;
897
+ IptcSection = FindSection(M_IPTC);
898
+
899
+ if (IptcSection){
900
+ show_IPTC(IptcSection->Data, IptcSection->Size);
901
+ }
902
+ }
903
+ printf("\n");
904
+ }
905
+ }
906
+
907
+ if (ThumbSaveName){
908
+ char OutFileName[PATH_MAX+1];
909
+ // Make a relative name.
910
+ RelativeName(OutFileName, ThumbSaveName, FileName);
911
+
912
+ if (SaveThumbnail(OutFileName)){
913
+ printf("Created: '%s'\n", OutFileName);
914
+ }
915
+ }
916
+
917
+ if (CreateExifSection){
918
+ // Make a new minimal exif section
919
+ create_EXIF();
920
+ Modified = TRUE;
921
+ }
922
+
923
+ if (RegenThumbnail){
924
+ if (RegenerateThumbnail(FileName)){
925
+ Modified = TRUE;
926
+ }
927
+ }
928
+
929
+ if (ThumbInsertName){
930
+ char ThumbFileName[PATH_MAX+1];
931
+ // Make a relative name.
932
+ RelativeName(ThumbFileName, ThumbInsertName, FileName);
933
+
934
+ if (ReplaceThumbnail(ThumbFileName)){
935
+ Modified = TRUE;
936
+ }
937
+ }else if (TrimExif){
938
+ // Deleting thumbnail is just replacing it with a null thumbnail.
939
+ if (ReplaceThumbnail(NULL)){
940
+ Modified = TRUE;
941
+ }
942
+ }
943
+
944
+ if (
945
+ #ifdef MATTHIAS
946
+ AddComment || RemComment ||
947
+ #endif
948
+ EditComment || CommentInsertfileName || CommentInsertLiteral){
949
+
950
+ Section_t * CommentSec;
951
+ char Comment[MAX_COMMENT_SIZE+1];
952
+ int CommentSize;
953
+
954
+ CommentSec = FindSection(M_COM);
955
+
956
+ if (CommentSec == NULL){
957
+ unsigned char * DummyData;
958
+
959
+ DummyData = (uchar *) malloc(3);
960
+ DummyData[0] = 0;
961
+ DummyData[1] = 2;
962
+ DummyData[2] = 0;
963
+ CommentSec = CreateSection(M_COM, DummyData, 2);
964
+ }
965
+
966
+ CommentSize = CommentSec->Size-2;
967
+ if (CommentSize > MAX_COMMENT_SIZE){
968
+ fprintf(stderr, "Truncating comment at %d chars\n",MAX_COMMENT_SIZE);
969
+ CommentSize = MAX_COMMENT_SIZE;
970
+ }
971
+
972
+ if (CommentInsertfileName){
973
+ // Read a new comment section from file.
974
+ char CommentFileName[PATH_MAX+1];
975
+ FILE * CommentFile;
976
+
977
+ // Make a relative name.
978
+ RelativeName(CommentFileName, CommentInsertfileName, FileName);
979
+
980
+ CommentFile = fopen(CommentFileName,"r");
981
+ if (CommentFile == NULL){
982
+ printf("Could not open '%s'\n",CommentFileName);
983
+ }else{
984
+ // Read it in.
985
+ // Replace the section.
986
+ CommentSize = fread(Comment, 1, 999, CommentFile);
987
+ fclose(CommentFile);
988
+ if (CommentSize < 0) CommentSize = 0;
989
+ }
990
+ }else if (CommentInsertLiteral){
991
+ strncpy(Comment, CommentInsertLiteral, MAX_COMMENT_SIZE);
992
+ CommentSize = strlen(Comment);
993
+ }else{
994
+ #ifdef MATTHIAS
995
+ char CommentZt[MAX_COMMENT_SIZE+1];
996
+ memcpy(CommentZt, (char *)CommentSec->Data+2, CommentSize);
997
+ CommentZt[CommentSize] = '\0';
998
+ if (ModifyDescriptComment(Comment, CommentZt)){
999
+ Modified = TRUE;
1000
+ CommentSize = strlen(Comment);
1001
+ }
1002
+ if (EditComment)
1003
+ #else
1004
+ memcpy(Comment, (char *)CommentSec->Data+2, CommentSize);
1005
+ #endif
1006
+ {
1007
+ char EditFileName[PATH_MAX+5];
1008
+ strcpy(EditFileName, FileName);
1009
+ strcat(EditFileName, ".txt");
1010
+
1011
+ CommentSize = FileEditComment(EditFileName, Comment, CommentSize);
1012
+ }
1013
+ }
1014
+
1015
+ if (strcmp(Comment, (char *)CommentSec->Data+2)){
1016
+ // Discard old comment section and put a new one in.
1017
+ int size;
1018
+ size = CommentSize+2;
1019
+ free(CommentSec->Data);
1020
+ CommentSec->Size = size;
1021
+ CommentSec->Data = malloc(size);
1022
+ CommentSec->Data[0] = (uchar)(size >> 8);
1023
+ CommentSec->Data[1] = (uchar)(size);
1024
+ memcpy((CommentSec->Data)+2, Comment, size-2);
1025
+ Modified = TRUE;
1026
+ }
1027
+ if (!Modified){
1028
+ printf("Comment not modified\n");
1029
+ }
1030
+ }
1031
+
1032
+
1033
+ if (CommentSavefileName){
1034
+ Section_t * CommentSec;
1035
+ CommentSec = FindSection(M_COM);
1036
+
1037
+ if (CommentSec != NULL){
1038
+ char OutFileName[PATH_MAX+1];
1039
+ FILE * CommentFile;
1040
+
1041
+ // Make a relative name.
1042
+ RelativeName(OutFileName, CommentSavefileName, FileName);
1043
+
1044
+ CommentFile = fopen(OutFileName,"w");
1045
+
1046
+ if (CommentFile){
1047
+ fwrite((char *)CommentSec->Data+2, CommentSec->Size-2 ,1, CommentFile);
1048
+ fclose(CommentFile);
1049
+ }else{
1050
+ ErrFatal("Could not write comment file");
1051
+ }
1052
+ }else{
1053
+ printf("File '%s' contains no comment section\n",FileName);
1054
+ }
1055
+ }
1056
+
1057
+ if (ExifTimeAdjust || ExifTimeSet || DateSetChars || FileTimeToExif){
1058
+ if (ImageInfo.numDateTimeTags){
1059
+ struct tm tm;
1060
+ time_t UnixTime;
1061
+ char TempBuf[50];
1062
+ int a;
1063
+ Section_t * ExifSection;
1064
+ if (ExifTimeSet){
1065
+ // A time to set was specified.
1066
+ UnixTime = ExifTimeSet;
1067
+ }else{
1068
+ if (FileTimeToExif){
1069
+ FileTimeAsString(ImageInfo.DateTime);
1070
+ }
1071
+ if (DateSetChars){
1072
+ memcpy(ImageInfo.DateTime, DateSet, DateSetChars);
1073
+ a = 1970;
1074
+ sscanf(DateSet, "%d", &a);
1075
+ if (a < 1970){
1076
+ strcpy(TempBuf, ImageInfo.DateTime);
1077
+ goto skip_unixtime;
1078
+ }
1079
+ }
1080
+ // A time offset to adjust by was specified.
1081
+ if (!Exif2tm(&tm, ImageInfo.DateTime)) goto badtime;
1082
+
1083
+ // Convert to unix 32 bit time value, add offset, and convert back.
1084
+ UnixTime = mktime(&tm);
1085
+ if ((int)UnixTime == -1) goto badtime;
1086
+ UnixTime += ExifTimeAdjust;
1087
+ }
1088
+ tm = *localtime(&UnixTime);
1089
+
1090
+ // Print to temp buffer first to avoid putting null termination in destination.
1091
+ // snprintf() would do the trick, hbut not available everywhere (like FreeBSD 4.4)
1092
+ sprintf(TempBuf, "%04d:%02d:%02d %02d:%02d:%02d",
1093
+ tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
1094
+ tm.tm_hour, tm.tm_min, tm.tm_sec);
1095
+
1096
+ skip_unixtime:
1097
+ ExifSection = FindSection(M_EXIF);
1098
+
1099
+ for (a = 0; a < ImageInfo.numDateTimeTags; a++) {
1100
+ uchar * Pointer;
1101
+ Pointer = ExifSection->Data+ImageInfo.DateTimeOffsets[a]+8;
1102
+ memcpy(Pointer, TempBuf, 19);
1103
+ }
1104
+ memcpy(ImageInfo.DateTime, TempBuf, 19);
1105
+
1106
+ Modified = TRUE;
1107
+ }else{
1108
+ printf("File '%s' contains no Exif timestamp to change\n", FileName);
1109
+ }
1110
+ }
1111
+
1112
+ if (DeleteComments){
1113
+ if (RemoveSectionType(M_COM)) Modified = TRUE;
1114
+ }
1115
+ if (DeleteExif){
1116
+ if (RemoveSectionType(M_EXIF)) Modified = TRUE;
1117
+ }
1118
+ if (DeleteIptc){
1119
+ if (RemoveSectionType(M_IPTC)) Modified = TRUE;
1120
+ }
1121
+ if (DeleteXmp){
1122
+ if (RemoveSectionType(M_XMP)) Modified = TRUE;
1123
+ }
1124
+ if (DeleteUnknown){
1125
+ if (RemoveUnknownSections()) Modified = TRUE;
1126
+ }
1127
+
1128
+
1129
+ if (Modified){
1130
+ char BackupName[PATH_MAX+5];
1131
+ struct stat buf;
1132
+
1133
+ if (!Quiet) printf("Modified: %s\n",FileName);
1134
+
1135
+ strcpy(BackupName, FileName);
1136
+ strcat(BackupName, ".t");
1137
+
1138
+ // Remove any .old file name that may pre-exist
1139
+ unlink(BackupName);
1140
+
1141
+ // Rename the old file.
1142
+ rename(FileName, BackupName);
1143
+
1144
+ // Write the new file.
1145
+ WriteJpegFile(FileName);
1146
+
1147
+ // Copy the access rights from original file
1148
+ if (stat(BackupName, &buf) == 0){
1149
+ // set Unix access rights and time to new file
1150
+ struct utimbuf mtime;
1151
+ chmod(FileName, buf.st_mode);
1152
+
1153
+ mtime.actime = buf.st_mtime;
1154
+ mtime.modtime = buf.st_mtime;
1155
+
1156
+ utime(FileName, &mtime);
1157
+ }
1158
+
1159
+ // Now that we are done, remove original file.
1160
+ unlink(BackupName);
1161
+ }
1162
+
1163
+
1164
+ if (Exif2FileTime){
1165
+ // Set the file date to the date from the exif header.
1166
+ if (ImageInfo.numDateTimeTags){
1167
+ // Converte the file date to Unix time.
1168
+ struct tm tm;
1169
+ time_t UnixTime;
1170
+ struct utimbuf mtime;
1171
+ if (!Exif2tm(&tm, ImageInfo.DateTime)) goto badtime;
1172
+
1173
+ UnixTime = mktime(&tm);
1174
+ if ((int)UnixTime == -1){
1175
+ goto badtime;
1176
+ }
1177
+
1178
+ mtime.actime = UnixTime;
1179
+ mtime.modtime = UnixTime;
1180
+
1181
+ if (utime(FileName, &mtime) != 0){
1182
+ printf("Error: Could not change time of file '%s'\n",FileName);
1183
+ }else{
1184
+ if (!Quiet) printf("%s\n",FileName);
1185
+ }
1186
+ }else{
1187
+ printf("File '%s' contains no Exif timestamp\n", FileName);
1188
+ }
1189
+ }
1190
+
1191
+ // Feature to rename image according to date and time from camera.
1192
+ // I use this feature to put images from multiple digicams in sequence.
1193
+
1194
+ if (RenameToDate){
1195
+ DoFileRenaming(FileName);
1196
+ }
1197
+ DiscardData();
1198
+ return;
1199
+ badtime:
1200
+ printf("Error: Time '%s': cannot convert to Unix time\n",ImageInfo.DateTime);
1201
+ DiscardData();
1202
+ }
1203
+
1204
+ //--------------------------------------------------------------------------
1205
+ // complain about bad state of the command line.
1206
+ //--------------------------------------------------------------------------
1207
+ static void Usage (void)
1208
+ {
1209
+ printf("Jhead is a program for manipulating settings and thumnails in Exif jpeg headers\n"
1210
+ "used by most Digital Cameras. v"JHEAD_VERSION" Matthias Wandel, Nov 06 2009.\n"
1211
+ "http://www.sentex.net/~mwandel/jhead\n"
1212
+ "\n");
1213
+
1214
+ printf("Usage: %s [options] files\n", progname);
1215
+ printf("Where:\n"
1216
+ " files path/filenames with or without wildcards\n"
1217
+
1218
+ "[options] are:\n"
1219
+ "\nGENERAL METADATA:\n"
1220
+ " -te <name> Transfer exif header from another image file <name>\n"
1221
+ " Uses same name mangling as '-st' option\n"
1222
+ " -dc Delete comment field (as left by progs like Photoshop & Compupic)\n"
1223
+ " -de Strip Exif section (smaller JPEG file, but lose digicam info)\n"
1224
+ " -di Delete IPTC section (from Photoshop, or Picasa)\n"
1225
+ " -dx Deletex XMP section\n"
1226
+ " -du Delete non image sections except for Exif and comment sections\n"
1227
+ " -purejpg Strip all unnecessary data from jpeg (combines -dc -de and -du)\n"
1228
+ " -mkexif Create new minimal exif section (overwrites pre-existing exif)\n"
1229
+ " -ce Edit comment field. Uses environment variable 'editor' to\n"
1230
+ " determine which editor to use. If editor not set, uses VI\n"
1231
+ " under Unix and notepad with windows\n"
1232
+ " -cs <name> Save comment section to a file\n"
1233
+ " -ci <name> Insert comment section from a file. -cs and -ci use same naming\n"
1234
+ " scheme as used by the -st option\n"
1235
+ " -cl string Insert literal comment string\n"
1236
+
1237
+ "\nDATE / TIME MANIPULATION:\n"
1238
+ " -ft Set file modification time to Exif time\n"
1239
+ " -dsft Set Exif time to file modification time\n"
1240
+ " -n[format-string]\n"
1241
+ " Rename files according to date. Uses exif date if present, file\n"
1242
+ " date otherwise. If the optional format-string is not supplied,\n"
1243
+ " the format is mmdd-hhmmss. If a format-string is given, it is\n"
1244
+ " is passed to the 'strftime' function for formatting\n"
1245
+ " In addition to strftime format codes:\n"
1246
+ " '%%f' as part of the string will include the original file name\n"
1247
+ " '%%i' will include a sequence number, starting from 1. You can\n"
1248
+ " You can specify '%%03i' for example to get leading zeros.\n"
1249
+ " This feature is useful for ordering files from multiple digicams to\n"
1250
+ " sequence of taking. Only renames files whose names are mostly\n"
1251
+ " numerical (as assigned by digicam)\n"
1252
+ " The '.jpg' is automatically added to the end of the name. If the\n"
1253
+ " destination name already exists, a letter or digit is added to \n"
1254
+ " the end of the name to make it unique.\n"
1255
+ " The new name may include a path as part of the name. If this path\n"
1256
+ " does not exist, it will be created\n"
1257
+ " -nf[format-string]\n"
1258
+ " Same as -n, but rename regardless of original name\n"
1259
+ " -a (Windows only) Rename files with same name but different extension\n"
1260
+ " Use together with -n to rename .AVI files from exif in .THM files\n"
1261
+ " for example\n"
1262
+ " -ta<+|->h[:mm[:ss]]\n"
1263
+ " Adjust time by h:mm backwards or forwards. Useful when having\n"
1264
+ " taken pictures with the wrong time set on the camera, such as when\n"
1265
+ " traveling across time zones or DST changes. Dates can be adjusted\n"
1266
+ " by offsetting by 24 hours or more. For large date adjustments,\n"
1267
+ " use the -da option\n"
1268
+ " -da<date>-<date>\n"
1269
+ " Adjust date by large amounts. This is used to fix photos from\n"
1270
+ " cameras where the date got set back to the default camera date\n"
1271
+ " by accident or battery removal.\n"
1272
+ " To deal with different months and years having different numbers of\n"
1273
+ " days, a simple date-month-year offset would result in unexpected\n"
1274
+ " results. Instead, the difference is specified as desired date\n"
1275
+ " minus original date. Date is specified as yyyy:mm:dd or as date\n"
1276
+ " and time in the format yyyy:mm:dd/hh:mm:ss\n"
1277
+ " -ts<time> Set the Exif internal time to <time>. <time> is in the format\n"
1278
+ " yyyy:mm:dd-hh:mm:ss\n"
1279
+ " -ds<date> Set the Exif internal date. <date> is in the format YYYY:MM:DD\n"
1280
+ " or YYYY:MM or YYYY\n"
1281
+
1282
+ "\nTHUMBNAIL MANIPULATION:\n"
1283
+ " -dt Remove exif integral thumbnails. Typically trims 10k\n"
1284
+ " -st <name> Save Exif thumbnail, if there is one, in file <name>\n"
1285
+ " If output file name contains the substring \"&i\" then the\n"
1286
+ " image file name is substitute for the &i. Note that quotes around\n"
1287
+ " the argument are required for the '&' to be passed to the program.\n"
1288
+ #ifndef _WIN32
1289
+ " An output name of '-' causes thumbnail to be written to stdout\n"
1290
+ #endif
1291
+ " -rt <name> Replace Exif thumbnail. Can only be done with headers that\n"
1292
+ " already contain a thumbnail.\n"
1293
+ " -rgt[size] Regnerate exif thumbnail. Only works if image already\n"
1294
+ " contains a thumbail. size specifies maximum height or width of\n"
1295
+ " thumbnail. Relies on 'mogrify' programs to be on path\n"
1296
+
1297
+ "\nROTATION TAG MANIPULATION:\n"
1298
+ " -autorot Invoke jpegtran to rotate images according to Exif orientation tag\n"
1299
+ " Note: Windows users must get jpegtran for this to work\n"
1300
+ " -norot Zero out the rotation tag. This to avoid some browsers from\n"
1301
+ " rotating the image again after you rotated it but neglected to\n"
1302
+ " clear the rotation tag\n"
1303
+
1304
+ "\nOUTPUT VERBOSITY CONTROL:\n"
1305
+ " -h help (this text)\n"
1306
+ " -v even more verbose output\n"
1307
+ " -q Quiet (no messages on success, like Unix)\n"
1308
+ " -V Show jhead version\n"
1309
+ " -exifmap Dump header bytes, annotate. Pipe thru sort for better viewing\n"
1310
+ " -se Supress error messages relating to corrupt exif header structure\n"
1311
+ " -c concise output\n"
1312
+ " -nofinfo Don't show file info (name/size/date)\n"
1313
+
1314
+ "\nFILE MATCHING AND SELECTION:\n"
1315
+ " -model model\n"
1316
+ " Only process files from digicam containing model substring in\n"
1317
+ " camera model description\n"
1318
+ " -exonly Skip all files that don't have an exif header (skip all jpegs that\n"
1319
+ " were not created by digicam)\n"
1320
+ " -cmd command\n"
1321
+ " Apply 'command' to every file, then re-insert exif and command\n"
1322
+ " sections into the image. &i will be substituted for the input file\n"
1323
+ " name, and &o (if &o is used). Use quotes around the command string\n"
1324
+ " This is most useful in conjunction with the free ImageMagick tool. \n"
1325
+ " For example, with my Canon S100, which suboptimally compresses\n"
1326
+ " jpegs I can specify\n"
1327
+ " jhead -cmd \"mogrify -quality 80 &i\" *.jpg\n"
1328
+ " to re-compress a lot of images using ImageMagick to half the size,\n"
1329
+ " and no visible loss of quality while keeping the exif header\n"
1330
+ " Another invocation I like to use is jpegtran (hard to find for\n"
1331
+ " windows). I type:\n"
1332
+ " jhead -cmd \"jpegtran -progressive &i &o\" *.jpg\n"
1333
+ " to convert jpegs to progressive jpegs (Unix jpegtran syntax\n"
1334
+ " differs slightly)\n"
1335
+ " -orp Only operate on 'portrait' aspect ratio images\n"
1336
+ " -orl Only operate on 'landscape' aspect ratio images\n"
1337
+ #ifdef _WIN32
1338
+ " -r No longer supported. Use the ** wildcard to recurse directories\n"
1339
+ " with instead.\n"
1340
+ " examples:\n"
1341
+ " jhead **/*.jpg\n"
1342
+ " jhead \"c:\\my photos\\**\\*.jpg\"\n"
1343
+ #endif
1344
+
1345
+
1346
+ #ifdef MATTHIAS
1347
+ "\n"
1348
+ " -cr Remove comment tag (my way)\n"
1349
+ " -ca Add comment tag (my way)\n"
1350
+ " -ar Auto resize to fit in 1024x1024, but never less than half\n"
1351
+ #endif //MATTHIAS
1352
+
1353
+
1354
+ );
1355
+
1356
+ exit(EXIT_FAILURE);
1357
+ }
1358
+
1359
+
1360
+ //--------------------------------------------------------------------------
1361
+ // Parse specified date or date+time from command line.
1362
+ //--------------------------------------------------------------------------
1363
+ time_t ParseCmdDate(char * DateSpecified)
1364
+ {
1365
+ int a;
1366
+ struct tm tm;
1367
+ time_t UnixTime;
1368
+
1369
+ tm.tm_wday = -1;
1370
+ tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
1371
+
1372
+ a = sscanf(DateSpecified, "%d:%d:%d/%d:%d:%d",
1373
+ &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
1374
+ &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
1375
+
1376
+ if (a != 3 && a < 5){
1377
+ // Date must be YYYY:MM:DD, YYYY:MM:DD+HH:MM
1378
+ // or YYYY:MM:DD+HH:MM:SS
1379
+ ErrFatal("Could not parse specified date");
1380
+ }
1381
+ tm.tm_isdst = -1;
1382
+ tm.tm_mon -= 1; // Adjust for unix zero-based months
1383
+ tm.tm_year -= 1900; // Adjust for year starting at 1900
1384
+
1385
+ UnixTime = mktime(&tm);
1386
+ if (UnixTime == -1){
1387
+ ErrFatal("Specified time is invalid or out of range");
1388
+ }
1389
+
1390
+ return UnixTime;
1391
+ }
1392
+
1393
+ //--------------------------------------------------------------------------
1394
+ // The main program.
1395
+ //--------------------------------------------------------------------------
1396
+ int main (int argc, char **argv)
1397
+ {
1398
+ int argn;
1399
+ char * arg;
1400
+ progname = argv[0];
1401
+
1402
+ for (argn=1;argn<argc;argn++){
1403
+ arg = argv[argn];
1404
+ if (arg[0] != '-') break; // Filenames from here on.
1405
+
1406
+ // General metadata options:
1407
+ if (!strcmp(arg,"-te")){
1408
+ ExifXferScrFile = argv[++argn];
1409
+ DoModify = TRUE;
1410
+ }else if (!strcmp(arg,"-dc")){
1411
+ DeleteComments = TRUE;
1412
+ DoModify = TRUE;
1413
+ }else if (!strcmp(arg,"-de")){
1414
+ DeleteExif = TRUE;
1415
+ DoModify = TRUE;
1416
+ }else if (!strcmp(arg,"-di")){
1417
+ DeleteIptc = TRUE;
1418
+ DoModify = TRUE;
1419
+ }else if (!strcmp(arg,"-dx")){
1420
+ DeleteXmp = TRUE;
1421
+ DoModify = TRUE;
1422
+ }else if (!strcmp(arg, "-du")){
1423
+ DeleteUnknown = TRUE;
1424
+ DoModify = TRUE;
1425
+ }else if (!strcmp(arg, "-purejpg")){
1426
+ DeleteExif = TRUE;
1427
+ DeleteComments = TRUE;
1428
+ DeleteIptc = TRUE;
1429
+ DeleteUnknown = TRUE;
1430
+ DeleteXmp = TRUE;
1431
+ DoModify = TRUE;
1432
+ }else if (!strcmp(arg,"-ce")){
1433
+ EditComment = TRUE;
1434
+ DoModify = TRUE;
1435
+ }else if (!strcmp(arg,"-cs")){
1436
+ CommentSavefileName = argv[++argn];
1437
+ }else if (!strcmp(arg,"-ci")){
1438
+ CommentInsertfileName = argv[++argn];
1439
+ DoModify = TRUE;
1440
+ }else if (!strcmp(arg,"-cl")){
1441
+ CommentInsertLiteral = argv[++argn];
1442
+ DoModify = TRUE;
1443
+ }else if (!strcmp(arg,"-mkexif")){
1444
+ CreateExifSection = TRUE;
1445
+ DoModify = TRUE;
1446
+
1447
+ // Output verbosity control
1448
+ }else if (!strcmp(arg,"-h")){
1449
+ Usage();
1450
+ }else if (!strcmp(arg,"-v")){
1451
+ ShowTags = TRUE;
1452
+ }else if (!strcmp(arg,"-q")){
1453
+ Quiet = TRUE;
1454
+ }else if (!strcmp(arg,"-V")){
1455
+ printf("Jhead version: "JHEAD_VERSION" Compiled: "__DATE__"\n");
1456
+ exit(0);
1457
+ }else if (!strcmp(arg,"-exifmap")){
1458
+ DumpExifMap = TRUE;
1459
+ }else if (!strcmp(arg,"-se")){
1460
+ SupressNonFatalErrors = TRUE;
1461
+ }else if (!strcmp(arg,"-c")){
1462
+ ShowConcise = TRUE;
1463
+ }else if (!strcmp(arg,"-nofinfo")){
1464
+ ShowFileInfo = 0;
1465
+
1466
+ // Thumbnail manipulation options
1467
+ }else if (!strcmp(arg,"-dt")){
1468
+ TrimExif = TRUE;
1469
+ DoModify = TRUE;
1470
+ }else if (!strcmp(arg,"-st")){
1471
+ ThumbSaveName = argv[++argn];
1472
+ DoReadAction = TRUE;
1473
+ }else if (!strcmp(arg,"-rt")){
1474
+ ThumbInsertName = argv[++argn];
1475
+ DoModify = TRUE;
1476
+ }else if (!memcmp(arg,"-rgt", 4)){
1477
+ RegenThumbnail = 160;
1478
+ sscanf(arg+4, "%d", &RegenThumbnail);
1479
+ if (RegenThumbnail > 320){
1480
+ ErrFatal("Specified thumbnail geometry too big!");
1481
+ }
1482
+ DoModify = TRUE;
1483
+
1484
+ // Rotation tag manipulation
1485
+ }else if (!strcmp(arg,"-autorot")){
1486
+ AutoRotate = 1;
1487
+ DoModify = TRUE;
1488
+ }else if (!strcmp(arg,"-norot")){
1489
+ AutoRotate = 1;
1490
+ ZeroRotateTagOnly = 1;
1491
+ DoModify = TRUE;
1492
+
1493
+ // Date/Time manipulation options
1494
+ }else if (!memcmp(arg,"-n",2)){
1495
+ RenameToDate = 1;
1496
+ DoReadAction = TRUE; // Rename doesn't modify file, so count as read action.
1497
+ arg+=2;
1498
+ if (*arg == 'f'){
1499
+ RenameToDate = 2;
1500
+ arg++;
1501
+ }
1502
+ if (*arg){
1503
+ // A strftime format string is supplied.
1504
+ strftime_args = arg;
1505
+ #ifdef _WIN32
1506
+ SlashToNative(strftime_args);
1507
+ #endif
1508
+ //printf("strftime_args = %s\n",arg);
1509
+ }
1510
+ }else if (!strcmp(arg,"-a")){
1511
+ #ifndef _WIN32
1512
+ ErrFatal("Error: -a only supported in Windows version");
1513
+ #else
1514
+ RenameAssociatedFiles = TRUE;
1515
+ #endif
1516
+ }else if (!strcmp(arg,"-ft")){
1517
+ Exif2FileTime = TRUE;
1518
+ DoReadAction = TRUE;
1519
+ }else if (!memcmp(arg,"-ta",3)){
1520
+ // Time adjust feature.
1521
+ int hours, minutes, seconds, n;
1522
+ minutes = seconds = 0;
1523
+ if (arg[3] != '-' && arg[3] != '+'){
1524
+ ErrFatal("Error: -ta must be followed by +/- and a time");
1525
+ }
1526
+ n = sscanf(arg+4, "%d:%d:%d", &hours, &minutes, &seconds);
1527
+
1528
+ if (n < 1){
1529
+ ErrFatal("Error: -ta must be immediately followed by time");
1530
+ }
1531
+ if (ExifTimeAdjust) ErrFatal("Can only use one of -da or -ta options at once");
1532
+ ExifTimeAdjust = hours*3600 + minutes*60 + seconds;
1533
+ if (arg[3] == '-') ExifTimeAdjust = -ExifTimeAdjust;
1534
+ DoModify = TRUE;
1535
+ }else if (!memcmp(arg,"-da",3)){
1536
+ // Date adjust feature (large time adjustments)
1537
+ time_t NewDate, OldDate = 0;
1538
+ char * pOldDate;
1539
+ NewDate = ParseCmdDate(arg+3);
1540
+ pOldDate = strstr(arg+1, "-");
1541
+ if (pOldDate){
1542
+ OldDate = ParseCmdDate(pOldDate+1);
1543
+ }else{
1544
+ ErrFatal("Must specifiy second date for -da option");
1545
+ }
1546
+ if (ExifTimeAdjust) ErrFatal("Can only use one of -da or -ta options at once");
1547
+ ExifTimeAdjust = NewDate-OldDate;
1548
+ DoModify = TRUE;
1549
+ }else if (!memcmp(arg,"-dsft",5)){
1550
+ // Set file time to date/time in exif
1551
+ FileTimeToExif = TRUE;
1552
+ DoModify = TRUE;
1553
+ }else if (!memcmp(arg,"-ds",3)){
1554
+ // Set date feature
1555
+ int a;
1556
+ // Check date validity and copy it. Could be incompletely specified.
1557
+ strcpy(DateSet, "0000:01:01");
1558
+ for (a=0;arg[a+3];a++){
1559
+ if (isdigit(DateSet[a])){
1560
+ if (!isdigit(arg[a+3])){
1561
+ a = 0;
1562
+ break;
1563
+ }
1564
+ }else{
1565
+ if (arg[a+3] != ':'){
1566
+ a=0;
1567
+ break;
1568
+ }
1569
+ }
1570
+ DateSet[a] = arg[a+3];
1571
+ }
1572
+ if (a < 4 || a > 10){
1573
+ ErrFatal("Date must be in format YYYY, YYYY:MM, or YYYY:MM:DD");
1574
+ }
1575
+ DateSetChars = a;
1576
+ DoModify = TRUE;
1577
+ }else if (!memcmp(arg,"-ts",3)){
1578
+ // Set the exif time.
1579
+ // Time must be specified as "yyyy:mm:dd-hh:mm:ss"
1580
+ char * c;
1581
+ struct tm tm;
1582
+
1583
+ c = strstr(arg+1, "-");
1584
+ if (c) *c = ' '; // Replace '-' with a space.
1585
+
1586
+ if (!Exif2tm(&tm, arg+3)){
1587
+ ErrFatal("-ts option must be followed by time in format yyyy:mmm:dd-hh:mm:ss\n"
1588
+ "Example: jhead -ts2001:01:01-12:00:00 foo.jpg");
1589
+ }
1590
+
1591
+ ExifTimeSet = mktime(&tm);
1592
+
1593
+ if ((int)ExifTimeSet == -1) ErrFatal("Time specified is out of range");
1594
+ DoModify = TRUE;
1595
+
1596
+ // File matching and selection
1597
+ }else if (!strcmp(arg,"-model")){
1598
+ if (argn+1 >= argc) Usage(); // No extra argument.
1599
+ FilterModel = argv[++argn];
1600
+ }else if (!strcmp(arg,"-exonly")){
1601
+ ExifOnly = 1;
1602
+ }else if (!strcmp(arg,"-orp")){
1603
+ PortraitOnly = 1;
1604
+ }else if (!strcmp(arg,"-orl")){
1605
+ PortraitOnly = -1;
1606
+ }else if (!strcmp(arg,"-cmd")){
1607
+ if (argn+1 >= argc) Usage(); // No extra argument.
1608
+ ApplyCommand = argv[++argn];
1609
+ DoModify = TRUE;
1610
+
1611
+ #ifdef MATTHIAS
1612
+ }else if (!strcmp(arg,"-ca")){
1613
+ // Its a literal comment. Add.
1614
+ AddComment = argv[++argn];
1615
+ DoModify = TRUE;
1616
+ }else if (!strcmp(arg,"-cr")){
1617
+ // Its a literal comment. Remove this keyword.
1618
+ RemComment = argv[++argn];
1619
+ DoModify = TRUE;
1620
+ }else if (!strcmp(arg,"-ar")){
1621
+ AutoResize = TRUE;
1622
+ ShowConcise = TRUE;
1623
+ ApplyCommand = (char *)1; // Must be non null so it does commands.
1624
+ DoModify = TRUE;
1625
+ #endif // MATTHIAS
1626
+ }else{
1627
+ printf("Argument '%s' not understood\n",arg);
1628
+ printf("Use jhead -h for list of arguments\n");
1629
+ exit(-1);
1630
+ }
1631
+ if (argn >= argc){
1632
+ // Used an extra argument - becuase the last argument
1633
+ // used up an extr argument.
1634
+ ErrFatal("Extra argument required");
1635
+ }
1636
+ }
1637
+ if (argn == argc){
1638
+ ErrFatal("No files to process. Use -h for help");
1639
+ }
1640
+
1641
+ if (ThumbSaveName != NULL && strcmp(ThumbSaveName, "&i") == 0){
1642
+ printf("Error: By specifying \"&i\" for the thumbail name, your original file\n"
1643
+ " will be overwitten. If this is what you really want,\n"
1644
+ " specify -st \"./&i\" to override this check\n");
1645
+ exit(0);
1646
+ }
1647
+
1648
+ if (RegenThumbnail){
1649
+ if (ThumbSaveName || ThumbInsertName){
1650
+ printf("Error: Cannot regen and save or insert thumbnail in same run\n");
1651
+ exit(0);
1652
+ }
1653
+ }
1654
+
1655
+ if (EditComment){
1656
+ if (CommentSavefileName != NULL || CommentInsertfileName != NULL){
1657
+ printf("Error: Cannot use -ce option in combination with -cs or -ci\n");
1658
+ exit(0);
1659
+ }
1660
+ }
1661
+
1662
+
1663
+ if (ExifXferScrFile){
1664
+ if (FilterModel || ApplyCommand){
1665
+ ErrFatal("Error: Filter by model and/or applying command to files\n"
1666
+ " invalid while transfering Exif headers");
1667
+ }
1668
+ }
1669
+
1670
+ FileSequence = 0;
1671
+ for (;argn<argc;argn++){
1672
+ FilesMatched = FALSE;
1673
+
1674
+ #ifdef _WIN32
1675
+ SlashToNative(argv[argn]);
1676
+ // Use my globbing module to do fancier wildcard expansion with recursive
1677
+ // subdirectories under Windows.
1678
+ MyGlob(argv[argn], ProcessFile);
1679
+ #else
1680
+ // Under linux, don't do any extra fancy globbing - shell globbing is
1681
+ // pretty fancy as it is - although not as good as myglob.c
1682
+ ProcessFile(argv[argn]);
1683
+ #endif
1684
+
1685
+ if (!FilesMatched){
1686
+ fprintf(stderr, "Error: No files matched '%s'\n",argv[argn]);
1687
+ }
1688
+ }
1689
+
1690
+ if (FileSequence == 0){
1691
+ return EXIT_FAILURE;
1692
+ }else{
1693
+ return EXIT_SUCCESS;
1694
+ }
1695
+ }
1696
+
1697
+