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. :: 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/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/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_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): 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):