diff --git a/docs/source/api.rst b/docs/source/api.rst
index e1efdac..7584718 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -5,7 +5,7 @@ API
Jp2k
----
.. autoclass:: glymur.Jp2k
- :members: read, write, read_bands, get_codestream
+ :members: read, write, wrap, read_bands, get_codestream
Individual Boxes
----------------
@@ -24,9 +24,9 @@ ColourSpecificationBox
.. autoclass:: glymur.jp2box.ColourSpecificationBox
:members:
-ComponentDefinitionBox
+ChannelDefinitionBox
''''''''''''''''''''''
-.. autoclass:: glymur.jp2box.ComponentDefinitionBox
+.. autoclass:: glymur.jp2box.ChannelDefinitionBox
:members:
ComponentMappingBox
diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst
index 355ff14..a4c0167 100644
--- a/docs/source/how_do_i.rst
+++ b/docs/source/how_do_i.rst
@@ -34,6 +34,92 @@ codestream box, only the main header is printed. It is possible to print
>>> print(j.get_codestream())
+Add XML Metadata?
+=================
+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`, you can use the **wrap** method with
+no box argument: ::
+
+ >>> import glymur
+ >>> jfile = glymur.data.goodstuff()
+ >>> j2k = glymur.Jp2k(jfile)
+ >>> jp2 = j2k.wrap("newfile.jp2")
+ >>> print(jp2)
+ File: newfile.jp2
+ JPEG 2000 Signature Box (jP ) @ (0, 12)
+ Signature: 0d0a870a
+ File Type Box (ftyp) @ (12, 20)
+ Brand: jp2
+ Compatibility: ['jp2 ']
+ JP2 Header Box (jp2h) @ (32, 45)
+ Image Header Box (ihdr) @ (40, 22)
+ Size: [800 480 3]
+ Bitdepth: 8
+ Signed: False
+ Compression: wavelet
+ Colorspace Unknown: False
+ Colour Specification Box (colr) @ (62, 15)
+ Method: enumerated colorspace
+ Precedence: 0
+ Colorspace: sRGB
+ Contiguous Codestream Box (jp2c) @ (77, 115228)
+ Main header:
+ .
+ . (truncated)
+ .
+
+The raw codestream was wrapped in a JP2 jacket with four boxes in the outer
+layer (the signature, file type, JP2 header, and contiguous codestream), with
+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,
+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 ::
+
+ >>> boxes = jp2.box # The box attribute is the list of JP2 boxes
+ >>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml')
+ >>> boxes.insert(3, xmlbox)
+ >>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes)
+ >>> print(jp2_xml)
+ File: newfile_with_xml.jp2
+ JPEG 2000 Signature Box (jP ) @ (0, 12)
+ Signature: 0d0a870a
+ File Type Box (ftyp) @ (12, 20)
+ Brand: jp2
+ Compatibility: ['jp2 ']
+ JP2 Header Box (jp2h) @ (32, 45)
+ Image Header Box (ihdr) @ (40, 22)
+ Size: [800 480 3]
+ Bitdepth: 8
+ Signed: False
+ Compression: wavelet
+ Colorspace Unknown: False
+ Colour Specification Box (colr) @ (62, 15)
+ Method: enumerated colorspace
+ Precedence: 0
+ Colorspace: sRGB
+ XML Box (xml ) @ (77, 76)
+
+ Light Ale
+
+
+ Contiguous Codestream Box (jp2c) @ (153, 115236)
+ Main header:
+ .
+ . (truncated)
+ .
+
+
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 04c208f..327dd19 100644
--- a/glymur/codestream.py
+++ b/glymur/codestream.py
@@ -175,7 +175,7 @@ class Codestream(object):
segment = self._parseGenericSegment(f, marker_id)
else:
- msg = 'Invalid marker id encountered at byte {0:d}'
+ msg = 'Invalid marker id encountered at byte {0:d} '
msg += 'in codestream: "0x{1:x}"'
msg = msg.format(offset, marker_id)
raise IOError(msg)
diff --git a/glymur/jp2box.py b/glymur/jp2box.py
index 54edfbd..bc20bbf 100644
--- a/glymur/jp2box.py
+++ b/glymur/jp2box.py
@@ -958,12 +958,6 @@ class JP2HeaderBox(Jp2kBox):
def _write(self, f):
"""Write a JP2 Header box to file.
"""
- # Make sure they are in proper order.
- if self.box[0].id != 'ihdr':
- msg = "The first box in the JP2 Header superbox must be the Image "
- msg += "Header box."
- raise IOError(msg)
-
# Write the contained boxes, then come back and write the length.
orig_pos = f.tell()
f.write(struct.pack('>I', 0))
@@ -1590,7 +1584,10 @@ class XMLBox(Jp2kBox):
msg = Jp2kBox.__str__(self)
xml = self.xml
if self.xml is not None:
- msg += _pretty_print_xml(self.xml)
+ try:
+ msg += _pretty_print_xml(self.xml)
+ except TypeError:
+ msg += _pretty_print_xml(self.xml.getroot())
else:
msg += '\n {0}'.format(xml)
return msg
diff --git a/glymur/jp2k.py b/glymur/jp2k.py
index 846d7fd..93a3a93 100644
--- a/glymur/jp2k.py
+++ b/glymur/jp2k.py
@@ -408,14 +408,29 @@ class Jp2k(Jp2kBox):
self._parse()
def wrap(self, filename, boxes=None):
- """Write a codestream, wrapping it in the JP2 format.
+ """Write the codestream back out to file, wrapped in new JP2 jacket.
Parameters
----------
filename : str
JP2 file to be created from a raw codestream.
boxes : list
- JP2 box definitions to define the JP2 file format.
+ JP2 box definitions to define the JP2 file format. If not
+ provided, a default ""jacket" is assumed, consisting of JP2
+ signature, file type, JP2 header, and contiguous codestream boxes.
+
+ Returns
+ -------
+ jp2 : Jp2k object
+ Newly wrapped Jp2k object.
+
+ Examples
+ --------
+ >>> import glymur, tempfile
+ >>> jfile = glymur.data.goodstuff()
+ >>> j2k = glymur.Jp2k(jfile)
+ >>> tfile = tempfile.NamedTemporaryFile(suffix='jp2')
+ >>> j2k.wrap(tfile.name)
"""
if boxes is None:
# Try to create a reasonable default.
@@ -502,14 +517,31 @@ class Jp2k(Jp2kBox):
box._write(ofile)
else:
# The codestream gets written last.
- ofile.write(struct.pack('>I', self.length + 8))
- ofile.write('jp2c'.encode())
-
- with open(self.filename, 'rb') as ifile:
- ofile.write(ifile.read())
+ if len(self.box) == 0:
+ # Am I a raw codestream? If so, then it is pretty
+ # easy, just write the codestream box header plus all
+ # of myself out to file.
+ ofile.write(struct.pack('>I', self.length + 8))
+ ofile.write('jp2c'.encode())
+ with open(self.filename, 'rb') as ifile:
+ ofile.write(ifile.read())
+ else:
+ # OK, I'm a jp2 file. Need to find out where the
+ # raw codestream actually starts.
+ jp2c = [box for box in self.box if box.id == 'jp2c']
+ jp2c = jp2c[0]
+ ofile.write(struct.pack('>I', jp2c.length + 8))
+ ofile.write('jp2c'.encode())
+ with open(self.filename, 'rb') as ifile:
+ # Seek 8 bytes past the L, T fields to get to the
+ # raw codestream.
+ ifile.seek(jp2c.offset + 8)
+ ofile.write(ifile.read(jp2c.length - 8))
ofile.flush()
+ jp2 = Jp2k(filename)
+ return jp2
def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False):
diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py
index 59a241c..75a16b0 100644
--- a/glymur/test/test_jp2box.py
+++ b/glymur/test/test_jp2box.py
@@ -234,44 +234,6 @@ class TestXML(unittest.TestCase):
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
- j2k = Jp2k(self.j2kfile)
- c = j2k.get_codestream()
- height = c.segment[1].Ysiz
- width = c.segment[1].Xsiz
- num_components = len(c.segment[1].XRsiz)
-
- self.jP = JPEG2000SignatureBox()
- self.ftyp = FileTypeBox()
- self.jp2h = JP2HeaderBox()
- self.jp2c = ContiguousCodestreamBox()
- self.ihdr = ImageHeaderBox(height=height, width=width,
- num_components=num_components)
- self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
-
- def tearDown(self):
- pass
-
- def test_basic_xml(self):
- # Should be able to write an XMLBox.
- j2k = Jp2k(self.j2kfile)
-
- self.jp2h.box = [self.ihdr, self.colr]
-
- the_xml = ET.fromstring('0')
- xmlb = glymur.jp2box.XMLBox(xml=the_xml)
- self.assertEqual(ET.tostring(xmlb.xml),
- b'0')
-
- boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
-
- with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
- j2k.wrap(tfile.name, boxes=boxes)
- jp2 = Jp2k(tfile.name)
- self.assertEqual(jp2.box[3].id, 'xml ')
- self.assertEqual(ET.tostring(jp2.box[3].xml),
- b'0')
-
- def test_xml_from_file(self):
raw_xml = b"""
@@ -295,30 +257,77 @@ class TestXML(unittest.TestCase):
"""
- with tempfile.NamedTemporaryFile(suffix=".xml") as tfile:
+ with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile:
tfile.write(raw_xml)
tfile.flush()
+ self.xmlfile = tfile.name
- j2k = Jp2k(self.j2kfile)
-
- self.jp2h.box = [self.ihdr, self.colr]
+ j2k = Jp2k(self.j2kfile)
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
- xmlb = glymur.jp2box.XMLBox(filename=tfile.name)
- boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
- with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
- j2k.wrap(tfile.name, boxes=boxes)
- jp2 = Jp2k(tfile.name)
+ self.jP = JPEG2000SignatureBox()
+ self.ftyp = FileTypeBox()
+ self.jp2h = JP2HeaderBox()
+ self.jp2c = ContiguousCodestreamBox()
+ self.ihdr = ImageHeaderBox(height=height, width=width,
+ num_components=num_components)
+ self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
- output_boxes = [box.id for box in jp2.box]
- self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ',
- 'jp2c'])
+ def tearDown(self):
+ os.unlink(self.xmlfile)
+ pass
- elts = jp2.box[3].xml.findall('country')
- self.assertEqual(len(elts), 3)
+ def test_negative_both_file_and_xml_provided(self):
+ """The XML should come from only one source."""
+ j2k = Jp2k(self.j2kfile)
+ xml_object = ET.parse(self.xmlfile)
+ with self.assertRaises((IOError, OSError)) as ce:
+ xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile, xml=xml_object)
- neighbor = elts[1].find('neighbor')
- self.assertEqual(neighbor.attrib['name'], 'Malaysia')
- self.assertEqual(neighbor.attrib['direction'], 'N')
+ def test_basic_xml(self):
+ # Should be able to write an XMLBox.
+ j2k = Jp2k(self.j2kfile)
+
+ self.jp2h.box = [self.ihdr, self.colr]
+
+ the_xml = ET.fromstring('0')
+ xmlb = glymur.jp2box.XMLBox(xml=the_xml)
+ self.assertEqual(ET.tostring(xmlb.xml),
+ b'0')
+
+ boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
+
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+ jp2 = Jp2k(tfile.name)
+ self.assertEqual(jp2.box[3].id, 'xml ')
+ self.assertEqual(ET.tostring(jp2.box[3].xml),
+ b'0')
+
+ def test_xml_from_file(self):
+ j2k = Jp2k(self.j2kfile)
+
+ self.jp2h.box = [self.ihdr, self.colr]
+
+ xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile)
+ boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+ jp2 = Jp2k(tfile.name)
+
+ output_boxes = [box.id for box in jp2.box]
+ self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ',
+ 'jp2c'])
+
+ elts = jp2.box[3].xml.findall('country')
+ self.assertEqual(len(elts), 3)
+
+ neighbor = elts[1].find('neighbor')
+ self.assertEqual(neighbor.attrib['name'], 'Malaysia')
+ self.assertEqual(neighbor.attrib['direction'], 'N')
class TestColourSpecificationBox(unittest.TestCase):
@@ -369,22 +378,25 @@ class TestColourSpecificationBox(unittest.TestCase):
def test_ColourSpecificationBox_with_colorspace_and_icc(self):
# Colour specification boxes can't have both.
- with self.assertRaises(IOError):
+ with self.assertRaises((OSError, IOError)):
colorspace = glymur.core.SRGB
icc_profile = b'\x01\x02\x03\x04'
- b = glymur.jp2box.ColourSpecificationBox(colorspace, icc_profile)
+ b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
+ icc_profile=icc_profile)
def test_ColourSpecificationBox_with_bad_method(self):
colorspace = glymur.core.SRGB
method = -1
with self.assertRaises(IOError):
- b = glymur.jp2box.ColourSpecificationBox(colorspace, method)
+ b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
+ method=method)
def test_ColourSpecificationBox_with_bad_approximation(self):
colorspace = glymur.core.SRGB
- approximation = -1
+ approx = -1
with self.assertRaises(IOError):
- b = glymur.jp2box.ColourSpecificationBox(colorspace, approximation)
+ b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
+ approximation=approx)
@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None,