Merge branch 'issue104' of github.com:quintusdias/glymur into issue104
Conflicts: CHANGES.txt
This commit is contained in:
commit
f3d43cef7f
14 changed files with 1026 additions and 736 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
Oct 22, 2013 - v0.5.7 Super box constructors now take optional box list
|
Oct 24, 2013 - v0.6.0 Palette box palette changed to 2D numpy array. Removed
|
||||||
argument. Removed ssiz attribute from SIZsegment class.
|
nemo Exif and simple XMP UUIDs in favor of larger XMP UUID. Refactored
|
||||||
|
Exif UUID code into _uuid_io sub package. Superbox constructors now take
|
||||||
|
optional box list argument. Removed ssiz attribute from SIZsegment class.
|
||||||
|
|
||||||
Oct 13, 2013 - v0.5.6 Fixed handling of non-ascii chars in XML boxes. Fixed
|
Oct 13, 2013 - v0.5.6 Fixed handling of non-ascii chars in XML boxes. Fixed
|
||||||
some docstring errors in jp2box module.
|
some docstring errors in jp2box module.
|
||||||
|
|
|
||||||
540
glymur/_uuid_io/Exif.py
Normal file
540
glymur/_uuid_io/Exif.py
Normal file
|
|
@ -0,0 +1,540 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Handlers for Exif UUIDs. Be nice if we would find a standard for this.
|
||||||
|
"""
|
||||||
|
import pprint
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
if sys.hexversion < 0x02070000:
|
||||||
|
# pylint: disable=F0401,E0611
|
||||||
|
from ordereddict import OrderedDict
|
||||||
|
else:
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
class UUIDExif(object):
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
read_buffer : bytes
|
||||||
|
Raw byte stream consisting of the UUID data.
|
||||||
|
endian : str
|
||||||
|
Either '<' for big-endian, or '>' for little-endian.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, read_buffer):
|
||||||
|
"""Interpret raw buffer consisting of Exif IFD.
|
||||||
|
"""
|
||||||
|
exif_image = None
|
||||||
|
exif_photo = None
|
||||||
|
exif_gpsinfo = None
|
||||||
|
exif_iop = None
|
||||||
|
|
||||||
|
self.read_buffer = read_buffer
|
||||||
|
|
||||||
|
# Ignore the first six bytes.
|
||||||
|
# Next 8 should be (73, 73, 42, 8) or (77, 77, 42, 8)
|
||||||
|
data = struct.unpack('<BB', read_buffer[6:8])
|
||||||
|
if data[0] == 73 and data[1] == 73:
|
||||||
|
# little endian
|
||||||
|
self.endian = '<'
|
||||||
|
elif data[0] == 77 and data[1] == 77:
|
||||||
|
# big endian
|
||||||
|
self.endian = '>'
|
||||||
|
else:
|
||||||
|
msg = "Bad byte order indication: {0}".format(read_buffer[6:8])
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
_, offset = struct.unpack(self.endian + 'HI', read_buffer[8:14])
|
||||||
|
|
||||||
|
# This is the 'Exif Image' portion.
|
||||||
|
exif = _ExifImageIfd(self.endian, read_buffer[6:], offset)
|
||||||
|
exif_image = exif.processed_ifd
|
||||||
|
|
||||||
|
if 'ExifTag' in exif_image.keys():
|
||||||
|
offset = exif_image['ExifTag']
|
||||||
|
photo_ifd = _ExifPhotoIfd(self.endian, read_buffer[6:], offset)
|
||||||
|
exif_photo = photo_ifd.processed_ifd
|
||||||
|
|
||||||
|
if 'InteroperabilityTag' in exif_photo.keys():
|
||||||
|
offset = exif_photo['InteroperabilityTag']
|
||||||
|
interop = _ExifInteroperabilityIfd(self.endian,
|
||||||
|
read_buffer[6:],
|
||||||
|
offset)
|
||||||
|
exif_iop = interop.processed_ifd
|
||||||
|
|
||||||
|
if 'GPSTag' in exif_image.keys():
|
||||||
|
offset = exif_image['GPSTag']
|
||||||
|
gps = _ExifGPSInfoIfd(self.endian, read_buffer[6:], offset)
|
||||||
|
exif_gpsinfo = gps.processed_ifd
|
||||||
|
|
||||||
|
self.ifds = OrderedDict()
|
||||||
|
self.ifds['Image'] = exif_image
|
||||||
|
self.ifds['Photo'] = exif_photo
|
||||||
|
self.ifds['GPSInfo'] = exif_gpsinfo
|
||||||
|
self.ifds['Iop'] = exif_iop
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# 2.7 has trouble pretty-printing ordered dicts, so print them
|
||||||
|
# as regular dicts. Not ideal, but at least it's good on 3.3+.
|
||||||
|
if sys.hexversion < 0x03000000:
|
||||||
|
data = dict(self.ifds)
|
||||||
|
else:
|
||||||
|
data = self.ifds
|
||||||
|
return '\n' + pprint.pformat(data)
|
||||||
|
|
||||||
|
|
||||||
|
class _Ifd(object):
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
read_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.
|
||||||
|
processed_ifd : dictionary
|
||||||
|
Maps tag name 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, read_buffer, offset):
|
||||||
|
self.endian = endian
|
||||||
|
self.read_buffer = read_buffer
|
||||||
|
self.processed_ifd = OrderedDict()
|
||||||
|
|
||||||
|
self.num_tags, = struct.unpack(endian + 'H',
|
||||||
|
read_buffer[offset:offset + 2])
|
||||||
|
|
||||||
|
fmt = self.endian + 'HHII' * self.num_tags
|
||||||
|
ifd_buffer = read_buffer[offset + 2:offset + 2 + self.num_tags * 12]
|
||||||
|
data = struct.unpack(fmt, ifd_buffer)
|
||||||
|
self.raw_ifd = OrderedDict()
|
||||||
|
for j, tag in enumerate(data[0::4]):
|
||||||
|
# The offset to the tag offset/payload is the offset to the IFD
|
||||||
|
# plus 2 bytes for the number of tags plus 12 bytes for each
|
||||||
|
# tag entry plus 8 bytes to the offset/payload itself.
|
||||||
|
toffp = read_buffer[offset + 10 + j * 12:offset + 10 + j * 12 + 4]
|
||||||
|
tag_data = self.parse_tag(data[j * 4 + 1],
|
||||||
|
data[j * 4 + 2],
|
||||||
|
toffp)
|
||||||
|
self.raw_ifd[tag] = tag_data
|
||||||
|
|
||||||
|
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.read_buffer[offset:offset + payload_size]
|
||||||
|
|
||||||
|
if dtype == 2:
|
||||||
|
# ASCII
|
||||||
|
if sys.hexversion < 0x03000000:
|
||||||
|
payload = target_buffer.rstrip('\x00')
|
||||||
|
else:
|
||||||
|
payload = target_buffer.decode('utf-8').rstrip('\x00')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def post_process(self, tagnum2name):
|
||||||
|
"""Map the tag name instead of tag number to the tag value.
|
||||||
|
"""
|
||||||
|
for tag, value in self.raw_ifd.items():
|
||||||
|
try:
|
||||||
|
tag_name = tagnum2name[tag]
|
||||||
|
except KeyError:
|
||||||
|
# Ok, we don't recognize this tag. Just use the numeric id.
|
||||||
|
msg = 'Unrecognized Exif tag "{0}".'.format(tag)
|
||||||
|
warnings.warn(msg, UserWarning)
|
||||||
|
tag_name = tag
|
||||||
|
self.processed_ifd[tag_name] = value
|
||||||
|
|
||||||
|
|
||||||
|
class _ExifImageIfd(_Ifd):
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
tagnum2name : dict
|
||||||
|
Maps Exif image tag numbers to the tag names.
|
||||||
|
ifd : dict
|
||||||
|
Maps tag names to tag values.
|
||||||
|
"""
|
||||||
|
tagnum2name = {11: 'ProcessingSoftware',
|
||||||
|
254: 'NewSubfileType',
|
||||||
|
255: 'SubfileType',
|
||||||
|
256: 'ImageWidth',
|
||||||
|
257: 'ImageLength',
|
||||||
|
258: 'BitsPerSample',
|
||||||
|
259: 'Compression',
|
||||||
|
262: 'PhotometricInterpretation',
|
||||||
|
263: 'Threshholding',
|
||||||
|
264: 'CellWidth',
|
||||||
|
265: 'CellLength',
|
||||||
|
266: 'FillOrder',
|
||||||
|
269: 'DocumentName',
|
||||||
|
270: 'ImageDescription',
|
||||||
|
271: 'Make',
|
||||||
|
272: 'Model',
|
||||||
|
273: 'StripOffsets',
|
||||||
|
274: 'Orientation',
|
||||||
|
277: 'SamplesPerPixel',
|
||||||
|
278: 'RowsPerStrip',
|
||||||
|
279: 'StripByteCounts',
|
||||||
|
282: 'XResolution',
|
||||||
|
283: 'YResolution',
|
||||||
|
284: 'PlanarConfiguration',
|
||||||
|
290: 'GrayResponseUnit',
|
||||||
|
291: 'GrayResponseCurve',
|
||||||
|
292: 'T4Options',
|
||||||
|
293: 'T6Options',
|
||||||
|
296: 'ResolutionUnit',
|
||||||
|
301: 'TransferFunction',
|
||||||
|
305: 'Software',
|
||||||
|
306: 'DateTime',
|
||||||
|
315: 'Artist',
|
||||||
|
316: 'HostComputer',
|
||||||
|
317: 'Predictor',
|
||||||
|
318: 'WhitePoint',
|
||||||
|
319: 'PrimaryChromaticities',
|
||||||
|
320: 'ColorMap',
|
||||||
|
321: 'HalftoneHints',
|
||||||
|
322: 'TileWidth',
|
||||||
|
323: 'TileLength',
|
||||||
|
324: 'TileOffsets',
|
||||||
|
325: 'TileByteCounts',
|
||||||
|
330: 'SubIFDs',
|
||||||
|
332: 'InkSet',
|
||||||
|
333: 'InkNames',
|
||||||
|
334: 'NumberOfInks',
|
||||||
|
336: 'DotRange',
|
||||||
|
337: 'TargetPrinter',
|
||||||
|
338: 'ExtraSamples',
|
||||||
|
339: 'SampleFormat',
|
||||||
|
340: 'SMinSampleValue',
|
||||||
|
341: 'SMaxSampleValue',
|
||||||
|
342: 'TransferRange',
|
||||||
|
343: 'ClipPath',
|
||||||
|
344: 'XClipPathUnits',
|
||||||
|
345: 'YClipPathUnits',
|
||||||
|
346: 'Indexed',
|
||||||
|
347: 'JPEGTables',
|
||||||
|
351: 'OPIProxy',
|
||||||
|
512: 'JPEGProc',
|
||||||
|
513: 'JPEGInterchangeFormat',
|
||||||
|
514: 'JPEGInterchangeFormatLength',
|
||||||
|
515: 'JPEGRestartInterval',
|
||||||
|
517: 'JPEGLosslessPredictors',
|
||||||
|
518: 'JPEGPointTransforms',
|
||||||
|
519: 'JPEGQTables',
|
||||||
|
520: 'JPEGDCTables',
|
||||||
|
521: 'JPEGACTables',
|
||||||
|
529: 'YCbCrCoefficients',
|
||||||
|
530: 'YCbCrSubSampling',
|
||||||
|
531: 'YCbCrPositioning',
|
||||||
|
532: 'ReferenceBlackWhite',
|
||||||
|
700: 'XMLPacket',
|
||||||
|
18246: 'Rating',
|
||||||
|
18249: 'RatingPercent',
|
||||||
|
32781: 'ImageID',
|
||||||
|
33421: 'CFARepeatPatternDim',
|
||||||
|
33422: 'CFAPattern',
|
||||||
|
33423: 'BatteryLevel',
|
||||||
|
33432: 'Copyright',
|
||||||
|
33434: 'ExposureTime',
|
||||||
|
33437: 'FNumber',
|
||||||
|
33723: 'IPTCNAA',
|
||||||
|
34377: 'ImageResources',
|
||||||
|
34665: 'ExifTag',
|
||||||
|
34675: 'InterColorProfile',
|
||||||
|
34850: 'ExposureProgram',
|
||||||
|
34852: 'SpectralSensitivity',
|
||||||
|
34853: 'GPSTag',
|
||||||
|
34855: 'ISOSpeedRatings',
|
||||||
|
34856: 'OECF',
|
||||||
|
34857: 'Interlace',
|
||||||
|
34858: 'TimeZoneOffset',
|
||||||
|
34859: 'SelfTimerMode',
|
||||||
|
36867: 'DateTimeOriginal',
|
||||||
|
37122: 'CompressedBitsPerPixel',
|
||||||
|
37377: 'ShutterSpeedValue',
|
||||||
|
37378: 'ApertureValue',
|
||||||
|
37379: 'BrightnessValue',
|
||||||
|
37380: 'ExposureBiasValue',
|
||||||
|
37381: 'MaxApertureValue',
|
||||||
|
37382: 'SubjectDistance',
|
||||||
|
37383: 'MeteringMode',
|
||||||
|
37384: 'LightSource',
|
||||||
|
37385: 'Flash',
|
||||||
|
37386: 'FocalLength',
|
||||||
|
37387: 'FlashEnergy',
|
||||||
|
37388: 'SpatialFrequencyResponse',
|
||||||
|
37389: 'Noise',
|
||||||
|
37390: 'FocalPlaneXResolution',
|
||||||
|
37391: 'FocalPlaneYResolution',
|
||||||
|
37392: 'FocalPlaneResolutionUnit',
|
||||||
|
37393: 'ImageNumber',
|
||||||
|
37394: 'SecurityClassification',
|
||||||
|
37395: 'ImageHistory',
|
||||||
|
37396: 'SubjectLocation',
|
||||||
|
37397: 'ExposureIndex',
|
||||||
|
37398: 'TIFFEPStandardID',
|
||||||
|
37399: 'SensingMethod',
|
||||||
|
40091: 'XPTitle',
|
||||||
|
40092: 'XPComment',
|
||||||
|
40093: 'XPAuthor',
|
||||||
|
40094: 'XPKeywords',
|
||||||
|
40095: 'XPSubject',
|
||||||
|
50341: 'PrintImageMatching',
|
||||||
|
50706: 'DNGVersion',
|
||||||
|
50707: 'DNGBackwardVersion',
|
||||||
|
50708: 'UniqueCameraModel',
|
||||||
|
50709: 'LocalizedCameraModel',
|
||||||
|
50710: 'CFAPlaneColor',
|
||||||
|
50711: 'CFALayout',
|
||||||
|
50712: 'LinearizationTable',
|
||||||
|
50713: 'BlackLevelRepeatDim',
|
||||||
|
50714: 'BlackLevel',
|
||||||
|
50715: 'BlackLevelDeltaH',
|
||||||
|
50716: 'BlackLevelDeltaV',
|
||||||
|
50717: 'WhiteLevel',
|
||||||
|
50718: 'DefaultScale',
|
||||||
|
50719: 'DefaultCropOrigin',
|
||||||
|
50720: 'DefaultCropSize',
|
||||||
|
50721: 'ColorMatrix1',
|
||||||
|
50722: 'ColorMatrix2',
|
||||||
|
50723: 'CameraCalibration1',
|
||||||
|
50724: 'CameraCalibration2',
|
||||||
|
50725: 'ReductionMatrix1',
|
||||||
|
50726: 'ReductionMatrix2',
|
||||||
|
50727: 'AnalogBalance',
|
||||||
|
50728: 'AsShotNeutral',
|
||||||
|
50729: 'AsShotWhiteXY',
|
||||||
|
50730: 'BaselineExposure',
|
||||||
|
50731: 'BaselineNoise',
|
||||||
|
50732: 'BaselineSharpness',
|
||||||
|
50733: 'BayerGreenSplit',
|
||||||
|
50734: 'LinearResponseLimit',
|
||||||
|
50735: 'CameraSerialNumber',
|
||||||
|
50736: 'LensInfo',
|
||||||
|
50737: 'ChromaBlurRadius',
|
||||||
|
50738: 'AntiAliasStrength',
|
||||||
|
50739: 'ShadowScale',
|
||||||
|
50740: 'DNGPrivateData',
|
||||||
|
50741: 'MakerNoteSafety',
|
||||||
|
50778: 'CalibrationIlluminant1',
|
||||||
|
50779: 'CalibrationIlluminant2',
|
||||||
|
50780: 'BestQualityScale',
|
||||||
|
50781: 'RawDataUniqueID',
|
||||||
|
50827: 'OriginalRawFileName',
|
||||||
|
50828: 'OriginalRawFileData',
|
||||||
|
50829: 'ActiveArea',
|
||||||
|
50830: 'MaskedAreas',
|
||||||
|
50831: 'AsShotICCProfile',
|
||||||
|
50832: 'AsShotPreProfileMatrix',
|
||||||
|
50833: 'CurrentICCProfile',
|
||||||
|
50834: 'CurrentPreProfileMatrix',
|
||||||
|
50879: 'ColorimetricReference',
|
||||||
|
50931: 'CameraCalibrationSignature',
|
||||||
|
50932: 'ProfileCalibrationSignature',
|
||||||
|
50934: 'AsShotProfileName',
|
||||||
|
50935: 'NoiseReductionApplied',
|
||||||
|
50936: 'ProfileName',
|
||||||
|
50937: 'ProfileHueSatMapDims',
|
||||||
|
50938: 'ProfileHueSatMapData1',
|
||||||
|
50939: 'ProfileHueSatMapData2',
|
||||||
|
50940: 'ProfileToneCurve',
|
||||||
|
50941: 'ProfileEmbedPolicy',
|
||||||
|
50942: 'ProfileCopyright',
|
||||||
|
50964: 'ForwardMatrix1',
|
||||||
|
50965: 'ForwardMatrix2',
|
||||||
|
50966: 'PreviewApplicationName',
|
||||||
|
50967: 'PreviewApplicationVersion',
|
||||||
|
50968: 'PreviewSettingsName',
|
||||||
|
50969: 'PreviewSettingsDigest',
|
||||||
|
50970: 'PreviewColorSpace',
|
||||||
|
50971: 'PreviewDateTime',
|
||||||
|
50972: 'RawImageDigest',
|
||||||
|
50973: 'OriginalRawFileDigest',
|
||||||
|
50974: 'SubTileBlockSize',
|
||||||
|
50975: 'RowInterleaveFactor',
|
||||||
|
50981: 'ProfileLookTableDims',
|
||||||
|
50982: 'ProfileLookTableData',
|
||||||
|
51008: 'OpcodeList1',
|
||||||
|
51009: 'OpcodeList2',
|
||||||
|
51022: 'OpcodeList3',
|
||||||
|
51041: 'NoiseProfile'}
|
||||||
|
|
||||||
|
def __init__(self, endian, read_buffer, offset):
|
||||||
|
_Ifd.__init__(self, endian, read_buffer, offset)
|
||||||
|
self.post_process(self.tagnum2name)
|
||||||
|
|
||||||
|
|
||||||
|
class _ExifPhotoIfd(_Ifd):
|
||||||
|
"""Represents tags found in the Exif sub ifd.
|
||||||
|
"""
|
||||||
|
tagnum2name = {33434: 'ExposureTime',
|
||||||
|
33437: 'FNumber',
|
||||||
|
34850: 'ExposureProgram',
|
||||||
|
34852: 'SpectralSensitivity',
|
||||||
|
34855: 'ISOSpeedRatings',
|
||||||
|
34856: 'OECF',
|
||||||
|
34864: 'SensitivityType',
|
||||||
|
34865: 'StandardOutputSensitivity',
|
||||||
|
34866: 'RecommendedExposureIndex',
|
||||||
|
34867: 'ISOSpeed',
|
||||||
|
34868: 'ISOSpeedLatitudeyyy',
|
||||||
|
34869: 'ISOSpeedLatitudezzz',
|
||||||
|
36864: 'ExifVersion',
|
||||||
|
36867: 'DateTimeOriginal',
|
||||||
|
36868: 'DateTimeDigitized',
|
||||||
|
37121: 'ComponentsConfiguration',
|
||||||
|
37122: 'CompressedBitsPerPixel',
|
||||||
|
37377: 'ShutterSpeedValue',
|
||||||
|
37378: 'ApertureValue',
|
||||||
|
37379: 'BrightnessValue',
|
||||||
|
37380: 'ExposureBiasValue',
|
||||||
|
37381: 'MaxApertureValue',
|
||||||
|
37382: 'SubjectDistance',
|
||||||
|
37383: 'MeteringMode',
|
||||||
|
37384: 'LightSource',
|
||||||
|
37385: 'Flash',
|
||||||
|
37386: 'FocalLength',
|
||||||
|
37396: 'SubjectArea',
|
||||||
|
37500: 'MakerNote',
|
||||||
|
37510: 'UserComment',
|
||||||
|
37520: 'SubSecTime',
|
||||||
|
37521: 'SubSecTimeOriginal',
|
||||||
|
37522: 'SubSecTimeDigitized',
|
||||||
|
40960: 'FlashpixVersion',
|
||||||
|
40961: 'ColorSpace',
|
||||||
|
40962: 'PixelXDimension',
|
||||||
|
40963: 'PixelYDimension',
|
||||||
|
40964: 'RelatedSoundFile',
|
||||||
|
40965: 'InteroperabilityTag',
|
||||||
|
41483: 'FlashEnergy',
|
||||||
|
41484: 'SpatialFrequencyResponse',
|
||||||
|
41486: 'FocalPlaneXResolution',
|
||||||
|
41487: 'FocalPlaneYResolution',
|
||||||
|
41488: 'FocalPlaneResolutionUnit',
|
||||||
|
41492: 'SubjectLocation',
|
||||||
|
41493: 'ExposureIndex',
|
||||||
|
41495: 'SensingMethod',
|
||||||
|
41728: 'FileSource',
|
||||||
|
41729: 'SceneType',
|
||||||
|
41730: 'CFAPattern',
|
||||||
|
41985: 'CustomRendered',
|
||||||
|
41986: 'ExposureMode',
|
||||||
|
41987: 'WhiteBalance',
|
||||||
|
41988: 'DigitalZoomRatio',
|
||||||
|
41989: 'FocalLengthIn35mmFilm',
|
||||||
|
41990: 'SceneCaptureType',
|
||||||
|
41991: 'GainControl',
|
||||||
|
41992: 'Contrast',
|
||||||
|
41993: 'Saturation',
|
||||||
|
41994: 'Sharpness',
|
||||||
|
41995: 'DeviceSettingDescription',
|
||||||
|
41996: 'SubjectDistanceRange',
|
||||||
|
42016: 'ImageUniqueID',
|
||||||
|
42032: 'CameraOwnerName',
|
||||||
|
42033: 'BodySerialNumber',
|
||||||
|
42034: 'LensSpecification',
|
||||||
|
42035: 'LensMake',
|
||||||
|
42036: 'LensModel',
|
||||||
|
42037: 'LensSerialNumber'}
|
||||||
|
|
||||||
|
def __init__(self, endian, read_buffer, offset):
|
||||||
|
_Ifd.__init__(self, endian, read_buffer, offset)
|
||||||
|
self.post_process(self.tagnum2name)
|
||||||
|
|
||||||
|
|
||||||
|
class _ExifGPSInfoIfd(_Ifd):
|
||||||
|
"""Represents information found in the GPSInfo sub 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, read_buffer, offset):
|
||||||
|
_Ifd.__init__(self, endian, read_buffer, offset)
|
||||||
|
self.post_process(self.tagnum2name)
|
||||||
|
|
||||||
|
|
||||||
|
class _ExifInteroperabilityIfd(_Ifd):
|
||||||
|
"""Represents tags found in the Interoperability sub IFD.
|
||||||
|
"""
|
||||||
|
tagnum2name = {1: 'InteroperabilityIndex',
|
||||||
|
2: 'InteroperabilityVersion',
|
||||||
|
4096: 'RelatedImageFileFormat',
|
||||||
|
4097: 'RelatedImageWidth',
|
||||||
|
4098: 'RelatedImageLength'}
|
||||||
|
|
||||||
|
def __init__(self, endian, read_buffer, offset):
|
||||||
|
_Ifd.__init__(self, endian, read_buffer, offset)
|
||||||
|
self.post_process(self.tagnum2name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
46
glymur/_uuid_io/XMP.py
Normal file
46
glymur/_uuid_io/XMP.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Handler for a UUID for XMP.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
|
||||||
|
from ..core import _pretty_print_xml
|
||||||
|
|
||||||
|
class UUIDXMP(object):
|
||||||
|
"""
|
||||||
|
Handler for a UUID for XMP.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
packet : ElementTree
|
||||||
|
XML conforming to the XMP specifications.
|
||||||
|
|
||||||
|
References
|
||||||
|
----------
|
||||||
|
.. [XMP] International Organization for Standardication. ISO/IEC
|
||||||
|
16684-1:2012 - Graphic technology -- Extensible metadata platform (XMP)
|
||||||
|
specification -- Part 1: Data model, serialization and core properties
|
||||||
|
"""
|
||||||
|
def __init__(self, read_buffer):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
read_buffer : byte array
|
||||||
|
sequence of bytes that can be decoded into an XMP packet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# XMP data. Parse as XML.
|
||||||
|
if sys.hexversion < 0x03000000:
|
||||||
|
# 2.x strings same as bytes
|
||||||
|
elt = ET.fromstring(read_buffer)
|
||||||
|
else:
|
||||||
|
# 3.x takes strings, not bytes.
|
||||||
|
text = read_buffer.decode('utf-8')
|
||||||
|
elt = ET.fromstring(text)
|
||||||
|
self.packet = ET.ElementTree(elt)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _pretty_print_xml(self.packet)
|
||||||
6
glymur/_uuid_io/__init__.py
Normal file
6
glymur/_uuid_io/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""
|
||||||
|
Sub package for handling various UUIDs.
|
||||||
|
"""
|
||||||
|
from .Exif import UUIDExif
|
||||||
|
from .XMP import UUIDXMP
|
||||||
|
from .generic import UUIDGeneric
|
||||||
27
glymur/_uuid_io/generic.py
Normal file
27
glymur/_uuid_io/generic.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Handler for a generic UUID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class UUIDGeneric(object):
|
||||||
|
"""
|
||||||
|
Handler for a generic UUID that is not currently recognized.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
data : byte array
|
||||||
|
Sequence of uninterpreted bytes as read from the file.
|
||||||
|
"""
|
||||||
|
def __init__(self, read_buffer):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
read_buffer : byte array
|
||||||
|
sequence of bytes as read from the file.
|
||||||
|
"""
|
||||||
|
self.data = read_buffer
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{0} bytes'.format(len(self.data))
|
||||||
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
"""Core definitions to be shared amongst the modules.
|
"""Core definitions to be shared amongst the modules.
|
||||||
"""
|
"""
|
||||||
|
import copy
|
||||||
|
import xml.etree.cElementTree as ET
|
||||||
|
|
||||||
# Progression order
|
# Progression order
|
||||||
LRCP = 0
|
LRCP = 0
|
||||||
RLCP = 1
|
RLCP = 1
|
||||||
|
|
@ -73,3 +76,45 @@ _CAPABILITIES_DISPLAY = {
|
||||||
1: '0',
|
1: '0',
|
||||||
2: '1',
|
2: '1',
|
||||||
3: '3'}
|
3: '3'}
|
||||||
|
|
||||||
|
|
||||||
|
def _pretty_print_xml(xml, level=0):
|
||||||
|
"""Pretty print XML data.
|
||||||
|
"""
|
||||||
|
xml = copy.deepcopy(xml)
|
||||||
|
_indent(xml.getroot(), level=level)
|
||||||
|
xmltext = ET.tostring(xml.getroot(), encoding='utf-8').decode('utf-8')
|
||||||
|
|
||||||
|
# Indent it a bit.
|
||||||
|
lst = [(' ' + x) for x in xmltext.split('\n')]
|
||||||
|
try:
|
||||||
|
xml = '\n'.join(lst)
|
||||||
|
return '\n{0}'.format(xml)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
# This can happen on python 2.x if the character set contains certain
|
||||||
|
# non-ascii characters. Just print out the corresponding xml char
|
||||||
|
# entities instead.
|
||||||
|
xml = u'\n'.join(lst)
|
||||||
|
text = u'\n{0}'.format(xml)
|
||||||
|
text = text.encode('ascii', 'xmlcharrefreplace')
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _indent(elem, level=0):
|
||||||
|
"""Recipe for pretty printing XML. Please see
|
||||||
|
|
||||||
|
http://effbot.org/zone/element-lib.htm#prettyprint
|
||||||
|
"""
|
||||||
|
i = "\n" + level * " "
|
||||||
|
if len(elem):
|
||||||
|
if not elem.text or not elem.text.strip():
|
||||||
|
elem.text = i + " "
|
||||||
|
if not elem.tail or not elem.tail.strip():
|
||||||
|
elem.tail = i
|
||||||
|
for elem in elem:
|
||||||
|
_indent(elem, level + 1)
|
||||||
|
if not elem.tail or not elem.tail.strip():
|
||||||
|
elem.tail = i
|
||||||
|
else:
|
||||||
|
if level and (not elem.tail or not elem.tail.strip()):
|
||||||
|
elem.tail = i
|
||||||
|
|
|
||||||
Binary file not shown.
631
glymur/jp2box.py
631
glymur/jp2box.py
|
|
@ -13,13 +13,13 @@ References
|
||||||
|
|
||||||
# pylint: disable=C0302,R0903,R0913
|
# pylint: disable=C0302,R0903,R0913
|
||||||
|
|
||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
import xml.etree.cElementTree as ET
|
import xml.etree.cElementTree as ET
|
||||||
|
|
@ -38,6 +38,9 @@ from .core import _COLORSPACE_MAP_DISPLAY
|
||||||
from .core import _COLOR_TYPE_MAP_DISPLAY
|
from .core import _COLOR_TYPE_MAP_DISPLAY
|
||||||
from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
|
from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
|
||||||
from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD
|
from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD
|
||||||
|
from .core import _pretty_print_xml
|
||||||
|
|
||||||
|
from . import _uuid_io
|
||||||
|
|
||||||
_METHOD_DISPLAY = {
|
_METHOD_DISPLAY = {
|
||||||
ENUMERATED_COLORSPACE: 'enumerated colorspace',
|
ENUMERATED_COLORSPACE: 'enumerated colorspace',
|
||||||
|
|
@ -2192,8 +2195,11 @@ class UUIDBox(Jp2kBox):
|
||||||
----------
|
----------
|
||||||
the_uuid : uuid.UUID
|
the_uuid : uuid.UUID
|
||||||
Identifies the type of UUID box.
|
Identifies the type of UUID box.
|
||||||
|
data : object
|
||||||
|
Specific to each type of UUID. There are handlers for XMP, Exif,
|
||||||
|
and unknown UUIDs.
|
||||||
raw_data : byte array
|
raw_data : byte array
|
||||||
This is the "payload" of data for the specified UUID.
|
Sequence of uninterpreted bytes as read from the file.
|
||||||
length : int
|
length : int
|
||||||
length of the box in bytes.
|
length of the box in bytes.
|
||||||
offset : int
|
offset : int
|
||||||
|
|
@ -2203,34 +2209,25 @@ class UUIDBox(Jp2kBox):
|
||||||
self.uuid = the_uuid
|
self.uuid = the_uuid
|
||||||
self.raw_data = raw_data
|
self.raw_data = raw_data
|
||||||
|
|
||||||
if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'):
|
try:
|
||||||
# XMP data. Parse as XML. Seems to be a difference between
|
if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'):
|
||||||
# ElementTree in version 2.7 and 3.3.
|
self.data = _uuid_io.UUIDXMP(raw_data)
|
||||||
if sys.hexversion < 0x03000000:
|
self._type = 'XMP'
|
||||||
elt = ET.fromstring(raw_data)
|
elif the_uuid.bytes == b'JpgTiffExif->JP2':
|
||||||
|
self.data = _uuid_io.UUIDExif(raw_data)
|
||||||
|
self._type = 'Exif'
|
||||||
else:
|
else:
|
||||||
text = raw_data.decode('utf-8')
|
self.data = _uuid_io.UUIDGeneric(raw_data)
|
||||||
elt = ET.fromstring(text)
|
self._type = 'unknown'
|
||||||
self.data = ET.ElementTree(elt)
|
except Exception:
|
||||||
self._type = 'XMP'
|
# In case of any exception, create the generic UUID.
|
||||||
elif the_uuid.bytes == b'JpgTiffExif->JP2':
|
self.data = _uuid_io.UUIDGeneric(raw_data)
|
||||||
exif_obj = Exif(raw_data)
|
|
||||||
ifds = OrderedDict()
|
|
||||||
ifds['Image'] = exif_obj.exif_image
|
|
||||||
ifds['Photo'] = exif_obj.exif_photo
|
|
||||||
ifds['GPSInfo'] = exif_obj.exif_gpsinfo
|
|
||||||
ifds['Iop'] = exif_obj.exif_iop
|
|
||||||
self.data = ifds
|
|
||||||
self._type = 'Exif'
|
|
||||||
else:
|
|
||||||
self.data = raw_data
|
|
||||||
self._type = 'unknown'
|
self._type = 'unknown'
|
||||||
|
msg = "Error encountered during UUID processing, "
|
||||||
|
msg += "the UUID will be treated as generic.\n\n{0}"
|
||||||
|
warnings.warn(msg.format(traceback.format_exc()))
|
||||||
|
|
||||||
if length == 0:
|
self.raw_data = raw_data
|
||||||
# Need to compute the length.
|
|
||||||
# The length is 8 (L and T fields) + 16 (length of UUID identifier)
|
|
||||||
# + length of uuid data.
|
|
||||||
length = 24 + len(self.data)
|
|
||||||
|
|
||||||
self.length = length
|
self.length = length
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
|
|
@ -2243,41 +2240,32 @@ class UUIDBox(Jp2kBox):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
msg = '{0}\n'
|
msg = '{0}\n'
|
||||||
msg += ' UUID: {1}{2}\n'
|
msg += ' UUID: {1} ({2})\n'
|
||||||
msg += ' UUID Data: {3}'
|
msg += ' UUID Data: {3}'
|
||||||
|
|
||||||
if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'):
|
|
||||||
uuid_type = ' (XMP)'
|
|
||||||
uuid_data = _pretty_print_xml(self.data)
|
|
||||||
elif self.uuid.bytes == b'JpgTiffExif->JP2':
|
|
||||||
uuid_type = ' (Exif)'
|
|
||||||
# 2.7 has trouble pretty-printing ordered dicts, so print them
|
|
||||||
# as regular dicts. Not ideal, but at least it's good on 3.3+.
|
|
||||||
if sys.hexversion < 0x03000000:
|
|
||||||
data = dict(self.data)
|
|
||||||
else:
|
|
||||||
data = self.data
|
|
||||||
uuid_data = '\n' + pprint.pformat(data)
|
|
||||||
else:
|
|
||||||
uuid_type = ''
|
|
||||||
uuid_data = '{0} bytes'.format(len(self.data))
|
|
||||||
|
|
||||||
msg = msg.format(Jp2kBox.__str__(self),
|
msg = msg.format(Jp2kBox.__str__(self),
|
||||||
self.uuid,
|
self.uuid,
|
||||||
uuid_type,
|
self._type,
|
||||||
uuid_data)
|
str(self.data))
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
def write(self, fptr):
|
def write(self, fptr):
|
||||||
"""Write a UUID box box to file.
|
"""Write a UUID box box to file.
|
||||||
"""
|
"""
|
||||||
if self._type != 'XMP':
|
if self._type != 'XMP':
|
||||||
msg = "Only XMP UUID boxes can currently be written."
|
msg = "Only XMP UUID boxes can currently be written."
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
read_buffer = struct.pack('>I4s', self.length, 'uuid')
|
serialized = b'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>'
|
||||||
|
serialized += ET.tostring(self.data.packet.getroot(), encoding='utf-8')
|
||||||
|
serialized += b'<?xpacket end="w"?>'
|
||||||
|
if self.length == 0:
|
||||||
|
self.length = 24 + len(serialized)
|
||||||
|
read_buffer = struct.pack('>I4s', self.length, b'uuid')
|
||||||
fptr.write(read_buffer)
|
fptr.write(read_buffer)
|
||||||
fptr.write(self.data)
|
fptr.write(self.uuid.bytes)
|
||||||
|
fptr.write(serialized)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse(fptr, offset, length):
|
def parse(fptr, offset, length):
|
||||||
|
|
@ -2306,511 +2294,6 @@ class UUIDBox(Jp2kBox):
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
|
||||||
class Exif(object):
|
|
||||||
"""
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
read_buffer : bytes
|
|
||||||
Raw byte stream consisting of the UUID data.
|
|
||||||
endian : str
|
|
||||||
Either '<' for big-endian, or '>' for little-endian.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, read_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.read_buffer = read_buffer
|
|
||||||
|
|
||||||
# Ignore the first six bytes.
|
|
||||||
# Next 8 should be (73, 73, 42, 8)
|
|
||||||
data = struct.unpack('<BBHI', read_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, read_buffer[6:], offset)
|
|
||||||
self.exif_image = exif.processed_ifd
|
|
||||||
|
|
||||||
if 'ExifTag' in self.exif_image.keys():
|
|
||||||
offset = self.exif_image['ExifTag']
|
|
||||||
photo = _ExifPhotoIfd(self.endian, read_buffer[6:], offset)
|
|
||||||
self.exif_photo = photo.processed_ifd
|
|
||||||
|
|
||||||
if 'InteroperabilityTag' in self.exif_photo.keys():
|
|
||||||
offset = self.exif_photo['InteroperabilityTag']
|
|
||||||
interop = _ExifInteroperabilityIfd(self.endian,
|
|
||||||
read_buffer[6:],
|
|
||||||
offset)
|
|
||||||
self.iop = interop.processed_ifd
|
|
||||||
|
|
||||||
if 'GPSTag' in self.exif_image.keys():
|
|
||||||
offset = self.exif_image['GPSTag']
|
|
||||||
gps = _ExifGPSInfoIfd(self.endian, read_buffer[6:], offset)
|
|
||||||
self.exif_gpsinfo = gps.processed_ifd
|
|
||||||
|
|
||||||
|
|
||||||
class _Ifd(object):
|
|
||||||
"""
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
read_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.
|
|
||||||
processed_ifd : dictionary
|
|
||||||
Maps tag name 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, read_buffer, offset):
|
|
||||||
self.endian = endian
|
|
||||||
self.read_buffer = read_buffer
|
|
||||||
self.processed_ifd = OrderedDict()
|
|
||||||
|
|
||||||
self.num_tags, = struct.unpack(endian + 'H',
|
|
||||||
read_buffer[offset:offset + 2])
|
|
||||||
|
|
||||||
fmt = self.endian + 'HHII' * self.num_tags
|
|
||||||
ifd_buffer = read_buffer[offset + 2:offset + 2 + self.num_tags * 12]
|
|
||||||
data = struct.unpack(fmt, ifd_buffer)
|
|
||||||
self.raw_ifd = OrderedDict()
|
|
||||||
for j, tag in enumerate(data[0::4]):
|
|
||||||
# The offset to the tag offset/payload is the offset to the IFD
|
|
||||||
# plus 2 bytes for the number of tags plus 12 bytes for each
|
|
||||||
# tag entry plus 8 bytes to the offset/payload itself.
|
|
||||||
toffp = read_buffer[offset + 10 + j * 12:offset + 10 + j * 12 + 4]
|
|
||||||
tag_data = self.parse_tag(data[j * 4 + 1],
|
|
||||||
data[j * 4 + 2],
|
|
||||||
toffp)
|
|
||||||
self.raw_ifd[tag] = tag_data
|
|
||||||
|
|
||||||
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.read_buffer[offset:offset + payload_size]
|
|
||||||
|
|
||||||
if dtype == 2:
|
|
||||||
# ASCII
|
|
||||||
if sys.hexversion < 0x03000000:
|
|
||||||
payload = target_buffer.rstrip('\x00')
|
|
||||||
else:
|
|
||||||
payload = target_buffer.decode('utf-8').rstrip('\x00')
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def post_process(self, tagnum2name):
|
|
||||||
"""Map the tag name instead of tag number to the tag value.
|
|
||||||
"""
|
|
||||||
for tag, value in self.raw_ifd.items():
|
|
||||||
try:
|
|
||||||
tag_name = tagnum2name[tag]
|
|
||||||
except KeyError:
|
|
||||||
# Ok, we don't recognize this tag. Just use the numeric id.
|
|
||||||
msg = 'Unrecognized Exif tag "{0}".'.format(tag)
|
|
||||||
warnings.warn(msg, UserWarning)
|
|
||||||
tag_name = tag
|
|
||||||
self.processed_ifd[tag_name] = value
|
|
||||||
|
|
||||||
|
|
||||||
class _ExifImageIfd(_Ifd):
|
|
||||||
"""
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
tagnum2name : dict
|
|
||||||
Maps Exif image tag numbers to the tag names.
|
|
||||||
ifd : dict
|
|
||||||
Maps tag names to tag values.
|
|
||||||
"""
|
|
||||||
tagnum2name = {11: 'ProcessingSoftware',
|
|
||||||
254: 'NewSubfileType',
|
|
||||||
255: 'SubfileType',
|
|
||||||
256: 'ImageWidth',
|
|
||||||
257: 'ImageLength',
|
|
||||||
258: 'BitsPerSample',
|
|
||||||
259: 'Compression',
|
|
||||||
262: 'PhotometricInterpretation',
|
|
||||||
263: 'Threshholding',
|
|
||||||
264: 'CellWidth',
|
|
||||||
265: 'CellLength',
|
|
||||||
266: 'FillOrder',
|
|
||||||
269: 'DocumentName',
|
|
||||||
270: 'ImageDescription',
|
|
||||||
271: 'Make',
|
|
||||||
272: 'Model',
|
|
||||||
273: 'StripOffsets',
|
|
||||||
274: 'Orientation',
|
|
||||||
277: 'SamplesPerPixel',
|
|
||||||
278: 'RowsPerStrip',
|
|
||||||
279: 'StripByteCounts',
|
|
||||||
282: 'XResolution',
|
|
||||||
283: 'YResolution',
|
|
||||||
284: 'PlanarConfiguration',
|
|
||||||
290: 'GrayResponseUnit',
|
|
||||||
291: 'GrayResponseCurve',
|
|
||||||
292: 'T4Options',
|
|
||||||
293: 'T6Options',
|
|
||||||
296: 'ResolutionUnit',
|
|
||||||
301: 'TransferFunction',
|
|
||||||
305: 'Software',
|
|
||||||
306: 'DateTime',
|
|
||||||
315: 'Artist',
|
|
||||||
316: 'HostComputer',
|
|
||||||
317: 'Predictor',
|
|
||||||
318: 'WhitePoint',
|
|
||||||
319: 'PrimaryChromaticities',
|
|
||||||
320: 'ColorMap',
|
|
||||||
321: 'HalftoneHints',
|
|
||||||
322: 'TileWidth',
|
|
||||||
323: 'TileLength',
|
|
||||||
324: 'TileOffsets',
|
|
||||||
325: 'TileByteCounts',
|
|
||||||
330: 'SubIFDs',
|
|
||||||
332: 'InkSet',
|
|
||||||
333: 'InkNames',
|
|
||||||
334: 'NumberOfInks',
|
|
||||||
336: 'DotRange',
|
|
||||||
337: 'TargetPrinter',
|
|
||||||
338: 'ExtraSamples',
|
|
||||||
339: 'SampleFormat',
|
|
||||||
340: 'SMinSampleValue',
|
|
||||||
341: 'SMaxSampleValue',
|
|
||||||
342: 'TransferRange',
|
|
||||||
343: 'ClipPath',
|
|
||||||
344: 'XClipPathUnits',
|
|
||||||
345: 'YClipPathUnits',
|
|
||||||
346: 'Indexed',
|
|
||||||
347: 'JPEGTables',
|
|
||||||
351: 'OPIProxy',
|
|
||||||
512: 'JPEGProc',
|
|
||||||
513: 'JPEGInterchangeFormat',
|
|
||||||
514: 'JPEGInterchangeFormatLength',
|
|
||||||
515: 'JPEGRestartInterval',
|
|
||||||
517: 'JPEGLosslessPredictors',
|
|
||||||
518: 'JPEGPointTransforms',
|
|
||||||
519: 'JPEGQTables',
|
|
||||||
520: 'JPEGDCTables',
|
|
||||||
521: 'JPEGACTables',
|
|
||||||
529: 'YCbCrCoefficients',
|
|
||||||
530: 'YCbCrSubSampling',
|
|
||||||
531: 'YCbCrPositioning',
|
|
||||||
532: 'ReferenceBlackWhite',
|
|
||||||
700: 'XMLPacket',
|
|
||||||
18246: 'Rating',
|
|
||||||
18249: 'RatingPercent',
|
|
||||||
32781: 'ImageID',
|
|
||||||
33421: 'CFARepeatPatternDim',
|
|
||||||
33422: 'CFAPattern',
|
|
||||||
33423: 'BatteryLevel',
|
|
||||||
33432: 'Copyright',
|
|
||||||
33434: 'ExposureTime',
|
|
||||||
33437: 'FNumber',
|
|
||||||
33723: 'IPTCNAA',
|
|
||||||
34377: 'ImageResources',
|
|
||||||
34665: 'ExifTag',
|
|
||||||
34675: 'InterColorProfile',
|
|
||||||
34850: 'ExposureProgram',
|
|
||||||
34852: 'SpectralSensitivity',
|
|
||||||
34853: 'GPSTag',
|
|
||||||
34855: 'ISOSpeedRatings',
|
|
||||||
34856: 'OECF',
|
|
||||||
34857: 'Interlace',
|
|
||||||
34858: 'TimeZoneOffset',
|
|
||||||
34859: 'SelfTimerMode',
|
|
||||||
36867: 'DateTimeOriginal',
|
|
||||||
37122: 'CompressedBitsPerPixel',
|
|
||||||
37377: 'ShutterSpeedValue',
|
|
||||||
37378: 'ApertureValue',
|
|
||||||
37379: 'BrightnessValue',
|
|
||||||
37380: 'ExposureBiasValue',
|
|
||||||
37381: 'MaxApertureValue',
|
|
||||||
37382: 'SubjectDistance',
|
|
||||||
37383: 'MeteringMode',
|
|
||||||
37384: 'LightSource',
|
|
||||||
37385: 'Flash',
|
|
||||||
37386: 'FocalLength',
|
|
||||||
37387: 'FlashEnergy',
|
|
||||||
37388: 'SpatialFrequencyResponse',
|
|
||||||
37389: 'Noise',
|
|
||||||
37390: 'FocalPlaneXResolution',
|
|
||||||
37391: 'FocalPlaneYResolution',
|
|
||||||
37392: 'FocalPlaneResolutionUnit',
|
|
||||||
37393: 'ImageNumber',
|
|
||||||
37394: 'SecurityClassification',
|
|
||||||
37395: 'ImageHistory',
|
|
||||||
37396: 'SubjectLocation',
|
|
||||||
37397: 'ExposureIndex',
|
|
||||||
37398: 'TIFFEPStandardID',
|
|
||||||
37399: 'SensingMethod',
|
|
||||||
40091: 'XPTitle',
|
|
||||||
40092: 'XPComment',
|
|
||||||
40093: 'XPAuthor',
|
|
||||||
40094: 'XPKeywords',
|
|
||||||
40095: 'XPSubject',
|
|
||||||
50341: 'PrintImageMatching',
|
|
||||||
50706: 'DNGVersion',
|
|
||||||
50707: 'DNGBackwardVersion',
|
|
||||||
50708: 'UniqueCameraModel',
|
|
||||||
50709: 'LocalizedCameraModel',
|
|
||||||
50710: 'CFAPlaneColor',
|
|
||||||
50711: 'CFALayout',
|
|
||||||
50712: 'LinearizationTable',
|
|
||||||
50713: 'BlackLevelRepeatDim',
|
|
||||||
50714: 'BlackLevel',
|
|
||||||
50715: 'BlackLevelDeltaH',
|
|
||||||
50716: 'BlackLevelDeltaV',
|
|
||||||
50717: 'WhiteLevel',
|
|
||||||
50718: 'DefaultScale',
|
|
||||||
50719: 'DefaultCropOrigin',
|
|
||||||
50720: 'DefaultCropSize',
|
|
||||||
50721: 'ColorMatrix1',
|
|
||||||
50722: 'ColorMatrix2',
|
|
||||||
50723: 'CameraCalibration1',
|
|
||||||
50724: 'CameraCalibration2',
|
|
||||||
50725: 'ReductionMatrix1',
|
|
||||||
50726: 'ReductionMatrix2',
|
|
||||||
50727: 'AnalogBalance',
|
|
||||||
50728: 'AsShotNeutral',
|
|
||||||
50729: 'AsShotWhiteXY',
|
|
||||||
50730: 'BaselineExposure',
|
|
||||||
50731: 'BaselineNoise',
|
|
||||||
50732: 'BaselineSharpness',
|
|
||||||
50733: 'BayerGreenSplit',
|
|
||||||
50734: 'LinearResponseLimit',
|
|
||||||
50735: 'CameraSerialNumber',
|
|
||||||
50736: 'LensInfo',
|
|
||||||
50737: 'ChromaBlurRadius',
|
|
||||||
50738: 'AntiAliasStrength',
|
|
||||||
50739: 'ShadowScale',
|
|
||||||
50740: 'DNGPrivateData',
|
|
||||||
50741: 'MakerNoteSafety',
|
|
||||||
50778: 'CalibrationIlluminant1',
|
|
||||||
50779: 'CalibrationIlluminant2',
|
|
||||||
50780: 'BestQualityScale',
|
|
||||||
50781: 'RawDataUniqueID',
|
|
||||||
50827: 'OriginalRawFileName',
|
|
||||||
50828: 'OriginalRawFileData',
|
|
||||||
50829: 'ActiveArea',
|
|
||||||
50830: 'MaskedAreas',
|
|
||||||
50831: 'AsShotICCProfile',
|
|
||||||
50832: 'AsShotPreProfileMatrix',
|
|
||||||
50833: 'CurrentICCProfile',
|
|
||||||
50834: 'CurrentPreProfileMatrix',
|
|
||||||
50879: 'ColorimetricReference',
|
|
||||||
50931: 'CameraCalibrationSignature',
|
|
||||||
50932: 'ProfileCalibrationSignature',
|
|
||||||
50934: 'AsShotProfileName',
|
|
||||||
50935: 'NoiseReductionApplied',
|
|
||||||
50936: 'ProfileName',
|
|
||||||
50937: 'ProfileHueSatMapDims',
|
|
||||||
50938: 'ProfileHueSatMapData1',
|
|
||||||
50939: 'ProfileHueSatMapData2',
|
|
||||||
50940: 'ProfileToneCurve',
|
|
||||||
50941: 'ProfileEmbedPolicy',
|
|
||||||
50942: 'ProfileCopyright',
|
|
||||||
50964: 'ForwardMatrix1',
|
|
||||||
50965: 'ForwardMatrix2',
|
|
||||||
50966: 'PreviewApplicationName',
|
|
||||||
50967: 'PreviewApplicationVersion',
|
|
||||||
50968: 'PreviewSettingsName',
|
|
||||||
50969: 'PreviewSettingsDigest',
|
|
||||||
50970: 'PreviewColorSpace',
|
|
||||||
50971: 'PreviewDateTime',
|
|
||||||
50972: 'RawImageDigest',
|
|
||||||
50973: 'OriginalRawFileDigest',
|
|
||||||
50974: 'SubTileBlockSize',
|
|
||||||
50975: 'RowInterleaveFactor',
|
|
||||||
50981: 'ProfileLookTableDims',
|
|
||||||
50982: 'ProfileLookTableData',
|
|
||||||
51008: 'OpcodeList1',
|
|
||||||
51009: 'OpcodeList2',
|
|
||||||
51022: 'OpcodeList3',
|
|
||||||
51041: 'NoiseProfile'}
|
|
||||||
|
|
||||||
def __init__(self, endian, read_buffer, offset):
|
|
||||||
_Ifd.__init__(self, endian, read_buffer, offset)
|
|
||||||
self.post_process(self.tagnum2name)
|
|
||||||
|
|
||||||
|
|
||||||
class _ExifPhotoIfd(_Ifd):
|
|
||||||
"""Represents tags found in the Exif sub ifd.
|
|
||||||
"""
|
|
||||||
tagnum2name = {33434: 'ExposureTime',
|
|
||||||
33437: 'FNumber',
|
|
||||||
34850: 'ExposureProgram',
|
|
||||||
34852: 'SpectralSensitivity',
|
|
||||||
34855: 'ISOSpeedRatings',
|
|
||||||
34856: 'OECF',
|
|
||||||
34864: 'SensitivityType',
|
|
||||||
34865: 'StandardOutputSensitivity',
|
|
||||||
34866: 'RecommendedExposureIndex',
|
|
||||||
34867: 'ISOSpeed',
|
|
||||||
34868: 'ISOSpeedLatitudeyyy',
|
|
||||||
34869: 'ISOSpeedLatitudezzz',
|
|
||||||
36864: 'ExifVersion',
|
|
||||||
36867: 'DateTimeOriginal',
|
|
||||||
36868: 'DateTimeDigitized',
|
|
||||||
37121: 'ComponentsConfiguration',
|
|
||||||
37122: 'CompressedBitsPerPixel',
|
|
||||||
37377: 'ShutterSpeedValue',
|
|
||||||
37378: 'ApertureValue',
|
|
||||||
37379: 'BrightnessValue',
|
|
||||||
37380: 'ExposureBiasValue',
|
|
||||||
37381: 'MaxApertureValue',
|
|
||||||
37382: 'SubjectDistance',
|
|
||||||
37383: 'MeteringMode',
|
|
||||||
37384: 'LightSource',
|
|
||||||
37385: 'Flash',
|
|
||||||
37386: 'FocalLength',
|
|
||||||
37396: 'SubjectArea',
|
|
||||||
37500: 'MakerNote',
|
|
||||||
37510: 'UserComment',
|
|
||||||
37520: 'SubSecTime',
|
|
||||||
37521: 'SubSecTimeOriginal',
|
|
||||||
37522: 'SubSecTimeDigitized',
|
|
||||||
40960: 'FlashpixVersion',
|
|
||||||
40961: 'ColorSpace',
|
|
||||||
40962: 'PixelXDimension',
|
|
||||||
40963: 'PixelYDimension',
|
|
||||||
40964: 'RelatedSoundFile',
|
|
||||||
40965: 'InteroperabilityTag',
|
|
||||||
41483: 'FlashEnergy',
|
|
||||||
41484: 'SpatialFrequencyResponse',
|
|
||||||
41486: 'FocalPlaneXResolution',
|
|
||||||
41487: 'FocalPlaneYResolution',
|
|
||||||
41488: 'FocalPlaneResolutionUnit',
|
|
||||||
41492: 'SubjectLocation',
|
|
||||||
41493: 'ExposureIndex',
|
|
||||||
41495: 'SensingMethod',
|
|
||||||
41728: 'FileSource',
|
|
||||||
41729: 'SceneType',
|
|
||||||
41730: 'CFAPattern',
|
|
||||||
41985: 'CustomRendered',
|
|
||||||
41986: 'ExposureMode',
|
|
||||||
41987: 'WhiteBalance',
|
|
||||||
41988: 'DigitalZoomRatio',
|
|
||||||
41989: 'FocalLengthIn35mmFilm',
|
|
||||||
41990: 'SceneCaptureType',
|
|
||||||
41991: 'GainControl',
|
|
||||||
41992: 'Contrast',
|
|
||||||
41993: 'Saturation',
|
|
||||||
41994: 'Sharpness',
|
|
||||||
41995: 'DeviceSettingDescription',
|
|
||||||
41996: 'SubjectDistanceRange',
|
|
||||||
42016: 'ImageUniqueID',
|
|
||||||
42032: 'CameraOwnerName',
|
|
||||||
42033: 'BodySerialNumber',
|
|
||||||
42034: 'LensSpecification',
|
|
||||||
42035: 'LensMake',
|
|
||||||
42036: 'LensModel',
|
|
||||||
42037: 'LensSerialNumber'}
|
|
||||||
|
|
||||||
def __init__(self, endian, read_buffer, offset):
|
|
||||||
_Ifd.__init__(self, endian, read_buffer, offset)
|
|
||||||
self.post_process(self.tagnum2name)
|
|
||||||
|
|
||||||
|
|
||||||
class _ExifGPSInfoIfd(_Ifd):
|
|
||||||
"""Represents information found in the GPSInfo sub 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, read_buffer, offset):
|
|
||||||
_Ifd.__init__(self, endian, read_buffer, offset)
|
|
||||||
self.post_process(self.tagnum2name)
|
|
||||||
|
|
||||||
|
|
||||||
class _ExifInteroperabilityIfd(_Ifd):
|
|
||||||
"""Represents tags found in the Interoperability sub IFD.
|
|
||||||
"""
|
|
||||||
tagnum2name = {1: 'InteroperabilityIndex',
|
|
||||||
2: 'InteroperabilityVersion',
|
|
||||||
4096: 'RelatedImageFileFormat',
|
|
||||||
4097: 'RelatedImageWidth',
|
|
||||||
4098: 'RelatedImageLength'}
|
|
||||||
|
|
||||||
def __init__(self, endian, read_buffer, offset):
|
|
||||||
_Ifd.__init__(self, endian, read_buffer, offset)
|
|
||||||
self.post_process(self.tagnum2name)
|
|
||||||
|
|
||||||
|
|
||||||
# Map each box ID to the corresponding class.
|
# Map each box ID to the corresponding class.
|
||||||
_BOX_WITH_ID = {
|
_BOX_WITH_ID = {
|
||||||
'asoc': AssociationBox,
|
'asoc': AssociationBox,
|
||||||
|
|
@ -2835,45 +2318,3 @@ _BOX_WITH_ID = {
|
||||||
'url ': DataEntryURLBox,
|
'url ': DataEntryURLBox,
|
||||||
'uuid': UUIDBox,
|
'uuid': UUIDBox,
|
||||||
'xml ': XMLBox}
|
'xml ': XMLBox}
|
||||||
|
|
||||||
|
|
||||||
def _indent(elem, level=0):
|
|
||||||
"""Recipe for pretty printing XML. Please see
|
|
||||||
|
|
||||||
http://effbot.org/zone/element-lib.htm#prettyprint
|
|
||||||
"""
|
|
||||||
i = "\n" + level * " "
|
|
||||||
if len(elem):
|
|
||||||
if not elem.text or not elem.text.strip():
|
|
||||||
elem.text = i + " "
|
|
||||||
if not elem.tail or not elem.tail.strip():
|
|
||||||
elem.tail = i
|
|
||||||
for elem in elem:
|
|
||||||
_indent(elem, level + 1)
|
|
||||||
if not elem.tail or not elem.tail.strip():
|
|
||||||
elem.tail = i
|
|
||||||
else:
|
|
||||||
if level and (not elem.tail or not elem.tail.strip()):
|
|
||||||
elem.tail = i
|
|
||||||
|
|
||||||
|
|
||||||
def _pretty_print_xml(xml, level=0):
|
|
||||||
"""Pretty print XML data.
|
|
||||||
"""
|
|
||||||
xml = copy.deepcopy(xml)
|
|
||||||
_indent(xml.getroot(), level=level)
|
|
||||||
xmltext = ET.tostring(xml.getroot(), encoding='utf-8').decode('utf-8')
|
|
||||||
|
|
||||||
# Indent it a bit.
|
|
||||||
lst = [(' ' + x) for x in xmltext.split('\n')]
|
|
||||||
try:
|
|
||||||
xml = '\n'.join(lst)
|
|
||||||
return '\n{0}'.format(xml)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
# This can happen on python 2.x if the character set contains certain
|
|
||||||
# non-ascii characters. Just print out the corresponding xml char
|
|
||||||
# entities instead.
|
|
||||||
xml = u'\n'.join(lst)
|
|
||||||
text = u'\n{0}'.format(xml)
|
|
||||||
text = text.encode('ascii', 'xmlcharrefreplace')
|
|
||||||
return text
|
|
||||||
|
|
|
||||||
|
|
@ -1017,7 +1017,7 @@ class Jp2k(Jp2kBox):
|
||||||
>>> jp2 = glymur.Jp2k(jfile)
|
>>> jp2 = glymur.Jp2k(jfile)
|
||||||
>>> codestream = jp2.get_codestream()
|
>>> codestream = jp2.get_codestream()
|
||||||
>>> print(codestream.segment[1])
|
>>> print(codestream.segment[1])
|
||||||
SIZ marker segment @ (3137, 47)
|
SIZ marker segment @ (3233, 47)
|
||||||
Profile: 2
|
Profile: 2
|
||||||
Reference Grid Height, Width: (1456 x 2592)
|
Reference Grid Height, Width: (1456 x 2592)
|
||||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||||
|
|
|
||||||
|
|
@ -167,3 +167,90 @@ def read_pgx_header(pgx_file):
|
||||||
|
|
||||||
header = header.rstrip()
|
header = header.rstrip()
|
||||||
return header, pos
|
return header, pos
|
||||||
|
|
||||||
|
nemo_xmp_box = """UUID Box (uuid) @ (77, 3146)
|
||||||
|
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
|
||||||
|
UUID Data:
|
||||||
|
<ns0:xmpmeta xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:ns0="adobe:ns:meta/" xmlns:ns2="http://ns.adobe.com/xap/1.0/" xmlns:ns3="http://ns.adobe.com/tiff/1.0/" xmlns:ns4="http://ns.adobe.com/exif/1.0/" xmlns:ns5="http://ns.adobe.com/photoshop/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns0:xmptk="Exempi + XMP Core 5.1.2">
|
||||||
|
<rdf:RDF>
|
||||||
|
<rdf:Description rdf:about="">
|
||||||
|
<ns2:CreatorTool>Google</ns2:CreatorTool>
|
||||||
|
<ns2:CreateDate>2013-02-09T14:47:53</ns2:CreateDate>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="">
|
||||||
|
<ns3:YCbCrPositioning>1</ns3:YCbCrPositioning>
|
||||||
|
<ns3:XResolution>72/1</ns3:XResolution>
|
||||||
|
<ns3:YResolution>72/1</ns3:YResolution>
|
||||||
|
<ns3:ResolutionUnit>2</ns3:ResolutionUnit>
|
||||||
|
<ns3:Make>HTC</ns3:Make>
|
||||||
|
<ns3:Model>HTC Glacier</ns3:Model>
|
||||||
|
<ns3:ImageWidth>2592</ns3:ImageWidth>
|
||||||
|
<ns3:ImageLength>1456</ns3:ImageLength>
|
||||||
|
<ns3:BitsPerSample>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>8</rdf:li>
|
||||||
|
<rdf:li>8</rdf:li>
|
||||||
|
<rdf:li>8</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</ns3:BitsPerSample>
|
||||||
|
<ns3:PhotometricInterpretation>2</ns3:PhotometricInterpretation>
|
||||||
|
<ns3:SamplesPerPixel>3</ns3:SamplesPerPixel>
|
||||||
|
<ns3:WhitePoint>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>1343036288/4294967295</rdf:li>
|
||||||
|
<rdf:li>1413044224/4294967295</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</ns3:WhitePoint>
|
||||||
|
<ns3:PrimaryChromaticities>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>2748779008/4294967295</rdf:li>
|
||||||
|
<rdf:li>1417339264/4294967295</rdf:li>
|
||||||
|
<rdf:li>1288490240/4294967295</rdf:li>
|
||||||
|
<rdf:li>2576980480/4294967295</rdf:li>
|
||||||
|
<rdf:li>644245120/4294967295</rdf:li>
|
||||||
|
<rdf:li>257698032/4294967295</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</ns3:PrimaryChromaticities>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="">
|
||||||
|
<ns4:ColorSpace>1</ns4:ColorSpace>
|
||||||
|
<ns4:PixelXDimension>2528</ns4:PixelXDimension>
|
||||||
|
<ns4:PixelYDimension>1424</ns4:PixelYDimension>
|
||||||
|
<ns4:FocalLength>353/100</ns4:FocalLength>
|
||||||
|
<ns4:GPSAltitudeRef>0</ns4:GPSAltitudeRef>
|
||||||
|
<ns4:GPSAltitude>0/1</ns4:GPSAltitude>
|
||||||
|
<ns4:GPSMapDatum>WGS-84</ns4:GPSMapDatum>
|
||||||
|
<ns4:DateTimeOriginal>2013-02-09T14:47:53</ns4:DateTimeOriginal>
|
||||||
|
<ns4:ISOSpeedRatings>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>76</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</ns4:ISOSpeedRatings>
|
||||||
|
<ns4:ExifVersion>0220</ns4:ExifVersion>
|
||||||
|
<ns4:FlashpixVersion>0100</ns4:FlashpixVersion>
|
||||||
|
<ns4:ComponentsConfiguration>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>1</rdf:li>
|
||||||
|
<rdf:li>2</rdf:li>
|
||||||
|
<rdf:li>3</rdf:li>
|
||||||
|
<rdf:li>0</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</ns4:ComponentsConfiguration>
|
||||||
|
<ns4:GPSLatitude>42,20.56N</ns4:GPSLatitude>
|
||||||
|
<ns4:GPSLongitude>71,5.29W</ns4:GPSLongitude>
|
||||||
|
<ns4:GPSTimeStamp>2013-02-09T19:47:53Z</ns4:GPSTimeStamp>
|
||||||
|
<ns4:GPSProcessingMethod>NETWORK</ns4:GPSProcessingMethod>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="">
|
||||||
|
<ns5:DateCreated>2013-02-09T14:47:53</ns5:DateCreated>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about="">
|
||||||
|
<dc:Creator>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>Glymur</rdf:li>
|
||||||
|
<rdf:li>Python XMP Toolkit</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</dc:Creator>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
||||||
|
</ns0:xmpmeta>"""
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,7 @@ class TestAppend(unittest.TestCase):
|
||||||
# The sequence of box IDs should be the same as before, but with an
|
# The sequence of box IDs should be the same as before, but with an
|
||||||
# xml box at the end.
|
# xml box at the end.
|
||||||
box_ids = [box.box_id for box in jp2.box]
|
box_ids = [box.box_id for box in jp2.box]
|
||||||
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml ']
|
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'jp2c', 'xml ']
|
||||||
self.assertEqual(box_ids, expected)
|
self.assertEqual(box_ids, expected)
|
||||||
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
||||||
b'<data>0</data>')
|
b'<data>0</data>')
|
||||||
|
|
@ -470,7 +470,7 @@ class TestAppend(unittest.TestCase):
|
||||||
# The sequence of box IDs should be the same as before, but with an
|
# The sequence of box IDs should be the same as before, but with an
|
||||||
# xml box at the end.
|
# xml box at the end.
|
||||||
box_ids = [box.box_id for box in jp2.box]
|
box_ids = [box.box_id for box in jp2.box]
|
||||||
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml ']
|
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'jp2c', 'xml ']
|
||||||
self.assertEqual(box_ids, expected)
|
self.assertEqual(box_ids, expected)
|
||||||
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
|
||||||
b'<data>0</data>')
|
b'<data>0</data>')
|
||||||
|
|
|
||||||
158
glymur/test/test_jp2box_uuid.py
Normal file
158
glymur/test/test_jp2box_uuid.py
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Test suite for printing.
|
||||||
|
"""
|
||||||
|
# C0302: don't care too much about having too many lines in a test module
|
||||||
|
# pylint: disable=C0302
|
||||||
|
|
||||||
|
# E061: unittest.mock introduced in 3.3 (python-2.7/pylint issue)
|
||||||
|
# pylint: disable=E0611,F0401
|
||||||
|
|
||||||
|
# R0904: Not too many methods in unittest.
|
||||||
|
# pylint: disable=R0904
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import warnings
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
|
||||||
|
if sys.hexversion < 0x02070000:
|
||||||
|
import unittest2 as unittest
|
||||||
|
else:
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
if sys.hexversion < 0x03000000:
|
||||||
|
from StringIO import StringIO
|
||||||
|
else:
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
if sys.hexversion <= 0x03030000:
|
||||||
|
from mock import patch
|
||||||
|
else:
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import glymur
|
||||||
|
from glymur import Jp2k
|
||||||
|
from .fixtures import OPJ_DATA_ROOT, opj_data_file, nemo_xmp_box
|
||||||
|
|
||||||
|
|
||||||
|
class TestUUIDExif(unittest.TestCase):
|
||||||
|
"""Tests for UUIDs of Exif type."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.jp2file = glymur.data.nemo()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Requires assertWarns, 3.2+")
|
||||||
|
def test_unrecognized_exif_tag(self):
|
||||||
|
"""Verify warning in case of unrecognized tag."""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
|
||||||
|
|
||||||
|
with open(self.jp2file, 'rb') as ifptr:
|
||||||
|
tfile.write(ifptr.read())
|
||||||
|
|
||||||
|
# Write L, T, UUID identifier.
|
||||||
|
tfile.write(struct.pack('>I4s', 52, b'uuid'))
|
||||||
|
tfile.write(b'JpgTiffExif->JP2')
|
||||||
|
|
||||||
|
tfile.write(b'Exif\x00\x00')
|
||||||
|
xbuffer = struct.pack('<BBHI', 73, 73, 42, 8)
|
||||||
|
tfile.write(xbuffer)
|
||||||
|
|
||||||
|
# We will write just a single tag.
|
||||||
|
tfile.write(struct.pack('<H', 1))
|
||||||
|
|
||||||
|
# The "Make" tag is tag no. 271. Corrupt it to 171.
|
||||||
|
tfile.write(struct.pack('<HHI4s', 171, 2, 3, b'HTC\x00'))
|
||||||
|
tfile.flush()
|
||||||
|
|
||||||
|
with self.assertWarns(UserWarning):
|
||||||
|
j = glymur.Jp2k(tfile.name)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Requires assertWarns, 3.2+")
|
||||||
|
def test_bad_tag_datatype(self):
|
||||||
|
"""Only certain datatypes are allowable"""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
|
||||||
|
|
||||||
|
with open(self.jp2file, 'rb') as ifptr:
|
||||||
|
tfile.write(ifptr.read())
|
||||||
|
|
||||||
|
# Write L, T, UUID identifier.
|
||||||
|
tfile.write(struct.pack('>I4s', 52, b'uuid'))
|
||||||
|
tfile.write(b'JpgTiffExif->JP2')
|
||||||
|
|
||||||
|
tfile.write(b'Exif\x00\x00')
|
||||||
|
xbuffer = struct.pack('<BBHI', 73, 73, 42, 8)
|
||||||
|
tfile.write(xbuffer)
|
||||||
|
|
||||||
|
# We will write just a single tag.
|
||||||
|
tfile.write(struct.pack('<H', 1))
|
||||||
|
|
||||||
|
# 2000 is not an allowable TIFF datatype.
|
||||||
|
tfile.write(struct.pack('<HHI4s', 271, 2000, 3, b'HTC\x00'))
|
||||||
|
tfile.flush()
|
||||||
|
|
||||||
|
with self.assertWarns(UserWarning):
|
||||||
|
j = glymur.Jp2k(tfile.name)
|
||||||
|
|
||||||
|
self.assertEqual(j.box[-1].box_id, 'uuid')
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Requires assertWarns, 3.2+")
|
||||||
|
def test_bad_tiff_header_byte_order_indication(self):
|
||||||
|
"""Only b'II' and b'MM' are allowed."""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
|
||||||
|
|
||||||
|
with open(self.jp2file, 'rb') as ifptr:
|
||||||
|
tfile.write(ifptr.read())
|
||||||
|
|
||||||
|
# Write L, T, UUID identifier.
|
||||||
|
tfile.write(struct.pack('>I4s', 52, b'uuid'))
|
||||||
|
tfile.write(b'JpgTiffExif->JP2')
|
||||||
|
|
||||||
|
tfile.write(b'Exif\x00\x00')
|
||||||
|
xbuffer = struct.pack('<BBHI', 74, 73, 42, 8)
|
||||||
|
tfile.write(xbuffer)
|
||||||
|
|
||||||
|
# We will write just a single tag.
|
||||||
|
tfile.write(struct.pack('<H', 1))
|
||||||
|
|
||||||
|
# 271 is the Make.
|
||||||
|
tfile.write(struct.pack('<HHI4s', 271, 2, 3, b'HTC\x00'))
|
||||||
|
tfile.flush()
|
||||||
|
|
||||||
|
with self.assertWarns(UserWarning):
|
||||||
|
j = glymur.Jp2k(tfile.name)
|
||||||
|
|
||||||
|
self.assertEqual(j.box[-1].box_id, 'uuid')
|
||||||
|
|
||||||
|
def test_big_endian(self):
|
||||||
|
"""Verify read of big-endian IFD."""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
|
||||||
|
|
||||||
|
with open(self.jp2file, 'rb') as ifptr:
|
||||||
|
tfile.write(ifptr.read())
|
||||||
|
|
||||||
|
# Write L, T, UUID identifier.
|
||||||
|
tfile.write(struct.pack('>I4s', 52, b'uuid'))
|
||||||
|
tfile.write(b'JpgTiffExif->JP2')
|
||||||
|
|
||||||
|
tfile.write(b'Exif\x00\x00')
|
||||||
|
xbuffer = struct.pack('>BBHI', 77, 77, 42, 8)
|
||||||
|
tfile.write(xbuffer)
|
||||||
|
|
||||||
|
# We will write just a single tag.
|
||||||
|
tfile.write(struct.pack('>H', 1))
|
||||||
|
|
||||||
|
# The "Make" tag is tag no. 271.
|
||||||
|
tfile.write(struct.pack('>HHI4s', 271, 2, 3, b'HTC\x00'))
|
||||||
|
tfile.flush()
|
||||||
|
|
||||||
|
jp2 = glymur.Jp2k(tfile.name)
|
||||||
|
self.assertEqual(jp2.box[-1].data.ifds['Image']['Make'], "HTC")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -109,7 +109,7 @@ class TestJp2k(unittest.TestCase):
|
||||||
jp2k = Jp2k(self.jp2file)
|
jp2k = Jp2k(self.jp2file)
|
||||||
|
|
||||||
# top-level boxes
|
# top-level boxes
|
||||||
self.assertEqual(len(jp2k.box), 6)
|
self.assertEqual(len(jp2k.box), 5)
|
||||||
|
|
||||||
self.assertEqual(jp2k.box[0].box_id, 'jP ')
|
self.assertEqual(jp2k.box[0].box_id, 'jP ')
|
||||||
self.assertEqual(jp2k.box[0].offset, 0)
|
self.assertEqual(jp2k.box[0].offset, 0)
|
||||||
|
|
@ -128,15 +128,11 @@ class TestJp2k(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(jp2k.box[3].box_id, 'uuid')
|
self.assertEqual(jp2k.box[3].box_id, 'uuid')
|
||||||
self.assertEqual(jp2k.box[3].offset, 77)
|
self.assertEqual(jp2k.box[3].offset, 77)
|
||||||
self.assertEqual(jp2k.box[3].length, 638)
|
self.assertEqual(jp2k.box[3].length, 3146)
|
||||||
|
|
||||||
self.assertEqual(jp2k.box[4].box_id, 'uuid')
|
self.assertEqual(jp2k.box[4].box_id, 'jp2c')
|
||||||
self.assertEqual(jp2k.box[4].offset, 715)
|
self.assertEqual(jp2k.box[4].offset, 3223)
|
||||||
self.assertEqual(jp2k.box[4].length, 2412)
|
self.assertEqual(jp2k.box[4].length, 1132296)
|
||||||
|
|
||||||
self.assertEqual(jp2k.box[5].box_id, 'jp2c')
|
|
||||||
self.assertEqual(jp2k.box[5].offset, 3127)
|
|
||||||
self.assertEqual(jp2k.box[5].length, 1132296)
|
|
||||||
|
|
||||||
# jp2h super box
|
# jp2h super box
|
||||||
self.assertEqual(len(jp2k.box[2].box), 2)
|
self.assertEqual(len(jp2k.box[2].box), 2)
|
||||||
|
|
@ -178,7 +174,7 @@ class TestJp2k(unittest.TestCase):
|
||||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
||||||
with open(self.jp2file, 'rb') as ifile:
|
with open(self.jp2file, 'rb') as ifile:
|
||||||
# Everything up until the jp2c box.
|
# Everything up until the jp2c box.
|
||||||
write_buffer = ifile.read(3127)
|
write_buffer = ifile.read(3223)
|
||||||
tfile.write(write_buffer)
|
tfile.write(write_buffer)
|
||||||
|
|
||||||
# The L field must be 1 in order to signal the presence of the
|
# The L field must be 1 in order to signal the presence of the
|
||||||
|
|
@ -199,9 +195,9 @@ class TestJp2k(unittest.TestCase):
|
||||||
|
|
||||||
jp2k = Jp2k(tfile.name)
|
jp2k = Jp2k(tfile.name)
|
||||||
|
|
||||||
self.assertEqual(jp2k.box[5].box_id, 'jp2c')
|
self.assertEqual(jp2k.box[4].box_id, 'jp2c')
|
||||||
self.assertEqual(jp2k.box[5].offset, 3127)
|
self.assertEqual(jp2k.box[4].offset, 3223)
|
||||||
self.assertEqual(jp2k.box[5].length, 1133427 + 8)
|
self.assertEqual(jp2k.box[4].length, 1133427 + 8)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
|
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
|
||||||
def test_length_field_is_zero(self):
|
def test_length_field_is_zero(self):
|
||||||
|
|
@ -366,45 +362,12 @@ class TestJp2k(unittest.TestCase):
|
||||||
def test_xmp_attribute(self):
|
def test_xmp_attribute(self):
|
||||||
"""Verify the XMP packet in the shipping example file can be read."""
|
"""Verify the XMP packet in the shipping example file can be read."""
|
||||||
j = Jp2k(self.jp2file)
|
j = Jp2k(self.jp2file)
|
||||||
xmp = j.box[4].data
|
xmp = j.box[3].data.packet
|
||||||
ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
|
ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
|
||||||
ns1 = '{http://ns.adobe.com/xap/1.0/}'
|
ns2 = '{http://ns.adobe.com/xap/1.0/}'
|
||||||
name = '{0}RDF/{0}Description'.format(ns0)
|
name = '{0}RDF/{0}Description/{1}CreatorTool'.format(ns0, ns2)
|
||||||
elt = xmp.find(name)
|
elt = xmp.find(name)
|
||||||
attr_value = elt.attrib['{0}CreatorTool'.format(ns1)]
|
self.assertEqual(elt.text, 'Google')
|
||||||
self.assertEqual(attr_value, 'glymur')
|
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
|
|
||||||
def test_unrecognized_exif_tag(self):
|
|
||||||
"""An unrecognized exif tag should be handled gracefully."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
|
||||||
shutil.copyfile(self.jp2file, tfile.name)
|
|
||||||
|
|
||||||
# The Exif UUID starts at byte 77. There are 8 bytes for the L and
|
|
||||||
# T fields, then 16 bytes for the UUID identifier, then 6 exif
|
|
||||||
# header bytes, then 8 bytes for the TIFF header, then 2 bytes
|
|
||||||
# the the Image IFD number of tags, where we finally find the first
|
|
||||||
# tag, "Make" (271). We'll corrupt it by changing it into 171,
|
|
||||||
# which does not correspond to any known Exif Image tag.
|
|
||||||
with open(tfile.name, 'r+b') as fptr:
|
|
||||||
fptr.seek(117)
|
|
||||||
write_buffer = struct.pack('<H', int(171))
|
|
||||||
fptr.write(write_buffer)
|
|
||||||
|
|
||||||
# Verify that a warning is issued, but only on python3.
|
|
||||||
# On python2, just suppress the warning.
|
|
||||||
if sys.hexversion < 0x03030000:
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("ignore")
|
|
||||||
j = Jp2k(tfile.name)
|
|
||||||
else:
|
|
||||||
with self.assertWarns(UserWarning):
|
|
||||||
j = Jp2k(tfile.name)
|
|
||||||
|
|
||||||
exif = j.box[3].data
|
|
||||||
# Were the tag == 271, 'Make' would be in the keys instead.
|
|
||||||
self.assertTrue(171 in exif['Image'].keys())
|
|
||||||
self.assertFalse('Make' in exif['Image'].keys())
|
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
|
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
|
||||||
|
|
@ -742,19 +705,22 @@ class TestJp2k_2_1(unittest.TestCase):
|
||||||
with open(self.jp2file, 'rb') as fptr:
|
with open(self.jp2file, 'rb') as fptr:
|
||||||
data = fptr.read()
|
data = fptr.read()
|
||||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
|
||||||
# Codestream starts at byte 3127. SIZ marker at 3137.
|
# Codestream starts at byte 3323. SIZ marker at 3233.
|
||||||
# COD marker at 3186. Subsampling at 3180.
|
# COD marker at 3282. Subsampling at 3276.
|
||||||
tfile.write(data[0:3179])
|
offset = 3223
|
||||||
|
tfile.write(data[0:offset+52])
|
||||||
|
|
||||||
# Make the DY bytes of the SIZ segment zero. That means that
|
# Make the DY bytes of the SIZ segment zero. That means that
|
||||||
# a subsampling factor is zero, which is illegal.
|
# a subsampling factor is zero, which is illegal.
|
||||||
tfile.write(b'\x00')
|
tfile.write(b'\x00')
|
||||||
tfile.write(data[3180:3182])
|
tfile.write(data[offset+53:offset+55])
|
||||||
tfile.write(b'\x00')
|
tfile.write(b'\x00')
|
||||||
tfile.write(data[3184:3186])
|
tfile.write(data[offset+57:offset+59])
|
||||||
|
#tfile.write(data[3184:3186])
|
||||||
tfile.write(b'\x00')
|
tfile.write(b'\x00')
|
||||||
|
|
||||||
tfile.write(data[3186:])
|
tfile.write(data[offset+59:])
|
||||||
|
#tfile.write(data[3186:])
|
||||||
tfile.flush()
|
tfile.flush()
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ else:
|
||||||
|
|
||||||
import glymur
|
import glymur
|
||||||
from glymur import Jp2k
|
from glymur import Jp2k
|
||||||
from .fixtures import OPJ_DATA_ROOT, opj_data_file
|
from .fixtures import OPJ_DATA_ROOT, opj_data_file, nemo_xmp_box
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||||
|
|
@ -230,7 +230,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[6])
|
print(codestream.segment[6])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['COC marker segment @ (3260, 9)',
|
lines = ['COC marker segment @ (3356, 9)',
|
||||||
' Associated component: 1',
|
' Associated component: 1',
|
||||||
' Coding style for this component: '
|
' Coding style for this component: '
|
||||||
+ 'Entropy coder, PARTITION = 0',
|
+ 'Entropy coder, PARTITION = 0',
|
||||||
|
|
@ -258,7 +258,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[2])
|
print(codestream.segment[2])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['COD marker segment @ (3186, 12)',
|
lines = ['COD marker segment @ (3282, 12)',
|
||||||
' Coding style:',
|
' Coding style:',
|
||||||
' Entropy coder, without partitions',
|
' Entropy coder, without partitions',
|
||||||
' SOP marker segments: False',
|
' SOP marker segments: False',
|
||||||
|
|
@ -424,7 +424,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[-1])
|
print(codestream.segment[-1])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['EOC marker segment @ (1135421, 0)']
|
lines = ['EOC marker segment @ (1135517, 0)']
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
|
@ -521,7 +521,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[7])
|
print(codestream.segment[7])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['QCC marker segment @ (3271, 8)',
|
lines = ['QCC marker segment @ (3367, 8)',
|
||||||
' Associated Component: 1',
|
' Associated Component: 1',
|
||||||
' Quantization style: no quantization, 2 guard bits',
|
' Quantization style: no quantization, 2 guard bits',
|
||||||
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
|
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
|
||||||
|
|
@ -537,7 +537,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[3])
|
print(codestream.segment[3])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['QCD marker segment @ (3200, 7)',
|
lines = ['QCD marker segment @ (3296, 7)',
|
||||||
' Quantization style: no quantization, 2 guard bits',
|
' Quantization style: no quantization, 2 guard bits',
|
||||||
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
|
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]']
|
||||||
|
|
||||||
|
|
@ -552,7 +552,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[1])
|
print(codestream.segment[1])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SIZ marker segment @ (3137, 47)',
|
lines = ['SIZ marker segment @ (3233, 47)',
|
||||||
' Profile: 2',
|
' Profile: 2',
|
||||||
' Reference Grid Height, Width: (1456 x 2592)',
|
' Reference Grid Height, Width: (1456 x 2592)',
|
||||||
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
||||||
|
|
@ -574,7 +574,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[0])
|
print(codestream.segment[0])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SOC marker segment @ (3135, 0)']
|
lines = ['SOC marker segment @ (3231, 0)']
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
|
@ -586,7 +586,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[10])
|
print(codestream.segment[10])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SOD marker segment @ (3302, 0)']
|
lines = ['SOD marker segment @ (3398, 0)']
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
|
@ -598,7 +598,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(codestream.segment[5])
|
print(codestream.segment[5])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SOT marker segment @ (3248, 10)',
|
lines = ['SOT marker segment @ (3344, 10)',
|
||||||
' Tile part index: 0',
|
' Tile part index: 0',
|
||||||
' Tile part length: 1132173',
|
' Tile part length: 1132173',
|
||||||
' Tile part instance: 0',
|
' Tile part instance: 0',
|
||||||
|
|
@ -632,22 +632,11 @@ class TestPrinting(unittest.TestCase):
|
||||||
"""Verify the printing of a UUID/XMP box."""
|
"""Verify the printing of a UUID/XMP box."""
|
||||||
j = glymur.Jp2k(self.jp2file)
|
j = glymur.Jp2k(self.jp2file)
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(j.box[4])
|
print(j.box[3])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lst = ['UUID Box (uuid) @ (715, 2412)',
|
expected = nemo_xmp_box
|
||||||
' UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)',
|
self.maxDiff = None
|
||||||
' UUID Data: ',
|
|
||||||
' <ns0:xmpmeta xmlns:ns0="adobe:ns:meta/" '
|
|
||||||
+ 'xmlns:ns2="http://ns.adobe.com/xap/1.0/" '
|
|
||||||
+ 'xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" '
|
|
||||||
+ 'ns0:xmptk="XMP Core 4.4.0-Exiv2">',
|
|
||||||
' <rdf:RDF>',
|
|
||||||
' <rdf:Description ns2:CreatorTool="glymur" '
|
|
||||||
+ 'rdf:about="" />',
|
|
||||||
' </rdf:RDF>',
|
|
||||||
' </ns0:xmpmeta>']
|
|
||||||
expected = '\n'.join(lst)
|
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
def test_codestream(self):
|
def test_codestream(self):
|
||||||
|
|
@ -657,8 +646,8 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(j.get_codestream())
|
print(j.get_codestream())
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
lst = ['Codestream:',
|
lst = ['Codestream:',
|
||||||
' SOC marker segment @ (3135, 0)',
|
' SOC marker segment @ (3231, 0)',
|
||||||
' SIZ marker segment @ (3137, 47)',
|
' SIZ marker segment @ (3233, 47)',
|
||||||
' Profile: 2',
|
' Profile: 2',
|
||||||
' Reference Grid Height, Width: (1456 x 2592)',
|
' Reference Grid Height, Width: (1456 x 2592)',
|
||||||
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
||||||
|
|
@ -668,7 +657,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
' Signed: (False, False, False)',
|
' Signed: (False, False, False)',
|
||||||
' Vertical, Horizontal Subsampling: '
|
' Vertical, Horizontal Subsampling: '
|
||||||
+ '((1, 1), (1, 1), (1, 1))',
|
+ '((1, 1), (1, 1), (1, 1))',
|
||||||
' COD marker segment @ (3186, 12)',
|
' COD marker segment @ (3282, 12)',
|
||||||
' Coding style:',
|
' Coding style:',
|
||||||
' Entropy coder, without partitions',
|
' Entropy coder, without partitions',
|
||||||
' SOP marker segments: False',
|
' SOP marker segments: False',
|
||||||
|
|
@ -690,11 +679,11 @@ class TestPrinting(unittest.TestCase):
|
||||||
' Vertically stripe causal context: False',
|
' Vertically stripe causal context: False',
|
||||||
' Predictable termination: False',
|
' Predictable termination: False',
|
||||||
' Segmentation symbols: False',
|
' Segmentation symbols: False',
|
||||||
' QCD marker segment @ (3200, 7)',
|
' QCD marker segment @ (3296, 7)',
|
||||||
' Quantization style: no quantization, '
|
' Quantization style: no quantization, '
|
||||||
+ '2 guard bits',
|
+ '2 guard bits',
|
||||||
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]',
|
' Step size: [(0, 8), (0, 9), (0, 9), (0, 10)]',
|
||||||
' CME marker segment @ (3209, 37)',
|
' CME marker segment @ (3305, 37)',
|
||||||
' "Created by OpenJPEG version 2.0.0"']
|
' "Created by OpenJPEG version 2.0.0"']
|
||||||
expected = '\n'.join(lst)
|
expected = '\n'.join(lst)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
@ -1036,7 +1025,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
print(jp2.box[4])
|
print(jp2.box[4])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
lines = ['UUID Box (uuid) @ (1544, 25)',
|
lines = ['UUID Box (uuid) @ (1544, 25)',
|
||||||
' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71',
|
' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71 (unknown)',
|
||||||
' UUID Data: 1 bytes']
|
' UUID Data: 1 bytes']
|
||||||
|
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
|
|
@ -1046,59 +1035,42 @@ class TestPrinting(unittest.TestCase):
|
||||||
"Ordered dicts not printing well in 2.7")
|
"Ordered dicts not printing well in 2.7")
|
||||||
def test_exif_uuid(self):
|
def test_exif_uuid(self):
|
||||||
"""Verify printing of exif information"""
|
"""Verify printing of exif information"""
|
||||||
j = glymur.Jp2k(self.jp2file)
|
with tempfile.NamedTemporaryFile(suffix='.jp2', mode='wb') as tfile:
|
||||||
|
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
with open(self.jp2file, 'rb') as ifptr:
|
||||||
print(j.box[3])
|
tfile.write(ifptr.read())
|
||||||
actual = fake_out.getvalue().strip()
|
|
||||||
|
|
||||||
lines = ["UUID Box (uuid) @ (77, 638)",
|
# Write L, T, UUID identifier.
|
||||||
|
tfile.write(struct.pack('>I4s', 76, b'uuid'))
|
||||||
|
tfile.write(b'JpgTiffExif->JP2')
|
||||||
|
|
||||||
|
tfile.write(b'Exif\x00\x00')
|
||||||
|
xbuffer = struct.pack('<BBHI', 73, 73, 42, 8)
|
||||||
|
tfile.write(xbuffer)
|
||||||
|
|
||||||
|
# We will write just three tags.
|
||||||
|
tfile.write(struct.pack('<H', 3))
|
||||||
|
|
||||||
|
# The "Make" tag is tag no. 271.
|
||||||
|
tfile.write(struct.pack('<HHII', 256, 4, 1, 256))
|
||||||
|
tfile.write(struct.pack('<HHII', 257, 4, 1, 512))
|
||||||
|
tfile.write(struct.pack('<HHI4s', 271, 2, 3, b'HTC\x00'))
|
||||||
|
tfile.flush()
|
||||||
|
|
||||||
|
j = glymur.Jp2k(tfile.name)
|
||||||
|
|
||||||
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
|
print(j.box[5])
|
||||||
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
|
lines = ["UUID Box (uuid) @ (1135519, 76)",
|
||||||
" UUID: 4a706754-6966-6645-7869-662d3e4a5032 (Exif)",
|
" UUID: 4a706754-6966-6645-7869-662d3e4a5032 (Exif)",
|
||||||
" UUID Data: ",
|
" UUID Data: ",
|
||||||
"{'Image': {'Make': 'HTC',",
|
"{'Image': {'ImageWidth': 256,",
|
||||||
" 'Model': 'HTC Glacier',",
|
" 'ImageLength': 512,",
|
||||||
" 'XResolution': 72.0,",
|
" 'Make': 'HTC'},",
|
||||||
" 'YResolution': 72.0,",
|
" 'Photo': None,",
|
||||||
" 'ResolutionUnit': 2,",
|
" 'GPSInfo': None,",
|
||||||
" 'YCbCrPositioning': 1,",
|
|
||||||
" 'ExifTag': 138,",
|
|
||||||
" 'GPSTag': 354},",
|
|
||||||
" 'Photo': {'ISOSpeedRatings': 76,",
|
|
||||||
" 'ExifVersion': (48, 50, 50, 48),",
|
|
||||||
" 'DateTimeOriginal': '2013:02:09 14:47:53',",
|
|
||||||
" 'DateTimeDigitized': '2013:02:09 14:47:53',",
|
|
||||||
" 'ComponentsConfiguration': (1, 2, 3, 0),",
|
|
||||||
" 'FocalLength': 3.53,",
|
|
||||||
" 'FlashpixVersion': (48, 49, 48, 48),",
|
|
||||||
" 'ColorSpace': 1,",
|
|
||||||
" 'PixelXDimension': 2528,",
|
|
||||||
" 'PixelYDimension': 1424,",
|
|
||||||
" 'InteroperabilityTag': 324},",
|
|
||||||
" 'GPSInfo': {'GPSVersionID': (2, 2, 0),",
|
|
||||||
" 'GPSLatitudeRef': 'N',",
|
|
||||||
" 'GPSLatitude': [42.0, 20.0, 33.61],",
|
|
||||||
" 'GPSLongitudeRef': 'W',",
|
|
||||||
" 'GPSLongitude': [71.0, 5.0, 17.32],",
|
|
||||||
" 'GPSAltitudeRef': 0,",
|
|
||||||
" 'GPSAltitude': 0.0,",
|
|
||||||
" 'GPSTimeStamp': [19.0, 47.0, 53.0],",
|
|
||||||
" 'GPSMapDatum': 'WGS-84',",
|
|
||||||
" 'GPSProcessingMethod': (65,",
|
|
||||||
" 83,",
|
|
||||||
" 67,",
|
|
||||||
" 73,",
|
|
||||||
" 73,",
|
|
||||||
" 0,",
|
|
||||||
" 0,",
|
|
||||||
" 0,",
|
|
||||||
" 78,",
|
|
||||||
" 69,",
|
|
||||||
" 84,",
|
|
||||||
" 87,",
|
|
||||||
" 79,",
|
|
||||||
" 82,",
|
|
||||||
" 75),",
|
|
||||||
" 'GPSDateStamp': '2013:02:09'},",
|
|
||||||
" 'Iop': None}"]
|
" 'Iop': None}"]
|
||||||
|
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue