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