Merge branch 'issue152' into devel
This commit is contained in:
commit
a54ce4bad4
5 changed files with 186 additions and 11 deletions
|
|
@ -1,8 +1,9 @@
|
||||||
Feb 03, 2014 - Removed support for Python 2.6. Added write support for
|
Feb 03, 2014 - Removed support for Python 2.6. Added write support for
|
||||||
Palette and Component Mapping boxes. Added read support for JPX free,
|
JP2 Palette and Component Mapping boxes, JPX Association and
|
||||||
number list, data reference, fragment table, and fragment list boxes.
|
NumberList boxes. Added read support for JPX free, number list,
|
||||||
Palette box now a 2D numpy array instead of a list of 1D arrays.
|
data reference, fragment table, and fragment list boxes. Palette
|
||||||
JP2 super box constructors now take optional box list argument.
|
box now a 2D numpy array instead of a list of 1D arrays. JP2
|
||||||
|
super box constructors now take optional box list argument.
|
||||||
Fixed bug where JPX files with more than one codestream but
|
Fixed bug where JPX files with more than one codestream but
|
||||||
advertising jp2 compatibility were not being read.
|
advertising jp2 compatibility were not being read.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1369,6 +1369,21 @@ class AssociationBox(Jp2kBox):
|
||||||
|
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
def write(self, fptr):
|
||||||
|
"""Write an association box to file.
|
||||||
|
"""
|
||||||
|
# Write the contained boxes, then come back and write the length.
|
||||||
|
orig_pos = fptr.tell()
|
||||||
|
fptr.write(struct.pack('>I', 0))
|
||||||
|
fptr.write('asoc'.encode())
|
||||||
|
for box in self.box:
|
||||||
|
box.write(fptr)
|
||||||
|
|
||||||
|
end_pos = fptr.tell()
|
||||||
|
fptr.seek(orig_pos)
|
||||||
|
fptr.write(struct.pack('>I', end_pos - orig_pos))
|
||||||
|
fptr.seek(end_pos)
|
||||||
|
|
||||||
|
|
||||||
class JP2HeaderBox(Jp2kBox):
|
class JP2HeaderBox(Jp2kBox):
|
||||||
"""Container for JP2 header box information.
|
"""Container for JP2 header box information.
|
||||||
|
|
@ -2180,15 +2195,19 @@ class NumberListBox(Jp2kBox):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
msg = Jp2kBox.__str__(self)
|
msg = Jp2kBox.__str__(self)
|
||||||
for j, association in enumerate(self.associations):
|
for j, association in enumerate(self.associations):
|
||||||
|
msg += '\n Association[{0}]: '.format(j)
|
||||||
if association == 0:
|
if association == 0:
|
||||||
msg += '\n Association[{0}]: the rendered result'.format(j)
|
msg += 'the rendered result'
|
||||||
elif (association >> 24) == 1:
|
elif (association >> 24) == 1:
|
||||||
idx = association & 0x00FFFFFF
|
idx = association & 0x00FFFFFF
|
||||||
msg += '\n Association[{0}]: Codestream {0} '.format(idx)
|
msg += 'Codestream {0}'
|
||||||
|
msg = msg.format(idx)
|
||||||
elif (association >> 24) == 2:
|
elif (association >> 24) == 2:
|
||||||
idx = association & 0x00FFFFFF
|
idx = association & 0x00FFFFFF
|
||||||
msg += '\n Association[{0}]: Compositing Layer {0}'
|
msg += 'Compositing Layer {0}'
|
||||||
msg = msg.format(idx)
|
msg = msg.format(j, idx)
|
||||||
|
else:
|
||||||
|
msg += 'unrecognized'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
@ -2219,6 +2238,16 @@ class NumberListBox(Jp2kBox):
|
||||||
box = NumberListBox(lst, length=length, offset=offset)
|
box = NumberListBox(lst, length=length, offset=offset)
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
def write(self, fptr):
|
||||||
|
"""Write a NumberList box to file.
|
||||||
|
"""
|
||||||
|
fptr.write(struct.pack('>I', len(self.associations) * 4 + 8))
|
||||||
|
fptr.write(self.box_id.encode())
|
||||||
|
|
||||||
|
fmt = '>' + 'I' * len(self.associations)
|
||||||
|
write_buffer = struct.pack(fmt, *self.associations)
|
||||||
|
fptr.write(write_buffer)
|
||||||
|
|
||||||
|
|
||||||
class XMLBox(Jp2kBox):
|
class XMLBox(Jp2kBox):
|
||||||
"""Container for XML box information.
|
"""Container for XML box information.
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ from .lib import openjp2 as opj2
|
||||||
from . import version
|
from . import version
|
||||||
from .lib import c as libc
|
from .lib import c as libc
|
||||||
|
|
||||||
|
JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ',
|
||||||
|
'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
|
||||||
|
'uuid']
|
||||||
|
JPX_IDS = ['asoc', 'nlst']
|
||||||
|
|
||||||
class Jp2k(Jp2kBox):
|
class Jp2k(Jp2kBox):
|
||||||
"""JPEG 2000 file.
|
"""JPEG 2000 file.
|
||||||
|
|
@ -610,7 +614,7 @@ class Jp2k(Jp2kBox):
|
||||||
jp2c = [box for box in self.box
|
jp2c = [box for box in self.box
|
||||||
if box.box_id == 'jp2c']
|
if box.box_id == 'jp2c']
|
||||||
jp2c = jp2c[0]
|
jp2c = jp2c[0]
|
||||||
ofile.write(struct.pack('>I', jp2c.length + 8))
|
ofile.write(struct.pack('>I', jp2c.length))
|
||||||
ofile.write('jp2c'.encode())
|
ofile.write('jp2c'.encode())
|
||||||
with open(self.filename, 'rb') as ifile:
|
with open(self.filename, 'rb') as ifile:
|
||||||
# Seek 8 bytes past the L, T fields to get to the
|
# Seek 8 bytes past the L, T fields to get to the
|
||||||
|
|
@ -1171,6 +1175,61 @@ def _validate_jp2_box_sequence(boxes):
|
||||||
msg += "channel definition box."
|
msg += "channel definition box."
|
||||||
raise IOError(msg)
|
raise IOError(msg)
|
||||||
|
|
||||||
|
# The compatibility list must contain at a minimum 'jp2 '.
|
||||||
|
if 'jp2 ' not in boxes[1].compatibility_list:
|
||||||
|
msg = "The ftyp box must contain 'jp2 ' in the compatibility list."
|
||||||
|
raise IOError(msg)
|
||||||
|
|
||||||
|
# JPX checks.
|
||||||
|
_asoc_check(boxes)
|
||||||
|
_jpx_brand(boxes, boxes[1].brand)
|
||||||
|
_jpx_compatibility(boxes, boxes[1].compatibility_list)
|
||||||
|
|
||||||
|
def _jpx_brand(boxes, brand):
|
||||||
|
"""
|
||||||
|
If there is a JPX box then the brand must be 'jpx '.
|
||||||
|
"""
|
||||||
|
for box in boxes:
|
||||||
|
if box.box_id in JPX_IDS:
|
||||||
|
if brand != 'jpx ':
|
||||||
|
msg = "A JPX box requires that the file type box brand be "
|
||||||
|
msg += "'jpx '."
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
if hasattr(box, 'box') != 0:
|
||||||
|
# Same set of checks on any child boxes.
|
||||||
|
_jpx_brand(box.box, brand)
|
||||||
|
|
||||||
|
def _jpx_compatibility(boxes, compatibility_list):
|
||||||
|
"""
|
||||||
|
If there is a JPX box then the compatibility list must also contain 'jpx '.
|
||||||
|
"""
|
||||||
|
for box in boxes:
|
||||||
|
if box.box_id in JPX_IDS:
|
||||||
|
if 'jpx ' not in compatibility_list:
|
||||||
|
msg = "A JPX box requires that 'jpx ' be present in the "
|
||||||
|
msg += "ftype compatibility list."
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
if hasattr(box, 'box') != 0:
|
||||||
|
# Same set of checks on any child boxes.
|
||||||
|
_jpx_compatibility(box.box, compatibility_list)
|
||||||
|
|
||||||
|
|
||||||
|
def _asoc_check(boxes):
|
||||||
|
"""
|
||||||
|
Association boxes can only contain number list boxes and xml boxes, as far
|
||||||
|
as we know.
|
||||||
|
"""
|
||||||
|
for box in boxes:
|
||||||
|
if box.box_id == 'asoc':
|
||||||
|
if box.box[0].box_id != 'nlst' or box.box[1].box_id != 'xml ':
|
||||||
|
msg = "An Association box can only contain a NumberList box "
|
||||||
|
msg += "followed by an XML box."
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
if hasattr(box, 'box') != 0:
|
||||||
|
# Same set of checks on any child boxes.
|
||||||
|
_asoc_check(box.box)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def extract_image_cube(image):
|
def extract_image_cube(image):
|
||||||
"""Extract 3D image from openjpeg data structure.
|
"""Extract 3D image from openjpeg data structure.
|
||||||
|
|
|
||||||
|
|
@ -553,7 +553,7 @@ class TestWrap(unittest.TestCase):
|
||||||
self.verify_wrapped_raw(tfile.name)
|
self.verify_wrapped_raw(tfile.name)
|
||||||
|
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||||
def test_palette(self):
|
def test_jpx_to_jp2(self):
|
||||||
"""basic test for rewrapping a jpx file"""
|
"""basic test for rewrapping a jpx file"""
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
# This file has a rreq mask length that we do not recognize.
|
# This file has a rreq mask length that we do not recognize.
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,100 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
import xml.etree.cElementTree as ET
|
||||||
|
|
||||||
import glymur
|
import glymur
|
||||||
from glymur import Jp2k
|
from glymur import Jp2k
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||||
|
class TestJPXWrap(unittest.TestCase):
|
||||||
|
"""Test suite for wrapping JPX files."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.jp2file = glymur.data.nemo()
|
||||||
|
|
||||||
|
raw_xml = b"""<?xml version="1.0"?>
|
||||||
|
<data>
|
||||||
|
<country name="Liechtenstein">
|
||||||
|
<rank>1</rank>
|
||||||
|
<year>2008</year>
|
||||||
|
<gdppc>141100</gdppc>
|
||||||
|
<neighbor name="Austria" direction="E"/>
|
||||||
|
<neighbor name="Switzerland" direction="W"/>
|
||||||
|
</country>
|
||||||
|
</data>"""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile:
|
||||||
|
tfile.write(raw_xml)
|
||||||
|
tfile.flush()
|
||||||
|
self.xmlfile = tfile.name
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.unlink(self.xmlfile)
|
||||||
|
|
||||||
|
def test_association_box(self):
|
||||||
|
"""Wrap JP2 to JPX with asoc(nlst, xml)"""
|
||||||
|
jp2 = Jp2k(self.jp2file)
|
||||||
|
boxes = [jp2.box[idx] for idx in [0, 1, 2, 5]]
|
||||||
|
|
||||||
|
# The ftyp box must be modified to jpx with jp2 compatibility.
|
||||||
|
boxes[1].brand = 'jpx '
|
||||||
|
boxes[1].compatibility_list = ['jp2 ', 'jpx ']
|
||||||
|
|
||||||
|
numbers = (0, 1)
|
||||||
|
nlst = glymur.jp2box.NumberListBox(numbers)
|
||||||
|
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
|
||||||
|
xmlb = glymur.jp2box.XMLBox(xml=the_xml)
|
||||||
|
asoc = glymur.jp2box.AssociationBox([nlst, xmlb])
|
||||||
|
boxes.append(asoc)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||||
|
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
|
self.assertEqual(jpx.box[1].compatibility_list, ['jp2 ', 'jpx '])
|
||||||
|
self.assertEqual(jpx.box[-1].box_id, 'asoc')
|
||||||
|
self.assertEqual(jpx.box[-1].box[0].box_id, 'nlst')
|
||||||
|
self.assertEqual(jpx.box[-1].box[1].box_id, 'xml ')
|
||||||
|
self.assertEqual(jpx.box[-1].box[0].associations, numbers)
|
||||||
|
self.assertEqual(ET.tostring(jpx.box[-1].box[1].xml.getroot()),
|
||||||
|
b'<data>0</data>')
|
||||||
|
|
||||||
|
def test_jp2_to_jpx_sans_jp2_compatibility(self):
|
||||||
|
"""jp2 wrapped to jpx not including jp2 compatibility is wrong."""
|
||||||
|
jp2 = Jp2k(self.jp2file)
|
||||||
|
boxes = [jp2.box[idx] for idx in [0, 1, 2, 5]]
|
||||||
|
boxes[1].compatibility_list.append('jp2 ')
|
||||||
|
numbers = [0, 1]
|
||||||
|
nlst = glymur.jp2box.NumberListBox(numbers)
|
||||||
|
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
|
||||||
|
xmlb = glymur.jp2box.XMLBox(xml=the_xml)
|
||||||
|
asoc = glymur.jp2box.AssociationBox([nlst, xmlb])
|
||||||
|
boxes.append(asoc)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
|
def test_jp2_to_jpx_sans_jpx_brand(self):
|
||||||
|
"""Verify error when jp2 wrapped to jpx does not include jpx brand."""
|
||||||
|
jp2 = Jp2k(self.jp2file)
|
||||||
|
boxes = [jp2.box[idx] for idx in [0, 1, 2, 5]]
|
||||||
|
boxes[1].brand = 'jpx '
|
||||||
|
numbers = [0, 1]
|
||||||
|
nlst = glymur.jp2box.NumberListBox(numbers)
|
||||||
|
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
|
||||||
|
xmlb = glymur.jp2box.XMLBox(xml=the_xml)
|
||||||
|
asoc = glymur.jp2box.AssociationBox([nlst, xmlb])
|
||||||
|
boxes.append(asoc)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Warning assert on 2.x.")
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Warning assert on 2.x.")
|
||||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||||
class TestJPXOther(unittest.TestCase):
|
class TestJPX(unittest.TestCase):
|
||||||
"""Test suite for other JPX boxes."""
|
"""Test suite for other JPX boxes."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue