Files
Ivolution/ivolution/util/exif.py
Julien Lengrand-Lambert 9e95642c68 Loads of changes in the repo structure.
Data are now hosted as a subfolder of the code, and all the source files are grouped into the same package.
Bin folder has been removed, scripts are now directly placed in the root location.

Main package has been renamed to ivolution, this is a first step towards github renaming and full renaming of the project
2012-07-31 18:28:33 +02:00

676 lines
20 KiB
Python
Executable File

## ===========================================================================
## 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
## ===========================================================================