Back to index

plone3  3.1.7
exif.py
Go to the documentation of this file.
00001 # Library to extract EXIF information in digital camera image files
00002 #
00003 # To use this library call with:
00004 #    f=open(path_name, 'rb')
00005 #    tags=EXIF.process_file(f)
00006 # tags will now be a dictionary mapping names of EXIF tags to their
00007 # values in the file named by path_name.  You can process the tags
00008 # as you wish.  In particular, you can iterate through all the tags with:
00009 #     for tag in tags.keys():
00010 #         if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename',
00011 #                        'EXIF MakerNote'):
00012 #             print "Key: %s, value %s" % (tag, tags[tag])
00013 # (This code uses the if statement to avoid printing out a few of the
00014 # tags that tend to be long or boring.)
00015 #
00016 # The tags dictionary will include keys for all of the usual EXIF
00017 # tags, and will also include keys for Makernotes used by some
00018 # cameras, for which we have a good specification.
00019 #
00020 # Contains code from "exifdump.py" originally written by Thierry Bousch
00021 # <bousch@topo.math.u-psud.fr> and released into the public domain.
00022 #
00023 # Updated and turned into general-purpose library by Gene Cash
00024 # <email gcash at cfl.rr.com>
00025 #
00026 # This copyright license is intended to be similar to the FreeBSD license.
00027 #
00028 # Copyright 2002 Gene Cash All rights reserved.
00029 #
00030 # Redistribution and use in source and binary forms, with or without
00031 # modification, are permitted provided that the following conditions are
00032 # met:
00033 #
00034 #    1. Redistributions of source code must retain the above copyright
00035 #       notice, this list of conditions and the following disclaimer.
00036 #    2. Redistributions in binary form must reproduce the above copyright
00037 #       notice, this list of conditions and the following disclaimer in the
00038 #       documentation and/or other materials provided with the
00039 #       distribution.
00040 #
00041 # THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR
00042 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
00043 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
00044 # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
00045 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
00046 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
00047 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
00048 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
00049 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00050 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00051 # POSSIBILITY OF SUCH DAMAGE.
00052 #
00053 # This means you may do anything you want with this code, except claim you
00054 # wrote it. Also, if it breaks you get to keep both pieces.
00055 #
00056 # Patch Contributors:
00057 # * Simon J. Gerraty <sjg@crufty.net>
00058 #   s2n fix & orientation decode
00059 # * John T. Riedl <riedl@cs.umn.edu>
00060 #   Added support for newer Nikon type 3 Makernote format for D70 and some
00061 #   other Nikon cameras.
00062 # * Joerg Schaefer <schaeferj@gmx.net>
00063 #   Fixed subtle bug when faking an EXIF header, which affected maker notes
00064 #   using relative offsets, and a fix for Nikon D100.
00065 #
00066 # 21-AUG-99 TB  Last update by Thierry Bousch to his code.
00067 # 17-JAN-02 CEC Discovered code on web.
00068 #               Commented everything.
00069 #               Made small code improvements.
00070 #               Reformatted for readability.
00071 # 19-JAN-02 CEC Added ability to read TIFFs and JFIF-format JPEGs.
00072 #               Added ability to extract JPEG formatted thumbnail.
00073 #               Added ability to read GPS IFD (not tested).
00074 #               Converted IFD data structure to dictionaries indexed by
00075 #               tag name.
00076 #               Factored into library returning dictionary of IFDs plus
00077 #               thumbnail, if any.
00078 # 20-JAN-02 CEC Added MakerNote processing logic.
00079 #               Added Olympus MakerNote.
00080 #               Converted data structure to single-level dictionary, avoiding
00081 #               tag name collisions by prefixing with IFD name.  This makes
00082 #               it much easier to use.
00083 # 23-JAN-02 CEC Trimmed nulls from end of string values.
00084 # 25-JAN-02 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote.
00085 # 26-JAN-02 CEC Added ability to extract TIFF thumbnails.
00086 #               Added Nikon, Fujifilm, Casio MakerNotes.
00087 # 30-NOV-03 CEC Fixed problem with canon_decode_tag() not creating an
00088 #               IFD_Tag() object.
00089 # 15-FEB-04 CEC Finally fixed bit shift warning by converting Y to 0L.
00090 # 04-DEC-05 JFROCHE Reduce number of objects created.
00091 #
00092 
00093 # XXX: These are here because removing them broke existing ZODB instances.
00094 # we should remove them completely and have migrations to fix them up instead.
00095 class Ratio: 
00096     pass
00097 class IFD_Tag: 
00098     pass
00099 
00100 import logging
00101 LOG = logging.getLogger('exif')
00102 
00103 # field type descriptions as (length, abbreviation, full name) tuples
00104 FIELD_TYPES=(
00105     (0, 'X',  'Proprietary'), # no such type
00106     (1, 'B',  'Byte'),
00107     (1, 'A',  'ASCII'),
00108     (2, 'S',  'Short'),
00109     (4, 'L',  'Long'),
00110     (8, 'R',  'Ratio'),
00111     (1, 'SB', 'Signed Byte'),
00112     (1, 'U',  'Undefined'),
00113     (2, 'SS', 'Signed Short'),
00114     (4, 'SL', 'Signed Long'),
00115     (8, 'SR', 'Signed Ratio')
00116     )
00117 
00118 # dictionary of main EXIF tag names
00119 # first element of tuple is tag name, optional second element is
00120 # another dictionary giving names to values
00121 EXIF_TAGS={
00122     0x0100: ('ImageWidth', ),
00123     0x0101: ('ImageLength', ),
00124     0x0102: ('BitsPerSample', ),
00125     0x0103: ('Compression',
00126              {1: 'Uncompressed TIFF',
00127               6: 'JPEG Compressed'}),
00128     0x0106: ('PhotometricInterpretation', ),
00129     0x010A: ('FillOrder', ),
00130     0x010D: ('DocumentName', ),
00131     0x010E: ('ImageDescription', ),
00132     0x010F: ('Make', ),
00133     0x0110: ('Model', ),
00134     0x0111: ('StripOffsets', ),
00135     0x0112: ('Orientation',
00136              {1: 'Horizontal (normal)',
00137               2: 'Mirrored horizontal',
00138               3: 'Rotated 180',
00139               4: 'Mirrored vertical',
00140               5: 'Mirrored horizontal then rotated 90 CCW',
00141               6: 'Rotated 90 CW',
00142               7: 'Mirrored horizontal then rotated 90 CW',
00143               8: 'Rotated 90 CCW'}),
00144     0x0115: ('SamplesPerPixel', ),
00145     0x0116: ('RowsPerStrip', ),
00146     0x0117: ('StripByteCounts', ),
00147     0x011A: ('XResolution', ),
00148     0x011B: ('YResolution', ),
00149     0x011C: ('PlanarConfiguration', ),
00150     0x0128: ('ResolutionUnit',
00151              {1: 'Not Absolute',
00152               2: 'Pixels/Inch',
00153               3: 'Pixels/Centimeter'}),
00154     0x012D: ('TransferFunction', ),
00155     0x0131: ('Software', ),
00156     0x0132: ('DateTime', ),
00157     0x013B: ('Artist', ),
00158     0x013E: ('WhitePoint', ),
00159     0x013F: ('PrimaryChromaticities', ),
00160     0x0156: ('TransferRange', ),
00161     0x0200: ('JPEGProc', ),
00162     0x0201: ('JPEGInterchangeFormat', ),
00163     0x0202: ('JPEGInterchangeFormatLength', ),
00164     0x0211: ('YCbCrCoefficients', ),
00165     0x0212: ('YCbCrSubSampling', ),
00166     0x0213: ('YCbCrPositioning', ),
00167     0x0214: ('ReferenceBlackWhite', ),
00168     0x828D: ('CFARepeatPatternDim', ),
00169     0x828E: ('CFAPattern', ),
00170     0x828F: ('BatteryLevel', ),
00171     0x8298: ('Copyright', ),
00172     0x829A: ('ExposureTime', ),
00173     0x829D: ('FNumber', ),
00174     0x83BB: ('IPTC/NAA', ),
00175     0x8769: ('ExifOffset', ),
00176     0x8773: ('InterColorProfile', ),
00177     0x8822: ('ExposureProgram',
00178              {0: 'Unidentified',
00179               1: 'Manual',
00180               2: 'Program Normal',
00181               3: 'Aperture Priority',
00182               4: 'Shutter Priority',
00183               5: 'Program Creative',
00184               6: 'Program Action',
00185               7: 'Portrait Mode',
00186               8: 'Landscape Mode'}),
00187     0x8824: ('SpectralSensitivity', ),
00188     0x8825: ('GPSInfo', ),
00189     0x8827: ('ISOSpeedRatings', ),
00190     0x8828: ('OECF', ),
00191     # print as string
00192     0x9000: ('ExifVersion', lambda x: ''.join(map(chr, x))),
00193     0x9003: ('DateTimeOriginal', ),
00194     0x9004: ('DateTimeDigitized', ),
00195     0x9101: ('ComponentsConfiguration',
00196              {0: '',
00197               1: 'Y',
00198               2: 'Cb',
00199               3: 'Cr',
00200               4: 'Red',
00201               5: 'Green',
00202               6: 'Blue'}),
00203     0x9102: ('CompressedBitsPerPixel', ),
00204     0x9201: ('ShutterSpeedValue', ),
00205     0x9202: ('ApertureValue', ),
00206     0x9203: ('BrightnessValue', ),
00207     0x9204: ('ExposureBiasValue', ),
00208     0x9205: ('MaxApertureValue', ),
00209     0x9206: ('SubjectDistance', ),
00210     0x9207: ('MeteringMode',
00211              {0: 'Unidentified',
00212               1: 'Average',
00213               2: 'CenterWeightedAverage',
00214               3: 'Spot',
00215               4: 'MultiSpot'}),
00216     0x9208: ('LightSource',
00217              {0:   'Unknown',
00218               1:   'Daylight',
00219               2:   'Fluorescent',
00220               3:   'Tungsten',
00221               10:  'Flash',
00222               17:  'Standard Light A',
00223               18:  'Standard Light B',
00224               19:  'Standard Light C',
00225               20:  'D55',
00226               21:  'D65',
00227               22:  'D75',
00228               255: 'Other'}),
00229     0x9209: ('Flash', {0:  'No',
00230                        1:  'Fired',
00231                        5:  'Fired (?)', # no return sensed
00232                        7:  'Fired (!)', # return sensed
00233                        9:  'Fill Fired',
00234                        13: 'Fill Fired (?)',
00235                        15: 'Fill Fired (!)',
00236                        16: 'Off',
00237                        24: 'Auto Off',
00238                        25: 'Auto Fired',
00239                        29: 'Auto Fired (?)',
00240                        31: 'Auto Fired (!)',
00241                        32: 'Not Available'}),
00242     0x920A: ('FocalLength', ),
00243     0x927C: ('MakerNote', ),
00244     # print as string
00245     0x9286: ('UserComment', lambda x: ''.join(map(chr, x))),
00246     0x9290: ('SubSecTime', ),
00247     0x9291: ('SubSecTimeOriginal', ),
00248     0x9292: ('SubSecTimeDigitized', ),
00249     # print as string
00250     0xA000: ('FlashPixVersion', lambda x: ''.join(map(chr, x))),
00251     0xA001: ('ColorSpace', ),
00252     0xA002: ('ExifImageWidth', ),
00253     0xA003: ('ExifImageLength', ),
00254     0xA005: ('InteroperabilityOffset', ),
00255     0xA20B: ('FlashEnergy', ),               # 0x920B in TIFF/EP
00256     0xA20C: ('SpatialFrequencyResponse', ),  # 0x920C    -  -
00257     0xA20E: ('FocalPlaneXResolution', ),     # 0x920E    -  -
00258     0xA20F: ('FocalPlaneYResolution', ),     # 0x920F    -  -
00259     0xA210: ('FocalPlaneResolutionUnit', ),  # 0x9210    -  -
00260     0xA214: ('SubjectLocation', ),           # 0x9214    -  -
00261     0xA215: ('ExposureIndex', ),             # 0x9215    -  -
00262     0xA217: ('SensingMethod', ),             # 0x9217    -  -
00263     0xA300: ('FileSource',
00264              {3: 'Digital Camera'}),
00265     0xA301: ('SceneType',
00266              {1: 'Directly Photographed'}),
00267     0xA302: ('CVAPattern',),
00268     }
00269 
00270 # interoperability tags
00271 INTR_TAGS={
00272     0x0001: ('InteroperabilityIndex', ),
00273     0x0002: ('InteroperabilityVersion', ),
00274     0x1000: ('RelatedImageFileFormat', ),
00275     0x1001: ('RelatedImageWidth', ),
00276     0x1002: ('RelatedImageLength', ),
00277     }
00278 
00279 # GPS tags (not used yet, haven't seen camera with GPS)
00280 GPS_TAGS={
00281     0x0000: ('GPSVersionID', ),
00282     0x0001: ('GPSLatitudeRef', ),
00283     0x0002: ('GPSLatitude', ),
00284     0x0003: ('GPSLongitudeRef', ),
00285     0x0004: ('GPSLongitude', ),
00286     0x0005: ('GPSAltitudeRef', ),
00287     0x0006: ('GPSAltitude', ),
00288     0x0007: ('GPSTimeStamp', ),
00289     0x0008: ('GPSSatellites', ),
00290     0x0009: ('GPSStatus', ),
00291     0x000A: ('GPSMeasureMode', ),
00292     0x000B: ('GPSDOP', ),
00293     0x000C: ('GPSSpeedRef', ),
00294     0x000D: ('GPSSpeed', ),
00295     0x000E: ('GPSTrackRef', ),
00296     0x000F: ('GPSTrack', ),
00297     0x0010: ('GPSImgDirectionRef', ),
00298     0x0011: ('GPSImgDirection', ),
00299     0x0012: ('GPSMapDatum', ),
00300     0x0013: ('GPSDestLatitudeRef', ),
00301     0x0014: ('GPSDestLatitude', ),
00302     0x0015: ('GPSDestLongitudeRef', ),
00303     0x0016: ('GPSDestLongitude', ),
00304     0x0017: ('GPSDestBearingRef', ),
00305     0x0018: ('GPSDestBearing', ),
00306     0x0019: ('GPSDestDistanceRef', ),
00307     0x001A: ('GPSDestDistance', )
00308     }
00309 
00310 # Nikon E99x MakerNote Tags
00311 # http://members.tripod.com/~tawba/990exif.htm
00312 MAKERNOTE_NIKON_NEWER_TAGS={
00313     0x0002: ('ISOSetting', ),
00314     0x0003: ('ColorMode', ),
00315     0x0004: ('Quality', ),
00316     0x0005: ('Whitebalance', ),
00317     0x0006: ('ImageSharpening', ),
00318     0x0007: ('FocusMode', ),
00319     0x0008: ('FlashSetting', ),
00320     0x0009: ('AutoFlashMode', ),
00321     0x000B: ('WhiteBalanceBias', ),
00322     0x000C: ('WhiteBalanceRBCoeff', ),
00323     0x000F: ('ISOSelection', ),
00324     0x0012: ('FlashCompensation', ),
00325     0x0013: ('ISOSpeedRequested', ),
00326     0x0016: ('PhotoCornerCoordinates', ),
00327     0x0018: ('FlashBracketCompensationApplied', ),
00328     0x0019: ('AEBracketCompensationApplied', ),
00329     0x0080: ('ImageAdjustment', ),
00330     0x0081: ('ToneCompensation', ),
00331     0x0082: ('AuxiliaryLens', ),
00332     0x0083: ('LensType', ),
00333     0x0084: ('LensMinMaxFocalMaxAperture', ),
00334     0x0085: ('ManualFocusDistance', ),
00335     0x0086: ('DigitalZoomFactor', ),
00336     0x0088: ('AFFocusPosition',
00337              {0x0000: 'Center',
00338               0x0100: 'Top',
00339               0x0200: 'Bottom',
00340               0x0300: 'Left',
00341               0x0400: 'Right'}),
00342     0x0089: ('BracketingMode',
00343              {0x00: 'Single frame, no bracketing',
00344               0x01: 'Continuous, no bracketing',
00345               0x02: 'Timer, no bracketing',
00346               0x10: 'Single frame, exposure bracketing',
00347               0x11: 'Continuous, exposure bracketing',
00348               0x12: 'Timer, exposure bracketing',
00349               0x40: 'Single frame, white balance bracketing',
00350               0x41: 'Continuous, white balance bracketing',
00351               0x42: 'Timer, white balance bracketing'}),
00352     0x008D: ('ColorMode', ),
00353     0x008F: ('SceneMode?', ),
00354     0x0090: ('LightingType', ),
00355     0x0092: ('HueAdjustment', ),
00356     0x0094: ('Saturation',
00357              {-3: 'B&W',
00358               -2: '-2',
00359               -1: '-1',
00360               0:  '0',
00361               1:  '1',
00362               2:  '2'}),
00363     0x0095: ('NoiseReduction', ),
00364     0x00A7: ('TotalShutterReleases', ),
00365     0x00A9: ('ImageOptimization', ),
00366     0x00AA: ('Saturation', ),
00367     0x00AB: ('DigitalVariProgram', ),
00368     0x0010: ('DataDump', )
00369     }
00370 
00371 MAKERNOTE_NIKON_OLDER_TAGS={
00372     0x0003: ('Quality',
00373              {1: 'VGA Basic',
00374               2: 'VGA Normal',
00375               3: 'VGA Fine',
00376               4: 'SXGA Basic',
00377               5: 'SXGA Normal',
00378               6: 'SXGA Fine'}),
00379     0x0004: ('ColorMode',
00380              {1: 'Color',
00381               2: 'Monochrome'}),
00382     0x0005: ('ImageAdjustment',
00383              {0: 'Normal',
00384               1: 'Bright+',
00385               2: 'Bright-',
00386               3: 'Contrast+',
00387               4: 'Contrast-'}),
00388     0x0006: ('CCDSpeed',
00389              {0: 'ISO 80',
00390               2: 'ISO 160',
00391               4: 'ISO 320',
00392               5: 'ISO 100'}),
00393     0x0007: ('WhiteBalance',
00394              {0: 'Auto',
00395               1: 'Preset',
00396               2: 'Daylight',
00397               3: 'Incandescent',
00398               4: 'Fluorescent',
00399               5: 'Cloudy',
00400               6: 'Speed Light'})
00401     }
00402 
00403 # decode Olympus SpecialMode tag in MakerNote
00404 def olympus_special_mode(v):
00405     a={
00406         0: 'Normal',
00407         1: 'Unknown',
00408         2: 'Fast',
00409         3: 'Panorama'}
00410     b={
00411         0: 'Non-panoramic',
00412         1: 'Left to right',
00413         2: 'Right to left',
00414         3: 'Bottom to top',
00415         4: 'Top to bottom'}
00416     return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
00417 
00418 MAKERNOTE_OLYMPUS_TAGS={
00419     # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
00420     # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
00421     0x0100: ('JPEGThumbnail', ),
00422     0x0200: ('SpecialMode', olympus_special_mode),
00423     0x0201: ('JPEGQual',
00424              {1: 'SQ',
00425               2: 'HQ',
00426               3: 'SHQ'}),
00427     0x0202: ('Macro',
00428              {0: 'Normal',
00429               1: 'Macro'}),
00430     0x0204: ('DigitalZoom', ),
00431     0x0207: ('SoftwareRelease',  ),
00432     0x0208: ('PictureInfo',  ),
00433     # print as string
00434     0x0209: ('CameraID', lambda x: ''.join(map(chr, x))),
00435     0x0F00: ('DataDump',  )
00436     }
00437 
00438 MAKERNOTE_CASIO_TAGS={
00439     0x0001: ('RecordingMode',
00440              {1: 'Single Shutter',
00441               2: 'Panorama',
00442               3: 'Night Scene',
00443               4: 'Portrait',
00444               5: 'Landscape'}),
00445     0x0002: ('Quality',
00446              {1: 'Economy',
00447               2: 'Normal',
00448               3: 'Fine'}),
00449     0x0003: ('FocusingMode',
00450              {2: 'Macro',
00451               3: 'Auto Focus',
00452               4: 'Manual Focus',
00453               5: 'Infinity'}),
00454     0x0004: ('FlashMode',
00455              {1: 'Auto',
00456               2: 'On',
00457               3: 'Off',
00458               4: 'Red Eye Reduction'}),
00459     0x0005: ('FlashIntensity',
00460              {11: 'Weak',
00461               13: 'Normal',
00462               15: 'Strong'}),
00463     0x0006: ('Object Distance', ),
00464     0x0007: ('WhiteBalance',
00465              {1:   'Auto',
00466               2:   'Tungsten',
00467               3:   'Daylight',
00468               4:   'Fluorescent',
00469               5:   'Shade',
00470               129: 'Manual'}),
00471     0x000B: ('Sharpness',
00472              {0: 'Normal',
00473               1: 'Soft',
00474               2: 'Hard'}),
00475     0x000C: ('Contrast',
00476              {0: 'Normal',
00477               1: 'Low',
00478               2: 'High'}),
00479     0x000D: ('Saturation',
00480              {0: 'Normal',
00481               1: 'Low',
00482               2: 'High'}),
00483     0x0014: ('CCDSpeed',
00484              {64:  'Normal',
00485               80:  'Normal',
00486               100: 'High',
00487               125: '+1.0',
00488               244: '+3.0',
00489               250: '+2.0',})
00490     }
00491 
00492 MAKERNOTE_FUJIFILM_TAGS={
00493     0x0000: ('NoteVersion', lambda x: ''.join(map(chr, x))),
00494     0x1000: ('Quality', ),
00495     0x1001: ('Sharpness',
00496              {1: 'Soft',
00497               2: 'Soft',
00498               3: 'Normal',
00499               4: 'Hard',
00500               5: 'Hard'}),
00501     0x1002: ('WhiteBalance',
00502              {0:    'Auto',
00503               256:  'Daylight',
00504               512:  'Cloudy',
00505               768:  'DaylightColor-Fluorescent',
00506               769:  'DaywhiteColor-Fluorescent',
00507               770:  'White-Fluorescent',
00508               1024: 'Incandescent',
00509               3840: 'Custom'}),
00510     0x1003: ('Color',
00511              {0:   'Normal',
00512               256: 'High',
00513               512: 'Low'}),
00514     0x1004: ('Tone',
00515              {0:   'Normal',
00516               256: 'High',
00517               512: 'Low'}),
00518     0x1010: ('FlashMode',
00519              {0: 'Auto',
00520               1: 'On',
00521               2: 'Off',
00522               3: 'Red Eye Reduction'}),
00523     0x1011: ('FlashStrength', ),
00524     0x1020: ('Macro',
00525              {0: 'Off',
00526               1: 'On'}),
00527     0x1021: ('FocusMode',
00528              {0: 'Auto',
00529               1: 'Manual'}),
00530     0x1030: ('SlowSync',
00531              {0: 'Off',
00532               1: 'On'}),
00533     0x1031: ('PictureMode',
00534              {0:   'Auto',
00535               1:   'Portrait',
00536               2:   'Landscape',
00537               4:   'Sports',
00538               5:   'Night',
00539               6:   'Program AE',
00540               256: 'Aperture Priority AE',
00541               512: 'Shutter Priority AE',
00542               768: 'Manual Exposure'}),
00543     0x1100: ('MotorOrBracket',
00544              {0: 'Off',
00545               1: 'On'}),
00546     0x1300: ('BlurWarning',
00547              {0: 'Off',
00548               1: 'On'}),
00549     0x1301: ('FocusWarning',
00550              {0: 'Off',
00551               1: 'On'}),
00552     0x1302: ('AEWarning',
00553              {0: 'Off',
00554               1: 'On'})
00555     }
00556 
00557 MAKERNOTE_CANON_TAGS={
00558     0x0006: ('ImageType', ),
00559     0x0007: ('FirmwareVersion', ),
00560     0x0008: ('ImageNumber', ),
00561     0x0009: ('OwnerName', )
00562     }
00563 
00564 # see http://www.burren.cx/david/canon.html by David Burren
00565 # this is in element offset, name, optional value dictionary format
00566 MAKERNOTE_CANON_TAG_0x001={
00567     1: ('Macromode',
00568         {1: 'Macro',
00569          2: 'Normal'}),
00570     2: ('SelfTimer', ),
00571     3: ('Quality',
00572         {2: 'Normal',
00573          3: 'Fine',
00574          5: 'Superfine'}),
00575     4: ('FlashMode',
00576         {0: 'Flash Not Fired',
00577          1: 'Auto',
00578          2: 'On',
00579          3: 'Red-Eye Reduction',
00580          4: 'Slow Synchro',
00581          5: 'Auto + Red-Eye Reduction',
00582          6: 'On + Red-Eye Reduction',
00583          16: 'external flash'}),
00584     5: ('ContinuousDriveMode',
00585         {0: 'Single Or Timer',
00586          1: 'Continuous'}),
00587     7: ('FocusMode',
00588         {0: 'One-Shot',
00589          1: 'AI Servo',
00590          2: 'AI Focus',
00591          3: 'MF',
00592          4: 'Single',
00593          5: 'Continuous',
00594          6: 'MF'}),
00595     10: ('ImageSize',
00596          {0: 'Large',
00597           1: 'Medium',
00598           2: 'Small'}),
00599     11: ('EasyShootingMode',
00600          {0: 'Full Auto',
00601           1: 'Manual',
00602           2: 'Landscape',
00603           3: 'Fast Shutter',
00604           4: 'Slow Shutter',
00605           5: 'Night',
00606           6: 'B&W',
00607           7: 'Sepia',
00608           8: 'Portrait',
00609           9: 'Sports',
00610           10: 'Macro/Close-Up',
00611           11: 'Pan Focus'}),
00612     12: ('DigitalZoom',
00613          {0: 'None',
00614           1: '2x',
00615           2: '4x'}),
00616     13: ('Contrast',
00617          {0xFFFF: 'Low',
00618           0: 'Normal',
00619           1: 'High'}),
00620     14: ('Saturation',
00621          {0xFFFF: 'Low',
00622           0: 'Normal',
00623           1: 'High'}),
00624     15: ('Sharpness',
00625          {0xFFFF: 'Low',
00626           0: 'Normal',
00627           1: 'High'}),
00628     16: ('ISO',
00629          {0: 'See ISOSpeedRatings Tag',
00630           15: 'Auto',
00631           16: '50',
00632           17: '100',
00633           18: '200',
00634           19: '400'}),
00635     17: ('MeteringMode',
00636          {3: 'Evaluative',
00637           4: 'Partial',
00638           5: 'Center-weighted'}),
00639     18: ('FocusType',
00640          {0: 'Manual',
00641           1: 'Auto',
00642           3: 'Close-Up (Macro)',
00643           8: 'Locked (Pan Mode)'}),
00644     19: ('AFPointSelected',
00645          {0x3000: 'None (MF)',
00646           0x3001: 'Auto-Selected',
00647           0x3002: 'Right',
00648           0x3003: 'Center',
00649           0x3004: 'Left'}),
00650     20: ('ExposureMode',
00651          {0: 'Easy Shooting',
00652           1: 'Program',
00653           2: 'Tv-priority',
00654           3: 'Av-priority',
00655           4: 'Manual',
00656           5: 'A-DEP'}),
00657     23: ('LongFocalLengthOfLensInFocalUnits', ),
00658     24: ('ShortFocalLengthOfLensInFocalUnits', ),
00659     25: ('FocalUnitsPerMM', ),
00660     28: ('FlashActivity',
00661          {0: 'Did Not Fire',
00662           1: 'Fired'}),
00663     29: ('FlashDetails',
00664          {14: 'External E-TTL',
00665           13: 'Internal Flash',
00666           11: 'FP Sync Used',
00667           7: '2nd("Rear")-Curtain Sync Used',
00668           4: 'FP Sync Enabled'}),
00669     32: ('FocusMode',
00670          {0: 'Single',
00671           1: 'Continuous'})
00672     }
00673 
00674 MAKERNOTE_CANON_TAG_0x004={
00675     7: ('WhiteBalance',
00676         {0: 'Auto',
00677          1: 'Sunny',
00678          2: 'Cloudy',
00679          3: 'Tungsten',
00680          4: 'Fluorescent',
00681          5: 'Flash',
00682          6: 'Custom'}),
00683     9: ('SequenceNumber', ),
00684     14: ('AFPointUsed', ),
00685     15: ('FlashBias',
00686         {0XFFC0: '-2 EV',
00687          0XFFCC: '-1.67 EV',
00688          0XFFD0: '-1.50 EV',
00689          0XFFD4: '-1.33 EV',
00690          0XFFE0: '-1 EV',
00691          0XFFEC: '-0.67 EV',
00692          0XFFF0: '-0.50 EV',
00693          0XFFF4: '-0.33 EV',
00694          0X0000: '0 EV',
00695          0X000C: '0.33 EV',
00696          0X0010: '0.50 EV',
00697          0X0014: '0.67 EV',
00698          0X0020: '1 EV',
00699          0X002C: '1.33 EV',
00700          0X0030: '1.50 EV',
00701          0X0034: '1.67 EV',
00702          0X0040: '2 EV'}),
00703     19: ('SubjectDistance', )
00704     }
00705 
00706 # extract multibyte integer in Motorola format (little endian)
00707 def s2n_motorola(str):
00708     x=0
00709     for c in str:
00710         x=(x << 8) | ord(c)
00711     return x
00712 
00713 # extract multibyte integer in Intel format (big endian)
00714 def s2n_intel(str):
00715     x=0
00716     y=0L
00717     for c in str:
00718         x=x | (ord(c) << y)
00719         y=y+8
00720     return x
00721 
00722 # ratio object that eventually will be able to reduce itself to lowest
00723 # common denominator for printing
00724 def gcd(a, b):
00725    if b == 0:
00726       return a
00727    else:
00728       return gcd(b, a % b)
00729 
00730 # class that handles an EXIF header
00731 class EXIF_header:
00732     def __init__(self, file, endian, offset, fake_exif, debug=0):
00733         self.file=file
00734         self.endian=endian
00735         self.offset=offset
00736         self.fake_exif=fake_exif
00737         self.debug=debug
00738         self.tags={}
00739 
00740     # convert slice to integer, based on sign and endian flags
00741     # usually this offset is assumed to be relative to the beginning of the
00742     # start of the EXIF information.  For some cameras that use relative tags,
00743     # this offset may be relative to some other starting point.
00744     def s2n(self, offset, length, signed=0):
00745         self.file.seek(self.offset+offset)
00746         slice=self.file.read(length)
00747         if self.endian == 'I':
00748             val=s2n_intel(slice)
00749         else:
00750             val=s2n_motorola(slice)
00751         # Sign extension ?
00752         if signed:
00753             msb=1L << (8*length-1)
00754             if val & msb:
00755                 val=val-(msb << 1)
00756         return val
00757 
00758     # convert offset to string
00759     def n2s(self, offset, length):
00760         s=''
00761         for i in range(length):
00762             if self.endian == 'I':
00763                 s=s+chr(offset & 0xFF)
00764             else:
00765                 s=chr(offset & 0xFF)+s
00766             offset=offset >> 8
00767         return s
00768 
00769     # return first IFD
00770     def first_IFD(self):
00771         return self.s2n(4, 4)
00772 
00773     # return pointer to next IFD
00774     def next_IFD(self, ifd):
00775         entries=self.s2n(ifd, 2)
00776         return self.s2n(ifd+2+12*entries, 4)
00777 
00778     # return list of IFDs in header
00779     def list_IFDs(self):
00780         i=self.first_IFD()
00781         a=[]
00782         while i:
00783             a.append(i)
00784             i=self.next_IFD(i)
00785         return a
00786 
00787     # return list of entries in this IFD
00788     def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0):
00789         entries=self.s2n(ifd, 2)
00790         for i in range(entries):
00791             # entry is index of start of this IFD in the file
00792             entry=ifd+2+12*i
00793             tag=self.s2n(entry, 2)
00794             # get tag name.  We do it early to make debugging easier
00795             tag_entry=dict.get(tag)
00796             if tag_entry:
00797                 tag_name=tag_entry[0]
00798             else:
00799                 tag_name='Tag 0x%04X' % tag
00800             field_type=self.s2n(entry+2, 2)
00801             if not 0 < field_type < len(FIELD_TYPES):
00802                 # unknown field type
00803                 raise ValueError, \
00804                       'unknown type %d in tag 0x%04X' % (field_type, tag)
00805 
00806             typelen=FIELD_TYPES[field_type][0]
00807             count=self.s2n(entry+4, 4)
00808             offset=entry+8
00809             if count*typelen > 4:
00810                 # offset is not the value; it's a pointer to the value
00811                 # if relative we set things up so s2n will seek to the right
00812                 # place when it adds self.offset.  Note that this 'relative'
00813                 # is for the Nikon type 3 makernote.  Other cameras may use
00814                 # other relative offsets, which would have to be computed here
00815                 # slightly differently.
00816                 if relative:
00817                     tmp_offset=self.s2n(offset, 4)
00818                     offset=tmp_offset+ifd-self.offset+4
00819                     if self.fake_exif:
00820                         offset=offset+18
00821                 else:
00822                     offset=self.s2n(offset, 4)
00823             field_offset=offset
00824             if field_type == 2:
00825                 # special case: null-terminated ASCII string
00826                 if count != 0:
00827                     self.file.seek(self.offset+offset)
00828                     values=self.file.read(count)
00829                     values=values.strip().replace('\x00','')
00830                 else:
00831                     values=''
00832             else:
00833                 values=[]
00834                 signed=(field_type in [6, 8, 9, 10])
00835                 for j in range(count):
00836                     if field_type in (5, 10):
00837                         # a ratio => reduce it if possible
00838                         num = self.s2n(offset,   4, signed)
00839                         den = self.s2n(offset+4, 4, signed)
00840                         div=gcd(num, den)
00841                         if div > 1:
00842                           num=num/div
00843                           den=den/div
00844                         if(den==1):
00845                           value_j = str(num)
00846                         else:
00847                           value_j = "%d/%d" % (num, den)
00848                     else:
00849                         value_j=self.s2n(offset, typelen, signed)
00850                     values.append(value_j)
00851                     offset=offset+typelen
00852             # now "values" is either a string or an array
00853             if count == 1 and field_type != 2:
00854                 printable=str(values[0])
00855             else:
00856                 printable=str(values)
00857             # compute printable version of values
00858             if tag_entry:
00859                 if len(tag_entry) != 1:
00860                     # optional 2nd tag element is present
00861                     if callable(tag_entry[1]):
00862                         # call mapping function
00863                         printable=tag_entry[1](values)
00864                     else:
00865                         printable=''
00866                         for i in values:
00867                             # use lookup table for this tag
00868                             printable+=tag_entry[1].get(i, repr(i))
00869             printable=printable.strip().replace('\x00','')
00870             if(values):
00871                 self.tags[ifd_name+' '+tag_name] = (values[0],printable,field_offset)
00872             if self.debug:
00873                 print ' debug:   %s: %s' % (tag_name,
00874                                             repr(self.tags[ifd_name+' '+tag_name]))
00875 
00876     # decode all the camera-specific MakerNote formats
00877 
00878     # Note is the data that comprises this MakerNote.  The MakerNote will
00879     # likely have pointers in it that point to other parts of the file.  We'll
00880     # use self.offset as the starting point for most of those pointers, since
00881     # they are relative to the beginning of the file.
00882     #
00883     # If the MakerNote is in a newer format, it may use relative addressing
00884     # within the MakerNote.  In that case we'll use relative addresses for the
00885     # pointers.
00886     #
00887     # As an aside: it's not just to be annoying that the manufacturers use
00888     # relative offsets.  It's so that if the makernote has to be moved by the
00889     # picture software all of the offsets don't have to be adjusted.  Overall,
00890     # this is probably the right strategy for makernotes, though the spec is
00891     # ambiguous.  (The spec does not appear to imagine that makernotes would
00892     # follow EXIF format internally.  Once they did, it's ambiguous whether
00893     # the offsets should be from the header at the start of all the EXIF info,
00894     # or from the header at the start of the makernote.)
00895     def decode_maker_note(self):
00896         note=self.tags['EXIF MakerNote']
00897         make=self.tags['Image Make'][1]
00898         model=self.tags['Image Model'][1]
00899 
00900         # Nikon
00901         # The maker note usually starts with the word Nikon, followed by the
00902         # type of the makernote (1 or 2, as a short).  If the word Nikon is
00903         # not at the start of the makernote, it's probably type 2, since some
00904         # cameras work that way.
00905         if make in ('NIKON', 'NIKON CORPORATION'):
00906             if note.values[0:7] == [78, 105, 107, 111, 110, 00, 01]:
00907                 if self.debug:
00908                     print "Looks like a type 1 Nikon MakerNote."
00909                 self.dump_IFD(note[2]+8, 'MakerNote',
00910                               dict=MAKERNOTE_NIKON_OLDER_TAGS)
00911             elif note.values[0:7] == [78, 105, 107, 111, 110, 00, 02]:
00912                 if self.debug:
00913                     print "Looks like a labeled type 2 Nikon MakerNote"
00914                 if note[0][12:14] != [0, 42] and note[0][12:14] != [42L, 0L]:
00915                     raise ValueError, "Missing marker tag '42' in MakerNote."
00916                 # skip the Makernote label and the TIFF header
00917                 self.dump_IFD(note[2]+10+8, 'MakerNote',
00918                               dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1)
00919             else:
00920                 # E99x or D1
00921                 if self.debug:
00922                     print "Looks like an unlabeled type 2 Nikon MakerNote"
00923                 self.dump_IFD(note[2], 'MakerNote',
00924                               dict=MAKERNOTE_NIKON_NEWER_TAGS)
00925             return
00926 
00927         # Olympus
00928         if make[:7] == 'OLYMPUS':
00929             self.dump_IFD(note[2]+8, 'MakerNote',
00930                           dict=MAKERNOTE_OLYMPUS_TAGS)
00931             return
00932 
00933         # Casio
00934         if make == 'Casio':
00935             self.dump_IFD(note[2], 'MakerNote',
00936                           dict=MAKERNOTE_CASIO_TAGS)
00937             return
00938 
00939         # Fujifilm
00940         if make == 'FUJIFILM':
00941             # bug: everything else is "Motorola" endian, but the MakerNote
00942             # is "Intel" endian
00943             endian=self.endian
00944             self.endian='I'
00945             # bug: IFD offsets are from beginning of MakerNote, not
00946             # beginning of file header
00947             offset=self.offset
00948             self.offset+=note[2]
00949             # process note with bogus values (note is actually at offset 12)
00950             self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
00951             # reset to correct values
00952             self.endian=endian
00953             self.offset=offset
00954             return
00955 
00956         # Canon
00957         if make == 'Canon':
00958             self.dump_IFD(note[2], 'MakerNote',dict=MAKERNOTE_CANON_TAGS)
00959             for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
00960                       ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
00961                 self.canon_decode_tag(self.tags[i[0]][0], i[1])
00962             return
00963 
00964     # decode Canon MakerNote tag based on offset within tag
00965     # see http://www.burren.cx/david/canon.html by David Burren
00966     def canon_decode_tag(self, value, dict):
00967         for i in range(1, len(value)):
00968             x=dict.get(i, ('Unknown', ))
00969             if self.debug:
00970                 print i, x
00971             name=x[0]
00972             if len(x) > 1:
00973                 val=x[1].get(value[i], 'Unknown')
00974             else:
00975                 val=value[i]
00976             # it's not a real IFD Tag but we fake one to make everybody
00977             # happy. this will have a "proprietary" type
00978             self.tags['MakerNote '+name]=str(val)
00979 
00980 # process an image file (expects an open file object)
00981 # this is the function that has to deal with all the arbitrary nasty bits
00982 # of the EXIF standard
00983 def process_file(file, debug=0):
00984     data=file.read(12)
00985     if data[0:4] in ['II*\x00', 'MM\x00*']:
00986         # it's a TIFF file
00987         file.seek(0)
00988         endian=file.read(1)
00989         file.read(1)
00990         offset=0
00991         fake_exif=0
00992     elif data[0:2] == '\xFF\xD8':
00993         # it's a JPEG file
00994         # skip JFIF style header(s)
00995         fake_exif=0
00996         while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM'):
00997             length=ord(data[4])*256+ord(data[5])
00998             file.read(length-8)
00999             # fake an EXIF beginning of file
01000             data='\xFF\x00'+file.read(10)
01001             fake_exif=1
01002         if data[2] == '\xFF' and data[6:10] == 'Exif':
01003             # detected EXIF header
01004             offset=file.tell()
01005             endian=file.read(1)
01006         else:
01007             # no EXIF information
01008             return {}
01009     else:
01010         # file format not recognized
01011         return {}
01012 
01013     # deal with the EXIF info we found
01014     if debug:
01015         print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
01016     hdr=EXIF_header(file, endian, offset, fake_exif, debug)
01017     ifd_list=hdr.list_IFDs()
01018     ctr=0
01019     for i in ifd_list:
01020         if ctr == 0:
01021             IFD_name='Image'
01022         elif ctr == 1:
01023             IFD_name='Thumbnail'
01024             thumb_ifd=i
01025         else:
01026             IFD_name='IFD %d' % ctr
01027         if debug:
01028             print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
01029         hdr.dump_IFD(i, IFD_name)
01030         # EXIF IFD
01031         exif_off=hdr.tags.get(IFD_name+' ExifOffset')
01032         if exif_off:
01033             if debug:
01034                 print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
01035             hdr.dump_IFD(exif_off[0], 'EXIF')
01036             # Interoperability IFD contained in EXIF IFD
01037             intr_off=hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
01038             if intr_off:
01039                 if debug:
01040                     print ' EXIF Interoperability SubSubIFD at offset %d:' \
01041                           % intr_off.values[0]
01042                 hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
01043                              dict=INTR_TAGS)
01044         # GPS IFD
01045         gps_off=hdr.tags.get(IFD_name+' GPSInfo')
01046         if gps_off:
01047             if debug:
01048                 print ' GPS SubIFD at offset %d:' % gps_off.values[0]
01049             hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS)
01050         ctr+=1
01051 
01052     if hdr.tags.has_key('EXIF MakerNote'):
01053         try:
01054             hdr.decode_maker_note()
01055         except:
01056             pass
01057         del hdr.tags['EXIF MakerNote']
01058     dict = {}
01059     tags = hdr.tags.keys()
01060     tags.sort()
01061     for tag in tags:
01062         hdr.tags[tag] = hdr.tags[tag][1]
01063     return hdr.tags
01064 
01065 # library test/debug function (dump given files)
01066 if __name__ == '__main__':
01067     import sys
01068     import gc
01069     import time
01070     gc.enable()
01071     start_time = time.clock()
01072     start = len(gc.get_objects())
01073     print "%s object at the beginning" % start
01074 
01075     if len(sys.argv) < 2:
01076         print 'Usage: %s files...\n' % sys.argv[0]
01077         sys.exit(0)
01078 
01079     for filename in sys.argv[1:]:
01080         try:
01081             file=open(filename, 'rb')
01082         except:
01083             print filename, 'unreadable'
01084             print
01085             continue
01086         print filename+':'
01087         # data=process_file(file, 1) # with debug info
01088         data=process_file(file)
01089         if not data:
01090             print 'No EXIF information found'
01091             continue
01092 
01093         x=data.keys()
01094         x.sort()
01095         for i in x:
01096            print "   %s: %s" % (i, data[i])
01097     end_time = time.clock()
01098     duration = end_time - start_time
01099     end = len(gc.get_objects())
01100     print ""
01101     print "---------------------------------------------"
01102     print "%s object at the end" % end
01103     print "so %s object have been created for one photo" % (end-start)
01104     print "---------------------------------------------"
01105     print "Time needed : %s" % duration
01106