## =========================================================================== ## NAME: exif ## TYPE: python script ## CONTENT: library for parsing EXIF headers ## =========================================================================== ## AUTHORS: rft Robert F. Tobler ## =========================================================================== ## HISTORY: ## ## 10-Aug-01 11:14:20 rft last modification ## 09-Aug-01 16:51:05 rft created ## =========================================================================== import string ASCII = 0 BINARY = 1 ## --------------------------------------------------------------------------- ## 'Tiff' ## This class provides the Exif header as a file-like object and hides ## endian-specific data access. ## --------------------------------------------------------------------------- class Tiff: def __init__(self, data, file = None): self.data = data self.file = file self.endpos = len(data) self.pos = 0 if self.data[0:2] == "MM": self.S0 = 1 ; self.S1 = 0 self.L0 = 3 ; self.L1 = 2 ; self.L2 = 1 ; self.L3 = 0 else: self.S0 = 0 ; self.S1 = 1 self.L0 = 0 ; self.L1 = 1 ; self.L2 = 2 ; self.L3 = 3 def seek(self, pos): self.pos = pos if self.pos > self.endpos: self.data += self.file.read( self.endpos - self.pos ) def tell(self): return self.pos def read(self, len): old_pos = self.pos self.pos = self.pos + len if self.pos > self.endpos: self.data += self.file.read( self.endpos - self.pos ) return self.data[old_pos:self.pos] def byte(self, signed = 0): pos = self.pos self.pos = pos + 1 if self.pos > self.endpos: self.data += self.file.read( self.endpos - self.pos ) hi = ord(self.data[pos]) if hi > 127 and signed: hi = hi - 256 return hi def short(self, signed = 0): pos = self.pos self.pos = pos + 2 if self.pos > self.endpos: self.data += self.file.read( self.endpos - self.pos ) hi = ord(self.data[pos+self.S1]) if hi > 127 and signed: hi = hi - 256 return (hi<<8)|ord(self.data[pos+self.S0]) def long(self, signed = 0): pos = self.pos self.pos = pos + 4 if self.pos > self.endpos: self.data += self.file.read( self.endpos - self.pos ) hi = ord(self.data[pos+self.L3]) if hi > 127 and not signed: hi = long(hi) return (hi<<24) | (ord(self.data[pos+self.L2])<<16) \ | (ord(self.data[pos+self.L1])<<8) | ord(self.data[pos+self.L0]) ## --------------------------------------------------------------------------- ## 'Type', 'Type...' ## A small hierarchy of objects that knows how to read each type of tag ## field from a tiff file, and how to pretty-print each type of tag. ## ## The method 'read' is used to read a tag with a given count from the ## supplied tiff file. ## ## The method 'str_table' is used to pretty-print the value table of a ## tag if no special format for this tag is present. ## --------------------------------------------------------------------------- class Type: def str_table(self, table): result = [] for val in table: result.append(self.str_value(val)) return string.join(result, ", ") def str_value(self, val): return str(val) class TypeByte(Type): def __init__(self): self.name = "BYTE" ; self.len = 1 def read(self, tiff, count): result = [] for i in range(0, count): table.append(tiff.byte()) return table class TypeAscii: def __init__(self): self.name = "ASCII" ; self.len = 1 def read(self, tiff, count): return tiff.read(count-1) def str_table(self, table): return string.strip(table) class TypeShort(Type): def __init__(self): self.name = "SHORT" ; self.len = 2 def read(self, tiff, count): table = [] for i in range(0, count): table.append(tiff.short()) return table class TypeLong(Type): def __init__(self): self.name = "LONG" ; self.len = 4 def read(self, tiff, count): table = [] for i in range(0, count): table.append(tiff.long()) return table class TypeRatio(Type): def __init__(self): self.name = "RATIO" ; self.len = 8 def read(self, tiff, count): table = [] for i in range(0, count): table.append((tiff.long(), tiff.long())) return table def str_value(self, val): return "%d/%d" %(val[0], val[1]) class TypeSByte(Type): def __init__(self): self.name = "SBYTE" ; self.len = 1 def read(self, tiff, count): table = [] for i in range(0, count): table.append(tiff.byte(signed=1)) return table class TypeUndef(TypeByte): def __init__(self): self.name = "UNDEF" ; self.len = 1 def read(self, tiff, count): return tiff.read(count) def str_table(self, table): result = map( lambda x: str(ord(x)), table ) # this next line is somehow much more efficient than using str() return '[ ' + string.join( result, ',' ) + ' ]' class TypeSShort(Type): def __init__(self): self.name = "SSHORT" ; self.len = 2 def read(self, tiff, count): table = [] for i in range(0, count): table.append(tiff.short(signed=1)) return table class TypeSLong(Type): def __init__(self): self.name = "SLONG" ; self.len = 4 def read(self, tiff, count): table = [] for i in range(0, count): table.append(tiff.short(signed=1)) return table class TypeSRatio(TypeRatio): def __init__(self): self.name = "SRATIO" ; self.len = 8 def read(self, tiff, count): table = [] for i in range(0, count): table.append((tiff.long(signed=1), tiff.long(signed=1))) return table class TypeFloat: def __init__(self): self.name = "FLOAT" ; self.len = 4 def read(self, tiff, count): return tiff.read(4 * count) class TypeDouble: def __init__(self): self.name = "DOUBLE" ; self.len = 8 def read(self, tiff, count): return tiff.read(8 * count) TYPE_MAP = { 1: TypeByte(), 2: TypeAscii(), 3: TypeShort(), 4: TypeLong(), 5: TypeRatio(), 6: TypeSByte(), 7: TypeUndef(), 8: TypeSShort(), 9: TypeSLong(), 10: TypeSRatio(), 11: TypeFloat(), 12: TypeDouble(), } ## --------------------------------------------------------------------------- ## 'Tag' ## A tag knows about its name and an optional format. ## --------------------------------------------------------------------------- class Tag: def __init__(self, name, format = None): self.name = name self.format = format ## --------------------------------------------------------------------------- ## 'Format', 'Format...' ## A small hierarchy of objects that provide special formats for certain ## tags in the EXIF standard. ## ## The method 'str_table' is used to pretty-print the value table of a ## tag. It gets the table of tags that have already been parsed as a ## parameter in order to handle vendor specific extensions. ## --------------------------------------------------------------------------- class Format: def str_table(self, table, value_map): result = [] for val in table: result.append(self.str_value(val)) return string.join(result, ", ") class FormatMap: def __init__(self, map, make_ext = {}): self.map = map self.make_ext = make_ext def str_table(self, table, value_map): if len(table) == 1: key = table[0] else: key = table value = self.map.get(key) if not value: make = value_map.get("Make") if make: value = self.make_ext.get(make,{}).get(key) if not value: value = `key` return value class FormatRatioAsFloat(Format): def str_value(self, val): if val[1] == 0: return "0.0" return "%g" % (val[0]/float(val[1])) class FormatRatioAsBias(Format): def str_value(self, val): if val[1] == 0: return "0.0" if val[0] > 0: return "+%3.1f" % (val[0]/float(val[1])) if val[0] < 0: return "-%3.1f" % (-val[0]/float(val[1])) return "0.0" def format_time(t): if t > 0.5: return "%g" % t if t > 0.1: return "1/%g" % (0.1*int(10/t+0.5)) return "1/%d" % int(1/t+0.5) class FormatRatioAsTime(Format): def str_value(self, val): if val[1] == 0: return "0.0" return format_time(val[0]/float(val[1])) class FormatRatioAsApexTime(Format): def str_value(self, val): if val[1] == 0: return "0.0" return format_time(pow(0.5, val[0]/float(val[1]))) ## --------------------------------------------------------------------------- ## The EXIF parser is completely table driven. ## --------------------------------------------------------------------------- ## --------------------------------------------------------------------------- ## Nikon 99x MakerNote Tags http://members.tripod.com/~tawba/990exif.htm ## --------------------------------------------------------------------------- NIKON_99x_MAKERNOTE_TAG_MAP = { 0x0001: Tag('MN_0x0001'), 0x0002: Tag('MN_ISOSetting'), 0x0003: Tag('MN_ColorMode'), 0x0004: Tag('MN_Quality'), 0x0005: Tag('MN_Whitebalance'), 0x0006: Tag('MN_ImageSharpening'), 0x0007: Tag('MN_FocusMode'), 0x0008: Tag('MN_FlashSetting'), 0x000A: Tag('MN_0x000A'), 0x000F: Tag('MN_ISOSelection'), 0x0080: Tag('MN_ImageAdjustment'), 0x0082: Tag('MN_AuxiliaryLens'), 0x0085: Tag('MN_ManualFocusDistance', FormatRatioAsFloat() ), 0x0086: Tag('MN_DigitalZoomFactor', FormatRatioAsFloat() ), 0x0088: Tag('MN_AFFocusPosition', FormatMap({ '\00\00\00\00': 'Center', '\00\01\00\00': 'Top', '\00\02\00\00': 'Bottom', '\00\03\00\00': 'Left', '\00\04\00\00': 'Right', })), 0x008f: Tag('MN_0x008f'), 0x0094: Tag('MN_Saturation', FormatMap({ 0: '0', 1: '1', 2: '2', -3: 'B&W', -2: '-2', -1: '-1', })), 0x0095: Tag('MN_NoiseReduction'), 0x0010: Tag('MN_DataDump'), 0x0011: Tag('MN_0x0011'), 0x0e00: Tag('MN_0x0e00'), } ## --------------------------------------------------------------------------- ## 'MakerNote...' ## This currently only parses Nikon E990, and Nikon E995 MakerNote tags. ## Additional objects with a 'parse' function can be placed here to add ## support for other cameras. This function adds the pretty-printed ## information in the MakerNote to the 'value_map' that is supplied. ## --------------------------------------------------------------------------- class MakerNoteTags: def __init__(self, tag_map): self.tag_map = tag_map def parse(self, tiff, mode, tag_len, value_map): num_entries = tiff.short() if verbose_opt: print num_entries, 'tags' for field in range(0, num_entries): parse_tag(tiff, mode, value_map, self.tag_map) NIKON_99x_MAKERNOTE = MakerNoteTags(NIKON_99x_MAKERNOTE_TAG_MAP) ## --------------------------------------------------------------------------- ## 'MAKERNOTE_MAP' ## Interpretation of the MakerNote tag indexed by 'Make', 'Model' pairs. ## --------------------------------------------------------------------------- MAKERNOTE_MAP = { ('NIKON', 'E990'): NIKON_99x_MAKERNOTE, ('NIKON', 'E995'): NIKON_99x_MAKERNOTE, } ## --------------------------------------------------------------------------- ## 'TAG_MAP' ## This is the map of tags that drives the parser. ## --------------------------------------------------------------------------- TAG_MAP = { 0x00fe: Tag('NewSubFileType'), 0x0100: Tag('ImageWidth'), 0x0101: Tag('ImageLength'), 0x0102: Tag('BitsPerSample'), 0x0103: Tag('Compression'), 0x0106: Tag('PhotometricInterpretation'), 0x010a: Tag('FillOrder'), 0x010d: Tag('DocumentName'), 0x010e: Tag('ImageDescription'), 0x010f: Tag('Make'), 0x0110: Tag('Model'), 0x0111: Tag('StripOffsets'), 0x0112: Tag('Orientation'), 0x0115: Tag('SamplesPerPixel'), 0x0116: Tag('RowsPerStrip'), 0x0117: Tag('StripByteCounts'), 0x011a: Tag('XResolution'), 0x011b: Tag('YResolution'), 0x011c: Tag('PlanarConfiguration'), 0x0128: Tag('ResolutionUnit', FormatMap({ 1: 'Not Absoulute', 2: 'Inch', 3: 'Centimeter' })), 0x012d: Tag('TransferFunction'), 0x0131: Tag('Software'), 0x0132: Tag('DateTime'), 0x013b: Tag('Artist'), 0x013e: Tag('WhitePoint'), 0x013f: Tag('PrimaryChromaticities'), 0x0142: Tag('TileWidth'), 0x0143: Tag('TileLength'), 0x0144: Tag('TileOffsets'), 0x0145: Tag('TileByteCounts'), 0x014a: Tag('SubIFDs'), 0x0156: Tag('TransferRange'), 0x015b: Tag('JPEGTables'), 0x0201: Tag('JPEGInterchangeFormat'), 0x0202: Tag('JPEGInterchangeFormatLength'), 0x0211: Tag('YCbCrCoefficients'), 0x0212: Tag('YCbCrSubSampling'), 0x0213: Tag('YCbCrPositioning'), 0x0214: Tag('ReferenceBlackWhite'), 0x828d: Tag('CFARepeatPatternDim'), 0x828e: Tag('CFAPattern'), 0x828f: Tag('BatteryLevel'), 0x8298: Tag('Copyright'), 0x829a: Tag('ExposureTime', FormatRatioAsTime() ), 0x829d: Tag('FNumber', FormatRatioAsFloat() ), 0x83bb: Tag('IPTC_NAA'), 0x8773: Tag('InterColorProfile'), 0x8822: Tag('ExposureProgram', FormatMap({ 0: 'Unidentified', 1: 'Manual', 2: 'Program Normal', 3: 'Aperture Priority', 4: 'Shutter Priority', 5: 'Program Creative', 6: 'Program Action', 7: 'Portrait Mode', 8: 'Landscape Mode', })), 0x8824: Tag('SpectralSensitivity'), 0x8825: Tag('GPSInfo'), 0x8827: Tag('ISOSpeedRatings'), 0x8828: Tag('OECF'), 0x8829: Tag('Interlace'), 0x882a: Tag('TimeZoneOffset'), 0x882b: Tag('SelfTimerMode'), 0x8769: Tag('ExifOffset'), 0x9000: Tag('ExifVersion'), 0x9003: Tag('DateTimeOriginal'), 0x9004: Tag('DateTimeDigitized'), 0x9101: Tag('ComponentsConfiguration'), 0x9102: Tag('CompressedBitsPerPixel'), 0x9201: Tag('ShutterSpeedValue', FormatRatioAsApexTime() ), 0x9202: Tag('ApertureValue', FormatRatioAsFloat() ), 0x9203: Tag('BrightnessValue'), 0x9204: Tag('ExposureBiasValue', FormatRatioAsBias() ), 0x9205: Tag('MaxApertureValue', FormatRatioAsFloat() ), 0x9206: Tag('SubjectDistance'), 0x9207: Tag('MeteringMode', FormatMap({ 0: 'Unidentified', 1: 'Average', 2: 'CenterWeightedAverage', 3: 'Spot', 4: 'MultiSpot', }, make_ext = { 'NIKON': { 5: 'Matrix' }, })), 0x9208: Tag('LightSource', FormatMap({ 0: 'Unknown', 1: 'Daylight', 2: 'Fluorescent', 3: 'Tungsten', 10: 'Flash', 17: 'Standard light A', 18: 'Standard light B', 19: 'Standard light C', 20: 'D55', 21: 'D65', 22: 'D75', 255: 'Other' })), 0x9209: Tag('Flash', FormatMap({ 0: 'no', 1: 'fired', 5: 'fired (?)', # no return sensed 7: 'fired (!)', # return sensed 9: 'fill fired', 13: 'fill fired (?)', 15: 'fill fired (!)', 16: 'off', 24: 'auto off', 25: 'auto fired', 29: 'auto fired (?)', 31: 'auto fired (!)', 32: 'not available' })), 0x920a: Tag('FocalLength', FormatRatioAsFloat()), 0x920b: Tag('FlashEnergy'), 0x920c: Tag('SpatialFrequencyResponse'), 0x920d: Tag('Noise'), 0x920e: Tag('FocalPlaneXResolution'), 0x920f: Tag('FocalPlaneYResolution'), 0x9210: Tag('FocalPlaneResolutionUnit', FormatMap({ 1: 'Inch', 2: 'Meter', 3: 'Centimeter', 4: 'Millimeter', 5: 'Micrometer', })), 0x9211: Tag('ImageNumber'), 0x9212: Tag('SecurityClassification'), 0x9213: Tag('ImageHistory'), 0x9214: Tag('SubjectLocation'), 0x9215: Tag('ExposureIndex'), 0x9216: Tag('TIFF_EPStandardID'), 0x9217: Tag('SensingMethod'), 0x927c: Tag('MakerNote'), 0xa001: Tag('ColorSpace'), 0xa002: Tag('ExifImageWidth'), 0xa003: Tag('ExifImageHeight'), 0xa005: Tag('Interoperability_IFD_Pointer'), } def parse_tag(tiff, mode, value_map, tag_map): tag_id = tiff.short() type_no = tiff.short() count = tiff.long() tag = tag_map.get(tag_id) if not tag: tag = Tag("Tag0x%x" % tag_id) type = TYPE_MAP[type_no] if verbose_opt: print "%30s:" % tag.name, if verbose_opt > 1: print "%6s %3d" % (type.name, count), pos = tiff.tell() tag_len = type.len * count if tag_len > 4: tag_offset = tiff.long() tiff.seek(tag_offset) if verbose_opt > 1: print "@%03x :" % tag_offset, else: if verbose_opt > 1: print " :", if tag.name == 'MakerNote': makernote = MAKERNOTE_MAP.get((value_map['Make'],value_map['Model'])) if makernote: makernote.parse(tiff, mode, tag_len, value_map) value_table = None else: value_table = type.read(tiff, count) else: value_table = type.read(tiff, count) if value_table: if mode == ASCII: if tag.format: val = tag.format.str_table(value_table, value_map) else: val = type.str_table(value_table) else: val = value_table value_map[tag.name] = val if verbose_opt: if value_map.has_key(tag.name): print val, print tiff.seek(pos+4) def parse_ifd(tiff, mode, offset, value_map): tiff.seek(offset) num_entries = tiff.short() if verbose_opt > 1: print "%30s: %3d @%03x" % ("IFD", num_entries, offset) for field in range(0, num_entries): parse_tag(tiff, mode, value_map, TAG_MAP) offset = tiff.long() return offset def parse_tiff(tiff, mode): value_map = {} order = tiff.read(2) if tiff.short() == 42: offset = tiff.long() while offset > 0: offset = parse_ifd(tiff, mode, offset, value_map) if offset == 0: # special handling to get if value_map.has_key('ExifOffset'): # next EXIF IFD offset = value_map['ExifOffset'] if mode == ASCII: offset = int(offset) else: offset = offset[0] del value_map['ExifOffset'] return value_map def parse_tiff_fortiff(tiff, mode): """Parse a real tiff file, not an EXIF tiff file.""" value_map = {} order = tiff.read(2) if tiff.short() == 42: offset = tiff.long() # build a list of small tags, we don't want to parse the huge stuff stags = [] while offset > 0: tiff.seek(offset) num_entries = tiff.short() if verbose_opt > 1: print "%30s: %3d @%03x" % ("IFD", num_entries, offset) for field in range(0, num_entries): pos = tiff.tell() tag_id = tiff.short() type_no = tiff.short() length = tiff.long() valoff = tiff.long() #print TAG_MAP[ tag_id ].name, length if tag_id == 0x8769: if mode == ASCII: valoff = int(valoff) else: valoff = valoff[0] stags += [ (tag_id, valoff) ] elif length < 1024: stags += [ (tag_id, pos) ] offset = tiff.long() # IMPORTANT: we read the 0st ifd only for this. # The second is reserved for the thumbnail, whatever is in there # we ignore. break for p in stags: (tag_id, pos) = p if tag_id == 0x8769: parse_ifd(tiff, mode, pos, value_map) else: tiff.seek( pos ) parse_tag(tiff, mode, value_map, TAG_MAP) return value_map ## --------------------------------------------------------------------------- ## 'parse' ## This is the function for parsing the EXIF structure in a file given ## the path of the file. ## The function returns a map which contains all the exif tags that ## were found, indexed by the name of the tag. The value of each tag ## is already converted to a nicely formatted string. ## --------------------------------------------------------------------------- def parse(path_name, verbose = 0, mode = 0): global verbose_opt verbose_opt = verbose try: file = open(path_name, "rb") data = file.read(12) if data[0:4] == '\377\330\377\341' and data[6:10] == 'Exif': # JPEG length = ord(data[4]) * 256 + ord(data[5]) if verbose > 1: print '%30s: %d' % ("EXIF header length",length) tiff = Tiff(file.read(length-8)) value_map = parse_tiff(tiff, mode) elif data[0:2] in [ 'II', 'MM' ] and ord(data[2]) == 42: # Tiff tiff = Tiff(data,file) tiff.seek(0) value_map = parse_tiff_fortiff(tiff, mode) else: # Some other file format, sorry. value_map = {} file.close() except IOError: value_map = {} return value_map ## ===========================================================================