diff --git a/CHANGES.txt b/CHANGES.txt index f80b04b..410f243 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +Jul 23, 2013 - v0.2.5 Fixed inconsistency in XML handling, now all instances + are always ElementTree objects (issue82). + Jul 21, 2013 - v0.2.4 Fixed markdown bug for Fedora 17 information, fixed out-of-date windows information (issue79). Fixed incorrect interpretation of Psot parameter (issue78). diff --git a/docs/source/conf.py b/docs/source/conf.py index bd59b1b..d9f833e 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.1' # The full version, including alpha/beta/rc tags. -release = '0.2.4' +release = '0.2.5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/detailed_installation.rst b/docs/source/detailed_installation.rst index 06c6b79..b737843 100644 --- a/docs/source/detailed_installation.rst +++ b/docs/source/detailed_installation.rst @@ -137,8 +137,17 @@ platforms. Testing ''''''' -If you wish to run the tests (strongly recommended :-), you can either run them -from within python as follows ... :: +There are two environment variables you may wish to set before running the +tests. + + * **OPJ_DATA_ROOT** - points to directory for OpenJPEG test data + * **FORMAT_CORPUS_ROOT** - points to directory for format-corpus repository (see https://github.com/openplanets/format-corpus) + +Setting these two environment variables is not required, as any tests using +either of them will be skipped. + +In order to run the tests, you can either run them from within +python as follows ... :: >>> import glymur >>> glymur.runtests() diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 8abb693..9ffaee5 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -185,9 +185,8 @@ Work with XMP UUIDs? ==================== The example JP2 file shipped with glymur has an XMP UUID. :: - >>> from glymur import Jp2k - >>> file = glymur.data.nemo() - >>> j = Jp2k(file) + >>> import glymur + >>> j = glymur.Jp2k(glymur.data.nemo()) >>> print(j.box[4]) UUID Box (uuid) @ (715, 2412) UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) @@ -198,7 +197,7 @@ The example JP2 file shipped with glymur has an XMP UUID. :: -Since the UUID data in this case is returned as an ElementTree Element, one can +Since the UUID data in this case is returned as an ElementTree instance, one can use ElementTree to access the data. For example, to extract the **CreatorTool** attribute value, the following would work:: diff --git a/glymur/__init__.py b/glymur/__init__.py index a69d28f..fb37ef0 100644 --- a/glymur/__init__.py +++ b/glymur/__init__.py @@ -6,7 +6,7 @@ from .jp2k import Jp2k from .jp2dump import jp2dump from . import data -from . import test +#from . import test def runtests(): diff --git a/glymur/codestream.py b/glymur/codestream.py index df1575c..c4d8b4b 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -102,7 +102,14 @@ class Codestream(object): while True: read_buffer = fptr.read(2) - marker_id, = struct.unpack('>H', read_buffer) + try: + marker_id, = struct.unpack('>H', read_buffer) + except struct.error: + # Treat this as a warning. + msg = "Marker had length {0} instead of expected length of 2 " + msg += "bytes. Codestream parsing terminated." + warnings.warn(msg.format(len(read_buffer))) + break if marker_id == 0xff90 and header_only: # Start-of-tile (SOT) means that we are out of the main header diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 5baebac..319bf46 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -22,6 +22,7 @@ from .codestream import Codestream from .core import SRGB from .core import GREYSCALE from .core import PROGRESSION_ORDER +from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE from .jp2box import Jp2kBox from .jp2box import JPEG2000SignatureBox from .jp2box import FileTypeBox @@ -159,6 +160,24 @@ class Jp2k(Jp2kBox): # boxes) here. fptr.seek(0) self.box = self.parse_superbox(fptr) + self._validate() + + def _validate(self): + """Validate the JPEG 2000 outermost superbox. + """ + # A jp2-branded file cannot contain an "any ICC profile + ftyp = self.box[1] + if ftyp.brand == 'jp2 ': + jp2h = [box for box in self.box if box.box_id == 'jp2h'][0] + colrs = [box for box in jp2h.box if box.box_id == 'colr'] + for colr in colrs: + if colr.method not in (ENUMERATED_COLORSPACE, + RESTRICTED_ICC_PROFILE): + msg = "Color Specification box method must specify either " + msg += "an enumerated colorspace or a restricted ICC " + msg += "profile if the file type box brand is 'jp2 '." + warnings.warn(msg) + # pylint: disable-msg=W0221 def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None, diff --git a/glymur/test/test_conformance.py b/glymur/test/test_conformance.py new file mode 100644 index 0000000..ec179c2 --- /dev/null +++ b/glymur/test/test_conformance.py @@ -0,0 +1,133 @@ +""" +These tests deal with JPX/JP2/J2K images in the format-corpus repository. +""" +#pylint: disable-all + +import os +import sys + +if sys.hexversion < 0x02070000: + import unittest2 as unittest +else: + import unittest + +import warnings + +from glymur import Jp2k +import glymur + +try: + format_corpus_data_root = os.environ['FORMAT_CORPUS_DATA_ROOT'] +except KeyError: + format_corpus_data_root = None +except: + raise + +try: + opj_data_root = os.environ['OPJ_DATA_ROOT'] +except KeyError: + opj_data_root = None +except: + raise + + +@unittest.skipIf(format_corpus_data_root is None, + "FORMAT_CORPUS_DATA_ROOT environment variable not set") +@unittest.skipIf(sys.hexversion < 0x03020000, + "Requires features introduced in 3.2 (assertWarns)") +class TestSuiteFormatCorpus(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_balloon_trunc1(self): + # Has one byte shaved off of EOC marker. + jfile = os.path.join(format_corpus_data_root, + 'jp2k-test/byteCorruption/balloon_trunc1.jp2') + j2k = Jp2k(jfile) + with self.assertWarns(UserWarning): + c = j2k.get_codestream(header_only=False) + + # The last segment is truncated, so there should not be an EOC marker. + self.assertNotEqual(c.segment[-1].marker_id, 'EOC') + + # The codestream is not as long as claimed. + with self.assertRaises(OSError): + j2k.read(rlevel=-1) + + def test_balloon_trunc2(self): + # Shortened by 5000 bytes. + jfile = os.path.join(format_corpus_data_root, + 'jp2k-test/byteCorruption/balloon_trunc2.jp2') + j2k = Jp2k(jfile) + with self.assertWarns(UserWarning): + c = j2k.get_codestream(header_only=False) + + # The last segment is truncated, so there should not be an EOC marker. + self.assertNotEqual(c.segment[-1].marker_id, 'EOC') + + # The codestream is not as long as claimed. + with self.assertRaises(OSError): + j2k.read(rlevel=-1) + + def test_balloon_trunc3(self): + # Most of last tile is missing. + jfile = os.path.join(format_corpus_data_root, + 'jp2k-test/byteCorruption/balloon_trunc3.jp2') + j2k = Jp2k(jfile) + with self.assertWarns(UserWarning): + c = j2k.get_codestream(header_only=False) + + # The last segment is truncated, so there should not be an EOC marker. + self.assertNotEqual(c.segment[-1].marker_id, 'EOC') + + # Should error out, it does not. + #with self.assertRaises(OSError): + # j2k.read(rlevel=-1) + + def test_jp2_brand_vs_any_icc_profile(self): + # If 'jp2 ', then the method cannot be any icc profile. + jfile = os.path.join(format_corpus_data_root, + 'jp2k-test', 'icc', + 'balloon_eciRGBv2_ps_adobeplugin.jpf') + with self.assertWarns(UserWarning): + j2k = Jp2k(jfile) + + def test_jp2_brand_vs_any_icc_profile_multiple_colr(self): + # Has colr box, one that conforms, one that does not. + + # Wrong 'brand' field; contains two versions of ICC profile: one + # embedded using "Any ICC" method; other embedded using "Restricted + # ICC" method, with description ("Modified eciRGB v2") and profileClass + # ("Input Device") changed relative to original profile. + lst = [format_corpus_data_root, 'jp2k-test', 'icc', + 'balloon_eciRGBv2_ps_adobeplugin_jp2compatible.jpf'] + jfile = os.path.join(*lst) + with self.assertWarns(UserWarning): + j2k = Jp2k(jfile) + + +@unittest.skipIf(opj_data_root is None, + "OPJ_DATA_ROOT environment variable not set") +@unittest.skipIf(sys.hexversion < 0x03020000, + "Requires features introduced in 3.2 (assertWarns)") +class TestSuiteOpj(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_jp2_brand_vs_any_icc_profile(self): + # If 'jp2 ', then the method cannot be any icc profile. + filename = os.path.join(opj_data_root, + 'input/nonregression/text_GBR.jp2') + with self.assertWarns(UserWarning): + j2k = Jp2k(filename) + +if __name__ == "__main__": + unittest.main() diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index 4aab788..4699b06 100644 --- a/glymur/test/test_opj_suite.py +++ b/glymur/test/test_opj_suite.py @@ -990,7 +990,11 @@ class TestSuite(unittest.TestCase): def test_NR_DEC_text_GBR_jp2_29_decode(self): jfile = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') - data = Jp2k(jfile).read() + with warnings.catch_warnings(): + # brand is 'jp2 ', but has any icc profile. + warnings.simplefilter("ignore") + jp2 = Jp2k(jfile) + data = jp2.read() self.assertTrue(True) def test_NR_DEC_pacs_ge_j2k_30_decode(self): @@ -7260,7 +7264,10 @@ class TestSuiteDump(unittest.TestCase): def test_NR_text_GBR_dump(self): jfile = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') - jp2 = Jp2k(jfile) + with warnings.catch_warnings(): + # brand is 'jp2 ', but has any icc profile. + warnings.simplefilter("ignore") + jp2 = Jp2k(jfile) ids = [box.box_id for box in jp2.box] lst = ['jP ', 'ftyp', 'rreq', 'jp2h', diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 09f828c..79a12e1 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -4,6 +4,7 @@ import pkg_resources import struct import sys import tempfile +import warnings if sys.hexversion < 0x02070000: import unittest2 as unittest @@ -277,9 +278,12 @@ class TestPrinting(unittest.TestCase): "OPJ_DATA_ROOT environment variable not set") def test_icc_profile(self): filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') - j = glymur.Jp2k(filename) + with warnings.catch_warnings(): + # brand is 'jp2 ', but has any icc profile. + warnings.simplefilter("ignore") + jp2 = Jp2k(filename) with patch('sys.stdout', new=StringIO()) as fake_out: - print(j.box[3].box[1]) + print(jp2.box[3].box[1]) actual = fake_out.getvalue().strip() lin27 = ["Colour Specification Box (colr) @ (179, 1339)", " Method: any ICC profile", @@ -902,10 +906,13 @@ class TestPrinting(unittest.TestCase): # ICC profiles may be used in JP2, but the approximation field should # be zero unless we have jpx. This file does both. filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') - j = glymur.Jp2k(filename) + with warnings.catch_warnings(): + # brand is 'jp2 ', but has any icc profile. + warnings.simplefilter("ignore") + jp2 = Jp2k(filename) with patch('sys.stdout', new=StringIO()) as fake_out: - print(j.box[3].box[1]) + print(jp2.box[3].box[1]) actual = fake_out.getvalue().strip() lines = ["Colour Specification Box (colr) @ (179, 1339)", " Method: any ICC profile", @@ -942,10 +949,13 @@ class TestPrinting(unittest.TestCase): def test_uuid(self): # UUID box filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') - j = glymur.Jp2k(filename) + with warnings.catch_warnings(): + # brand is 'jp2 ', but has any icc profile. + warnings.simplefilter("ignore") + jp2 = Jp2k(filename) with patch('sys.stdout', new=StringIO()) as fake_out: - print(j.box[4]) + print(jp2.box[4]) actual = fake_out.getvalue().strip() lines = ['UUID Box (uuid) @ (1544, 25)', ' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71', diff --git a/setup.py b/setup.py index 95e778b..8d88a3a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages import sys kwargs = {'name': 'Glymur', - 'version': '0.2.4', + 'version': '0.2.5', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans',