//-------------------------------------------------------------------------- // Program to pull the information out of various types of EXIF digital // camera files and show it in a reasonably consistent way // // Version 2.86 // // Compiling under Windows: // Make sure you have Microsoft's compiler on the path, then run make.bat // // Dec 1999 - Mar 2009 // // by Matthias Wandel www.sentex.net/~mwandel //-------------------------------------------------------------------------- #include "jhead.h" #include <sys/stat.h> #include <utils/Log.h> #define JHEAD_VERSION "2.87" // This #define turns on features that are too very specific to // how I organize my photos. Best to ignore everything inside #ifdef MATTHIAS //#define MATTHIAS #ifdef _WIN32 #include <io.h> #endif static int FilesMatched; static int FileSequence; static const char * CurrentFile; static const char * progname; // program name for error messages //-------------------------------------------------------------------------- // Command line options flags static int TrimExif = FALSE; // Cut off exif beyond interesting data. static int RenameToDate = 0; // 1=rename, 2=rename all. #ifdef _WIN32 static int RenameAssociatedFiles = FALSE; #endif static char * strftime_args = NULL; // Format for new file name. static int Exif2FileTime = FALSE; static int DoModify = FALSE; static int DoReadAction = FALSE; int ShowTags = FALSE; // Do not show raw by default. static int Quiet = FALSE; // Be quiet on success (like unix programs) int DumpExifMap = FALSE; static int ShowConcise = FALSE; static int CreateExifSection = FALSE; static char * ApplyCommand = NULL; // Apply this command to all images. static char * FilterModel = NULL; static int ExifOnly = FALSE; static int PortraitOnly = FALSE; static time_t ExifTimeAdjust = 0; // Timezone adjust static time_t ExifTimeSet = 0; // Set exif time to a value. static char DateSet[11]; static unsigned DateSetChars = 0; static unsigned FileTimeToExif = FALSE; static int DeleteComments = FALSE; static int DeleteExif = FALSE; static int DeleteIptc = FALSE; static int DeleteXmp = FALSE; static int DeleteUnknown = FALSE; static char * ThumbSaveName = NULL; // If not NULL, use this string to make up // the filename to store the thumbnail to. static char * ThumbInsertName = NULL; // If not NULL, use this string to make up // the filename to retrieve the thumbnail from. static int RegenThumbnail = FALSE; static char * ExifXferScrFile = NULL;// Extract Exif header from this file, and // put it into the Jpegs processed. static int EditComment = FALSE; // Invoke an editor for editing the comment static int SupressNonFatalErrors = FALSE; // Wether or not to pint warnings on recoverable errors static char * CommentSavefileName = NULL; // Save comment to this file. static char * CommentInsertfileName = NULL; // Insert comment from this file. static char * CommentInsertLiteral = NULL; // Insert this comment (from command line) static int AutoRotate = FALSE; static int ZeroRotateTagOnly = FALSE; static int ShowFileInfo = TRUE; // Indicates to show standard file info // (file name, file size, file date) #ifdef MATTHIAS // This #ifdef to take out less than elegant stuff for editing // the comments in a JPEG. The programs rdjpgcom and wrjpgcom // included with Linux distributions do a better job. static char * AddComment = NULL; // Add this tag. static char * RemComment = NULL; // Remove this tag static int AutoResize = FALSE; #endif // MATTHIAS //-------------------------------------------------------------------------- // Error exit handler //-------------------------------------------------------------------------- void ErrFatal(char * msg) { LOGE("Error : %s\n", msg); if (CurrentFile) fprintf(stderr,"in file '%s'\n",CurrentFile); exit(EXIT_FAILURE); } //-------------------------------------------------------------------------- // Report non fatal errors. Now that microsoft.net modifies exif headers, // there's corrupted ones, and there could be more in the future. //-------------------------------------------------------------------------- void ErrNonfatal(char * msg, int a1, int a2) { LOGV("Nonfatal Error : "); LOGV(msg, a1, a2); if (SupressNonFatalErrors) return; fprintf(stderr,"\nNonfatal Error : "); if (CurrentFile) fprintf(stderr,"'%s' ",CurrentFile); fprintf(stderr, msg, a1, a2); fprintf(stderr, "\n"); } //-------------------------------------------------------------------------- // Set file time as exif time. //-------------------------------------------------------------------------- void FileTimeAsString(char * TimeStr) { struct tm ts; ts = *localtime(&ImageInfo.FileDateTime); strftime(TimeStr, 20, "%Y:%m:%d %H:%M:%S", &ts); } #if 0 // not used -- possible security risk with use of system, sprintf, etc. //-------------------------------------------------------------------------- // Invoke an editor for editing a string. //-------------------------------------------------------------------------- static int FileEditComment(char * TempFileName, char * Comment, int CommentSize) { FILE * file; int a; char QuotedPath[PATH_MAX+10]; file = fopen(TempFileName, "w"); if (file == NULL){ fprintf(stderr, "Can't create file '%s'\n",TempFileName); ErrFatal("could not create temporary file"); } fwrite(Comment, CommentSize, 1, file); fclose(file); fflush(stdout); // So logs are contiguous. { char * Editor; Editor = getenv("EDITOR"); if (Editor == NULL){ #ifdef _WIN32 Editor = "notepad"; #else Editor = "vi"; #endif } if (strlen(Editor) > PATH_MAX) ErrFatal("env too long"); sprintf(QuotedPath, "%s \"%s\"",Editor, TempFileName); a = system(QuotedPath); } if (a != 0){ perror("Editor failed to launch"); exit(-1); } file = fopen(TempFileName, "r"); if (file == NULL){ ErrFatal("could not open temp file for read"); } // Read the file back in. CommentSize = fread(Comment, 1, 999, file); fclose(file); unlink(TempFileName); return CommentSize; } #ifdef MATTHIAS //-------------------------------------------------------------------------- // Modify one of the lines in the comment field. // This very specific to the photo album program stuff. //-------------------------------------------------------------------------- static char KnownTags[][10] = {"date", "desc","scan_date","author", "keyword","videograb", "show_raw","panorama","titlepix",""}; static int ModifyDescriptComment(char * OutComment, char * SrcComment) { char Line[500]; int Len; int a,i; unsigned l; int HasScandate = FALSE; int TagExists = FALSE; int Modified = FALSE; Len = 0; OutComment[0] = 0; for (i=0;;i++){ if (SrcComment[i] == '\r' || SrcComment[i] == '\n' || SrcComment[i] == 0 || Len >= 199){ // Process the line. if (Len > 0){ Line[Len] = 0; //printf("Line: '%s'\n",Line); for (a=0;;a++){ l = strlen(KnownTags[a]); if (!l){ // Unknown tag. Discard it. printf("Error: Unknown tag '%s'\n", Line); // Deletes the tag. Modified = TRUE; break; } if (memcmp(Line, KnownTags[a], l) == 0){ if (Line[l] == ' ' || Line[l] == '=' || Line[l] == 0){ // Its a good tag. if (Line[l] == ' ') Line[l] = '='; // Use equal sign for clarity. if (a == 2) break; // Delete 'orig_path' tag. if (a == 3) HasScandate = TRUE; if (RemComment){ if (strlen(RemComment) == l){ if (!memcmp(Line, RemComment, l)){ Modified = TRUE; break; } } } if (AddComment){ // Overwrite old comment of same tag with new one. if (!memcmp(Line, AddComment, l+1)){ TagExists = TRUE; strncpy(Line, AddComment, sizeof(Line)); Modified = TRUE; } } strncat(OutComment, Line, MAX_COMMENT_SIZE-5-strlen(OutComment)); strcat(OutComment, "\n"); break; } } } } Line[Len = 0] = 0; if (SrcComment[i] == 0) break; }else{ Line[Len++] = SrcComment[i]; } } if (AddComment && TagExists == FALSE){ strncat(OutComment, AddComment, MAX_COMMENT_SIZE-5-strlen(OutComment)); strcat(OutComment, "\n"); Modified = TRUE; } if (!HasScandate && !ImageInfo.DateTime[0]){ // Scan date is not in the file yet, and it doesn't have one built in. Add it. char Temp[30]; sprintf(Temp, "scan_date=%s", ctime(&ImageInfo.FileDateTime)); strncat(OutComment, Temp, MAX_COMMENT_SIZE-5-strlen(OutComment)); Modified = TRUE; } return Modified; } //-------------------------------------------------------------------------- // Automatic make smaller command stuff //-------------------------------------------------------------------------- static int AutoResizeCmdStuff(void) { static char CommandString[PATH_MAX+1]; double scale; ApplyCommand = CommandString; if (ImageInfo.Height <= 1280 && ImageInfo.Width <= 1280){ printf("not resizing %dx%x '%s'\n",ImageInfo.Height, ImageInfo.Width, ImageInfo.FileName); return FALSE; } scale = 1024.0 / ImageInfo.Height; if (1024.0 / ImageInfo.Width < scale) scale = 1024.0 / ImageInfo.Width; if (scale < 0.5) scale = 0.5; // Don't scale down by more than a factor of two. sprintf(CommandString, "mogrify -geometry %dx%d -quality 85 &i",(int)(ImageInfo.Width*scale), (int)(ImageInfo.Height*scale)); return TRUE; } #endif // MATTHIAS //-------------------------------------------------------------------------- // Escape an argument such that it is interpreted literally by the shell // (returns the number of written characters) //-------------------------------------------------------------------------- static int shellescape(char* to, const char* from) { int i, j; i = j = 0; // Enclosing characters in double quotes preserves the literal value of // all characters within the quotes, with the exception of $, `, and \. to[j++] = '"'; while(from[i]) { #ifdef _WIN32 // Under WIN32, there isn't really anything dangerous you can do with // escape characters, plus windows users aren't as sercurity paranoid. // Hence, no need to do fancy escaping. to[j++] = from[i++]; #else switch(from[i]) { case '"': case '$': case '`': case '\\': to[j++] = '\\'; // Fallthru... default: to[j++] = from[i++]; } #endif if (j >= PATH_MAX) ErrFatal("max path exceeded"); } to[j++] = '"'; return j; } //-------------------------------------------------------------------------- // Apply the specified command to the JPEG file. //-------------------------------------------------------------------------- static void DoCommand(const char * FileName, int ShowIt) { int a,e; char ExecString[PATH_MAX*3]; char TempName[PATH_MAX+10]; int TempUsed = FALSE; e = 0; // Generate an unused temporary file name in the destination directory // (a is the number of characters to copy from FileName) a = strlen(FileName)-1; while(a > 0 && FileName[a-1] != SLASH) a--; memcpy(TempName, FileName, a); strcpy(TempName+a, "XXXXXX"); mktemp(TempName); if(!TempName[0]) { ErrFatal("Cannot find available temporary file name"); } // Build the exec string. &i and &o in the exec string get replaced by input and output files. for (a=0;;a++){ if (ApplyCommand[a] == '&'){ if (ApplyCommand[a+1] == 'i'){ // Input file. e += shellescape(ExecString+e, FileName); a += 1; continue; } if (ApplyCommand[a+1] == 'o'){ // Needs an output file distinct from the input file. e += shellescape(ExecString+e, TempName); a += 1; TempUsed = TRUE; continue; } } ExecString[e++] = ApplyCommand[a]; if (ApplyCommand[a] == 0) break; } if (ShowIt) printf("Cmd:%s\n",ExecString); errno = 0; a = system(ExecString); if (a || errno){ // A command can however fail without errno getting set or system returning an error. if (errno) perror("system"); ErrFatal("Problem executing specified command"); } if (TempUsed){ // Don't delete original file until we know a new one was created by the command. struct stat dummy; if (stat(TempName, &dummy) == 0){ unlink(FileName); rename(TempName, FileName); }else{ ErrFatal("specified command did not produce expected output file"); } } } //-------------------------------------------------------------------------- // check if this file should be skipped based on contents. //-------------------------------------------------------------------------- static int CheckFileSkip(void) { // I sometimes add code here to only process images based on certain // criteria - for example, only to convert non progressive Jpegs to progressives, etc.. if (FilterModel){ // Filtering processing by camera model. // This feature is useful when pictures from multiple cameras are colated, // the its found that one of the cameras has the time set incorrectly. if (strstr(ImageInfo.CameraModel, FilterModel) == NULL){ // Skip. return TRUE; } } if (ExifOnly){ // Filtering by EXIF only. Skip all files that have no Exif. if (FindSection(M_EXIF) == NULL){ return TRUE; } } if (PortraitOnly == 1){ if (ImageInfo.Width > ImageInfo.Height) return TRUE; } if (PortraitOnly == -1){ if (ImageInfo.Width < ImageInfo.Height) return TRUE; } return FALSE; } //-------------------------------------------------------------------------- // Subsititute original name for '&i' if present in specified name. // This to allow specifying relative names when manipulating multiple files. //-------------------------------------------------------------------------- static void RelativeName(char * OutFileName, const char * NamePattern, const char * OrigName) { // If the filename contains substring "&i", then substitute the // filename for that. This gives flexibility in terms of processing // multiple files at a time. char * Subst; Subst = strstr(NamePattern, "&i"); if (Subst){ strncpy(OutFileName, NamePattern, Subst-NamePattern); OutFileName[Subst-NamePattern] = 0; strncat(OutFileName, OrigName, PATH_MAX); strncat(OutFileName, Subst+2, PATH_MAX); }else{ strncpy(OutFileName, NamePattern, PATH_MAX); } } #ifdef _WIN32 //-------------------------------------------------------------------------- // Rename associated files //-------------------------------------------------------------------------- void RenameAssociated(const char * FileName, char * NewBaseName) { int a; int PathLen; int ExtPos; char FilePattern[_MAX_PATH+1]; char NewName[_MAX_PATH+1]; struct _finddata_t finddata; long find_handle; for(ExtPos = strlen(FileName);FileName[ExtPos-1] != '.';){ if (--ExtPos == 0) return; // No extension! } memcpy(FilePattern, FileName, ExtPos); FilePattern[ExtPos] = '*'; FilePattern[ExtPos+1] = '\0'; for(PathLen = strlen(FileName);FileName[PathLen-1] != SLASH;){ if (--PathLen == 0) break; } find_handle = _findfirst(FilePattern, &finddata); for (;;){ if (find_handle == -1) break; // Eliminate the obvious patterns. if (!memcmp(finddata.name, ".",2)) goto next_file; if (!memcmp(finddata.name, "..",3)) goto next_file; if (finddata.attrib & _A_SUBDIR) goto next_file; strncpy(FilePattern+PathLen, finddata.name, PATH_MAX-PathLen); // full name with path strcpy(NewName, NewBaseName); for(a = strlen(finddata.name);finddata.name[a] != '.';){ if (--a == 0) goto next_file; } strncat(NewName, finddata.name+a, _MAX_PATH-strlen(NewName)); // add extension to new name if (rename(FilePattern, NewName) == 0){ if (!Quiet){ printf("%s --> %s\n",FilePattern, NewName); } } next_file: if (_findnext(find_handle, &finddata) != 0) break; } _findclose(find_handle); } #endif //-------------------------------------------------------------------------- // Handle renaming of files by date. //-------------------------------------------------------------------------- static void DoFileRenaming(const char * FileName) { int NumAlpha = 0; int NumDigit = 0; int PrefixPart = 0; // Where the actual filename starts. int ExtensionPart; // Where the file extension starts. int a; struct tm tm; char NewBaseName[PATH_MAX*2]; int AddLetter = 0; char NewName[PATH_MAX+2]; ExtensionPart = strlen(FileName); for (a=0;FileName[a];a++){ if (FileName[a] == SLASH){ // Don't count path component. NumAlpha = 0; NumDigit = 0; PrefixPart = a+1; } if (FileName[a] == '.') ExtensionPart = a; // Remember where extension starts. if (isalpha(FileName[a])) NumAlpha += 1; // Tally up alpha vs. digits to judge wether to rename. if (isdigit(FileName[a])) NumDigit += 1; } if (RenameToDate <= 1){ // If naming isn't forced, ensure name is mostly digits, or leave it alone. if (NumAlpha > 8 || NumDigit < 4){ return; } } if (!Exif2tm(&tm, ImageInfo.DateTime)){ printf("File '%s' contains no exif date stamp. Using file date\n",FileName); // Use file date/time instead. tm = *localtime(&ImageInfo.FileDateTime); } strncpy(NewBaseName, FileName, PATH_MAX); // Get path component of name. if (strftime_args){ // Complicated scheme for flexibility. Just pass the args to strftime. time_t UnixTime; char *s; char pattern[PATH_MAX+20]; int n = ExtensionPart - PrefixPart; // Call mktime to get weekday and such filled in. UnixTime = mktime(&tm); if ((int)UnixTime == -1){ printf("Could not convert %s to unix time",ImageInfo.DateTime); return; } // Substitute "%f" for the original name (minus path & extension) pattern[PATH_MAX-1]=0; strncpy(pattern, strftime_args, PATH_MAX-1); while ((s = strstr(pattern, "%f")) && strlen(pattern) + n < PATH_MAX-1){ memmove(s + n, s + 2, strlen(s+2) + 1); memmove(s, FileName + PrefixPart, n); } { // Sequential number renaming part. // '%i' type pattern becomes sequence number. int ppos = -1; for (a=0;pattern[a];a++){ if (pattern[a] == '%'){ ppos = a; }else if (pattern[a] == 'i'){ if (ppos >= 0 && a<ppos+4){ // Replace this part with a number. char pat[8], num[16]; int l,nl; memcpy(pat, pattern+ppos, 4); pat[a-ppos] = 'd'; // Replace 'i' with 'd' for '%d' pat[a-ppos+1] = '\0'; sprintf(num, pat, FileSequence); // let printf do the number formatting. nl = strlen(num); l = strlen(pattern+a+1); if (ppos+nl+l+1 >= PATH_MAX) ErrFatal("str overflow"); memmove(pattern+ppos+nl, pattern+a+1, l+1); memcpy(pattern+ppos, num, nl); break; } }else if (!isdigit(pattern[a])){ ppos = -1; } } } strftime(NewName, PATH_MAX, pattern, &tm); }else{ // My favourite scheme. sprintf(NewName, "%02d%02d-%02d%02d%02d", tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); } NewBaseName[PrefixPart] = 0; CatPath(NewBaseName, NewName); AddLetter = isdigit(NewBaseName[strlen(NewBaseName)-1]); for (a=0;;a++){ char NewName[PATH_MAX+10]; char NameExtra[3]; struct stat dummy; if (a){ // Generate a suffix for the file name if previous choice of names is taken. // depending on wether the name ends in a letter or digit, pick the opposite to separate // it. This to avoid using a separator character - this because any good separator // is before the '.' in ascii, and so sorting the names would put the later name before // the name without suffix, causing the pictures to more likely be out of order. if (AddLetter){ NameExtra[0] = (char)('a'-1+a); // Try a,b,c,d... for suffix if it ends in a number. }else{ NameExtra[0] = (char)('0'-1+a); // Try 0,1,2,3... for suffix if it ends in a latter. } NameExtra[1] = 0; }else{ NameExtra[0] = 0; } sprintf(NewName, "%s%s.jpg", NewBaseName, NameExtra); if (!strcmp(FileName, NewName)) break; // Skip if its already this name. if (!EnsurePathExists(NewBaseName)){ break; } if (stat(NewName, &dummy)){ // This name does not pre-exist. if (rename(FileName, NewName) == 0){ printf("%s --> %s\n",FileName, NewName); #ifdef _WIN32 if (RenameAssociatedFiles){ sprintf(NewName, "%s%s", NewBaseName, NameExtra); RenameAssociated(FileName, NewName); } #endif }else{ printf("Error: Couldn't rename '%s' to '%s'\n",FileName, NewName); } break; } if (a > 25 || (!AddLetter && a > 9)){ printf("Possible new names for for '%s' already exist\n",FileName); break; } } } //-------------------------------------------------------------------------- // Rotate the image and its thumbnail //-------------------------------------------------------------------------- static int DoAutoRotate(const char * FileName) { if (ImageInfo.Orientation >= 2 && ImageInfo.Orientation <= 8){ const char * Argument; Argument = ClearOrientation(); if (!ZeroRotateTagOnly){ char RotateCommand[PATH_MAX*2+50]; if (Argument == NULL){ ErrFatal("Orientation screwup"); } sprintf(RotateCommand, "jpegtran -trim -%s -outfile &o &i", Argument); ApplyCommand = RotateCommand; DoCommand(FileName, FALSE); ApplyCommand = NULL; // Now rotate the thumbnail, if there is one. if (ImageInfo.ThumbnailOffset && ImageInfo.ThumbnailSize && ImageInfo.ThumbnailAtEnd){ // Must have a thumbnail that exists and is modifieable. char ThumbTempName_in[PATH_MAX+5]; char ThumbTempName_out[PATH_MAX+5]; strcpy(ThumbTempName_in, FileName); strcat(ThumbTempName_in, ".thi"); strcpy(ThumbTempName_out, FileName); strcat(ThumbTempName_out, ".tho"); SaveThumbnail(ThumbTempName_in); sprintf(RotateCommand,"jpegtran -trim -%s -outfile \"%s\" \"%s\"", Argument, ThumbTempName_out, ThumbTempName_in); if (system(RotateCommand) == 0){ // Put the thumbnail back in the header ReplaceThumbnail(ThumbTempName_out); } unlink(ThumbTempName_in); unlink(ThumbTempName_out); } } return TRUE; } return FALSE; } //-------------------------------------------------------------------------- // Regenerate the thumbnail using mogrify //-------------------------------------------------------------------------- static int RegenerateThumbnail(const char * FileName) { char ThumbnailGenCommand[PATH_MAX*2+50]; if (ImageInfo.ThumbnailOffset == 0 || ImageInfo.ThumbnailAtEnd == FALSE){ // There is no thumbnail, or the thumbnail is not at the end. return FALSE; } sprintf(ThumbnailGenCommand, "mogrify -thumbnail %dx%d \"%s\"", RegenThumbnail, RegenThumbnail, FileName); if (system(ThumbnailGenCommand) == 0){ // Put the thumbnail back in the header return ReplaceThumbnail(FileName); }else{ ErrFatal("Unable to run 'mogrify' command"); return FALSE; } } //-------------------------------------------------------------------------- // Do selected operations to one file at a time. //-------------------------------------------------------------------------- void ProcessFile(const char * FileName) { int Modified = FALSE; ReadMode_t ReadMode; if (strlen(FileName) >= PATH_MAX-1){ // Protect against buffer overruns in strcpy / strcat's on filename ErrFatal("filename too long"); } ReadMode = READ_METADATA; CurrentFile = FileName; FilesMatched = 1; ResetJpgfile(); // Start with an empty image information structure. memset(&ImageInfo, 0, sizeof(ImageInfo)); ImageInfo.FlashUsed = -1; ImageInfo.MeteringMode = -1; ImageInfo.Whitebalance = -1; // Store file date/time. { struct stat st; if (stat(FileName, &st) >= 0){ ImageInfo.FileDateTime = st.st_mtime; ImageInfo.FileSize = st.st_size; }else{ ErrFatal("No such file"); } } if (DoModify || RenameToDate || Exif2FileTime){ if (access(FileName, 2 /*W_OK*/)){ printf("Skipping readonly file '%s'\n",FileName); return; } } strncpy(ImageInfo.FileName, FileName, PATH_MAX); if (ApplyCommand || AutoRotate){ // Applying a command is special - the headers from the file have to be // pre-read, then the command executed, and then the image part of the file read. if (!ReadJpegFile(FileName, READ_METADATA)) return; #ifdef MATTHIAS if (AutoResize){ // Automatic resize computation - to customize for each run... if (AutoResizeCmdStuff() == 0){ DiscardData(); return; } } #endif // MATTHIAS if (CheckFileSkip()){ DiscardData(); return; } DiscardAllButExif(); if (AutoRotate){ if (DoAutoRotate(FileName)){ Modified = TRUE; } }else{ struct stat dummy; DoCommand(FileName, Quiet ? FALSE : TRUE); if (stat(FileName, &dummy)){ // The file is not there anymore. Perhaps the command // was a delete or a move. So we are all done. return; } Modified = TRUE; } ReadMode = READ_IMAGE; // Don't re-read exif section again on next read. }else if (ExifXferScrFile){ char RelativeExifName[PATH_MAX+1]; // Make a relative name. RelativeName(RelativeExifName, ExifXferScrFile, FileName); if(!ReadJpegFile(RelativeExifName, READ_METADATA)) return; DiscardAllButExif(); // Don't re-read exif section again on next read. Modified = TRUE; ReadMode = READ_IMAGE; } if (DoModify){ ReadMode |= READ_IMAGE; } if (!ReadJpegFile(FileName, ReadMode)) return; if (CheckFileSkip()){ DiscardData(); return; } FileSequence += 1; // Count files processed. if (ShowConcise){ ShowConciseImageInfo(); }else{ if (!(DoModify || DoReadAction) || ShowTags){ ShowImageInfo(ShowFileInfo); { // if IPTC section is present, show it also. Section_t * IptcSection; IptcSection = FindSection(M_IPTC); if (IptcSection){ show_IPTC(IptcSection->Data, IptcSection->Size); } } printf("\n"); } } if (ThumbSaveName){ char OutFileName[PATH_MAX+1]; // Make a relative name. RelativeName(OutFileName, ThumbSaveName, FileName); if (SaveThumbnail(OutFileName)){ printf("Created: '%s'\n", OutFileName); } } if (CreateExifSection){ // Make a new minimal exif section create_EXIF(NULL, 0, 0); Modified = TRUE; } if (RegenThumbnail){ if (RegenerateThumbnail(FileName)){ Modified = TRUE; } } if (ThumbInsertName){ char ThumbFileName[PATH_MAX+1]; // Make a relative name. RelativeName(ThumbFileName, ThumbInsertName, FileName); if (ReplaceThumbnail(ThumbFileName)){ Modified = TRUE; } }else if (TrimExif){ // Deleting thumbnail is just replacing it with a null thumbnail. if (ReplaceThumbnail(NULL)){ Modified = TRUE; } } if ( #ifdef MATTHIAS AddComment || RemComment || #endif EditComment || CommentInsertfileName || CommentInsertLiteral){ Section_t * CommentSec; char Comment[MAX_COMMENT_SIZE+1]; int CommentSize; CommentSec = FindSection(M_COM); if (CommentSec == NULL){ unsigned char * DummyData; DummyData = (uchar *) malloc(3); DummyData[0] = 0; DummyData[1] = 2; DummyData[2] = 0; CommentSec = CreateSection(M_COM, DummyData, 2); } CommentSize = CommentSec->Size-2; if (CommentSize > MAX_COMMENT_SIZE){ fprintf(stderr, "Truncating comment at %d chars\n",MAX_COMMENT_SIZE); CommentSize = MAX_COMMENT_SIZE; } if (CommentInsertfileName){ // Read a new comment section from file. char CommentFileName[PATH_MAX+1]; FILE * CommentFile; // Make a relative name. RelativeName(CommentFileName, CommentInsertfileName, FileName); CommentFile = fopen(CommentFileName,"r"); if (CommentFile == NULL){ printf("Could not open '%s'\n",CommentFileName); }else{ // Read it in. // Replace the section. CommentSize = fread(Comment, 1, 999, CommentFile); fclose(CommentFile); if (CommentSize < 0) CommentSize = 0; } }else if (CommentInsertLiteral){ strncpy(Comment, CommentInsertLiteral, MAX_COMMENT_SIZE); CommentSize = strlen(Comment); }else{ #ifdef MATTHIAS char CommentZt[MAX_COMMENT_SIZE+1]; memcpy(CommentZt, (char *)CommentSec->Data+2, CommentSize); CommentZt[CommentSize] = '\0'; if (ModifyDescriptComment(Comment, CommentZt)){ Modified = TRUE; CommentSize = strlen(Comment); } if (EditComment) #else memcpy(Comment, (char *)CommentSec->Data+2, CommentSize); #endif { char EditFileName[PATH_MAX+5]; strcpy(EditFileName, FileName); strcat(EditFileName, ".txt"); CommentSize = FileEditComment(EditFileName, Comment, CommentSize); } } if (strcmp(Comment, (char *)CommentSec->Data+2)){ // Discard old comment section and put a new one in. int size; size = CommentSize+2; free(CommentSec->Data); CommentSec->Size = size; CommentSec->Data = malloc(size); CommentSec->Data[0] = (uchar)(size >> 8); CommentSec->Data[1] = (uchar)(size); memcpy((CommentSec->Data)+2, Comment, size-2); Modified = TRUE; } if (!Modified){ printf("Comment not modified\n"); } } if (CommentSavefileName){ Section_t * CommentSec; CommentSec = FindSection(M_COM); if (CommentSec != NULL){ char OutFileName[PATH_MAX+1]; FILE * CommentFile; // Make a relative name. RelativeName(OutFileName, CommentSavefileName, FileName); CommentFile = fopen(OutFileName,"w"); if (CommentFile){ fwrite((char *)CommentSec->Data+2, CommentSec->Size-2 ,1, CommentFile); fclose(CommentFile); }else{ ErrFatal("Could not write comment file"); } }else{ printf("File '%s' contains no comment section\n",FileName); } } if (ExifTimeAdjust || ExifTimeSet || DateSetChars || FileTimeToExif){ if (ImageInfo.numDateTimeTags){ struct tm tm; time_t UnixTime; char TempBuf[50]; int a; Section_t * ExifSection; if (ExifTimeSet){ // A time to set was specified. UnixTime = ExifTimeSet; }else{ if (FileTimeToExif){ FileTimeAsString(ImageInfo.DateTime); } if (DateSetChars){ memcpy(ImageInfo.DateTime, DateSet, DateSetChars); a = 1970; sscanf(DateSet, "%d", &a); if (a < 1970){ strcpy(TempBuf, ImageInfo.DateTime); goto skip_unixtime; } } // A time offset to adjust by was specified. if (!Exif2tm(&tm, ImageInfo.DateTime)) goto badtime; // Convert to unix 32 bit time value, add offset, and convert back. UnixTime = mktime(&tm); if ((int)UnixTime == -1) goto badtime; UnixTime += ExifTimeAdjust; } tm = *localtime(&UnixTime); // Print to temp buffer first to avoid putting null termination in destination. // snprintf() would do the trick, hbut not available everywhere (like FreeBSD 4.4) sprintf(TempBuf, "%04d:%02d:%02d %02d:%02d:%02d", tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); skip_unixtime: ExifSection = FindSection(M_EXIF); for (a = 0; a < ImageInfo.numDateTimeTags; a++) { uchar * Pointer; Pointer = ExifSection->Data+ImageInfo.DateTimeOffsets[a]+8; memcpy(Pointer, TempBuf, 19); } memcpy(ImageInfo.DateTime, TempBuf, 19); Modified = TRUE; }else{ printf("File '%s' contains no Exif timestamp to change\n", FileName); } } if (DeleteComments){ if (RemoveSectionType(M_COM)) Modified = TRUE; } if (DeleteExif){ if (RemoveSectionType(M_EXIF)) Modified = TRUE; } if (DeleteIptc){ if (RemoveSectionType(M_IPTC)) Modified = TRUE; } if (DeleteXmp){ if (RemoveSectionType(M_XMP)) Modified = TRUE; } if (DeleteUnknown){ if (RemoveUnknownSections()) Modified = TRUE; } if (Modified){ char BackupName[PATH_MAX+5]; struct stat buf; if (!Quiet) printf("Modified: %s\n",FileName); strcpy(BackupName, FileName); strcat(BackupName, ".t"); // Remove any .old file name that may pre-exist unlink(BackupName); // Rename the old file. rename(FileName, BackupName); // Write the new file. if (WriteJpegFile(FileName)) { // Copy the access rights from original file if (stat(BackupName, &buf) == 0){ // set Unix access rights and time to new file struct utimbuf mtime; chmod(FileName, buf.st_mode); mtime.actime = buf.st_mtime; mtime.modtime = buf.st_mtime; utime(FileName, &mtime); } // Now that we are done, remove original file. unlink(BackupName); } else { // move back the backup file rename(BackupName, FileName); } } if (Exif2FileTime){ // Set the file date to the date from the exif header. if (ImageInfo.numDateTimeTags){ // Converte the file date to Unix time. struct tm tm; time_t UnixTime; struct utimbuf mtime; if (!Exif2tm(&tm, ImageInfo.DateTime)) goto badtime; UnixTime = mktime(&tm); if ((int)UnixTime == -1){ goto badtime; } mtime.actime = UnixTime; mtime.modtime = UnixTime; if (utime(FileName, &mtime) != 0){ printf("Error: Could not change time of file '%s'\n",FileName); }else{ if (!Quiet) printf("%s\n",FileName); } }else{ printf("File '%s' contains no Exif timestamp\n", FileName); } } // Feature to rename image according to date and time from camera. // I use this feature to put images from multiple digicams in sequence. if (RenameToDate){ DoFileRenaming(FileName); } DiscardData(); return; badtime: printf("Error: Time '%s': cannot convert to Unix time\n",ImageInfo.DateTime); DiscardData(); } //-------------------------------------------------------------------------- // complain about bad state of the command line. //-------------------------------------------------------------------------- static void Usage (void) { printf("Jhead is a program for manipulating settings and thumnails in Exif jpeg headers\n" "used by most Digital Cameras. v"JHEAD_VERSION" Matthias Wandel, Mar 02 2009.\n" "http://www.sentex.net/~mwandel/jhead\n" "\n"); printf("Usage: %s [options] files\n", progname); printf("Where:\n" " files path/filenames with or without wildcards\n" "[options] are:\n" "\nGENERAL METADATA:\n" " -te <name> Transfer exif header from another image file <name>\n" " Uses same name mangling as '-st' option\n" " -dc Delete comment field (as left by progs like Photoshop & Compupic)\n" " -de Strip Exif section (smaller JPEG file, but lose digicam info)\n" " -di Delete IPTC section (from Photoshop, or Picasa)\n" " -dx Deletex XMP section\n" " -du Delete non image sections except for Exif and comment sections\n" " -purejpg Strip all unnecessary data from jpeg (combines -dc -de and -du)\n" " -mkexif Create new minimal exif section (overwrites pre-existing exif)\n" " -ce Edit comment field. Uses environment variable 'editor' to\n" " determine which editor to use. If editor not set, uses VI\n" " under Unix and notepad with windows\n" " -cs <name> Save comment section to a file\n" " -ci <name> Insert comment section from a file. -cs and -ci use same naming\n" " scheme as used by the -st option\n" " -cl string Insert literal comment string\n" "\nDATE / TIME MANIPULATION:\n" " -ft Set file modification time to Exif time\n" " -dsft Set Exif time to file modification time\n" " -n[format-string]\n" " Rename files according to date. Uses exif date if present, file\n" " date otherwise. If the optional format-string is not supplied,\n" " the format is mmdd-hhmmss. If a format-string is given, it is\n" " is passed to the 'strftime' function for formatting\n" " In addition to strftime format codes:\n" " '%%f' as part of the string will include the original file name\n" " '%%i' will include a sequence number, starting from 1. You can\n" " You can specify '%%03i' for example to get leading zeros.\n" " This feature is useful for ordering files from multiple digicams to\n" " sequence of taking. Only renames files whose names are mostly\n" " numerical (as assigned by digicam)\n" " The '.jpg' is automatically added to the end of the name. If the\n" " destination name already exists, a letter or digit is added to \n" " the end of the name to make it unique.\n" " The new name may include a path as part of the name. If this path\n" " does not exist, it will be created\n" " -nf[format-string]\n" " Same as -n, but rename regardless of original name\n" " -a (Windows only) Rename files with same name but different extension\n" " Use together with -n to rename .AVI files from exif in .THM files\n" " for example\n" " -ta<+|->h[:mm[:ss]]\n" " Adjust time by h:mm backwards or forwards. Useful when having\n" " taken pictures with the wrong time set on the camera, such as when\n" " traveling across time zones or DST changes. Dates can be adjusted\n" " by offsetting by 24 hours or more. For large date adjustments,\n" " use the -da option\n" " -da<date>-<date>\n" " Adjust date by large amounts. This is used to fix photos from\n" " cameras where the date got set back to the default camera date\n" " by accident or battery removal.\n" " To deal with different months and years having different numbers of\n" " days, a simple date-month-year offset would result in unexpected\n" " results. Instead, the difference is specified as desired date\n" " minus original date. Date is specified as yyyy:mm:dd or as date\n" " and time in the format yyyy:mm:dd/hh:mm:ss\n" " -ts<time> Set the Exif internal time to <time>. <time> is in the format\n" " yyyy:mm:dd-hh:mm:ss\n" " -ds<date> Set the Exif internal date. <date> is in the format YYYY:MM:DD\n" " or YYYY:MM or YYYY\n" "\nTHUMBNAIL MANIPULATION:\n" " -dt Remove exif integral thumbnails. Typically trims 10k\n" " -st <name> Save Exif thumbnail, if there is one, in file <name>\n" " If output file name contains the substring \"&i\" then the\n" " image file name is substitute for the &i. Note that quotes around\n" " the argument are required for the '&' to be passed to the program.\n" #ifndef _WIN32 " An output name of '-' causes thumbnail to be written to stdout\n" #endif " -rt <name> Replace Exif thumbnail. Can only be done with headers that\n" " already contain a thumbnail.\n" " -rgt[size] Regnerate exif thumbnail. Only works if image already\n" " contains a thumbail. size specifies maximum height or width of\n" " thumbnail. Relies on 'mogrify' programs to be on path\n" "\nROTATION TAG MANIPULATION:\n" " -autorot Invoke jpegtran to rotate images according to Exif orientation tag\n" " Note: Windows users must get jpegtran for this to work\n" " -norot Zero out the rotation tag. This to avoid some browsers from\n" " rotating the image again after you rotated it but neglected to\n" " clear the rotation tag\n" "\nOUTPUT VERBOSITY CONTROL:\n" " -h help (this text)\n" " -v even more verbose output\n" " -q Quiet (no messages on success, like Unix)\n" " -V Show jhead version\n" " -exifmap Dump header bytes, annotate. Pipe thru sort for better viewing\n" " -se Supress error messages relating to corrupt exif header structure\n" " -c concise output\n" " -nofinfo Don't show file info (name/size/date)\n" "\nFILE MATCHING AND SELECTION:\n" " -model model\n" " Only process files from digicam containing model substring in\n" " camera model description\n" " -exonly Skip all files that don't have an exif header (skip all jpegs that\n" " were not created by digicam)\n" " -cmd command\n" " Apply 'command' to every file, then re-insert exif and command\n" " sections into the image. &i will be substituted for the input file\n" " name, and &o (if &o is used). Use quotes around the command string\n" " This is most useful in conjunction with the free ImageMagick tool. \n" " For example, with my Canon S100, which suboptimally compresses\n" " jpegs I can specify\n" " jhead -cmd \"mogrify -quality 80 &i\" *.jpg\n" " to re-compress a lot of images using ImageMagick to half the size,\n" " and no visible loss of quality while keeping the exif header\n" " Another invocation I like to use is jpegtran (hard to find for\n" " windows). I type:\n" " jhead -cmd \"jpegtran -progressive &i &o\" *.jpg\n" " to convert jpegs to progressive jpegs (Unix jpegtran syntax\n" " differs slightly)\n" " -orp Only operate on 'portrait' aspect ratio images\n" " -orl Only operate on 'landscape' aspect ratio images\n" #ifdef _WIN32 " -r No longer supported. Use the ** wildcard to recurse directories\n" " with instead.\n" " examples:\n" " jhead **/*.jpg\n" " jhead \"c:\\my photos\\**\\*.jpg\"\n" #endif #ifdef MATTHIAS "\n" " -cr Remove comment tag (my way)\n" " -ca Add comment tag (my way)\n" " -ar Auto resize to fit in 1024x1024, but never less than half\n" #endif //MATTHIAS ); exit(EXIT_FAILURE); } //-------------------------------------------------------------------------- // Parse specified date or date+time from command line. //-------------------------------------------------------------------------- time_t ParseCmdDate(char * DateSpecified) { int a; struct tm tm; time_t UnixTime; tm.tm_wday = -1; tm.tm_hour = tm.tm_min = tm.tm_sec = 0; a = sscanf(DateSpecified, "%d:%d:%d/%d:%d:%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec); if (a != 3 && a < 5){ // Date must be YYYY:MM:DD, YYYY:MM:DD+HH:MM // or YYYY:MM:DD+HH:MM:SS ErrFatal("Could not parse specified date"); } tm.tm_isdst = -1; tm.tm_mon -= 1; // Adjust for unix zero-based months tm.tm_year -= 1900; // Adjust for year starting at 1900 UnixTime = mktime(&tm); if (UnixTime == -1){ ErrFatal("Specified time is invalid or out of range"); } return UnixTime; } //-------------------------------------------------------------------------- // The main program. //-------------------------------------------------------------------------- #if 0 int main (int argc, char **argv) { int argn; char * arg; progname = argv[0]; for (argn=1;argn<argc;argn++){ arg = argv[argn]; if (arg[0] != '-') break; // Filenames from here on. // General metadata options: if (!strcmp(arg,"-te")){ ExifXferScrFile = argv[++argn]; DoModify = TRUE; }else if (!strcmp(arg,"-dc")){ DeleteComments = TRUE; DoModify = TRUE; }else if (!strcmp(arg,"-de")){ DeleteExif = TRUE; DoModify = TRUE; }else if (!strcmp(arg,"-di")){ DeleteIptc = TRUE; DoModify = TRUE; }else if (!strcmp(arg,"-dx")){ DeleteXmp = TRUE; DoModify = TRUE; }else if (!strcmp(arg, "-du")){ DeleteUnknown = TRUE; DoModify = TRUE; }else if (!strcmp(arg, "-purejpg")){ DeleteExif = TRUE; DeleteComments = TRUE; DeleteIptc = TRUE; DeleteUnknown = TRUE; DeleteXmp = TRUE; DoModify = TRUE; }else if (!strcmp(arg,"-ce")){ EditComment = TRUE; DoModify = TRUE; }else if (!strcmp(arg,"-cs")){ CommentSavefileName = argv[++argn]; }else if (!strcmp(arg,"-ci")){ CommentInsertfileName = argv[++argn]; DoModify = TRUE; }else if (!strcmp(arg,"-cl")){ CommentInsertLiteral = argv[++argn]; DoModify = TRUE; }else if (!strcmp(arg,"-mkexif")){ CreateExifSection = TRUE; DoModify = TRUE; // Output verbosity control }else if (!strcmp(arg,"-h")){ Usage(); }else if (!strcmp(arg,"-v")){ ShowTags = TRUE; }else if (!strcmp(arg,"-q")){ Quiet = TRUE; }else if (!strcmp(arg,"-V")){ printf("Jhead version: "JHEAD_VERSION" Compiled: "__DATE__"\n"); exit(0); }else if (!strcmp(arg,"-exifmap")){ DumpExifMap = TRUE; }else if (!strcmp(arg,"-se")){ SupressNonFatalErrors = TRUE; }else if (!strcmp(arg,"-c")){ ShowConcise = TRUE; }else if (!strcmp(arg,"-nofinfo")){ ShowFileInfo = 0; // Thumbnail manipulation options }else if (!strcmp(arg,"-dt")){ TrimExif = TRUE; DoModify = TRUE; }else if (!strcmp(arg,"-st")){ ThumbSaveName = argv[++argn]; DoReadAction = TRUE; }else if (!strcmp(arg,"-rt")){ ThumbInsertName = argv[++argn]; DoModify = TRUE; }else if (!memcmp(arg,"-rgt", 4)){ RegenThumbnail = 160; sscanf(arg+4, "%d", &RegenThumbnail); if (RegenThumbnail > 320){ ErrFatal("Specified thumbnail geometry too big!"); } DoModify = TRUE; // Rotation tag manipulation }else if (!strcmp(arg,"-autorot")){ AutoRotate = 1; DoModify = TRUE; }else if (!strcmp(arg,"-norot")){ AutoRotate = 1; ZeroRotateTagOnly = 1; DoModify = TRUE; // Date/Time manipulation options }else if (!memcmp(arg,"-n",2)){ RenameToDate = 1; DoReadAction = TRUE; // Rename doesn't modify file, so count as read action. arg+=2; if (*arg == 'f'){ RenameToDate = 2; arg++; } if (*arg){ // A strftime format string is supplied. strftime_args = arg; #ifdef _WIN32 SlashToNative(strftime_args); #endif //printf("strftime_args = %s\n",arg); } }else if (!strcmp(arg,"-a")){ #ifndef _WIN32 ErrFatal("Error: -a only supported in Windows version"); #else RenameAssociatedFiles = TRUE; #endif }else if (!strcmp(arg,"-ft")){ Exif2FileTime = TRUE; DoReadAction = TRUE; }else if (!memcmp(arg,"-ta",3)){ // Time adjust feature. int hours, minutes, seconds, n; minutes = seconds = 0; if (arg[3] != '-' && arg[3] != '+'){ ErrFatal("Error: -ta must be followed by +/- and a time"); } n = sscanf(arg+4, "%d:%d:%d", &hours, &minutes, &seconds); if (n < 1){ ErrFatal("Error: -ta must be immediately followed by time"); } if (ExifTimeAdjust) ErrFatal("Can only use one of -da or -ta options at once"); ExifTimeAdjust = hours*3600 + minutes*60 + seconds; if (arg[3] == '-') ExifTimeAdjust = -ExifTimeAdjust; DoModify = TRUE; }else if (!memcmp(arg,"-da",3)){ // Date adjust feature (large time adjustments) time_t NewDate, OldDate = 0; char * pOldDate; NewDate = ParseCmdDate(arg+3); pOldDate = strstr(arg+1, "-"); if (pOldDate){ OldDate = ParseCmdDate(pOldDate+1); }else{ ErrFatal("Must specifiy second date for -da option"); } if (ExifTimeAdjust) ErrFatal("Can only use one of -da or -ta options at once"); ExifTimeAdjust = NewDate-OldDate; DoModify = TRUE; }else if (!memcmp(arg,"-dsft",5)){ // Set file time to date/time in exif FileTimeToExif = TRUE; DoModify = TRUE; }else if (!memcmp(arg,"-ds",3)){ // Set date feature int a; // Check date validity and copy it. Could be incompletely specified. strcpy(DateSet, "0000:01:01"); for (a=0;arg[a+3];a++){ if (isdigit(DateSet[a])){ if (!isdigit(arg[a+3])){ a = 0; break; } }else{ if (arg[a+3] != ':'){ a=0; break; } } DateSet[a] = arg[a+3]; } if (a < 4 || a > 10){ ErrFatal("Date must be in format YYYY, YYYY:MM, or YYYY:MM:DD"); } DateSetChars = a; DoModify = TRUE; }else if (!memcmp(arg,"-ts",3)){ // Set the exif time. // Time must be specified as "yyyy:mm:dd-hh:mm:ss" char * c; struct tm tm; c = strstr(arg+1, "-"); if (c) *c = ' '; // Replace '-' with a space. if (!Exif2tm(&tm, arg+3)){ ErrFatal("-ts option must be followed by time in format yyyy:mmm:dd-hh:mm:ss\n" "Example: jhead -ts2001:01:01-12:00:00 foo.jpg"); } ExifTimeSet = mktime(&tm); if ((int)ExifTimeSet == -1) ErrFatal("Time specified is out of range"); DoModify = TRUE; // File matching and selection }else if (!strcmp(arg,"-model")){ if (argn+1 >= argc) Usage(); // No extra argument. FilterModel = argv[++argn]; }else if (!strcmp(arg,"-exonly")){ ExifOnly = 1; }else if (!strcmp(arg,"-orp")){ PortraitOnly = 1; }else if (!strcmp(arg,"-orl")){ PortraitOnly = -1; }else if (!strcmp(arg,"-cmd")){ if (argn+1 >= argc) Usage(); // No extra argument. ApplyCommand = argv[++argn]; DoModify = TRUE; #ifdef MATTHIAS }else if (!strcmp(arg,"-ca")){ // Its a literal comment. Add. AddComment = argv[++argn]; DoModify = TRUE; }else if (!strcmp(arg,"-cr")){ // Its a literal comment. Remove this keyword. RemComment = argv[++argn]; DoModify = TRUE; }else if (!strcmp(arg,"-ar")){ AutoResize = TRUE; ShowConcise = TRUE; ApplyCommand = (char *)1; // Must be non null so it does commands. DoModify = TRUE; #endif // MATTHIAS }else{ printf("Argument '%s' not understood\n",arg); printf("Use jhead -h for list of arguments\n"); exit(-1); } if (argn >= argc){ // Used an extra argument - becuase the last argument // used up an extr argument. ErrFatal("Extra argument required"); } } if (argn == argc){ ErrFatal("No files to process. Use -h for help"); } if (ThumbSaveName != NULL && strcmp(ThumbSaveName, "&i") == 0){ printf("Error: By specifying \"&i\" for the thumbail name, your original file\n" " will be overwitten. If this is what you really want,\n" " specify -st \"./&i\" to override this check\n"); exit(0); } if (RegenThumbnail){ if (ThumbSaveName || ThumbInsertName){ printf("Error: Cannot regen and save or insert thumbnail in same run\n"); exit(0); } } if (EditComment){ if (CommentSavefileName != NULL || CommentInsertfileName != NULL){ printf("Error: Cannot use -ce option in combination with -cs or -ci\n"); exit(0); } } if (ExifXferScrFile){ if (FilterModel || ApplyCommand){ ErrFatal("Error: Filter by model and/or applying command to files\n" " invalid while transfering Exif headers"); } } FileSequence = 0; for (;argn<argc;argn++){ FilesMatched = FALSE; #ifdef _WIN32 SlashToNative(argv[argn]); // Use my globbing module to do fancier wildcard expansion with recursive // subdirectories under Windows. MyGlob(argv[argn], ProcessFile); #else // Under linux, don't do any extra fancy globbing - shell globbing is // pretty fancy as it is - although not as good as myglob.c ProcessFile(argv[argn]); #endif if (!FilesMatched){ fprintf(stderr, "Error: No files matched '%s'\n",argv[argn]); } } if (FileSequence == 0){ return EXIT_FAILURE; }else{ return EXIT_SUCCESS; } } #endif #endif // commented out -- security risk