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):