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