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: