diff --git a/CHANGES.txt b/CHANGES.txt index bd5acf8..b8c6f05 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,5 @@ +Jun 05, 2013 - v0.1.4 Added Exif UUID read support. + Jun 02, 2013 - v0.1.3p1 Raising IOErrors when code block size and precinct sizes are not in harmony. Added statement to docs about upstream library dependence. Added roadmap to docs. diff --git a/docs/source/api.rst b/docs/source/api.rst index 3ab4997..fb2fd61 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,5 +1,6 @@ +--- API -=== +--- Jp2k ---- diff --git a/docs/source/conf.py b/docs/source/conf.py index 46d2d3c..0d630c6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -78,7 +78,7 @@ copyright = u'2013, John Evans' # The short X.Y version. version = '0.1' # The full version, including alpha/beta/rc tags. -release = '0.1.3p1' +release = '0.1.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index b0fa9b5..fa28c2d 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -1,6 +1,6 @@ -************ +------------ How do I...? -************ +------------ Get the code? ============= @@ -12,7 +12,7 @@ Go to either of Display metadata? ================= -There are two ways. From the unix command line, the script **jp2dump** is +There are two ways. From the unix command line, the script *jp2dump* is available. :: $ jp2dump /path/to/glymur/installation/data/nemo.jp2 @@ -25,9 +25,9 @@ From within Python, it is as simple as printing the Jp2k object, i.e. :: >>> j = Jp2k(file) >>> print(j) -The primary emphasis is on JP2 metadata, but it is possible to -display just raw codestream as well. This will display metadata present in the -codestream's main header only. :: +This prints the metadata found in the JP2 boxes, but in the case of the +codestream box, only the main header is printed. It is possible to print +**only** the codestream information as well, i.e. :: >>> print(j.get_codestream()) diff --git a/docs/source/index.rst b/docs/source/index.rst index ffc9fa7..1a7ddda 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,8 +3,9 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +================================== Welcome to glymur's documentation! -=================================== +================================== Contents: @@ -16,9 +17,9 @@ Contents: how_do_i roadmap - +------------------ Indices and tables -================== +------------------ * :ref:`genindex` * :ref:`modindex` diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index c8102ec..bce2620 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -1,6 +1,6 @@ -========================================= +---------------------------------------- Glymur: a Python interface for JPEG 2000 -========================================= +---------------------------------------- **Glymur** contains a Python interface to the OpenJPEG library which allows linux and mac users to read and write JPEG 2000 files. For more @@ -13,15 +13,15 @@ Glymur supports both reading and writing of JPEG 2000 images (part 1). Writing JPEG 2000 images is currently limited to images that can fit in memory, however. -Of particular focus is retrieval of metadata. Reading XMP UUID boxes -is supported, as the data block consists of an XMP packet, which is -just XML. There is some very limited support for reading JPX metadata. -For instance, *asoc* and *labl* boxes are recognized, so GMLJP2 metadata -can be retrieved from such JPX files. +Of particular focus is retrieval of metadata. Reading Exif UUIDs is supported, +as is reading XMP UUIDs as the XMP data packet is just XML. There is +some very limited support for reading JPX metadata. For instance, +**asoc** and **labl** boxes are recognized, so GMLJP2 metadata can +be retrieved from such JPX files. ------------- +'''''''''''' Requirements ------------- +'''''''''''' glymur works on Python 2.7 and 3.3. Python 3.3 is strongly recommended. OpenJPEG @@ -133,9 +133,9 @@ Windows ------- Not currently supported. ------------------------------------- +'''''''''''''''''''''''''''''''''''' Installation, Testing, Configuration ------------------------------------- +'''''''''''''''''''''''''''''''''''' From this point forward, python3 will be referred to as just "python". diff --git a/docs/source/roadmap.rst b/docs/source/roadmap.rst index d010885..63036b3 100644 --- a/docs/source/roadmap.rst +++ b/docs/source/roadmap.rst @@ -1,9 +1,10 @@ +------- Roadmap -======= +------- Here's an incomplete list of what I'd like to focus on in the near future. * continue to monitor upstream changes in the **openjp2** library - * add read support or Exif UUIDs * investigate using CFFI or cython instead of ctypes to wrap **openjp2** + * add read support for ICC profiles diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 29acab4..b8361d3 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -14,6 +14,7 @@ References import copy import math import os +import pprint import struct import sys import uuid @@ -1570,6 +1571,9 @@ class UUIDBox(Jp2kBox): 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)' + uuid_data = '\n' + pprint.pformat(self.data) else: uuid_type = '' uuid_data = '{0} bytes'.format(len(self.data)) @@ -1618,12 +1622,522 @@ class UUIDBox(Jp2kBox): else: text = buffer.decode('utf-8') kwargs['data'] = ET.fromstring(text) + elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2': + e = Exif(buffer) + d = {} + d['Exif'] = e.exif_image + d['Photo'] = e.exif_photo + d['GPSInfo'] = e.exif_gpsinfo + d['Iop'] = e.exif_iop + kwargs['data'] = d else: kwargs['data'] = buffer box = UUIDBox(**kwargs) return box +class Exif: + """ + Attributes + ---------- + buffer : bytes + Raw byte stream consisting of the UUID data. + endian : str + Either '<' for big-endian, or '>' for little-endian. + """ + + def __init__(self, buffer): + """Interpret raw buffer consisting of Exif IFD. + """ + self.exif_image = None + self.exif_photo = None + self.exif_gpsinfo = None + self.exif_iop = None + + self.buffer = buffer + + # Ignore the first six bytes. + # Next 8 should be (73, 73, 42, 8) + data = struct.unpack('' for little-endian. + num_tags : int + Number of tags in the IFD. + raw_ifd : dictionary + Maps tag number to "mildly-interpreted" tag value. + """ + datatype2fmt = {1: ('B', 1), + 2: ('B', 1), + 3: ('H', 2), + 4: ('I', 4), + 5: ('II', 8), + 7: ('B', 1), + 9: ('i', 4), + 10: ('ii', 8)} + + def __init__(self, endian, buffer, offset): + self.endian = endian + self.buffer = buffer + + self.num_tags, = struct.unpack(endian + 'H', + buffer[offset:offset + 2]) + + fmt = self.endian + 'HHII' * self.num_tags + ifd_buffer = buffer[offset + 2:offset + 2 + self.num_tags * 12] + data = struct.unpack(fmt, ifd_buffer) + self.raw_ifd = {} + 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 = 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.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 + + +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, buffer, offset): + Ifd.__init__(self, endian, buffer, offset) + + # Now post process the raw IFD. + self.ifd = {} + for tag, value in self.raw_ifd.items(): + tag_name = self.tagnum2name[tag] + self.ifd[tag_name] = value + + +class ExifPhotoIfd(Ifd): + tagnum2name = {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, buffer, offset): + Ifd.__init__(self, endian, buffer, offset) + + # Now post process the raw IFD. + self.ifd = {} + for tag, value in self.raw_ifd.items(): + tag_name = self.tagnum2name[tag] + self.ifd[tag_name] = value + + +class ExifGPSInfoIfd(Ifd): + tagnum2name = {0: 'GPSVersionID', + 1: 'GPSLatitudeRef', + 2: 'GPSLatitude', + 3: 'GPSLongitudeRef', + 4: 'GPSLongitude', + 5: 'GPSAltitudeRef', + 6: 'GPSAltitude', + 7: 'GPSTimeStamp', + 8: 'GPSSatellites', + 9: 'GPSStatus', + 10: 'GPSMeasureMode', + 11: 'GPSDOP', + 12: 'GPSSpeedRef', + 13: 'GPSSpeed', + 14: 'GPSTrackRef', + 15: 'GPSTrack', + 16: 'GPSImgDirectionRef', + 17: 'GPSImgDirection', + 18: 'GPSMapDatum', + 19: 'GPSDestLatitudeRef', + 20: 'GPSDestLatitude', + 21: 'GPSDestLongitudeRef', + 22: 'GPSDestLongitude', + 23: 'GPSDestBearingRef', + 24: 'GPSDestBearing', + 25: 'GPSDestDistanceRef', + 26: 'GPSDestDistance', + 27: 'GPSProcessingMethod', + 28: 'GPSAreaInformation', + 29: 'GPSDateStamp', + 30: 'GPSDifferential'} + + def __init__(self, endian, buffer, offset): + Ifd.__init__(self, endian, buffer, offset) + + # Now post process the raw IFD. + self.ifd = {} + for tag, value in self.raw_ifd.items(): + tag_name = self.tagnum2name[tag] + self.ifd[tag_name] = value + + +class ExifInteroperabilityIfd(Ifd): + tagnum2name = {1: 'InteroperabilityIndex', + 2: 'InteroperabilityVersion', + 4096: 'RelatedImageFileFormat', + 4097: 'RelatedImageWidth', + 4098: 'RelatedImageLength'} + + def __init__(self, endian, buffer, offset): + Ifd.__init__(self, endian, buffer, offset) + + # Now post process the raw IFD. + self.ifd = {} + for tag, value in self.raw_ifd.items(): + tag_name = self.tagnum2name[tag] + self.ifd[tag_name] = value + # Map each box ID to the corresponding class. _box_with_id = { 'asoc': AssociationBox, diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index e05235a..f84bd40 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -52,6 +52,7 @@ class TestJp2k(unittest.TestCase): @classmethod def setUpClass(cls): + # Setup a JP2 file with a bad XML box. jp2file = pkg_resources.resource_filename(glymur.__name__, "data/nemo.jp2") with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 0278d63..8b7ba34 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -11,6 +11,7 @@ else: from io import StringIO import glymur +from glymur import Jp2k try: data_root = os.environ['OPJ_DATA_ROOT'] @@ -22,6 +23,22 @@ except: class TestPrinting(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Setup a plain JP2 file without the two UUID boxes. + jp2file = pkg_resources.resource_filename(glymur.__name__, + "data/nemo.jp2") + with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: + cls._plain_nemo_file = tfile.name + ijfile = Jp2k(jp2file) + data = ijfile.read(reduce=3) + ojfile = Jp2k(cls._plain_nemo_file, 'wb') + ojfile.write(data) + + @classmethod + def tearDownClass(cls): + os.unlink(cls._plain_nemo_file) + def setUp(self): # Save sys.stdout. self.stdout = sys.stdout @@ -30,15 +47,14 @@ class TestPrinting(unittest.TestCase): "data/nemo.jp2") # Save the output of dumping nemo.jp2 for more than one test. - lines = ['File: nemo.jp2', - 'JPEG 2000 Signature Box (jP ) @ (0, 12)', + lines = ['JPEG 2000 Signature Box (jP ) @ (0, 12)', ' Signature: 0d0a870a', 'File Type Box (ftyp) @ (12, 20)', ' Brand: jp2 ', " Compatibility: ['jp2 ']", 'JP2 Header Box (jp2h) @ (32, 45)', ' Image Header Box (ihdr) @ (40, 22)', - ' Size: [1456 2592 3]', + ' Size: [182 324 3]', ' Bitdepth: 8', ' Signed: False', ' Compression: wavelet', @@ -47,45 +63,29 @@ class TestPrinting(unittest.TestCase): ' Method: enumerated colorspace', ' Precedence: 0', ' Colorspace: sRGB', - 'UUID Box (uuid) @ (77, 638)', - ' UUID: 4a706754-6966-6645-7869-662d3e4a5032', - ' UUID Data: 614 bytes', - 'UUID Box (uuid) @ (715, 2412)', - ' UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)', - ' UUID Data: ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - 'Contiguous Codestream Box (jp2c) @ (3127, 1133427)', + 'Contiguous Codestream Box (jp2c) @ (77, 112814)', ' Main header:', - ' SOC marker segment @ (3135, 0)', - ' SIZ marker segment @ (3137, 47)', + ' SOC marker segment @ (85, 0)', + ' SIZ marker segment @ (87, 47)', ' Profile: 2', - ' Reference Grid Height, Width: (1456 x 2592)', + ' Reference Grid Height, Width: (182 x 324)', ' Vertical, Horizontal Reference Grid Offset: ' + '(0 x 0)', - ' Reference Tile Height, Width: (512 x 512)', + ' Reference Tile Height, Width: (182 x 324)', ' Vertical, Horizontal Reference Tile Offset: ' + '(0 x 0)', ' Bitdepth: (8, 8, 8)', ' Signed: (False, False, False)', ' Vertical, Horizontal Subsampling: ' + '((1, 1), (1, 1), (1, 1))', - ' COD marker segment @ (3186, 12)', + ' COD marker segment @ (136, 12)', ' Coding style:', ' Entropy coder, without partitions', ' SOP marker segments: False', ' EPH marker segments: False', ' Coding style parameters:', ' Progression order: LRCP', - ' Number of layers: 3', + ' Number of layers: 1', ' Multiple component transformation usage: ' + 'reversible', ' Number of resolutions: 6', @@ -102,26 +102,29 @@ class TestPrinting(unittest.TestCase): + 'False', ' Predictable termination: False', ' Segmentation symbols: False', - ' QCD marker segment @ (3200, 19)', + ' QCD marker segment @ (150, 19)', ' Quantization style: no quantization, ' + '2 guard bits', ' Step size: [(0, 8), (0, 9), (0, 9), ' + '(0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), ' + '(0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), ' + '(0, 10)]'] - self.expectedNemo = '\n'.join(lines) + self.expectedPlain = '\n'.join(lines) def tearDown(self): # Restore stdout. sys.stdout = self.stdout - #import pdb; pdb.set_trace() def test_jp2dump(self): - glymur.jp2dump(self.jp2file) + glymur.jp2dump(self._plain_nemo_file) actual = sys.stdout.getvalue().strip() - self.actual = actual - self.expected = self.expectedNemo - self.assertEqual(actual, self.expectedNemo) + + # Get rid of the filename line, as it is not set in stone. + lst = actual.split('\n') + lst = lst[1:] + actual = '\n'.join(lst) + + self.assertEqual(actual, self.expectedPlain) def test_COC_segment(self): j = glymur.Jp2k(self.jp2file) @@ -429,17 +432,42 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_entire_file(self): + def test_xmp(self): + # Verify the printing of a UUID/XMP box. j = glymur.Jp2k(self.jp2file) + print(j.box[4]) + actual = sys.stdout.getvalue().strip() + lst = ['UUID Box (uuid) @ (715, 2412)', + ' UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)', + ' UUID Data: ', + ' ', + ' ', + ' ', + ' ', + ' '] + expected = '\n'.join(lst) + self.assertEqual(actual, expected) + + def test_entire_file(self): + j = glymur.Jp2k(self._plain_nemo_file) print(j) actual = sys.stdout.getvalue().strip() - self.assertEqual(actual, self.expectedNemo) + + # Get rid of the filename line, as it is not set in stone. + lst = actual.split('\n') + lst = lst[1:] + actual = '\n'.join(lst) + + self.assertEqual(actual, self.expectedPlain) def test_codestream(self): j = glymur.Jp2k(self.jp2file) print(j.get_codestream()) actual = sys.stdout.getvalue().strip() - lst = ['Codestream:', ' SOC marker segment @ (3135, 0)', ' SIZ marker segment @ (3137, 47)', @@ -782,5 +810,63 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) + def test_exif_uuid(self): + j = glymur.Jp2k(self.jp2file) + + print(j.box[3]) + actual = sys.stdout.getvalue().strip() + + lines = ["UUID Box (uuid) @ (77, 638)", + " UUID: 4a706754-6966-6645-7869-662d3e4a5032 (Exif)", + " UUID Data: ", + "{'Exif': {'ExifTag': 138,", + " 'GPSTag': 354,", + " 'Make': 'HTC',", + " 'Model': 'HTC Glacier',", + " 'ResolutionUnit': 2,", + " 'XResolution': 72.0,", + " 'YCbCrPositioning': 1,", + " 'YResolution': 72.0},", + " 'GPSInfo': {'GPSAltitude': 0.0,", + " 'GPSAltitudeRef': 0,", + " 'GPSDateStamp': '2013:02:09',", + " 'GPSLatitude': [42.0, 20.0, 33.61],", + " 'GPSLatitudeRef': 'N',", + " 'GPSLongitude': [71.0, 5.0, 17.32],", + " 'GPSLongitudeRef': 'W',", + " 'GPSMapDatum': 'WGS-84',", + " 'GPSProcessingMethod': (65,", + " 83,", + " 67,", + " 73,", + " 73,", + " 0,", + " 0,", + " 0,", + " 78,", + " 69,", + " 84,", + " 87,", + " 79,", + " 82,", + " 75),", + " 'GPSTimeStamp': [19.0, 47.0, 53.0],", + " 'GPSVersionID': (2, 2, 0)},", + " 'Iop': None,", + " 'Photo': {'ColorSpace': 1,", + " 'ComponentsConfiguration': (1, 2, 3, 0),", + " 'DateTimeDigitized': '2013:02:09 14:47:53',", + " 'DateTimeOriginal': '2013:02:09 14:47:53',", + " 'ExifVersion': (48, 50, 50, 48),", + " 'FlashpixVersion': (48, 49, 48, 48),", + " 'FocalLength': 3.53,", + " 'ISOSpeedRatings': 76,", + " 'InteroperabilityTag': 324,", + " 'PixelXDimension': 2528,", + " 'PixelYDimension': 1424}}"] + + expected = '\n'.join(lines) + self.assertEqual(actual, expected) + if __name__ == "__main__": unittest.main() diff --git a/setup.py b/setup.py index 449cb96..5febec4 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from distutils.core import setup kwargs = {'name': 'Glymur', - 'version': '0.1.3p1', + 'version': '0.1.4', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans',