You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
9.8 KiB
Python
324 lines
9.8 KiB
Python
# igc2kmz/exif.py igc2kmz EXIF functions
|
|
# 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 datetime
|
|
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):
|
|
sl = slice(offset + 2 + 12 * i, offset + 2 + 12 * i + 8)
|
|
tag, data_type, count = struct.unpack(self.byte_order_char + 'HHL',
|
|
self.data[sl])
|
|
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:
|
|
sl = slice(offset + 2 + 12 * i + 8, offset + 2 + 12 * i + 12)
|
|
data_offset, = struct.unpack(self.byte_order_char + 'L',
|
|
self.data[sl])
|
|
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 (tag, 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])
|
|
sl = slice(offset + 2 + 12 * n, offset + 2 + 12 * n + 2)
|
|
offset, = struct.unpack(self.byte_order_char + 'H', self.data[sl])
|
|
|
|
|
|
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 = {
|
|
0x0001: 'InteroperabilityIndex',
|
|
}
|
|
|
|
IFD_POINTER_TAGS = {
|
|
0x8769: EXIF_IFD_TAGS,
|
|
0x8825: GPS_INFO_IFD_TAGS,
|
|
0xa005: INTEROPERABILITY_INFO_IFD_TAGS,
|
|
}
|
|
|
|
|
|
def exif(data):
|
|
tiff = TIFF(data)
|
|
result = {}
|
|
for ifd_offset in tiff.ifd_offsets():
|
|
for tag, value in tiff.ifd_tags(ifd_offset):
|
|
if tag in IFD_POINTER_TAGS:
|
|
ifd_tags = IFD_POINTER_TAGS[tag]
|
|
for tag, value in tiff.ifd_tags(value):
|
|
result[ifd_tags.get(tag, tag)] = value
|
|
else:
|
|
result[TAGS.get(tag, tag)] = value
|
|
return result
|
|
|
|
|
|
def parse_angle(value):
|
|
return sum(n / d for n, d in zip([float(n) / d for n, d in value],
|
|
(1, 60, 3600)))
|
|
|
|
|
|
def parse_datetime(value):
|
|
return datetime.datetime.strptime(value.rstrip('\0'), '%Y:%m:%d %H:%M:%S')
|
|
|
|
|
|
CHARSET = {
|
|
'ASCII\0\0\0': 'ascii',
|
|
'JIS\0\0\0\0\0': 'shift_jis',
|
|
'UNICODE\0': 'utf_8',
|
|
'\0\0\0\0\0\0\0\0': 'latin_1',
|
|
}
|
|
|
|
|
|
def parse_usercomment(value):
|
|
value = ''.join(map(chr, value))
|
|
if value[0:8] in CHARSET:
|
|
return value[8:].rstrip('\0').decode(CHARSET[value[0:8]])
|
|
else:
|
|
return value
|
|
|
|
|
|
SOI = 0xffd8
|
|
APP1 = 0xffe1
|
|
SOF = 0xffc0
|
|
|
|
|
|
class JPEG(object):
|
|
|
|
def __init__(self, file):
|
|
self.exif = {}
|
|
self.height = self.width = None
|
|
for tag, data in JPEG.chunks(file):
|
|
if tag == APP1 and data[0:6] == 'Exif\0\0':
|
|
self.exif = exif(data[6:])
|
|
elif tag == SOF:
|
|
self.height, self.width = struct.unpack('>HH', data[1:5])
|
|
|
|
@classmethod
|
|
def chunks(self, file):
|
|
if struct.unpack('>H', file.read(2)) != (SOI,):
|
|
raise SyntaxError, "Missing SOI header"
|
|
tag, = struct.unpack('>H', file.read(2))
|
|
while tag > 0xff00:
|
|
size, = struct.unpack('>H', file.read(2))
|
|
yield (tag, file.read(size - 2))
|
|
tag, = struct.unpack('>H', file.read(2))
|