Merge our EXIF.py with upstream version 1.0.2
[joel/kofoto.git] / src / packages / kofoto / EXIF.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 # pylint: disable-msg=W0311, W0622, C0103, W0402, C0322, W0141, C0302
5 # pylint: disable-msg=R0913, W0621, W0612, R0914, W0102, W0702, R0912, R0915
6 # pylint: disable-msg=W0702
7
8 # Library to extract EXIF information in digital camera image files
9 #
10 # To use this library call with:
11 #    f=open(path_name, 'rb')
12 #    tags=EXIF.process_file(f)
13 #
14 # If you want to ignore makerNote, pass the -q or --quick
15 # command line arguments, or as
16 #    tags=EXIF.process_file(f, details=False)
17 #
18 # This is usefull when you are retrieving a large list of images
19 #
20 # Returned tags will be a dictionary mapping names of EXIF tags to their
21 # values in the file named by path_name.  You can process the tags
22 # as you wish.  In particular, you can iterate through all the tags with:
23 #     for tag in tags.keys():
24 #         if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename',
25 #                        'EXIF MakerNote'):
26 #             print "Key: %s, value %s" % (tag, tags[tag])
27 # (This code uses the if statement to avoid printing out a few of the
28 # tags that tend to be long or boring.)
29 #
30 # The tags dictionary will include keys for all of the usual EXIF
31 # tags, and will also include keys for Makernotes used by some
32 # cameras, for which we have a good specification.
33 #
34 # Contains code from "exifdump.py" originally written by Thierry Bousch
35 # <bousch@topo.math.u-psud.fr> and released into the public domain.
36 #
37 # Updated and turned into general-purpose library by Gene Cash
38 #
39 # This copyright license is intended to be similar to the FreeBSD license.
40 #
41 # Copyright (c) 2002 Gene Cash All rights reserved.
42 #
43 # Redistribution and use in source and binary forms, with or without
44 # modification, are permitted provided that the following conditions are
45 # met:
46 #
47 #    1. Redistributions of source code must retain the above copyright
48 #       notice, this list of conditions and the following disclaimer.
49 #    2. Redistributions in binary form must reproduce the above copyright
50 #       notice, this list of conditions and the following disclaimer in the
51 #       documentation and/or other materials provided with the
52 #       distribution.
53 #
54 # THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR
55 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
56 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
57 # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
58 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
59 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
60 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
61 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
62 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
63 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
64 # POSSIBILITY OF SUCH DAMAGE.
65 #
66 # This means you may do anything you want with this code, except claim you
67 # wrote it. Also, if it breaks you get to keep both pieces.
68 #
69 # Patch Contributors:
70 # * Simon J. Gerraty <sjg@crufty.net>
71 #   s2n fix & orientation decode
72 # * John T. Riedl <riedl@cs.umn.edu>
73 #   Added support for newer Nikon type 3 Makernote format for D70 and some
74 #   other Nikon cameras.
75 # * Joerg Schaefer <schaeferj@gmx.net>
76 #   Fixed subtle bug when faking an EXIF header, which affected maker notes
77 #   using relative offsets, and a fix for Nikon D100.
78 #
79 # 21-AUG-99 TB  Last update by Thierry Bousch to his code.
80 # 17-JAN-02 CEC Discovered code on web.
81 #               Commented everything.
82 #               Made small code improvements.
83 #               Reformatted for readability.
84 # 19-JAN-02 CEC Added ability to read TIFFs and JFIF-format JPEGs.
85 #               Added ability to extract JPEG formatted thumbnail.
86 #               Added ability to read GPS IFD (not tested).
87 #               Converted IFD data structure to dictionaries indexed by
88 #               tag name.
89 #               Factored into library returning dictionary of IFDs plus
90 #               thumbnail, if any.
91 # 20-JAN-02 CEC Added MakerNote processing logic.
92 #               Added Olympus MakerNote.
93 #               Converted data structure to single-level dictionary, avoiding
94 #               tag name collisions by prefixing with IFD name.  This makes
95 #               it much easier to use.
96 # 23-JAN-02 CEC Trimmed nulls from end of string values.
97 # 25-JAN-02 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote.
98 # 26-JAN-02 CEC Added ability to extract TIFF thumbnails.
99 #               Added Nikon, Fujifilm, Casio MakerNotes.
100 # 30-NOV-03 CEC Fixed problem with canon_decode_tag() not creating an
101 #               IFD_Tag() object.
102 # 15-FEB-04 CEC Finally fixed bit shift warning by converting Y to 0L.
103 #
104 # ---------------------------- End original notices ------------------------- #
105
106 # 2007-18-01 - Ianaré Sévi <ianare@gmail.com>
107 # Fixed a couple errors and assuming maintenance of the library.
108
109 # 2007-24-03 - Ianaré Sévi
110 # Can now ignore MakerNotes Tags for faster processing.
111
112 # 2007-03-05 - Martin Stone <mj_stone@users.sourceforge.net>
113 # Fix for inverted detailed flag and Photoshop header
114
115
116
117
118
119 # ===== CODE START ==== #
120
121 # field type descriptions as (length, abbreviation, full name) tuples
122 FIELD_TYPES=(
123     (0, 'X', 'Proprietary'), # no such type
124     (1, 'B', 'Byte'),
125     (1, 'A', 'ASCII'),
126     (2, 'S', 'Short'),
127     (4, 'L', 'Long'),
128     (8, 'R', 'Ratio'),
129     (1, 'SB', 'Signed Byte'),
130     (1, 'U', 'Undefined'),
131     (2, 'SS', 'Signed Short'),
132     (4, 'SL', 'Signed Long'),
133     (8, 'SR', 'Signed Ratio')
134     )
135
136 # dictionary of main EXIF tag names
137 # first element of tuple is tag name, optional second element is
138 # another dictionary giving names to values
139 EXIF_TAGS={
140     0x0100: ('ImageWidth', ),
141     0x0101: ('ImageLength', ),
142     0x0102: ('BitsPerSample', ),
143     0x0103: ('Compression',
144              {1: 'Uncompressed TIFF',
145               6: 'JPEG Compressed'}),
146     0x0106: ('PhotometricInterpretation', ),
147     0x010A: ('FillOrder', ),
148     0x010D: ('DocumentName', ),
149     0x010E: ('ImageDescription', ),
150     0x010F: ('Make', ),
151     0x0110: ('Model', ),
152     0x0111: ('StripOffsets', ),
153     0x0112: ('Orientation',
154              {1: 'Horizontal (normal)',
155               2: 'Mirrored horizontal',
156               3: 'Rotated 180',
157               4: 'Mirrored vertical',
158               5: 'Mirrored horizontal then rotated 90 CCW',
159               6: 'Rotated 90 CW',
160               7: 'Mirrored horizontal then rotated 90 CW',
161               8: 'Rotated 90 CCW'}),
162     0x0115: ('SamplesPerPixel', ),
163     0x0116: ('RowsPerStrip', ),
164     0x0117: ('StripByteCounts', ),
165     0x011A: ('XResolution', ),
166     0x011B: ('YResolution', ),
167     0x011C: ('PlanarConfiguration', ),
168     0x0128: ('ResolutionUnit',
169              {1: 'Not Absolute',
170               2: 'Pixels/Inch',
171               3: 'Pixels/Centimeter'}),
172     0x012D: ('TransferFunction', ),
173     0x0131: ('Software', ),
174     0x0132: ('DateTime', ),
175     0x013B: ('Artist', ),
176     0x013E: ('WhitePoint', ),
177     0x013F: ('PrimaryChromaticities', ),
178     0x0156: ('TransferRange', ),
179     0x0200: ('JPEGProc', ),
180     0x0201: ('JPEGInterchangeFormat', ),
181     0x0202: ('JPEGInterchangeFormatLength', ),
182     0x0211: ('YCbCrCoefficients', ),
183     0x0212: ('YCbCrSubSampling', ),
184     0x0213: ('YCbCrPositioning', ),
185     0x0214: ('ReferenceBlackWhite', ),
186     0x828D: ('CFARepeatPatternDim', ),
187     0x828E: ('CFAPattern', ),
188     0x828F: ('BatteryLevel', ),
189     0x8298: ('Copyright', ),
190     0x829A: ('ExposureTime', ),
191     0x829D: ('FNumber', ),
192     0x83BB: ('IPTC/NAA', ),
193     0x8769: ('ExifOffset', ),
194     0x8773: ('InterColorProfile', ),
195     0x8822: ('ExposureProgram',
196              {0: 'Unidentified',
197               1: 'Manual',
198               2: 'Program Normal',
199               3: 'Aperture Priority',
200               4: 'Shutter Priority',
201               5: 'Program Creative',
202               6: 'Program Action',
203               7: 'Portrait Mode',
204               8: 'Landscape Mode'}),
205     0x8824: ('SpectralSensitivity', ),
206     0x8825: ('GPSInfo', ),
207     0x8827: ('ISOSpeedRatings', ),
208     0x8828: ('OECF', ),
209     # print as string
210     0x9000: ('ExifVersion', lambda x: ''.join(map(chr, x))),
211     0x9003: ('DateTimeOriginal', ),
212     0x9004: ('DateTimeDigitized', ),
213     0x9101: ('ComponentsConfiguration',
214              {0: '',
215               1: 'Y',
216               2: 'Cb',
217               3: 'Cr',
218               4: 'Red',
219               5: 'Green',
220               6: 'Blue'}),
221     0x9102: ('CompressedBitsPerPixel', ),
222     0x9201: ('ShutterSpeedValue', ),
223     0x9202: ('ApertureValue', ),
224     0x9203: ('BrightnessValue', ),
225     0x9204: ('ExposureBiasValue', ),
226     0x9205: ('MaxApertureValue', ),
227     0x9206: ('SubjectDistance', ),
228     0x9207: ('MeteringMode',
229              {0: 'Unidentified',
230               1: 'Average',
231               2: 'CenterWeightedAverage',
232               3: 'Spot',
233               4: 'MultiSpot'}),
234     0x9208: ('LightSource',
235              {0:   'Unknown',
236               1:   'Daylight',
237               2:   'Fluorescent',
238               3:   'Tungsten',
239               10:  'Flash',
240               17:  'Standard Light A',
241               18:  'Standard Light B',
242               19:  'Standard Light C',
243               20:  'D55',
244               21:  'D65',
245               22:  'D75',
246               255: 'Other'}),
247     0x9209: ('Flash', {0:  'No',
248                        1:  'Fired',
249                        5:  'Fired (?)', # no return sensed
250                        7:  'Fired (!)', # return sensed
251                        9:  'Fill Fired',
252                        13: 'Fill Fired (?)',
253                        15: 'Fill Fired (!)',
254                        16: 'Off',
255                        24: 'Auto Off',
256                        25: 'Auto Fired',
257                        29: 'Auto Fired (?)',
258                        31: 'Auto Fired (!)',
259                        32: 'Not Available'}),
260     0x920A: ('FocalLength', ),
261     0x927C: ('MakerNote', ),
262     # print as string
263     0x9286: ('UserComment', lambda x: ''.join(map(chr, x))),
264     0x9290: ('SubSecTime', ),
265     0x9291: ('SubSecTimeOriginal', ),
266     0x9292: ('SubSecTimeDigitized', ),
267     # print as string
268     0xA000: ('FlashPixVersion', lambda x: ''.join(map(chr, x))),
269     0xA001: ('ColorSpace', ),
270     0xA002: ('ExifImageWidth', ),
271     0xA003: ('ExifImageLength', ),
272     0xA005: ('InteroperabilityOffset', ),
273     0xA20B: ('FlashEnergy', ),               # 0x920B in TIFF/EP
274     0xA20C: ('SpatialFrequencyResponse', ),  # 0x920C    -  -
275     0xA20E: ('FocalPlaneXResolution', ),     # 0x920E    -  -
276     0xA20F: ('FocalPlaneYResolution', ),     # 0x920F    -  -
277     0xA210: ('FocalPlaneResolutionUnit', ),  # 0x9210    -  -
278     0xA214: ('SubjectLocation', ),           # 0x9214    -  -
279     0xA215: ('ExposureIndex', ),             # 0x9215    -  -
280     0xA217: ('SensingMethod', ),             # 0x9217    -  -
281     0xA300: ('FileSource',
282              {3: 'Digital Camera'}),
283     0xA301: ('SceneType',
284              {1: 'Directly Photographed'}),
285     0xA302: ('CVAPattern',),
286     }
287
288 # interoperability tags
289 INTR_TAGS={
290     0x0001: ('InteroperabilityIndex', ),
291     0x0002: ('InteroperabilityVersion', ),
292     0x1000: ('RelatedImageFileFormat', ),
293     0x1001: ('RelatedImageWidth', ),
294     0x1002: ('RelatedImageLength', ),
295     }
296
297 # GPS tags (not used yet, haven't seen camera with GPS)
298 GPS_TAGS={
299     0x0000: ('GPSVersionID', ),
300     0x0001: ('GPSLatitudeRef', ),
301     0x0002: ('GPSLatitude', ),
302     0x0003: ('GPSLongitudeRef', ),
303     0x0004: ('GPSLongitude', ),
304     0x0005: ('GPSAltitudeRef', ),
305     0x0006: ('GPSAltitude', ),
306     0x0007: ('GPSTimeStamp', ),
307     0x0008: ('GPSSatellites', ),
308     0x0009: ('GPSStatus', ),
309     0x000A: ('GPSMeasureMode', ),
310     0x000B: ('GPSDOP', ),
311     0x000C: ('GPSSpeedRef', ),
312     0x000D: ('GPSSpeed', ),
313     0x000E: ('GPSTrackRef', ),
314     0x000F: ('GPSTrack', ),
315     0x0010: ('GPSImgDirectionRef', ),
316     0x0011: ('GPSImgDirection', ),
317     0x0012: ('GPSMapDatum', ),
318     0x0013: ('GPSDestLatitudeRef', ),
319     0x0014: ('GPSDestLatitude', ),
320     0x0015: ('GPSDestLongitudeRef', ),
321     0x0016: ('GPSDestLongitude', ),
322     0x0017: ('GPSDestBearingRef', ),
323     0x0018: ('GPSDestBearing', ),
324     0x0019: ('GPSDestDistanceRef', ),
325     0x001A: ('GPSDestDistance', )
326     }
327
328 # ignore these tags when quick processing
329 # 0x927C is MakerNote Tags
330 # 0x9286 is user comment
331 IGNORE_TAGS=(0x9286, 0x927C)
332
333
334 # Nikon E99x MakerNote Tags
335 # http://members.tripod.com/~tawba/990exif.htm
336 MAKERNOTE_NIKON_NEWER_TAGS={
337     0x0002: ('ISOSetting', ),
338     0x0003: ('ColorMode', ),
339     0x0004: ('Quality', ),
340     0x0005: ('Whitebalance', ),
341     0x0006: ('ImageSharpening', ),
342     0x0007: ('FocusMode', ),
343     0x0008: ('FlashSetting', ),
344     0x0009: ('AutoFlashMode', ),
345     0x000B: ('WhiteBalanceBias', ),
346     0x000C: ('WhiteBalanceRBCoeff', ),
347     0x000F: ('ISOSelection', ),
348     0x0012: ('FlashCompensation', ),
349     0x0013: ('ISOSpeedRequested', ),
350     0x0016: ('PhotoCornerCoordinates', ),
351     0x0018: ('FlashBracketCompensationApplied', ),
352     0x0019: ('AEBracketCompensationApplied', ),
353     0x0080: ('ImageAdjustment', ),
354     0x0081: ('ToneCompensation', ),
355     0x0082: ('AuxiliaryLens', ),
356     0x0083: ('LensType', ),
357     0x0084: ('LensMinMaxFocalMaxAperture', ),
358     0x0085: ('ManualFocusDistance', ),
359     0x0086: ('DigitalZoomFactor', ),
360     0x0088: ('AFFocusPosition',
361              {0x0000: 'Center',
362               0x0100: 'Top',
363               0x0200: 'Bottom',
364               0x0300: 'Left',
365               0x0400: 'Right'}),
366     0x0089: ('BracketingMode',
367              {0x00: 'Single frame, no bracketing',
368               0x01: 'Continuous, no bracketing',
369               0x02: 'Timer, no bracketing',
370               0x10: 'Single frame, exposure bracketing',
371               0x11: 'Continuous, exposure bracketing',
372               0x12: 'Timer, exposure bracketing',
373               0x40: 'Single frame, white balance bracketing',
374               0x41: 'Continuous, white balance bracketing',
375               0x42: 'Timer, white balance bracketing'}),
376     0x008D: ('ColorMode', ),
377     0x008F: ('SceneMode?', ),
378     0x0090: ('LightingType', ),
379     0x0092: ('HueAdjustment', ),
380     0x0094: ('Saturation',
381              {-3: 'B&W',
382               -2: '-2',
383               -1: '-1',
384               0:  '0',
385               1:  '1',
386               2:  '2'}),
387     0x0095: ('NoiseReduction', ),
388     0x00A7: ('TotalShutterReleases', ),
389     0x00A9: ('ImageOptimization', ),
390     0x00AA: ('Saturation', ),
391     0x00AB: ('DigitalVariProgram', ),
392     0x0010: ('DataDump', )
393     }
394
395 MAKERNOTE_NIKON_OLDER_TAGS={
396     0x0003: ('Quality',
397              {1: 'VGA Basic',
398               2: 'VGA Normal',
399               3: 'VGA Fine',
400               4: 'SXGA Basic',
401               5: 'SXGA Normal',
402               6: 'SXGA Fine'}),
403     0x0004: ('ColorMode',
404              {1: 'Color',
405               2: 'Monochrome'}),
406     0x0005: ('ImageAdjustment',
407              {0: 'Normal',
408               1: 'Bright+',
409               2: 'Bright-',
410               3: 'Contrast+',
411               4: 'Contrast-'}),
412     0x0006: ('CCDSpeed',
413              {0: 'ISO 80',
414               2: 'ISO 160',
415               4: 'ISO 320',
416               5: 'ISO 100'}),
417     0x0007: ('WhiteBalance',
418              {0: 'Auto',
419               1: 'Preset',
420               2: 'Daylight',
421               3: 'Incandescent',
422               4: 'Fluorescent',
423               5: 'Cloudy',
424               6: 'Speed Light'})
425     }
426
427 # decode Olympus SpecialMode tag in MakerNote
428 def olympus_special_mode(v):
429     a={
430         0: 'Normal',
431         1: 'Unknown',
432         2: 'Fast',
433         3: 'Panorama'}
434     b={
435         0: 'Non-panoramic',
436         1: 'Left to right',
437         2: 'Right to left',
438         3: 'Bottom to top',
439         4: 'Top to bottom'}
440     return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
441         
442 MAKERNOTE_OLYMPUS_TAGS={
443     # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
444     # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
445     0x0100: ('JPEGThumbnail', ),
446     0x0200: ('SpecialMode', olympus_special_mode),
447     0x0201: ('JPEGQual',
448              {1: 'SQ',
449               2: 'HQ',
450               3: 'SHQ'}),
451     0x0202: ('Macro',
452              {0: 'Normal',
453               1: 'Macro'}),
454     0x0204: ('DigitalZoom', ),
455     0x0207: ('SoftwareRelease',  ),
456     0x0208: ('PictureInfo',  ),
457     # print as string
458     0x0209: ('CameraID', lambda x: ''.join(map(chr, x))),
459     0x0F00: ('DataDump',  )
460     }
461
462 MAKERNOTE_CASIO_TAGS={
463     0x0001: ('RecordingMode',
464              {1: 'Single Shutter',
465               2: 'Panorama',
466               3: 'Night Scene',
467               4: 'Portrait',
468               5: 'Landscape'}),
469     0x0002: ('Quality',
470              {1: 'Economy',
471               2: 'Normal',
472               3: 'Fine'}),
473     0x0003: ('FocusingMode',
474              {2: 'Macro',
475               3: 'Auto Focus',
476               4: 'Manual Focus',
477               5: 'Infinity'}),
478     0x0004: ('FlashMode',
479              {1: 'Auto',
480               2: 'On',
481               3: 'Off',
482               4: 'Red Eye Reduction'}),
483     0x0005: ('FlashIntensity',
484              {11: 'Weak',
485               13: 'Normal',
486               15: 'Strong'}),
487     0x0006: ('Object Distance', ),
488     0x0007: ('WhiteBalance',
489              {1:   'Auto',
490               2:   'Tungsten',
491               3:   'Daylight',
492               4:   'Fluorescent',
493               5:   'Shade',
494               129: 'Manual'}),
495     0x000B: ('Sharpness',
496              {0: 'Normal',
497               1: 'Soft',
498               2: 'Hard'}),
499     0x000C: ('Contrast',
500              {0: 'Normal',
501               1: 'Low',
502               2: 'High'}),
503     0x000D: ('Saturation',
504              {0: 'Normal',
505               1: 'Low',
506               2: 'High'}),
507     0x0014: ('CCDSpeed',
508              {64:  'Normal',
509               80:  'Normal',
510               100: 'High',
511               125: '+1.0',
512               244: '+3.0',
513               250: '+2.0',})
514     }
515
516 MAKERNOTE_FUJIFILM_TAGS={
517     0x0000: ('NoteVersion', lambda x: ''.join(map(chr, x))),
518     0x1000: ('Quality', ),
519     0x1001: ('Sharpness',
520              {1: 'Soft',
521               2: 'Soft',
522               3: 'Normal',
523               4: 'Hard',
524               5: 'Hard'}),
525     0x1002: ('WhiteBalance',
526              {0:    'Auto',
527               256:  'Daylight',
528               512:  'Cloudy',
529               768:  'DaylightColor-Fluorescent',
530               769:  'DaywhiteColor-Fluorescent',
531               770:  'White-Fluorescent',
532               1024: 'Incandescent',
533               3840: 'Custom'}),
534     0x1003: ('Color',
535              {0:   'Normal',
536               256: 'High',
537               512: 'Low'}),
538     0x1004: ('Tone',
539              {0:   'Normal',
540               256: 'High',
541               512: 'Low'}),
542     0x1010: ('FlashMode',
543              {0: 'Auto',
544               1: 'On',
545               2: 'Off',
546               3: 'Red Eye Reduction'}),
547     0x1011: ('FlashStrength', ),
548     0x1020: ('Macro',
549              {0: 'Off',
550               1: 'On'}),
551     0x1021: ('FocusMode',
552              {0: 'Auto',
553               1: 'Manual'}),
554     0x1030: ('SlowSync',
555              {0: 'Off',
556               1: 'On'}),
557     0x1031: ('PictureMode',
558              {0:   'Auto',
559               1:   'Portrait',
560               2:   'Landscape',
561               4:   'Sports',
562               5:   'Night',
563               6:   'Program AE',
564               256: 'Aperture Priority AE',
565               512: 'Shutter Priority AE',
566               768: 'Manual Exposure'}),
567     0x1100: ('MotorOrBracket',
568              {0: 'Off',
569               1: 'On'}),
570     0x1300: ('BlurWarning',
571              {0: 'Off',
572               1: 'On'}),
573     0x1301: ('FocusWarning',
574              {0: 'Off',
575               1: 'On'}),
576     0x1302: ('AEWarning',
577              {0: 'Off',
578               1: 'On'})
579     }
580
581 MAKERNOTE_CANON_TAGS={
582     0x0006: ('ImageType', ),
583     0x0007: ('FirmwareVersion', ),
584     0x0008: ('ImageNumber', ),
585     0x0009: ('OwnerName', )
586     }
587
588 # see http://www.burren.cx/david/canon.html by David Burren
589 # this is in element offset, name, optional value dictionary format
590 MAKERNOTE_CANON_TAG_0x001={
591     1: ('Macromode',
592         {1: 'Macro',
593          2: 'Normal'}),
594     2: ('SelfTimer', ),
595     3: ('Quality',
596         {2: 'Normal',
597          3: 'Fine',
598          5: 'Superfine'}),
599     4: ('FlashMode',
600         {0: 'Flash Not Fired',
601          1: 'Auto',
602          2: 'On',
603          3: 'Red-Eye Reduction',
604          4: 'Slow Synchro',
605          5: 'Auto + Red-Eye Reduction',
606          6: 'On + Red-Eye Reduction',
607          16: 'external flash'}),
608     5: ('ContinuousDriveMode',
609         {0: 'Single Or Timer',
610          1: 'Continuous'}),
611     7: ('FocusMode',
612         {0: 'One-Shot',
613          1: 'AI Servo',
614          2: 'AI Focus',
615          3: 'MF',
616          4: 'Single',
617          5: 'Continuous',
618          6: 'MF'}),
619     10: ('ImageSize',
620          {0: 'Large',
621           1: 'Medium',
622           2: 'Small'}),
623     11: ('EasyShootingMode',
624          {0: 'Full Auto',
625           1: 'Manual',
626           2: 'Landscape',
627           3: 'Fast Shutter',
628           4: 'Slow Shutter',
629           5: 'Night',
630           6: 'B&W',
631           7: 'Sepia',
632           8: 'Portrait',
633           9: 'Sports',
634           10: 'Macro/Close-Up',
635           11: 'Pan Focus'}),
636     12: ('DigitalZoom',
637          {0: 'None',
638           1: '2x',
639           2: '4x'}),
640     13: ('Contrast',
641          {0xFFFF: 'Low',
642           0: 'Normal',
643           1: 'High'}),
644     14: ('Saturation',
645          {0xFFFF: 'Low',
646           0: 'Normal',
647           1: 'High'}),
648     15: ('Sharpness',
649          {0xFFFF: 'Low',
650           0: 'Normal',
651           1: 'High'}),
652     16: ('ISO',
653          {0: 'See ISOSpeedRatings Tag',
654           15: 'Auto',
655           16: '50',
656           17: '100',
657           18: '200',
658           19: '400'}),
659     17: ('MeteringMode',
660          {3: 'Evaluative',
661           4: 'Partial',
662           5: 'Center-weighted'}),
663     18: ('FocusType',
664          {0: 'Manual',
665           1: 'Auto',
666           3: 'Close-Up (Macro)',
667           8: 'Locked (Pan Mode)'}),
668     19: ('AFPointSelected',
669          {0x3000: 'None (MF)',
670           0x3001: 'Auto-Selected',
671           0x3002: 'Right',
672           0x3003: 'Center',
673           0x3004: 'Left'}),
674     20: ('ExposureMode',
675          {0: 'Easy Shooting',
676           1: 'Program',
677           2: 'Tv-priority',
678           3: 'Av-priority',
679           4: 'Manual',
680           5: 'A-DEP'}),
681     23: ('LongFocalLengthOfLensInFocalUnits', ),
682     24: ('ShortFocalLengthOfLensInFocalUnits', ),
683     25: ('FocalUnitsPerMM', ),
684     28: ('FlashActivity',
685          {0: 'Did Not Fire',
686           1: 'Fired'}),
687     29: ('FlashDetails',
688          {14: 'External E-TTL',
689           13: 'Internal Flash',
690           11: 'FP Sync Used',
691           7: '2nd("Rear")-Curtain Sync Used',
692           4: 'FP Sync Enabled'}),
693     32: ('FocusMode',
694          {0: 'Single',
695           1: 'Continuous'})
696     }
697
698 MAKERNOTE_CANON_TAG_0x004={
699     7: ('WhiteBalance',
700         {0: 'Auto',
701          1: 'Sunny',
702          2: 'Cloudy',
703          3: 'Tungsten',
704          4: 'Fluorescent',
705          5: 'Flash',
706          6: 'Custom'}),
707     9: ('SequenceNumber', ),
708     14: ('AFPointUsed', ),
709     15: ('FlashBias',
710         {0XFFC0: '-2 EV',
711          0XFFCC: '-1.67 EV',
712          0XFFD0: '-1.50 EV',
713          0XFFD4: '-1.33 EV',
714          0XFFE0: '-1 EV',
715          0XFFEC: '-0.67 EV',
716          0XFFF0: '-0.50 EV',
717          0XFFF4: '-0.33 EV',
718          0X0000: '0 EV',
719          0X000C: '0.33 EV',
720          0X0010: '0.50 EV',
721          0X0014: '0.67 EV',
722          0X0020: '1 EV',
723          0X002C: '1.33 EV',
724          0X0030: '1.50 EV',
725          0X0034: '1.67 EV',
726          0X0040: '2 EV'}),
727     19: ('SubjectDistance', )
728     }
729
730 # extract multibyte integer in Motorola format (little endian)
731 def s2n_motorola(str):
732     x=0
733     for c in str:
734         x=(x << 8) | ord(c)
735     return x
736
737 # extract multibyte integer in Intel format (big endian)
738 def s2n_intel(str):
739     x=0
740     y=0L
741     for c in str:
742         x=x | (ord(c) << y)
743         y=y+8
744     return x
745
746 # ratio object that eventually will be able to reduce itself to lowest
747 # common denominator for printing
748 def gcd(a, b):
749    if b == 0:
750       return a
751    else:
752       return gcd(b, a % b)
753
754 class Ratio:
755     def __init__(self, num, den):
756         self.num=num
757         self.den=den
758
759     def __repr__(self):
760         self.reduce()
761         if self.den == 1:
762             return str(self.num)
763         return '%d/%d' % (self.num, self.den)
764
765     def reduce(self):
766         div=gcd(self.num, self.den)
767         if div > 1:
768             self.num=self.num/div
769             self.den=self.den/div
770
771 # for ease of dealing with tags
772 class IFD_Tag:
773     def __init__(self, printable, tag, field_type, values, field_offset,
774                  field_length):
775         # printable version of data
776         self.printable=printable
777         # tag ID number
778         self.tag=tag
779         # field type as index into FIELD_TYPES
780         self.field_type=field_type
781         # offset of start of field in bytes from beginning of IFD
782         self.field_offset=field_offset
783         # length of data field in bytes
784         self.field_length=field_length
785         # either a string or array of data items
786         self.values=values
787         
788     def __str__(self):
789         return self.printable
790     
791     def __repr__(self):
792         return '(0x%04X) %s=%s @ %d' % (self.tag,
793                                         FIELD_TYPES[self.field_type][2],
794                                         self.printable,
795                                         self.field_offset)
796
797 # class that handles an EXIF header
798 class EXIF_header:
799     def __init__(self, file, endian, offset, fake_exif, debug=0):
800         self.file=file
801         self.endian=endian
802         self.offset=offset
803         self.fake_exif=fake_exif
804         self.debug=debug
805         self.tags={}
806         
807     # convert slice to integer, based on sign and endian flags
808     # usually this offset is assumed to be relative to the beginning of the
809     # start of the EXIF information.  For some cameras that use relative tags,
810     # this offset may be relative to some other starting point.
811     def s2n(self, offset, length, signed=0):
812         self.file.seek(self.offset+offset)
813         slice=self.file.read(length)
814         if self.endian == 'I':
815             val=s2n_intel(slice)
816         else:
817             val=s2n_motorola(slice)
818         # Sign extension ?
819         if signed:
820             msb=1L << (8*length-1)
821             if val & msb:
822                 val=val-(msb << 1)
823         return val
824
825     # convert offset to string
826     def n2s(self, offset, length):
827         s=''
828         for i in range(length):
829             if self.endian == 'I':
830                 s=s+chr(offset & 0xFF)
831             else:
832                 s=chr(offset & 0xFF)+s
833             offset=offset >> 8
834         return s
835     
836     # return first IFD
837     def first_IFD(self):
838         return self.s2n(4, 4)
839
840     # return pointer to next IFD
841     def next_IFD(self, ifd):
842         entries=self.s2n(ifd, 2)
843         return self.s2n(ifd+2+12*entries, 4)
844
845     # return list of IFDs in header
846     def list_IFDs(self):
847         i=self.first_IFD()
848         a=[]
849         while i:
850             a.append(i)
851             i=self.next_IFD(i)
852         return a
853
854     # return list of entries in this IFD
855     def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0):
856         entries=self.s2n(ifd, 2)
857         for i in range(entries):
858             # entry is index of start of this IFD in the file
859             entry=ifd+2+12*i
860             tag=self.s2n(entry, 2)
861             
862             #if not (tag in IGNORE_TAGS and detailed == False):
863             #    pass
864             # ignore certain tags for faster processing
865             if not (tag in IGNORE_TAGS and detailed == False):
866                 # get tag name.  We do it early to make debugging easier
867                 tag_entry=dict.get(tag)
868                 if tag_entry:
869                     tag_name=tag_entry[0]
870                 else:
871                     tag_name='Tag 0x%04X' % tag
872                 
873                 field_type=self.s2n(entry+2, 2)
874                 if not 0 < field_type < len(FIELD_TYPES):
875                     # unknown field type
876                     raise ValueError, \
877                           'unknown type %d in tag 0x%04X' % (field_type, tag)
878                 typelen=FIELD_TYPES[field_type][0]
879                 count=self.s2n(entry+4, 4)
880                 offset=entry+8
881                 if count*typelen > 4:
882                     # offset is not the value; it's a pointer to the value
883                     # if relative we set things up so s2n will seek to the right
884                     # place when it adds self.offset.  Note that this 'relative'
885                     # is for the Nikon type 3 makernote.  Other cameras may use
886                     # other relative offsets, which would have to be computed here
887                     # slightly differently.
888                     if relative:
889                         tmp_offset=self.s2n(offset, 4)
890                         offset=tmp_offset+ifd-self.offset+4
891                         if self.fake_exif:
892                             offset=offset+18
893                     else:
894                         offset=self.s2n(offset, 4)
895                 field_offset=offset
896                 if field_type == 2:
897                     # special case: null-terminated ASCII string
898                     if count != 0:
899                         self.file.seek(self.offset+offset)
900                         values=self.file.read(count)
901                         values=values.strip().replace('\x00','')
902                     else:
903                         values=''
904                 else:
905                     values=[]
906                     signed=(field_type in [6, 8, 9, 10])
907                     for j in range(count):
908                         if field_type in (5, 10):
909                             # a ratio
910                             value_j=Ratio(self.s2n(offset,   4, signed),
911                                           self.s2n(offset+4, 4, signed))
912                         else:
913                             value_j=self.s2n(offset, typelen, signed)
914                         values.append(value_j)
915                         offset=offset+typelen
916                 # now "values" is either a string or an array
917                 if count == 1 and field_type != 2:
918                     printable=str(values[0])
919                 else:
920                     printable=str(values)
921                 # compute printable version of values
922                 if tag_entry:
923                     if len(tag_entry) != 1:
924                         # optional 2nd tag element is present
925                         if callable(tag_entry[1]):
926                             # call mapping function
927                             try:
928                                 printable=tag_entry[1](values)
929                             except:
930                                 # Ugly work-around for now. /Joel 2005-01-27
931                                 continue
932                         else:
933                             printable=''
934                             for i in values:
935                                 # use lookup table for this tag
936                                 printable+=tag_entry[1].get(i, repr(i))
937                            
938                 self.tags[ifd_name+' '+tag_name]=IFD_Tag(printable, tag,
939                                                          field_type,
940                                                          values, field_offset,
941                                                          count*typelen)
942                 if self.debug:
943                     print ' debug:   %s: %s' % (tag_name,
944                                                 repr(self.tags[ifd_name+' '+tag_name]))
945
946     # extract uncompressed TIFF thumbnail (like pulling teeth)
947     # we take advantage of the pre-existing layout in the thumbnail IFD as
948     # much as possible
949     def extract_TIFF_thumbnail(self, thumb_ifd):
950         entries=self.s2n(thumb_ifd, 2)
951         # this is header plus offset to IFD ...
952         if self.endian == 'M':
953             tiff='MM\x00*\x00\x00\x00\x08'
954         else:
955             tiff='II*\x00\x08\x00\x00\x00'
956         # ... plus thumbnail IFD data plus a null "next IFD" pointer
957         self.file.seek(self.offset+thumb_ifd)
958         tiff+=self.file.read(entries*12+2)+'\x00\x00\x00\x00'
959         
960         # fix up large value offset pointers into data area
961         for i in range(entries):
962             entry=thumb_ifd+2+12*i
963             tag=self.s2n(entry, 2)
964             field_type=self.s2n(entry+2, 2)
965             typelen=FIELD_TYPES[field_type][0]
966             count=self.s2n(entry+4, 4)
967             oldoff=self.s2n(entry+8, 4)
968             # start of the 4-byte pointer area in entry
969             ptr=i*12+18
970             # remember strip offsets location
971             if tag == 0x0111:
972                 strip_off=ptr
973                 strip_len=count*typelen
974             # is it in the data area?
975             if count*typelen > 4:
976                 # update offset pointer (nasty "strings are immutable" crap)
977                 # should be able to say "tiff[ptr:ptr+4]=newoff"
978                 newoff=len(tiff)
979                 tiff=tiff[:ptr]+self.n2s(newoff, 4)+tiff[ptr+4:]
980                 # remember strip offsets location
981                 if tag == 0x0111:
982                     strip_off=newoff
983                     strip_len=4
984                 # get original data and store it
985                 self.file.seek(self.offset+oldoff)
986                 tiff+=self.file.read(count*typelen)
987                 
988         # add pixel strips and update strip offset info
989         old_offsets=self.tags['Thumbnail StripOffsets'].values
990         old_counts=self.tags['Thumbnail StripByteCounts'].values
991         for i in range(len(old_offsets)):
992             # update offset pointer (more nasty "strings are immutable" crap)
993             offset=self.n2s(len(tiff), strip_len)
994             tiff=tiff[:strip_off]+offset+tiff[strip_off+strip_len:]
995             strip_off+=strip_len
996             # add pixel strip to end
997             self.file.seek(self.offset+old_offsets[i])
998             tiff+=self.file.read(old_counts[i])
999             
1000         self.tags['TIFFThumbnail']=tiff
1001         
1002     # decode all the camera-specific MakerNote formats
1003
1004     # Note is the data that comprises this MakerNote.  The MakerNote will
1005     # likely have pointers in it that point to other parts of the file.  We'll
1006     # use self.offset as the starting point for most of those pointers, since
1007     # they are relative to the beginning of the file.
1008     #
1009     # If the MakerNote is in a newer format, it may use relative addressing
1010     # within the MakerNote.  In that case we'll use relative addresses for the
1011     # pointers.
1012     #
1013     # As an aside: it's not just to be annoying that the manufacturers use
1014     # relative offsets.  It's so that if the makernote has to be moved by the
1015     # picture software all of the offsets don't have to be adjusted.  Overall,
1016     # this is probably the right strategy for makernotes, though the spec is
1017     # ambiguous.  (The spec does not appear to imagine that makernotes would
1018     # follow EXIF format internally.  Once they did, it's ambiguous whether
1019     # the offsets should be from the header at the start of all the EXIF info,
1020     # or from the header at the start of the makernote.)
1021     def decode_maker_note(self):
1022         note=self.tags['EXIF MakerNote']
1023         make=self.tags['Image Make'].printable
1024         if hasattr(self.tags, 'Image Model'):
1025             model=self.tags['Image Model'].printable
1026         else:
1027             model='TAG_UNAVAILABLE'
1028
1029         # Nikon
1030         # The maker note usually starts with the word Nikon, followed by the
1031         # type of the makernote (1 or 2, as a short).  If the word Nikon is
1032         # not at the start of the makernote, it's probably type 2, since some
1033         # cameras work that way.
1034         if make in ('NIKON', 'NIKON CORPORATION'):
1035             if note.values[0:7] == [78, 105, 107, 111, 110, 00, 01]:
1036                 if self.debug:
1037                     print "Looks like a type 1 Nikon MakerNote."
1038                 self.dump_IFD(note.field_offset+8, 'MakerNote',
1039                               dict=MAKERNOTE_NIKON_OLDER_TAGS)
1040             elif note.values[0:7] == [78, 105, 107, 111, 110, 00, 02]:
1041                 if self.debug:
1042                     print "Looks like a labeled type 2 Nikon MakerNote"
1043                 if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]:
1044                     raise ValueError, "Missing marker tag '42' in MakerNote."
1045                 # skip the Makernote label and the TIFF header
1046                 self.dump_IFD(note.field_offset+10+8, 'MakerNote',
1047                               dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1)
1048             else:
1049                 # E99x or D1
1050                 if self.debug:
1051                     print "Looks like an unlabeled type 2 Nikon MakerNote"
1052                 self.dump_IFD(note.field_offset, 'MakerNote',
1053                               dict=MAKERNOTE_NIKON_NEWER_TAGS)
1054             return
1055
1056         # Olympus
1057         if make[:7] == 'OLYMPUS':
1058             self.dump_IFD(note.field_offset+8, 'MakerNote',
1059                           dict=MAKERNOTE_OLYMPUS_TAGS)
1060             return
1061
1062         # Casio
1063         if make == 'Casio':
1064             self.dump_IFD(note.field_offset, 'MakerNote',
1065                           dict=MAKERNOTE_CASIO_TAGS)
1066             return
1067         
1068         # Fujifilm
1069         if make == 'FUJIFILM':
1070             # bug: everything else is "Motorola" endian, but the MakerNote
1071             # is "Intel" endian
1072             endian=self.endian
1073             self.endian='I'
1074             # bug: IFD offsets are from beginning of MakerNote, not
1075             # beginning of file header
1076             offset=self.offset
1077             self.offset+=note.field_offset
1078             # process note with bogus values (note is actually at offset 12)
1079             self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
1080             # reset to correct values
1081             self.endian=endian
1082             self.offset=offset
1083             return
1084         
1085         # Canon
1086         if make == 'Canon':
1087             self.dump_IFD(note.field_offset, 'MakerNote',
1088                           dict=MAKERNOTE_CANON_TAGS)
1089             for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
1090                       ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
1091                 self.canon_decode_tag(self.tags[i[0]].values, i[1])
1092             return
1093
1094     # decode Canon MakerNote tag based on offset within tag
1095     # see http://www.burren.cx/david/canon.html by David Burren
1096     def canon_decode_tag(self, value, dict):
1097         for i in range(1, len(value)):
1098             x=dict.get(i, ('Unknown', ))
1099             if self.debug:
1100                 print i, x
1101             name=x[0]
1102             if len(x) > 1:
1103                 val=x[1].get(value[i], 'Unknown')
1104             else:
1105                 val=value[i]
1106             # it's not a real IFD Tag but we fake one to make everybody
1107             # happy. this will have a "proprietary" type
1108             self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None,
1109                                                  None, None)
1110
1111 # process an image file (expects an open file object)
1112 # this is the function that has to deal with all the arbitrary nasty bits
1113 # of the EXIF standard
1114 def process_file(file, details=True, debug=0):
1115     # yah it's cheesy...
1116     global detailed
1117     detailed = details
1118     
1119     # by default do not fake an EXIF beginning
1120     fake_exif=0
1121     
1122     # determine whether it's a JPEG or TIFF
1123     data=file.read(12)
1124     if data[0:4] in ['II*\x00', 'MM\x00*']:
1125         # it's a TIFF file
1126         file.seek(0)
1127         endian=file.read(1)
1128         file.read(1)
1129         offset=0
1130     elif data[0:2] == '\xFF\xD8':
1131         # it's a JPEG file
1132         while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM', 'Phot'):
1133             length=ord(data[4])*256+ord(data[5])
1134             file.read(length-8)
1135             # fake an EXIF beginning of file
1136             data='\xFF\x00'+file.read(10)
1137             fake_exif=1
1138         if data[2] == '\xFF' and data[6:10] == 'Exif':
1139             # detected EXIF header
1140             offset=file.tell()
1141             endian=file.read(1)
1142         else:
1143             # no EXIF information
1144             return {}
1145     else:
1146         # file format not recognized
1147         return {}
1148
1149     # deal with the EXIF info we found
1150     if debug:
1151         print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
1152     hdr=EXIF_header(file, endian, offset, fake_exif, debug)
1153     ifd_list=hdr.list_IFDs()
1154     ctr=0
1155     for i in ifd_list:
1156         if ctr == 0:
1157             IFD_name='Image'
1158         elif ctr == 1:
1159             IFD_name='Thumbnail'
1160             thumb_ifd=i
1161         else:
1162             IFD_name='IFD %d' % ctr
1163         if debug:
1164             print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
1165         hdr.dump_IFD(i, IFD_name)
1166         # EXIF IFD
1167         exif_off=hdr.tags.get(IFD_name+' ExifOffset')
1168         if exif_off:
1169             if debug:
1170                 print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
1171             hdr.dump_IFD(exif_off.values[0], 'EXIF')
1172             # Interoperability IFD contained in EXIF IFD
1173             intr_off=hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
1174             if intr_off:
1175                 if debug:
1176                     print ' EXIF Interoperability SubSubIFD at offset %d:' \
1177                           % intr_off.values[0]
1178                 hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
1179                              dict=INTR_TAGS)
1180         # GPS IFD
1181         gps_off=hdr.tags.get(IFD_name+' GPSInfo')
1182         if gps_off:
1183             if debug:
1184                 print ' GPS SubIFD at offset %d:' % gps_off.values[0]
1185             hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS)
1186         ctr+=1
1187
1188     # extract uncompressed TIFF thumbnail
1189     thumb=hdr.tags.get('Thumbnail Compression')
1190     if thumb and thumb.printable == 'Uncompressed TIFF':
1191         hdr.extract_TIFF_thumbnail(thumb_ifd)
1192         
1193     # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
1194     thumb_off=hdr.tags.get('Thumbnail JPEGInterchangeFormat')
1195     if thumb_off:
1196         file.seek(offset+thumb_off.values[0])
1197         size=hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
1198         hdr.tags['JPEGThumbnail']=file.read(size)
1199         
1200     # deal with MakerNote contained in EXIF IFD
1201     if hdr.tags.has_key('EXIF MakerNote') and detailed:
1202         hdr.decode_maker_note()
1203
1204     # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
1205     # since it's not allowed in a uncompressed TIFF IFD
1206     if not hdr.tags.has_key('JPEGThumbnail'):
1207         thumb_off=hdr.tags.get('MakerNote JPEGThumbnail')
1208         if thumb_off:
1209             file.seek(offset+thumb_off.values[0])
1210             hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
1211             
1212     return hdr.tags
1213
1214
1215 # library test/debug function (dump given files)
1216 if __name__ == '__main__':
1217     import sys
1218     
1219     if len(sys.argv) < 2:
1220         print "Usage: %s [options] file1 [file2 ...]\n\nOptions:\n-q --quick : do not process MakerNotes for faster processing\n" % sys.argv[0]
1221         sys.exit(0)
1222     
1223     if sys.argv[1] == "-q" or sys.argv[1] == "--quick":
1224         fileNamesStart = 2
1225         detailed = False
1226     else:
1227         fileNamesStart = 1
1228         detailed = True
1229     
1230     for filename in sys.argv[fileNamesStart:]:
1231         try:
1232             file=open(filename, 'rb')
1233         except:
1234             print filename, 'unreadable'
1235             print
1236             continue
1237         print filename+':'
1238         # data=process_file(file, debug=1) # with debug info and all tags
1239         data=process_file(file, details=detailed)
1240         if not data:
1241             print 'No EXIF information found'
1242             continue
1243
1244         x=data.keys()
1245         x.sort()
1246         for i in x:
1247             if i in ('JPEGThumbnail', 'TIFFThumbnail'):
1248                 continue
1249             try:
1250                 print '   %s (%s): %s' % \
1251                       (i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
1252             except:
1253                 print 'error', i, '"', data[i], '"'
1254         if data.has_key('JPEGThumbnail'):
1255             print 'File has JPEG thumbnail'
1256         print
1257