diff --git a/CHANGES.txt b/CHANGES.txt index 0d23501..93aef33 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,7 @@ -Oct 22, 2013 - v0.5.7 Super box constructors now take optional box list - argument. Removed ssiz attribute from SIZsegment class. +Oct 24, 2013 - v0.6.0 Palette box palette changed to 2D numpy array. Removed + 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 some docstring errors in jp2box module. diff --git a/glymur/_uuid_io/Exif.py b/glymur/_uuid_io/Exif.py new file mode 100644 index 0000000..fb5e2a1 --- /dev/null +++ b/glymur/_uuid_io/Exif.py @@ -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('' 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) + + + diff --git a/glymur/_uuid_io/XMP.py b/glymur/_uuid_io/XMP.py new file mode 100644 index 0000000..0451a92 --- /dev/null +++ b/glymur/_uuid_io/XMP.py @@ -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) diff --git a/glymur/_uuid_io/__init__.py b/glymur/_uuid_io/__init__.py new file mode 100644 index 0000000..a23c2ce --- /dev/null +++ b/glymur/_uuid_io/__init__.py @@ -0,0 +1,6 @@ +""" +Sub package for handling various UUIDs. +""" +from .Exif import UUIDExif +from .XMP import UUIDXMP +from .generic import UUIDGeneric diff --git a/glymur/_uuid_io/generic.py b/glymur/_uuid_io/generic.py new file mode 100644 index 0000000..bad68a2 --- /dev/null +++ b/glymur/_uuid_io/generic.py @@ -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)) + diff --git a/glymur/core.py b/glymur/core.py index 22b5a19..620d0e3 100644 --- a/glymur/core.py +++ b/glymur/core.py @@ -1,5 +1,8 @@ """Core definitions to be shared amongst the modules. """ +import copy +import xml.etree.cElementTree as ET + # Progression order LRCP = 0 RLCP = 1 @@ -73,3 +76,45 @@ _CAPABILITIES_DISPLAY = { 1: '0', 2: '1', 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 diff --git a/glymur/data/nemo.jp2 b/glymur/data/nemo.jp2 index 55d199c..838583d 100644 Binary files a/glymur/data/nemo.jp2 and b/glymur/data/nemo.jp2 differ diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 627e164..c02f497 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -13,13 +13,13 @@ References # pylint: disable=C0302,R0903,R0913 -import copy import datetime import math import os import pprint import struct import sys +import traceback import uuid import warnings 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 ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD +from .core import _pretty_print_xml + +from . import _uuid_io _METHOD_DISPLAY = { ENUMERATED_COLORSPACE: 'enumerated colorspace', @@ -2192,8 +2195,11 @@ class UUIDBox(Jp2kBox): ---------- the_uuid : uuid.UUID 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 - This is the "payload" of data for the specified UUID. + Sequence of uninterpreted bytes as read from the file. length : int length of the box in bytes. offset : int @@ -2203,34 +2209,25 @@ class UUIDBox(Jp2kBox): self.uuid = the_uuid self.raw_data = raw_data - if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - # XMP data. Parse as XML. Seems to be a difference between - # ElementTree in version 2.7 and 3.3. - if sys.hexversion < 0x03000000: - elt = ET.fromstring(raw_data) + try: + if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): + self.data = _uuid_io.UUIDXMP(raw_data) + self._type = 'XMP' + elif the_uuid.bytes == b'JpgTiffExif->JP2': + self.data = _uuid_io.UUIDExif(raw_data) + self._type = 'Exif' else: - text = raw_data.decode('utf-8') - elt = ET.fromstring(text) - self.data = ET.ElementTree(elt) - self._type = 'XMP' - elif the_uuid.bytes == b'JpgTiffExif->JP2': - 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.data = _uuid_io.UUIDGeneric(raw_data) + self._type = 'unknown' + except Exception: + # In case of any exception, create the generic UUID. + self.data = _uuid_io.UUIDGeneric(raw_data) self._type = 'unknown' - - if length == 0: - # 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) + msg = "Error encountered during UUID processing, " + msg += "the UUID will be treated as generic.\n\n{0}" + warnings.warn(msg.format(traceback.format_exc())) + + self.raw_data = raw_data self.length = length self.offset = offset @@ -2243,41 +2240,32 @@ class UUIDBox(Jp2kBox): def __str__(self): msg = '{0}\n' - msg += ' UUID: {1}{2}\n' + msg += ' UUID: {1} ({2})\n' 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), self.uuid, - uuid_type, - uuid_data) + self._type, + str(self.data)) return msg + def write(self, fptr): """Write a UUID box box to file. """ if self._type != 'XMP': msg = "Only XMP UUID boxes can currently be written." raise NotImplementedError(msg) - read_buffer = struct.pack('>I4s', self.length, 'uuid') + serialized = b'' + serialized += ET.tostring(self.data.packet.getroot(), encoding='utf-8') + serialized += b'' + if self.length == 0: + self.length = 24 + len(serialized) + read_buffer = struct.pack('>I4s', self.length, b'uuid') fptr.write(read_buffer) - fptr.write(self.data) + fptr.write(self.uuid.bytes) + fptr.write(serialized) @staticmethod def parse(fptr, offset, length): @@ -2306,511 +2294,6 @@ class UUIDBox(Jp2kBox): 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('' 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. _BOX_WITH_ID = { 'asoc': AssociationBox, @@ -2835,45 +2318,3 @@ _BOX_WITH_ID = { 'url ': DataEntryURLBox, 'uuid': UUIDBox, '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 diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 6684e3e..4d7071e 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -1017,7 +1017,7 @@ class Jp2k(Jp2kBox): >>> jp2 = glymur.Jp2k(jfile) >>> codestream = jp2.get_codestream() >>> print(codestream.segment[1]) - SIZ marker segment @ (3137, 47) + SIZ marker segment @ (3233, 47) Profile: 2 Reference Grid Height, Width: (1456 x 2592) Vertical, Horizontal Reference Grid Offset: (0 x 0) diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index b872f1d..68c0706 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -167,3 +167,90 @@ def read_pgx_header(pgx_file): header = header.rstrip() return header, pos + +nemo_xmp_box = """UUID Box (uuid) @ (77, 3146) + UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) + UUID Data: + + + + Google + 2013-02-09T14:47:53 + + + 1 + 72/1 + 72/1 + 2 + HTC + HTC Glacier + 2592 + 1456 + + + 8 + 8 + 8 + + + 2 + 3 + + + 1343036288/4294967295 + 1413044224/4294967295 + + + + + 2748779008/4294967295 + 1417339264/4294967295 + 1288490240/4294967295 + 2576980480/4294967295 + 644245120/4294967295 + 257698032/4294967295 + + + + + 1 + 2528 + 1424 + 353/100 + 0 + 0/1 + WGS-84 + 2013-02-09T14:47:53 + + + 76 + + + 0220 + 0100 + + + 1 + 2 + 3 + 0 + + + 42,20.56N + 71,5.29W + 2013-02-09T19:47:53Z + NETWORK + + + 2013-02-09T14:47:53 + + + + + Glymur + Python XMP Toolkit + + + + + """ diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 844c56c..b83f86d 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -421,7 +421,7 @@ class TestAppend(unittest.TestCase): # The sequence of box IDs should be the same as before, but with an # xml box at the end. 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(ET.tostring(jp2.box[-1].xml.getroot()), b'0') @@ -470,7 +470,7 @@ class TestAppend(unittest.TestCase): # The sequence of box IDs should be the same as before, but with an # xml box at the end. 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(ET.tostring(jp2.box[-1].xml.getroot()), b'0') diff --git a/glymur/test/test_jp2box_uuid.py b/glymur/test/test_jp2box_uuid.py new file mode 100644 index 0000000..e8cb7be --- /dev/null +++ b/glymur/test/test_jp2box_uuid.py @@ -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('I4s', 52, b'uuid')) + tfile.write(b'JpgTiffExif->JP2') + + tfile.write(b'Exif\x00\x00') + xbuffer = struct.pack('I4s', 52, b'uuid')) + tfile.write(b'JpgTiffExif->JP2') + + tfile.write(b'Exif\x00\x00') + xbuffer = 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() diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 15da73d..c38cacd 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -109,7 +109,7 @@ class TestJp2k(unittest.TestCase): jp2k = Jp2k(self.jp2file) # 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].offset, 0) @@ -128,15 +128,11 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[3].box_id, 'uuid') 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].offset, 715) - self.assertEqual(jp2k.box[4].length, 2412) - - self.assertEqual(jp2k.box[5].box_id, 'jp2c') - self.assertEqual(jp2k.box[5].offset, 3127) - self.assertEqual(jp2k.box[5].length, 1132296) + self.assertEqual(jp2k.box[4].box_id, 'jp2c') + self.assertEqual(jp2k.box[4].offset, 3223) + self.assertEqual(jp2k.box[4].length, 1132296) # jp2h super box self.assertEqual(len(jp2k.box[2].box), 2) @@ -178,7 +174,7 @@ class TestJp2k(unittest.TestCase): with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: with open(self.jp2file, 'rb') as ifile: # Everything up until the jp2c box. - write_buffer = ifile.read(3127) + write_buffer = ifile.read(3223) tfile.write(write_buffer) # 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) - self.assertEqual(jp2k.box[5].box_id, 'jp2c') - self.assertEqual(jp2k.box[5].offset, 3127) - self.assertEqual(jp2k.box[5].length, 1133427 + 8) + self.assertEqual(jp2k.box[4].box_id, 'jp2c') + self.assertEqual(jp2k.box[4].offset, 3223) + self.assertEqual(jp2k.box[4].length, 1133427 + 8) @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") def test_length_field_is_zero(self): @@ -366,45 +362,12 @@ class TestJp2k(unittest.TestCase): def test_xmp_attribute(self): """Verify the XMP packet in the shipping example file can be read.""" 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#}' - ns1 = '{http://ns.adobe.com/xap/1.0/}' - name = '{0}RDF/{0}Description'.format(ns0) + ns2 = '{http://ns.adobe.com/xap/1.0/}' + name = '{0}RDF/{0}Description/{1}CreatorTool'.format(ns0, ns2) elt = xmp.find(name) - attr_value = elt.attrib['{0}CreatorTool'.format(ns1)] - 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('', - ' ', - ' ', - ' ', - ' '] - expected = '\n'.join(lst) + expected = nemo_xmp_box + self.maxDiff = None self.assertEqual(actual, expected) def test_codestream(self): @@ -657,8 +646,8 @@ class TestPrinting(unittest.TestCase): print(j.get_codestream()) actual = fake_out.getvalue().strip() lst = ['Codestream:', - ' SOC marker segment @ (3135, 0)', - ' SIZ marker segment @ (3137, 47)', + ' SOC marker segment @ (3231, 0)', + ' SIZ marker segment @ (3233, 47)', ' Profile: 2', ' Reference Grid Height, Width: (1456 x 2592)', ' Vertical, Horizontal Reference Grid Offset: (0 x 0)', @@ -668,7 +657,7 @@ class TestPrinting(unittest.TestCase): ' Signed: (False, False, False)', ' Vertical, Horizontal Subsampling: ' + '((1, 1), (1, 1), (1, 1))', - ' COD marker segment @ (3186, 12)', + ' COD marker segment @ (3282, 12)', ' Coding style:', ' Entropy coder, without partitions', ' SOP marker segments: False', @@ -690,11 +679,11 @@ class TestPrinting(unittest.TestCase): ' Vertically stripe causal context: False', ' Predictable termination: False', ' Segmentation symbols: False', - ' QCD marker segment @ (3200, 7)', + ' QCD marker segment @ (3296, 7)', ' Quantization style: no quantization, ' + '2 guard bits', ' 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"'] expected = '\n'.join(lst) self.assertEqual(actual, expected) @@ -1036,7 +1025,7 @@ class TestPrinting(unittest.TestCase): print(jp2.box[4]) actual = fake_out.getvalue().strip() lines = ['UUID Box (uuid) @ (1544, 25)', - ' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71', + ' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71 (unknown)', ' UUID Data: 1 bytes'] expected = '\n'.join(lines) @@ -1046,59 +1035,42 @@ class TestPrinting(unittest.TestCase): "Ordered dicts not printing well in 2.7") def test_exif_uuid(self): """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: - print(j.box[3]) - actual = fake_out.getvalue().strip() + with open(self.jp2file, 'rb') as ifptr: + tfile.write(ifptr.read()) - 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('