source: subversion/applications/editors/pyosmeditor/EXIF.py @ 30277

Last change on this file since 30277 was 1846, checked in by joerg, 13 years ago

set executable property

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