Merge branch 'issue152' into devel

This commit is contained in:
jevans 2014-02-05 20:59:07 -05:00
commit a54ce4bad4
5 changed files with 186 additions and 11 deletions

View file

@ -1,8 +1,9 @@
Feb 03, 2014 - Removed support for Python 2.6. Added write support for
Palette and Component Mapping boxes. Added read support for JPX free,
number list, data reference, fragment table, and fragment list boxes.
Palette box now a 2D numpy array instead of a list of 1D arrays.
JP2 super box constructors now take optional box list argument.
JP2 Palette and Component Mapping boxes, JPX Association and
NumberList boxes. Added read support for JPX free, number list,
data reference, fragment table, and fragment list boxes. Palette
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
advertising jp2 compatibility were not being read.

View file

@ -1369,6 +1369,21 @@ class AssociationBox(Jp2kBox):
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):
"""Container for JP2 header box information.
@ -2180,15 +2195,19 @@ class NumberListBox(Jp2kBox):
def __str__(self):
msg = Jp2kBox.__str__(self)
for j, association in enumerate(self.associations):
msg += '\n Association[{0}]: '.format(j)
if association == 0:
msg += '\n Association[{0}]: the rendered result'.format(j)
msg += 'the rendered result'
elif (association >> 24) == 1:
idx = association & 0x00FFFFFF
msg += '\n Association[{0}]: Codestream {0} '.format(idx)
msg += 'Codestream {0}'
msg = msg.format(idx)
elif (association >> 24) == 2:
idx = association & 0x00FFFFFF
msg += '\n Association[{0}]: Compositing Layer {0}'
msg = msg.format(idx)
msg += 'Compositing Layer {0}'
msg = msg.format(j, idx)
else:
msg += 'unrecognized'
return msg
def __repr__(self):
@ -2219,6 +2238,16 @@ class NumberListBox(Jp2kBox):
box = NumberListBox(lst, length=length, offset=offset)
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):
"""Container for XML box information.

View file

@ -37,6 +37,10 @@ from .lib import openjp2 as opj2
from . import version
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):
"""JPEG 2000 file.
@ -610,7 +614,7 @@ class Jp2k(Jp2kBox):
jp2c = [box for box in self.box
if box.box_id == 'jp2c']
jp2c = jp2c[0]
ofile.write(struct.pack('>I', jp2c.length + 8))
ofile.write(struct.pack('>I', jp2c.length))
ofile.write('jp2c'.encode())
with open(self.filename, 'rb') as ifile:
# Seek 8 bytes past the L, T fields to get to the
@ -1170,6 +1174,61 @@ def _validate_jp2_box_sequence(boxes):
msg = "All color channels must be defined in the "
msg += "channel definition box."
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):

View file

@ -553,7 +553,7 @@ class TestWrap(unittest.TestCase):
self.verify_wrapped_raw(tfile.name)
@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"""
with warnings.catch_warnings():
# This file has a rreq mask length that we do not recognize.

View file

@ -9,14 +9,100 @@ import sys
import tempfile
import unittest
import warnings
import xml.etree.cElementTree as ET
import glymur
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(os.name == "nt", "Temporary file issue on window.")
class TestJPXOther(unittest.TestCase):
class TestJPX(unittest.TestCase):
"""Test suite for other JPX boxes."""
def setUp(self):