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 // Version 1.7
5 //
6 //
7 // Compiling under Unix:
8 // Use: cc -O3 -o jhead jhead.c exif.c -lm
9 //
10 // Compiling under Windows: Use MSVC5 or MSVC6, from command line:
11 // cl -Ox jhead.c exif.c myglob.c
12 //
13 // Dec 1999 - May 2002
14 //
15 // by Matthias Wandel (mwandel@rim.net),
16 //--------------------------------------------------------------------------
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <memory.h>
20 #include <stdlib.h>
21 #include <string.h>
22 #include <time.h>
23 #include <sys/stat.h>
24 #include <errno.h>
25 #include <ctype.h>
26
27 #define JHEAD_VERSION "1.7"
28
29 // This #define turns on features that are too very specific to
30 // how I organize my photos. Best to ignore everything inside #ifdef MATTHIAS
31 //#define MATTHIAS
32
33 #ifdef _WIN32
34 #include <process.h>
35 #include <io.h>
36 #include <sys/utime.h>
37 #else
38 #include <utime.h>
39 #include <sys/types.h>
40 #include <unistd.h>
41 #include <errno.h>
42 #include <limits.h>
43 #endif
44
45 #include "jhead.h"
46
47 static int FilesMatched;
48
49 static const char * CurrentFile;
50
51 //--------------------------------------------------------------------------
52 // Command line options flags
53 static int DoModify = FALSE;
54 int ShowTags = FALSE; // Do not show raw by default.
55
56 static int SupressNonFatalErrors = TRUE; // Wether or not to pint warnings on recoverable errors
57
58
59 #ifdef MATTHIAS
60 // This #ifdef to take out less than elegant stuff for editing
61 // the comments in a jpeg. The programs rdjpgcom and wrjpgcom
62 // included with Linux distributions do a better job.
63
64 static char * AddComment = NULL; // Add this tag.
65 static char * RemComment = NULL; // Remove this tag
66 static int AutoResize = FALSE;
67 #endif // MATTHIAS
68
69 //--------------------------------------------------------------------------
70 // Error exit handler
71 //--------------------------------------------------------------------------
ErrFatal(char * msg)72 void ErrFatal(char * msg)
73 {
74 fprintf(stderr,"Error : %s\n", msg);
75 if (CurrentFile) fprintf(stderr,"in file '%s'\n",CurrentFile);
76 exit(EXIT_FAILURE);
77 }
78
79 //--------------------------------------------------------------------------
80 // Report non fatal errors. Now that microsoft.net modifies exif headers,
81 // there's corrupted ones, and there could be more in the future.
82 //--------------------------------------------------------------------------
ErrNonfatal(char * msg,int a1,int a2)83 void ErrNonfatal(char * msg, int a1, int a2)
84 {
85 if (SupressNonFatalErrors) return;
86
87 fprintf(stderr,"Nonfatal Error : ");
88 if (CurrentFile) fprintf(stderr,"'%s' ",CurrentFile);
89 fprintf(stderr, msg, a1, a2);
90 fprintf(stderr, "\n");
91 }
92
93 //--------------------------------------------------------------------------
94 // Do selected operations to one file at a time.
95 //--------------------------------------------------------------------------
ProcessFile(const char * FileName)96 void ProcessFile(const char * FileName)
97 {
98 #ifdef APPLY_COMMAND
99 int Modified = FALSE;
100 #endif /* APPLY_COMMAND */
101 ReadMode_t ReadMode = READ_EXIF;
102
103 CurrentFile = FileName;
104
105 ResetJpgfile();
106
107 // Start with an empty image information structure.
108 memset(&ImageInfo, 0, sizeof(ImageInfo));
109 ImageInfo.FlashUsed = -1;
110 ImageInfo.MeteringMode = -1;
111
112 // Store file date/time.
113 {
114 struct stat st;
115 if (stat(FileName, &st) >= 0){
116 ImageInfo.FileDateTime = st.st_mtime;
117 ImageInfo.FileSize = st.st_size;
118 }else{
119 ErrFatal("No such file");
120 }
121 }
122
123 strncpy(ImageInfo.FileName, FileName, PATH_MAX);
124
125 FilesMatched += 1;
126
127 FilesMatched = TRUE; // Turns off complaining that nothing matched.
128
129 if (DoModify){
130 ReadMode |= READ_IMAGE;
131 }
132
133 if (!ReadJpegFile(FileName, ReadMode)) return;
134
135 #ifdef VERBOSE
136 if (CheckFileSkip()){
137 DiscardData();
138 return;
139 }
140
141 if (ShowConcise){
142 ShowConciseImageInfo();
143 }else{
144 if (!(DoModify || DoReadAction) || ShowTags){
145 ShowImageInfo();
146 }
147 }
148
149 if (ThumbnailName){
150 if (ImageInfo.ThumbnailPointer){
151 FILE * ThumbnailFile;
152 char OutFileName[PATH_MAX+1];
153
154 // Make a relative name.
155 RelativeName(OutFileName, ThumbnailName, FileName);
156
157 #ifndef _WIN32
158 if (strcmp(ThumbnailName, "-") == 0){
159 // A filename of '-' indicates thumbnail goes to stdout.
160 // This doesn't make much sense under Windows, so this feature is unix only.
161 ThumbnailFile = stdout;
162 }else
163 #endif
164 {
165 ThumbnailFile = fopen(OutFileName,"wb");
166 }
167
168 if (ThumbnailFile){
169 fwrite(ImageInfo.ThumbnailPointer, ImageInfo.ThumbnailSize ,1, ThumbnailFile);
170 fclose(ThumbnailFile);
171 if (ThumbnailFile != stdout){
172 printf("Created: '%s'\n", OutFileName);
173 }else{
174 // No point in printing to stdout when that is where the thumbnail goes!
175 }
176 }else{
177 ErrFatal("Could not write thumbnail file");
178 }
179 }else{
180 printf("Image '%s' contains no thumbnail\n",FileName);
181 }
182 }
183
184 #ifdef MATTHIAS
185 if (AddComment || RemComment|| EditComment){
186 #else
187 if (EditComment){
188 #endif
189 Section_t * CommentSec;
190 char Comment[1000];
191 int CommentSize;
192
193 CommentSec = FindSection(M_COM);
194
195 if (CommentSec == NULL){
196 unsigned char * DummyData;
197
198 DummyData = (uchar *) malloc(3);
199 DummyData[0] = 0;
200 DummyData[1] = 2;
201 DummyData[2] = 0;
202 CommentSec = CreateSection(M_COM, DummyData, 2);
203 }
204
205 CommentSize = CommentSec->Size-2;
206
207 #ifdef MATTHIAS
208 if (ModifyDescriptComment(Comment, (char *)CommentSec->Data+2)){
209 Modified = TRUE;
210 CommentSize = strlen(Comment);
211 }
212 if (EditComment)
213 #else
214 memcpy(Comment, (char *)CommentSec->Data+2, CommentSize);
215 #endif
216 {
217 char EditFileName[PATH_MAX+4];
218 strcpy(EditFileName, FileName);
219 strcat(EditFileName, ".txt");
220
221 CommentSize = FileEditComment(EditFileName, Comment, CommentSize);
222 }
223
224 if (strcmp(Comment, (char *)CommentSec->Data+2)){
225 // Discard old comment section and put a new one in.
226 int size;
227 size = CommentSize+2;
228 free(CommentSec->Data);
229 CommentSec->Size = size;
230 CommentSec->Data = malloc(size);
231 CommentSec->Data[0] = (uchar)(size >> 8);
232 CommentSec->Data[1] = (uchar)(size);
233 memcpy((CommentSec->Data)+2, Comment, size-2);
234 Modified = TRUE;
235 }
236 if (!Modified){
237 printf("Comment not modified\n");
238 }
239 }
240
241 if (ExifTimeAdjust || ExifTimeSet){
242 if (ImageInfo.DatePointer){
243 struct tm tm;
244 time_t UnixTime;
245 char TempBuf[50];
246
247 if (ExifTimeSet){
248 // A time to set was specified.
249 UnixTime = ExifTimeSet;
250 }else{
251 // A time offset to adjust by was specified.
252 if (!Exif2tm(&tm, ImageInfo.DateTime)) goto badtime;
253
254 // Convert to unix 32 bit time value, add offset, and convert back.
255 UnixTime = mktime(&tm);
256 if ((int)UnixTime == -1) goto badtime;
257 UnixTime += ExifTimeAdjust;
258 }
259 tm = *localtime(&UnixTime);
260
261 // Print to temp buffer first to avoid putting null termination in destination.
262 // snprintf() would do the trick ,but not available everywhere (like FreeBSD 4.4)
263 sprintf(TempBuf, "%04d:%02d:%02d %02d:%02d:%02d",
264 tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
265 tm.tm_hour, tm.tm_min, tm.tm_sec);
266
267 memcpy(ImageInfo.DatePointer, TempBuf, 19);
268
269 Modified = TRUE;
270 }else{
271 printf("File '%s' contains no Exif timestamp to change\n", FileName);
272 }
273 }
274
275 if (TrimExif){
276 if (TrimExifFunc()) Modified = TRUE;
277 }
278
279 if (DeleteComments){
280 if (RemoveSectionType(M_COM)) Modified = TRUE;
281 }
282 if (DeleteExif){
283 if (RemoveSectionType(M_EXIF)) Modified = TRUE;
284 }
285
286
287 if (Modified){
288 char BackupName[400];
289 printf("Modified: %s\n",FileName);
290
291 strcpy(BackupName, FileName);
292 strcat(BackupName, ".t");
293
294 // Remove any .old file name that may pre-exist
295 unlink(BackupName);
296
297 // Rename the old file.
298 rename(FileName, BackupName);
299
300 // Write the new file.
301 WriteJpegFile(FileName);
302
303 // Now that we are done, remove original file.
304 unlink(BackupName);
305 }
306
307
308 if (Exif2FileTime){
309 // Set the file date to the date from the exif header.
310 if (ImageInfo.DateTime[0]){
311 // Converte the file date to Unix time.
312 struct tm tm;
313 time_t UnixTime;
314 struct utimbuf mtime;
315 if (!Exif2tm(&tm, ImageInfo.DateTime)) goto badtime;
316
317 UnixTime = mktime(&tm);
318 if ((int)UnixTime == -1){
319 goto badtime;
320 }
321
322 mtime.actime = UnixTime;
323 mtime.modtime = UnixTime;
324
325 if (utime(FileName, &mtime) != 0){
326 printf("Error: Could not change time of file '%s'\n",FileName);
327 }else{
328 printf("%s\n",FileName);
329 }
330 }else{
331 printf("File '%s' contains no Exif timestamp\n", FileName);
332 }
333 }
334
335 // Feature to rename image according to date and time from camera.
336 // I use this feature to put images from multiple digicams in sequence.
337
338 if (RenameToDate){
339 int NumAlpha = 0;
340 int NumDigit = 0;
341 int PrefixPart = 0;
342
343 for (a=0;FileName[a];a++){
344 if (FileName[a] == '/' || FileName[a] == '\\'){
345 // Don't count path compoenent.
346 NumAlpha = 0;
347 NumDigit = 0;
348 PrefixPart = a+1;
349 }
350 if (isalpha(FileName[a])) NumAlpha += 1;
351 if (isdigit(FileName[a])) NumDigit += 1;
352 }
353
354 if ((NumAlpha <= 8 && NumDigit >= 2) || RenameToDate > 1){
355 if (ImageInfo.DateTime[0]){
356 struct tm tm;
357 if (Exif2tm(&tm, ImageInfo.DateTime)){
358 char NewBaseName[PATH_MAX*2];
359
360 strcpy(NewBaseName, FileName); // Get path component of name.
361
362 if (strftime_args){
363 // Complicated scheme for flexibility. Just pass the args to strftime.
364 time_t UnixTime;
365
366 // Call mktime to get weekday and such filled in.
367 UnixTime = mktime(&tm);
368 if ((int)UnixTime == -1) goto badtime;
369 strftime(NewBaseName+PrefixPart, 99, strftime_args, &tm);
370 }else{
371 // My favourite scheme.
372 sprintf(NewBaseName+PrefixPart, "%02d%02d-%02d%02d%02d",
373 tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
374 }
375
376 for (a=0;;a++){
377 char NewName[120];
378 char NameExtra[3];
379 struct stat dummy;
380
381 if (a){
382 // Generate a suffix for the file name if previous choice of names is taken.
383 // depending on wether the name ends in a letter or digit, pick the opposite to separate
384 // it. This to avoid using a separator character - this because any good separator
385 // is before the '.' in ascii, and so sorting the names would put the later name before
386 // the name without suffix, causing the pictures to more likely be out of order.
387 if (isdigit(NewBaseName[strlen(NewBaseName)-1])){
388 NameExtra[0] = 'a'-1+a; // Try a,b,c,d... for suffix if it ends in a letter.
389 }else{
390 NameExtra[0] = '0'-1+a; // Try 1,2,3,4... for suffix if it ends in a char.
391 }
392 NameExtra[1] = 0;
393 }else{
394 NameExtra[0] = 0;
395 }
396
397 sprintf(NewName, "%s%s.jpg", NewBaseName, NameExtra);
398
399 if (!strcmp(FileName, NewName)) break; // Skip if its already this name.
400
401 if (stat(NewName, &dummy)){
402 // This name does not pre-exist.
403 if (rename(FileName, NewName) == 0){
404 printf("%s --> %s\n",FileName, NewName);
405 }else{
406 printf("Error: Couldn't rename '%s' to '%s'\n",FileName, NewName);
407 }
408 break;
409 }
410
411 if (a >= 9){
412 printf("Dest name for '%s' already exists\n",FileName);
413 break;
414 }
415 }
416 }else{
417 printf("File '%s' contains no Exif timestamp\n", FileName);
418 }
419 }
420 }
421 }
422 if(0){
423 badtime:
424 printf("Error: Time '%s': cannot convert to Unix time\n",ImageInfo.DateTime);
425 }
426 DiscardData();
427 #endif /* VERBOSE */
428 }
429
430