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