Returning a Jp2k object from the wrap method.

This commit is contained in:
John Evans 2013-06-27 08:23:52 -04:00
commit cde8c47e75
6 changed files with 205 additions and 78 deletions

View file

@ -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

View file

@ -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` : ::
<?xml version="1.0"?>
<favorite_things>
<category>Light Ale</category>
</favorite_things>
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)
<favorite_things>
<category>Light Ale</category>
</favorite_things>
Contiguous Codestream Box (jp2c) @ (153, 115236)
Main header:
.
. (truncated)
.
Work with XMP UUIDs?
====================
The example JP2 file shipped with glymur has an XMP UUID. ::

View file

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

View file

@ -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

View file

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

View file

@ -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('<?xml version="1.0"?><data>0</data>')
xmlb = glymur.jp2box.XMLBox(xml=the_xml)
self.assertEqual(ET.tostring(xmlb.xml),
b'<data>0</data>')
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'<data>0</data>')
def test_xml_from_file(self):
raw_xml = b"""<?xml version="1.0"?>
<data>
<country name="Liechtenstein">
@ -295,30 +257,77 @@ class TestXML(unittest.TestCase):
<neighbor name="Colombia" direction="E"/>
</country>
</data>"""
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('<?xml version="1.0"?><data>0</data>')
xmlb = glymur.jp2box.XMLBox(xml=the_xml)
self.assertEqual(ET.tostring(xmlb.xml),
b'<data>0</data>')
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'<data>0</data>')
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,