From b881e44814127ce9c5af0f1ce58f86c1473cac79 Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 20 Jun 2013 22:03:03 -0400 Subject: [PATCH 01/25] JPEG2000SignatureBox can now has simple constructor call. --- glymur/jp2box.py | 9 ++++++++- glymur/test/test_jp2box.py | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 glymur/test/test_jp2box.py diff --git a/glymur/jp2box.py b/glymur/jp2box.py index d4a44e9..aa92f2a 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -59,6 +59,11 @@ class Jp2kBox(object): msg += " @ ({0}, {1})".format(self.offset, self.length) return msg + def _write(self, f): + """Must be implemented in a subclass. + """ + raise NotImplementedError("Not supported for this box.") + def _parse_superbox(self, f): """Parse a superbox (box consisting of nothing but other boxes. @@ -882,8 +887,10 @@ class JPEG2000SignatureBox(Jp2kBox): Four-byte tuple identifying the file as JPEG 2000. """ def __init__(self, **kwargs): - Jp2kBox.__init__(self, id='', longname='JPEG 2000 Signature') + Jp2kBox.__init__(self, id='jP ', longname='JPEG 2000 Signature') self.__dict__.update(**kwargs) + if 'signature' not in kwargs.keys(): + self.signature = (13, 10, 135, 10) def __str__(self): msg = Jp2kBox.__str__(self) diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py new file mode 100644 index 0000000..40b85b8 --- /dev/null +++ b/glymur/test/test_jp2box.py @@ -0,0 +1,27 @@ +import unittest + +import numpy as np +import pkg_resources + +import glymur + +@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, + "Missing openjp2 library.") +class TestJp2Boxes(unittest.TestCase): + + def setUp(self): + self.jp2file = pkg_resources.resource_filename(glymur.__name__, + "data/nemo.jp2") + + def tearDown(self): + pass + + def test_default_JPEG2000SignatureBox(self): + # Should be able to instantiate a JPEG2000SignatureBox + b = glymur.jp2box.JPEG2000SignatureBox() + self.assertEqual(b.signature, (13, 10, 135, 10)) + + +if __name__ == "__main__": + unittest.main() + From c8b0e26971be2468a59c1bcfb2ce376da035c635 Mon Sep 17 00:00:00 2001 From: John Evans Date: Fri, 21 Jun 2013 06:34:33 -0400 Subject: [PATCH 02/25] Added FileTypeBox default constructor defaults. --- glymur/jp2box.py | 8 +++++++- glymur/test/test_jp2box.py | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index aa92f2a..bde8a67 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -596,8 +596,14 @@ class FileTypeBox(Jp2kBox): List of file conformance profiles. """ def __init__(self, **kwargs): - Jp2kBox.__init__(self, id='', longname='File Type') + Jp2kBox.__init__(self, id='ftyp', longname='File Type') self.__dict__.update(**kwargs) + if 'brand' not in kwargs.keys(): + self.brand = 'jp2 ' + if 'minor_version' not in kwargs.keys(): + self.minor_version = 0 + if 'compatibility_box' not in kwargs.keys(): + self.compatibility_box = ['jp2 '] def __str__(self): lst = [Jp2kBox.__str__(self), diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 40b85b8..3dca867 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -21,6 +21,13 @@ class TestJp2Boxes(unittest.TestCase): b = glymur.jp2box.JPEG2000SignatureBox() self.assertEqual(b.signature, (13, 10, 135, 10)) + def test_default_FileTypeBox(self): + # Should be able to instantiate a FileTypeBox + b = glymur.jp2box.FileTypeBox() + self.assertEqual(b.brand, 'jp2 ') + self.assertEqual(b.minor_version, 0) + self.assertEqual(b.compatibility_box, ['jp2 ']) + if __name__ == "__main__": unittest.main() From dd1c20480c828744b3c56f6bb91ae5ec2633cdbe Mon Sep 17 00:00:00 2001 From: John Evans Date: Sat, 22 Jun 2013 07:48:44 -0400 Subject: [PATCH 03/25] Added default ImageHeaderBox case. --- glymur/jp2box.py | 59 ++++++++++++++++++++++++-------------- glymur/test/test_jp2box.py | 16 +++++++++++ 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index bde8a67..401106f 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -684,30 +684,47 @@ class ImageHeaderBox(Jp2kBox): Bits per component. signed : bool False if the image components are unsigned. - compression : nt + compression : int The compression type, should be 7 if JP2. - cspace_unknown : int - 0 if the color space is known and correctly specified. - ip_provided : int - 0 if the file does not contain intellectual propery rights information. + cspace_unknown : bool + false if the color space is known and correctly specified. + ip_provided : bool + false if the file does not contain intellectual propery rights information. """ - def __init__(self, **kwargs): - Jp2kBox.__init__(self, id='', longname='Image Header') + def __init__(self, *pargs, **kwargs): + """ + Examples + -------- + >>> import glymur + >>> box = glymur.jp2box.ImageHeaderBox([512, 256, 3]) + """ + Jp2kBox.__init__(self, id='ihdr', longname='Image Header') self.__dict__.update(**kwargs) + if len(pargs) == 1: + self.height = pargs[0][0] + self.width = pargs[0][1] + self.num_components = pargs[0][2] + self.bits_per_component = 8 + self.signed = False + self.compression = 7 + self.cspace_unknown = False + self.ip_provided = False def __str__(self): - lst = [Jp2kBox.__str__(self)] - lst.append('Size: [{0} {1} {2}]'.format(self.height, self.width, - self.num_components)) - lst.append('Bitdepth: {0}'.format(self.bits_per_component)) - lst.append('Signed: {0}'.format(self.signed)) - if self.compression == 7: - lst.append('Compression: wavelet') - if self.cspace_unknown: - lst.append('Colorspace Unknown: True') - else: - lst.append('Colorspace Unknown: False') - return '\n '.join(lst) + msg = Jp2kBox.__str__(self) + msg = "{0}" + msg += '\n Size: [{1} {2} {3}]' + msg += '\n Bitdepth: {4}' + msg += '\n Signed: {5}' + msg += '\n Compression: {6}' + msg += '\n Colorspace Unknown: {7}' + msg = msg.format(Jp2kBox.__str__(self), + self.height, self.width, self.num_components, + self.bits_per_component, + self.signed, + 'wavelet' if self.compression == 7 else 'unknown', + self.cspace_unknown) + return msg @staticmethod def _parse(f, id, offset, length): @@ -741,8 +758,8 @@ class ImageHeaderBox(Jp2kBox): kwargs['bits_per_component'] = (params[3] & 0x7f) + 1 kwargs['signed'] = (params[3] & 0x80) > 1 kwargs['compression'] = params[4] - kwargs['cspace_unknown'] = params[5] - kwargs['ip_provided'] = params[6] + kwargs['cspace_unknown'] = True if params[5] else False + kwargs['ip_provided'] = True if params[6] else False box = ImageHeaderBox(**kwargs) return box diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 3dca867..8aa57c2 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -1,3 +1,4 @@ +import doctest import unittest import numpy as np @@ -5,6 +6,11 @@ import pkg_resources import glymur +# Doc tests should be run as well. +def load_tests(loader, tests, ignore): + tests.addTests(doctest.DocTestSuite('glymur.jp2box')) + return tests + @unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, "Missing openjp2 library.") class TestJp2Boxes(unittest.TestCase): @@ -28,6 +34,16 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(b.minor_version, 0) self.assertEqual(b.compatibility_box, ['jp2 ']) + def test_default_ImageHeaderBox(self): + # Should be able to instantiate an image header box. + b = glymur.jp2box.ImageHeaderBox([512, 256, 3]) + self.assertEqual(b.height, 512) + self.assertEqual(b.width, 256) + self.assertEqual(b.num_components, 3) + self.assertEqual(b.bits_per_component, 8) + self.assertFalse(b.signed) + self.assertFalse(b.cspace_unknown) + if __name__ == "__main__": unittest.main() From d3fe2c3df233c37e5440423a56412ebfa0054837 Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 22 Jun 2013 15:44:59 -0400 Subject: [PATCH 04/25] Added defaults to ColourSpecificationBox class --- glymur/jp2box.py | 51 +++++++++++++++++++---------- glymur/test/test_jp2box.py | 32 ++++++++++++++++-- glymur/test/test_jp2k.py | 2 +- glymur/test/test_opj_suite.py | 44 ++++++++++++------------- glymur/test/test_opj_suite_write.py | 2 +- 5 files changed, 87 insertions(+), 44 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 401106f..ff0ce85 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -31,6 +31,7 @@ from .core import _colorspace_map_display from .core import _color_type_map_display from .core import _method_display from .core import _reader_requirements_display +from .core import * class Jp2kBox(object): @@ -164,9 +165,22 @@ class ColourSpecificationBox(Jp2kBox): ICC profile header according to ICC profile specification. If colorspace is not None, then icc_profile must be empty. """ - def __init__(self, **kwargs): + def __init__(self, method=ENUMERATED_COLORSPACE, precedence=0, + approximation=1, colorspace=None, icc_profile=None, **kwargs): Jp2kBox.__init__(self, id='', longname='Colour Specification') + + if colorspace is not None and icc_profile is not None: + raise IOError("colorspace and icc_profile cannot both be set.") + if method not in (1, 2, 3, 4): + raise IOError("Invalid method.") + if approximation not in (0, 1, 2, 3, 4): + raise IOError("Invalid approximation.") self.__dict__.update(**kwargs) + self.method = method + self.precedence = precedence + self.approximation = approximation + self.colorspace = colorspace + self.icc_profile = icc_profile def __str__(self): msg = Jp2kBox.__str__(self) @@ -686,29 +700,30 @@ class ImageHeaderBox(Jp2kBox): False if the image components are unsigned. compression : int The compression type, should be 7 if JP2. - cspace_unknown : bool + colorspace_unknown : bool false if the color space is known and correctly specified. ip_provided : bool false if the file does not contain intellectual propery rights information. """ - def __init__(self, *pargs, **kwargs): + def __init__(self, height, width, num_components=1, signed=False, + bits_per_component=8, compression=7, colorspace_unknown=False, + ip_provided=False, **kwargs): """ Examples -------- >>> import glymur - >>> box = glymur.jp2box.ImageHeaderBox([512, 256, 3]) + >>> box = glymur.jp2box.ImageHeaderBox(height=512, width=256) """ Jp2kBox.__init__(self, id='ihdr', longname='Image Header') + self.height = height + self.width = width + self.num_components = num_components + self.signed = signed + self.bits_per_component = bits_per_component + self.compression = compression + self.colorspace_unknown = colorspace_unknown + self.ip_provided = False self.__dict__.update(**kwargs) - if len(pargs) == 1: - self.height = pargs[0][0] - self.width = pargs[0][1] - self.num_components = pargs[0][2] - self.bits_per_component = 8 - self.signed = False - self.compression = 7 - self.cspace_unknown = False - self.ip_provided = False def __str__(self): msg = Jp2kBox.__str__(self) @@ -723,7 +738,7 @@ class ImageHeaderBox(Jp2kBox): self.bits_per_component, self.signed, 'wavelet' if self.compression == 7 else 'unknown', - self.cspace_unknown) + self.colorspace_unknown) return msg @staticmethod @@ -752,16 +767,16 @@ class ImageHeaderBox(Jp2kBox): # Read the box information buffer = f.read(14) params = struct.unpack('>IIHBBBB', buffer) - kwargs['height'] = params[0] - kwargs['width'] = params[1] + height = params[0] + width = params[1] kwargs['num_components'] = params[2] kwargs['bits_per_component'] = (params[3] & 0x7f) + 1 kwargs['signed'] = (params[3] & 0x80) > 1 kwargs['compression'] = params[4] - kwargs['cspace_unknown'] = True if params[5] else False + kwargs['colorspace_unknown'] = True if params[5] else False kwargs['ip_provided'] = True if params[6] else False - box = ImageHeaderBox(**kwargs) + box = ImageHeaderBox(height, width, **kwargs) return box diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 8aa57c2..6a68cc5 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -36,13 +36,41 @@ class TestJp2Boxes(unittest.TestCase): def test_default_ImageHeaderBox(self): # Should be able to instantiate an image header box. - b = glymur.jp2box.ImageHeaderBox([512, 256, 3]) + b = glymur.jp2box.ImageHeaderBox(height=512, width=256, + num_components=3) self.assertEqual(b.height, 512) self.assertEqual(b.width, 256) self.assertEqual(b.num_components, 3) self.assertEqual(b.bits_per_component, 8) self.assertFalse(b.signed) - self.assertFalse(b.cspace_unknown) + self.assertFalse(b.colorspace_unknown) + + def test_default_ColourSpecificationBox(self): + b = glymur.jp2box.ColourSpecificationBox(colorspace=glymur.core.SRGB) + self.assertEqual(b.method, glymur.core.ENUMERATED_COLORSPACE) + self.assertEqual(b.precedence, 0) + self.assertEqual(b.approximation, 1) + self.assertEqual(b.colorspace, glymur.core.SRGB) + self.assertIsNone(b.icc_profile) + + def test_ColourSpecificationBox_with_colorspace_and_icc(self): + # Colour specification boxes can't have both. + with self.assertRaises(IOError): + colorspace = glymur.core.SRGB + icc_profile = b'\x01\x02\x03\x04' + b = glymur.jp2box.ColourSpecificationBox(colorspace, icc_profile) + + def test_ColourSpecificationBox_with_bad_method(self): + colorspace = glymur.core.SRGB + method = -1 + with self.assertRaises(IOError): + b = glymur.jp2box.ColourSpecificationBox(colorspace, method) + + def test_ColourSpecificationBox_with_bad_approximation(self): + colorspace = glymur.core.SRGB + approximation = -1 + with self.assertRaises(IOError): + b = glymur.jp2box.ColourSpecificationBox(colorspace, approximation) if __name__ == "__main__": diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 3c25cca..b15ca35 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -213,7 +213,7 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2k.box[2].box[0].signed, False) self.assertEqual(jp2k.box[2].box[0].compression, 7) - self.assertEqual(jp2k.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2k.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2k.box[2].box[0].ip_provided, False) self.assertEqual(jp2k.box[2].box[1].id, 'colr') diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index 9a7efde..0566d6c 100644 --- a/glymur/test/test_opj_suite.py +++ b/glymur/test/test_opj_suite.py @@ -4116,7 +4116,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[3].box[0].bits_per_component, 8) self.assertEqual(jp2.box[3].box[0].signed, False) self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[3].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[3].box[0].ip_provided, False) # Jp2 Header @@ -4158,7 +4158,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -4204,7 +4204,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -4250,7 +4250,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -4292,7 +4292,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[3].box[0].num_components, 3) self.assertEqual(jp2.box[3].box[0].signed, False) self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[3].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[3].box[0].ip_provided, False) # Jp2 Header @@ -4338,7 +4338,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 12) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -4387,7 +4387,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[3].box[0].bits_per_component, 16) self.assertEqual(jp2.box[3].box[0].signed, False) self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[3].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[3].box[0].ip_provided, False) # Jp2 Header @@ -4436,7 +4436,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -4488,7 +4488,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Palette box. @@ -5985,7 +5985,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -6118,7 +6118,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -6246,7 +6246,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -6386,7 +6386,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[3].box[0].bits_per_component, 8) self.assertEqual(jp2.box[3].box[0].signed, False) self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[3].box[0].cspace_unknown, True) + self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True) self.assertEqual(jp2.box[3].box[0].ip_provided, False) # Jp2 Header @@ -6497,7 +6497,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, True) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, True) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -6597,7 +6597,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[3].box[0].bits_per_component, 8) self.assertEqual(jp2.box[3].box[0].signed, False) self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[3].box[0].cspace_unknown, True) + self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True) self.assertEqual(jp2.box[3].box[0].ip_provided, False) # Jp2 Header @@ -6694,7 +6694,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, True) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, True) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -6813,7 +6813,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[3].box[0].bits_per_component, 1) self.assertEqual(jp2.box[3].box[0].signed, False) self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[3].box[0].cspace_unknown, True) + self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True) self.assertEqual(jp2.box[3].box[0].ip_provided, False) # Jp2 Header @@ -6927,7 +6927,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[3].box[0].bits_per_component, 4) self.assertEqual(jp2.box[3].box[0].signed, False) self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[3].box[0].cspace_unknown, True) + self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True) self.assertEqual(jp2.box[3].box[0].ip_provided, False) # Jp2 Header @@ -7035,7 +7035,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -7142,7 +7142,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -7241,7 +7241,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header @@ -7343,7 +7343,7 @@ class TestSuiteDump(unittest.TestCase): self.assertEqual(jp2.box[3].box[0].bits_per_component, 8) self.assertEqual(jp2.box[3].box[0].signed, False) self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[3].box[0].cspace_unknown, True) + self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True) self.assertEqual(jp2.box[3].box[0].ip_provided, False) # Jp2 Header diff --git a/glymur/test/test_opj_suite_write.py b/glymur/test/test_opj_suite_write.py index 0526fc3..fc86768 100644 --- a/glymur/test/test_opj_suite_write.py +++ b/glymur/test/test_opj_suite_write.py @@ -686,7 +686,7 @@ class TestSuiteWrite(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet - self.assertEqual(jp2.box[2].box[0].cspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) self.assertEqual(jp2.box[2].box[0].ip_provided, False) # Jp2 Header From c03f0caedff3bd44ad319c437b2ccd902621106c Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 22 Jun 2013 20:02:21 -0400 Subject: [PATCH 05/25] Added defaults for ContiguousCodestreamBox and JP2HeaderBox. --- glymur/jp2box.py | 12 +++++++----- glymur/test/test_jp2box.py | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index ff0ce85..3dfa1b8 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -543,9 +543,10 @@ class ContiguousCodestreamBox(Jp2kBox): List of segments in the codestream header. """ - def __init__(self, **kwargs): + def __init__(self, main_header=[], **kwargs): Jp2kBox.__init__(self, id='jp2c', longname='Contiguous Codestream') self.__dict__.update(**kwargs) + self.main_header = main_header def __str__(self): msg = Jp2kBox.__str__(self) @@ -701,12 +702,13 @@ class ImageHeaderBox(Jp2kBox): compression : int The compression type, should be 7 if JP2. colorspace_unknown : bool - false if the color space is known and correctly specified. + False if the color space is known and correctly specified. ip_provided : bool - false if the file does not contain intellectual propery rights information. + False if the file does not contain intellectual propery rights + information. """ def __init__(self, height, width, num_components=1, signed=False, - bits_per_component=8, compression=7, colorspace_unknown=False, + bits_per_component=8, compression=7, colorspace_unknown=False, ip_provided=False, **kwargs): """ Examples @@ -861,7 +863,7 @@ class JP2HeaderBox(Jp2kBox): List of boxes contained in this superbox. """ def __init__(self, **kwargs): - Jp2kBox.__init__(self, id='', longname='JP2 Header') + Jp2kBox.__init__(self, id='jp2h', longname='JP2 Header') self.__dict__.update(**kwargs) def __str__(self): diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 6a68cc5..93b4be0 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -5,12 +5,15 @@ import numpy as np import pkg_resources import glymur +from glymur.jp2box import * + # Doc tests should be run as well. def load_tests(loader, tests, ignore): tests.addTests(doctest.DocTestSuite('glymur.jp2box')) return tests + @unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, "Missing openjp2 library.") class TestJp2Boxes(unittest.TestCase): @@ -23,12 +26,12 @@ class TestJp2Boxes(unittest.TestCase): pass def test_default_JPEG2000SignatureBox(self): - # Should be able to instantiate a JPEG2000SignatureBox + # Should be able to instantiate a JPEG2000SignatureBox b = glymur.jp2box.JPEG2000SignatureBox() self.assertEqual(b.signature, (13, 10, 135, 10)) def test_default_FileTypeBox(self): - # Should be able to instantiate a FileTypeBox + # Should be able to instantiate a FileTypeBox b = glymur.jp2box.FileTypeBox() self.assertEqual(b.brand, 'jp2 ') self.assertEqual(b.minor_version, 0) @@ -72,7 +75,15 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): b = glymur.jp2box.ColourSpecificationBox(colorspace, approximation) + def test_default_JP2HeaderBox(self): + b1 = JP2HeaderBox() + b1.box = [ImageHeaderBox(height=512, width=256), + ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)] + + def test_default_ContiguousCodestreamBox(self): + b = ContiguousCodestreamBox() + self.assertEqual(b.id, 'jp2c') + self.assertEqual(b.main_header, []) if __name__ == "__main__": unittest.main() - From 19336fc15e884c080d19a0fd7de6a4a942cc4266 Mon Sep 17 00:00:00 2001 From: John Evans Date: Sun, 23 Jun 2013 07:41:31 -0400 Subject: [PATCH 06/25] Starting on the _write method. --- glymur/jp2box.py | 41 +++++++++++++++++++++++++++++++++++--- glymur/jp2k.py | 23 ++++++++++++++++++++- glymur/test/__init__.py | 1 + glymur/test/test_jp2box.py | 29 ++++++++++++++++++++++++++- 4 files changed, 89 insertions(+), 5 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 3dfa1b8..e6a7921 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -63,7 +63,8 @@ class Jp2kBox(object): def _write(self, f): """Must be implemented in a subclass. """ - raise NotImplementedError("Not supported for this box.") + msg = "Not supported for {0} box.".format(self.longname) + raise NotImplementedError(msg) def _parse_superbox(self, f): """Parse a superbox (box consisting of nothing but other boxes. @@ -617,8 +618,8 @@ class FileTypeBox(Jp2kBox): self.brand = 'jp2 ' if 'minor_version' not in kwargs.keys(): self.minor_version = 0 - if 'compatibility_box' not in kwargs.keys(): - self.compatibility_box = ['jp2 '] + if 'compatibility_list' not in kwargs.keys(): + self.compatibility_list = ['jp2 '] def __str__(self): lst = [Jp2kBox.__str__(self), @@ -629,6 +630,18 @@ class FileTypeBox(Jp2kBox): return msg + def _write(self, f): + """Write a File Type box to file. + """ + length = 16 + 4*len(self.compatibility_list) + f.write(struct.pack('>I', length)) + f.write('ftyp'.encode()) + f.write(self.brand.encode()) + f.write(struct.pack('>I', self.minor_version)) + + for item in self.compatibility_list: + f.write(item.encode()) + @staticmethod def _parse(f, id, offset, length): """Parse JPEG 2000 file type box. @@ -876,6 +889,21 @@ class JP2HeaderBox(Jp2kBox): msg += ''.join(strs) return msg + def _write(self, f): + """Write a JP2 Header box to file. + """ + # Write the contained boxes, then come back and write the length. + orig_pos = f.tell() + f.write(struct.pack('>I', 0)) + f.write('jp2h'.encode()) + for box in self.box: + box._write(f) + + end_pos = f.tell() + f.seek(orig_pos) + f.write(struct.pack('>I', end_pos - orig_pos)) + f.seek(end_pos) + @staticmethod def _parse(f, id, offset, length): """Parse JPEG 2000 header box. @@ -938,6 +966,13 @@ class JPEG2000SignatureBox(Jp2kBox): msg = msg.format(*self.signature) return msg + def _write(self, f): + """Write a JPEG 2000 Signature box to file. + """ + f.write(struct.pack('>I', 12)) + f.write(self.id.encode()) + f.write(struct.pack('>BBBB', *self.signature)) + @staticmethod def _parse(f, id, offset, length): """Parse JPEG 2000 signature box. diff --git a/glymur/jp2k.py b/glymur/jp2k.py index b09271e..fcd0893 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -17,7 +17,8 @@ import numpy as np from .codestream import Codestream from .core import progression_order -from .jp2box import Jp2kBox +from .core import SRGB +from .jp2box import * from .lib import openjp2 as opj2 _cspace_map = {'rgb': opj2._CLRSPC_SRGB, @@ -398,6 +399,26 @@ class Jp2k(Jp2kBox): self._parse() + def tojp2(self, filename): + """Write a codestream, wrapping it in the JP2 format. + + Parameters + ---------- + filename : str + JP2 file to be created from a raw codestream. + """ + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox(), + ContiguousCodestreamBox()] + boxes[2].box = [ImageHeaderBox(height=1456, + width=2592, + num_components=3), + ColourSpecificationBox(colorspace=SRGB)] + with open(filename, 'wb') as fp: + for box in boxes: + box._write(fp) + def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False): """Read a JPEG 2000 image. diff --git a/glymur/test/__init__.py b/glymur/test/__init__.py index 1e19d88..aec2d72 100644 --- a/glymur/test/__init__.py +++ b/glymur/test/__init__.py @@ -6,3 +6,4 @@ from .test_printing import TestPrinting as printing from .test_opj_suite import TestSuite as suite from .test_opj_suite_write import TestSuiteWrite as suitew from .test_opj_suite_neg import TestSuiteNegative as suiteneg +from .test_jp2box import TestJp2Boxes as box diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 93b4be0..bb068d2 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -1,10 +1,12 @@ import doctest +import tempfile import unittest import numpy as np import pkg_resources import glymur +from glymur import Jp2k from glymur.jp2box import * @@ -18,6 +20,26 @@ def load_tests(loader, tests, ignore): "Missing openjp2 library.") class TestJp2Boxes(unittest.TestCase): + @classmethod + def setUpClass(cls): + # We need a raw codestream, so use the one in nemo.jp2. + jp2file = pkg_resources.resource_filename(glymur.__name__, + "data/nemo.jp2") + j = Jp2k(jp2file) + c = [box for box in j.box if box.id == 'jp2c'][0] + + with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as ofile: + with open(jp2file, 'rb') as ifile: + # Everything up until the jp2c box. + ifile.seek(c.offset+8) + ofile.write(ifile.read(c.length)) + + cls.raw_codestream = ofile.name + + @classmethod + def tearDownClass(cls): + os.unlink(cls.raw_codestream) + def setUp(self): self.jp2file = pkg_resources.resource_filename(glymur.__name__, "data/nemo.jp2") @@ -35,7 +57,7 @@ class TestJp2Boxes(unittest.TestCase): b = glymur.jp2box.FileTypeBox() self.assertEqual(b.brand, 'jp2 ') self.assertEqual(b.minor_version, 0) - self.assertEqual(b.compatibility_box, ['jp2 ']) + self.assertEqual(b.compatibility_list, ['jp2 ']) def test_default_ImageHeaderBox(self): # Should be able to instantiate an image header box. @@ -85,5 +107,10 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(b.id, 'jp2c') self.assertEqual(b.main_header, []) + def test_tojp2(self): + j = Jp2k(self.raw_codestream) + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j.tojp2(tfile.name) + if __name__ == "__main__": unittest.main() From 2bb1d874b02700e32e72cd3e4d40059e9d621b08 Mon Sep 17 00:00:00 2001 From: John Evans Date: Sun, 23 Jun 2013 11:36:56 -0400 Subject: [PATCH 07/25] Success with default conversion from j2c to jp2. --- glymur/jp2box.py | 35 ++++++++++++++++++- glymur/jp2k.py | 34 ++++++++++++++---- glymur/test/test_jp2box.py | 70 +++++++++++++++++++++++++++++++++++--- 3 files changed, 127 insertions(+), 12 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index e6a7921..fd0a356 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -167,7 +167,7 @@ class ColourSpecificationBox(Jp2kBox): colorspace is not None, then icc_profile must be empty. """ def __init__(self, method=ENUMERATED_COLORSPACE, precedence=0, - approximation=1, colorspace=None, icc_profile=None, **kwargs): + approximation=0, colorspace=None, icc_profile=None, **kwargs): Jp2kBox.__init__(self, id='', longname='Colour Specification') if colorspace is not None and icc_profile is not None: @@ -208,6 +208,20 @@ class ColourSpecificationBox(Jp2kBox): return msg + def _write(self, f): + """Write an Colour Specification box to file. + """ + length = 15 if self.icc_profile is None else 11 + len(self.icc_profile) + f.write(struct.pack('>I', length)) + f.write('colr'.encode()) + + buffer = struct.pack('>BBBI', + self.method, + self.precedence, + self.approximation, + self.colorspace) + f.write(buffer) + @staticmethod def _parse(f, id, offset, length): """Parse JPEG 2000 color specification box. @@ -756,6 +770,25 @@ class ImageHeaderBox(Jp2kBox): self.colorspace_unknown) return msg + def _write(self, f): + """Write an Image Header box to file. + """ + f.write(struct.pack('>I', 22)) + f.write('ihdr'.encode()) + + # signedness and bps are stored together in a single byte + bit_depth_signedness = 0x80 if self.signed else 0x00 + bit_depth_signedness |= self.bits_per_component - 1 + buffer = struct.pack('>IIHBBBB', + self.height, + self.width, + self.num_components, + bit_depth_signedness, + self.compression, + 1 if self.colorspace_unknown else 0, + 1 if self.ip_provided else 0) + f.write(buffer) + @staticmethod def _parse(f, id, offset, length): """Parse JPEG 2000 image header box. diff --git a/glymur/jp2k.py b/glymur/jp2k.py index fcd0893..0409c03 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -407,17 +407,37 @@ class Jp2k(Jp2kBox): filename : str JP2 file to be created from a raw codestream. """ + if len(self.box) > 0: + msg = "This method can only be used on files consisting only of " + msg += "a raw codestream. It cannot be used on a file that " + msg += "already has the JP2 format." + raise IOError(msg) + boxes = [JPEG2000SignatureBox(), FileTypeBox(), - JP2HeaderBox(), - ContiguousCodestreamBox()] - boxes[2].box = [ImageHeaderBox(height=1456, - width=2592, - num_components=3), + JP2HeaderBox()] + c = self.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ImageHeaderBox(height=height, + width=width, + num_components=num_components), ColourSpecificationBox(colorspace=SRGB)] - with open(filename, 'wb') as fp: + with open(filename, 'wb') as ofile: for box in boxes: - box._write(fp) + box._write(ofile) + + # The codestream gets written last. + ofile.write(struct.pack('>I', self.length + 8)) + ofile.write('jp2c'.encode()) + + with open(self.filename, 'rb') as ifile: + ofile.write(ifile.read()) + + ofile.flush() + + def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False): """Read a JPEG 2000 image. diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index bb068d2..27bf303 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -28,7 +28,7 @@ class TestJp2Boxes(unittest.TestCase): j = Jp2k(jp2file) c = [box for box in j.box if box.id == 'jp2c'][0] - with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as ofile: + with tempfile.NamedTemporaryFile(suffix='.j2c', delete=False) as ofile: with open(jp2file, 'rb') as ifile: # Everything up until the jp2c box. ifile.seek(c.offset+8) @@ -74,7 +74,7 @@ class TestJp2Boxes(unittest.TestCase): b = glymur.jp2box.ColourSpecificationBox(colorspace=glymur.core.SRGB) self.assertEqual(b.method, glymur.core.ENUMERATED_COLORSPACE) self.assertEqual(b.precedence, 0) - self.assertEqual(b.approximation, 1) + self.assertEqual(b.approximation, 0) self.assertEqual(b.colorspace, glymur.core.SRGB) self.assertIsNone(b.icc_profile) @@ -108,9 +108,71 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(b.main_header, []) def test_tojp2(self): - j = Jp2k(self.raw_codestream) + j2k = Jp2k(self.raw_codestream) + with tempfile.NamedTemporaryFile(suffix=".jp2", mode="wb") as tfile: + j2k.tojp2(tfile.name) + + jp2 = Jp2k(tfile.name) + self.assertEqual(len(jp2.box), 4) + + self.assertEqual(jp2.box[0].id, 'jP ') + self.assertEqual(jp2.box[0].offset, 0) + self.assertEqual(jp2.box[0].length, 12) + self.assertEqual(jp2.box[0].longname, 'JPEG 2000 Signature') + + self.assertEqual(jp2.box[1].id, 'ftyp') + self.assertEqual(jp2.box[1].offset, 12) + self.assertEqual(jp2.box[1].length, 20) + self.assertEqual(jp2.box[1].longname, 'File Type') + + self.assertEqual(jp2.box[2].id, 'jp2h') + self.assertEqual(jp2.box[2].offset, 32) + self.assertEqual(jp2.box[2].length, 45) + self.assertEqual(jp2.box[2].longname, 'JP2 Header') + + self.assertEqual(jp2.box[3].id, 'jp2c') + self.assertEqual(jp2.box[3].offset, 77) + self.assertEqual(jp2.box[3].length, 1133427) + + # jp2h super box + self.assertEqual(len(jp2.box[2].box), 2) + + self.assertEqual(jp2.box[2].box[0].id, 'ihdr') + self.assertEqual(jp2.box[2].box[0].offset, 40) + self.assertEqual(jp2.box[2].box[0].length, 22) + self.assertEqual(jp2.box[2].box[0].longname, 'Image Header') + self.assertEqual(jp2.box[2].box[0].height, 1456) + self.assertEqual(jp2.box[2].box[0].width, 2592) + self.assertEqual(jp2.box[2].box[0].num_components, 3) + self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) + self.assertEqual(jp2.box[2].box[0].signed, False) + self.assertEqual(jp2.box[2].box[0].compression, 7) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].ip_provided, False) + + self.assertEqual(jp2.box[2].box[1].id, 'colr') + self.assertEqual(jp2.box[2].box[1].offset, 62) + self.assertEqual(jp2.box[2].box[1].length, 15) + self.assertEqual(jp2.box[2].box[1].longname, 'Colour Specification') + self.assertEqual(jp2.box[2].box[1].precedence, 0) + self.assertEqual(jp2.box[2].box[1].approximation, 0) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) + self.assertIsNone(jp2.box[2].box[1].icc_profile) + + def test_image_header_box_not_first_in_jp2_header(self): + # The specification says that ihdr must be the first box in jp2h. + self.assertTrue(False) + + def test_color_specification_box_with_icc_profile(self): + # Not sure how this should be done so don't allow it. + self.assertTrue(False) + + def test_tojp2_on_jp2(self): + # Should not use "tojp2" method on file that is already jp2. + j = glymur.Jp2k(self.jp2file) with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - j.tojp2(tfile.name) + with self.assertRaises(IOError): + j.tojp2(tfile.name) if __name__ == "__main__": unittest.main() From 79a26135bfeedd931bf5b88e5723ac7598a18532 Mon Sep 17 00:00:00 2001 From: John Evans Date: Sun, 23 Jun 2013 18:06:15 -0400 Subject: [PATCH 08/25] Changed "tojp2" to "wrap". Added goodstuff.j2k --- glymur/data/__init__.py | 19 +++++- glymur/data/goodstuff.j2k | Bin 0 -> 115220 bytes glymur/jp2box.py | 10 ++++ glymur/jp2k.py | 30 +++++----- glymur/test/test_jp2box.py | 117 ++++++++++++++++++++++++++++++++++--- 5 files changed, 152 insertions(+), 24 deletions(-) create mode 100644 glymur/data/goodstuff.j2k diff --git a/glymur/data/__init__.py b/glymur/data/__init__.py index ff39c0e..3d82c0e 100644 --- a/glymur/data/__init__.py +++ b/glymur/data/__init__.py @@ -1,8 +1,10 @@ """Shipping JPEG 2000 files. -There is only one JP2 file at the moment, "nemo.jp2", converted from the -original JPEG photo of the aftermath of NEMO, the nor'easter that shutdown -Boston in February of 2013. +These include: + nemo.jp2: converted from the original JPEG photo of the aftermath of NEMO, + the nor'easter that shutdown Boston in February of 2013. + goodstuff.j2k: my favorite bevorage. + """ import pkg_resources @@ -17,3 +19,14 @@ def nemo(): """ file = pkg_resources.resource_filename(__name__, "nemo.jp2") return file + +def goodstuff(): + """Shortcut for specifying path to goodstuff.j2k. + + Returns + ------- + file : str + Platform-independent path to goodstuff.j2k. + """ + file = pkg_resources.resource_filename(__name__, "goodstuff.j2k") + return file diff --git a/glymur/data/goodstuff.j2k b/glymur/data/goodstuff.j2k new file mode 100644 index 0000000000000000000000000000000000000000..3e3c1b6eba9b82cac88194236b13462ee1ec7b51 GIT binary patch literal 115220 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 @~^hF(ZTF@H`}+ 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 literal 0 HcmV?d00001 diff --git a/glymur/jp2box.py b/glymur/jp2box.py index fd0a356..09d5251 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -211,6 +211,10 @@ class ColourSpecificationBox(Jp2kBox): def _write(self, f): """Write an Colour Specification box to file. """ + if self.colorspace is None: + msg = "Writing Colour Specification boxes without enumerated " + msg += "colorspaces is not supported at this time." + raise NotImplementedError(msg) length = 15 if self.icc_profile is None else 11 + len(self.icc_profile) f.write(struct.pack('>I', length)) f.write('colr'.encode()) @@ -925,6 +929,12 @@ class JP2HeaderBox(Jp2kBox): def _write(self, f): """Write a JP2 Header box to file. """ + # Make sure they are in proper order. + if self.box[0].id != 'ihdr': + msg = "The first box in the JP2 Header superbox must be the Image " + msg += "Header box." + raise IOError(msg) + # Write the contained boxes, then come back and write the length. orig_pos = f.tell() f.write(struct.pack('>I', 0)) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 0409c03..4d679b9 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -399,13 +399,15 @@ class Jp2k(Jp2kBox): self._parse() - def tojp2(self, filename): + def wrap(self, filename, boxes=None): """Write a codestream, wrapping it in the JP2 format. Parameters ---------- filename : str - JP2 file to be created from a raw codestream. + JP2 file to be created from a raw codestream. + boxes : list + JP2 box definitions to define the JP2 file format. """ if len(self.box) > 0: msg = "This method can only be used on files consisting only of " @@ -413,17 +415,19 @@ class Jp2k(Jp2kBox): msg += "already has the JP2 format." raise IOError(msg) - boxes = [JPEG2000SignatureBox(), - FileTypeBox(), - JP2HeaderBox()] - c = self.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - boxes[2].box = [ImageHeaderBox(height=height, - width=width, - num_components=num_components), - ColourSpecificationBox(colorspace=SRGB)] + if boxes is None: + # Try to create a reasonable default. + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox()] + c = self.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ImageHeaderBox(height=height, + width=width, + num_components=num_components), + ColourSpecificationBox(colorspace=SRGB)] with open(filename, 'wb') as ofile: for box in boxes: box._write(ofile) diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 27bf303..17cb70e 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -107,10 +107,10 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(b.id, 'jp2c') self.assertEqual(b.main_header, []) - def test_tojp2(self): + def test_wrap(self): j2k = Jp2k(self.raw_codestream) with tempfile.NamedTemporaryFile(suffix=".jp2", mode="wb") as tfile: - j2k.tojp2(tfile.name) + j2k.wrap(tfile.name) jp2 = Jp2k(tfile.name) self.assertEqual(len(jp2.box), 4) @@ -159,20 +159,121 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) self.assertIsNone(jp2.box[2].box[1].icc_profile) + def test_default_layout_but_with_specified_boxes(self): + j2k = Jp2k(self.raw_codestream) + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox()] + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ImageHeaderBox(height=height, + width=width, + num_components=num_components), + ColourSpecificationBox(colorspace=glymur.core.SRGB)] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + + jp2 = Jp2k(tfile.name) + self.assertEqual(len(jp2.box), 4) + + self.assertEqual(jp2.box[0].id, 'jP ') + self.assertEqual(jp2.box[0].offset, 0) + self.assertEqual(jp2.box[0].length, 12) + self.assertEqual(jp2.box[0].longname, 'JPEG 2000 Signature') + + self.assertEqual(jp2.box[1].id, 'ftyp') + self.assertEqual(jp2.box[1].offset, 12) + self.assertEqual(jp2.box[1].length, 20) + self.assertEqual(jp2.box[1].longname, 'File Type') + + self.assertEqual(jp2.box[2].id, 'jp2h') + self.assertEqual(jp2.box[2].offset, 32) + self.assertEqual(jp2.box[2].length, 45) + self.assertEqual(jp2.box[2].longname, 'JP2 Header') + + self.assertEqual(jp2.box[3].id, 'jp2c') + self.assertEqual(jp2.box[3].offset, 77) + self.assertEqual(jp2.box[3].length, 1133427) + + # jp2h super box + self.assertEqual(len(jp2.box[2].box), 2) + + self.assertEqual(jp2.box[2].box[0].id, 'ihdr') + self.assertEqual(jp2.box[2].box[0].offset, 40) + self.assertEqual(jp2.box[2].box[0].length, 22) + self.assertEqual(jp2.box[2].box[0].longname, 'Image Header') + self.assertEqual(jp2.box[2].box[0].height, 1456) + self.assertEqual(jp2.box[2].box[0].width, 2592) + self.assertEqual(jp2.box[2].box[0].num_components, 3) + self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) + self.assertEqual(jp2.box[2].box[0].signed, False) + self.assertEqual(jp2.box[2].box[0].compression, 7) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].ip_provided, False) + + self.assertEqual(jp2.box[2].box[1].id, 'colr') + self.assertEqual(jp2.box[2].box[1].offset, 62) + self.assertEqual(jp2.box[2].box[1].length, 15) + self.assertEqual(jp2.box[2].box[1].longname, + 'Colour Specification') + self.assertEqual(jp2.box[2].box[1].precedence, 0) + self.assertEqual(jp2.box[2].box[1].approximation, 0) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) + self.assertIsNone(jp2.box[2].box[1].icc_profile) + def test_image_header_box_not_first_in_jp2_header(self): # The specification says that ihdr must be the first box in jp2h. - self.assertTrue(False) + j2k = Jp2k(self.raw_codestream) + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox()] + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ColourSpecificationBox(colorspace=glymur.core.SRGB), + ImageHeaderBox(height=height, + width=width, + num_components=num_components)] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) - def test_color_specification_box_with_icc_profile(self): - # Not sure how this should be done so don't allow it. - self.assertTrue(False) - def test_tojp2_on_jp2(self): + def test_color_specification_box_with_out_enumerated_colorspace(self): + j2k = Jp2k(self.raw_codestream) + boxes = [JPEG2000SignatureBox(), + FileTypeBox(), + JP2HeaderBox()] + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + boxes[2].box = [ImageHeaderBox(height=height, + width=width, + num_components=num_components), + ColourSpecificationBox(colorspace=None)] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(NotImplementedError): + j2k.wrap(tfile.name, boxes=boxes) + + def test_wrap_on_jp2(self): # Should not use "tojp2" method on file that is already jp2. j = glymur.Jp2k(self.jp2file) with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: with self.assertRaises(IOError): - j.tojp2(tfile.name) + j.wrap(tfile.name) + + def test_default_xml(self): + # Should be able to write an xml box. + self.assertTrue(False) + + def test_default_component_definition(self): + # Should be able to specify a component definition box in order to, + # say, create an image with an alpha layer. + self.assertTrue(False) if __name__ == "__main__": unittest.main() From 2ca9c14724c8a51982df29200def07b908ded435 Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 24 Jun 2013 18:58:17 -0400 Subject: [PATCH 09/25] Closes #63 --- glymur/jp2k.py | 34 +++++++++++++++++++--------------- glymur/test/test_jp2box.py | 31 +++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 4d679b9..1a1e3f2 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -409,17 +409,12 @@ class Jp2k(Jp2kBox): boxes : list JP2 box definitions to define the JP2 file format. """ - if len(self.box) > 0: - msg = "This method can only be used on files consisting only of " - msg += "a raw codestream. It cannot be used on a file that " - msg += "already has the JP2 format." - raise IOError(msg) - if boxes is None: # Try to create a reasonable default. boxes = [JPEG2000SignatureBox(), FileTypeBox(), - JP2HeaderBox()] + JP2HeaderBox(), + ContiguousCodestreamBox()] c = self.get_codestream() height = c.segment[1].Ysiz width = c.segment[1].Xsiz @@ -428,16 +423,25 @@ class Jp2k(Jp2kBox): width=width, num_components=num_components), ColourSpecificationBox(colorspace=SRGB)] + + # Check for a bad sequence of boxes. + # 1st two boxes must be 'jP ' and 'ftyp' + if boxes[0].id != 'jP ' or boxes[1].id != 'ftyp': + msg = "The first box must be the signature box and the second " + msg += "must be the file type box." + raise IOError(msg) + with open(filename, 'wb') as ofile: for box in boxes: - box._write(ofile) - - # The codestream gets written last. - ofile.write(struct.pack('>I', self.length + 8)) - ofile.write('jp2c'.encode()) - - with open(self.filename, 'rb') as ifile: - ofile.write(ifile.read()) + if box.id != 'jp2c': + box._write(ofile) + else: + # The codestream gets written last. + ofile.write(struct.pack('>I', self.length + 8)) + ofile.write('jp2c'.encode()) + + with open(self.filename, 'rb') as ifile: + ofile.write(ifile.read()) ofile.flush() diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 17cb70e..291b1ba 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -163,7 +163,8 @@ class TestJp2Boxes(unittest.TestCase): j2k = Jp2k(self.raw_codestream) boxes = [JPEG2000SignatureBox(), FileTypeBox(), - JP2HeaderBox()] + JP2HeaderBox(), + ContiguousCodestreamBox()] c = j2k.get_codestream() height = c.segment[1].Ysiz width = c.segment[1].Xsiz @@ -259,13 +260,6 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(NotImplementedError): j2k.wrap(tfile.name, boxes=boxes) - def test_wrap_on_jp2(self): - # Should not use "tojp2" method on file that is already jp2. - j = glymur.Jp2k(self.jp2file) - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - with self.assertRaises(IOError): - j.wrap(tfile.name) - def test_default_xml(self): # Should be able to write an xml box. self.assertTrue(False) @@ -275,5 +269,26 @@ class TestJp2Boxes(unittest.TestCase): # say, create an image with an alpha layer. self.assertTrue(False) + def test_first_2_boxes_not_jP_and_ftyp(self): + j2k = Jp2k(self.raw_codestream) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + jP = JPEG2000SignatureBox() + ftyp = FileTypeBox() + jp2h = JP2HeaderBox() + jp2c = ContiguousCodestreamBox() + colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + jp2h.box = [ihdr, colr] + boxes = [ftyp, jP, jp2h, jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) + + if __name__ == "__main__": unittest.main() From 90c027e5ca65c6ee2852c03fb7e65ca33945b03b Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 24 Jun 2013 19:04:12 -0400 Subject: [PATCH 10/25] Closes #62 --- glymur/jp2k.py | 9 +++++++++ glymur/test/test_jp2box.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 1a1e3f2..ea24b54 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -431,6 +431,15 @@ class Jp2k(Jp2kBox): msg += "must be the file type box." raise IOError(msg) + # jp2c must be preceeded by jp2h + jp2h_lst = [idx for (idx, box) in enumerate(boxes) if box.id == 'jp2h'] + jp2h_idx = jp2h_lst[0] + jp2c_lst = [idx for (idx, box) in enumerate(boxes) if box.id == 'jp2c'] + jp2c_idx = jp2c_lst[0] + if jp2h_idx >= jp2c_idx: + msg = "The codestream box must be preceeded by a jp2 header box." + raise IOError(msg) + with open(filename, 'wb') as ofile: for box in boxes: if box.id != 'jp2c': diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 291b1ba..78591a4 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -289,6 +289,26 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) + def test_jp2h_not_preceeding_jp2c(self): + j2k = Jp2k(self.raw_codestream) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + jP = JPEG2000SignatureBox() + ftyp = FileTypeBox() + jp2h = JP2HeaderBox() + jp2c = ContiguousCodestreamBox() + colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + jp2h.box = [ihdr, colr] + boxes = [jP, ftyp, jp2c, jp2h] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + #with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) + if __name__ == "__main__": unittest.main() From b095ad9720e59089bba52a02dfb9e2ec6a59088a Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 24 Jun 2013 19:12:23 -0400 Subject: [PATCH 11/25] Closes #61 --- glymur/test/test_jp2box.py | 155 ++++++++++++++----------------------- 1 file changed, 56 insertions(+), 99 deletions(-) diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 78591a4..9c0e56b 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -107,57 +107,59 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(b.id, 'jp2c') self.assertEqual(b.main_header, []) + def verify_wrapped_raw(self, jp2file): + jp2 = Jp2k(jp2file) + self.assertEqual(len(jp2.box), 4) + + self.assertEqual(jp2.box[0].id, 'jP ') + self.assertEqual(jp2.box[0].offset, 0) + self.assertEqual(jp2.box[0].length, 12) + self.assertEqual(jp2.box[0].longname, 'JPEG 2000 Signature') + + self.assertEqual(jp2.box[1].id, 'ftyp') + self.assertEqual(jp2.box[1].offset, 12) + self.assertEqual(jp2.box[1].length, 20) + self.assertEqual(jp2.box[1].longname, 'File Type') + + self.assertEqual(jp2.box[2].id, 'jp2h') + self.assertEqual(jp2.box[2].offset, 32) + self.assertEqual(jp2.box[2].length, 45) + self.assertEqual(jp2.box[2].longname, 'JP2 Header') + + self.assertEqual(jp2.box[3].id, 'jp2c') + self.assertEqual(jp2.box[3].offset, 77) + self.assertEqual(jp2.box[3].length, 1133427) + + # jp2h super box + self.assertEqual(len(jp2.box[2].box), 2) + + self.assertEqual(jp2.box[2].box[0].id, 'ihdr') + self.assertEqual(jp2.box[2].box[0].offset, 40) + self.assertEqual(jp2.box[2].box[0].length, 22) + self.assertEqual(jp2.box[2].box[0].longname, 'Image Header') + self.assertEqual(jp2.box[2].box[0].height, 1456) + self.assertEqual(jp2.box[2].box[0].width, 2592) + self.assertEqual(jp2.box[2].box[0].num_components, 3) + self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) + self.assertEqual(jp2.box[2].box[0].signed, False) + self.assertEqual(jp2.box[2].box[0].compression, 7) + self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) + self.assertEqual(jp2.box[2].box[0].ip_provided, False) + + self.assertEqual(jp2.box[2].box[1].id, 'colr') + self.assertEqual(jp2.box[2].box[1].offset, 62) + self.assertEqual(jp2.box[2].box[1].length, 15) + self.assertEqual(jp2.box[2].box[1].longname, 'Colour Specification') + self.assertEqual(jp2.box[2].box[1].precedence, 0) + self.assertEqual(jp2.box[2].box[1].approximation, 0) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) + self.assertIsNone(jp2.box[2].box[1].icc_profile) + def test_wrap(self): j2k = Jp2k(self.raw_codestream) with tempfile.NamedTemporaryFile(suffix=".jp2", mode="wb") as tfile: j2k.wrap(tfile.name) - - jp2 = Jp2k(tfile.name) - self.assertEqual(len(jp2.box), 4) - - self.assertEqual(jp2.box[0].id, 'jP ') - self.assertEqual(jp2.box[0].offset, 0) - self.assertEqual(jp2.box[0].length, 12) - self.assertEqual(jp2.box[0].longname, 'JPEG 2000 Signature') - - self.assertEqual(jp2.box[1].id, 'ftyp') - self.assertEqual(jp2.box[1].offset, 12) - self.assertEqual(jp2.box[1].length, 20) - self.assertEqual(jp2.box[1].longname, 'File Type') - - self.assertEqual(jp2.box[2].id, 'jp2h') - self.assertEqual(jp2.box[2].offset, 32) - self.assertEqual(jp2.box[2].length, 45) - self.assertEqual(jp2.box[2].longname, 'JP2 Header') - - self.assertEqual(jp2.box[3].id, 'jp2c') - self.assertEqual(jp2.box[3].offset, 77) - self.assertEqual(jp2.box[3].length, 1133427) - - # jp2h super box - self.assertEqual(len(jp2.box[2].box), 2) - - self.assertEqual(jp2.box[2].box[0].id, 'ihdr') - self.assertEqual(jp2.box[2].box[0].offset, 40) - self.assertEqual(jp2.box[2].box[0].length, 22) - self.assertEqual(jp2.box[2].box[0].longname, 'Image Header') - self.assertEqual(jp2.box[2].box[0].height, 1456) - self.assertEqual(jp2.box[2].box[0].width, 2592) - self.assertEqual(jp2.box[2].box[0].num_components, 3) - self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) - self.assertEqual(jp2.box[2].box[0].signed, False) - self.assertEqual(jp2.box[2].box[0].compression, 7) - self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) - self.assertEqual(jp2.box[2].box[0].ip_provided, False) - - self.assertEqual(jp2.box[2].box[1].id, 'colr') - self.assertEqual(jp2.box[2].box[1].offset, 62) - self.assertEqual(jp2.box[2].box[1].length, 15) - self.assertEqual(jp2.box[2].box[1].longname, 'Colour Specification') - self.assertEqual(jp2.box[2].box[1].precedence, 0) - self.assertEqual(jp2.box[2].box[1].approximation, 0) - self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) - self.assertIsNone(jp2.box[2].box[1].icc_profile) + self.verify_wrapped_raw(tfile.name) def test_default_layout_but_with_specified_boxes(self): j2k = Jp2k(self.raw_codestream) @@ -175,61 +177,15 @@ class TestJp2Boxes(unittest.TestCase): ColourSpecificationBox(colorspace=glymur.core.SRGB)] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name, boxes=boxes) - - jp2 = Jp2k(tfile.name) - self.assertEqual(len(jp2.box), 4) - - self.assertEqual(jp2.box[0].id, 'jP ') - self.assertEqual(jp2.box[0].offset, 0) - self.assertEqual(jp2.box[0].length, 12) - self.assertEqual(jp2.box[0].longname, 'JPEG 2000 Signature') - - self.assertEqual(jp2.box[1].id, 'ftyp') - self.assertEqual(jp2.box[1].offset, 12) - self.assertEqual(jp2.box[1].length, 20) - self.assertEqual(jp2.box[1].longname, 'File Type') - - self.assertEqual(jp2.box[2].id, 'jp2h') - self.assertEqual(jp2.box[2].offset, 32) - self.assertEqual(jp2.box[2].length, 45) - self.assertEqual(jp2.box[2].longname, 'JP2 Header') - - self.assertEqual(jp2.box[3].id, 'jp2c') - self.assertEqual(jp2.box[3].offset, 77) - self.assertEqual(jp2.box[3].length, 1133427) - - # jp2h super box - self.assertEqual(len(jp2.box[2].box), 2) - - self.assertEqual(jp2.box[2].box[0].id, 'ihdr') - self.assertEqual(jp2.box[2].box[0].offset, 40) - self.assertEqual(jp2.box[2].box[0].length, 22) - self.assertEqual(jp2.box[2].box[0].longname, 'Image Header') - self.assertEqual(jp2.box[2].box[0].height, 1456) - self.assertEqual(jp2.box[2].box[0].width, 2592) - self.assertEqual(jp2.box[2].box[0].num_components, 3) - self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) - self.assertEqual(jp2.box[2].box[0].signed, False) - self.assertEqual(jp2.box[2].box[0].compression, 7) - self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False) - self.assertEqual(jp2.box[2].box[0].ip_provided, False) - - self.assertEqual(jp2.box[2].box[1].id, 'colr') - self.assertEqual(jp2.box[2].box[1].offset, 62) - self.assertEqual(jp2.box[2].box[1].length, 15) - self.assertEqual(jp2.box[2].box[1].longname, - 'Colour Specification') - self.assertEqual(jp2.box[2].box[1].precedence, 0) - self.assertEqual(jp2.box[2].box[1].approximation, 0) - self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) - self.assertIsNone(jp2.box[2].box[1].icc_profile) + self.verify_wrapped_raw(tfile.name) def test_image_header_box_not_first_in_jp2_header(self): # The specification says that ihdr must be the first box in jp2h. j2k = Jp2k(self.raw_codestream) boxes = [JPEG2000SignatureBox(), FileTypeBox(), - JP2HeaderBox()] + JP2HeaderBox(), + ContiguousCodestreamBox()] c = j2k.get_codestream() height = c.segment[1].Ysiz width = c.segment[1].Xsiz @@ -247,7 +203,8 @@ class TestJp2Boxes(unittest.TestCase): j2k = Jp2k(self.raw_codestream) boxes = [JPEG2000SignatureBox(), FileTypeBox(), - JP2HeaderBox()] + JP2HeaderBox(), + ContiguousCodestreamBox()] c = j2k.get_codestream() height = c.segment[1].Ysiz width = c.segment[1].Xsiz @@ -306,8 +263,8 @@ class TestJp2Boxes(unittest.TestCase): jp2h.box = [ihdr, colr] boxes = [jP, ftyp, jp2c, jp2h] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - #with self.assertRaises(IOError): - j2k.wrap(tfile.name, boxes=boxes) + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) if __name__ == "__main__": From 046b86d0edad36666bfc592a92b55f2395bae91b Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 24 Jun 2013 19:16:03 -0400 Subject: [PATCH 12/25] Closes #60 --- glymur/jp2k.py | 6 ++++++ glymur/test/test_jp2box.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index ea24b54..50c53d8 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -440,6 +440,12 @@ class Jp2k(Jp2kBox): msg = "The codestream box must be preceeded by a jp2 header box." raise IOError(msg) + jp2h = boxes[jp2h_idx] + if jp2h.box[0].id != 'ihdr': + msg = "The first box in the jp2 header box must be the image " + msg += "header box." + raise IOError(msg) + with open(filename, 'wb') as ofile: for box in boxes: if box.id != 'jp2c': diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 9c0e56b..a55f90f 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -266,6 +266,26 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) + def test_ihdr_not_first_in_jp2h(self): + j2k = Jp2k(self.raw_codestream) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + jP = JPEG2000SignatureBox() + ftyp = FileTypeBox() + jp2h = JP2HeaderBox() + jp2c = ContiguousCodestreamBox() + colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + jp2h.box = [colr, ihdr] + boxes = [jP, ftyp, jp2h, jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) + if __name__ == "__main__": unittest.main() From 47f42fdb88935bd8525999a046c653b165f2bf9d Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 24 Jun 2013 19:29:20 -0400 Subject: [PATCH 13/25] Closes #59 --- glymur/jp2box.py | 2 +- glymur/jp2k.py | 8 ++++++++ glymur/test/test_jp2box.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 09d5251..2b012b8 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -168,7 +168,7 @@ class ColourSpecificationBox(Jp2kBox): """ def __init__(self, method=ENUMERATED_COLORSPACE, precedence=0, approximation=0, colorspace=None, icc_profile=None, **kwargs): - Jp2kBox.__init__(self, id='', longname='Colour Specification') + Jp2kBox.__init__(self, id='colr', longname='Colour Specification') if colorspace is not None and icc_profile is not None: raise IOError("colorspace and icc_profile cannot both be set.") diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 50c53d8..015b63b 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -440,12 +440,20 @@ class Jp2k(Jp2kBox): msg = "The codestream box must be preceeded by a jp2 header box." raise IOError(msg) + # 1st jp2 header box must be ihdr jp2h = boxes[jp2h_idx] if jp2h.box[0].id != 'ihdr': msg = "The first box in the jp2 header box must be the image " msg += "header box." raise IOError(msg) + # colr must be present in jp2 header box. + jp2hb = jp2h.box + colr_lst = [j for (j, box) in enumerate(jp2h.box) if box.id == 'colr'] + if len(colr_lst) == 0: + msg = "The jp2 header box must contain a color definition box." + raise IOError(msg) + with open(filename, 'wb') as ofile: for box in boxes: if box.id != 'jp2c': diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index a55f90f..8c0730b 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -286,6 +286,26 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) + def test_colr_box_not_in_jp2h(self): + j2k = Jp2k(self.raw_codestream) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + jP = JPEG2000SignatureBox() + ftyp = FileTypeBox() + jp2h = JP2HeaderBox() + jp2c = ContiguousCodestreamBox() + colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + jp2h.box = [ihdr] + boxes = [jP, ftyp, jp2h, jp2c, colr] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) + if __name__ == "__main__": unittest.main() From f70dd87608260e498c7b6f69b2abca97caac7dc9 Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 24 Jun 2013 20:06:52 -0400 Subject: [PATCH 14/25] Added XML box support. --- glymur/jp2box.py | 10 ++++++- glymur/test/test_jp2box.py | 55 +++++++++++++++++++++++++++++++++++--- glymur/test/test_jp2k.py | 1 - 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 2b012b8..99a1dee 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1538,7 +1538,7 @@ class XMLBox(Jp2kBox): XML section. """ def __init__(self, **kwargs): - Jp2kBox.__init__(self, id='', longname='XML') + Jp2kBox.__init__(self, id='xml ', longname='XML') self.__dict__.update(**kwargs) def __str__(self): @@ -1550,6 +1550,14 @@ class XMLBox(Jp2kBox): msg += '\n {0}'.format(xml) return msg + def _write(self, f): + """Write an XML box to file. + """ + buffer = ET.tostring(self.xml, encoding='utf-8') + f.write(struct.pack('>I', len(buffer) + 8)) + f.write(self.id.encode()) + f.write(buffer) + @staticmethod def _parse(f, id, offset, length): """Parse XML box. diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 8c0730b..2562339 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -1,5 +1,6 @@ import doctest import tempfile +import xml.etree.cElementTree as ET import unittest import numpy as np @@ -47,6 +48,13 @@ class TestJp2Boxes(unittest.TestCase): def tearDown(self): pass + def test_default_XML(self): + # Should be able to instantiate an XMLBox + xml = ET.fromstring('0') + b = glymur.jp2box.XMLBox(xml=xml) + self.assertEqual(ET.tostring(b.xml), + b'0') + def test_default_JPEG2000SignatureBox(self): # Should be able to instantiate a JPEG2000SignatureBox b = glymur.jp2box.JPEG2000SignatureBox() @@ -217,10 +225,6 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(NotImplementedError): j2k.wrap(tfile.name, boxes=boxes) - def test_default_xml(self): - # Should be able to write an xml box. - self.assertTrue(False) - def test_default_component_definition(self): # Should be able to specify a component definition box in order to, # say, create an image with an alpha layer. @@ -306,6 +310,49 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) + def test_missing_colr_box(self): + j2k = Jp2k(self.raw_codestream) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + jP = JPEG2000SignatureBox() + ftyp = FileTypeBox() + jp2h = JP2HeaderBox() + jp2c = ContiguousCodestreamBox() + ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + jp2h.box = [ihdr] + boxes = [jP, ftyp, jp2h, jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) + + def test_basic_xml(self): + j2k = Jp2k(self.raw_codestream) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + jP = JPEG2000SignatureBox() + ftyp = FileTypeBox() + jp2h = JP2HeaderBox() + jp2c = ContiguousCodestreamBox() + ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + jp2h.box = [ihdr, colr] + + the_xml = ET.fromstring('0') + xml = glymur.jp2box.XMLBox(xml=the_xml) + boxes = [jP, ftyp, jp2h, xml, jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + jp2 = Jp2k(tfile.name) + self.assertEqual(jp2.box[3].id, 'xml ') + if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index b15ca35..479c1d7 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -15,7 +15,6 @@ if sys.hexversion <= 0x03030000: else: from unittest.mock import patch import warnings -from xml.etree import cElementTree as ET import numpy as np import pkg_resources From 881132fbef3e6897f42eb2e33dd6bb46d4d1067c Mon Sep 17 00:00:00 2001 From: John Evans Date: Tue, 25 Jun 2013 07:04:24 -0400 Subject: [PATCH 15/25] Closes #57 --- glymur/jp2k.py | 28 +++++++++++++++++++++------- glymur/test/test_jp2box.py | 18 ++++++++++++++++++ glymur/test/test_jp2k.py | 23 +++++++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 015b63b..6f20ae9 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -142,7 +142,7 @@ class Jp2k(Jp2kBox): def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None, cbsize=None, psizes=None, grid_offset=None, sop=False, subsam=None, tilesize=None, prog=None, modesw=None, - colorspace=None, verbose=False): + colorspace=None, verbose=False, mct=None): """Write image data to a JP2/JPX/J2k file. Intended usage of the various parameters follows that of OpenJPEG's opj_compress utility. @@ -168,6 +168,9 @@ class Jp2k(Jp2kBox): If true, write SOP marker after each header packet. grid_offset : tuple, optional Offset (DY, DX) of the origin of the image in the reference grid. + mct : bool, optional + Specifies usage of the multi component transform. If not + specified, defaults to True if the colorspace is RGB. modesw : int, optional Mode switch. 1 = BYPASS(LAZY) @@ -337,6 +340,18 @@ class Jp2k(Jp2kBox): else: colorspace = _cspace_map[colorspace] + if mct is None: + if colorspace == opj2._CLRSPC_SRGB: + cparams.tcp_mct = 1 + else: + cparams.tcp_mct = 0 + else: + if mct and colorspace == opj2._CLRSPC_GRAY: + msg = "Cannot specify usage of the multi component transform " + msg += "if the colorspace is gray." + raise IOError(msg) + cparams.tcp_mct = 1 if mct else 0 + if img_array.dtype == np.uint8: comp_prec = 8 elif img_array.dtype == np.uint16: @@ -373,12 +388,6 @@ class Jp2k(Jp2kBox): src = layer.ctypes.data ctypes.memmove(dest, src, layer.nbytes) - # set multi-component transform? - if image.contents.numcomps == 3: - cparams.tcp_mct = 1 - else: - cparams.tcp_mct = 0 - codec = opj2._create_compress(codec_fmt) if verbose: @@ -435,6 +444,11 @@ class Jp2k(Jp2kBox): jp2h_lst = [idx for (idx, box) in enumerate(boxes) if box.id == 'jp2h'] jp2h_idx = jp2h_lst[0] jp2c_lst = [idx for (idx, box) in enumerate(boxes) if box.id == 'jp2c'] + if len(jp2c_lst) == 0: + msg = "A codestream box must be defined in the outermost " + msg += "list of boxes." + raise IOError(msg) + jp2c_idx = jp2c_lst[0] if jp2h_idx >= jp2c_idx: msg = "The codestream box must be preceeded by a jp2 header box." diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 2562339..bf34347 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -310,6 +310,24 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) + def test_missing_codestream(self): + j2k = Jp2k(self.raw_codestream) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + jP = JPEG2000SignatureBox() + ftyp = FileTypeBox() + jp2h = JP2HeaderBox() + ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + jp2h.box = [ihdr] + boxes = [jP, ftyp, jp2h] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) + def test_missing_colr_box(self): j2k = Jp2k(self.raw_codestream) c = j2k.get_codestream() diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 479c1d7..aab2bca 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -76,6 +76,8 @@ class TestJp2k(unittest.TestCase): def setUp(self): self.jp2file = pkg_resources.resource_filename(glymur.__name__, "data/nemo.jp2") + self.j2kfile = pkg_resources.resource_filename(glymur.__name__, + "data/goodstuff.j2k") def tearDown(self): pass @@ -152,6 +154,27 @@ class TestJp2k(unittest.TestCase): subsetdata = j.read(area=(0, 0, 512, 512)) np.testing.assert_array_equal(tiledata, subsetdata) + def test_write_srgb_without_mct(self): + j2k = Jp2k(self.j2kfile) + expdata = j2k.read() + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + ofile = Jp2k(tfile.name, 'wb') + ofile.write(expdata, mct=False) + actdata = ofile.read() + np.testing.assert_array_equal(actdata, expdata) + + c = ofile.get_codestream() + self.assertEqual(c.segment[2].SPcod[3], 0) # no mct + + def test_write_grayscale_with_mct(self): + # MCT usage makes no sense for grayscale images. + j2k = Jp2k(self.j2kfile) + expdata = j2k.read() + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + ofile = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError): + ofile.write(expdata[:,:,0], mct=True) + def test_write_cprl(self): # Issue 17 j = Jp2k(self.jp2file) From 511f32f2ce19337c4bb822e1884cefa7d9d0b21d Mon Sep 17 00:00:00 2001 From: John Evans Date: Tue, 25 Jun 2013 12:59:37 -0400 Subject: [PATCH 16/25] Added possibility of creating XMLBox by reading an XML file. --- glymur/jp2box.py | 24 +++++++++++++++-- glymur/test/test_jp2box.py | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 99a1dee..363611a 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1537,8 +1537,24 @@ class XMLBox(Jp2kBox): xml : ElementTree.Element XML section. """ - def __init__(self, **kwargs): + def __init__(self, xml=None, filename=None, **kwargs): + """ + Parameters + ---------- + xml : ElementTree + An ElementTree object already existing in python. + filename : str + File from which to read XML. If filename is not None, then the xml + keyword argument must be None. + """ Jp2kBox.__init__(self, id='xml ', longname='XML') + if filename is not None and xml is not None: + msg = "Only one of either filename or xml should be provided." + raise IOError(msg) + if filename is not None: + self.xml = ET.parse(filename) + else: + self.xml = xml self.__dict__.update(**kwargs) def __str__(self): @@ -1553,7 +1569,11 @@ class XMLBox(Jp2kBox): def _write(self, f): """Write an XML box to file. """ - buffer = ET.tostring(self.xml, encoding='utf-8') + try: + buffer = ET.tostring(self.xml, encoding='utf-8') + except AttributeError: + buffer = ET.tostring(self.xml.getroot(), encoding='utf-8') + f.write(struct.pack('>I', len(buffer) + 8)) f.write(self.id.encode()) f.write(buffer) diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index bf34347..32e95a6 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -371,6 +371,60 @@ class TestJp2Boxes(unittest.TestCase): jp2 = Jp2k(tfile.name) self.assertEqual(jp2.box[3].id, 'xml ') + def test_xml_from_file(self): + raw_xml = b""" + + + 1 + 2008 + 141100 + + + + + 4 + 2011 + 59900 + + + + 68 + 2011 + 13600 + + + + """ + with tempfile.NamedTemporaryFile(suffix=".xml") as tfile: + tfile.write(raw_xml) + tfile.flush() + + j2k = Jp2k(self.raw_codestream) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + jP = JPEG2000SignatureBox() + ftyp = FileTypeBox() + jp2h = JP2HeaderBox() + jp2c = ContiguousCodestreamBox() + ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + jp2h.box = [ihdr, colr] + + xmlb = glymur.jp2box.XMLBox(filename=tfile.name) + boxes = [jP, ftyp, jp2h, xmlb, jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + jp2 = Jp2k(tfile.name) + + output_boxes = [box.id for box in jp2.box] + self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ', + 'jp2c']) + self.assertIsNotNone(jp2.box[3].xml) + if __name__ == "__main__": unittest.main() From 2ea53f35cfadf422b7b4fb0cc0cfba10cb5fd941 Mon Sep 17 00:00:00 2001 From: John Evans Date: Tue, 25 Jun 2013 18:22:19 -0400 Subject: [PATCH 17/25] Added two negative tests for ChannelDefinition. --- glymur/jp2box.py | 52 +++++++++++++++++++++++------------ glymur/test/test_jp2box.py | 33 ++++++++++++++++++---- glymur/test/test_opj_suite.py | 8 +++--- glymur/test/test_printing.py | 10 +++---- 4 files changed, 71 insertions(+), 32 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 363611a..034dec5 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -392,7 +392,7 @@ class _ICCProfile(object): self.header = header -class ComponentDefinitionBox(Jp2kBox): +class ChannelDefinitionBox(Jp2kBox): """Container for component definition box information. Attributes @@ -405,27 +405,43 @@ class ComponentDefinitionBox(Jp2kBox): offset of the box from the start of the file. longname : str more verbose description of the box. - component_number : int - number of the component - component_type : int - type of the component + index : int + number of the channel + channel_type : int + type of the channel association : int - number of the associated color + index of the associated color """ - def __init__(self, **kwargs): - Jp2kBox.__init__(self, id='', longname='Component Definition') + def __init__(self, index, channel_type, association, **kwargs): + Jp2kBox.__init__(self, id='cdef', longname='Channel Definition') + if len(index) != len(channel_type) or len(index) != len(association): + msg = "Length of channel definition box inputs must be the same." + raise IOError(msg) + + # channel types must be one of 0, 1, 2, 65535 + if any(x not in [0, 1, 2, 65535] for x in channel_type): + msg = "Channel types must be in the set of\n\n" + msg += " 0 - colour image data for associated color\n" + msg += " 1 - opacity\n" + msg += " 2 - premultiplied opacity\n" + msg += " 65535 - unspecified" + raise IOError(msg) + + self.index = index + self.channel_type = channel_type + self.association = association self.__dict__.update(**kwargs) def __str__(self): msg = Jp2kBox.__str__(self) for j in range(len(self.association)): - color_type_string = _color_type_map_display[self.component_type[j]] + color_type_string = _color_type_map_display[self.channel_type[j]] if self.association[j] == 0: assn = 'whole image' else: assn = str(self.association[j]) - msg += '\n Component {0} ({1}) ==> ({2})' - msg = msg.format(self.component_number[j], color_type_string, assn) + msg += '\n Channel {0} ({1}) ==> ({2})' + msg = msg.format(self.index[j], color_type_string, assn) return msg @staticmethod @@ -456,17 +472,17 @@ class ComponentDefinitionBox(Jp2kBox): buffer = f.read(2) N, = struct.unpack('>H', buffer) - component_number = [] - component_type = [] + index = [] + chan_type = [] association = [] buffer = f.read(N * 6) data = struct.unpack('>' + 'HHH' * N, buffer) - kwargs['component_number'] = data[0:N * 6:3] - kwargs['component_type'] = data[1:N * 6:3] - kwargs['association'] = data[2:N * 6:3] + index = data[0:N * 6:3] + channel_type = data[1:N * 6:3] + association = data[2:N * 6:3] - box = ComponentDefinitionBox(**kwargs) + box = ChannelDefinitionBox(index, channel_type, association, **kwargs) return box @@ -2426,7 +2442,7 @@ class _ExifInteroperabilityIfd(_Ifd): # Map each box ID to the corresponding class. _box_with_id = { 'asoc': AssociationBox, - 'cdef': ComponentDefinitionBox, + 'cdef': ChannelDefinitionBox, 'cmap': ComponentMappingBox, 'colr': ColourSpecificationBox, 'jP ': JPEG2000SignatureBox, diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 32e95a6..4b929cd 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -17,6 +17,34 @@ def load_tests(loader, tests, ignore): return tests +@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, + "Missing openjp2 library.") +class TestChannelDefinition(unittest.TestCase): + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + def tearDown(self): + pass + + def test_bad_type(self): + # Channel types are limited to 0, 1, 2, 65535 + # Should reject if not all of index, channel_type, association the + # same length. + with self.assertRaises(IOError): + box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], + channel_type=[0, 0, 3], + association=[0, 1, 2]) + + def test_wrong_lengths(self): + # Should reject if not all of index, channel_type, association the + # same length. + with self.assertRaises(IOError): + box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], + channel_type=[0, 0], + association=[0, 1, 2]) + @unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, "Missing openjp2 library.") class TestJp2Boxes(unittest.TestCase): @@ -225,11 +253,6 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(NotImplementedError): j2k.wrap(tfile.name, boxes=boxes) - def test_default_component_definition(self): - # Should be able to specify a component definition box in order to, - # say, create an image with an alpha layer. - self.assertTrue(False) - def test_first_2_boxes_not_jP_and_ftyp(self): j2k = Jp2k(self.raw_codestream) c = j2k.get_codestream() diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index 0566d6c..4e47971 100644 --- a/glymur/test/test_opj_suite.py +++ b/glymur/test/test_opj_suite.py @@ -4170,8 +4170,8 @@ class TestSuiteDump(unittest.TestCase): # Jp2 Header # Channel Definition - self.assertEqual(jp2.box[2].box[2].component_number, (0, 1, 2)) - self.assertEqual(jp2.box[2].box[2].component_type, (0, 0, 0)) # color + self.assertEqual(jp2.box[2].box[2].index, (0, 1, 2)) + self.assertEqual(jp2.box[2].box[2].channel_type, (0, 0, 0)) # color self.assertEqual(jp2.box[2].box[2].association, (3, 2, 1)) # reverse def test_NR_file3_dump(self): @@ -6706,8 +6706,8 @@ class TestSuiteDump(unittest.TestCase): # Jp2 Header # Channel Definition - self.assertEqual(jp2.box[2].box[2].component_number, (0, 1)) - self.assertEqual(jp2.box[2].box[2].component_type, (0, 1)) # opacity + self.assertEqual(jp2.box[2].box[2].index, (0, 1)) + self.assertEqual(jp2.box[2].box[2].channel_type, (0, 1)) # opacity self.assertEqual(jp2.box[2].box[2].association, (0, 0)) # both main c = jp2.box[3].main_header diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index f948c3e..7ad0105 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -676,15 +676,15 @@ class TestPrinting(unittest.TestCase): @unittest.skipIf(data_root is None, "OPJ_DATA_ROOT environment variable not set") - def test_component_definition(self): + def test_channel_definition(self): filename = os.path.join(data_root, 'input/conformance/file2.jp2') j = glymur.Jp2k(filename) print(j.box[2].box[2]) actual = sys.stdout.getvalue().strip() - lines = ['Component Definition Box (cdef) @ (81, 28)', - ' Component 0 (color) ==> (3)', - ' Component 1 (color) ==> (2)', - ' Component 2 (color) ==> (1)'] + lines = ['Channel Definition Box (cdef) @ (81, 28)', + ' Channel 0 (color) ==> (3)', + ' Channel 1 (color) ==> (2)', + ' Channel 2 (color) ==> (1)'] expected = '\n'.join(lines) self.assertEqual(actual, expected) From 4a41685a88e83755d4bfe889d5e10b1fd7cc34b3 Mon Sep 17 00:00:00 2001 From: John Evans Date: Wed, 26 Jun 2013 07:15:47 -0400 Subject: [PATCH 18/25] Abstracted XML testing into own class. Added more negative cdef tests. --- glymur/jp2k.py | 13 +++ glymur/test/test_jp2box.py | 214 +++++++++++++++++++++++-------------- 2 files changed, 146 insertions(+), 81 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 6f20ae9..d755cb0 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -468,6 +468,19 @@ class Jp2k(Jp2kBox): msg = "The jp2 header box must contain a color definition box." raise IOError(msg) + # Any cdef box must be in the jp2 header following the image header. + cdef_lst = [j for (j, box) in enumerate(boxes) if box.id == 'cdef'] + if len(cdef_lst) != 0: + msg = "Any channel defintion box must be in the JP2 header " + msg += "following the image header." + raise IOError(msg) + + cdef_lst = [j for (j, box) in enumerate(jp2h.box) if box.id == 'cdef'] + if len(cdef_lst) > 1: + msg = "Only one channel definition box is allowed in the " + msg += "JP2 header." + raise IOError(msg) + with open(filename, 'wb') as ofile: for box in boxes: if box.id != 'jp2c': diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 4b929cd..d39cc64 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -17,17 +17,61 @@ def load_tests(loader, tests, ignore): return tests -@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, - "Missing openjp2 library.") class TestChannelDefinition(unittest.TestCase): def setUp(self): self.jp2file = glymur.data.nemo() self.j2kfile = glymur.data.goodstuff() + j2k = Jp2k(self.j2kfile) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + self.jP = JPEG2000SignatureBox() + self.ftyp = FileTypeBox() + self.jp2h = JP2HeaderBox() + self.jp2c = ContiguousCodestreamBox() + self.ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + self.colr_rgb = ColourSpecificationBox(colorspace=glymur.core.SRGB) + def tearDown(self): pass + def test_only_one_cdef_in_jp2_header(self): + """There can only be one channel definition box in the jp2 header.""" + j2k = Jp2k(self.j2kfile) + + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], + channel_type=[0, 0, 0], + association=[0, 1, 2]) + + boxes = [self.ihdr, cdef, self.colr_rgb, cdef] + self.jp2h.box = boxes + + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) + + def test_not_in_jp2_header(self): + j2k = Jp2k(self.j2kfile) + boxes = [self.ihdr, self.colr_rgb] + self.jp2h.box = boxes + + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], + channel_type=[0, 0, 0], + association=[0, 1, 2]) + + boxes = [self.jP, self.ftyp, self.jp2h, cdef, self.jp2c] + + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) + def test_bad_type(self): # Channel types are limited to 0, 1, 2, 65535 # Should reject if not all of index, channel_type, association the @@ -45,6 +89,93 @@ class TestChannelDefinition(unittest.TestCase): channel_type=[0, 0], association=[0, 1, 2]) +class TestXML(unittest.TestCase): + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + j2k = Jp2k(self.j2kfile) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + self.jP = JPEG2000SignatureBox() + self.ftyp = FileTypeBox() + self.jp2h = JP2HeaderBox() + self.jp2c = ContiguousCodestreamBox() + self.ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + + def tearDown(self): + pass + + def test_basic_xml(self): + j2k = Jp2k(self.j2kfile) + + self.jp2h.box = [self.ihdr, self.colr] + + the_xml = ET.fromstring('0') + xml = glymur.jp2box.XMLBox(xml=the_xml) + boxes = [self.jP, self.ftyp, self.jp2h, xml, self.jp2c] + + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + jp2 = Jp2k(tfile.name) + self.assertEqual(jp2.box[3].id, 'xml ') + + def test_xml_from_file(self): + raw_xml = b""" + + + 1 + 2008 + 141100 + + + + + 4 + 2011 + 59900 + + + + 68 + 2011 + 13600 + + + + """ + with tempfile.NamedTemporaryFile(suffix=".xml") as tfile: + tfile.write(raw_xml) + tfile.flush() + + j2k = Jp2k(self.j2kfile) + + self.jp2h.box = [self.ihdr, self.colr] + + xmlb = glymur.jp2box.XMLBox(filename=tfile.name) + boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + jp2 = Jp2k(tfile.name) + + output_boxes = [box.id for box in jp2.box] + self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ', + 'jp2c']) + + elts = jp2.box[3].xml.findall('country') + self.assertEqual(len(elts), 3) + + neighbor = elts[1].find('neighbor') + self.assertEqual(neighbor.attrib['name'], 'Malaysia') + self.assertEqual(neighbor.attrib['direction'], 'N') + + @unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, "Missing openjp2 library.") class TestJp2Boxes(unittest.TestCase): @@ -370,84 +501,5 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) - def test_basic_xml(self): - j2k = Jp2k(self.raw_codestream) - c = j2k.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - - jP = JPEG2000SignatureBox() - ftyp = FileTypeBox() - jp2h = JP2HeaderBox() - jp2c = ContiguousCodestreamBox() - ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) - colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) - jp2h.box = [ihdr, colr] - - the_xml = ET.fromstring('0') - xml = glymur.jp2box.XMLBox(xml=the_xml) - boxes = [jP, ftyp, jp2h, xml, jp2c] - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - j2k.wrap(tfile.name, boxes=boxes) - jp2 = Jp2k(tfile.name) - self.assertEqual(jp2.box[3].id, 'xml ') - - def test_xml_from_file(self): - raw_xml = b""" - - - 1 - 2008 - 141100 - - - - - 4 - 2011 - 59900 - - - - 68 - 2011 - 13600 - - - - """ - with tempfile.NamedTemporaryFile(suffix=".xml") as tfile: - tfile.write(raw_xml) - tfile.flush() - - j2k = Jp2k(self.raw_codestream) - c = j2k.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - - jP = JPEG2000SignatureBox() - ftyp = FileTypeBox() - jp2h = JP2HeaderBox() - jp2c = ContiguousCodestreamBox() - ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) - colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) - jp2h.box = [ihdr, colr] - - xmlb = glymur.jp2box.XMLBox(filename=tfile.name) - boxes = [jP, ftyp, jp2h, xmlb, jp2c] - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - j2k.wrap(tfile.name, boxes=boxes) - jp2 = Jp2k(tfile.name) - - output_boxes = [box.id for box in jp2.box] - self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ', - 'jp2c']) - self.assertIsNotNone(jp2.box[3].xml) - - if __name__ == "__main__": unittest.main() From 9f98d822b24455f87deaceac1c1c6a3728987f34 Mon Sep 17 00:00:00 2001 From: John Evans Date: Wed, 26 Jun 2013 08:38:46 -0400 Subject: [PATCH 19/25] More refactoring. --- glymur/test/test_jp2box.py | 231 ++++++++++++++----------------------- 1 file changed, 86 insertions(+), 145 deletions(-) diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index d39cc64..8fa6cf6 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -46,7 +46,7 @@ class TestChannelDefinition(unittest.TestCase): cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], channel_type=[0, 0, 0], - association=[0, 1, 2]) + association=[1, 2, 3]) boxes = [self.ihdr, cdef, self.colr_rgb, cdef] self.jp2h.box = boxes @@ -64,7 +64,7 @@ class TestChannelDefinition(unittest.TestCase): cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], channel_type=[0, 0, 0], - association=[0, 1, 2]) + association=[1, 2, 3]) boxes = [self.jP, self.ftyp, self.jp2h, cdef, self.jp2c] @@ -79,7 +79,7 @@ class TestChannelDefinition(unittest.TestCase): with self.assertRaises(IOError): box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], channel_type=[0, 0, 3], - association=[0, 1, 2]) + association=[1, 2, 3]) def test_wrong_lengths(self): # Should reject if not all of index, channel_type, association the @@ -87,7 +87,7 @@ class TestChannelDefinition(unittest.TestCase): with self.assertRaises(IOError): box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], channel_type=[0, 0], - association=[0, 1, 2]) + association=[1, 2, 3]) class TestXML(unittest.TestCase): @@ -113,18 +113,24 @@ class TestXML(unittest.TestCase): pass def test_basic_xml(self): + # Should be able to write an XMLBox. j2k = Jp2k(self.j2kfile) self.jp2h.box = [self.ihdr, self.colr] the_xml = ET.fromstring('0') - xml = glymur.jp2box.XMLBox(xml=the_xml) - boxes = [self.jP, self.ftyp, self.jp2h, xml, self.jp2c] + xmlb = glymur.jp2box.XMLBox(xml=the_xml) + self.assertEqual(ET.tostring(xmlb.xml), + b'0') + + boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name, boxes=boxes) jp2 = Jp2k(tfile.name) self.assertEqual(jp2.box[3].id, 'xml ') + self.assertEqual(ET.tostring(jp2.box[3].xml), + b'0') def test_xml_from_file(self): raw_xml = b""" @@ -176,66 +182,43 @@ class TestXML(unittest.TestCase): self.assertEqual(neighbor.attrib['direction'], 'N') -@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, - "Missing openjp2 library.") -class TestJp2Boxes(unittest.TestCase): - - @classmethod - def setUpClass(cls): - # We need a raw codestream, so use the one in nemo.jp2. - jp2file = pkg_resources.resource_filename(glymur.__name__, - "data/nemo.jp2") - j = Jp2k(jp2file) - c = [box for box in j.box if box.id == 'jp2c'][0] - - with tempfile.NamedTemporaryFile(suffix='.j2c', delete=False) as ofile: - with open(jp2file, 'rb') as ifile: - # Everything up until the jp2c box. - ifile.seek(c.offset+8) - ofile.write(ifile.read(c.length)) - - cls.raw_codestream = ofile.name - - @classmethod - def tearDownClass(cls): - os.unlink(cls.raw_codestream) +class TestColourSpecificationBox(unittest.TestCase): def setUp(self): - self.jp2file = pkg_resources.resource_filename(glymur.__name__, - "data/nemo.jp2") + self.j2kfile = glymur.data.goodstuff() + + j2k = Jp2k(self.j2kfile) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) + + self.jP = JPEG2000SignatureBox() + self.ftyp = FileTypeBox() + self.jp2h = JP2HeaderBox() + self.jp2c = ContiguousCodestreamBox() + self.ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) def tearDown(self): pass - def test_default_XML(self): - # Should be able to instantiate an XMLBox - xml = ET.fromstring('0') - b = glymur.jp2box.XMLBox(xml=xml) - self.assertEqual(ET.tostring(b.xml), - b'0') + def test_color_specification_box_with_out_enumerated_colorspace(self): + j2k = Jp2k(self.j2kfile) - def test_default_JPEG2000SignatureBox(self): - # Should be able to instantiate a JPEG2000SignatureBox - b = glymur.jp2box.JPEG2000SignatureBox() - self.assertEqual(b.signature, (13, 10, 135, 10)) + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(NotImplementedError): + j2k.wrap(tfile.name, boxes=boxes) - def test_default_FileTypeBox(self): - # Should be able to instantiate a FileTypeBox - b = glymur.jp2box.FileTypeBox() - self.assertEqual(b.brand, 'jp2 ') - self.assertEqual(b.minor_version, 0) - self.assertEqual(b.compatibility_list, ['jp2 ']) - - def test_default_ImageHeaderBox(self): - # Should be able to instantiate an image header box. - b = glymur.jp2box.ImageHeaderBox(height=512, width=256, - num_components=3) - self.assertEqual(b.height, 512) - self.assertEqual(b.width, 256) - self.assertEqual(b.num_components, 3) - self.assertEqual(b.bits_per_component, 8) - self.assertFalse(b.signed) - self.assertFalse(b.colorspace_unknown) + def test_missing_colr_box(self): + j2k = Jp2k(self.j2kfile) + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes[2].box = [self.ihdr] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + j2k.wrap(tfile.name, boxes=boxes) def test_default_ColourSpecificationBox(self): b = glymur.jp2box.ColourSpecificationBox(colorspace=glymur.core.SRGB) @@ -264,6 +247,40 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): b = glymur.jp2box.ColourSpecificationBox(colorspace, approximation) + +@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, + "Missing openjp2 library.") +class TestJp2Boxes(unittest.TestCase): + + def setUp(self): + self.j2kfile = glymur.data.goodstuff() + + def tearDown(self): + pass + + def test_default_JPEG2000SignatureBox(self): + # Should be able to instantiate a JPEG2000SignatureBox + b = glymur.jp2box.JPEG2000SignatureBox() + self.assertEqual(b.signature, (13, 10, 135, 10)) + + def test_default_FileTypeBox(self): + # Should be able to instantiate a FileTypeBox + b = glymur.jp2box.FileTypeBox() + self.assertEqual(b.brand, 'jp2 ') + self.assertEqual(b.minor_version, 0) + self.assertEqual(b.compatibility_list, ['jp2 ']) + + def test_default_ImageHeaderBox(self): + # Should be able to instantiate an image header box. + b = glymur.jp2box.ImageHeaderBox(height=512, width=256, + num_components=3) + self.assertEqual(b.height, 512) + self.assertEqual(b.width, 256) + self.assertEqual(b.num_components, 3) + self.assertEqual(b.bits_per_component, 8) + self.assertFalse(b.signed) + self.assertFalse(b.colorspace_unknown) + def test_default_JP2HeaderBox(self): b1 = JP2HeaderBox() b1.box = [ImageHeaderBox(height=512, width=256), @@ -275,6 +292,7 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(b.main_header, []) def verify_wrapped_raw(self, jp2file): + # Shared method by at least two tests. jp2 = Jp2k(jp2file) self.assertEqual(len(jp2.box), 4) @@ -295,7 +313,7 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(jp2.box[3].id, 'jp2c') self.assertEqual(jp2.box[3].offset, 77) - self.assertEqual(jp2.box[3].length, 1133427) + self.assertEqual(jp2.box[3].length, 115228) # jp2h super box self.assertEqual(len(jp2.box[2].box), 2) @@ -304,8 +322,8 @@ class TestJp2Boxes(unittest.TestCase): self.assertEqual(jp2.box[2].box[0].offset, 40) self.assertEqual(jp2.box[2].box[0].length, 22) self.assertEqual(jp2.box[2].box[0].longname, 'Image Header') - self.assertEqual(jp2.box[2].box[0].height, 1456) - self.assertEqual(jp2.box[2].box[0].width, 2592) + self.assertEqual(jp2.box[2].box[0].height, 800) + self.assertEqual(jp2.box[2].box[0].width, 480) self.assertEqual(jp2.box[2].box[0].num_components, 3) self.assertEqual(jp2.box[2].box[0].bits_per_component, 8) self.assertEqual(jp2.box[2].box[0].signed, False) @@ -323,13 +341,13 @@ class TestJp2Boxes(unittest.TestCase): self.assertIsNone(jp2.box[2].box[1].icc_profile) def test_wrap(self): - j2k = Jp2k(self.raw_codestream) - with tempfile.NamedTemporaryFile(suffix=".jp2", mode="wb") as tfile: + j2k = Jp2k(self.j2kfile) + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name) self.verify_wrapped_raw(tfile.name) def test_default_layout_but_with_specified_boxes(self): - j2k = Jp2k(self.raw_codestream) + j2k = Jp2k(self.j2kfile) boxes = [JPEG2000SignatureBox(), FileTypeBox(), JP2HeaderBox(), @@ -348,7 +366,7 @@ class TestJp2Boxes(unittest.TestCase): def test_image_header_box_not_first_in_jp2_header(self): # The specification says that ihdr must be the first box in jp2h. - j2k = Jp2k(self.raw_codestream) + j2k = Jp2k(self.j2kfile) boxes = [JPEG2000SignatureBox(), FileTypeBox(), JP2HeaderBox(), @@ -366,26 +384,8 @@ class TestJp2Boxes(unittest.TestCase): j2k.wrap(tfile.name, boxes=boxes) - def test_color_specification_box_with_out_enumerated_colorspace(self): - j2k = Jp2k(self.raw_codestream) - boxes = [JPEG2000SignatureBox(), - FileTypeBox(), - JP2HeaderBox(), - ContiguousCodestreamBox()] - c = j2k.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - boxes[2].box = [ImageHeaderBox(height=height, - width=width, - num_components=num_components), - ColourSpecificationBox(colorspace=None)] - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - with self.assertRaises(NotImplementedError): - j2k.wrap(tfile.name, boxes=boxes) - def test_first_2_boxes_not_jP_and_ftyp(self): - j2k = Jp2k(self.raw_codestream) + j2k = Jp2k(self.j2kfile) c = j2k.get_codestream() height = c.segment[1].Ysiz width = c.segment[1].Xsiz @@ -405,7 +405,7 @@ class TestJp2Boxes(unittest.TestCase): j2k.wrap(tfile.name, boxes=boxes) def test_jp2h_not_preceeding_jp2c(self): - j2k = Jp2k(self.raw_codestream) + j2k = Jp2k(self.j2kfile) c = j2k.get_codestream() height = c.segment[1].Ysiz width = c.segment[1].Xsiz @@ -424,48 +424,8 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) - def test_ihdr_not_first_in_jp2h(self): - j2k = Jp2k(self.raw_codestream) - c = j2k.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - - jP = JPEG2000SignatureBox() - ftyp = FileTypeBox() - jp2h = JP2HeaderBox() - jp2c = ContiguousCodestreamBox() - colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) - ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) - jp2h.box = [colr, ihdr] - boxes = [jP, ftyp, jp2h, jp2c] - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - with self.assertRaises(IOError): - j2k.wrap(tfile.name, boxes=boxes) - - def test_colr_box_not_in_jp2h(self): - j2k = Jp2k(self.raw_codestream) - c = j2k.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - - jP = JPEG2000SignatureBox() - ftyp = FileTypeBox() - jp2h = JP2HeaderBox() - jp2c = ContiguousCodestreamBox() - colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) - ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) - jp2h.box = [ihdr] - boxes = [jP, ftyp, jp2h, jp2c, colr] - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - with self.assertRaises(IOError): - j2k.wrap(tfile.name, boxes=boxes) - def test_missing_codestream(self): - j2k = Jp2k(self.raw_codestream) + j2k = Jp2k(self.j2kfile) c = j2k.get_codestream() height = c.segment[1].Ysiz width = c.segment[1].Xsiz @@ -482,24 +442,5 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) - def test_missing_colr_box(self): - j2k = Jp2k(self.raw_codestream) - c = j2k.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - - jP = JPEG2000SignatureBox() - ftyp = FileTypeBox() - jp2h = JP2HeaderBox() - jp2c = ContiguousCodestreamBox() - ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) - jp2h.box = [ihdr] - boxes = [jP, ftyp, jp2h, jp2c] - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - with self.assertRaises(IOError): - j2k.wrap(tfile.name, boxes=boxes) - if __name__ == "__main__": unittest.main() From 028b2972859dee2f943e2e40bbae1a9982b8aa6a Mon Sep 17 00:00:00 2001 From: jevans Date: Wed, 26 Jun 2013 19:37:43 -0400 Subject: [PATCH 20/25] Added cdef write support. --- glymur/jp2box.py | 13 ++++ glymur/test/test_jp2box.py | 111 +++++++++++++++++++++++++++++++++++ glymur/test/test_printing.py | 3 +- 3 files changed, 125 insertions(+), 2 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 034dec5..54edfbd 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -444,6 +444,19 @@ class ChannelDefinitionBox(Jp2kBox): msg = msg.format(self.index[j], color_type_string, assn) return msg + def _write(self, f): + """Write a channel definition box to file. + """ + N = len(self.association) + f.write(struct.pack('>I', 8 + 2 + N * 6)) + f.write('cdef'.encode('utf-8')) + f.write(struct.pack('>H', N)) + for j in range(N): + f.write(struct.pack('>' + 'H' * 3, + self.index[j], + self.channel_type[j], + self.association[j])) + @staticmethod def _parse(f, id, offset, length): """Parse component definition box. diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 8fa6cf6..89b66d1 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -19,6 +19,36 @@ def load_tests(loader, tests, ignore): class TestChannelDefinition(unittest.TestCase): + @classmethod + def setUpClass(cls): + """Need a one_plane plane image for greyscale testing.""" + j2k = Jp2k(glymur.data.goodstuff()) + data = j2k.read() + # Write the first component back out to file. + with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile: + grey_j2k = Jp2k(tfile.name, 'wb') + grey_j2k.write(data[:,:,0]) + cls.one_plane = tfile.name + # Write the first two components back out to file. + with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile: + grey_j2k = Jp2k(tfile.name, 'wb') + grey_j2k.write(data[:,:,0:1]) + cls.two_planes = tfile.name + # Write four components back out to file. + with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile: + rgba_jp2 = Jp2k(tfile.name, 'wb') + shape = (data.shape[0], data.shape[1], 1) + alpha = np.zeros((shape), dtype=data.dtype) + data4 = np.concatenate((data, alpha), axis=2) + rgba_jp2.write(data4) + cls.four_planes = tfile.name + + @classmethod + def tearDownClass(cls): + os.unlink(cls.one_plane) + os.unlink(cls.two_planes) + os.unlink(cls.four_planes) + def setUp(self): self.jp2file = glymur.data.nemo() self.j2kfile = glymur.data.goodstuff() @@ -36,10 +66,91 @@ class TestChannelDefinition(unittest.TestCase): self.ihdr = ImageHeaderBox(height=height, width=width, num_components=num_components) self.colr_rgb = ColourSpecificationBox(colorspace=glymur.core.SRGB) + self.colr_gr = ColourSpecificationBox(colorspace=glymur.core.GREYSCALE) def tearDown(self): pass + def test_rgb(self): + """Just regular RGB.""" + j2k = Jp2k(self.j2kfile) + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], + channel_type=[0, 0, 0], + association=[1, 2, 3]) + boxes = [self.ihdr, self.colr_rgb, cdef] + self.jp2h.box = boxes + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + + jp2 = Jp2k(tfile.name) + jp2h = jp2.box[2] + boxes = [box.id for box in jp2h.box] + self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) + self.assertEqual(jp2h.box[2].index, (0, 1, 2)) + self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0)) + self.assertEqual(jp2h.box[2].association, (1, 2, 3)) + + def test_rgba(self): + """Just regular RGBA.""" + j2k = Jp2k(self.four_planes) + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2, 3], + channel_type=[0, 0, 0, 1], + association=[1, 2, 3, 0]) + boxes = [self.ihdr, self.colr_rgb, cdef] + self.jp2h.box = boxes + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + + jp2 = Jp2k(tfile.name) + jp2h = jp2.box[2] + boxes = [box.id for box in jp2h.box] + self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) + self.assertEqual(jp2h.box[2].index, (0, 1, 2, 3)) + self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0, 1)) + self.assertEqual(jp2h.box[2].association, (1, 2, 3, 0)) + + def test_grey(self): + """Just regular greyscale.""" + j2k = Jp2k(self.one_plane) + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0], + channel_type=[0], + association=[1]) + boxes = [self.ihdr, self.colr_gr, cdef] + self.jp2h.box = boxes + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + + jp2 = Jp2k(tfile.name) + jp2h = jp2.box[2] + boxes = [box.id for box in jp2h.box] + self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) + self.assertEqual(jp2h.box[2].index, (0,)) + self.assertEqual(jp2h.box[2].channel_type, (0,)) + self.assertEqual(jp2h.box[2].association, (1,)) + + def test_grey_alpha(self): + """Just regular greyscale plus alpha.""" + j2k = Jp2k(self.two_planes) + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1], + channel_type=[0, 1], + association=[1, 0]) + boxes = [self.ihdr, self.colr_gr, cdef] + self.jp2h.box = boxes + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + + jp2 = Jp2k(tfile.name) + jp2h = jp2.box[2] + boxes = [box.id for box in jp2h.box] + self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) + self.assertEqual(jp2h.box[2].index, (0, 1)) + self.assertEqual(jp2h.box[2].channel_type, (0, 1)) + self.assertEqual(jp2h.box[2].association, (1, 0)) + def test_only_one_cdef_in_jp2_header(self): """There can only be one channel definition box in the jp2 header.""" j2k = Jp2k(self.j2kfile) diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 7ad0105..4090a2d 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -30,8 +30,7 @@ class TestPrintingNeedsLib(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") + jp2file = glymur.data.nemo() with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: cls._plain_nemo_file = tfile.name ijfile = Jp2k(jp2file) From c5f54704b4bd799505e9efe885f53db16ad5978e Mon Sep 17 00:00:00 2001 From: jevans Date: Wed, 26 Jun 2013 20:27:46 -0400 Subject: [PATCH 21/25] Negative tests for images with alpha layers. --- glymur/core.py | 6 ++++++ glymur/jp2k.py | 19 +++++++++++++++++-- glymur/test/test_jp2box.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/glymur/core.py b/glymur/core.py index 73a286e..6483558 100644 --- a/glymur/core.py +++ b/glymur/core.py @@ -74,6 +74,12 @@ _color_type_map_display = { _PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity', _UNSPECIFIED: 'unspecified'} +# color channel definitions. +RED = 1 +GREEN = 2 +BLUE = 3 +GREY = 1 + # enumerated color channel associations _rgb_colorspace = {"R": 1, "G": 2, "B": 3} _greyscale_colorspace = {"Y": 1} diff --git a/glymur/jp2k.py b/glymur/jp2k.py index d755cb0..846d7fd 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -16,8 +16,7 @@ import warnings import numpy as np from .codestream import Codestream -from .core import progression_order -from .core import SRGB +from .core import * from .jp2box import * from .lib import openjp2 as opj2 @@ -467,6 +466,7 @@ class Jp2k(Jp2kBox): if len(colr_lst) == 0: msg = "The jp2 header box must contain a color definition box." raise IOError(msg) + colr = jp2h.box[colr_lst[0]] # Any cdef box must be in the jp2 header following the image header. cdef_lst = [j for (j, box) in enumerate(boxes) if box.id == 'cdef'] @@ -480,6 +480,21 @@ class Jp2k(Jp2kBox): msg = "Only one channel definition box is allowed in the " msg += "JP2 header." raise IOError(msg) + elif len(cdef_lst) == 1: + cdef = jp2h.box[cdef_lst[0]] + assn = cdef.association + typ = cdef.channel_type + index = cdef.index + if colr.colorspace == SRGB: + if any([chan + 1 not in assn or typ[chan] != 0 for chan in [0, 1, 2]]): + msg = "All color channels must be defined in the " + msg += "channel definition box." + raise IOError(msg) + elif colr.colorspace == GREYSCALE: + if 0 not in typ: + msg = "All color channels must be defined in the " + msg += "channel definition box." + raise IOError(msg) with open(filename, 'wb') as ofile: for box in boxes: diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 89b66d1..59a241c 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -111,6 +111,19 @@ class TestChannelDefinition(unittest.TestCase): self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0, 1)) self.assertEqual(jp2h.box[2].association, (1, 2, 3, 0)) + def test_bad_rgba(self): + """R, G, and B must be specified.""" + j2k = Jp2k(self.four_planes) + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2, 3], + channel_type=[0, 0, 1, 1], + association=[1, 2, 3, 0]) + boxes = [self.ihdr, self.colr_rgb, cdef] + self.jp2h.box = boxes + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError) as ce: + j2k.wrap(tfile.name, boxes=boxes) + def test_grey(self): """Just regular greyscale.""" j2k = Jp2k(self.one_plane) @@ -151,6 +164,21 @@ class TestChannelDefinition(unittest.TestCase): self.assertEqual(jp2h.box[2].channel_type, (0, 1)) self.assertEqual(jp2h.box[2].association, (1, 0)) + def test_bad_grey_alpha(self): + """A greyscale image with alpha layer must specify Y""" + j2k = Jp2k(self.two_planes) + + # This cdef box + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1], + channel_type=[1, 1], + association=[0, 1]) + boxes = [self.ihdr, self.colr_gr, cdef] + self.jp2h.box = boxes + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises((OSError, IOError)) as ce: + j2k.wrap(tfile.name, boxes=boxes) + def test_only_one_cdef_in_jp2_header(self): """There can only be one channel definition box in the jp2 header.""" j2k = Jp2k(self.j2kfile) From cde8c47e752581fa063b57d58f948cf3ba63564b Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 27 Jun 2013 08:23:52 -0400 Subject: [PATCH 22/25] Returning a Jp2k object from the wrap method. --- docs/source/api.rst | 6 +- docs/source/how_do_i.rst | 86 ++++++++++++++++++++++++ glymur/codestream.py | 2 +- glymur/jp2box.py | 11 ++-- glymur/jp2k.py | 46 +++++++++++-- glymur/test/test_jp2box.py | 132 ++++++++++++++++++++----------------- 6 files changed, 205 insertions(+), 78 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index e1efdac..7584718 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -5,7 +5,7 @@ API Jp2k ---- .. autoclass:: glymur.Jp2k - :members: read, write, read_bands, get_codestream + :members: read, write, wrap, read_bands, get_codestream Individual Boxes ---------------- @@ -24,9 +24,9 @@ ColourSpecificationBox .. autoclass:: glymur.jp2box.ColourSpecificationBox :members: -ComponentDefinitionBox +ChannelDefinitionBox '''''''''''''''''''''' -.. autoclass:: glymur.jp2box.ComponentDefinitionBox +.. autoclass:: glymur.jp2box.ChannelDefinitionBox :members: ComponentMappingBox diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 355ff14..a4c0167 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -34,6 +34,92 @@ codestream box, only the main header is printed. It is possible to print >>> print(j.get_codestream()) +Add XML Metadata? +================= +An existing raw codestream (or JP2 file) can be wrapped (re-wrapped) in a +user-defined set of JP2 boxes. To get just a minimal JP2 jacket on the +codestream provided by `goodstuff.j2k`, you can use the **wrap** method with +no box argument: :: + + >>> import glymur + >>> jfile = glymur.data.goodstuff() + >>> j2k = glymur.Jp2k(jfile) + >>> jp2 = j2k.wrap("newfile.jp2") + >>> print(jp2) + File: newfile.jp2 + 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: [800 480 3] + Bitdepth: 8 + Signed: False + Compression: wavelet + Colorspace Unknown: False + Colour Specification Box (colr) @ (62, 15) + Method: enumerated colorspace + Precedence: 0 + Colorspace: sRGB + Contiguous Codestream Box (jp2c) @ (77, 115228) + Main header: + . + . (truncated) + . + +The raw codestream was wrapped in a JP2 jacket with four boxes in the outer +layer (the signature, file type, JP2 header, and contiguous codestream), with +two additional boxes (image header and color specification) contained in the +JP2 header superbox. + +XML boxes are not in the minimal set of box requirements for the JP2 format, so +in order to add an XML box into the mix, we'll need to specify all of the +boxes. If you already have a JP2 jacket in place, you can just reuse it, +though. Take the following example content in an XML file `favorites.xml` : :: + + + + Light Ale + + +and add it after the JP2 header box, but before the codestream box :: + + >>> boxes = jp2.box # The box attribute is the list of JP2 boxes + >>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml') + >>> boxes.insert(3, xmlbox) + >>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes) + >>> print(jp2_xml) + File: newfile_with_xml.jp2 + 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: [800 480 3] + Bitdepth: 8 + Signed: False + Compression: wavelet + Colorspace Unknown: False + Colour Specification Box (colr) @ (62, 15) + Method: enumerated colorspace + Precedence: 0 + Colorspace: sRGB + XML Box (xml ) @ (77, 76) + + Light Ale + + + Contiguous Codestream Box (jp2c) @ (153, 115236) + Main header: + . + . (truncated) + . + + Work with XMP UUIDs? ==================== The example JP2 file shipped with glymur has an XMP UUID. :: diff --git a/glymur/codestream.py b/glymur/codestream.py index 04c208f..327dd19 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -175,7 +175,7 @@ class Codestream(object): segment = self._parseGenericSegment(f, marker_id) else: - msg = 'Invalid marker id encountered at byte {0:d}' + msg = 'Invalid marker id encountered at byte {0:d} ' msg += 'in codestream: "0x{1:x}"' msg = msg.format(offset, marker_id) raise IOError(msg) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 54edfbd..bc20bbf 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -958,12 +958,6 @@ class JP2HeaderBox(Jp2kBox): def _write(self, f): """Write a JP2 Header box to file. """ - # Make sure they are in proper order. - if self.box[0].id != 'ihdr': - msg = "The first box in the JP2 Header superbox must be the Image " - msg += "Header box." - raise IOError(msg) - # Write the contained boxes, then come back and write the length. orig_pos = f.tell() f.write(struct.pack('>I', 0)) @@ -1590,7 +1584,10 @@ class XMLBox(Jp2kBox): msg = Jp2kBox.__str__(self) xml = self.xml if self.xml is not None: - msg += _pretty_print_xml(self.xml) + try: + msg += _pretty_print_xml(self.xml) + except TypeError: + msg += _pretty_print_xml(self.xml.getroot()) else: msg += '\n {0}'.format(xml) return msg diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 846d7fd..93a3a93 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -408,14 +408,29 @@ class Jp2k(Jp2kBox): self._parse() def wrap(self, filename, boxes=None): - """Write a codestream, wrapping it in the JP2 format. + """Write the codestream back out to file, wrapped in new JP2 jacket. Parameters ---------- filename : str JP2 file to be created from a raw codestream. boxes : list - JP2 box definitions to define the JP2 file format. + JP2 box definitions to define the JP2 file format. If not + provided, a default ""jacket" is assumed, consisting of JP2 + signature, file type, JP2 header, and contiguous codestream boxes. + + Returns + ------- + jp2 : Jp2k object + Newly wrapped Jp2k object. + + Examples + -------- + >>> import glymur, tempfile + >>> jfile = glymur.data.goodstuff() + >>> j2k = glymur.Jp2k(jfile) + >>> tfile = tempfile.NamedTemporaryFile(suffix='jp2') + >>> j2k.wrap(tfile.name) """ if boxes is None: # Try to create a reasonable default. @@ -502,14 +517,31 @@ class Jp2k(Jp2kBox): box._write(ofile) else: # The codestream gets written last. - ofile.write(struct.pack('>I', self.length + 8)) - ofile.write('jp2c'.encode()) - - with open(self.filename, 'rb') as ifile: - ofile.write(ifile.read()) + if len(self.box) == 0: + # Am I a raw codestream? If so, then it is pretty + # easy, just write the codestream box header plus all + # of myself out to file. + ofile.write(struct.pack('>I', self.length + 8)) + ofile.write('jp2c'.encode()) + with open(self.filename, 'rb') as ifile: + ofile.write(ifile.read()) + else: + # OK, I'm a jp2 file. Need to find out where the + # raw codestream actually starts. + jp2c = [box for box in self.box if box.id == 'jp2c'] + jp2c = jp2c[0] + ofile.write(struct.pack('>I', jp2c.length + 8)) + ofile.write('jp2c'.encode()) + with open(self.filename, 'rb') as ifile: + # Seek 8 bytes past the L, T fields to get to the + # raw codestream. + ifile.seek(jp2c.offset + 8) + ofile.write(ifile.read(jp2c.length - 8)) ofile.flush() + jp2 = Jp2k(filename) + return jp2 def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False): diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 59a241c..75a16b0 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -234,44 +234,6 @@ class TestXML(unittest.TestCase): self.jp2file = glymur.data.nemo() self.j2kfile = glymur.data.goodstuff() - j2k = Jp2k(self.j2kfile) - c = j2k.get_codestream() - height = c.segment[1].Ysiz - width = c.segment[1].Xsiz - num_components = len(c.segment[1].XRsiz) - - self.jP = JPEG2000SignatureBox() - self.ftyp = FileTypeBox() - self.jp2h = JP2HeaderBox() - self.jp2c = ContiguousCodestreamBox() - self.ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) - self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) - - def tearDown(self): - pass - - def test_basic_xml(self): - # Should be able to write an XMLBox. - j2k = Jp2k(self.j2kfile) - - self.jp2h.box = [self.ihdr, self.colr] - - the_xml = ET.fromstring('0') - xmlb = glymur.jp2box.XMLBox(xml=the_xml) - self.assertEqual(ET.tostring(xmlb.xml), - b'0') - - boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] - - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - j2k.wrap(tfile.name, boxes=boxes) - jp2 = Jp2k(tfile.name) - self.assertEqual(jp2.box[3].id, 'xml ') - self.assertEqual(ET.tostring(jp2.box[3].xml), - b'0') - - def test_xml_from_file(self): raw_xml = b""" @@ -295,30 +257,77 @@ class TestXML(unittest.TestCase): """ - with tempfile.NamedTemporaryFile(suffix=".xml") as tfile: + with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile: tfile.write(raw_xml) tfile.flush() + self.xmlfile = tfile.name - j2k = Jp2k(self.j2kfile) - - self.jp2h.box = [self.ihdr, self.colr] + j2k = Jp2k(self.j2kfile) + c = j2k.get_codestream() + height = c.segment[1].Ysiz + width = c.segment[1].Xsiz + num_components = len(c.segment[1].XRsiz) - xmlb = glymur.jp2box.XMLBox(filename=tfile.name) - boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] - with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - j2k.wrap(tfile.name, boxes=boxes) - jp2 = Jp2k(tfile.name) + self.jP = JPEG2000SignatureBox() + self.ftyp = FileTypeBox() + self.jp2h = JP2HeaderBox() + self.jp2c = ContiguousCodestreamBox() + self.ihdr = ImageHeaderBox(height=height, width=width, + num_components=num_components) + self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) - output_boxes = [box.id for box in jp2.box] - self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ', - 'jp2c']) + def tearDown(self): + os.unlink(self.xmlfile) + pass - elts = jp2.box[3].xml.findall('country') - self.assertEqual(len(elts), 3) + def test_negative_both_file_and_xml_provided(self): + """The XML should come from only one source.""" + j2k = Jp2k(self.j2kfile) + xml_object = ET.parse(self.xmlfile) + with self.assertRaises((IOError, OSError)) as ce: + xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile, xml=xml_object) - neighbor = elts[1].find('neighbor') - self.assertEqual(neighbor.attrib['name'], 'Malaysia') - self.assertEqual(neighbor.attrib['direction'], 'N') + def test_basic_xml(self): + # Should be able to write an XMLBox. + j2k = Jp2k(self.j2kfile) + + self.jp2h.box = [self.ihdr, self.colr] + + the_xml = ET.fromstring('0') + xmlb = glymur.jp2box.XMLBox(xml=the_xml) + self.assertEqual(ET.tostring(xmlb.xml), + b'0') + + boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] + + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + jp2 = Jp2k(tfile.name) + self.assertEqual(jp2.box[3].id, 'xml ') + self.assertEqual(ET.tostring(jp2.box[3].xml), + b'0') + + def test_xml_from_file(self): + j2k = Jp2k(self.j2kfile) + + self.jp2h.box = [self.ihdr, self.colr] + + xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile) + boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + jp2 = Jp2k(tfile.name) + + output_boxes = [box.id for box in jp2.box] + self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ', + 'jp2c']) + + elts = jp2.box[3].xml.findall('country') + self.assertEqual(len(elts), 3) + + neighbor = elts[1].find('neighbor') + self.assertEqual(neighbor.attrib['name'], 'Malaysia') + self.assertEqual(neighbor.attrib['direction'], 'N') class TestColourSpecificationBox(unittest.TestCase): @@ -369,22 +378,25 @@ class TestColourSpecificationBox(unittest.TestCase): def test_ColourSpecificationBox_with_colorspace_and_icc(self): # Colour specification boxes can't have both. - with self.assertRaises(IOError): + with self.assertRaises((OSError, IOError)): colorspace = glymur.core.SRGB icc_profile = b'\x01\x02\x03\x04' - b = glymur.jp2box.ColourSpecificationBox(colorspace, icc_profile) + b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + icc_profile=icc_profile) def test_ColourSpecificationBox_with_bad_method(self): colorspace = glymur.core.SRGB method = -1 with self.assertRaises(IOError): - b = glymur.jp2box.ColourSpecificationBox(colorspace, method) + b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + method=method) def test_ColourSpecificationBox_with_bad_approximation(self): colorspace = glymur.core.SRGB - approximation = -1 + approx = -1 with self.assertRaises(IOError): - b = glymur.jp2box.ColourSpecificationBox(colorspace, approximation) + b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + approximation=approx) @unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None, From 2d22c9e0128de905983f4fcba9d0bd051517250a Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 27 Jun 2013 16:34:39 -0400 Subject: [PATCH 23/25] Fixed doctest for wrap. --- glymur/jp2k.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 93a3a93..96ead49 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -426,11 +426,11 @@ class Jp2k(Jp2kBox): Examples -------- - >>> import glymur, tempfile + >>> import glymur, tempfile >>> jfile = glymur.data.goodstuff() >>> j2k = glymur.Jp2k(jfile) >>> tfile = tempfile.NamedTemporaryFile(suffix='jp2') - >>> j2k.wrap(tfile.name) + >>> jp2 = j2k.wrap(tfile.name) """ if boxes is None: # Try to create a reasonable default. @@ -446,9 +446,9 @@ class Jp2k(Jp2kBox): width=width, num_components=num_components), ColourSpecificationBox(colorspace=SRGB)] - + # Check for a bad sequence of boxes. - # 1st two boxes must be 'jP ' and 'ftyp' + # 1st two boxes must be 'jP ' and 'ftyp' if boxes[0].id != 'jP ' or boxes[1].id != 'ftyp': msg = "The first box must be the signature box and the second " msg += "must be the file type box." @@ -501,7 +501,8 @@ class Jp2k(Jp2kBox): typ = cdef.channel_type index = cdef.index if colr.colorspace == SRGB: - if any([chan + 1 not in assn or typ[chan] != 0 for chan in [0, 1, 2]]): + if any([chan + 1 not in assn or typ[chan] != 0 + for chan in [0, 1, 2]]): msg = "All color channels must be defined in the " msg += "channel definition box." raise IOError(msg) @@ -543,7 +544,6 @@ class Jp2k(Jp2kBox): jp2 = Jp2k(filename) return jp2 - def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False): """Read a JPEG 2000 image. From 517faeb1e4511ec8fcfe0b5173c326ef61d22033 Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 27 Jun 2013 16:35:10 -0400 Subject: [PATCH 24/25] updating for 0.1.10 release. --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ef8ef6c..efc70fc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +Jun 27, 2013 - v0.1.10 Can wrap codestreams in custom JP2 jackets. Exposing + parameter to specify multi component transform. Added a raw codestream + file. + Jun 16, 2013 - v0.1.9 Reading ICC profile headers as ordered dicts. Exif dictionaries changed to ordered dicts. Honoring XDG_CONFIG_HOME environment variable. From 62e7afa118a1ef12fc4d3692511d7a9139f54686 Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 27 Jun 2013 16:35:55 -0400 Subject: [PATCH 25/25] pep8 work. --- docs/source/how_do_i.rst | 1 - glymur/data/__init__.py | 1 + glymur/jp2box.py | 8 ++++---- glymur/test/test_jp2box.py | 31 +++++++++++++++++++------------ glymur/test/test_jp2k.py | 2 +- 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index a4c0167..a15b865 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -119,7 +119,6 @@ and add it after the JP2 header box, but before the codestream box :: . (truncated) . - Work with XMP UUIDs? ==================== The example JP2 file shipped with glymur has an XMP UUID. :: diff --git a/glymur/data/__init__.py b/glymur/data/__init__.py index 3d82c0e..bebfb08 100644 --- a/glymur/data/__init__.py +++ b/glymur/data/__init__.py @@ -20,6 +20,7 @@ def nemo(): file = pkg_resources.resource_filename(__name__, "nemo.jp2") return file + def goodstuff(): """Shortcut for specifying path to goodstuff.j2k. diff --git a/glymur/jp2box.py b/glymur/jp2box.py index bc20bbf..e0107f3 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -216,7 +216,7 @@ class ColourSpecificationBox(Jp2kBox): msg += "colorspaces is not supported at this time." raise NotImplementedError(msg) length = 15 if self.icc_profile is None else 11 + len(self.icc_profile) - f.write(struct.pack('>I', length)) + f.write(struct.pack('>I', length)) f.write('colr'.encode()) buffer = struct.pack('>BBBI', @@ -806,12 +806,12 @@ class ImageHeaderBox(Jp2kBox): def _write(self, f): """Write an Image Header box to file. """ - f.write(struct.pack('>I', 22)) + f.write(struct.pack('>I', 22)) f.write('ihdr'.encode()) # signedness and bps are stored together in a single byte bit_depth_signedness = 0x80 if self.signed else 0x00 - bit_depth_signedness |= self.bits_per_component - 1 + bit_depth_signedness |= self.bits_per_component - 1 buffer = struct.pack('>IIHBBBB', self.height, self.width, @@ -960,7 +960,7 @@ class JP2HeaderBox(Jp2kBox): """ # Write the contained boxes, then come back and write the length. orig_pos = f.tell() - f.write(struct.pack('>I', 0)) + f.write(struct.pack('>I', 0)) f.write('jp2h'.encode()) for box in self.box: box._write(f) diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 75a16b0..e0500f5 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -27,12 +27,12 @@ class TestChannelDefinition(unittest.TestCase): # Write the first component back out to file. with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile: grey_j2k = Jp2k(tfile.name, 'wb') - grey_j2k.write(data[:,:,0]) + grey_j2k.write(data[:, :, 0]) cls.one_plane = tfile.name # Write the first two components back out to file. with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile: grey_j2k = Jp2k(tfile.name, 'wb') - grey_j2k.write(data[:,:,0:1]) + grey_j2k.write(data[:, :, 0:1]) cls.two_planes = tfile.name # Write four components back out to file. with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile: @@ -58,13 +58,13 @@ class TestChannelDefinition(unittest.TestCase): height = c.segment[1].Ysiz width = c.segment[1].Xsiz num_components = len(c.segment[1].XRsiz) - + self.jP = JPEG2000SignatureBox() self.ftyp = FileTypeBox() self.jp2h = JP2HeaderBox() self.jp2c = ContiguousCodestreamBox() self.ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) + num_components=num_components) self.colr_rgb = ColourSpecificationBox(colorspace=glymur.core.SRGB) self.colr_gr = ColourSpecificationBox(colorspace=glymur.core.GREYSCALE) @@ -168,7 +168,7 @@ class TestChannelDefinition(unittest.TestCase): """A greyscale image with alpha layer must specify Y""" j2k = Jp2k(self.two_planes) - # This cdef box + # This cdef box cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1], channel_type=[1, 1], association=[0, 1]) @@ -228,6 +228,7 @@ class TestChannelDefinition(unittest.TestCase): channel_type=[0, 0], association=[1, 2, 3]) + class TestXML(unittest.TestCase): def setUp(self): @@ -258,7 +259,7 @@ class TestXML(unittest.TestCase): """ with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile: - tfile.write(raw_xml) + tfile.write(raw_xml) tfile.flush() self.xmlfile = tfile.name @@ -267,13 +268,13 @@ class TestXML(unittest.TestCase): height = c.segment[1].Ysiz width = c.segment[1].Xsiz num_components = len(c.segment[1].XRsiz) - + self.jP = JPEG2000SignatureBox() self.ftyp = FileTypeBox() self.jp2h = JP2HeaderBox() self.jp2c = ContiguousCodestreamBox() self.ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) + num_components=num_components) self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) def tearDown(self): @@ -340,13 +341,13 @@ class TestColourSpecificationBox(unittest.TestCase): height = c.segment[1].Ysiz width = c.segment[1].Xsiz num_components = len(c.segment[1].XRsiz) - + self.jP = JPEG2000SignatureBox() self.ftyp = FileTypeBox() self.jp2h = JP2HeaderBox() self.jp2c = ContiguousCodestreamBox() self.ihdr = ImageHeaderBox(height=height, width=width, - num_components=num_components) + num_components=num_components) def tearDown(self): pass @@ -444,7 +445,7 @@ class TestJp2Boxes(unittest.TestCase): def verify_wrapped_raw(self, jp2file): # Shared method by at least two tests. - jp2 = Jp2k(jp2file) + jp2 = Jp2k(jp2file) self.assertEqual(len(jp2.box), 4) self.assertEqual(jp2.box[0].id, 'jP ') @@ -497,6 +498,13 @@ class TestJp2Boxes(unittest.TestCase): j2k.wrap(tfile.name) self.verify_wrapped_raw(tfile.name) + def test_wrap_jp2(self): + j2k = Jp2k(self.j2kfile) + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + jp2 = j2k.wrap(tfile.name) + boxes = [box.id for box in jp2.box] + self.assertEqual(boxes, ['jP ', 'ftyp', 'jp2h', 'jp2c']) + def test_default_layout_but_with_specified_boxes(self): j2k = Jp2k(self.j2kfile) boxes = [JPEG2000SignatureBox(), @@ -534,7 +542,6 @@ class TestJp2Boxes(unittest.TestCase): with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) - def test_first_2_boxes_not_jP_and_ftyp(self): j2k = Jp2k(self.j2kfile) c = j2k.get_codestream() diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index aab2bca..e5f03cd 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -173,7 +173,7 @@ class TestJp2k(unittest.TestCase): with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: ofile = Jp2k(tfile.name, 'wb') with self.assertRaises(IOError): - ofile.write(expdata[:,:,0], mct=True) + ofile.write(expdata[:, :, 0], mct=True) def test_write_cprl(self): # Issue 17