diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 6d5c3ee..81d8e6c 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1849,11 +1849,29 @@ class XMLBox(Jp2kBox): """ num_bytes = offset + length - fptr.tell() read_buffer = fptr.read(num_bytes) - text = read_buffer.decode('utf-8') + try: + text = read_buffer.decode('utf-8') + 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: + 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)