From f8ceb09a9b784ea7da25b25a755d3e7439d6d969 Mon Sep 17 00:00:00 2001 From: John Evans Date: Sat, 15 Jun 2013 15:05:56 -0400 Subject: [PATCH] ICC header fields now read as ordered dicts (#46) --- CHANGES.txt | 4 +- glymur/jp2box.py | 145 +++++++++++++--------------------- glymur/test/test_icc.py | 37 +++++---- glymur/test/test_opj_suite.py | 8 +- glymur/test/test_printing.py | 53 +++++++------ 5 files changed, 108 insertions(+), 139 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d878edc..749716c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Jun 15, 2013 - v0.1.9 Reading ICC profile headers. Exif dictionaries changed - to ordered dicts. +Jun 15, 2013 - v0.1.9 Reading ICC profile headers as ordered dicts. Exif + dictionaries changed to ordered dicts. Jun 14, 2013 - v0.1.8 Added reduce=-1 option to get lowest resolution thumbnail. diff --git a/glymur/jp2box.py b/glymur/jp2box.py index c8b6c35..8d74635 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -155,9 +155,9 @@ class ColourSpecificationBox(Jp2kBox): colorspace : int or None Enumerated colorspace, corresponds to one of 'sRGB', 'greyscale', or 'YCC'. If not None, then icc_profile must be None. - icc_profile : _ICCProfile or None - ICC profile header according to ICC profile specification. If not - None, then color_space must be None. + icc_profile : dict + ICC profile header according to ICC profile specification. If + colorspace is not None, then icc_profile must be empty. """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Colour Specification') @@ -176,7 +176,9 @@ class ColourSpecificationBox(Jp2kBox): x = _colorspace_map_display[self.colorspace] msg += '\n Colorspace: {0}'.format(x) else: - msg += '\n ICC Profile: {0}'.format(self.icc_profile.__str__()) + x = pprint.pformat(self.icc_profile) + lines = [' ' * 8 + y for y in x.split('\n')] + msg += '\n ICC Profile:\n{0}'.format('\n'.join(lines)) return msg @@ -229,7 +231,7 @@ class ColourSpecificationBox(Jp2kBox): kwargs['icc_profile'] = None else: icc_profile = _ICCProfile(f.read(n)) - kwargs['icc_profile'] = icc_profile + kwargs['icc_profile'] = icc_profile.header box = ColourSpecificationBox(**kwargs) return box @@ -279,110 +281,71 @@ class _ICCProfile: def __init__(self, buffer): self._raw_buffer = buffer + header = collections.OrderedDict() - self.size, = struct.unpack('>I', self._raw_buffer[0:4]) - self.preferred_cmm_type, = struct.unpack('>I', self._raw_buffer[4:8]) + data = struct.unpack('>IIBB', self._raw_buffer[0:10]) + header['Size'] = data[0] + header['Preferred CMM Type'] = data[1] + major = data[2] + minor = (data[3] & 0xf0) >> 4 + bugfix = (data[3] & 0x0f) + header['Version'] = '{0}.{1}.{2}'.format(major, minor, bugfix) - data = struct.unpack('>BB', self._raw_buffer[8:10]) - major = data[0] - minor = (data[1] & 0xf0) >> 4 - bugfix = (data[1] & 0x0f) - self.version = '{0}.{1}.{2}'.format(major, minor, bugfix) - - self.device_class = self.profile_class[self._raw_buffer[12:16]] - self.colour_space = self.colour_space_dict[self._raw_buffer[16:20]] - self.connection_space = self.colour_space_dict[self._raw_buffer[20:24]] + header['Device Class'] = self.profile_class[self._raw_buffer[12:16]] + header['Color Space'] = self.colour_space_dict[self._raw_buffer[16:20]] + data = self.colour_space_dict[self._raw_buffer[20:24]] + header['Connection Space'] = data data = struct.unpack('>HHHHHH', self._raw_buffer[24:36]) - self.datetime = datetime.datetime(*data) - self.file_signature = buffer[36:40].decode('utf-8') + header['Datetime'] = datetime.datetime(*data) + header['File Signature'] = buffer[36:40].decode('utf-8') if buffer[40:44] == b'\x00\x00\x00\x00': - self.platform = 'unrecognized' + header['Platform'] = 'unrecognized' else: - self.platform = buffer[40:44].decode('utf-8') + header['Platform'] = buffer[40:44].decode('utf-8') - self.flags, = struct.unpack('>I', buffer[44:48]) + x, = struct.unpack('>I', buffer[44:48]) + y = 'embedded, ' if x & 0x01 else 'not embedded, ' + y += 'cannot ' if x & 0x02 else 'can ' + y += 'be used independently' + header['Flags'] = y - self.device_manufacturer = buffer[48:52].decode('utf-8') + header['Device Manufacturer'] = buffer[48:52].decode('utf-8') if buffer[52:56] == b'\x00\x00\x00\x00': - self.device_model = '' + device_model = '' else: - self.device_model = buffer[52:56].decode('utf-8') - self.device_attributes, = struct.unpack('>Q', buffer[56:64]) - self.rendering_intent, = struct.unpack('>I', buffer[64:68]) + device_model = buffer[52:56].decode('utf-8') + header['Device Model'] = device_model + + x, = struct.unpack('>Q', buffer[56:64]) + y = 'transparency, ' if x & 0x01 else 'reflective, ' + y += 'matte, ' if x & 0x02 else 'glossy, ' + y += 'negative ' if x & 0x04 else 'positive ' + y += 'media polarity, ' + y += 'black and white media' if x & 0x08 else 'color media' + header['Device Attributes'] = y + + x, = struct.unpack('>I', buffer[64:68]) + try: + header['Rendering Intent'] = self.rendering_intent_dict[x] + except KeyError: + header['Rendering Intent'] = 'unknown' data = struct.unpack('>iii', buffer[68:80]) - self.illuminant = np.array(data, dtype=np.float64) / 65536 + header['Illuminant'] = np.array(data, dtype=np.float64) / 65536 if buffer[80:84] == b'\x00\x00\x00\x00': - self.creator = 'unrecognized' + creator = 'unrecognized' else: - self.creator = buffer[80:84].decode('utf-8') + creator = buffer[80:84].decode('utf-8') + header['Creator'] = creator - self.profile_id = buffer[84:100] - self.reserved = buffer[100:127] + if header['Version'][0] == '4': + header['Profile Id'] = buffer[84:100] - def __str__(self): - msg = "\n Size: {0}" - msg += "\n Preferred CMM type: {1:x}" - msg += "\n Version: {2}" - msg += "\n Device class signature: {3}" - msg += "\n Color space: {4}" - msg += "\n Connection space: {5}" - msg += "\n Creation time: {6}" - msg += "\n File signature: {7}" - msg += "\n Platform: {8}" - msg += "\n Flags: {9}" - msg += "\n Device manufacturer: {10}" - msg += "\n Device model: {11}" - msg += "\n Device attributes: {12}" - msg += "\n Rendering intent: {13}" - msg += "\n Illuminant: {14}" - msg += "\n Creator signature: {15}" + # Final 27 bytes are reserved. - if self.flags & 0x01: - flag_string = 'embedded, ' - else: - flag_string = 'not embedded, ' - if self.flags & 0x02: - flag_string += 'cannot be used independently' - else: - flag_string += 'can be used independently' - - if self.device_attributes & 0x01: - attr_string = 'transparency, ' - else: - attr_string = 'reflective, ' - if self.device_attributes & 0x02: - attr_string += 'matte, ' - else: - attr_string += 'glossy, ' - if self.device_attributes & 0x04: - attr_string += 'negative media polarity, ' - else: - attr_string += 'positive media polarity, ' - if self.device_attributes & 0x08: - attr_string += 'black and white media' - else: - attr_string += 'color media' - - msg = msg.format(self.size, - self.preferred_cmm_type, - self.version, - self.device_class, - self.colour_space, - self.connection_space, - self.datetime, - self.file_signature, - self.platform, - flag_string, - self.device_manufacturer, - self.device_model, - attr_string, - self.rendering_intent_dict[self.rendering_intent], - self.illuminant, - self.creator) - return(msg) + self.header = header class ComponentDefinitionBox(Jp2kBox): diff --git a/glymur/test/test_icc.py b/glymur/test/test_icc.py index c6081ff..bbce1d6 100644 --- a/glymur/test/test_icc.py +++ b/glymur/test/test_icc.py @@ -35,32 +35,31 @@ class TestICC(unittest.TestCase): filename = os.path.join(data_root, 'input/conformance/file5.jp2') j = Jp2k(filename) profile = j.box[3].box[1].icc_profile - self.assertEqual(profile.size, 546) - self.assertEqual(profile.preferred_cmm_type, 0) - self.assertEqual(profile.version, '2.2.0') - self.assertEqual(profile.device_class, 'input device profile') - self.assertEqual(profile.colour_space, 'RGB') - self.assertEqual(profile.datetime, + self.assertEqual(profile['Size'], 546) + self.assertEqual(profile['Preferred CMM Type'], 0) + self.assertEqual(profile['Version'], '2.2.0') + self.assertEqual(profile['Device Class'], 'input device profile') + self.assertEqual(profile['Color Space'], 'RGB') + self.assertEqual(profile['Datetime'], datetime.datetime(2001, 8, 30, 13, 32, 37)) - self.assertEqual(profile.file_signature, 'acsp') - self.assertEqual(profile.platform, 'unrecognized') - self.assertTrue(profile.flags & 0x01) # embedded - self.assertFalse(profile.flags & 0x02) # use anywhere + self.assertEqual(profile['File Signature'], 'acsp') + self.assertEqual(profile['Platform'], 'unrecognized') + self.assertEqual(profile['Flags'], + 'embedded, can be used independently') - self.assertEqual(profile.device_manufacturer, 'KODA') - self.assertEqual(profile.device_model, 'ROMM') + self.assertEqual(profile['Device Manufacturer'], 'KODA') + self.assertEqual(profile['Device Model'], 'ROMM') - self.assertFalse(profile.device_attributes & 0x01) # reflective - self.assertFalse(profile.device_attributes & 0x02) # glossy - self.assertFalse(profile.device_attributes & 0x04) # positive - self.assertFalse(profile.device_attributes & 0x08) # colour - self.assertEqual(profile.rendering_intent & 0x00ff, 0) # perceptual + self.assertEqual(profile['Device Attributes'], + 'reflective, glossy, positive media polarity, ' + + 'color media') + self.assertEqual(profile['Rendering Intent'], 'perceptual') - np.testing.assert_almost_equal(profile.illuminant, + np.testing.assert_almost_equal(profile['Illuminant'], (0.964203, 1.000000, 0.824905), decimal=6) - self.assertEqual(profile.creator, 'JPEG') + self.assertEqual(profile['Creator'], 'JPEG') @unittest.skipIf(sys.hexversion < 0x03020000, "Uses features introduced in 3.2.") diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index 80192f8..90e9b54 100644 --- a/glymur/test/test_opj_suite.py +++ b/glymur/test/test_opj_suite.py @@ -3579,7 +3579,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].method, 2) # enumerated self.assertEqual(jp2.box[3].box[1].precedence, 0) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact - self.assertEqual(jp2.box[3].box[1].icc_profile.size, 546) + self.assertEqual(jp2.box[3].box[1].icc_profile['Size'], 546) self.assertIsNone(jp2.box[3].box[1].colorspace) # Jp2 Header @@ -3674,7 +3674,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].method, 2) self.assertEqual(jp2.box[3].box[1].precedence, 0) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact - self.assertEqual(jp2.box[3].box[1].icc_profile.size, 13332) + self.assertEqual(jp2.box[3].box[1].icc_profile['Size'], 13332) self.assertIsNone(jp2.box[3].box[1].colorspace) # Jp2 Header @@ -3723,7 +3723,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 2) # enumerated self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 1) # JPX exact - self.assertEqual(jp2.box[2].box[1].icc_profile.size, 414) + self.assertEqual(jp2.box[2].box[1].icc_profile['Size'], 414) self.assertIsNone(jp2.box[2].box[1].colorspace) # XML box @@ -6630,7 +6630,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].method, 3) # any icc self.assertEqual(jp2.box[3].box[1].precedence, 2) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact - self.assertEqual(jp2.box[3].box[1].icc_profile.size, 1328) + self.assertEqual(jp2.box[3].box[1].icc_profile['Size'], 1328) self.assertIsNone(jp2.box[3].box[1].colorspace) # UUID boxes. All mentioned in the RREQ box. diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 50b5033..723070b 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -775,6 +775,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) + @unittest.skipIf(sys.hexversion < 0x03000000, + "Ordered dicts not printing well in 2.7") @unittest.skipIf(data_root is None, "OPJ_DATA_ROOT environment variable not set") def test_jpx_approximation_with_icc_profile(self): @@ -785,29 +787,32 @@ class TestPrinting(unittest.TestCase): print(j.box[3].box[1]) actual = sys.stdout.getvalue().strip() - lines = ['Colour Specification Box (colr) @ (179, 1339)', - ' Method: any ICC profile', - ' Precedence: 2', - ' Approximation: accurately represents ' - + 'correct colorspace definition', - ' ICC Profile: ', - ' Size: 1328', - ' Preferred CMM type: 6170706c', - ' Version: 2.2.0', - ' Device class signature: display device profile', - ' Color space: RGB', - ' Connection space: XYZ', - ' Creation time: 2009-02-25 11:26:11', - ' File signature: acsp', - ' Platform: APPL', - ' Flags: not embedded, can be used independently', - ' Device manufacturer: appl', - ' Device model: ', - ' Device attributes: ' - + 'reflective, glossy, positive media polarity, color media', - ' Rendering intent: perceptual', - ' Illuminant: [ 0.96420288 1. 0.8249054 ]', - ' Creator signature: appl'] + lines = ["Colour Specification Box (colr) @ (179, 1339)", + " Method: any ICC profile", + " Precedence: 2", + " Approximation: accurately represents " + + "correct colorspace definition", + " ICC Profile:", + " {'Size': 1328,", + " 'Preferred CMM Type': 1634758764,", + " 'Version': '2.2.0',", + " 'Device Class': 'display device profile',", + " 'Color Space': 'RGB',", + " 'Connection Space': 'XYZ',", + " 'Datetime': " + + "datetime.datetime(2009, 2, 25, 11, 26, 11),", + " 'File Signature': 'acsp',", + " 'Platform': 'APPL',", + " 'Flags': 'not embedded, " + + "can be used independently',", + " 'Device Manufacturer': 'appl',", + " 'Device Model': '',", + " 'Device Attributes': 'reflective, glossy, " + + "positive media polarity, color media',", + " 'Rendering Intent': 'perceptual',", + " 'Illuminant': array([ 0.96420288, 1. ," + + " 0.8249054 ]),", + " 'Creator': 'appl'}"] expected = '\n'.join(lines) self.assertEqual(actual, expected) @@ -828,6 +833,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) + @unittest.skipIf(sys.hexversion < 0x03000000, + "Ordered dicts not printing well in 2.7") def test_exif_uuid(self): j = glymur.Jp2k(self.jp2file)