Returning a Jp2k object from the wrap method.
This commit is contained in:
parent
c5f54704b4
commit
cde8c47e75
6 changed files with 205 additions and 78 deletions
|
|
@ -5,7 +5,7 @@ API
|
||||||
Jp2k
|
Jp2k
|
||||||
----
|
----
|
||||||
.. autoclass:: glymur.Jp2k
|
.. autoclass:: glymur.Jp2k
|
||||||
:members: read, write, read_bands, get_codestream
|
:members: read, write, wrap, read_bands, get_codestream
|
||||||
|
|
||||||
Individual Boxes
|
Individual Boxes
|
||||||
----------------
|
----------------
|
||||||
|
|
@ -24,9 +24,9 @@ ColourSpecificationBox
|
||||||
.. autoclass:: glymur.jp2box.ColourSpecificationBox
|
.. autoclass:: glymur.jp2box.ColourSpecificationBox
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
ComponentDefinitionBox
|
ChannelDefinitionBox
|
||||||
''''''''''''''''''''''
|
''''''''''''''''''''''
|
||||||
.. autoclass:: glymur.jp2box.ComponentDefinitionBox
|
.. autoclass:: glymur.jp2box.ChannelDefinitionBox
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
ComponentMappingBox
|
ComponentMappingBox
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,92 @@ codestream box, only the main header is printed. It is possible to print
|
||||||
|
|
||||||
>>> print(j.get_codestream())
|
>>> 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?
|
Work with XMP UUIDs?
|
||||||
====================
|
====================
|
||||||
The example JP2 file shipped with glymur has an XMP UUID. ::
|
The example JP2 file shipped with glymur has an XMP UUID. ::
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ class Codestream(object):
|
||||||
segment = self._parseGenericSegment(f, marker_id)
|
segment = self._parseGenericSegment(f, marker_id)
|
||||||
|
|
||||||
else:
|
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 += 'in codestream: "0x{1:x}"'
|
||||||
msg = msg.format(offset, marker_id)
|
msg = msg.format(offset, marker_id)
|
||||||
raise IOError(msg)
|
raise IOError(msg)
|
||||||
|
|
|
||||||
|
|
@ -958,12 +958,6 @@ class JP2HeaderBox(Jp2kBox):
|
||||||
def _write(self, f):
|
def _write(self, f):
|
||||||
"""Write a JP2 Header box to file.
|
"""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.
|
# Write the contained boxes, then come back and write the length.
|
||||||
orig_pos = f.tell()
|
orig_pos = f.tell()
|
||||||
f.write(struct.pack('>I', 0))
|
f.write(struct.pack('>I', 0))
|
||||||
|
|
@ -1590,7 +1584,10 @@ class XMLBox(Jp2kBox):
|
||||||
msg = Jp2kBox.__str__(self)
|
msg = Jp2kBox.__str__(self)
|
||||||
xml = self.xml
|
xml = self.xml
|
||||||
if self.xml is not None:
|
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:
|
else:
|
||||||
msg += '\n {0}'.format(xml)
|
msg += '\n {0}'.format(xml)
|
||||||
return msg
|
return msg
|
||||||
|
|
|
||||||
|
|
@ -408,14 +408,29 @@ class Jp2k(Jp2kBox):
|
||||||
self._parse()
|
self._parse()
|
||||||
|
|
||||||
def wrap(self, filename, boxes=None):
|
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
|
Parameters
|
||||||
----------
|
----------
|
||||||
filename : str
|
filename : str
|
||||||
JP2 file to be created from a raw codestream.
|
JP2 file to be created from a raw codestream.
|
||||||
boxes : list
|
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:
|
if boxes is None:
|
||||||
# Try to create a reasonable default.
|
# Try to create a reasonable default.
|
||||||
|
|
@ -502,14 +517,31 @@ class Jp2k(Jp2kBox):
|
||||||
box._write(ofile)
|
box._write(ofile)
|
||||||
else:
|
else:
|
||||||
# The codestream gets written last.
|
# The codestream gets written last.
|
||||||
ofile.write(struct.pack('>I', self.length + 8))
|
if len(self.box) == 0:
|
||||||
ofile.write('jp2c'.encode())
|
# Am I a raw codestream? If so, then it is pretty
|
||||||
|
# easy, just write the codestream box header plus all
|
||||||
with open(self.filename, 'rb') as ifile:
|
# of myself out to file.
|
||||||
ofile.write(ifile.read())
|
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()
|
ofile.flush()
|
||||||
|
|
||||||
|
jp2 = Jp2k(filename)
|
||||||
|
return jp2
|
||||||
|
|
||||||
|
|
||||||
def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False):
|
def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False):
|
||||||
|
|
|
||||||
|
|
@ -234,44 +234,6 @@ class TestXML(unittest.TestCase):
|
||||||
self.jp2file = glymur.data.nemo()
|
self.jp2file = glymur.data.nemo()
|
||||||
self.j2kfile = glymur.data.goodstuff()
|
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"?>
|
raw_xml = b"""<?xml version="1.0"?>
|
||||||
<data>
|
<data>
|
||||||
<country name="Liechtenstein">
|
<country name="Liechtenstein">
|
||||||
|
|
@ -295,30 +257,77 @@ class TestXML(unittest.TestCase):
|
||||||
<neighbor name="Colombia" direction="E"/>
|
<neighbor name="Colombia" direction="E"/>
|
||||||
</country>
|
</country>
|
||||||
</data>"""
|
</data>"""
|
||||||
with tempfile.NamedTemporaryFile(suffix=".xml") as tfile:
|
with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile:
|
||||||
tfile.write(raw_xml)
|
tfile.write(raw_xml)
|
||||||
tfile.flush()
|
tfile.flush()
|
||||||
|
self.xmlfile = tfile.name
|
||||||
|
|
||||||
j2k = Jp2k(self.j2kfile)
|
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.jp2h.box = [self.ihdr, self.colr]
|
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)
|
||||||
|
|
||||||
xmlb = glymur.jp2box.XMLBox(filename=tfile.name)
|
def tearDown(self):
|
||||||
boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
|
os.unlink(self.xmlfile)
|
||||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
pass
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
|
||||||
jp2 = Jp2k(tfile.name)
|
|
||||||
|
|
||||||
output_boxes = [box.id for box in jp2.box]
|
def test_negative_both_file_and_xml_provided(self):
|
||||||
self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ',
|
"""The XML should come from only one source."""
|
||||||
'jp2c'])
|
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)
|
||||||
|
|
||||||
elts = jp2.box[3].xml.findall('country')
|
def test_basic_xml(self):
|
||||||
self.assertEqual(len(elts), 3)
|
# Should be able to write an XMLBox.
|
||||||
|
j2k = Jp2k(self.j2kfile)
|
||||||
|
|
||||||
neighbor = elts[1].find('neighbor')
|
self.jp2h.box = [self.ihdr, self.colr]
|
||||||
self.assertEqual(neighbor.attrib['name'], 'Malaysia')
|
|
||||||
self.assertEqual(neighbor.attrib['direction'], 'N')
|
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):
|
class TestColourSpecificationBox(unittest.TestCase):
|
||||||
|
|
@ -369,22 +378,25 @@ class TestColourSpecificationBox(unittest.TestCase):
|
||||||
|
|
||||||
def test_ColourSpecificationBox_with_colorspace_and_icc(self):
|
def test_ColourSpecificationBox_with_colorspace_and_icc(self):
|
||||||
# Colour specification boxes can't have both.
|
# Colour specification boxes can't have both.
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises((OSError, IOError)):
|
||||||
colorspace = glymur.core.SRGB
|
colorspace = glymur.core.SRGB
|
||||||
icc_profile = b'\x01\x02\x03\x04'
|
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):
|
def test_ColourSpecificationBox_with_bad_method(self):
|
||||||
colorspace = glymur.core.SRGB
|
colorspace = glymur.core.SRGB
|
||||||
method = -1
|
method = -1
|
||||||
with self.assertRaises(IOError):
|
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):
|
def test_ColourSpecificationBox_with_bad_approximation(self):
|
||||||
colorspace = glymur.core.SRGB
|
colorspace = glymur.core.SRGB
|
||||||
approximation = -1
|
approx = -1
|
||||||
with self.assertRaises(IOError):
|
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,
|
@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue