bgneal@1: #!/usr/bin/env python
bgneal@1: # -*- coding: utf-8 -*-
bgneal@1: #
bgneal@1: # Library to extract EXIF information from digital camera image files
bgneal@1: # http://sourceforge.net/projects/exif-py/
bgneal@1: #
bgneal@1: # VERSION 1.0.7
bgneal@1: #
bgneal@1: # To use this library call with:
bgneal@1: #    f = open(path_name, 'rb')
bgneal@1: #    tags = EXIF.process_file(f)
bgneal@1: #
bgneal@1: # To ignore makerNote tags, pass the -q or --quick
bgneal@1: # command line arguments, or as
bgneal@1: #    f = open(path_name, 'rb')
bgneal@1: #    tags = EXIF.process_file(f, details=False)
bgneal@1: #
bgneal@1: # To stop processing after a certain tag is retrieved,
bgneal@1: # pass the -t TAG or --stop-tag TAG argument, or as
bgneal@1: #    f = open(path_name, 'rb')
bgneal@1: #    tags = EXIF.process_file(f, stop_tag='TAG')
bgneal@1: #
bgneal@1: # where TAG is a valid tag name, ex 'DateTimeOriginal'
bgneal@1: #
bgneal@1: # These are useful when you are retrieving a large list of images
bgneal@1: #
bgneal@1: # Returned tags will be a dictionary mapping names of EXIF tags to their
bgneal@1: # values in the file named by path_name.  You can process the tags
bgneal@1: # as you wish.  In particular, you can iterate through all the tags with:
bgneal@1: #     for tag in tags.keys():
bgneal@1: #         if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename',
bgneal@1: #                        'EXIF MakerNote'):
bgneal@1: #             print "Key: %s, value %s" % (tag, tags[tag])
bgneal@1: # (This code uses the if statement to avoid printing out a few of the
bgneal@1: # tags that tend to be long or boring.)
bgneal@1: #
bgneal@1: # The tags dictionary will include keys for all of the usual EXIF
bgneal@1: # tags, and will also include keys for Makernotes used by some
bgneal@1: # cameras, for which we have a good specification.
bgneal@1: #
bgneal@1: # Note that the dictionary keys are the IFD name followed by the
bgneal@1: # tag name. For example:
bgneal@1: # 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode'
bgneal@1: #
bgneal@1: # Copyright (c) 2002-2007 Gene Cash All rights reserved
bgneal@1: # Copyright (c) 2007 Ianaré Sévi All rights reserved
bgneal@1: #
bgneal@1: # Redistribution and use in source and binary forms, with or without
bgneal@1: # modification, are permitted provided that the following conditions
bgneal@1: # are met:
bgneal@1: #
bgneal@1: #  1. Redistributions of source code must retain the above copyright
bgneal@1: #     notice, this list of conditions and the following disclaimer.
bgneal@1: #
bgneal@1: #  2. Redistributions in binary form must reproduce the above
bgneal@1: #     copyright notice, this list of conditions and the following
bgneal@1: #     disclaimer in the documentation and/or other materials provided
bgneal@1: #     with the distribution.
bgneal@1: #
bgneal@1: #  3. Neither the name of the authors nor the names of its contributors
bgneal@1: #     may be used to endorse or promote products derived from this
bgneal@1: #     software without specific prior written permission.
bgneal@1: #
bgneal@1: # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
bgneal@1: # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
bgneal@1: # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
bgneal@1: # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
bgneal@1: # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
bgneal@1: # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
bgneal@1: # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
bgneal@1: # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
bgneal@1: # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
bgneal@1: # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
bgneal@1: # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
bgneal@1: #
bgneal@1: #
bgneal@1: # ----- See 'changes.txt' file for all contributors and changes ----- #
bgneal@1: #
bgneal@1: 
bgneal@1: 
bgneal@1: # Don't throw an exception when given an out of range character.
bgneal@1: def make_string(seq):
bgneal@1:     str = ""
bgneal@1:     for c in seq:
bgneal@1:         # Screen out non-printing characters
bgneal@1:         if 32 <= c and c < 256:
bgneal@1:             str += chr(c)
bgneal@1:     # If no printing chars
bgneal@1:     if not str:
bgneal@1:         return seq
bgneal@1:     return str
bgneal@1: 
bgneal@1: # Special version to deal with the code in the first 8 bytes of a user comment.
bgneal@1: def make_string_uc(seq):
bgneal@1:     code = seq[0:8]
bgneal@1:     seq = seq[8:]
bgneal@1:     # Of course, this is only correct if ASCII, and the standard explicitly
bgneal@1:     # allows JIS and Unicode.
bgneal@1:     return make_string(seq)
bgneal@1: 
bgneal@1: # field type descriptions as (length, abbreviation, full name) tuples
bgneal@1: FIELD_TYPES = (
bgneal@1:     (0, 'X', 'Proprietary'), # no such type
bgneal@1:     (1, 'B', 'Byte'),
bgneal@1:     (1, 'A', 'ASCII'),
bgneal@1:     (2, 'S', 'Short'),
bgneal@1:     (4, 'L', 'Long'),
bgneal@1:     (8, 'R', 'Ratio'),
bgneal@1:     (1, 'SB', 'Signed Byte'),
bgneal@1:     (1, 'U', 'Undefined'),
bgneal@1:     (2, 'SS', 'Signed Short'),
bgneal@1:     (4, 'SL', 'Signed Long'),
bgneal@1:     (8, 'SR', 'Signed Ratio'),
bgneal@1:     )
bgneal@1: 
bgneal@1: # dictionary of main EXIF tag names
bgneal@1: # first element of tuple is tag name, optional second element is
bgneal@1: # another dictionary giving names to values
bgneal@1: EXIF_TAGS = {
bgneal@1:     0x0100: ('ImageWidth', ),
bgneal@1:     0x0101: ('ImageLength', ),
bgneal@1:     0x0102: ('BitsPerSample', ),
bgneal@1:     0x0103: ('Compression',
bgneal@1:              {1: 'Uncompressed TIFF',
bgneal@1:               6: 'JPEG Compressed'}),
bgneal@1:     0x0106: ('PhotometricInterpretation', ),
bgneal@1:     0x010A: ('FillOrder', ),
bgneal@1:     0x010D: ('DocumentName', ),
bgneal@1:     0x010E: ('ImageDescription', ),
bgneal@1:     0x010F: ('Make', ),
bgneal@1:     0x0110: ('Model', ),
bgneal@1:     0x0111: ('StripOffsets', ),
bgneal@1:     0x0112: ('Orientation',
bgneal@1:              {1: 'Horizontal (normal)',
bgneal@1:               2: 'Mirrored horizontal',
bgneal@1:               3: 'Rotated 180',
bgneal@1:               4: 'Mirrored vertical',
bgneal@1:               5: 'Mirrored horizontal then rotated 90 CCW',
bgneal@1:               6: 'Rotated 90 CW',
bgneal@1:               7: 'Mirrored horizontal then rotated 90 CW',
bgneal@1:               8: 'Rotated 90 CCW'}),
bgneal@1:     0x0115: ('SamplesPerPixel', ),
bgneal@1:     0x0116: ('RowsPerStrip', ),
bgneal@1:     0x0117: ('StripByteCounts', ),
bgneal@1:     0x011A: ('XResolution', ),
bgneal@1:     0x011B: ('YResolution', ),
bgneal@1:     0x011C: ('PlanarConfiguration', ),
bgneal@1:     0x0128: ('ResolutionUnit',
bgneal@1:              {1: 'Not Absolute',
bgneal@1:               2: 'Pixels/Inch',
bgneal@1:               3: 'Pixels/Centimeter'}),
bgneal@1:     0x012D: ('TransferFunction', ),
bgneal@1:     0x0131: ('Software', ),
bgneal@1:     0x0132: ('DateTime', ),
bgneal@1:     0x013B: ('Artist', ),
bgneal@1:     0x013E: ('WhitePoint', ),
bgneal@1:     0x013F: ('PrimaryChromaticities', ),
bgneal@1:     0x0156: ('TransferRange', ),
bgneal@1:     0x0200: ('JPEGProc', ),
bgneal@1:     0x0201: ('JPEGInterchangeFormat', ),
bgneal@1:     0x0202: ('JPEGInterchangeFormatLength', ),
bgneal@1:     0x0211: ('YCbCrCoefficients', ),
bgneal@1:     0x0212: ('YCbCrSubSampling', ),
bgneal@1:     0x0213: ('YCbCrPositioning', ),
bgneal@1:     0x0214: ('ReferenceBlackWhite', ),
bgneal@1:     0x828D: ('CFARepeatPatternDim', ),
bgneal@1:     0x828E: ('CFAPattern', ),
bgneal@1:     0x828F: ('BatteryLevel', ),
bgneal@1:     0x8298: ('Copyright', ),
bgneal@1:     0x829A: ('ExposureTime', ),
bgneal@1:     0x829D: ('FNumber', ),
bgneal@1:     0x83BB: ('IPTC/NAA', ),
bgneal@1:     0x8769: ('ExifOffset', ),
bgneal@1:     0x8773: ('InterColorProfile', ),
bgneal@1:     0x8822: ('ExposureProgram',
bgneal@1:              {0: 'Unidentified',
bgneal@1:               1: 'Manual',
bgneal@1:               2: 'Program Normal',
bgneal@1:               3: 'Aperture Priority',
bgneal@1:               4: 'Shutter Priority',
bgneal@1:               5: 'Program Creative',
bgneal@1:               6: 'Program Action',
bgneal@1:               7: 'Portrait Mode',
bgneal@1:               8: 'Landscape Mode'}),
bgneal@1:     0x8824: ('SpectralSensitivity', ),
bgneal@1:     0x8825: ('GPSInfo', ),
bgneal@1:     0x8827: ('ISOSpeedRatings', ),
bgneal@1:     0x8828: ('OECF', ),
bgneal@1:     # print as string
bgneal@1:     0x9000: ('ExifVersion', make_string),
bgneal@1:     0x9003: ('DateTimeOriginal', ),
bgneal@1:     0x9004: ('DateTimeDigitized', ),
bgneal@1:     0x9101: ('ComponentsConfiguration',
bgneal@1:              {0: '',
bgneal@1:               1: 'Y',
bgneal@1:               2: 'Cb',
bgneal@1:               3: 'Cr',
bgneal@1:               4: 'Red',
bgneal@1:               5: 'Green',
bgneal@1:               6: 'Blue'}),
bgneal@1:     0x9102: ('CompressedBitsPerPixel', ),
bgneal@1:     0x9201: ('ShutterSpeedValue', ),
bgneal@1:     0x9202: ('ApertureValue', ),
bgneal@1:     0x9203: ('BrightnessValue', ),
bgneal@1:     0x9204: ('ExposureBiasValue', ),
bgneal@1:     0x9205: ('MaxApertureValue', ),
bgneal@1:     0x9206: ('SubjectDistance', ),
bgneal@1:     0x9207: ('MeteringMode',
bgneal@1:              {0: 'Unidentified',
bgneal@1:               1: 'Average',
bgneal@1:               2: 'CenterWeightedAverage',
bgneal@1:               3: 'Spot',
bgneal@1:               4: 'MultiSpot'}),
bgneal@1:     0x9208: ('LightSource',
bgneal@1:              {0: 'Unknown',
bgneal@1:               1: 'Daylight',
bgneal@1:               2: 'Fluorescent',
bgneal@1:               3: 'Tungsten',
bgneal@1:               10: 'Flash',
bgneal@1:               17: 'Standard Light A',
bgneal@1:               18: 'Standard Light B',
bgneal@1:               19: 'Standard Light C',
bgneal@1:               20: 'D55',
bgneal@1:               21: 'D65',
bgneal@1:               22: 'D75',
bgneal@1:               255: 'Other'}),
bgneal@1:     0x9209: ('Flash', {0: 'No',
bgneal@1:                        1: 'Fired',
bgneal@1:                        5: 'Fired (?)', # no return sensed
bgneal@1:                        7: 'Fired (!)', # return sensed
bgneal@1:                        9: 'Fill Fired',
bgneal@1:                        13: 'Fill Fired (?)',
bgneal@1:                        15: 'Fill Fired (!)',
bgneal@1:                        16: 'Off',
bgneal@1:                        24: 'Auto Off',
bgneal@1:                        25: 'Auto Fired',
bgneal@1:                        29: 'Auto Fired (?)',
bgneal@1:                        31: 'Auto Fired (!)',
bgneal@1:                        32: 'Not Available'}),
bgneal@1:     0x920A: ('FocalLength', ),
bgneal@1:     0x9214: ('SubjectArea', ),
bgneal@1:     0x927C: ('MakerNote', ),
bgneal@1:     # print as string
bgneal@1:     0x9286: ('UserComment', make_string_uc),  # First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode
bgneal@1:     0x9290: ('SubSecTime', ),
bgneal@1:     0x9291: ('SubSecTimeOriginal', ),
bgneal@1:     0x9292: ('SubSecTimeDigitized', ),
bgneal@1:     # print as string
bgneal@1:     0xA000: ('FlashPixVersion', make_string),
bgneal@1:     0xA001: ('ColorSpace', ),
bgneal@1:     0xA002: ('ExifImageWidth', ),
bgneal@1:     0xA003: ('ExifImageLength', ),
bgneal@1:     0xA005: ('InteroperabilityOffset', ),
bgneal@1:     0xA20B: ('FlashEnergy', ),               # 0x920B in TIFF/EP
bgneal@1:     0xA20C: ('SpatialFrequencyResponse', ),  # 0x920C    -  -
bgneal@1:     0xA20E: ('FocalPlaneXResolution', ),     # 0x920E    -  -
bgneal@1:     0xA20F: ('FocalPlaneYResolution', ),     # 0x920F    -  -
bgneal@1:     0xA210: ('FocalPlaneResolutionUnit', ),  # 0x9210    -  -
bgneal@1:     0xA214: ('SubjectLocation', ),           # 0x9214    -  -
bgneal@1:     0xA215: ('ExposureIndex', ),             # 0x9215    -  -
bgneal@1:     0xA217: ('SensingMethod', ),             # 0x9217    -  -
bgneal@1:     0xA300: ('FileSource',
bgneal@1:              {3: 'Digital Camera'}),
bgneal@1:     0xA301: ('SceneType',
bgneal@1:              {1: 'Directly Photographed'}),
bgneal@1:     0xA302: ('CVAPattern', ),
bgneal@1:     0xA401: ('CustomRendered', ),
bgneal@1:     0xA402: ('ExposureMode',
bgneal@1:              {0: 'Auto Exposure',
bgneal@1:               1: 'Manual Exposure',
bgneal@1:               2: 'Auto Bracket'}),
bgneal@1:     0xA403: ('WhiteBalance',
bgneal@1:              {0: 'Auto',
bgneal@1:               1: 'Manual'}),
bgneal@1:     0xA404: ('DigitalZoomRatio', ),
bgneal@1:     0xA405: ('FocalLengthIn35mm', ),
bgneal@1:     0xA406: ('SceneCaptureType', ),
bgneal@1:     0xA407: ('GainControl', ),
bgneal@1:     0xA408: ('Contrast', ),
bgneal@1:     0xA409: ('Saturation', ),
bgneal@1:     0xA40A: ('Sharpness', ),
bgneal@1:     0xA40C: ('SubjectDistanceRange', ),
bgneal@1:     }
bgneal@1: 
bgneal@1: # interoperability tags
bgneal@1: INTR_TAGS = {
bgneal@1:     0x0001: ('InteroperabilityIndex', ),
bgneal@1:     0x0002: ('InteroperabilityVersion', ),
bgneal@1:     0x1000: ('RelatedImageFileFormat', ),
bgneal@1:     0x1001: ('RelatedImageWidth', ),
bgneal@1:     0x1002: ('RelatedImageLength', ),
bgneal@1:     }
bgneal@1: 
bgneal@1: # GPS tags (not used yet, haven't seen camera with GPS)
bgneal@1: GPS_TAGS = {
bgneal@1:     0x0000: ('GPSVersionID', ),
bgneal@1:     0x0001: ('GPSLatitudeRef', ),
bgneal@1:     0x0002: ('GPSLatitude', ),
bgneal@1:     0x0003: ('GPSLongitudeRef', ),
bgneal@1:     0x0004: ('GPSLongitude', ),
bgneal@1:     0x0005: ('GPSAltitudeRef', ),
bgneal@1:     0x0006: ('GPSAltitude', ),
bgneal@1:     0x0007: ('GPSTimeStamp', ),
bgneal@1:     0x0008: ('GPSSatellites', ),
bgneal@1:     0x0009: ('GPSStatus', ),
bgneal@1:     0x000A: ('GPSMeasureMode', ),
bgneal@1:     0x000B: ('GPSDOP', ),
bgneal@1:     0x000C: ('GPSSpeedRef', ),
bgneal@1:     0x000D: ('GPSSpeed', ),
bgneal@1:     0x000E: ('GPSTrackRef', ),
bgneal@1:     0x000F: ('GPSTrack', ),
bgneal@1:     0x0010: ('GPSImgDirectionRef', ),
bgneal@1:     0x0011: ('GPSImgDirection', ),
bgneal@1:     0x0012: ('GPSMapDatum', ),
bgneal@1:     0x0013: ('GPSDestLatitudeRef', ),
bgneal@1:     0x0014: ('GPSDestLatitude', ),
bgneal@1:     0x0015: ('GPSDestLongitudeRef', ),
bgneal@1:     0x0016: ('GPSDestLongitude', ),
bgneal@1:     0x0017: ('GPSDestBearingRef', ),
bgneal@1:     0x0018: ('GPSDestBearing', ),
bgneal@1:     0x0019: ('GPSDestDistanceRef', ),
bgneal@1:     0x001A: ('GPSDestDistance', ),
bgneal@1:     }
bgneal@1: 
bgneal@1: # Ignore these tags when quick processing
bgneal@1: # 0x927C is MakerNote Tags
bgneal@1: # 0x9286 is user comment
bgneal@1: IGNORE_TAGS=(0x9286, 0x927C)
bgneal@1: 
bgneal@1: # http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp
bgneal@1: def nikon_ev_bias(seq):
bgneal@1:     # First digit seems to be in steps of 1/6 EV.
bgneal@1:     # Does the third value mean the step size?  It is usually 6,
bgneal@1:     # but it is 12 for the ExposureDifference.
bgneal@1:     #
bgneal@1:     if seq == [252, 1, 6, 0]:
bgneal@1:         return "-2/3 EV"
bgneal@1:     if seq == [253, 1, 6, 0]:
bgneal@1:         return "-1/2 EV"
bgneal@1:     if seq == [254, 1, 6, 0]:
bgneal@1:         return "-1/3 EV"
bgneal@1:     if seq == [0, 1, 6, 0]:
bgneal@1:         return "0 EV"
bgneal@1:     if seq == [2, 1, 6, 0]:
bgneal@1:         return "+1/3 EV"
bgneal@1:     if seq == [3, 1, 6, 0]:
bgneal@1:         return "+1/2 EV"
bgneal@1:     if seq == [4, 1, 6, 0]:
bgneal@1:         return "+2/3 EV"
bgneal@1:     # Handle combinations not in the table.
bgneal@1:     a = seq[0]
bgneal@1:     # Causes headaches for the +/- logic, so special case it.
bgneal@1:     if a == 0:
bgneal@1:         return "0 EV"
bgneal@1:     if a > 127:
bgneal@1:         a = 256 - a
bgneal@1:         ret_str = "-"
bgneal@1:     else:
bgneal@1:         ret_str = "+"
bgneal@1:     b = seq[2]	# Assume third value means the step size
bgneal@1:     whole = a / b
bgneal@1:     a = a % b
bgneal@1:     if whole != 0:
bgneal@1:         ret_str = ret_str + str(whole) + " "
bgneal@1:     if a == 0:
bgneal@1:         ret_str = ret_str + "EV"
bgneal@1:     else:
bgneal@1:         r = Ratio(a, b)
bgneal@1:         ret_str = ret_str + r.__repr__() + " EV"
bgneal@1:     return ret_str
bgneal@1: 
bgneal@1: # Nikon E99x MakerNote Tags
bgneal@1: MAKERNOTE_NIKON_NEWER_TAGS={
bgneal@1:     0x0001: ('MakernoteVersion', make_string),	# Sometimes binary
bgneal@1:     0x0002: ('ISOSetting', ),
bgneal@1:     0x0003: ('ColorMode', ),
bgneal@1:     0x0004: ('Quality', ),
bgneal@1:     0x0005: ('Whitebalance', ),
bgneal@1:     0x0006: ('ImageSharpening', ),
bgneal@1:     0x0007: ('FocusMode', ),
bgneal@1:     0x0008: ('FlashSetting', ),
bgneal@1:     0x0009: ('AutoFlashMode', ),
bgneal@1:     0x000B: ('WhiteBalanceBias', ),
bgneal@1:     0x000C: ('WhiteBalanceRBCoeff', ),
bgneal@1:     0x000D: ('ProgramShift', nikon_ev_bias),
bgneal@1:     # Nearly the same as the other EV vals, but step size is 1/12 EV (?)
bgneal@1:     0x000E: ('ExposureDifference', nikon_ev_bias),
bgneal@1:     0x000F: ('ISOSelection', ),
bgneal@1:     0x0011: ('NikonPreview', ),
bgneal@1:     0x0012: ('FlashCompensation', nikon_ev_bias),
bgneal@1:     0x0013: ('ISOSpeedRequested', ),
bgneal@1:     0x0016: ('PhotoCornerCoordinates', ),
bgneal@1:     # 0x0017: Unknown, but most likely an EV value
bgneal@1:     0x0018: ('FlashBracketCompensationApplied', nikon_ev_bias),
bgneal@1:     0x0019: ('AEBracketCompensationApplied', ),
bgneal@1:     0x001A: ('ImageProcessing', ),
bgneal@1:     0x0080: ('ImageAdjustment', ),
bgneal@1:     0x0081: ('ToneCompensation', ),
bgneal@1:     0x0082: ('AuxiliaryLens', ),
bgneal@1:     0x0083: ('LensType', ),
bgneal@1:     0x0084: ('LensMinMaxFocalMaxAperture', ),
bgneal@1:     0x0085: ('ManualFocusDistance', ),
bgneal@1:     0x0086: ('DigitalZoomFactor', ),
bgneal@1:     0x0087: ('FlashMode',
bgneal@1:              {0x00: 'Did Not Fire',
bgneal@1:               0x01: 'Fired, Manual',
bgneal@1:               0x07: 'Fired, External',
bgneal@1:               0x08: 'Fired, Commander Mode ',
bgneal@1:               0x09: 'Fired, TTL Mode'}),
bgneal@1:     0x0088: ('AFFocusPosition',
bgneal@1:              {0x0000: 'Center',
bgneal@1:               0x0100: 'Top',
bgneal@1:               0x0200: 'Bottom',
bgneal@1:               0x0300: 'Left',
bgneal@1:               0x0400: 'Right'}),
bgneal@1:     0x0089: ('BracketingMode',
bgneal@1:              {0x00: 'Single frame, no bracketing',
bgneal@1:               0x01: 'Continuous, no bracketing',
bgneal@1:               0x02: 'Timer, no bracketing',
bgneal@1:               0x10: 'Single frame, exposure bracketing',
bgneal@1:               0x11: 'Continuous, exposure bracketing',
bgneal@1:               0x12: 'Timer, exposure bracketing',
bgneal@1:               0x40: 'Single frame, white balance bracketing',
bgneal@1:               0x41: 'Continuous, white balance bracketing',
bgneal@1:               0x42: 'Timer, white balance bracketing'}),
bgneal@1:     0x008A: ('AutoBracketRelease', ),
bgneal@1:     0x008B: ('LensFStops', ),
bgneal@1:     0x008C: ('NEFCurve2', ),
bgneal@1:     0x008D: ('ColorMode', ),
bgneal@1:     0x008F: ('SceneMode', ),
bgneal@1:     0x0090: ('LightingType', ),
bgneal@1:     0x0091: ('ShotInfo', ),	# First 4 bytes are probably a version number in ASCII
bgneal@1:     0x0092: ('HueAdjustment', ),
bgneal@1:     # 0x0093: ('SaturationAdjustment', ),
bgneal@1:     0x0094: ('Saturation',	# Name conflict with 0x00AA !!
bgneal@1:              {-3: 'B&W',
bgneal@1:               -2: '-2',
bgneal@1:               -1: '-1',
bgneal@1:               0: '0',
bgneal@1:               1: '1',
bgneal@1:               2: '2'}),
bgneal@1:     0x0095: ('NoiseReduction', ),
bgneal@1:     0x0096: ('NEFCurve2', ),
bgneal@1:     0x0097: ('ColorBalance', ),
bgneal@1:     0x0098: ('LensData', ),	# First 4 bytes are a version number in ASCII
bgneal@1:     0x0099: ('RawImageCenter', ),
bgneal@1:     0x009A: ('SensorPixelSize', ),
bgneal@1:     0x009C: ('Scene Assist', ),
bgneal@1:     0x00A0: ('SerialNumber', ),
bgneal@1:     0x00A2: ('ImageDataSize', ),
bgneal@1:     # A4: In NEF, looks like a 4 byte ASCII version number
bgneal@1:     0x00A5: ('ImageCount', ),
bgneal@1:     0x00A6: ('DeletedImageCount', ),
bgneal@1:     0x00A7: ('TotalShutterReleases', ),
bgneal@1:     # A8: ExposureMode?  JPG: First 4 bytes are probably a version number in ASCII
bgneal@1:     # But in a sample NEF, its 8 zeros, then the string "NORMAL"
bgneal@1:     0x00A9: ('ImageOptimization', ),
bgneal@1:     0x00AA: ('Saturation', ),
bgneal@1:     0x00AB: ('DigitalVariProgram', ),
bgneal@1:     0x00AC: ('ImageStabilization', ),
bgneal@1:     0x00AD: ('Responsive AF', ),	# 'AFResponse'
bgneal@1:     0x0010: ('DataDump', ),
bgneal@1:     }
bgneal@1: 
bgneal@1: MAKERNOTE_NIKON_OLDER_TAGS = {
bgneal@1:     0x0003: ('Quality',
bgneal@1:              {1: 'VGA Basic',
bgneal@1:               2: 'VGA Normal',
bgneal@1:               3: 'VGA Fine',
bgneal@1:               4: 'SXGA Basic',
bgneal@1:               5: 'SXGA Normal',
bgneal@1:               6: 'SXGA Fine'}),
bgneal@1:     0x0004: ('ColorMode',
bgneal@1:              {1: 'Color',
bgneal@1:               2: 'Monochrome'}),
bgneal@1:     0x0005: ('ImageAdjustment',
bgneal@1:              {0: 'Normal',
bgneal@1:               1: 'Bright+',
bgneal@1:               2: 'Bright-',
bgneal@1:               3: 'Contrast+',
bgneal@1:               4: 'Contrast-'}),
bgneal@1:     0x0006: ('CCDSpeed',
bgneal@1:              {0: 'ISO 80',
bgneal@1:               2: 'ISO 160',
bgneal@1:               4: 'ISO 320',
bgneal@1:               5: 'ISO 100'}),
bgneal@1:     0x0007: ('WhiteBalance',
bgneal@1:              {0: 'Auto',
bgneal@1:               1: 'Preset',
bgneal@1:               2: 'Daylight',
bgneal@1:               3: 'Incandescent',
bgneal@1:               4: 'Fluorescent',
bgneal@1:               5: 'Cloudy',
bgneal@1:               6: 'Speed Light'}),
bgneal@1:     }
bgneal@1: 
bgneal@1: # decode Olympus SpecialMode tag in MakerNote
bgneal@1: def olympus_special_mode(v):
bgneal@1:     a={
bgneal@1:         0: 'Normal',
bgneal@1:         1: 'Unknown',
bgneal@1:         2: 'Fast',
bgneal@1:         3: 'Panorama'}
bgneal@1:     b={
bgneal@1:         0: 'Non-panoramic',
bgneal@1:         1: 'Left to right',
bgneal@1:         2: 'Right to left',
bgneal@1:         3: 'Bottom to top',
bgneal@1:         4: 'Top to bottom'}
bgneal@1:     if v[0] not in a or v[2] not in b:
bgneal@1:         return v
bgneal@1:     return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
bgneal@1: 
bgneal@1: MAKERNOTE_OLYMPUS_TAGS={
bgneal@1:     # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
bgneal@1:     # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
bgneal@1:     0x0100: ('JPEGThumbnail', ),
bgneal@1:     0x0200: ('SpecialMode', olympus_special_mode),
bgneal@1:     0x0201: ('JPEGQual',
bgneal@1:              {1: 'SQ',
bgneal@1:               2: 'HQ',
bgneal@1:               3: 'SHQ'}),
bgneal@1:     0x0202: ('Macro',
bgneal@1:              {0: 'Normal',
bgneal@1:              1: 'Macro',
bgneal@1:              2: 'SuperMacro'}),
bgneal@1:     0x0203: ('BWMode',
bgneal@1:              {0: 'Off',
bgneal@1:              1: 'On'}),
bgneal@1:     0x0204: ('DigitalZoom', ),
bgneal@1:     0x0205: ('FocalPlaneDiagonal', ),
bgneal@1:     0x0206: ('LensDistortionParams', ),
bgneal@1:     0x0207: ('SoftwareRelease', ),
bgneal@1:     0x0208: ('PictureInfo', ),
bgneal@1:     0x0209: ('CameraID', make_string), # print as string
bgneal@1:     0x0F00: ('DataDump', ),
bgneal@1:     0x0300: ('PreCaptureFrames', ),
bgneal@1:     0x0404: ('SerialNumber', ),
bgneal@1:     0x1000: ('ShutterSpeedValue', ),
bgneal@1:     0x1001: ('ISOValue', ),
bgneal@1:     0x1002: ('ApertureValue', ),
bgneal@1:     0x1003: ('BrightnessValue', ),
bgneal@1:     0x1004: ('FlashMode', ),
bgneal@1:     0x1004: ('FlashMode',
bgneal@1:        {2: 'On',
bgneal@1:         3: 'Off'}),
bgneal@1:     0x1005: ('FlashDevice',
bgneal@1:        {0: 'None',
bgneal@1:         1: 'Internal',
bgneal@1:         4: 'External',
bgneal@1:         5: 'Internal + External'}),
bgneal@1:     0x1006: ('ExposureCompensation', ),
bgneal@1:     0x1007: ('SensorTemperature', ),
bgneal@1:     0x1008: ('LensTemperature', ),
bgneal@1:     0x100b: ('FocusMode',
bgneal@1:        {0: 'Auto',
bgneal@1:         1: 'Manual'}),
bgneal@1:     0x1017: ('RedBalance', ),
bgneal@1:     0x1018: ('BlueBalance', ),
bgneal@1:     0x101a: ('SerialNumber', ),
bgneal@1:     0x1023: ('FlashExposureComp', ),
bgneal@1:     0x1026: ('ExternalFlashBounce',
bgneal@1:        {0: 'No',
bgneal@1:         1: 'Yes'}),
bgneal@1:     0x1027: ('ExternalFlashZoom', ),
bgneal@1:     0x1028: ('ExternalFlashMode', ),
bgneal@1:     0x1029: ('Contrast 	int16u',
bgneal@1:        {0: 'High',
bgneal@1:         1: 'Normal',
bgneal@1:         2: 'Low'}),
bgneal@1:     0x102a: ('SharpnessFactor', ),
bgneal@1:     0x102b: ('ColorControl', ),
bgneal@1:     0x102c: ('ValidBits', ),
bgneal@1:     0x102d: ('CoringFilter', ),
bgneal@1:     0x102e: ('OlympusImageWidth', ),
bgneal@1:     0x102f: ('OlympusImageHeight', ),
bgneal@1:     0x1034: ('CompressionRatio', ),
bgneal@1:     0x1035: ('PreviewImageValid',
bgneal@1:        {0: 'No',
bgneal@1:         1: 'Yes'}),
bgneal@1:     0x1036: ('PreviewImageStart', ),
bgneal@1:     0x1037: ('PreviewImageLength', ),
bgneal@1:     0x1039: ('CCDScanMode',
bgneal@1:        {0: 'Interlaced',
bgneal@1:         1: 'Progressive'}),
bgneal@1:     0x103a: ('NoiseReduction',
bgneal@1:        {0: 'Off',
bgneal@1:         1: 'On'}),
bgneal@1:     0x103b: ('InfinityLensStep', ),
bgneal@1:     0x103c: ('NearLensStep', ),
bgneal@1: 
bgneal@1:     # TODO - these need extra definitions
bgneal@1:     # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html
bgneal@1:     0x2010: ('Equipment', ),
bgneal@1:     0x2020: ('CameraSettings', ),
bgneal@1:     0x2030: ('RawDevelopment', ),
bgneal@1:     0x2040: ('ImageProcessing', ),
bgneal@1:     0x2050: ('FocusInfo', ),
bgneal@1:     0x3000: ('RawInfo ', ),
bgneal@1:     }
bgneal@1: 
bgneal@1: # 0x2020 CameraSettings
bgneal@1: MAKERNOTE_OLYMPUS_TAG_0x2020={
bgneal@1:     0x0100: ('PreviewImageValid',
bgneal@1:         {0: 'No',
bgneal@1:          1: 'Yes'}),
bgneal@1:     0x0101: ('PreviewImageStart', ),
bgneal@1:     0x0102: ('PreviewImageLength', ),
bgneal@1:     0x0200: ('ExposureMode', {
bgneal@1:         1: 'Manual',
bgneal@1:         2: 'Program',
bgneal@1:         3: 'Aperture-priority AE',
bgneal@1:         4: 'Shutter speed priority AE',
bgneal@1:         5: 'Program-shift'}),
bgneal@1:     0x0201: ('AELock',
bgneal@1:        {0: 'Off',
bgneal@1:         1: 'On'}),
bgneal@1:     0x0202: ('MeteringMode',
bgneal@1:        {2: 'Center Weighted',
bgneal@1:         3: 'Spot',
bgneal@1:         5: 'ESP',
bgneal@1:         261: 'Pattern+AF',
bgneal@1:         515: 'Spot+Highlight control',
bgneal@1:         1027: 'Spot+Shadow control'}),
bgneal@1:     0x0300: ('MacroMode',
bgneal@1:        {0: 'Off',
bgneal@1:         1: 'On'}),
bgneal@1:     0x0301: ('FocusMode',
bgneal@1:        {0: 'Single AF',
bgneal@1:         1: 'Sequential shooting AF',
bgneal@1:         2: 'Continuous AF',
bgneal@1:         3: 'Multi AF',
bgneal@1:         10: 'MF'}),
bgneal@1:     0x0302: ('FocusProcess',
bgneal@1:        {0: 'AF Not Used',
bgneal@1:         1: 'AF Used'}),
bgneal@1:     0x0303: ('AFSearch',
bgneal@1:        {0: 'Not Ready',
bgneal@1:         1: 'Ready'}),
bgneal@1:     0x0304: ('AFAreas', ),
bgneal@1:     0x0401: ('FlashExposureCompensation', ),
bgneal@1:     0x0500: ('WhiteBalance2',
bgneal@1:        {0: 'Auto',
bgneal@1:         16: '7500K (Fine Weather with Shade)',
bgneal@1:         17: '6000K (Cloudy)',
bgneal@1:         18: '5300K (Fine Weather)',
bgneal@1:         20: '3000K (Tungsten light)',
bgneal@1:         21: '3600K (Tungsten light-like)',
bgneal@1:         33: '6600K (Daylight fluorescent)',
bgneal@1:         34: '4500K (Neutral white fluorescent)',
bgneal@1:         35: '4000K (Cool white fluorescent)',
bgneal@1:         48: '3600K (Tungsten light-like)',
bgneal@1:         256: 'Custom WB 1',
bgneal@1:         257: 'Custom WB 2',
bgneal@1:         258: 'Custom WB 3',
bgneal@1:         259: 'Custom WB 4',
bgneal@1:         512: 'Custom WB 5400K',
bgneal@1:         513: 'Custom WB 2900K',
bgneal@1:         514: 'Custom WB 8000K', }),
bgneal@1:     0x0501: ('WhiteBalanceTemperature', ),
bgneal@1:     0x0502: ('WhiteBalanceBracket', ),
bgneal@1:     0x0503: ('CustomSaturation', ), # (3 numbers: 1. CS Value, 2. Min, 3. Max)
bgneal@1:     0x0504: ('ModifiedSaturation',
bgneal@1:        {0: 'Off',
bgneal@1:         1: 'CM1 (Red Enhance)',
bgneal@1:         2: 'CM2 (Green Enhance)',
bgneal@1:         3: 'CM3 (Blue Enhance)',
bgneal@1:         4: 'CM4 (Skin Tones)'}),
bgneal@1:     0x0505: ('ContrastSetting', ), # (3 numbers: 1. Contrast, 2. Min, 3. Max)
bgneal@1:     0x0506: ('SharpnessSetting', ), # (3 numbers: 1. Sharpness, 2. Min, 3. Max)
bgneal@1:     0x0507: ('ColorSpace',
bgneal@1:        {0: 'sRGB',
bgneal@1:         1: 'Adobe RGB',
bgneal@1:         2: 'Pro Photo RGB'}),
bgneal@1:     0x0509: ('SceneMode',
bgneal@1:        {0: 'Standard',
bgneal@1:         6: 'Auto',
bgneal@1:         7: 'Sport',
bgneal@1:         8: 'Portrait',
bgneal@1:         9: 'Landscape+Portrait',
bgneal@1:         10: 'Landscape',
bgneal@1:         11: 'Night scene',
bgneal@1:         13: 'Panorama',
bgneal@1:         16: 'Landscape+Portrait',
bgneal@1:         17: 'Night+Portrait',
bgneal@1:         19: 'Fireworks',
bgneal@1:         20: 'Sunset',
bgneal@1:         22: 'Macro',
bgneal@1:         25: 'Documents',
bgneal@1:         26: 'Museum',
bgneal@1:         28: 'Beach&Snow',
bgneal@1:         30: 'Candle',
bgneal@1:         35: 'Underwater Wide1',
bgneal@1:         36: 'Underwater Macro',
bgneal@1:         39: 'High Key',
bgneal@1:         40: 'Digital Image Stabilization',
bgneal@1:         44: 'Underwater Wide2',
bgneal@1:         45: 'Low Key',
bgneal@1:         46: 'Children',
bgneal@1:         48: 'Nature Macro'}),
bgneal@1:     0x050a: ('NoiseReduction',
bgneal@1:        {0: 'Off',
bgneal@1:         1: 'Noise Reduction',
bgneal@1:         2: 'Noise Filter',
bgneal@1:         3: 'Noise Reduction + Noise Filter',
bgneal@1:         4: 'Noise Filter (ISO Boost)',
bgneal@1:         5: 'Noise Reduction + Noise Filter (ISO Boost)'}),
bgneal@1:     0x050b: ('DistortionCorrection',
bgneal@1:        {0: 'Off',
bgneal@1:         1: 'On'}),
bgneal@1:     0x050c: ('ShadingCompensation',
bgneal@1:        {0: 'Off',
bgneal@1:         1: 'On'}),
bgneal@1:     0x050d: ('CompressionFactor', ),
bgneal@1:     0x050f: ('Gradation',
bgneal@1:        {'-1 -1 1': 'Low Key',
bgneal@1:         '0 -1 1': 'Normal',
bgneal@1:         '1 -1 1': 'High Key'}),
bgneal@1:     0x0520: ('PictureMode',
bgneal@1:        {1: 'Vivid',
bgneal@1:         2: 'Natural',
bgneal@1:         3: 'Muted',
bgneal@1:         256: 'Monotone',
bgneal@1:         512: 'Sepia'}),
bgneal@1:     0x0521: ('PictureModeSaturation', ),
bgneal@1:     0x0522: ('PictureModeHue?', ),
bgneal@1:     0x0523: ('PictureModeContrast', ),
bgneal@1:     0x0524: ('PictureModeSharpness', ),
bgneal@1:     0x0525: ('PictureModeBWFilter',
bgneal@1:        {0: 'n/a',
bgneal@1:         1: 'Neutral',
bgneal@1:         2: 'Yellow',
bgneal@1:         3: 'Orange',
bgneal@1:         4: 'Red',
bgneal@1:         5: 'Green'}),
bgneal@1:     0x0526: ('PictureModeTone',
bgneal@1:        {0: 'n/a',
bgneal@1:         1: 'Neutral',
bgneal@1:         2: 'Sepia',
bgneal@1:         3: 'Blue',
bgneal@1:         4: 'Purple',
bgneal@1:         5: 'Green'}),
bgneal@1:     0x0600: ('Sequence', ), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
bgneal@1:     0x0601: ('PanoramaMode', ), # (2 numbers: 1. Mode, 2. Shot number)
bgneal@1:     0x0603: ('ImageQuality2',
bgneal@1:        {1: 'SQ',
bgneal@1:         2: 'HQ',
bgneal@1:         3: 'SHQ',
bgneal@1:         4: 'RAW'}),
bgneal@1:     0x0901: ('ManometerReading', ),
bgneal@1:     }
bgneal@1: 
bgneal@1: 
bgneal@1: MAKERNOTE_CASIO_TAGS={
bgneal@1:     0x0001: ('RecordingMode',
bgneal@1:              {1: 'Single Shutter',
bgneal@1:               2: 'Panorama',
bgneal@1:               3: 'Night Scene',
bgneal@1:               4: 'Portrait',
bgneal@1:               5: 'Landscape'}),
bgneal@1:     0x0002: ('Quality',
bgneal@1:              {1: 'Economy',
bgneal@1:               2: 'Normal',
bgneal@1:               3: 'Fine'}),
bgneal@1:     0x0003: ('FocusingMode',
bgneal@1:              {2: 'Macro',
bgneal@1:               3: 'Auto Focus',
bgneal@1:               4: 'Manual Focus',
bgneal@1:               5: 'Infinity'}),
bgneal@1:     0x0004: ('FlashMode',
bgneal@1:              {1: 'Auto',
bgneal@1:               2: 'On',
bgneal@1:               3: 'Off',
bgneal@1:               4: 'Red Eye Reduction'}),
bgneal@1:     0x0005: ('FlashIntensity',
bgneal@1:              {11: 'Weak',
bgneal@1:               13: 'Normal',
bgneal@1:               15: 'Strong'}),
bgneal@1:     0x0006: ('Object Distance', ),
bgneal@1:     0x0007: ('WhiteBalance',
bgneal@1:              {1: 'Auto',
bgneal@1:               2: 'Tungsten',
bgneal@1:               3: 'Daylight',
bgneal@1:               4: 'Fluorescent',
bgneal@1:               5: 'Shade',
bgneal@1:               129: 'Manual'}),
bgneal@1:     0x000B: ('Sharpness',
bgneal@1:              {0: 'Normal',
bgneal@1:               1: 'Soft',
bgneal@1:               2: 'Hard'}),
bgneal@1:     0x000C: ('Contrast',
bgneal@1:              {0: 'Normal',
bgneal@1:               1: 'Low',
bgneal@1:               2: 'High'}),
bgneal@1:     0x000D: ('Saturation',
bgneal@1:              {0: 'Normal',
bgneal@1:               1: 'Low',
bgneal@1:               2: 'High'}),
bgneal@1:     0x0014: ('CCDSpeed',
bgneal@1:              {64: 'Normal',
bgneal@1:               80: 'Normal',
bgneal@1:               100: 'High',
bgneal@1:               125: '+1.0',
bgneal@1:               244: '+3.0',
bgneal@1:               250: '+2.0'}),
bgneal@1:     }
bgneal@1: 
bgneal@1: MAKERNOTE_FUJIFILM_TAGS={
bgneal@1:     0x0000: ('NoteVersion', make_string),
bgneal@1:     0x1000: ('Quality', ),
bgneal@1:     0x1001: ('Sharpness',
bgneal@1:              {1: 'Soft',
bgneal@1:               2: 'Soft',
bgneal@1:               3: 'Normal',
bgneal@1:               4: 'Hard',
bgneal@1:               5: 'Hard'}),
bgneal@1:     0x1002: ('WhiteBalance',
bgneal@1:              {0: 'Auto',
bgneal@1:               256: 'Daylight',
bgneal@1:               512: 'Cloudy',
bgneal@1:               768: 'DaylightColor-Fluorescent',
bgneal@1:               769: 'DaywhiteColor-Fluorescent',
bgneal@1:               770: 'White-Fluorescent',
bgneal@1:               1024: 'Incandescent',
bgneal@1:               3840: 'Custom'}),
bgneal@1:     0x1003: ('Color',
bgneal@1:              {0: 'Normal',
bgneal@1:               256: 'High',
bgneal@1:               512: 'Low'}),
bgneal@1:     0x1004: ('Tone',
bgneal@1:              {0: 'Normal',
bgneal@1:               256: 'High',
bgneal@1:               512: 'Low'}),
bgneal@1:     0x1010: ('FlashMode',
bgneal@1:              {0: 'Auto',
bgneal@1:               1: 'On',
bgneal@1:               2: 'Off',
bgneal@1:               3: 'Red Eye Reduction'}),
bgneal@1:     0x1011: ('FlashStrength', ),
bgneal@1:     0x1020: ('Macro',
bgneal@1:              {0: 'Off',
bgneal@1:               1: 'On'}),
bgneal@1:     0x1021: ('FocusMode',
bgneal@1:              {0: 'Auto',
bgneal@1:               1: 'Manual'}),
bgneal@1:     0x1030: ('SlowSync',
bgneal@1:              {0: 'Off',
bgneal@1:               1: 'On'}),
bgneal@1:     0x1031: ('PictureMode',
bgneal@1:              {0: 'Auto',
bgneal@1:               1: 'Portrait',
bgneal@1:               2: 'Landscape',
bgneal@1:               4: 'Sports',
bgneal@1:               5: 'Night',
bgneal@1:               6: 'Program AE',
bgneal@1:               256: 'Aperture Priority AE',
bgneal@1:               512: 'Shutter Priority AE',
bgneal@1:               768: 'Manual Exposure'}),
bgneal@1:     0x1100: ('MotorOrBracket',
bgneal@1:              {0: 'Off',
bgneal@1:               1: 'On'}),
bgneal@1:     0x1300: ('BlurWarning',
bgneal@1:              {0: 'Off',
bgneal@1:               1: 'On'}),
bgneal@1:     0x1301: ('FocusWarning',
bgneal@1:              {0: 'Off',
bgneal@1:               1: 'On'}),
bgneal@1:     0x1302: ('AEWarning',
bgneal@1:              {0: 'Off',
bgneal@1:               1: 'On'}),
bgneal@1:     }
bgneal@1: 
bgneal@1: MAKERNOTE_CANON_TAGS = {
bgneal@1:     0x0006: ('ImageType', ),
bgneal@1:     0x0007: ('FirmwareVersion', ),
bgneal@1:     0x0008: ('ImageNumber', ),
bgneal@1:     0x0009: ('OwnerName', ),
bgneal@1:     }
bgneal@1: 
bgneal@1: # this is in element offset, name, optional value dictionary format
bgneal@1: MAKERNOTE_CANON_TAG_0x001 = {
bgneal@1:     1: ('Macromode',
bgneal@1:         {1: 'Macro',
bgneal@1:          2: 'Normal'}),
bgneal@1:     2: ('SelfTimer', ),
bgneal@1:     3: ('Quality',
bgneal@1:         {2: 'Normal',
bgneal@1:          3: 'Fine',
bgneal@1:          5: 'Superfine'}),
bgneal@1:     4: ('FlashMode',
bgneal@1:         {0: 'Flash Not Fired',
bgneal@1:          1: 'Auto',
bgneal@1:          2: 'On',
bgneal@1:          3: 'Red-Eye Reduction',
bgneal@1:          4: 'Slow Synchro',
bgneal@1:          5: 'Auto + Red-Eye Reduction',
bgneal@1:          6: 'On + Red-Eye Reduction',
bgneal@1:          16: 'external flash'}),
bgneal@1:     5: ('ContinuousDriveMode',
bgneal@1:         {0: 'Single Or Timer',
bgneal@1:          1: 'Continuous'}),
bgneal@1:     7: ('FocusMode',
bgneal@1:         {0: 'One-Shot',
bgneal@1:          1: 'AI Servo',
bgneal@1:          2: 'AI Focus',
bgneal@1:          3: 'MF',
bgneal@1:          4: 'Single',
bgneal@1:          5: 'Continuous',
bgneal@1:          6: 'MF'}),
bgneal@1:     10: ('ImageSize',
bgneal@1:          {0: 'Large',
bgneal@1:           1: 'Medium',
bgneal@1:           2: 'Small'}),
bgneal@1:     11: ('EasyShootingMode',
bgneal@1:          {0: 'Full Auto',
bgneal@1:           1: 'Manual',
bgneal@1:           2: 'Landscape',
bgneal@1:           3: 'Fast Shutter',
bgneal@1:           4: 'Slow Shutter',
bgneal@1:           5: 'Night',
bgneal@1:           6: 'B&W',
bgneal@1:           7: 'Sepia',
bgneal@1:           8: 'Portrait',
bgneal@1:           9: 'Sports',
bgneal@1:           10: 'Macro/Close-Up',
bgneal@1:           11: 'Pan Focus'}),
bgneal@1:     12: ('DigitalZoom',
bgneal@1:          {0: 'None',
bgneal@1:           1: '2x',
bgneal@1:           2: '4x'}),
bgneal@1:     13: ('Contrast',
bgneal@1:          {0xFFFF: 'Low',
bgneal@1:           0: 'Normal',
bgneal@1:           1: 'High'}),
bgneal@1:     14: ('Saturation',
bgneal@1:          {0xFFFF: 'Low',
bgneal@1:           0: 'Normal',
bgneal@1:           1: 'High'}),
bgneal@1:     15: ('Sharpness',
bgneal@1:          {0xFFFF: 'Low',
bgneal@1:           0: 'Normal',
bgneal@1:           1: 'High'}),
bgneal@1:     16: ('ISO',
bgneal@1:          {0: 'See ISOSpeedRatings Tag',
bgneal@1:           15: 'Auto',
bgneal@1:           16: '50',
bgneal@1:           17: '100',
bgneal@1:           18: '200',
bgneal@1:           19: '400'}),
bgneal@1:     17: ('MeteringMode',
bgneal@1:          {3: 'Evaluative',
bgneal@1:           4: 'Partial',
bgneal@1:           5: 'Center-weighted'}),
bgneal@1:     18: ('FocusType',
bgneal@1:          {0: 'Manual',
bgneal@1:           1: 'Auto',
bgneal@1:           3: 'Close-Up (Macro)',
bgneal@1:           8: 'Locked (Pan Mode)'}),
bgneal@1:     19: ('AFPointSelected',
bgneal@1:          {0x3000: 'None (MF)',
bgneal@1:           0x3001: 'Auto-Selected',
bgneal@1:           0x3002: 'Right',
bgneal@1:           0x3003: 'Center',
bgneal@1:           0x3004: 'Left'}),
bgneal@1:     20: ('ExposureMode',
bgneal@1:          {0: 'Easy Shooting',
bgneal@1:           1: 'Program',
bgneal@1:           2: 'Tv-priority',
bgneal@1:           3: 'Av-priority',
bgneal@1:           4: 'Manual',
bgneal@1:           5: 'A-DEP'}),
bgneal@1:     23: ('LongFocalLengthOfLensInFocalUnits', ),
bgneal@1:     24: ('ShortFocalLengthOfLensInFocalUnits', ),
bgneal@1:     25: ('FocalUnitsPerMM', ),
bgneal@1:     28: ('FlashActivity',
bgneal@1:          {0: 'Did Not Fire',
bgneal@1:           1: 'Fired'}),
bgneal@1:     29: ('FlashDetails',
bgneal@1:          {14: 'External E-TTL',
bgneal@1:           13: 'Internal Flash',
bgneal@1:           11: 'FP Sync Used',
bgneal@1:           7: '2nd("Rear")-Curtain Sync Used',
bgneal@1:           4: 'FP Sync Enabled'}),
bgneal@1:     32: ('FocusMode',
bgneal@1:          {0: 'Single',
bgneal@1:           1: 'Continuous'}),
bgneal@1:     }
bgneal@1: 
bgneal@1: MAKERNOTE_CANON_TAG_0x004 = {
bgneal@1:     7: ('WhiteBalance',
bgneal@1:         {0: 'Auto',
bgneal@1:          1: 'Sunny',
bgneal@1:          2: 'Cloudy',
bgneal@1:          3: 'Tungsten',
bgneal@1:          4: 'Fluorescent',
bgneal@1:          5: 'Flash',
bgneal@1:          6: 'Custom'}),
bgneal@1:     9: ('SequenceNumber', ),
bgneal@1:     14: ('AFPointUsed', ),
bgneal@1:     15: ('FlashBias',
bgneal@1:         {0XFFC0: '-2 EV',
bgneal@1:          0XFFCC: '-1.67 EV',
bgneal@1:          0XFFD0: '-1.50 EV',
bgneal@1:          0XFFD4: '-1.33 EV',
bgneal@1:          0XFFE0: '-1 EV',
bgneal@1:          0XFFEC: '-0.67 EV',
bgneal@1:          0XFFF0: '-0.50 EV',
bgneal@1:          0XFFF4: '-0.33 EV',
bgneal@1:          0X0000: '0 EV',
bgneal@1:          0X000C: '0.33 EV',
bgneal@1:          0X0010: '0.50 EV',
bgneal@1:          0X0014: '0.67 EV',
bgneal@1:          0X0020: '1 EV',
bgneal@1:          0X002C: '1.33 EV',
bgneal@1:          0X0030: '1.50 EV',
bgneal@1:          0X0034: '1.67 EV',
bgneal@1:          0X0040: '2 EV'}),
bgneal@1:     19: ('SubjectDistance', ),
bgneal@1:     }
bgneal@1: 
bgneal@1: # extract multibyte integer in Motorola format (little endian)
bgneal@1: def s2n_motorola(str):
bgneal@1:     x = 0
bgneal@1:     for c in str:
bgneal@1:         x = (x << 8) | ord(c)
bgneal@1:     return x
bgneal@1: 
bgneal@1: # extract multibyte integer in Intel format (big endian)
bgneal@1: def s2n_intel(str):
bgneal@1:     x = 0
bgneal@1:     y = 0L
bgneal@1:     for c in str:
bgneal@1:         x = x | (ord(c) << y)
bgneal@1:         y = y + 8
bgneal@1:     return x
bgneal@1: 
bgneal@1: # ratio object that eventually will be able to reduce itself to lowest
bgneal@1: # common denominator for printing
bgneal@1: def gcd(a, b):
bgneal@1:     if b == 0:
bgneal@1:         return a
bgneal@1:     else:
bgneal@1:         return gcd(b, a % b)
bgneal@1: 
bgneal@1: class Ratio:
bgneal@1:     def __init__(self, num, den):
bgneal@1:         self.num = num
bgneal@1:         self.den = den
bgneal@1: 
bgneal@1:     def __repr__(self):
bgneal@1:         self.reduce()
bgneal@1:         if self.den == 1:
bgneal@1:             return str(self.num)
bgneal@1:         return '%d/%d' % (self.num, self.den)
bgneal@1: 
bgneal@1:     def reduce(self):
bgneal@1:         div = gcd(self.num, self.den)
bgneal@1:         if div > 1:
bgneal@1:             self.num = self.num / div
bgneal@1:             self.den = self.den / div
bgneal@1: 
bgneal@1: # for ease of dealing with tags
bgneal@1: class IFD_Tag:
bgneal@1:     def __init__(self, printable, tag, field_type, values, field_offset,
bgneal@1:                  field_length):
bgneal@1:         # printable version of data
bgneal@1:         self.printable = printable
bgneal@1:         # tag ID number
bgneal@1:         self.tag = tag
bgneal@1:         # field type as index into FIELD_TYPES
bgneal@1:         self.field_type = field_type
bgneal@1:         # offset of start of field in bytes from beginning of IFD
bgneal@1:         self.field_offset = field_offset
bgneal@1:         # length of data field in bytes
bgneal@1:         self.field_length = field_length
bgneal@1:         # either a string or array of data items
bgneal@1:         self.values = values
bgneal@1: 
bgneal@1:     def __str__(self):
bgneal@1:         return self.printable
bgneal@1: 
bgneal@1:     def __repr__(self):
bgneal@1:         return '(0x%04X) %s=%s @ %d' % (self.tag,
bgneal@1:                                         FIELD_TYPES[self.field_type][2],
bgneal@1:                                         self.printable,
bgneal@1:                                         self.field_offset)
bgneal@1: 
bgneal@1: # class that handles an EXIF header
bgneal@1: class EXIF_header:
bgneal@1:     def __init__(self, file, endian, offset, fake_exif, debug=0):
bgneal@1:         self.file = file
bgneal@1:         self.endian = endian
bgneal@1:         self.offset = offset
bgneal@1:         self.fake_exif = fake_exif
bgneal@1:         self.debug = debug
bgneal@1:         self.tags = {}
bgneal@1: 
bgneal@1:     # convert slice to integer, based on sign and endian flags
bgneal@1:     # usually this offset is assumed to be relative to the beginning of the
bgneal@1:     # start of the EXIF information.  For some cameras that use relative tags,
bgneal@1:     # this offset may be relative to some other starting point.
bgneal@1:     def s2n(self, offset, length, signed=0):
bgneal@1:         self.file.seek(self.offset+offset)
bgneal@1:         slice=self.file.read(length)
bgneal@1:         if self.endian == 'I':
bgneal@1:             val=s2n_intel(slice)
bgneal@1:         else:
bgneal@1:             val=s2n_motorola(slice)
bgneal@1:         # Sign extension ?
bgneal@1:         if signed:
bgneal@1:             msb=1L << (8*length-1)
bgneal@1:             if val & msb:
bgneal@1:                 val=val-(msb << 1)
bgneal@1:         return val
bgneal@1: 
bgneal@1:     # convert offset to string
bgneal@1:     def n2s(self, offset, length):
bgneal@1:         s = ''
bgneal@1:         for dummy in range(length):
bgneal@1:             if self.endian == 'I':
bgneal@1:                 s = s + chr(offset & 0xFF)
bgneal@1:             else:
bgneal@1:                 s = chr(offset & 0xFF) + s
bgneal@1:             offset = offset >> 8
bgneal@1:         return s
bgneal@1: 
bgneal@1:     # return first IFD
bgneal@1:     def first_IFD(self):
bgneal@1:         return self.s2n(4, 4)
bgneal@1: 
bgneal@1:     # return pointer to next IFD
bgneal@1:     def next_IFD(self, ifd):
bgneal@1:         entries=self.s2n(ifd, 2)
bgneal@1:         return self.s2n(ifd+2+12*entries, 4)
bgneal@1: 
bgneal@1:     # return list of IFDs in header
bgneal@1:     def list_IFDs(self):
bgneal@1:         i=self.first_IFD()
bgneal@1:         a=[]
bgneal@1:         while i:
bgneal@1:             a.append(i)
bgneal@1:             i=self.next_IFD(i)
bgneal@1:         return a
bgneal@1: 
bgneal@1:     # return list of entries in this IFD
bgneal@1:     def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0, stop_tag='UNDEF'):
bgneal@1:         entries=self.s2n(ifd, 2)
bgneal@1:         for i in range(entries):
bgneal@1:             # entry is index of start of this IFD in the file
bgneal@1:             entry = ifd + 2 + 12 * i
bgneal@1:             tag = self.s2n(entry, 2)
bgneal@1: 
bgneal@1:             # get tag name early to avoid errors, help debug
bgneal@1:             tag_entry = dict.get(tag)
bgneal@1:             if tag_entry:
bgneal@1:                 tag_name = tag_entry[0]
bgneal@1:             else:
bgneal@1:                 tag_name = 'Tag 0x%04X' % tag
bgneal@1: 
bgneal@1:             # ignore certain tags for faster processing
bgneal@1:             if not (not detailed and tag in IGNORE_TAGS):
bgneal@1:                 field_type = self.s2n(entry + 2, 2)
bgneal@1:                 if not 0 < field_type < len(FIELD_TYPES):
bgneal@1:                     # unknown field type
bgneal@1:                     raise ValueError('unknown type %d in tag 0x%04X' % (field_type, tag))
bgneal@1:                 typelen = FIELD_TYPES[field_type][0]
bgneal@1:                 count = self.s2n(entry + 4, 4)
bgneal@1:                 offset = entry + 8
bgneal@1:                 if count * typelen > 4:
bgneal@1:                     # offset is not the value; it's a pointer to the value
bgneal@1:                     # if relative we set things up so s2n will seek to the right
bgneal@1:                     # place when it adds self.offset.  Note that this 'relative'
bgneal@1:                     # is for the Nikon type 3 makernote.  Other cameras may use
bgneal@1:                     # other relative offsets, which would have to be computed here
bgneal@1:                     # slightly differently.
bgneal@1:                     if relative:
bgneal@1:                         tmp_offset = self.s2n(offset, 4)
bgneal@1:                         offset = tmp_offset + ifd - self.offset + 4
bgneal@1:                         if self.fake_exif:
bgneal@1:                             offset = offset + 18
bgneal@1:                     else:
bgneal@1:                         offset = self.s2n(offset, 4)
bgneal@1:                 field_offset = offset
bgneal@1:                 if field_type == 2:
bgneal@1:                     # special case: null-terminated ASCII string
bgneal@1:                     if count != 0:
bgneal@1:                         self.file.seek(self.offset + offset)
bgneal@1:                         values = self.file.read(count)
bgneal@1:                         values = values.strip().replace('\x00', '')
bgneal@1:                     else:
bgneal@1:                         values = ''
bgneal@1:                 else:
bgneal@1:                     values = []
bgneal@1:                     signed = (field_type in [6, 8, 9, 10])
bgneal@1:                     for dummy in range(count):
bgneal@1:                         if field_type in (5, 10):
bgneal@1:                             # a ratio
bgneal@1:                             value = Ratio(self.s2n(offset, 4, signed),
bgneal@1:                                           self.s2n(offset + 4, 4, signed))
bgneal@1:                         else:
bgneal@1:                             value = self.s2n(offset, typelen, signed)
bgneal@1:                         values.append(value)
bgneal@1:                         offset = offset + typelen
bgneal@1:                 # now "values" is either a string or an array
bgneal@1:                 if count == 1 and field_type != 2:
bgneal@1:                     printable=str(values[0])
bgneal@1:                 else:
bgneal@1:                     printable=str(values)
bgneal@1:                 # compute printable version of values
bgneal@1:                 if tag_entry:
bgneal@1:                     if len(tag_entry) != 1:
bgneal@1:                         # optional 2nd tag element is present
bgneal@1:                         if callable(tag_entry[1]):
bgneal@1:                             # call mapping function
bgneal@1:                             printable = tag_entry[1](values)
bgneal@1:                         else:
bgneal@1:                             printable = ''
bgneal@1:                             for i in values:
bgneal@1:                                 # use lookup table for this tag
bgneal@1:                                 printable += tag_entry[1].get(i, repr(i))
bgneal@1: 
bgneal@1:                 self.tags[ifd_name + ' ' + tag_name] = IFD_Tag(printable, tag,
bgneal@1:                                                           field_type,
bgneal@1:                                                           values, field_offset,
bgneal@1:                                                           count * typelen)
bgneal@1:                 if self.debug:
bgneal@1:                     print ' debug:   %s: %s' % (tag_name,
bgneal@1:                                                 repr(self.tags[ifd_name + ' ' + tag_name]))
bgneal@1: 
bgneal@1:             if tag_name == stop_tag:
bgneal@1:                 break
bgneal@1: 
bgneal@1:     # extract uncompressed TIFF thumbnail (like pulling teeth)
bgneal@1:     # we take advantage of the pre-existing layout in the thumbnail IFD as
bgneal@1:     # much as possible
bgneal@1:     def extract_TIFF_thumbnail(self, thumb_ifd):
bgneal@1:         entries = self.s2n(thumb_ifd, 2)
bgneal@1:         # this is header plus offset to IFD ...
bgneal@1:         if self.endian == 'M':
bgneal@1:             tiff = 'MM\x00*\x00\x00\x00\x08'
bgneal@1:         else:
bgneal@1:             tiff = 'II*\x00\x08\x00\x00\x00'
bgneal@1:         # ... plus thumbnail IFD data plus a null "next IFD" pointer
bgneal@1:         self.file.seek(self.offset+thumb_ifd)
bgneal@1:         tiff += self.file.read(entries*12+2)+'\x00\x00\x00\x00'
bgneal@1: 
bgneal@1:         # fix up large value offset pointers into data area
bgneal@1:         for i in range(entries):
bgneal@1:             entry = thumb_ifd + 2 + 12 * i
bgneal@1:             tag = self.s2n(entry, 2)
bgneal@1:             field_type = self.s2n(entry+2, 2)
bgneal@1:             typelen = FIELD_TYPES[field_type][0]
bgneal@1:             count = self.s2n(entry+4, 4)
bgneal@1:             oldoff = self.s2n(entry+8, 4)
bgneal@1:             # start of the 4-byte pointer area in entry
bgneal@1:             ptr = i * 12 + 18
bgneal@1:             # remember strip offsets location
bgneal@1:             if tag == 0x0111:
bgneal@1:                 strip_off = ptr
bgneal@1:                 strip_len = count * typelen
bgneal@1:             # is it in the data area?
bgneal@1:             if count * typelen > 4:
bgneal@1:                 # update offset pointer (nasty "strings are immutable" crap)
bgneal@1:                 # should be able to say "tiff[ptr:ptr+4]=newoff"
bgneal@1:                 newoff = len(tiff)
bgneal@1:                 tiff = tiff[:ptr] + self.n2s(newoff, 4) + tiff[ptr+4:]
bgneal@1:                 # remember strip offsets location
bgneal@1:                 if tag == 0x0111:
bgneal@1:                     strip_off = newoff
bgneal@1:                     strip_len = 4
bgneal@1:                 # get original data and store it
bgneal@1:                 self.file.seek(self.offset + oldoff)
bgneal@1:                 tiff += self.file.read(count * typelen)
bgneal@1: 
bgneal@1:         # add pixel strips and update strip offset info
bgneal@1:         old_offsets = self.tags['Thumbnail StripOffsets'].values
bgneal@1:         old_counts = self.tags['Thumbnail StripByteCounts'].values
bgneal@1:         for i in range(len(old_offsets)):
bgneal@1:             # update offset pointer (more nasty "strings are immutable" crap)
bgneal@1:             offset = self.n2s(len(tiff), strip_len)
bgneal@1:             tiff = tiff[:strip_off] + offset + tiff[strip_off + strip_len:]
bgneal@1:             strip_off += strip_len
bgneal@1:             # add pixel strip to end
bgneal@1:             self.file.seek(self.offset + old_offsets[i])
bgneal@1:             tiff += self.file.read(old_counts[i])
bgneal@1: 
bgneal@1:         self.tags['TIFFThumbnail'] = tiff
bgneal@1: 
bgneal@1:     # decode all the camera-specific MakerNote formats
bgneal@1: 
bgneal@1:     # Note is the data that comprises this MakerNote.  The MakerNote will
bgneal@1:     # likely have pointers in it that point to other parts of the file.  We'll
bgneal@1:     # use self.offset as the starting point for most of those pointers, since
bgneal@1:     # they are relative to the beginning of the file.
bgneal@1:     #
bgneal@1:     # If the MakerNote is in a newer format, it may use relative addressing
bgneal@1:     # within the MakerNote.  In that case we'll use relative addresses for the
bgneal@1:     # pointers.
bgneal@1:     #
bgneal@1:     # As an aside: it's not just to be annoying that the manufacturers use
bgneal@1:     # relative offsets.  It's so that if the makernote has to be moved by the
bgneal@1:     # picture software all of the offsets don't have to be adjusted.  Overall,
bgneal@1:     # this is probably the right strategy for makernotes, though the spec is
bgneal@1:     # ambiguous.  (The spec does not appear to imagine that makernotes would
bgneal@1:     # follow EXIF format internally.  Once they did, it's ambiguous whether
bgneal@1:     # the offsets should be from the header at the start of all the EXIF info,
bgneal@1:     # or from the header at the start of the makernote.)
bgneal@1:     def decode_maker_note(self):
bgneal@1:         note = self.tags['EXIF MakerNote']
bgneal@1:         make = self.tags['Image Make'].printable
bgneal@1:         # model = self.tags['Image Model'].printable # unused
bgneal@1: 
bgneal@1:         # Nikon
bgneal@1:         # The maker note usually starts with the word Nikon, followed by the
bgneal@1:         # type of the makernote (1 or 2, as a short).  If the word Nikon is
bgneal@1:         # not at the start of the makernote, it's probably type 2, since some
bgneal@1:         # cameras work that way.
bgneal@1:         if make in ('NIKON', 'NIKON CORPORATION'):
bgneal@1:             if note.values[0:7] == [78, 105, 107, 111, 110, 0, 1]:
bgneal@1:                 if self.debug:
bgneal@1:                     print "Looks like a type 1 Nikon MakerNote."
bgneal@1:                 self.dump_IFD(note.field_offset+8, 'MakerNote',
bgneal@1:                               dict=MAKERNOTE_NIKON_OLDER_TAGS)
bgneal@1:             elif note.values[0:7] == [78, 105, 107, 111, 110, 0, 2]:
bgneal@1:                 if self.debug:
bgneal@1:                     print "Looks like a labeled type 2 Nikon MakerNote"
bgneal@1:                 if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]:
bgneal@1:                     raise ValueError("Missing marker tag '42' in MakerNote.")
bgneal@1:                 # skip the Makernote label and the TIFF header
bgneal@1:                 self.dump_IFD(note.field_offset+10+8, 'MakerNote',
bgneal@1:                               dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1)
bgneal@1:             else:
bgneal@1:                 # E99x or D1
bgneal@1:                 if self.debug:
bgneal@1:                     print "Looks like an unlabeled type 2 Nikon MakerNote"
bgneal@1:                 self.dump_IFD(note.field_offset, 'MakerNote',
bgneal@1:                               dict=MAKERNOTE_NIKON_NEWER_TAGS)
bgneal@1:             return
bgneal@1: 
bgneal@1:         # Olympus
bgneal@1:         if make.startswith('OLYMPUS'):
bgneal@1:             self.dump_IFD(note.field_offset+8, 'MakerNote',
bgneal@1:                           dict=MAKERNOTE_OLYMPUS_TAGS)
bgneal@1:             # TODO
bgneal@1:             #for i in (('MakerNote Tag 0x2020', MAKERNOTE_OLYMPUS_TAG_0x2020),):
bgneal@1:             #    self.decode_olympus_tag(self.tags[i[0]].values, i[1])
bgneal@1:             #return
bgneal@1: 
bgneal@1:         # Casio
bgneal@1:         if make == 'Casio':
bgneal@1:             self.dump_IFD(note.field_offset, 'MakerNote',
bgneal@1:                           dict=MAKERNOTE_CASIO_TAGS)
bgneal@1:             return
bgneal@1: 
bgneal@1:         # Fujifilm
bgneal@1:         if make == 'FUJIFILM':
bgneal@1:             # bug: everything else is "Motorola" endian, but the MakerNote
bgneal@1:             # is "Intel" endian
bgneal@1:             endian = self.endian
bgneal@1:             self.endian = 'I'
bgneal@1:             # bug: IFD offsets are from beginning of MakerNote, not
bgneal@1:             # beginning of file header
bgneal@1:             offset = self.offset
bgneal@1:             self.offset += note.field_offset
bgneal@1:             # process note with bogus values (note is actually at offset 12)
bgneal@1:             self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
bgneal@1:             # reset to correct values
bgneal@1:             self.endian = endian
bgneal@1:             self.offset = offset
bgneal@1:             return
bgneal@1: 
bgneal@1:         # Canon
bgneal@1:         if make == 'Canon':
bgneal@1:             self.dump_IFD(note.field_offset, 'MakerNote',
bgneal@1:                           dict=MAKERNOTE_CANON_TAGS)
bgneal@1:             for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
bgneal@1:                       ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
bgneal@1:                 self.canon_decode_tag(self.tags[i[0]].values, i[1])
bgneal@1:             return
bgneal@1: 
bgneal@1:     # decode Olympus MakerNote tag based on offset within tag
bgneal@1:     def olympus_decode_tag(self, value, dict):
bgneal@1:         pass
bgneal@1: 
bgneal@1:     # decode Canon MakerNote tag based on offset within tag
bgneal@1:     # see http://www.burren.cx/david/canon.html by David Burren
bgneal@1:     def canon_decode_tag(self, value, dict):
bgneal@1:         for i in range(1, len(value)):
bgneal@1:             x=dict.get(i, ('Unknown', ))
bgneal@1:             if self.debug:
bgneal@1:                 print i, x
bgneal@1:             name=x[0]
bgneal@1:             if len(x) > 1:
bgneal@1:                 val=x[1].get(value[i], 'Unknown')
bgneal@1:             else:
bgneal@1:                 val=value[i]
bgneal@1:             # it's not a real IFD Tag but we fake one to make everybody
bgneal@1:             # happy. this will have a "proprietary" type
bgneal@1:             self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None,
bgneal@1:                                                  None, None)
bgneal@1: 
bgneal@1: # process an image file (expects an open file object)
bgneal@1: # this is the function that has to deal with all the arbitrary nasty bits
bgneal@1: # of the EXIF standard
bgneal@1: def process_file(f, stop_tag='UNDEF', details=True, debug=False):
bgneal@1:     # yah it's cheesy...
bgneal@1:     global detailed
bgneal@1:     detailed = details
bgneal@1: 
bgneal@1:     # by default do not fake an EXIF beginning
bgneal@1:     fake_exif = 0
bgneal@1: 
bgneal@1:     # determine whether it's a JPEG or TIFF
bgneal@1:     data = f.read(12)
bgneal@1:     if data[0:4] in ['II*\x00', 'MM\x00*']:
bgneal@1:         # it's a TIFF file
bgneal@1:         f.seek(0)
bgneal@1:         endian = f.read(1)
bgneal@1:         f.read(1)
bgneal@1:         offset = 0
bgneal@1:     elif data[0:2] == '\xFF\xD8':
bgneal@1:         # it's a JPEG file
bgneal@1:         while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM', 'Phot'):
bgneal@1:             length = ord(data[4])*256+ord(data[5])
bgneal@1:             f.read(length-8)
bgneal@1:             # fake an EXIF beginning of file
bgneal@1:             data = '\xFF\x00'+f.read(10)
bgneal@1:             fake_exif = 1
bgneal@1:         if data[2] == '\xFF' and data[6:10] == 'Exif':
bgneal@1:             # detected EXIF header
bgneal@1:             offset = f.tell()
bgneal@1:             endian = f.read(1)
bgneal@1:         else:
bgneal@1:             # no EXIF information
bgneal@1:             return {}
bgneal@1:     else:
bgneal@1:         # file format not recognized
bgneal@1:         return {}
bgneal@1: 
bgneal@1:     # deal with the EXIF info we found
bgneal@1:     if debug:
bgneal@1:         print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
bgneal@1:     hdr = EXIF_header(f, endian, offset, fake_exif, debug)
bgneal@1:     ifd_list = hdr.list_IFDs()
bgneal@1:     ctr = 0
bgneal@1:     for i in ifd_list:
bgneal@1:         if ctr == 0:
bgneal@1:             IFD_name = 'Image'
bgneal@1:         elif ctr == 1:
bgneal@1:             IFD_name = 'Thumbnail'
bgneal@1:             thumb_ifd = i
bgneal@1:         else:
bgneal@1:             IFD_name = 'IFD %d' % ctr
bgneal@1:         if debug:
bgneal@1:             print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
bgneal@1:         hdr.dump_IFD(i, IFD_name, stop_tag=stop_tag)
bgneal@1:         # EXIF IFD
bgneal@1:         exif_off = hdr.tags.get(IFD_name+' ExifOffset')
bgneal@1:         if exif_off:
bgneal@1:             if debug:
bgneal@1:                 print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
bgneal@1:             hdr.dump_IFD(exif_off.values[0], 'EXIF', stop_tag=stop_tag)
bgneal@1:             # Interoperability IFD contained in EXIF IFD
bgneal@1:             intr_off = hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
bgneal@1:             if intr_off:
bgneal@1:                 if debug:
bgneal@1:                     print ' EXIF Interoperability SubSubIFD at offset %d:' \
bgneal@1:                           % intr_off.values[0]
bgneal@1:                 hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
bgneal@1:                              dict=INTR_TAGS, stop_tag=stop_tag)
bgneal@1:         # GPS IFD
bgneal@1:         gps_off = hdr.tags.get(IFD_name+' GPSInfo')
bgneal@1:         if gps_off:
bgneal@1:             if debug:
bgneal@1:                 print ' GPS SubIFD at offset %d:' % gps_off.values[0]
bgneal@1:             hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS, stop_tag=stop_tag)
bgneal@1:         ctr += 1
bgneal@1: 
bgneal@1:     # extract uncompressed TIFF thumbnail
bgneal@1:     thumb = hdr.tags.get('Thumbnail Compression')
bgneal@1:     if thumb and thumb.printable == 'Uncompressed TIFF':
bgneal@1:         hdr.extract_TIFF_thumbnail(thumb_ifd)
bgneal@1: 
bgneal@1:     # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
bgneal@1:     thumb_off = hdr.tags.get('Thumbnail JPEGInterchangeFormat')
bgneal@1:     if thumb_off:
bgneal@1:         f.seek(offset+thumb_off.values[0])
bgneal@1:         size = hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
bgneal@1:         hdr.tags['JPEGThumbnail'] = f.read(size)
bgneal@1: 
bgneal@1:     # deal with MakerNote contained in EXIF IFD
bgneal@1:     if 'EXIF MakerNote' in hdr.tags and detailed:
bgneal@1:         hdr.decode_maker_note()
bgneal@1: 
bgneal@1:     # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
bgneal@1:     # since it's not allowed in a uncompressed TIFF IFD
bgneal@1:     if 'JPEGThumbnail' not in hdr.tags:
bgneal@1:         thumb_off=hdr.tags.get('MakerNote JPEGThumbnail')
bgneal@1:         if thumb_off:
bgneal@1:             f.seek(offset+thumb_off.values[0])
bgneal@1:             hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
bgneal@1: 
bgneal@1:     return hdr.tags
bgneal@1: 
bgneal@1: 
bgneal@1: # show command line usage
bgneal@1: def usage(exit_status):
bgneal@1:     msg = 'Usage: EXIF.py [OPTIONS] file1 [file2 ...]\n'
bgneal@1:     msg += 'Extract EXIF information from digital camera image files.\n\nOptions:\n'
bgneal@1:     msg += '-q --quick   Do not process MakerNotes.\n'
bgneal@1:     msg += '-t TAG --stop-tag TAG   Stop processing when this tag is retrieved.\n'
bgneal@1:     msg += '-d --debug   Run in debug mode.\n'
bgneal@1:     print msg
bgneal@1:     sys.exit(exit_status)
bgneal@1: 
bgneal@1: # library test/debug function (dump given files)
bgneal@1: if __name__ == '__main__':
bgneal@1:     import sys
bgneal@1:     import getopt
bgneal@1: 
bgneal@1:     # parse command line options/arguments
bgneal@1:     try:
bgneal@1:         opts, args = getopt.getopt(sys.argv[1:], "hqdt:v", ["help", "quick", "debug", "stop-tag="])
bgneal@1:     except getopt.GetoptError:
bgneal@1:         usage(2)
bgneal@1:     if args == []:
bgneal@1:         usage(2)
bgneal@1:     detailed = True
bgneal@1:     stop_tag = 'UNDEF'
bgneal@1:     debug = False
bgneal@1:     for o, a in opts:
bgneal@1:         if o in ("-h", "--help"):
bgneal@1:             usage(0)
bgneal@1:         if o in ("-q", "--quick"):
bgneal@1:             detailed = False
bgneal@1:         if o in ("-t", "--stop-tag"):
bgneal@1:             stop_tag = a
bgneal@1:         if o in ("-d", "--debug"):
bgneal@1:             debug = True
bgneal@1: 
bgneal@1:     # output info for each file
bgneal@1:     for filename in args:
bgneal@1:         try:
bgneal@1:             file=open(filename, 'rb')
bgneal@1:         except:
bgneal@1:             print "'%s' is unreadable\n"%filename
bgneal@1:             continue
bgneal@1:         print filename + ':'
bgneal@1:         # get the tags
bgneal@1:         data = process_file(file, stop_tag=stop_tag, details=detailed, debug=debug)
bgneal@1:         if not data:
bgneal@1:             print 'No EXIF information found'
bgneal@1:             continue
bgneal@1: 
bgneal@1:         x=data.keys()
bgneal@1:         x.sort()
bgneal@1:         for i in x:
bgneal@1:             if i in ('JPEGThumbnail', 'TIFFThumbnail'):
bgneal@1:                 continue
bgneal@1:             try:
bgneal@1:                 print '   %s (%s): %s' % \
bgneal@1:                       (i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
bgneal@1:             except:
bgneal@1:                 print 'error', i, '"', data[i], '"'
bgneal@1:         if 'JPEGThumbnail' in data:
bgneal@1:             print 'File has JPEG thumbnail'
bgneal@1:         print
bgneal@1: