From 047f9192de92f08613d773492834c488bf6d3549 Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 19 Sep 2013 14:09:28 -0400 Subject: [PATCH 1/8] Tests failed when OPJ_DATA_ROOT not set. --- glymur/test/test_opj_suite.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index a5daf3e..456ff9b 100644 --- a/glymur/test/test_opj_suite.py +++ b/glymur/test/test_opj_suite.py @@ -6724,6 +6724,8 @@ class TestSuiteDump(unittest.TestCase): [8, 9, 9, 10, 9, 9, 10, 9, 9, 10, 9, 9, 10, 9, 9, 10]) +@unittest.skipIf(OPJ_DATA_ROOT is None, + "OPJ_DATA_ROOT environment variable not set") @unittest.skipIf(glymur.version.openjpeg_version_tuple[0] == 1, "Feature not supported in glymur until openjpeg 2.0") class TestSuite_bands(unittest.TestCase): @@ -6853,6 +6855,8 @@ class TestSuite_bands(unittest.TestCase): self.assertTrue(True) +@unittest.skipIf(OPJ_DATA_ROOT is None, + "OPJ_DATA_ROOT environment variable not set") @unittest.skipIf(glymur.version.openjpeg_version_tuple[0] == 1, "Tests not passing until 2.0") class TestSuite2point0(unittest.TestCase): @@ -6915,6 +6919,8 @@ class TestSuite2point0(unittest.TestCase): self.assertTrue(True) +@unittest.skipIf(OPJ_DATA_ROOT is None, + "OPJ_DATA_ROOT environment variable not set") @unittest.skipIf(OPENJP2_IS_V2_OFFICIAL, "Test not in done in v2.0.0 official") @unittest.skipIf(glymur.version.openjpeg_version_tuple[0] == 1, From 47ec633ff873936508beaa08982823885445e6bc Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 19 Sep 2013 14:09:52 -0400 Subject: [PATCH 2/8] Being more resilient when faced with seemingly unparseable xml. If the XML content starts out with bad bytes, scan ahead to try to find the " -1: + text = read_buffer[decl_start:].decode('utf-8') + else: + raise + + # Let the user know that the XML box was problematic. + msg = 'A UnicodeDecodeError was encountered parsing an XML box at ' + msg += 'byte position {0} ({1}), but the XML was still recovered.' + msg = msg.format(offset, ude.reason) + warnings.warn(msg, UserWarning) + # Strip out any trailing nulls, as they can foul up XML parsing. text = text.rstrip(chr(0)) + # Scan for the start of the xml declaration. + try: elt = ET.fromstring(text) xml = ET.ElementTree(elt) diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index bbf551b..1ab5b29 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -71,7 +71,7 @@ class TestJp2kBadXmlFile(unittest.TestCase): with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: cls._bad_xml_file = tfile.name with open(jp2file, 'rb') as ifile: - # Everything up until the jp2c box. + # Everything up until the UUID box. write_buffer = ifile.read(77) tfile.write(write_buffer) @@ -119,7 +119,68 @@ class TestJp2kBadXmlFile(unittest.TestCase): self.assertIsNone(jp2k.box[3].xml) -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and not OPENJP2_IS_V2_OFFICIAL, +@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") +class TestBadButRecoverableXmlFile(unittest.TestCase): + """Test suite for XML box that is bad, but we can still recover the XML.""" + + @classmethod + def setUpClass(cls): + """Setup a JP2 file with bad bytes preceding the XML. We only need + to do this once per class rather than once per test. + """ + jp2file = glymur.data.nemo() + with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: + cls._bad_xml_file = tfile.name + with open(jp2file, 'rb') as ifile: + # Everything up until the UUID box. + write_buffer = ifile.read(77) + tfile.write(write_buffer) + + # Write the xml box with bad xml + # Length = 64, id is 'xml '. + write_buffer = struct.pack('>I4s', int(64), b'xml ') + tfile.write(write_buffer) + + # Write out 8 bad bytes. + write_buffer = b'\x00\x00\x07\x90xml ' + tfile.write(write_buffer) + + # Write out 48 good bytes constituting the XML payload. + write_buffer = b'this is a test' + write_buffer = write_buffer.encode() + tfile.write(write_buffer) + + # Get the rest of the input file. + write_buffer = ifile.read() + tfile.write(write_buffer) + tfile.flush() + + @classmethod + def tearDownClass(cls): + os.unlink(cls._bad_xml_file) + + @unittest.skipIf(sys.hexversion < 0x03020000, + "Uses features introduced in 3.2.") + def test_bad_xml_box_warning(self): + """Should warn in case of bad XML""" + with self.assertWarns(UserWarning): + Jp2k(self._bad_xml_file) + + def test_recover_from_bad_xml(self): + """Should be able to recover info from xml box with bad xml.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + jp2 = Jp2k(self._bad_xml_file) + + self.assertEqual(jp2.box[3].box_id, 'xml ') + self.assertEqual(jp2.box[3].offset, 77) + self.assertEqual(jp2.box[3].length, 64) + self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()), + b'this is a test') + + +@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and + not OPENJP2_IS_V2_OFFICIAL, "Missing openjp2 library version 2.0+.") class TestJp2k_2_1(unittest.TestCase): """Test suite for version 2.0+ of openjpeg software""" @@ -312,56 +373,6 @@ class TestJp2k_1_x(unittest.TestCase): with self.assertRaises(IOError): j.read(rlevel=6) - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_write_with_jp2_in_caps(self): - """should be able to write with JP2 suffix.""" - j2k = Jp2k(self.j2kfile) - expdata = j2k.read() - with tempfile.NamedTemporaryFile(suffix='.JP2') as tfile: - ofile = Jp2k(tfile.name, 'wb') - ofile.write(expdata) - actdata = ofile.read() - np.testing.assert_array_equal(actdata, expdata) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_write_srgb_without_mct(self): - """should be able to write RGB without specifying mct""" - 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) - - codestream = ofile.get_codestream() - self.assertEqual(codestream.segment[2].spcod[3], 0) # no mct - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - 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) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_write_cprl(self): - """Must be able to write a CPRL progression order file""" - # Issue 17 - j = Jp2k(self.jp2file) - expdata = j.read(rlevel=1) - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - ofile = Jp2k(tfile.name, 'wb') - ofile.write(expdata, prog='CPRL') - actdata = ofile.read() - np.testing.assert_array_equal(actdata, expdata) - - codestream = ofile.get_codestream() - self.assertEqual(codestream.segment[2].spcod[0], glymur.core.CPRL) - def test_jp2_boxes(self): """Verify the boxes of a JP2 file. Basic jp2 test.""" jp2k = Jp2k(self.jp2file) From 5fb827c61e7c14113f0b21453d225ba2d6705842 Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 19 Sep 2013 16:27:19 -0400 Subject: [PATCH 3/8] Attempt to fix travis build. #120 --- .travis.yml | 9 +++------ requirements.txt | 3 +++ 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml index a8a0d1b..f4fc733 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,13 +10,10 @@ before_install: - sudo apt-get install -qq python3-numpy # command to install dependencies -install: - - pip install . --use-mirrors +install: "pip install -r requirements.txt --use-mirrors" # command to run tests -script: - - "python -m unittest discover" +script: "python -m unittest discover" notifications: - email: - - john.g.evans.ne@gmail.com + email: "john.g.evans.ne@gmail.com" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9dd042a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy +contextlib2>=0.4.0 +mock>=1.0.1 From e6ffa0e271b0dde9f66395ed521e58848ab5eb8a Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 19 Sep 2013 16:38:53 -0400 Subject: [PATCH 4/8] Renamed to travis-requirements so as to not be confused with readthedocs. #120 --- .travis.yml | 2 +- requirements.txt => travis-requirements.txt | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename requirements.txt => travis-requirements.txt (100%) diff --git a/.travis.yml b/.travis.yml index f4fc733..4898e65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ before_install: - sudo apt-get install -qq python3-numpy # command to install dependencies -install: "pip install -r requirements.txt --use-mirrors" +install: "pip install -r travis-requirements.txt --use-mirrors" # command to run tests script: "python -m unittest discover" diff --git a/requirements.txt b/travis-requirements.txt similarity index 100% rename from requirements.txt rename to travis-requirements.txt From 28dc0265e453b6b86237f0176a73d634680382be Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 19 Sep 2013 20:56:33 -0400 Subject: [PATCH 5/8] Updated fix for python3. #118 --- glymur/jp2box.py | 2 +- glymur/test/test_jp2k.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 81d8e6c..f05e9de 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1854,7 +1854,7 @@ class XMLBox(Jp2kBox): except UnicodeDecodeError as ude: # Possibly bad string of bytes to begin with. # Try to search for -1: text = read_buffer[decl_start:].decode('utf-8') else: diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 1ab5b29..4f06745 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -147,7 +147,6 @@ class TestBadButRecoverableXmlFile(unittest.TestCase): # Write out 48 good bytes constituting the XML payload. write_buffer = b'this is a test' - write_buffer = write_buffer.encode() tfile.write(write_buffer) # Get the rest of the input file. From 4ff3a94bb12fbc218e07a156f9efbbb86a180859 Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 19 Sep 2013 21:39:31 -0400 Subject: [PATCH 6/8] Basic refactoring, pylint work. #118 --- glymur/test/test_jp2box.py | 366 +++++++++++++++++++++++---------- glymur/test/test_jp2box_xml.py | 266 ++++++++++++++++++++++++ glymur/test/test_jp2k.py | 123 ----------- 3 files changed, 525 insertions(+), 230 deletions(-) create mode 100644 glymur/test/test_jp2box_xml.py diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 71dbd72..72b9f9d 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -320,113 +320,6 @@ class TestChannelDefinition(unittest.TestCase): association=association) -@unittest.skipIf(os.name == "nt", "Temporary file issue on window.") -class TestXML(unittest.TestCase): - """Test suite for XML boxes.""" - - def setUp(self): - self.jp2file = glymur.data.nemo() - self.j2kfile = glymur.data.goodstuff() - - raw_xml = b""" - - - 1 - 2008 - 141100 - - - - - 4 - 2011 - 59900 - - - - 68 - 2011 - 13600 - - - - """ - with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile: - tfile.write(raw_xml) - tfile.flush() - self.xmlfile = tfile.name - - j2k = Jp2k(self.j2kfile) - codestream = j2k.get_codestream() - height = codestream.segment[1].ysiz - width = codestream.segment[1].xsiz - num_components = len(codestream.segment[1].xrsiz) - - self.jp2b = 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): - os.unlink(self.xmlfile) - - def test_negative_file_and_xml(self): - """The XML should come from only one source.""" - xml_object = ET.parse(self.xmlfile) - with self.assertRaises((IOError, OSError)): - glymur.jp2box.XMLBox(filename=self.xmlfile, xml=xml_object) - - @unittest.skipIf(os.name == "nt", - "Problems using NamedTemporaryFile on windows.") - def test_basic_xml(self): - """Should be able to write a basic 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.jp2b, 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].box_id, 'xml ') - self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()), - b'0') - - @unittest.skipIf(os.name == "nt", - "Problems using NamedTemporaryFile on windows.") - def test_xml_from_file(self): - """Must be able to create an XML box from an XML file.""" - j2k = Jp2k(self.j2kfile) - - self.jp2h.box = [self.ihdr, self.colr] - - xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile) - boxes = [self.jp2b, 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.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): """Test suite for colr box instantiation.""" @@ -856,5 +749,264 @@ class TestJpxBoxes(unittest.TestCase): self.assertEqual(len(jpx.box[5].box), 0) +@unittest.skipIf(os.name == "nt", "Temporary file issue on window.") +class TestChannelDefinition(unittest.TestCase): + """Test suite for channel definition boxes.""" + + @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() + + j2k = Jp2k(self.j2kfile) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) + + self.jp2b = 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) + self.colr_gr = ColourSpecificationBox(colorspace=glymur.core.GREYSCALE) + + def tearDown(self): + pass + + def test_cdef_no_inputs(self): + """channel_type and association are required inputs.""" + with self.assertRaises(IOError): + glymur.jp2box.ChannelDefinitionBox() + + def test_rgb_with_index(self): + """Just regular RGB.""" + j2k = Jp2k(self.j2kfile) + channel_type = [COLOR, COLOR, COLOR] + association = [RED, GREEN, BLUE] + cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], + channel_type=channel_type, + association=association) + boxes = [self.ihdr, self.colr_rgb, cdef] + self.jp2h.box = boxes + boxes = [self.jp2b, 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.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, + (COLOR, COLOR, COLOR)) + self.assertEqual(jp2h.box[2].association, + (RED, GREEN, BLUE)) + + def test_rgb(self): + """Just regular RGB, but don't supply the optional index.""" + j2k = Jp2k(self.j2kfile) + channel_type = [COLOR, COLOR, COLOR] + association = [RED, GREEN, BLUE] + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + boxes = [self.ihdr, self.colr_rgb, cdef] + self.jp2h.box = boxes + boxes = [self.jp2b, 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.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, + (COLOR, COLOR, COLOR)) + self.assertEqual(jp2h.box[2].association, + (RED, GREEN, BLUE)) + + def test_rgba(self): + """Just regular RGBA.""" + j2k = Jp2k(self.four_planes) + channel_type = (COLOR, COLOR, COLOR, OPACITY) + association = (RED, GREEN, BLUE, WHOLE_IMAGE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + boxes = [self.ihdr, self.colr_rgb, cdef] + self.jp2h.box = boxes + boxes = [self.jp2b, 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.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, channel_type) + self.assertEqual(jp2h.box[2].association, association) + + def test_bad_rgba(self): + """R, G, and B must be specified.""" + j2k = Jp2k(self.four_planes) + channel_type = (COLOR, COLOR, OPACITY, OPACITY) + association = (RED, GREEN, BLUE, WHOLE_IMAGE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + boxes = [self.ihdr, self.colr_rgb, cdef] + self.jp2h.box = boxes + boxes = [self.jp2b, 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_grey(self): + """Just regular greyscale.""" + j2k = Jp2k(self.one_plane) + channel_type = (COLOR,) + association = (GREY,) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + boxes = [self.ihdr, self.colr_gr, cdef] + self.jp2h.box = boxes + boxes = [self.jp2b, 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.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, channel_type) + self.assertEqual(jp2h.box[2].association, association) + + def test_grey_alpha(self): + """Just regular greyscale plus alpha.""" + j2k = Jp2k(self.two_planes) + channel_type = (COLOR, OPACITY) + association = (GREY, WHOLE_IMAGE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + boxes = [self.ihdr, self.colr_gr, cdef] + self.jp2h.box = boxes + boxes = [self.jp2b, 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.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, channel_type) + self.assertEqual(jp2h.box[2].association, association) + + def test_bad_grey_alpha(self): + """A greyscale image with alpha layer must specify a color channel""" + j2k = Jp2k(self.two_planes) + + channel_type = (OPACITY, OPACITY) + association = (GREY, WHOLE_IMAGE) + + # This cdef box + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + boxes = [self.ihdr, self.colr_gr, cdef] + self.jp2h.box = boxes + boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises((OSError, IOError)): + j2k.wrap(tfile.name, boxes=boxes) + + def test_only_one_cdef_in_jp2h(self): + """There can only be one channel definition box in the jp2 header.""" + j2k = Jp2k(self.j2kfile) + + channel_type = (COLOR, COLOR, COLOR) + association = (RED, GREEN, BLUE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + + boxes = [self.ihdr, cdef, self.colr_rgb, cdef] + self.jp2h.box = boxes + + boxes = [self.jp2b, 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_jp2h(self): + """need cdef in jp2h""" + j2k = Jp2k(self.j2kfile) + boxes = [self.ihdr, self.colr_rgb] + self.jp2h.box = boxes + + channel_type = (COLOR, COLOR, COLOR) + association = (RED, GREEN, BLUE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + + boxes = [self.jp2b, 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 + same length. + """ + channel_type = (COLOR, COLOR, 3) + association = (RED, GREEN, BLUE) + with self.assertRaises(IOError): + glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + + def test_wrong_lengths(self): + """Should reject if not all of index, channel_type, association the + same length. + """ + channel_type = (COLOR, COLOR) + association = (RED, GREEN, BLUE) + with self.assertRaises(IOError): + glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + + if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_jp2box_xml.py b/glymur/test/test_jp2box_xml.py new file mode 100644 index 0000000..ba1ff15 --- /dev/null +++ b/glymur/test/test_jp2box_xml.py @@ -0,0 +1,266 @@ +""" +Test suite specifically targeting JP2 box layout. +""" +# E1103: return value from read may be list or np array +# pylint: disable=E1103 + +# F0401: unittest2 is needed on python-2.6 (pylint on 2.7) +# pylint: disable=F0401 + +# R0902: More than 7 instance attributes are just fine for testing. +# pylint: disable=R0902 + +# R0904: Seems like pylint is fooled in this situation +# pylint: disable=R0904 + +# W0613: load_tests doesn't need to use ignore or loader arguments. +# pylint: disable=W0613 + +import os +import struct +import sys +import tempfile +import warnings +import xml.etree.cElementTree as ET + +if sys.hexversion < 0x02070000: + import unittest2 as unittest +else: + import unittest + +import glymur +from glymur import Jp2k +from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox +from glymur.jp2box import FileTypeBox, ImageHeaderBox, JP2HeaderBox +from glymur.jp2box import JPEG2000SignatureBox + + +@unittest.skipIf(os.name == "nt", "Temporary file issue on window.") +class TestXML(unittest.TestCase): + """Test suite for XML boxes.""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + raw_xml = b""" + + + 1 + 2008 + 141100 + + + + + 4 + 2011 + 59900 + + + + 68 + 2011 + 13600 + + + + """ + with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile: + tfile.write(raw_xml) + tfile.flush() + self.xmlfile = tfile.name + + j2k = Jp2k(self.j2kfile) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) + + self.jp2b = 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): + os.unlink(self.xmlfile) + + def test_negative_file_and_xml(self): + """The XML should come from only one source.""" + xml_object = ET.parse(self.xmlfile) + with self.assertRaises((IOError, OSError)): + glymur.jp2box.XMLBox(filename=self.xmlfile, xml=xml_object) + + @unittest.skipIf(os.name == "nt", + "Problems using NamedTemporaryFile on windows.") + def test_basic_xml(self): + """Should be able to write a basic 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.jp2b, 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].box_id, 'xml ') + self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()), + b'0') + + @unittest.skipIf(os.name == "nt", + "Problems using NamedTemporaryFile on windows.") + def test_xml_from_file(self): + """Must be able to create an XML box from an XML file.""" + j2k = Jp2k(self.j2kfile) + + self.jp2h.box = [self.ihdr, self.colr] + + xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile) + boxes = [self.jp2b, 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.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(os.name == "nt", "NamedTemporaryFile issue on windows") +class TestJp2kBadXmlFile(unittest.TestCase): + """Test suite for bad XML box situations""" + + @classmethod + def setUpClass(cls): + """Setup a JP2 file with a bad XML box. We only need to do this once + per class rather than once per test. + """ + jp2file = glymur.data.nemo() + with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: + cls._bad_xml_file = tfile.name + with open(jp2file, 'rb') as ifile: + # Everything up until the UUID box. + write_buffer = ifile.read(77) + tfile.write(write_buffer) + + # Write the xml box with bad xml + # Length = 28, id is 'xml '. + write_buffer = struct.pack('>I4s', int(28), b'xml ') + tfile.write(write_buffer) + + write_buffer = 'this is a test' + write_buffer = write_buffer.encode() + tfile.write(write_buffer) + + # Get the rest of the input file. + write_buffer = ifile.read() + tfile.write(write_buffer) + tfile.flush() + + @classmethod + def tearDownClass(cls): + os.unlink(cls._bad_xml_file) + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + def tearDown(self): + pass + + @unittest.skipIf(sys.hexversion < 0x03020000, + "Uses features introduced in 3.2.") + def test_invalid_xml_box_warning(self): + """Should warn in case of bad XML""" + with self.assertWarns(UserWarning): + Jp2k(self._bad_xml_file) + + def test_invalid_xml_box(self): + """Should be able to recover info from xml box with bad xml.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + jp2k = Jp2k(self._bad_xml_file) + + self.assertEqual(jp2k.box[3].box_id, 'xml ') + self.assertEqual(jp2k.box[3].offset, 77) + self.assertEqual(jp2k.box[3].length, 28) + self.assertIsNone(jp2k.box[3].xml) + + +@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") +class TestBadButRecoverableXmlFile(unittest.TestCase): + """Test suite for XML box that is bad, but we can still recover the XML.""" + + @classmethod + def setUpClass(cls): + """Setup a JP2 file with bad bytes preceding the XML. We only need + to do this once per class rather than once per test. + """ + jp2file = glymur.data.nemo() + with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: + cls._bad_xml_file = tfile.name + with open(jp2file, 'rb') as ifile: + # Everything up until the UUID box. + write_buffer = ifile.read(77) + tfile.write(write_buffer) + + # Write the xml box with bad xml + # Length = 64, id is 'xml '. + write_buffer = struct.pack('>I4s', int(64), b'xml ') + tfile.write(write_buffer) + + # Write out 8 bad bytes. + write_buffer = b'\x00\x00\x07\x90xml ' + tfile.write(write_buffer) + + # Write out 48 good bytes constituting the XML payload. + write_buffer = b'' + tfile.write(write_buffer) + write_buffer = b'this is a test' + tfile.write(write_buffer) + + # Get the rest of the input file. + write_buffer = ifile.read() + tfile.write(write_buffer) + tfile.flush() + + @classmethod + def tearDownClass(cls): + os.unlink(cls._bad_xml_file) + + @unittest.skipIf(sys.hexversion < 0x03020000, + "Uses features introduced in 3.2.") + def test_bad_xml_box_warning(self): + """Should warn in case of bad XML""" + with self.assertWarns(UserWarning): + Jp2k(self._bad_xml_file) + + def test_recover_from_bad_xml(self): + """Should be able to recover info from xml box with bad xml.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + jp2 = Jp2k(self._bad_xml_file) + + self.assertEqual(jp2.box[3].box_id, 'xml ') + self.assertEqual(jp2.box[3].offset, 77) + self.assertEqual(jp2.box[3].length, 64) + self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()), + b'this is a test') + + diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 4f06745..0a7f509 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -55,129 +55,6 @@ def load_tests(loader, tests, ignore): return tests -@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, - "Missing openjp2 library.") -class TestJp2kBadXmlFile(unittest.TestCase): - """Test suite for bad XML box situations""" - - @classmethod - def setUpClass(cls): - """Setup a JP2 file with a bad XML box. We only need to do this once - per class rather than once per test. - """ - jp2file = pkg_resources.resource_filename(glymur.__name__, - "data/nemo.jp2") - with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: - cls._bad_xml_file = tfile.name - with open(jp2file, 'rb') as ifile: - # Everything up until the UUID box. - write_buffer = ifile.read(77) - tfile.write(write_buffer) - - # Write the xml box with bad xml - # Length = 28, id is 'xml '. - write_buffer = struct.pack('>I4s', int(28), b'xml ') - tfile.write(write_buffer) - - write_buffer = 'this is a test' - write_buffer = write_buffer.encode() - tfile.write(write_buffer) - - # Get the rest of the input file. - write_buffer = ifile.read() - tfile.write(write_buffer) - tfile.flush() - - @classmethod - def tearDownClass(cls): - os.unlink(cls._bad_xml_file) - - def setUp(self): - self.jp2file = glymur.data.nemo() - self.j2kfile = glymur.data.goodstuff() - - def tearDown(self): - pass - - @unittest.skipIf(sys.hexversion < 0x03020000, - "Uses features introduced in 3.2.") - def test_invalid_xml_box_warning(self): - """Should warn in case of bad XML""" - with self.assertWarns(UserWarning): - Jp2k(self._bad_xml_file) - - def test_invalid_xml_box(self): - """Should be able to recover info from xml box with bad xml.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - jp2k = Jp2k(self._bad_xml_file) - - self.assertEqual(jp2k.box[3].box_id, 'xml ') - self.assertEqual(jp2k.box[3].offset, 77) - self.assertEqual(jp2k.box[3].length, 28) - self.assertIsNone(jp2k.box[3].xml) - - -@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") -class TestBadButRecoverableXmlFile(unittest.TestCase): - """Test suite for XML box that is bad, but we can still recover the XML.""" - - @classmethod - def setUpClass(cls): - """Setup a JP2 file with bad bytes preceding the XML. We only need - to do this once per class rather than once per test. - """ - jp2file = glymur.data.nemo() - with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: - cls._bad_xml_file = tfile.name - with open(jp2file, 'rb') as ifile: - # Everything up until the UUID box. - write_buffer = ifile.read(77) - tfile.write(write_buffer) - - # Write the xml box with bad xml - # Length = 64, id is 'xml '. - write_buffer = struct.pack('>I4s', int(64), b'xml ') - tfile.write(write_buffer) - - # Write out 8 bad bytes. - write_buffer = b'\x00\x00\x07\x90xml ' - tfile.write(write_buffer) - - # Write out 48 good bytes constituting the XML payload. - write_buffer = b'this is a test' - tfile.write(write_buffer) - - # Get the rest of the input file. - write_buffer = ifile.read() - tfile.write(write_buffer) - tfile.flush() - - @classmethod - def tearDownClass(cls): - os.unlink(cls._bad_xml_file) - - @unittest.skipIf(sys.hexversion < 0x03020000, - "Uses features introduced in 3.2.") - def test_bad_xml_box_warning(self): - """Should warn in case of bad XML""" - with self.assertWarns(UserWarning): - Jp2k(self._bad_xml_file) - - def test_recover_from_bad_xml(self): - """Should be able to recover info from xml box with bad xml.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - jp2 = Jp2k(self._bad_xml_file) - - self.assertEqual(jp2.box[3].box_id, 'xml ') - self.assertEqual(jp2.box[3].offset, 77) - self.assertEqual(jp2.box[3].length, 64) - self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()), - b'this is a test') - - @unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and not OPENJP2_IS_V2_OFFICIAL, "Missing openjp2 library version 2.0+.") From d124549df1221fcabd73ef2219714b40e2538e6a Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 19 Sep 2013 21:43:31 -0400 Subject: [PATCH 7/8] Prepping for 0.5.1 release. --- CHANGES.txt | 3 +++ docs/source/conf.py | 2 +- glymur/version.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 35b355e..03d3b94 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +Sep 19, 2013 - v0.5.1 Added more resiliency to XML box parsing. Fixed tests + that failed if OPJ_DATA_ROOT not set. + Sep 16, 2013 - v0.5.0 Added write support for 1.5.x. Added version module. Aug 21, 2013 - v0.4.1 Fixed segfault with openjpeg 1.x when rlevel=-1 diff --git a/docs/source/conf.py b/docs/source/conf.py index 6b9eb18..25d944f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -78,7 +78,7 @@ copyright = u'2013, John Evans' # The short X.Y version. version = '0.5' # The full version, including alpha/beta/rc tags. -release = '0.5.0' +release = '0.5.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/glymur/version.py b/glymur/version.py index 740d610..274a222 100644 --- a/glymur/version.py +++ b/glymur/version.py @@ -13,7 +13,7 @@ from distutils.version import LooseVersion from .lib import openjpeg as opj from .lib import openjp2 as opj2 -version = "0.5.0" +version = "0.5.1rc1" _sv = LooseVersion(version) version_tuple = _sv.version From ce23560062423abfedd0c153f71202f1bab800a1 Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 19 Sep 2013 21:45:59 -0400 Subject: [PATCH 8/8] Finalizing 0.5.1 release. --- glymur/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glymur/version.py b/glymur/version.py index 274a222..8b1eb50 100644 --- a/glymur/version.py +++ b/glymur/version.py @@ -13,7 +13,7 @@ from distutils.version import LooseVersion from .lib import openjpeg as opj from .lib import openjp2 as opj2 -version = "0.5.1rc1" +version = "0.5.1" _sv = LooseVersion(version) version_tuple = _sv.version