Added Exif classes for preliminary Exif image, photo, iop, gps support.
This commit is contained in:
parent
0f20fc18af
commit
fd2fd63b28
1 changed files with 227 additions and 68 deletions
295
glymur/jp2box.py
295
glymur/jp2box.py
|
|
@ -1619,12 +1619,238 @@ class UUIDBox(Jp2kBox):
|
||||||
text = buffer.decode('utf-8')
|
text = buffer.decode('utf-8')
|
||||||
kwargs['data'] = ET.fromstring(text)
|
kwargs['data'] = ET.fromstring(text)
|
||||||
elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2':
|
elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2':
|
||||||
kwargs['data'] = _parse_exif(buffer)
|
kwargs['data'] = Exif(buffer).exif_image
|
||||||
else:
|
else:
|
||||||
kwargs['data'] = buffer
|
kwargs['data'] = buffer
|
||||||
box = UUIDBox(**kwargs)
|
box = UUIDBox(**kwargs)
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
class Exif:
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
buffer : bytes
|
||||||
|
Raw byte stream consisting of the UUID data.
|
||||||
|
endian : str
|
||||||
|
Either '<' for big-endian, or '>' for little-endian.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, buffer):
|
||||||
|
"""Interpret raw buffer consisting of Exif IFD.
|
||||||
|
"""
|
||||||
|
self.exif_image = None
|
||||||
|
self.exif_photo = None
|
||||||
|
self.exif_gpsinfo = None
|
||||||
|
self.exif_iop = None
|
||||||
|
|
||||||
|
self.buffer = buffer
|
||||||
|
|
||||||
|
# Ignore the first six bytes.
|
||||||
|
# Next 8 should be (73, 73, 42, 8)
|
||||||
|
data = struct.unpack('<BBHI', buffer[6:14])
|
||||||
|
if data[0] == 73 and data[1] == 73:
|
||||||
|
# little endian
|
||||||
|
self.endian = '<'
|
||||||
|
else:
|
||||||
|
# big endian
|
||||||
|
self.endian = '>'
|
||||||
|
offset = data[3]
|
||||||
|
|
||||||
|
# This is the 'Exif Image' portion.
|
||||||
|
exif = ExifImageIfd(self.endian, buffer[6:], offset)
|
||||||
|
self.exif_image = exif.ifd
|
||||||
|
|
||||||
|
if 'ExifTag' in self.exif_image.keys():
|
||||||
|
offset = self.exif_image['ExifTag']
|
||||||
|
photo = ExifPhotoIfd(self.endian, buffer[6:], offset)
|
||||||
|
self.exif_photo = photo.ifd
|
||||||
|
|
||||||
|
if 'InteroperabilityTag' in self.exif_photo.keys():
|
||||||
|
offset = self.exif_photo['InteroperabilityTag']
|
||||||
|
interop = ExifInteroperabilityIfd(self.endian,
|
||||||
|
buffer[6:],
|
||||||
|
offset)
|
||||||
|
self.iop = interop.ifd
|
||||||
|
|
||||||
|
if 'GPSTag' in self.exif_image.keys():
|
||||||
|
offset = self.exif_image['GPSTag']
|
||||||
|
gps = ExifGPSInfoIfd(self.endian, buffer[6:], offset)
|
||||||
|
self.exif_gpsinfo = gps.ifd
|
||||||
|
|
||||||
|
class Ifd:
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
buffer : bytes
|
||||||
|
Raw byte stream consisting of the UUID data.
|
||||||
|
datatype2fmt : dictionary
|
||||||
|
Class attribute, maps the TIFF enumerated datatype to the python
|
||||||
|
datatype and data width.
|
||||||
|
endian : str
|
||||||
|
Either '<' for big-endian, or '>' for little-endian.
|
||||||
|
num_tags : int
|
||||||
|
Number of tags in the IFD.
|
||||||
|
raw_ifd : dictionary
|
||||||
|
Maps tag number to "mildly-interpreted" tag value.
|
||||||
|
"""
|
||||||
|
datatype2fmt = {1: ('B', 1),
|
||||||
|
2: ('B', 1),
|
||||||
|
3: ('H', 2),
|
||||||
|
4: ('I', 4),
|
||||||
|
5: ('II', 8),
|
||||||
|
7: ('B', 1),
|
||||||
|
9: ('i', 4),
|
||||||
|
10: ('ii', 8)}
|
||||||
|
|
||||||
|
def __init__(self, endian, buffer, offset):
|
||||||
|
self.endian = endian
|
||||||
|
self.buffer = buffer
|
||||||
|
|
||||||
|
self.num_tags, = struct.unpack(endian + 'H', buffer[offset:offset + 2])
|
||||||
|
|
||||||
|
fmt = self.endian + 'HHII' * self.num_tags
|
||||||
|
data = struct.unpack(fmt, buffer[offset + 2:offset + 2 + self.num_tags * 12])
|
||||||
|
self.raw_ifd = {}
|
||||||
|
for j, tag in enumerate(data[0::4]):
|
||||||
|
tag_entry = buffer[offset + 2 + j * 12 + 8:offset + 2 + j * 12 + 8 + 4]
|
||||||
|
payload = self.parse_tag(data[j * 4 + 1],
|
||||||
|
data[j * 4 + 2],
|
||||||
|
tag_entry)
|
||||||
|
self.raw_ifd[tag] = payload
|
||||||
|
|
||||||
|
def parse_tag(self, dtype, count, offset_buf):
|
||||||
|
"""Interpret an Exif image tag data payload.
|
||||||
|
"""
|
||||||
|
fmt = self.datatype2fmt[dtype][0] * count
|
||||||
|
payload_size = self.datatype2fmt[dtype][1] * count
|
||||||
|
|
||||||
|
if payload_size <= 4:
|
||||||
|
# Interpret the payload from the 4 bytes in the tag entry.
|
||||||
|
target_buffer = offset_buf[:payload_size]
|
||||||
|
else:
|
||||||
|
# Interpret the payload at the offset specified by the 4 bytes in the
|
||||||
|
# tag entry.
|
||||||
|
offset, = struct.unpack(self.endian + 'I', offset_buf)
|
||||||
|
target_buffer = self.buffer[offset:offset + payload_size]
|
||||||
|
|
||||||
|
if dtype == 2:
|
||||||
|
# ASCII
|
||||||
|
payload = target_buffer.decode('utf-8').rstrip()
|
||||||
|
else:
|
||||||
|
payload = struct.unpack(self.endian + fmt, target_buffer)
|
||||||
|
if dtype == 5 or dtype == 10:
|
||||||
|
# Rational or Signed Rational. Construct the list of values.
|
||||||
|
rational_payload = []
|
||||||
|
for j in range(count):
|
||||||
|
value = float(payload[j * 2]) / float(payload[j * 2 + 1])
|
||||||
|
rational_payload.append(value)
|
||||||
|
payload = rational_payload
|
||||||
|
if count == 1:
|
||||||
|
# If just a single value, then return a scalar instead of a
|
||||||
|
# tuple.
|
||||||
|
payload = payload[0]
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class ExifImageIfd(Ifd):
|
||||||
|
tagnum2name = {271: 'Make',
|
||||||
|
272: 'Model',
|
||||||
|
282: 'XResolution',
|
||||||
|
283: 'YResolution',
|
||||||
|
296: 'ResolutionUnit',
|
||||||
|
531: 'YCbCrPositioning',
|
||||||
|
34665: 'ExifTag',
|
||||||
|
34853: 'GPSTag'}
|
||||||
|
|
||||||
|
def __init__(self, endian, buffer, offset):
|
||||||
|
Ifd.__init__(self, endian, buffer, offset)
|
||||||
|
|
||||||
|
# Now post process the raw IFD.
|
||||||
|
self.ifd = {}
|
||||||
|
for tag, value in self.raw_ifd.items():
|
||||||
|
tag_name = self.tagnum2name[tag]
|
||||||
|
self.ifd[tag_name] = value
|
||||||
|
|
||||||
|
class ExifPhotoIfd(Ifd):
|
||||||
|
tagnum2name = {34855: 'ISOSpeedRatings',
|
||||||
|
36864: 'ExifVersion',
|
||||||
|
36867: 'DateTimeOriginal',
|
||||||
|
36868: 'DateTimeDigitized',
|
||||||
|
37121: 'ComponentsConfiguration',
|
||||||
|
37386: 'FocalLength',
|
||||||
|
40960: 'FlashpixVersion',
|
||||||
|
40961: 'ColorSpace',
|
||||||
|
40962: 'PixelXDimension',
|
||||||
|
40963: 'PixelYDimension',
|
||||||
|
40965: 'InteroperabilityTag'}
|
||||||
|
|
||||||
|
def __init__(self, endian, buffer, offset):
|
||||||
|
Ifd.__init__(self, endian, buffer, offset)
|
||||||
|
|
||||||
|
# Now post process the raw IFD.
|
||||||
|
self.ifd = {}
|
||||||
|
for tag, value in self.raw_ifd.items():
|
||||||
|
tag_name = self.tagnum2name[tag]
|
||||||
|
self.ifd[tag_name] = value
|
||||||
|
|
||||||
|
class ExifGPSInfoIfd(Ifd):
|
||||||
|
tagnum2name = {0: 'GPSVersionID',
|
||||||
|
1: 'GPSLatitudeRef',
|
||||||
|
2: 'GPSLatitude',
|
||||||
|
3: 'GPSLongitudeRef',
|
||||||
|
4: 'GPSLongitude',
|
||||||
|
5: 'GPSAltitudeRef',
|
||||||
|
6: 'GPSAltitude',
|
||||||
|
7: 'GPSTimeStamp',
|
||||||
|
8: 'GPSSatellites',
|
||||||
|
9: 'GPSStatus',
|
||||||
|
10: 'GPSMeasureMode',
|
||||||
|
11: 'GPSDOP',
|
||||||
|
12: 'GPSSpeedRef',
|
||||||
|
13: 'GPSSpeed',
|
||||||
|
14: 'GPSTrackRef',
|
||||||
|
15: 'GPSTrack',
|
||||||
|
16: 'GPSImgDirectionRef',
|
||||||
|
17: 'GPSImgDirection',
|
||||||
|
18: 'GPSMapDatum',
|
||||||
|
19: 'GPSDestLatitudeRef',
|
||||||
|
20: 'GPSDestLatitude',
|
||||||
|
21: 'GPSDestLongitudeRef',
|
||||||
|
22: 'GPSDestLongitude',
|
||||||
|
23: 'GPSDestBearingRef',
|
||||||
|
24: 'GPSDestBearing',
|
||||||
|
25: 'GPSDestDistanceRef',
|
||||||
|
26: 'GPSDestDistance',
|
||||||
|
27: 'GPSProcessingMethod',
|
||||||
|
28: 'GPSAreaInformation',
|
||||||
|
29: 'GPSDateStamp',
|
||||||
|
30: 'GPSDifferential'}
|
||||||
|
|
||||||
|
def __init__(self, endian, buffer, offset):
|
||||||
|
Ifd.__init__(self, endian, buffer, offset)
|
||||||
|
|
||||||
|
# Now post process the raw IFD.
|
||||||
|
self.ifd = {}
|
||||||
|
for tag, value in self.raw_ifd.items():
|
||||||
|
tag_name = self.tagnum2name[tag]
|
||||||
|
self.ifd[tag_name] = value
|
||||||
|
|
||||||
|
class ExifInteroperabilityIfd(Ifd):
|
||||||
|
tagnum2name = {1: 'InteroperabilityIndex',
|
||||||
|
2: 'InteroperabilityVersion',
|
||||||
|
4096: 'RelatedImageFileFormat',
|
||||||
|
4097: 'RelatedImageWidth',
|
||||||
|
4098: 'RelatedImageLength'}
|
||||||
|
|
||||||
|
def __init__(self, endian, buffer, offset):
|
||||||
|
Ifd.__init__(self, endian, buffer, offset)
|
||||||
|
|
||||||
|
# Now post process the raw IFD.
|
||||||
|
self.ifd = {}
|
||||||
|
for tag, value in self.raw_ifd.items():
|
||||||
|
tag_name = self.tagnum2name[tag]
|
||||||
|
self.ifd[tag_name] = value
|
||||||
|
|
||||||
# Map each box ID to the corresponding class.
|
# Map each box ID to the corresponding class.
|
||||||
_box_with_id = {
|
_box_with_id = {
|
||||||
|
|
@ -1668,73 +1894,6 @@ def _indent(elem, level=0):
|
||||||
if level and (not elem.tail or not elem.tail.strip()):
|
if level and (not elem.tail or not elem.tail.strip()):
|
||||||
elem.tail = i
|
elem.tail = i
|
||||||
|
|
||||||
_tagnum2name = {271: 'Make', 272: 'Model',
|
|
||||||
282: 'XResolution', 283: 'YResolution',
|
|
||||||
296: 'ResolutionUnit',
|
|
||||||
531: 'YCbCrPositioning',
|
|
||||||
34665: 'ExifTag',
|
|
||||||
34853: 'GPSTag'}
|
|
||||||
|
|
||||||
def _parse_exif(buffer):
|
|
||||||
"""Interpret raw buffer consisting of Exif IFD.
|
|
||||||
"""
|
|
||||||
# Ignore the first six bytes.
|
|
||||||
# Next six should be (73, 73, 42, 8, numtags)
|
|
||||||
data = struct.unpack('<BBHIH', buffer[6:16])
|
|
||||||
num_tags = data[4]
|
|
||||||
|
|
||||||
fmt = '<' + 'HHII' * num_tags
|
|
||||||
data = struct.unpack(fmt, buffer[16:16 + num_tags * 12])
|
|
||||||
exif = {}
|
|
||||||
for j, tag in enumerate(data[0::4]):
|
|
||||||
offset_bytes = buffer[16 + j * 12 + 8:16 + j * 12 + 8 + 4]
|
|
||||||
exif[_tagnum2name[tag]] = _parse_exif_image_tag(data[j * 4 + 1],
|
|
||||||
data[j * 4 + 2],
|
|
||||||
offset_bytes,
|
|
||||||
buffer)
|
|
||||||
print(exif)
|
|
||||||
return exif
|
|
||||||
|
|
||||||
# Map the TIFF enumerated datatype to the python datatype and data width.
|
|
||||||
_datatype2fmt = {1: ('B', 1),
|
|
||||||
2: ('B', 1),
|
|
||||||
3: ('H', 2),
|
|
||||||
4: ('I', 4),
|
|
||||||
5: ('II', 8),
|
|
||||||
7: ('B', 1),
|
|
||||||
9: ('i', 4),
|
|
||||||
10: ('ii', 8)}
|
|
||||||
|
|
||||||
def _parse_exif_image_tag(datatype, count, offset_buffer, exif_buffer):
|
|
||||||
"""Interpret an Exif image tag data payload.
|
|
||||||
"""
|
|
||||||
fmt = _datatype2fmt[datatype][0] * count
|
|
||||||
payload_size = _datatype2fmt[datatype][1] * count
|
|
||||||
|
|
||||||
if payload_size <= 4:
|
|
||||||
# Interpret the payload from the 4 bytes in the tag entry.
|
|
||||||
target_buffer = offset_buffer[:payload_size]
|
|
||||||
else:
|
|
||||||
# Interpret the payload at the offset specified by the 4 bytes in the
|
|
||||||
# tag entry.
|
|
||||||
offset, = struct.unpack('<I', offset_buffer)
|
|
||||||
target_buffer = exif_buffer[6 + offset:6 + offset + payload_size]
|
|
||||||
|
|
||||||
if datatype == 2:
|
|
||||||
payload = target_buffer.decode('utf-8').rstrip()
|
|
||||||
else:
|
|
||||||
payload = struct.unpack('<' + fmt, target_buffer)
|
|
||||||
if datatype == 5:
|
|
||||||
rational_payload = []
|
|
||||||
for j in range(count):
|
|
||||||
value = float(payload[j * 2]) / float(payload[j * 2 + 1])
|
|
||||||
rational_payload.append(value)
|
|
||||||
payload = rational_payload
|
|
||||||
if count == 1:
|
|
||||||
payload = payload[0]
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
def _pretty_print_xml(xml, level=0):
|
def _pretty_print_xml(xml, level=0):
|
||||||
"""Pretty print XML data.
|
"""Pretty print XML data.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue