From 331ff48aa96e845f69e919e7bf7bb9000ced6f1e Mon Sep 17 00:00:00 2001 From: jevans Date: Fri, 14 Jun 2013 23:17:11 -0400 Subject: [PATCH] Added ICC header support. Closes #22 --- glymur/jp2box.py | 133 +++++++++++++++++++++------------- glymur/test/test_icc.py | 18 +++-- glymur/test/test_opj_suite.py | 27 ++++--- glymur/test/test_printing.py | 19 ++++- 4 files changed, 132 insertions(+), 65 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 6086038..9b3f4c6 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -154,9 +154,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 : byte array or None - ICC profile according to ICC profile specification. If not None, then - color_space 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. """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Colour Specification') @@ -175,8 +175,7 @@ class ColourSpecificationBox(Jp2kBox): x = _colorspace_map_display[self.colorspace] msg += '\n Colorspace: {0}'.format(x) else: - x = len(self.icc_profile) - msg += '\n ICC Profile: {0} bytes'.format(x) + msg += '\n ICC Profile: {0}'.format(self.icc_profile.__str__()) return msg @@ -222,14 +221,20 @@ class ColourSpecificationBox(Jp2kBox): # ICC profile kwargs['colorspace'] = None n = offset + length - f.tell() - icc_profile = ICCProfile(f.read(n)) - kwargs['icc_profile'] = icc_profile + if n < 128: + msg = "ICC profile header is corrupt, length is " + msg += "only {0} instead of 128." + warnings.warn(msg.format(n), UserWarning) + kwargs['icc_profile'] = None + else: + icc_profile = _ICCProfile(f.read(n)) + kwargs['icc_profile'] = icc_profile box = ColourSpecificationBox(**kwargs) return box -class ICCProfile: +class _ICCProfile: """ """ profile_class = {b'scnr': 'input device profile', @@ -266,31 +271,14 @@ class ICCProfile: b'ECLR': '14colour', b'FCLR': '15colour'} + rendering_intent_dict = {0: 'perceptual', + 1: 'media-relative colorimetric', + 2: 'saturation', + 3: 'ICC-absolute colorimetric'} + def __init__(self, buffer): self._raw_buffer = buffer - self.parse_header(buffer) - self.parse_tag_table(buffer) - def parse_tag_table(self, buffer): - """ - See section 7.3 of ICC1V4.2. - """ - num_tags, = struct.unpack('>I', buffer[128:132]) - tag_table = buffer[132:132 + num_tags * 12] - data = struct.unpack('>' + 'III' * num_tags, tag_table) - signature = data[0::3] - offset = data[1::3] - size = data[2::3] - for j in range(num_tags): - sig = buffer[132 + j * 4:132 + (j + 1) * 4] - print(sig) - import pdb; pdb.set_trace() - tag_buffer = buffer[offset[j]:offset[j] + size[j]] - if sig == b'desc': - table['desc'] = _MultiLocalizedUnicodeType(tag_buffer) - - def parse_header(self, buffer): - """See section 7.2""" self.size, = struct.unpack('>I', self._raw_buffer[0:4]) self.preferred_cmm_type, = struct.unpack('>I', self._raw_buffer[4:8]) @@ -311,11 +299,14 @@ class ICCProfile: self.platform = 'unrecognized' else: self.platform = buffer[40:44].decode('utf-8') - + self.flags, = struct.unpack('>I', buffer[44:48]) - + self.device_manufacturer = buffer[48:52].decode('utf-8') - self.device_model = buffer[52:56].decode('utf-8') + if buffer[52:56] == b'\x00\x00\x00\x00': + self.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]) @@ -326,28 +317,72 @@ class ICCProfile: self.creator = 'unrecognized' else: self.creator = buffer[80:84].decode('utf-8') - + self.profile_id = buffer[84:100] self.reserved = buffer[100:127] def __str__(self): - msg = "Profile size: {0}" - msg = "Preferred CMM type: {1}" + 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}" + + 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.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) -class _MultiLocalizedUnicodeType: - def __init__(self, buffer): - import pdb; pdb.set_trace() - self.id = buffer[0:4].decode('utf-8') - data = struct.unpack('>II', buffer[8:16]) - num_names = data[0] - self.record_size = data[1] - - data = struct.unpack('>' + 'HHII' * num_names, - buffer[16:16 + 12 * num_names]) - for j in range(num_names): - print(j) class ComponentDefinitionBox(Jp2kBox): """Container for component definition box information. diff --git a/glymur/test/test_icc.py b/glymur/test/test_icc.py index 30d41fc..c6081ff 100644 --- a/glymur/test/test_icc.py +++ b/glymur/test/test_icc.py @@ -21,17 +21,16 @@ except: raise +@unittest.skipIf(data_root is None, + "OPJ_DATA_ROOT environment variable not set") class TestICC(unittest.TestCase): def setUp(self): - self.jp2file = pkg_resources.resource_filename(glymur.__name__, - "data/nemo.jp2") + pass def tearDown(self): pass - @unittest.skipIf(data_root is None, - "OPJ_DATA_ROOT environment variable not set") def test_file5(self): filename = os.path.join(data_root, 'input/conformance/file5.jp2') j = Jp2k(filename) @@ -42,7 +41,7 @@ class TestICC(unittest.TestCase): self.assertEqual(profile.device_class, 'input device profile') self.assertEqual(profile.colour_space, 'RGB') self.assertEqual(profile.datetime, - datetime.datetime(2001,8,30,13,32,37)) + 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 @@ -63,6 +62,13 @@ class TestICC(unittest.TestCase): self.assertEqual(profile.creator, 'JPEG') + @unittest.skipIf(sys.hexversion < 0x03020000, + "Uses features introduced in 3.2.") + def test_invalid_profile_header(self): + jfile = os.path.join(data_root, + 'input/nonregression/orb-blue10-lin-jp2.jp2') + with self.assertWarns(UserWarning) as cw: + data = Jp2k(jfile).read() + if __name__ == "__main__": unittest.main() - diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index 15913e2..80192f8 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(len(jp2.box[3].box[1].icc_profile), 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(len(jp2.box[3].box[1].icc_profile), 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(len(jp2.box[2].box[1].icc_profile), 414) + self.assertEqual(jp2.box[2].box[1].icc_profile.size, 414) self.assertIsNone(jp2.box[2].box[1].colorspace) # XML box @@ -6394,7 +6394,10 @@ class TestSuite(unittest.TestCase): def test_NR_orb_blue10_lin_jp2_dump(self): jfile = os.path.join(data_root, 'input/nonregression/orb-blue10-lin-jp2.jp2') - jp2 = Jp2k(jfile) + with warnings.catch_warnings(): + # This file has an invalid ICC profile + warnings.simplefilter("ignore") + jp2 = Jp2k(jfile) ids = [box.id for box in jp2.box] self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', 'jp2c']) @@ -6426,7 +6429,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 2) # res icc self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2 - self.assertEqual(len(jp2.box[2].box[1].icc_profile), 1) + self.assertIsNone(jp2.box[2].box[1].icc_profile) self.assertIsNone(jp2.box[2].box[1].colorspace) c = jp2.box[3].main_header @@ -6490,7 +6493,10 @@ class TestSuite(unittest.TestCase): def test_NR_orb_blue10_win_jp2_dump(self): jfile = os.path.join(data_root, 'input/nonregression/orb-blue10-win-jp2.jp2') - jp2 = Jp2k(jfile) + with warnings.catch_warnings(): + # This file has an invalid ICC profile + warnings.simplefilter("ignore") + jp2 = Jp2k(jfile) ids = [box.id for box in jp2.box] self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', 'jp2c']) @@ -6522,7 +6528,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 2) # restricted icc self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2 - self.assertEqual(len(jp2.box[2].box[1].icc_profile), 1) + self.assertIsNone(jp2.box[2].box[1].icc_profile) self.assertIsNone(jp2.box[2].box[1].colorspace) c = jp2.box[3].main_header @@ -6624,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(len(jp2.box[3].box[1].icc_profile), 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. @@ -6859,7 +6865,10 @@ class TestSuite(unittest.TestCase): def test_NR_DEC_orb_blue_lin_jp2_25_decode(self): jfile = os.path.join(data_root, 'input/nonregression/orb-blue10-lin-jp2.jp2') - data = Jp2k(jfile).read() + with warnings.catch_warnings(): + # This file has an invalid ICC profile + warnings.simplefilter("ignore") + data = Jp2k(jfile).read() self.assertTrue(True) def test_NR_DEC_orb_blue_win_jp2_26_decode(self): diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 7680377..a0180c2 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -790,7 +790,24 @@ class TestPrinting(unittest.TestCase): ' Precedence: 2', ' Approximation: accurately represents ' + 'correct colorspace definition', - ' ICC Profile: 1328 bytes'] + ' 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'] expected = '\n'.join(lines) self.assertEqual(actual, expected)