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
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue