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 0b72b85..1e28b1e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -75,7 +75,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 c5a411d..724a3b7 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? ============= @@ -9,7 +9,7 @@ Go to http://github.com/quintusdias/glymur 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 @@ -22,8 +22,8 @@ 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 c090d71..1797017 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 XML. There is +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 @@ -132,9 +132,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 9edf010..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)) @@ -1610,7 +1614,7 @@ class UUIDBox(Jp2kBox): n = offset + length - f.tell() buffer = f.read(n) if kwargs['uuid'] == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - # XMP data. Parse as XML. Seems to be a difference between + # XMP data. Parse as XML. Seems to be a difference between # ElementTree in version 2.7 and 3.3. if sys.hexversion < 0x03000000: parser = ET.XMLParser(encoding='utf-8') @@ -1619,18 +1623,25 @@ class UUIDBox(Jp2kBox): text = buffer.decode('utf-8') kwargs['data'] = ET.fromstring(text) elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2': - kwargs['data'] = Exif(buffer).exif_image + 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. + Raw byte stream consisting of the UUID data. endian : str Either '<' for big-endian, or '>' for little-endian. """ @@ -1657,7 +1668,7 @@ class Exif: offset = data[3] # This is the 'Exif Image' portion. - exif = ExifImageIfd(self.endian, buffer[6:], offset) + exif = ExifImageIfd(self.endian, buffer[6:], offset) self.exif_image = exif.ifd if 'ExifTag' in self.exif_image.keys(): @@ -1677,12 +1688,13 @@ class Exif: gps = ExifGPSInfoIfd(self.endian, buffer[6:], offset) self.exif_gpsinfo = gps.ifd + class Ifd: """ Attributes ---------- buffer : bytes - Raw byte stream consisting of the UUID data. + Raw byte stream consisting of the UUID data. datatype2fmt : dictionary Class attribute, maps the TIFF enumerated datatype to the python datatype and data width. @@ -1706,36 +1718,45 @@ class Ifd: self.endian = endian self.buffer = buffer - self.num_tags, = struct.unpack(endian + 'H', buffer[offset:offset + 2]) - + self.num_tags, = struct.unpack(endian + 'H', + buffer[offset:offset + 2]) + fmt = self.endian + 'HHII' * self.num_tags - data = struct.unpack(fmt, buffer[offset + 2:offset + 2 + self.num_tags * 12]) + 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]): - tag_entry = buffer[offset + 2 + j * 12 + 8:offset + 2 + j * 12 + 8 + 4] - payload = self.parse_tag(data[j * 4 + 1], - data[j * 4 + 2], - tag_entry) - self.raw_ifd[tag] = payload + # 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. + # Interpret the payload at the offset specified by the 4 bytes in + # the tag entry. offset, = struct.unpack(self.endian + 'I', offset_buf) target_buffer = self.buffer[offset:offset + payload_size] - + if dtype == 2: # ASCII - payload = target_buffer.decode('utf-8').rstrip() + 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: @@ -1746,22 +1767,226 @@ class Ifd: rational_payload.append(value) payload = rational_payload if count == 1: - # If just a single value, then return a scalar instead of a + # If just a single value, then return a scalar instead of a # tuple. payload = payload[0] - - return payload + + return payload class ExifImageIfd(Ifd): - tagnum2name = {271: 'Make', + """ + 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', + 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', - 34853: 'GPSTag'} + 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) @@ -1772,18 +1997,77 @@ class ExifImageIfd(Ifd): tag_name = self.tagnum2name[tag] self.ifd[tag_name] = value + class ExifPhotoIfd(Ifd): - tagnum2name = {34855: 'ISOSpeedRatings', + 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', - 40965: 'InteroperabilityTag'} + 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) @@ -1794,6 +2078,7 @@ class ExifPhotoIfd(Ifd): tag_name = self.tagnum2name[tag] self.ifd[tag_name] = value + class ExifGPSInfoIfd(Ifd): tagnum2name = {0: 'GPSVersionID', 1: 'GPSLatitudeRef', @@ -1836,6 +2121,7 @@ class ExifGPSInfoIfd(Ifd): tag_name = self.tagnum2name[tag] self.ifd[tag_name] = value + class ExifInteroperabilityIfd(Ifd): tagnum2name = {1: 'InteroperabilityIndex', 2: 'InteroperabilityVersion', @@ -1875,6 +2161,7 @@ _box_with_id = { 'uuid': UUIDBox, 'xml ': XMLBox} + def _indent(elem, level=0): """Recipe for pretty printing XML. Please see @@ -1894,6 +2181,7 @@ def _indent(elem, level=0): 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. """ diff --git a/glymur/test/test_opj_suite_neg.py b/glymur/test/test_opj_suite_neg.py index 0862c42..75c9c6f 100644 --- a/glymur/test/test_opj_suite_neg.py +++ b/glymur/test/test_opj_suite_neg.py @@ -171,7 +171,7 @@ class TestSuiteNegative(unittest.TestCase): ofile.write(data, cbsize=(13, 12)) def test_codeblock_size_with_precinct_size(self): - # Seems like code block sizes should never exceed half that of + # Seems like code block sizes should never exceed half that of # precinct size. ifile = Jp2k(self.jp2file) data = ifile.read(reduce=3) diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index f0d1f7d..8b7ba34 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -810,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',