Add EXIF module.
This commit is contained in:
parent
445aabf2a5
commit
ec85273535
|
@ -0,0 +1,269 @@
|
|||
# igc2kmz/__init__.py igc2kmz EXIF module
|
||||
# Copyright (C) 2008 Tom Payne
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
BIG_ENDIAN, = struct.unpack('=H', 'MM')
|
||||
LITTLE_ENDIAN, = struct.unpack('=H', 'II')
|
||||
BYTE_ORDER_CHAR = {BIG_ENDIAN: '>', LITTLE_ENDIAN: '<'}
|
||||
|
||||
BYTE = 1
|
||||
ASCII = 2
|
||||
SHORT = 3
|
||||
LONG = 4
|
||||
RATIONAL = 5
|
||||
UNDEFINED = 7
|
||||
SLONG = 9
|
||||
SRATIONAL = 10
|
||||
DATA_TYPE_LENGTH = {BYTE: 1, ASCII: 1, SHORT: 2, LONG: 4, RATIONAL: 8, UNDEFINED: 1, SLONG: 4, SRATIONAL: 8}
|
||||
DATA_TYPE_FORMAT = {BYTE: 'B', ASCII: 'c', SHORT: 'H', LONG: 'L', RATIONAL: 'L', UNDEFINED: 'B', SLONG: 'l', SRATIONAL: 'l'}
|
||||
|
||||
|
||||
class SyntaxError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class Tiff(object):
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
self.byte_order, = struct.unpack('=H', self.data[0:2])
|
||||
if not self.byte_order in BYTE_ORDER_CHAR:
|
||||
raise SyntaxError, 'Unsupported byte order %s' % repr(self.data[6:8])
|
||||
self.byte_order_char = BYTE_ORDER_CHAR[self.byte_order]
|
||||
self.version, self.first_ifd_offset = struct.unpack(self.byte_order_char + 'HL', self.data[2:8])
|
||||
if self.version != 42:
|
||||
raise SyntaxError, 'Unsupported version %s' % self.version
|
||||
|
||||
def ifd_tags(self, offset):
|
||||
n, = struct.unpack(self.byte_order_char + 'H', self.data[offset:offset + 2])
|
||||
for i in xrange(0, n):
|
||||
id, data_type, count = struct.unpack(self.byte_order_char + 'HHL', self.data[offset + 2 + 12 * i:offset + 2 + 12 * i + 8])
|
||||
if not data_type in DATA_TYPE_LENGTH:
|
||||
raise SyntaxError, 'Unrecognised data type %d' % data_type
|
||||
data_length = DATA_TYPE_LENGTH[data_type] * count
|
||||
if data_length > 4:
|
||||
data_offset, = struct.unpack(self.byte_order_char + 'L', self.data[offset + 2 + 12 * i + 8:offset + 2 + 12 * i + 12])
|
||||
data_slice = slice(data_offset, data_offset + data_length)
|
||||
else:
|
||||
data_slice = slice(offset + 2 + 12 * i + 8, offset + 2 + 12 * i + 8 + data_length)
|
||||
if data_type == ASCII:
|
||||
data = self.data[data_slice]
|
||||
else:
|
||||
if data_type == RATIONAL or data_type == SRATIONAL:
|
||||
l = struct.unpack('%s%d%s' % (self.byte_order_char, 2 * count, DATA_TYPE_FORMAT[data_type]), self.data[data_slice])
|
||||
data = zip(l[0::2], l[1::2])
|
||||
else:
|
||||
data = struct.unpack('%s%d%s' % (self.byte_order_char, count, DATA_TYPE_FORMAT[data_type]), self.data[data_slice])
|
||||
if count == 1:
|
||||
data, = data
|
||||
yield (id, data)
|
||||
|
||||
def ifd_offsets(self):
|
||||
offset = self.first_ifd_offset
|
||||
while offset:
|
||||
yield offset
|
||||
n, = struct.unpack(self.byte_order_char + 'H', self.data[offset:offset + 2])
|
||||
offset, = struct.unpack(self.byte_order_char + 'H', self.data[offset + 2 + 12 * n:offset + 2 + 12 * n + 2])
|
||||
|
||||
|
||||
TAGS = {
|
||||
0x8769: 'ExifIFDPointer',
|
||||
0x8825: 'GPSInfoIFDPointer',
|
||||
0xa005: 'InteroperabilityIFDPointer',
|
||||
0x0100: 'ImageWidth',
|
||||
0x0101: 'ImageHeight',
|
||||
0x0102: 'BitsPerSample',
|
||||
0x0103: 'Compression',
|
||||
0x0106: 'PhotometricInterpretation',
|
||||
0x0112: 'Orientation',
|
||||
0x0115: 'SamplesPerPixel',
|
||||
0x011c: 'PlanarConfiguration',
|
||||
0x0212: 'YCbCrSubSampling',
|
||||
0x0213: 'YCbCrPositioning',
|
||||
0x011a: 'XResolution',
|
||||
0x011b: 'YResolution',
|
||||
0x0128: 'ResolutionUnit',
|
||||
0x0111: 'StripOffsets',
|
||||
0x0116: 'RowsPerStrip',
|
||||
0x0117: 'StripByteCounts',
|
||||
0x0201: 'JPEGInterchangeFormat',
|
||||
0x0202: 'JPEGInterchangeFormatLength',
|
||||
0x012d: 'TransferFunction',
|
||||
0x013e: 'WhitePoint',
|
||||
0x013f: 'PrimaryChromaticities',
|
||||
0x0211: 'YCbCrCoefficients',
|
||||
0x0214: 'ReferenceBlackWhite',
|
||||
0x0132: 'DateTime',
|
||||
0x010e: 'ImageDescription',
|
||||
0x010f: 'Make',
|
||||
0x0110: 'Model',
|
||||
0x0131: 'Software',
|
||||
0x0138: 'Artist',
|
||||
0x8298: 'Copyright',
|
||||
}
|
||||
|
||||
EXIF_IFD_TAGS = {
|
||||
0x9000: 'ExifVersion',
|
||||
0xa000: 'FlashpixVersion',
|
||||
0xa001: 'ColorSpace',
|
||||
0xa002: 'PixelXDimension',
|
||||
0xa003: 'PixelYDimension',
|
||||
0x9101: 'ComponentsConfiguration',
|
||||
0x9102: 'CompressedBitsPerPixel',
|
||||
0x927c: 'MakerNote',
|
||||
0x9286: 'UserComment',
|
||||
0xa004: 'RelatedSoundFile',
|
||||
0x9003: 'DateTimeOriginal',
|
||||
0x9004: 'DateTimeDigitized',
|
||||
0x9290: 'SubsecTime',
|
||||
0x9291: 'SubsecTimeOriginal',
|
||||
0x9292: 'SubsecTimeDigitized',
|
||||
0x928a: 'ExposureTime',
|
||||
0x829d: 'FNumber',
|
||||
0x8822: 'ExposureProgram',
|
||||
0x8824: 'SpectralSensitivity',
|
||||
0x8827: 'ISOSpeedRatings',
|
||||
0x8828: 'OECF',
|
||||
0x9201: 'ShutterSpeedValue',
|
||||
0x9202: 'ApertureValue',
|
||||
0x9203: 'BrightnessValue',
|
||||
0x9204: 'ExposureBiasValue',
|
||||
0x9205: 'MaxApertureValue',
|
||||
0x9206: 'SubjectDistance',
|
||||
0x9207: 'MeteringMode',
|
||||
0x9208: 'LightSource',
|
||||
0x9209: 'Flash',
|
||||
0x9214: 'SubjectArea',
|
||||
0x920a: 'FocalLength',
|
||||
0xa20b: 'FlashEnergy',
|
||||
0xa20c: 'SpatialFrequencyResponse',
|
||||
0xa20e: 'FocalPlaneXResolution',
|
||||
0xa20f: 'FocalPlaneYResolution',
|
||||
0xa210: 'FocalPlaneResolutionUnit',
|
||||
0xa214: 'SubjectLocation',
|
||||
0xa215: 'ExposureIndex',
|
||||
0xa217: 'SensingMethod',
|
||||
0xa300: 'FileSource',
|
||||
0xa301: 'SceneType',
|
||||
0xa302: 'CFAPattern',
|
||||
0xa401: 'CustomRendered',
|
||||
0xa402: 'ExposureMode',
|
||||
0xa403: 'WhiteBalance',
|
||||
0xa404: 'DigitalZoomRatio',
|
||||
0xa405: 'FocalLengthIn35mmFilm',
|
||||
0xa406: 'SceneCaptureType',
|
||||
0xa407: 'GainControl',
|
||||
0xa408: 'Contrast',
|
||||
0xa409: 'Saturation',
|
||||
0xa40a: 'Sharpness',
|
||||
0xa40b: 'DeviceSettingDescription',
|
||||
0xa40c: 'SubjectDistanceRange',
|
||||
0xa420: 'ImageUniqueID',
|
||||
}
|
||||
|
||||
GPS_INFO_IFD_TAGS = {
|
||||
0x0000: 'GPSVersionID',
|
||||
0x0001: 'GPSLatitudeRef',
|
||||
0x0002: 'GPSLatitude',
|
||||
0x0003: 'GPSLongitudeRef',
|
||||
0x0004: 'GPSLongitude',
|
||||
0x0005: 'GPSAltitudeRef',
|
||||
0x0006: 'GPSAltitude',
|
||||
0x0007: 'GPSTimeStamp',
|
||||
0x0008: 'GPSSatellites',
|
||||
0x0009: 'GPSStatus',
|
||||
0x000a: 'GPSMeasureMode',
|
||||
0x000b: 'GPSDOP',
|
||||
0x000c: 'GPSSpeedRef',
|
||||
0x000d: 'GPSSpeed',
|
||||
0x000e: 'GPSTrackRef',
|
||||
0x000f: 'GPSTrack',
|
||||
0x0010: 'GPSImgDirectionRef',
|
||||
0x0011: 'GPSImgDirection',
|
||||
0x0012: 'GPSMapDatum',
|
||||
0x0013: 'GPSDestLatitudeRef',
|
||||
0x0014: 'GPSDestLatitude',
|
||||
0x0015: 'GPSDestLongitudeRef',
|
||||
0x0016: 'GPSDestLongitude',
|
||||
0x0017: 'GPSDestBearingRef',
|
||||
0x0018: 'GPSDestBearing',
|
||||
0x0019: 'GPSDestDistanceRef',
|
||||
0x001a: 'GPSDestDistance',
|
||||
0x001b: 'GPSProcessingMethod',
|
||||
0x001c: 'GPSAreaInformation',
|
||||
0x001d: 'GPSDateStamp',
|
||||
0x001e: 'GPSDifferential',
|
||||
}
|
||||
|
||||
INTEROPERABILITY_INFO_IFD_TAGS = {
|
||||
# TODO
|
||||
}
|
||||
|
||||
IFD_POINTER_IDS = {
|
||||
0x8769: EXIF_IFD_TAGS,
|
||||
0x8825: GPS_INFO_IFD_TAGS,
|
||||
0xa005: INTEROPERABILITY_INFO_IFD_TAGS,
|
||||
}
|
||||
|
||||
|
||||
class Exif(object):
|
||||
|
||||
def __init__(self, data):
|
||||
if data[0:4] != 'Exif':
|
||||
raise SyntaxError, 'Unrecognised EXIF header %s' % repr(data[0:6])
|
||||
self.tiff = Tiff(data[6:])
|
||||
|
||||
def tags(self):
|
||||
result = {}
|
||||
for ifd_offset in self.tiff.ifd_offsets():
|
||||
for id, value in self.tiff.ifd_tags(ifd_offset):
|
||||
if id in IFD_POINTER_IDS:
|
||||
ifd_id = id
|
||||
for id, value in self.tiff.ifd_tags(value):
|
||||
result[IFD_POINTER_IDS[ifd_id].get(id, id)] = value
|
||||
else:
|
||||
result[TAGS.get(id, id)] = value
|
||||
return result
|
||||
|
||||
|
||||
SOI = 0xffd8
|
||||
EOI = 0xffd9
|
||||
APP1 = 0xffe1
|
||||
|
||||
|
||||
class Jpeg(object):
|
||||
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
|
||||
def chunks(self):
|
||||
# FIXME The SOS (0xffda) chunk seems not to follow the spec...
|
||||
if struct.unpack('>H', self.file.read(2)) != (SOI,):
|
||||
raise SyntaxError
|
||||
id, = struct.unpack('>H', self.file.read(2))
|
||||
while id != EOI:
|
||||
size, = struct.unpack('>H', self.file.read(2))
|
||||
yield (id, self.file.read(size - 2))
|
||||
id, = struct.unpack('>H', self.file.read(2))
|
||||
|
||||
def exif(self):
|
||||
for id, data in self.chunks():
|
||||
if id == APP1:
|
||||
return Exif(data)
|
||||
return None
|
Loading…
Reference in New Issue