From d036c12a0768652be8aa25c3e6cc9fc28ab31664 Mon Sep 17 00:00:00 2001 From: John Evans Date: Thu, 15 Aug 2013 17:10:41 -0400 Subject: [PATCH 01/20] Handles testing gracefully when PIL not present. If PIL is not present, certain writing tests should not be run. These tests would show up as errors instead. Closes #102. --- glymur/test/fixtures.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index ad4fe6d..1c196fb 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -29,6 +29,12 @@ NO_READ_BACKEND_MSG += "order to run the tests in this suite." try: from matplotlib.pyplot import imread + + # The whole point of trying to import PIL is to determine if it's there + # or not. We won't use it directly. + # pylint: disable=F0401,W0611 + import PIL + NO_READ_BACKEND = False except ImportError: NO_READ_BACKEND = True From 61a5497ba74abcdf5d559211591cc43c08e8a608 Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 15 Aug 2013 20:28:15 -0400 Subject: [PATCH 02/20] Bumping for 0.3.2 release. --- CHANGES.txt | 3 +++ docs/source/conf.py | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index cc297ba..bba1195 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +Aug 15, 2013 - v0.3.2 Fixed test bug where missing Pillow package caused test + failures. + Aug 14, 2013 - v0.3.1 Exposed mantissa, exponent, and guard_bits fields in QCC and QCD segments. Exposed layers and code_block_size in COD segment. Exposed precinct_size in COC segment. diff --git a/docs/source/conf.py b/docs/source/conf.py index 573ddd5..cf140a3 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.3' # The full version, including alpha/beta/rc tags. -release = '0.3.1' +release = '0.3.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index ac05b8d..5cd8376 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages import sys kwargs = {'name': 'Glymur', - 'version': '0.3.1', + 'version': '0.3.2', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans', From 08ff86e4a24985f6e0eec93b1ed578660b4bb854 Mon Sep 17 00:00:00 2001 From: jevans Date: Sun, 18 Aug 2013 14:47:49 -0400 Subject: [PATCH 03/20] Added append capability, #103 --- glymur/jp2k.py | 40 ++++++++++++++- glymur/test/test_jp2box.py | 101 ++++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index ea8ef5e..d5c604c 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -432,6 +432,42 @@ class Jp2k(Jp2kBox): # Refresh the metadata. self.parse() + def append(self, box): + """Append a JP2 box to the file in-place. + + Parameters + ---------- + box : Jp2Box + Instance of a JP2 box. Currently only XML boxes are allowed. + """ + if self._codec_format == opj2.CODEC_J2K: + msg = "Only JP2 files can currently have boxes appended to them." + raise IOError(msg) + + if box.box_id != 'xml ': + raise IOError("Only XML boxes can currently be appended.") + + # Check the last box. If the length field is zero, then rewrite + # the length field to reflect the true length of the box. + with open(self.filename, 'rb') as ifile: + offset = self.box[-1].offset + ifile.seek(offset) + read_buffer = ifile.read(4) + box_length, = struct.unpack('>I', read_buffer) + if box_length == 0: + # Reopen the file in write mode and rewrite the length field. + true_box_length = os.path.getsize(ifile.name) - offset + with open(self.filename, 'r+b') as ofile: + ofile.seek(offset) + write_buffer = struct.pack('>I', true_box_length) + ofile.write(write_buffer) + + # Can now safely append the box. + with open(self.filename, 'ab') as ofile: + box.write(ofile) + + self.parse() + def wrap(self, filename, boxes=None): """Write the codestream back out to file, wrapped in new JP2 jacket. @@ -1133,6 +1169,7 @@ def _unpack_colorspace(colorspace, img_array, cparams): return colorspace + def _populate_comptparms(img_array, cparams): """Instantiate and populate comptparms structure. @@ -1171,6 +1208,7 @@ def _populate_comptparms(img_array, cparams): return comptparms + def _populate_image_struct(cparams, image, imgdata): """Populates image struct needed for compression. @@ -1203,6 +1241,7 @@ def _populate_image_struct(cparams, image, imgdata): return image + def _validate_compression_params(img_array, cparams): """Check that the compression parameters are valid. @@ -1313,4 +1352,3 @@ class LibraryNotFoundError(IOError): """ def __init__(self, msg): IOError.__init__(self, msg) - diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 15c0209..71dbd72 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -18,8 +18,11 @@ Test suite specifically targeting JP2 box layout. import doctest import os +import shutil +import struct import sys import tempfile +import uuid import xml.etree.cElementTree as ET if sys.hexversion < 0x02070000: @@ -502,6 +505,100 @@ class TestColourSpecificationBox(unittest.TestCase): approximation=approx) +@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, + "Missing openjp2 library.") +class TestAppend(unittest.TestCase): + """Tests for append method.""" + + def setUp(self): + self.j2kfile = glymur.data.goodstuff() + self.jp2file = glymur.data.nemo() + + def tearDown(self): + pass + + def test_append_xml(self): + """Should be able to append an XML box.""" + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + shutil.copyfile(self.jp2file, tfile.name) + + jp2 = Jp2k(tfile.name) + the_xml = ET.fromstring('0') + xmlbox = glymur.jp2box.XMLBox(xml=the_xml) + jp2.append(xmlbox) + + # The sequence of box IDs should be the same as before, but with an + # xml box at the end. + box_ids = [box.box_id for box in jp2.box] + expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml '] + self.assertEqual(box_ids, expected) + self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()), + b'0') + + def test_only_jp2_allowed_to_append(self): + """Only JP2 files are allowed to be appended.""" + with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: + shutil.copyfile(self.j2kfile, tfile.name) + + jp2 = Jp2k(tfile.name) + + # Make a UUID box. + uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000') + data = b'0123456789' + uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data) + with self.assertRaises(IOError): + jp2.append(uuidbox) + + def test_length_field_is_zero(self): + """L=0 (length field in box header) is handled. + + L=0 implies that the containing box is the last box. If this is not + handled properly, the appended box is never seen. + """ + baseline_jp2 = Jp2k(self.jp2file) + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + with open(self.jp2file, 'rb') as ifile: + # Everything up until the jp2c box. + offset = baseline_jp2.box[-1].offset + tfile.write(ifile.read(offset)) + + # Write the L, T fields of the jp2c box such that L == 0 + write_buffer = struct.pack('>I4s', int(0), b'jp2c') + tfile.write(write_buffer) + + # Write out the rest of the codestream. + ifile.seek(offset+8) + tfile.write(ifile.read()) + tfile.flush() + + jp2 = Jp2k(tfile.name) + the_xml = ET.fromstring('0') + xmlbox = glymur.jp2box.XMLBox(xml=the_xml) + jp2.append(xmlbox) + + # The sequence of box IDs should be the same as before, but with an + # xml box at the end. + box_ids = [box.box_id for box in jp2.box] + expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml '] + self.assertEqual(box_ids, expected) + self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()), + b'0') + + def test_only_xml_allowed_to_append(self): + """Only XML boxes are allowed to be appended.""" + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + shutil.copyfile(self.jp2file, tfile.name) + + jp2 = Jp2k(tfile.name) + + # Make a UUID box. + uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000') + data = b'0123456789' + uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data) + with self.assertRaises(IOError): + jp2.append(uuidbox) + + @unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, "Missing openjp2 library.") class TestWrap(unittest.TestCase): @@ -703,7 +800,7 @@ class TestJp2Boxes(unittest.TestCase): def test_default_ihdr(self): """Should be able to instantiate an image header box.""" ihdr = glymur.jp2box.ImageHeaderBox(height=512, width=256, - num_components=3) + num_components=3) self.assertEqual(ihdr.height, 512) self.assertEqual(ihdr.width, 256) self.assertEqual(ihdr.num_components, 3) @@ -715,7 +812,7 @@ class TestJp2Boxes(unittest.TestCase): """Should be able to set jp2h boxes.""" box = JP2HeaderBox() box.box = [ImageHeaderBox(height=512, width=256), - ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)] + ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)] self.assertTrue(True) def test_default_ccodestreambox(self): From 5edbe6f078ff12b2d64df5757145cca6104fa7d3 Mon Sep 17 00:00:00 2001 From: jevans Date: Sun, 18 Aug 2013 14:48:17 -0400 Subject: [PATCH 04/20] pylint, pep8 work. --- glymur/codestream.py | 2 +- glymur/test/fixtures.py | 2 +- glymur/test/test_callbacks.py | 2 +- glymur/test/test_codestream.py | 2 +- glymur/test/test_config.py | 2 +- glymur/test/test_opj_suite.py | 2 +- glymur/test/test_opj_suite_write.py | 3 ++- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/glymur/codestream.py b/glymur/codestream.py index 7a32190..e83c5cc 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -1308,7 +1308,7 @@ class QCCsegment(Segment): self.offset = offset self.mantissa, self.exponent = parse_quantization(self.spqcc, - self.sqcc) + self.sqcc) self.guard_bits = (self.sqcc & 0xe0) >> 5 def __str__(self): diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index 1c196fb..eec9e22 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -30,7 +30,7 @@ NO_READ_BACKEND_MSG += "order to run the tests in this suite." try: from matplotlib.pyplot import imread - # The whole point of trying to import PIL is to determine if it's there + # The whole point of trying to import PIL is to determine if it's there # or not. We won't use it directly. # pylint: disable=F0401,W0611 import PIL diff --git a/glymur/test/test_callbacks.py b/glymur/test/test_callbacks.py index 992ca3a..722c5da 100644 --- a/glymur/test/test_callbacks.py +++ b/glymur/test/test_callbacks.py @@ -91,7 +91,7 @@ class TestCallbacks15(unittest.TestCase): def test_info_callbacks_on_read(self): """Verify stdout when reading. - + Verify that we get the expected stdio output when our internal info callback handler is enabled. """ diff --git a/glymur/test/test_codestream.py b/glymur/test/test_codestream.py index f9d4d75..8db5039 100644 --- a/glymur/test/test_codestream.py +++ b/glymur/test/test_codestream.py @@ -5,7 +5,7 @@ Test suite for codestream parsing. # unittest doesn't work well with R0904. # pylint: disable=R0904 -# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2 +# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2 # pylint: disable=E1101 # unittest2 is python2.6 only (pylint/python-2.7) diff --git a/glymur/test/test_config.py b/glymur/test/test_config.py index b475b91..3a70ee1 100644 --- a/glymur/test/test_config.py +++ b/glymur/test/test_config.py @@ -4,7 +4,7 @@ OPENJP2 may be present in some form or other. # unittest doesn't work well with R0904. # pylint: disable=R0904 -# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2 +# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2 # pylint: disable=E1101 # unittest.mock only in Python 3.3 (python2.7/pylint import issue) diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index a264019..c622f44 100644 --- a/glymur/test/test_opj_suite.py +++ b/glymur/test/test_opj_suite.py @@ -17,7 +17,7 @@ suite. # unittest fools pylint with "too many public methods" # pylint: disable=R0904 -# Some tests use numpy test infrastructure, which means the tests never +# Some tests use numpy test infrastructure, which means the tests never # reference "self", so pylint claims it should be a function. No, no, no. # pylint: disable=R0201 diff --git a/glymur/test/test_opj_suite_write.py b/glymur/test/test_opj_suite_write.py index 6cc37f3..e6f1575 100644 --- a/glymur/test/test_opj_suite_write.py +++ b/glymur/test/test_opj_suite_write.py @@ -441,7 +441,8 @@ class TestSuiteWrite(unittest.TestCase): self.assertEqual(len(codestream.segment[2].spcod), 9) # 18 SOP segments. - nsops = [x.nsop for x in codestream.segment if x.marker_id == 'SOP'] + nsops = [x.nsop for x in codestream.segment + if x.marker_id == 'SOP'] self.assertEqual(nsops, list(range(18))) def test_NR_ENC_Bretagne2_ppm_7_encode(self): From e74718655fcbb80ff8bf40eae22d0c459752b82d Mon Sep 17 00:00:00 2001 From: jevans Date: Sun, 18 Aug 2013 19:58:45 -0400 Subject: [PATCH 05/20] Added example for append method. Closes #103. --- docs/source/how_do_i.rst | 50 +++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index eee558c..110ca81 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -3,7 +3,7 @@ How do I...? ------------ -Read the lowest resolution thumbnail? +read the lowest resolution thumbnail? ===================================== Printing the Jp2k object should reveal the number of resolutions (look in the COD segment section), but you can take a shortcut by supplying -1 as the @@ -14,7 +14,7 @@ resolution level. :: >>> j = glymur.Jp2k(file) >>> thumbnail = j.read(rlevel=-1) -Display metadata? +display metadata? ================= There are two ways. From the unix command line, the script *jp2dump* is available. :: @@ -34,8 +34,41 @@ codestream box, only the main header is printed. It is possible to print >>> print(j.get_codestream()) -Add XML Metadata? +add XML metadata? ================= +You can append any number of XML boxes to a JP2 file (not to a raw codestream). +Consider the following XML file `data.xml` : :: + + + + + Boston + 24.9 inches + + + Portland + 31.9 inches + + + New York City + 11.4 inches + + + +The **append** method can add an XML box (only XML boxes are currently +allowed):: + + >>> import shutil + >>> import glymur + >>> shutil.copyfile(glymur.data.nemo(), 'myfile.jp2') + >>> from xml.etree import cElementTree as ET + >>> jp2 = glymur.Jp2k('myfile.jp2') + >>> xmlbox = glymur.jp2box.XMLBox(filename='data.xml') + >>> jp2.append(xmlbox) + >>> print(jp2) + +add metadata in a more general fashion? +======================================= 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` (a file consisting of a raw codestream), @@ -87,7 +120,7 @@ though. Take the following example content in an XML file `favorites.xml` : :: 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') + >>> xmlbox = glymur.jp2box.XMLBox(filename='favorites.xml') >>> boxes.insert(3, xmlbox) >>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes) >>> print(jp2_xml) @@ -119,7 +152,12 @@ and add it after the JP2 header box, but before the codestream box :: . (truncated) . -Create an image with an alpha layer? +As to the question of which method you should use, **append** or **wrap**, +to add metadata, you should keep in mind that **wrap** produces a new JP2 file, +while **append** modifies an existing file and is currently limited to XML +boxes. + +create an image with an alpha layer? ==================================== OpenJPEG can create JP2 files with more than 3 components (requires @@ -181,7 +219,7 @@ Here's how the Preview application on the mac shows the RGBA image. .. image:: goodstuff_alpha.png -Work with XMP UUIDs? +work with XMP UUIDs? ==================== The example JP2 file shipped with glymur has an XMP UUID. :: From c16280df59ed02256d4ba14daa124f4edfd5cc04 Mon Sep 17 00:00:00 2001 From: jevans Date: Sun, 18 Aug 2013 20:02:21 -0400 Subject: [PATCH 06/20] Prepping for 0.4 release. --- CHANGES.txt | 2 ++ docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index bba1195..8a4fe86 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,5 @@ +Aug 18, 2013 - v0.4.0 Added append method. + Aug 15, 2013 - v0.3.2 Fixed test bug where missing Pillow package caused test failures. diff --git a/docs/source/conf.py b/docs/source/conf.py index cf140a3..fb2a685 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -76,9 +76,9 @@ copyright = u'2013, John Evans' # built documents. # # The short X.Y version. -version = '0.3' +version = '0.4' # The full version, including alpha/beta/rc tags. -release = '0.3.2' +release = '0.4.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 5cd8376..b57fef1 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages import sys kwargs = {'name': 'Glymur', - 'version': '0.3.2', + 'version': '0.4.0rc1', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans', From 0750fed2d07aae6ff5e4a8bcf55bef57605900be Mon Sep 17 00:00:00 2001 From: jevans Date: Sun, 18 Aug 2013 20:13:56 -0400 Subject: [PATCH 07/20] Finalizing 0.4.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b57fef1..6249662 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages import sys kwargs = {'name': 'Glymur', - 'version': '0.4.0rc1', + 'version': '0.4.0', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans', From 3e91c145501a3f760398febf08e613fcb6ade9ea Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 19 Aug 2013 18:47:46 -0400 Subject: [PATCH 08/20] First attempt at travis ci --- .travis.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3c58f95 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.3" + +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq numpy + +# command to install dependencies +install: + - pip install numpy -q --use-mirrors + - pip install matplotlib --use-mirrors + - pip install . --use-mirrors +# command to run tests +script: + - "python -m unittest discover" From d265968682caa2212a13893de38623984781d69f Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 19 Aug 2013 18:53:36 -0400 Subject: [PATCH 09/20] Make the travis build more verbose. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3c58f95..d365344 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,4 +15,4 @@ install: - pip install . --use-mirrors # command to run tests script: - - "python -m unittest discover" + - "python -m unittest discover -v" From d636a0e3e9805fd0cfdf7a9b284baa5bd273b295 Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 19 Aug 2013 19:03:11 -0400 Subject: [PATCH 10/20] Adding libopenjpeg2 to apt-get install, taking out numpy --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d365344..8b436de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: before_install: - sudo apt-get update -qq - - sudo apt-get install -qq numpy + - sudo apt-get install -qq libopenjpeg2 # command to install dependencies install: From efa2e23d628339306bf85934d6a9ac112f8ba932 Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 19 Aug 2013 19:19:07 -0400 Subject: [PATCH 11/20] Remove 2.6 build because it uses unittest2 instead of unittest. Closes #105 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8b436de..0ff068a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.6" - "2.7" - "3.3" @@ -15,4 +14,4 @@ install: - pip install . --use-mirrors # command to run tests script: - - "python -m unittest discover -v" + - "python -m unittest discover" From bbc7808175469acdf95567abb3e16127f30648bc Mon Sep 17 00:00:00 2001 From: jevans Date: Tue, 20 Aug 2013 18:53:37 -0400 Subject: [PATCH 12/20] Moved TestConfig tests into config. Reworked two jp2k tests. The jp2k tests were relying on the OPJ_DATA_ROOT test files, which was not necessary. #105 --- glymur/test/test_config.py | 43 +++++++++++++++++++++++++++++++ glymur/test/test_jp2k.py | 52 ++------------------------------------ 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/glymur/test/test_config.py b/glymur/test/test_config.py index 3a70ee1..af95bd3 100644 --- a/glymur/test/test_config.py +++ b/glymur/test/test_config.py @@ -90,5 +90,48 @@ class TestSuite(unittest.TestCase): with self.assertWarns(UserWarning): imp.reload(glymur.lib.openjp2) + +@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and + glymur.lib.openjpeg.OPENJPEG is None, + "Missing openjp2 library.") +class TestConfig(unittest.TestCase): + """Test suite for reading without proper library in place.""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + def tearDown(self): + pass + + def test_read_without_library(self): + """Don't have either openjp2 or openjpeg libraries? Must error out. + """ + with patch('glymur.lib.openjp2.OPENJP2', new=None): + with patch('glymur.lib.openjpeg.OPENJPEG', new=None): + with self.assertRaises(glymur.jp2k.LibraryNotFoundError): + glymur.Jp2k(self.jp2file).read() + + def test_read_bands_without_library(self): + """Don't have openjp2 library? Must error out. + """ + with patch('glymur.lib.openjp2.OPENJP2', new=None): + with patch('glymur.lib.openjpeg.OPENJPEG', new=None): + with self.assertRaises(glymur.jp2k.LibraryNotFoundError): + glymur.Jp2k(self.jp2file).read_bands() + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_write_without_library(self): + """Don't have openjp2 library? Must error out. + """ + data = glymur.Jp2k(self.j2kfile).read() + with patch('glymur.lib.openjp2.OPENJP2', new=None): + with patch('glymur.lib.openjpeg.OPENJPEG', new=None): + with self.assertRaises(glymur.jp2k.LibraryNotFoundError): + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + ofile = Jp2k(tfile.name, 'wb') + ofile.write(data) + + if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 747782f..f44906e 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -65,48 +65,6 @@ def load_tests(loader, tests, ignore): return tests -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and - glymur.lib.openjpeg.OPENJPEG is None, - "Missing openjp2 library.") -class TestConfig(unittest.TestCase): - """Test suite for reading without proper library in place.""" - - def setUp(self): - self.jp2file = glymur.data.nemo() - self.j2kfile = glymur.data.goodstuff() - - def tearDown(self): - pass - - def test_read_without_library(self): - """Don't have either openjp2 or openjpeg libraries? Must error out. - """ - with patch('glymur.lib.openjp2.OPENJP2', new=None): - with patch('glymur.lib.openjpeg.OPENJPEG', new=None): - with self.assertRaises(glymur.jp2k.LibraryNotFoundError): - glymur.Jp2k(self.jp2file).read() - - def test_read_bands_without_library(self): - """Don't have openjp2 library? Must error out. - """ - with patch('glymur.lib.openjp2.OPENJP2', new=None): - with patch('glymur.lib.openjpeg.OPENJPEG', new=None): - with self.assertRaises(glymur.jp2k.LibraryNotFoundError): - glymur.Jp2k(self.jp2file).read_bands() - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_write_without_library(self): - """Don't have openjp2 library? Must error out. - """ - data = glymur.Jp2k(self.j2kfile).read() - with patch('glymur.lib.openjp2.OPENJP2', new=None): - with patch('glymur.lib.openjpeg.OPENJPEG', new=None): - with self.assertRaises(glymur.jp2k.LibraryNotFoundError): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - ofile = Jp2k(tfile.name, 'wb') - ofile.write(data) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") @unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, "Missing openjp2 library.") @@ -333,13 +291,10 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[2].box[1].colorspace, glymur.core.SRGB) self.assertIsNone(jp2k.box[2].box[1].icc_profile) - @unittest.skipIf(DATA_ROOT is None, - "OPJ_DATA_ROOT environment variable not set") def test_j2k_box(self): """A J2K/J2C file must not have any boxes.""" # Verify that a J2K file has no boxes. - filename = os.path.join(DATA_ROOT, 'input/conformance/p0_01.j2k') - jp2k = Jp2k(filename) + jp2k = Jp2k(self.j2kfile) self.assertEqual(len(jp2k.box), 0) @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") @@ -425,14 +380,11 @@ class TestJp2k(unittest.TestCase): with self.assertRaises(RuntimeError): j.read() - @unittest.skipIf(DATA_ROOT is None, - "OPJ_DATA_ROOT environment variable not set") def test_empty_box_with_j2k(self): """Verify that the list of boxes in a J2C/J2K file is present, but empty. """ - filename = os.path.join(DATA_ROOT, 'input/conformance/p0_05.j2k') - j = Jp2k(filename) + j = Jp2k(self.j2kfile) self.assertEqual(j.box, []) @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") From 5c190e8d9e7c450c326e736c67f61e69977f84dc Mon Sep 17 00:00:00 2001 From: jevans Date: Tue, 20 Aug 2013 20:01:01 -0400 Subject: [PATCH 13/20] Added check for rlevel=-1 for openjpeg 1.5. Added 2.x tests to 1.5 suite. Closes #106 --- glymur/jp2k.py | 5 + glymur/test/test_jp2k.py | 617 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 622 insertions(+) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index d5c604c..9623abc 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -636,6 +636,11 @@ class Jp2k(Jp2kBox): """ self._subsampling_sanity_check() + if rlevel == -1: + # Get the lowest resolution thumbnail. + codestream = self.get_codestream() + rlevel = codestream.segment[2].spcod[4] + with ExitStack() as stack: # Set decoding parameters. dparameters = opj.DecompressionParametersType() diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index f44906e..2ca5d9b 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -38,6 +38,7 @@ import glymur from glymur import Jp2k from .fixtures import OPENJP2_IS_V2_OFFICIAL +from .fixtures import OPENJPEG_VERSION try: DATA_ROOT = os.environ['OPJ_DATA_ROOT'] @@ -367,6 +368,18 @@ class TestJp2k(unittest.TestCase): self.assertEqual(new_jp2.box[j].length, baseline_jp2.box[j].length) + def test_basic_jp2(self): + """Just a very basic test that reading a JP2 file does not error out. + """ + j2k = Jp2k(self.jp2file) + j2k.read(rlevel=1) + + def test_basic_j2k(self): + """Just a very basic test that reading a J2K file does not error out. + """ + j2k = Jp2k(self.j2kfile) + j2k.read() + @unittest.skipIf(DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") def test_read_differing_subsamples(self): @@ -772,6 +785,14 @@ class TestJp2k15(unittest.TestCase): def tearDown(self): pass + def test_rlevel_max(self): + """Verify that rlevel=-1 gets us the lowest resolution image""" + j = Jp2k(self.j2kfile) + thumbnail2 = j.read(rlevel=5) + thumbnail1 = j.read(rlevel=-1) + np.testing.assert_array_equal(thumbnail1, thumbnail2) + self.assertEqual(thumbnail1.shape, (25, 15, 3)) + def test_area(self): """Area option not allowed for 1.5.1. """ @@ -793,6 +814,215 @@ class TestJp2k15(unittest.TestCase): with self.assertRaises(TypeError): j2k.read(layer=1) + def test_rlevel_too_high(self): + """Should error out appropriately if reduce level too high""" + j = Jp2k(self.jp2file) + with self.assertRaises(ValueError): + j.read(rlevel=6) + + def test_not_jpeg2000(self): + """Should error out appropriately if not given a JPEG 2000 file.""" + filename = pkg_resources.resource_filename(glymur.__name__, "jp2k.py") + with self.assertRaises(IOError): + Jp2k(filename) + + def test_file_not_present(self): + """Should error out if reading from a file that does not exist""" + # Verify that we error out appropriately if not given an existing file + # at all. + with self.assertRaises(OSError): + filename = 'this file does not actually exist on the file system.' + Jp2k(filename) + + @unittest.skip("Writing requires openjp2 library at the moment.") + @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.skip("Writing requires openjp2 library at the moment.") + @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.skip("Writing requires openjp2 library at the moment.") + @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.skip("Writing requires openjp2 library at the moment.") + @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) + + # top-level boxes + self.assertEqual(len(jp2k.box), 6) + + self.assertEqual(jp2k.box[0].box_id, 'jP ') + self.assertEqual(jp2k.box[0].offset, 0) + self.assertEqual(jp2k.box[0].length, 12) + self.assertEqual(jp2k.box[0].longname, 'JPEG 2000 Signature') + + self.assertEqual(jp2k.box[1].box_id, 'ftyp') + self.assertEqual(jp2k.box[1].offset, 12) + self.assertEqual(jp2k.box[1].length, 20) + self.assertEqual(jp2k.box[1].longname, 'File Type') + + self.assertEqual(jp2k.box[2].box_id, 'jp2h') + self.assertEqual(jp2k.box[2].offset, 32) + self.assertEqual(jp2k.box[2].length, 45) + self.assertEqual(jp2k.box[2].longname, 'JP2 Header') + + self.assertEqual(jp2k.box[3].box_id, 'uuid') + self.assertEqual(jp2k.box[3].offset, 77) + self.assertEqual(jp2k.box[3].length, 638) + + self.assertEqual(jp2k.box[4].box_id, 'uuid') + self.assertEqual(jp2k.box[4].offset, 715) + self.assertEqual(jp2k.box[4].length, 2412) + + self.assertEqual(jp2k.box[5].box_id, 'jp2c') + self.assertEqual(jp2k.box[5].offset, 3127) + self.assertEqual(jp2k.box[5].length, 1132296) + + # jp2h super box + self.assertEqual(len(jp2k.box[2].box), 2) + + self.assertEqual(jp2k.box[2].box[0].box_id, 'ihdr') + self.assertEqual(jp2k.box[2].box[0].offset, 40) + self.assertEqual(jp2k.box[2].box[0].length, 22) + self.assertEqual(jp2k.box[2].box[0].longname, 'Image Header') + self.assertEqual(jp2k.box[2].box[0].height, 1456) + self.assertEqual(jp2k.box[2].box[0].width, 2592) + self.assertEqual(jp2k.box[2].box[0].num_components, 3) + 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].colorspace_unknown, False) + self.assertEqual(jp2k.box[2].box[0].ip_provided, False) + + self.assertEqual(jp2k.box[2].box[1].box_id, 'colr') + self.assertEqual(jp2k.box[2].box[1].offset, 62) + self.assertEqual(jp2k.box[2].box[1].length, 15) + self.assertEqual(jp2k.box[2].box[1].longname, 'Colour Specification') + self.assertEqual(jp2k.box[2].box[1].precedence, 0) + self.assertEqual(jp2k.box[2].box[1].approximation, 0) + self.assertEqual(jp2k.box[2].box[1].colorspace, glymur.core.SRGB) + self.assertIsNone(jp2k.box[2].box[1].icc_profile) + + def test_j2k_box(self): + """A J2K/J2C file must not have any boxes.""" + # Verify that a J2K file has no boxes. + jp2k = Jp2k(self.j2kfile) + self.assertEqual(len(jp2k.box), 0) + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_64bit_xl_field(self): + """XL field should be supported""" + # Verify that boxes with the XL field are properly read. + # Don't have such a file on hand, so we create one. Copy our example + # file, but making the codestream have a 64-bit XL field. + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + with open(self.jp2file, 'rb') as ifile: + # Everything up until the jp2c box. + write_buffer = ifile.read(3127) + tfile.write(write_buffer) + + # The L field must be 1 in order to signal the presence of the + # XL field. The actual length of the jp2c box increased by 8 + # (8 bytes for the XL field). + length = 1 + typ = b'jp2c' + xlen = 1133427 + 8 + write_buffer = struct.pack('>I4sQ', int(length), typ, xlen) + tfile.write(write_buffer) + + # Get the rest of the input file (minus the 8 bytes for L and + # T. + ifile.seek(8, 1) + write_buffer = ifile.read() + tfile.write(write_buffer) + tfile.flush() + + jp2k = Jp2k(tfile.name) + + self.assertEqual(jp2k.box[5].box_id, 'jp2c') + self.assertEqual(jp2k.box[5].offset, 3127) + self.assertEqual(jp2k.box[5].length, 1133427 + 8) + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_length_field_is_zero(self): + """L=0 (length field in box header) is allowed""" + # Verify that boxes with the L field as zero are correctly read. + # This should only happen in the last box of a JPEG 2000 file. + # Our example image has its last box at byte 588458. + baseline_jp2 = Jp2k(self.jp2file) + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + with open(self.jp2file, 'rb') as ifile: + # Everything up until the jp2c box. + write_buffer = ifile.read(588458) + tfile.write(write_buffer) + + length = 0 + typ = b'uuid' + write_buffer = struct.pack('>I4s', int(length), typ) + tfile.write(write_buffer) + + # Get the rest of the input file (minus the 8 bytes for L and + # T. + ifile.seek(8, 1) + write_buffer = ifile.read() + tfile.write(write_buffer) + tfile.flush() + + new_jp2 = Jp2k(tfile.name) + + # The top level boxes in each file should match. + for j in range(len(baseline_jp2.box)): + self.assertEqual(new_jp2.box[j].box_id, + baseline_jp2.box[j].box_id) + self.assertEqual(new_jp2.box[j].offset, + baseline_jp2.box[j].offset) + self.assertEqual(new_jp2.box[j].length, + baseline_jp2.box[j].length) + def test_basic_jp2(self): """This test is only useful when openjp2 is not available and OPJ_DATA_ROOT is not set. We need at least one @@ -809,6 +1039,393 @@ class TestJp2k15(unittest.TestCase): j2k = Jp2k(self.j2kfile) j2k.read() + @unittest.skipIf(DATA_ROOT is None, + "OPJ_DATA_ROOT environment variable not set") + def test_read_differing_subsamples(self): + """should error out with read used on differently subsampled images""" + # Verify that we error out appropriately if we use the read method + # on an image with differing subsamples + # + # Issue 86. + filename = os.path.join(DATA_ROOT, 'input/conformance/p0_05.j2k') + j = Jp2k(filename) + with self.assertRaises(RuntimeError): + j.read() + + def test_empty_box_with_j2k(self): + """Verify that the list of boxes in a J2C/J2K file is present, but + empty. + """ + j = Jp2k(self.j2kfile) + self.assertEqual(j.box, []) + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_cblkh_different_than_width(self): + """Verify that we can set a code block size where height does not equal + width. + """ + data = np.zeros((128, 128), dtype=np.uint8) + with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: + j = Jp2k(tfile.name, 'wb') + + # The code block dimensions are given as rows x columns. + j.write(data, cbsize=(16, 32)) + + codestream = j.get_codestream() + + # Code block size is reported as XY in the codestream. + self.assertEqual(tuple(codestream.segment[2].spcod[5:7]), (3, 2)) + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_too_many_dimensions(self): + """OpenJP2 only allows 2D or 3D images.""" + with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: + j = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError): + data = np.zeros((128, 128, 2, 2), dtype=np.uint8) + j.write(data) + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_unrecognized_jp2_clrspace(self): + """We only allow RGB and GRAYSCALE. Should error out with others""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError): + data = np.zeros((128, 128, 3), dtype=np.uint8) + j.write(data, colorspace='cmyk') + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_2d_rgb(self): + """RGB must have at least 3 components.""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError): + data = np.zeros((128, 128, 2), dtype=np.uint8) + j.write(data, colorspace='rgb') + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_colorspace_with_j2k(self): + """Specifying a colorspace with J2K does not make sense""" + with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: + j = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError): + data = np.zeros((128, 128, 3), dtype=np.uint8) + j.write(data, colorspace='rgb') + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_specify_rgb(self): + """specify RGB explicitly""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 3), dtype=np.uint8) + j.write(data, colorspace='rgb') + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB) + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_specify_gray(self): + """test gray explicitly specified (that's GRAY, not GREY)""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128), dtype=np.uint8) + j.write(data, colorspace='gray') + self.assertEqual(j.box[2].box[1].colorspace, + glymur.core.GREYSCALE) + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_specify_grey(self): + """test grey explicitly specified""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128), dtype=np.uint8) + j.write(data, colorspace='grey') + self.assertEqual(j.box[2].box[1].colorspace, + glymur.core.GREYSCALE) + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_grey_with_extra_component(self): + """version 2.0 cannot write gray + extra""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 2), dtype=np.uint8) + j.write(data) + self.assertEqual(j.box[2].box[0].height, 128) + self.assertEqual(j.box[2].box[0].width, 128) + self.assertEqual(j.box[2].box[0].num_components, 2) + self.assertEqual(j.box[2].box[1].colorspace, + glymur.core.GREYSCALE) + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_grey_with_two_extra_comps(self): + """should be able to write gray + two extra components""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 3), dtype=np.uint8) + j.write(data, colorspace='gray') + self.assertEqual(j.box[2].box[0].height, 128) + self.assertEqual(j.box[2].box[0].width, 128) + self.assertEqual(j.box[2].box[0].num_components, 3) + self.assertEqual(j.box[2].box[1].colorspace, + glymur.core.GREYSCALE) + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_rgb_with_extra_component(self): + """v2.0+ should be able to write extra components""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 4), dtype=np.uint8) + j.write(data) + self.assertEqual(j.box[2].box[0].height, 128) + self.assertEqual(j.box[2].box[0].width, 128) + self.assertEqual(j.box[2].box[0].num_components, 4) + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB) + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_extra_components_on_v2(self): + """must error out in 1.x with extra components.""" + # Extra components seems to require 2.0+. Verify that we error out. + with self.assertRaises(IOError): + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 4), dtype=np.uint8) + j.write(data) + + @unittest.skip("Writing requires openjp2 library at the moment.") + def test_specify_ycc(self): + """Should reject YCC""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError): + data = np.zeros((128, 128, 3), dtype=np.uint8) + j.write(data, colorspace='ycc') + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_uinf_ulst_url_boxes(self): + """Verify that we can read UINF, ULST, and URL boxes""" + # Verify that we can read UINF, ULST, and URL boxes. I don't have + # easy access to such a file, and there's no such file in the + # openjpeg repository, so I'll fake one. + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + with open(self.jp2file, 'rb') as ifile: + # Everything up until the jp2c box. + write_buffer = ifile.read(77) + tfile.write(write_buffer) + + # Write the UINF superbox + # Length = 50, id is uinf. + write_buffer = struct.pack('>I4s', int(50), b'uinf') + tfile.write(write_buffer) + + # Write the ULST box. + # Length is 26, 1 UUID, hard code that UUID as zeros. + write_buffer = struct.pack('>I4sHIIII', int(26), b'ulst', + int(1), int(0), int(0), int(0), + int(0)) + tfile.write(write_buffer) + + # Write the URL box. + # Length is 16, version is one byte, flag is 3 bytes, url + # is the rest. + write_buffer = struct.pack('>I4sBBBB', + int(16), b'url ', + int(0), int(0), int(0), int(0)) + tfile.write(write_buffer) + write_buffer = struct.pack('>ssss', b'a', b'b', b'c', b'd') + tfile.write(write_buffer) + + # Get the rest of the input file. + write_buffer = ifile.read() + tfile.write(write_buffer) + tfile.flush() + + jp2k = Jp2k(tfile.name) + + self.assertEqual(jp2k.box[3].box_id, 'uinf') + self.assertEqual(jp2k.box[3].offset, 77) + self.assertEqual(jp2k.box[3].length, 50) + + self.assertEqual(jp2k.box[3].box[0].box_id, 'ulst') + self.assertEqual(jp2k.box[3].box[0].offset, 85) + self.assertEqual(jp2k.box[3].box[0].length, 26) + ulst = [] + ulst.append(uuid.UUID('00000000-0000-0000-0000-000000000000')) + self.assertEqual(jp2k.box[3].box[0].ulst, ulst) + + self.assertEqual(jp2k.box[3].box[1].box_id, 'url ') + self.assertEqual(jp2k.box[3].box[1].offset, 111) + self.assertEqual(jp2k.box[3].box[1].length, 16) + self.assertEqual(jp2k.box[3].box[1].version, 0) + self.assertEqual(jp2k.box[3].box[1].flag, (0, 0, 0)) + self.assertEqual(jp2k.box[3].box[1].url, 'abcd') + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_xml_with_trailing_nulls(self): + """ElementTree doesn't like trailing null chars after valid XML text""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + with open(self.jp2file, 'rb') as ifile: + # Everything up until the jp2c box. + write_buffer = ifile.read(77) + tfile.write(write_buffer) + + # Write the xml box + # Length = 36, id is 'xml '. + write_buffer = struct.pack('>I4s', int(36), b'xml ') + tfile.write(write_buffer) + + write_buffer = 'this is a test' + chr(0) + 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() + + jp2k = Jp2k(tfile.name) + + self.assertEqual(jp2k.box[3].box_id, 'xml ') + self.assertEqual(jp2k.box[3].offset, 77) + self.assertEqual(jp2k.box[3].length, 36) + self.assertEqual(ET.tostring(jp2k.box[3].xml.getroot()), + b'this is a test') + + @unittest.skip("Writing requires openjp2 library at the moment.") + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_asoc_label_box(self): + """Test asoc and label box""" + # Construct a fake file with an asoc and a label box, as + # OpenJPEG doesn't have such a file. + data = Jp2k(self.jp2file).read(rlevel=1) + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + j.write(data) + + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile2: + + # Offset of the codestream is where we start. + read_buffer = tfile.read(77) + tfile2.write(read_buffer) + + # read the rest of the file, it's the codestream. + codestream = tfile.read() + + # Write the asoc superbox. + # Length = 36, id is 'asoc'. + write_buffer = struct.pack('>I4s', int(56), b'asoc') + tfile2.write(write_buffer) + + # Write the contained label box + write_buffer = struct.pack('>I4s', int(13), b'lbl ') + tfile2.write(write_buffer) + tfile2.write('label'.encode()) + + # Write the xml box + # Length = 36, id is 'xml '. + write_buffer = struct.pack('>I4s', int(35), b'xml ') + tfile2.write(write_buffer) + + write_buffer = 'this is a test' + write_buffer = write_buffer.encode() + tfile2.write(write_buffer) + + # Now append the codestream. + tfile2.write(codestream) + tfile2.flush() + + jasoc = Jp2k(tfile2.name) + self.assertEqual(jasoc.box[3].box_id, 'asoc') + self.assertEqual(jasoc.box[3].box[0].box_id, 'lbl ') + self.assertEqual(jasoc.box[3].box[0].label, 'label') + self.assertEqual(jasoc.box[3].box[1].box_id, 'xml ') + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + @unittest.skipIf(re.match('1\.[345]\.\d', OPENJPEG_VERSION) is not None, + "Segfault on official v1.x series.") + def test_openjpeg_library_message(self): + """Verify the error message produced by the openjpeg library""" + # This will confirm that the error callback mechanism is working. + with open(self.jp2file, 'rb') as fptr: + data = fptr.read() + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + # Codestream starts at byte 3127. SIZ marker at 3137. + # COD marker at 3186. Subsampling at 3180. + tfile.write(data[0:3179]) + + # Make the DY bytes of the SIZ segment zero. That means that + # a subsampling factor is zero, which is illegal. + tfile.write(b'\x00') + tfile.write(data[3180:3182]) + tfile.write(b'\x00') + tfile.write(data[3184:3186]) + tfile.write(b'\x00') + + tfile.write(data[3186:]) + tfile.flush() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + j = Jp2k(tfile.name) + regexp = re.compile(r'''OpenJPEG\slibrary\serror:\s+ + Invalid\svalues\sfor\scomp\s=\s0\s+ + :\sdx=1\sdy=0''', re.VERBOSE) + if sys.hexversion < 0x03020000: + with self.assertRaisesRegexp((IOError, OSError), regexp): + j.read(rlevel=1) + else: + with self.assertRaisesRegex((IOError, OSError), regexp): + j.read(rlevel=1) + + def test_xmp_attribute(self): + """Verify the XMP packet in the shipping example file can be read.""" + j = Jp2k(self.jp2file) + xmp = j.box[4].data + ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' + ns1 = '{http://ns.adobe.com/xap/1.0/}' + name = '{0}RDF/{0}Description'.format(ns0) + elt = xmp.find(name) + attr_value = elt.attrib['{0}CreatorTool'.format(ns1)] + self.assertEqual(attr_value, 'glymur') + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_unrecognized_exif_tag(self): + """An unrecognized exif tag should be handled gracefully.""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + shutil.copyfile(self.jp2file, tfile.name) + + # The Exif UUID starts at byte 77. There are 8 bytes for the L and + # T fields, then 16 bytes for the UUID identifier, then 6 exif + # header bytes, then 8 bytes for the TIFF header, then 2 bytes + # the the Image IFD number of tags, where we finally find the first + # tag, "Make" (271). We'll corrupt it by changing it into 171, + # which does not correspond to any known Exif Image tag. + with open(tfile.name, 'r+b') as fptr: + fptr.seek(117) + write_buffer = struct.pack(' Date: Tue, 20 Aug 2013 20:23:51 -0400 Subject: [PATCH 14/20] Trying to install debs for numpy and matplotlib instead of pip --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0ff068a..c1c3b39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,13 @@ python: before_install: - sudo apt-get update -qq - sudo apt-get install -qq libopenjpeg2 + - sudo apt-get install -qq numpy + - sudo apt-get install -qq matplotlib # command to install dependencies +# - pip install numpy -q --use-mirrors +# - pip install matplotlib --use-mirrors install: - - pip install numpy -q --use-mirrors - - pip install matplotlib --use-mirrors - pip install . --use-mirrors # command to run tests script: From b7e589c7819aaadf7dce3a83bf76465895f12e38 Mon Sep 17 00:00:00 2001 From: jevans Date: Tue, 20 Aug 2013 20:36:03 -0400 Subject: [PATCH 15/20] Don't need matplotlib yet, try python-numpy instead of numpy. --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c1c3b39..3e6ca20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,11 @@ python: before_install: - sudo apt-get update -qq - sudo apt-get install -qq libopenjpeg2 - - sudo apt-get install -qq numpy - - sudo apt-get install -qq matplotlib + - sudo apt-get install -qq python-numpy # command to install dependencies -# - pip install numpy -q --use-mirrors -# - pip install matplotlib --use-mirrors install: + - pip install numpy -q --use-mirrors - pip install . --use-mirrors # command to run tests script: From 08578115f625d4288ed491d36f19c5b54342dcc9 Mon Sep 17 00:00:00 2001 From: jevans Date: Tue, 20 Aug 2013 20:45:47 -0400 Subject: [PATCH 16/20] No need to use apt-get with numpy, and then also install with pip still on #105 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3e6ca20..30dd864 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ before_install: - sudo apt-get update -qq - sudo apt-get install -qq libopenjpeg2 - sudo apt-get install -qq python-numpy + - sudo apt-get install -qq python3-numpy # command to install dependencies install: - - pip install numpy -q --use-mirrors - pip install . --use-mirrors # command to run tests script: From 009bd647726b3291115bb20b2633be52ae426cd4 Mon Sep 17 00:00:00 2001 From: John Evans Date: Tue, 20 Aug 2013 22:10:38 -0400 Subject: [PATCH 17/20] Must explicitly check max rlevel on 1.x versions of library. Otherwise it errors badly on 1.5, segfaults on 1.3. Closes #106. --- glymur/jp2k.py | 14 +++++++++++--- glymur/test/test_jp2k.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 9623abc..bd90dfe 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -636,10 +636,18 @@ class Jp2k(Jp2kBox): """ self._subsampling_sanity_check() - if rlevel == -1: - # Get the lowest resolution thumbnail. + if rlevel != 0: + # Must check the specified rlevel against the maximum. + # OpenJPEG 1.3 will segfault if rlevel is too high. codestream = self.get_codestream() - rlevel = codestream.segment[2].spcod[4] + max_rlevel = codestream.segment[2].spcod[4] + if rlevel == -1: + # -1 is shorthand for the largest rlevel + rlevel = max_rlevel + if rlevel < -1 or rlevel > max_rlevel: + msg = "rlevel must be in the range [-1, {0}] for this image." + msg = msg.format(max_rlevel) + raise IOError(msg) with ExitStack() as stack: # Set decoding parameters. diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 2ca5d9b..ea19199 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -817,7 +817,7 @@ class TestJp2k15(unittest.TestCase): def test_rlevel_too_high(self): """Should error out appropriately if reduce level too high""" j = Jp2k(self.jp2file) - with self.assertRaises(ValueError): + with self.assertRaises(IOError): j.read(rlevel=6) def test_not_jpeg2000(self): From b53a838056c1f236b9d86424a7ba86a5c178b350 Mon Sep 17 00:00:00 2001 From: John Evans Date: Wed, 21 Aug 2013 10:18:06 -0400 Subject: [PATCH 18/20] Prepping for 0.4.1 release. --- CHANGES.txt | 2 ++ docs/source/conf.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8a4fe86..9a71f81 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,5 @@ +Aug 21, 2013 - v0.4.1 Fixed segfault with openjpeg 1.x when rlevel=-1 + Aug 18, 2013 - v0.4.0 Added append method. Aug 15, 2013 - v0.3.2 Fixed test bug where missing Pillow package caused test diff --git a/docs/source/conf.py b/docs/source/conf.py index fb2a685..c5dccb4 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.4' # The full version, including alpha/beta/rc tags. -release = '0.4.0' +release = '0.4.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 6249662..575eca6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages import sys kwargs = {'name': 'Glymur', - 'version': '0.4.0', + 'version': '0.4.1rc1', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans', From d0b63435151f7e5357aa1d1126440ac7ab4601c7 Mon Sep 17 00:00:00 2001 From: John Evans Date: Wed, 21 Aug 2013 10:33:36 -0400 Subject: [PATCH 19/20] minor wording changes --- docs/source/detailed_installation.rst | 5 +++-- docs/source/how_do_i.rst | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/source/detailed_installation.rst b/docs/source/detailed_installation.rst index 126efaa..dab4c3f 100644 --- a/docs/source/detailed_installation.rst +++ b/docs/source/detailed_installation.rst @@ -137,13 +137,14 @@ In addition, you must install contextlib2 and Pillow via pip. :: Windows ------- -32-bit WinPython 2.7.5 seems to work with OpenJPEG 1.X, 2.0, and the -development version, but still requires contextlib2 and mock to be +32-bit WinPython 2.7.5 seemed to work with OpenJPEG 1.X, 2.0, and the +development version, but still required contextlib2 and mock to be installed via pip. WinPython 3.3.2, however, seems to have trouble with OpenJPEG 2.0, so I would suggest using the development version there (I'm unwilling to spend ANY more time trying to figure out what the problem is there). +At the moment I do not have access to a win32 machine, and 64-bit windows is completely untested. diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 110ca81..cc0b227 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -55,8 +55,7 @@ Consider the following XML file `data.xml` : :: -The **append** method can add an XML box (only XML boxes are currently -allowed):: +The **append** method can add an XML box as shown below:: >>> import shutil >>> import glymur @@ -108,8 +107,8 @@ 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, +in order to add an XML box into the mix before the codestream box, we'll need to +re-specify all of the boxes. If you already have a JP2 jacket in place, you can just reuse that, though. Take the following example content in an XML file `favorites.xml` : :: @@ -117,7 +116,8 @@ 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 :: +In order to add the XML after the JP2 header box, but before the codestream box, +the following will work. :: >>> boxes = jp2.box # The box attribute is the list of JP2 boxes >>> xmlbox = glymur.jp2box.XMLBox(filename='favorites.xml') From 1dce49121fcd039b44e431f603cfd609274441a4 Mon Sep 17 00:00:00 2001 From: John Evans Date: Wed, 21 Aug 2013 11:07:29 -0400 Subject: [PATCH 20/20] Finalizing for 0.4.1 release. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 575eca6..ff533a7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages import sys kwargs = {'name': 'Glymur', - 'version': '0.4.1rc1', + 'version': '0.4.1', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans',