//-------------------------------------------------------------------------- // Program to pull the information out of various types of EXIF digital // camera files and show it in a reasonably consistent way // // Version 3.3 // // Compiling under Windows: // Make sure you have Microsoft's compiler on the path, then run make.bat // // Dec 1999 - March 2026 // // by Matthias Wandel http://woodgears.ca //-------------------------------------------------------------------------- #ifdef _WIN32 #include #endif #include "jhead.h" #include #define JHEAD_VERSION "3.3" // 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 // Bitmasks for DoModify: #define MODIFY_ANY 1 #define READ_ANY 2 #define JPEGS_ONLY 4 #define MODIFY_JPEG 5 #define READ_JPEG 6 static int DoModify = FALSE; static int FilesMatched; static int FilesProcessed; static const char * CurrentFile; static const char * progname; // program name for error messages #ifdef _WIN32 // On my windows boxes, I grab imagemagick 7 from the source. #define IMAGEMAGICK_PROGNAME "magick" #else // Upstream repo of my linuxe, even pi os 12, is imagemagick 6.9, which uses convert #define IMAGEMAGICK_PROGNAME "convert" #endif //-------------------------------------------------------------------------- // Command line options flags static int TrimExif = FALSE; // Cut off exif beyond interesting data. static int RenameToDate = 0; // 1=rename, 2=rename all. static char * strftime_args = NULL; // Format for new file name. static int Exif2FileTime = 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 int TrimExifTrailingZeroes = FALSE; static char * ApplyCommand = NULL; // Apply this command to all images. static char * FilterModel = NULL; static int ExifOnly = FALSE; // Only do images with exif header 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 DeleteMetadata = 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 int SuppressNonFatalErrors = FALSE; // Whether or not to pint warnings on recoverable errors 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 ClearRotation = FALSE; static int ShowFileDate = TRUE; // Indicates to show file date (rurn off for testing) //-------------------------------------------------------------------------- // Error exit handler //-------------------------------------------------------------------------- void ErrFatal(const char * msg) { fprintf(stderr,"\nError : %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(const char * msg, int a1, int a2) { if (SuppressNonFatalErrors) return; fprintf(stderr,"\nNonfatal Error : "); if (CurrentFile) fprintf(stderr,"'%s' ",CurrentFile); fprintf(stderr, msg, a1, a2); fprintf(stderr, "\n"); } //-------------------------------------------------------------------------- // 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 security 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"); #ifdef _WIN32 mktemp(TempName); #else int fd = mkstemp(TempName); if (fd == -1) ErrFatal("Cannot find available temporary file name"); close(fd); #endif // 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' || ApplyCommand[a+1] == 'o'){ if (e > PATH_MAX * 2) ErrFatal("Specified command line too long"); } 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){ struct stat buf; int stat_result = stat(FileName, &buf); unlink(FileName); rename(TempName, FileName); if (stat_result == 0){ // set Unix access rights and time to new file struct utimbuf mtime; chmod(FileName, buf.st_mode); mtime.actime = buf.st_atime; mtime.modtime = buf.st_mtime; utime(FileName, &mtime); } }else{ ErrFatal("specified command did not produce expected output file"); } } } //-------------------------------------------------------------------------- // check if this file should be skipped based on contents. //-------------------------------------------------------------------------- static int CheckFileSkip(void) { if (FilterModel){ // Filtering processing by camera model. // This feature is useful when pictures from multiple cameras are collated, // 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 (GetImgExifSectionData(NULL) == NULL){ return TRUE; } } return FALSE; } //-------------------------------------------------------------------------- // Substitute 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 > NamePattern+PATH_MAX-10){ ErrFatal("Bad file name pattern"); } if (Subst){ strncpy(OutFileName, NamePattern, Subst-NamePattern); OutFileName[Subst-NamePattern] = 0; strncat(OutFileName, OrigName, PATH_MAX-1-strlen(OutFileName)); strncat(OutFileName, Subst+2, PATH_MAX-1-strlen(OutFileName)); }else{ strncpy(OutFileName, NamePattern, PATH_MAX); } } //-------------------------------------------------------------------------- // Rotate the image and its thumbnail //-------------------------------------------------------------------------- static int DoAutoRotate(const char * FileName) { if (ImageInfo.Orientation != 1){ const char * Argument; Argument = ClearOrientation(); if (Argument == NULL) return FALSE; // orientation tag in image, nothing changed. char RotateCommand[PATH_MAX*2+50]; if (strlen(Argument) == 0){ // Unknown orientation, but still modified. return TRUE; // Image is still modified. } sprintf(RotateCommand, "jpegtran -copy all -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 modifiable. 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"); SaveImgThumbnail(ThumbTempName_in); sprintf(RotateCommand,"jpegtran -copy all -trim -%s -outfile \"%s\" \"%s\"", Argument, ThumbTempName_out, ThumbTempName_in); // Disallow characters in the filenames that could be used to execute arbitrary // shell commands with system() below. if (strpbrk(FileName, "\";'&|`$")) { ErrNonfatal("Command has invalid characters.", 0, 0); unlink(ThumbTempName_in); return FALSE; } if (system(RotateCommand) == 0){ // Put the thumbnail back in the header ReplaceImgThumbnail(ThumbTempName_out); } unlink(ThumbTempName_in); unlink(ThumbTempName_out); } return TRUE; } return FALSE; } //-------------------------------------------------------------------------- // Regenerate the thumbnail using imagemagick //-------------------------------------------------------------------------- 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; } // Disallow characters in the filename that could be used to execute arbitrary // shell commands with system() below. if(strpbrk(FileName, "\";'&|`$")) { ErrNonfatal("Filename has invalid characters.", 0, 0); return FALSE; } // This command replaces the current image with the thumbnail. We have the image // loaded it RAM at this point, so we will recreate it after. snprintf(ThumbnailGenCommand, sizeof(ThumbnailGenCommand), IMAGEMAGICK_PROGNAME" \"%s\" -thumbnail %dx%d -quality 80 \"%s-t\"", FileName, RegenThumbnail, RegenThumbnail, FileName); if (system(ThumbnailGenCommand) == 0){ // Put the thumbnail back in the header //Reuse command buffer for thumbnail name snprintf(ThumbnailGenCommand, sizeof(ThumbnailGenCommand),"%s-t",FileName); return ReplaceImgThumbnail(ThumbnailGenCommand); unlink(ThumbnailGenCommand); // Erase temprary thumbnail file }else{ ErrFatal("Unable to run imagemagick '"IMAGEMAGICK_PROGNAME"' command"); return FALSE; } } //-------------------------------------------------------------------------- // 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); } //-------------------------------------------------------------------------- // Do selected operations to one file at a time. //-------------------------------------------------------------------------- static 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; ResetImgfile(); Clear_EXIF(); // Start with an empty image information structure. memset(&ImageInfo, 0, sizeof(ImageInfo)); ImageInfo.FlashUsed = -1; ImageInfo.MeteringMode = -1; ImageInfo.Whitebalance = -1; ImageInfo.IsColor = -1; ImageInfo.Process = -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 & MODIFY_ANY) || 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 (!ReadImgFile(FileName, READ_METADATA)) return; if (ImageInfo.ImgTypeLoaded != IMG_TYPE_JPEG){ printf("This option for Jpeg files only\n"); DiscardImgData(); return; } if (CheckFileSkip()){ DiscardImgData(); return; } DiscardAllJpegSectionsButExif(); 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. } if (DoModify){ ReadMode |= READ_IMAGE; } if (!ReadImgFile(FileName, ReadMode)) return; if (CheckFileSkip()){ DiscardImgData(); return; } if (TrimExifTrailingZeroes){ Modified |= TrimImgExifTrailingZeros(); } FilesProcessed += 1; // Count files processed. if (ShowConcise){ ShowConciseImageInfo(); }else{ if (!(DoModify) || ShowTags){ ShowImageInfo(ShowFileDate); { // if IPTC section is present, show it also. ImgSect_t * IptcSection; IptcSection = FindImgSection(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 (SaveImgThumbnail(OutFileName)){ printf("Created: '%s'\n", OutFileName); } } if (CreateExifSection){ // Make a new minimal exif section, replacing the old one CreateImgExif(); 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 (ReplaceImgThumbnail(ThumbFileName)){ Modified = TRUE; } }else if (TrimExif){ // Deleting thumbnail is just replacing it with a null thumbnail. if (ReplaceImgThumbnail(NULL)){ Modified = TRUE; } } if (CommentInsertfileName || CommentInsertLiteral){ //printf("Current comment:'%s'\n",ImageInfo.Comments); char Comment[MAX_COMMENT_SIZE+1]; int CommentSize; 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, MAX_COMMENT_SIZE, CommentFile); fclose(CommentFile); if (CommentSize < 0) CommentSize = 0; } }else if (CommentInsertLiteral){ strncpy(Comment, CommentInsertLiteral, MAX_COMMENT_SIZE); CommentSize = strlen(Comment); } SetImgCommentTo(Comment); Modified = TRUE; } if (ClearRotation){ printf("clear rotation\n"); if (ClearOrientation()){ Modified = TRUE; } } if (ExifTimeAdjust || ExifTimeSet || DateSetChars || FileTimeToExif){ uchar * Data; unsigned Len; Data = GetImgExifSectionData(&Len); if (Data != NULL){ struct tm tm; time_t UnixTime; char TempBuf[50]; int a; 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, but 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: for (a = 0; a < ImageInfo.numDateTimeTags; a++) { uchar * Pointer; Pointer = Data+ImageInfo.DateTimeOffsets[a]; 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 (ImageInfo.Comments[0]){ SetImgCommentTo(NULL); Modified = TRUE; } } if (DeleteExif){ if (RemoveImgExif()) Modified = TRUE; } if (DeleteIptc){ if (ImageInfo.ImgTypeLoaded == IMG_TYPE_JPEG){ if (RemoveJpegSectionByType(M_IPTC)) Modified = TRUE; }else{ ErrFatal("Only implemented for jpeg"); } } if (DeleteXmp){ if (RemoveImgXmp()) Modified = TRUE; } if (DeleteMetadata){ if (RemoveMetadataImgSections()) 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. WriteImgFile(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); } if (Exif2FileTime){ // Set the file date to the date from the exif header. if (ImageInfo.numDateTimeTags){ // Convert 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("File '%s' time set from Exif\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, strftime_args); } DiscardImgData(); return; badtime: printf("Error: Time '%s': cannot convert to Unix time\n",ImageInfo.DateTime); DiscardImgData(); } //-------------------------------------------------------------------------- // complain about bad state of the command line. //-------------------------------------------------------------------------- static void Usage (void) { printf("Jhead is a program for manipulating settings and thumbnails in Exif jpeg headers\n" "used by most Digital Cameras. v"JHEAD_VERSION" Matthias Wandel, Oct 5 2020.\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" " -dc Delete comment field (as left by some programs)\n" " -de Strip Exif section (smaller JPEG file, but lose digicam info)\n" " -di Delete Jpeg IPTC section (from Photoshop, or Picasa)\n" " -dx Delete Jpeg XMP section\n" " -du Delete Jpeg 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" " -ci Insert comment section from a file.\n" " scheme as used by the -st option\n" " -cl string Insert literal comment string\n" " -zt Trim exif header trailing zeroes (Nikon 1 wastes 30k that way)\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" " %%d Day of month %%H Hour (24-hour)\n" " %%m Month number %%M Minute %%S Second\n" " %%y Year (2 digit 00 - 99) %%Y Year (4 digit 1980-2036)\n" " For more arguments, look up the 'strftime' function.\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.\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" " -ta<+|->h[:mm[:ss]]\n" " Adjust time by h:mm forwards or backwards. 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-\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