From cd606e1f9df52dfe8207dac6fb15b7a3f5c7ae80 Mon Sep 17 00:00:00 2001 From: jevans Date: Tue, 18 Feb 2014 20:01:28 -0500 Subject: [PATCH] Starting write support for ftbl and flst boxes. #175 --- glymur/jp2box.py | 50 +++++++++++++++++++++-- glymur/test/test_jp2box_jpx.py | 73 ++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 043a1be..4ce47ff 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -148,7 +148,8 @@ class Jp2kBox(object): box = UnknownBox(box_id, offset=start, length=num_bytes, longname='Unknown') - if fptr.tell() != start + 8: + cpos = fptr.tell() + if not ((cpos == start + 8) or (cpos == start + 16)): # If the file pointer has advanced, then the KeyError # ocurred during the parsing of the box. pass @@ -1122,6 +1123,18 @@ class FragmentListBox(Jp2kBox): self.length = length self.offset = offset + def _validate(self): + """Validate internal correctness.""" + if (((len(self.fragment_offset) != len(self.fragment_length)) or + (len(self.fragment_length) != len(self.data_reference)))): + msg = "The lengths of the fragment offsets, fragment lengths, and " + msg += "data reference items must be the same." + raise IOError(msg) + if any([x <= 0 for x in self.fragment_offset]): + raise IOError("Fragment offsets must all be positive.") + if any([x <= 0 for x in self.fragment_length]): + raise IOError("Fragment lengths must all be positive.") + def __repr__(self): msg = "glymur.jp2box.FragmentListBox()" return msg @@ -1141,6 +1154,22 @@ class FragmentListBox(Jp2kBox): return msg + def write(self, fptr): + """Write a fragment list box to file. + """ + self._validate() + num_items = len(self.fragment_offset) + length = 8 + 2 + num_items * 14 + fptr.write(struct.pack('>I', length)) + fptr.write(self.box_id.encode()) + fptr.write(struct.pack('>H', num_items)) + for j in range(num_items): + write_buffer = struct.pack('>QIH', + self.fragment_offset[j], + self.fragment_length[j], + self.data_reference[j]) + fptr.write(write_buffer) + @staticmethod def parse(fptr, offset, length): """Parse JPX free box. @@ -1184,10 +1213,11 @@ class FragmentTableBox(Jp2kBox): longname : str more verbose description of the box. """ - def __init__(self, length=0, offset=-1): + def __init__(self, box=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='ftbl', longname='Fragment Table') self.length = length self.offset = offset + self.box = box if box is not None else [] def __repr__(self): msg = "glymur.jp2box.FragmentTableBox()" @@ -1212,7 +1242,7 @@ class FragmentTableBox(Jp2kBox): Returns ------- - FreeBox instance + FragmentTableBox instance """ box = FragmentTableBox(length=length, offset=offset) @@ -1222,6 +1252,20 @@ class FragmentTableBox(Jp2kBox): return box + def _validate(self): + """Self-validate the box before writing.""" + box_ids = [box.box_id for box in self.box] + if len(box_ids) != 1 or box_ids[0] != 'flst': + msg = "Fragment table boxes must have a single fragment list " + msg += "box as a child box." + raise IOError(msg) + + def write(self, fptr): + """Write a fragment table box to file. + """ + self._validate() + self._write_superbox(fptr) + class FreeBox(Jp2kBox): diff --git a/glymur/test/test_jp2box_jpx.py b/glymur/test/test_jp2box_jpx.py index 6624291..99250c5 100644 --- a/glymur/test/test_jp2box_jpx.py +++ b/glymur/test/test_jp2box_jpx.py @@ -41,6 +41,34 @@ class TestJPXWrap(unittest.TestCase): def tearDown(self): os.unlink(self.xmlfile) + def test_ftbl(self): + """Write a fragment table box.""" + # Add a negative test where offset < 0 + # Add a negative test where length < 0 + # Add a negative test where ref > 0 but no data reference box. + # Add a negative test where more than one flst + # Add negative test where ftbl contained in a superbox. + jp2 = Jp2k(self.jp2file) + boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]] + + # The ftyp box must be modified to jpx. + boxes[1].brand = 'jpx ' + boxes[1].compatibility_list = ['jp2 ', 'jpxb'] + + offset = [89] + length = [1132288] + reference = [0] + flst = glymur.jp2box.FragmentListBox(offset, length, reference) + ftbl = glymur.jp2box.FragmentTableBox(box=[flst]) + boxes.append(ftbl) + + with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile: + jpx = jp2.wrap(tfile.name, boxes=boxes) + + self.assertEqual(jpx.box[1].compatibility_list, ['jp2 ', 'jpxb']) + self.assertEqual(jpx.box[-1].box_id, 'ftbl') + self.assertEqual(jpx.box[-1].box[0].box_id, 'flst') + def test_jpxb_compatibility(self): """Wrap JP2 to JPX, state jpxb compatibility""" jp2 = Jp2k(self.jp2file) @@ -194,6 +222,51 @@ class TestJPX(unittest.TestCase): def tearDown(self): pass + def test_flst_lens_not_the_same(self): + """A fragment list box items must be the same length.""" + offset = [89] + length = [1132288] + reference = [0, 0] + flst = glymur.jp2box.FragmentListBox(offset, length, reference) + with self.assertRaises(IOError): + with tempfile.TemporaryFile() as tfile: + flst.write(tfile) + + def test_flst_offsets_not_positive(self): + """A fragment list box offsets must be positive.""" + offset = [0] + length = [1132288] + reference = [0] + flst = glymur.jp2box.FragmentListBox(offset, length, reference) + with self.assertRaises(IOError): + with tempfile.TemporaryFile() as tfile: + flst.write(tfile) + + def test_flst_lengths_not_positive(self): + """A fragment list box lengths must be positive.""" + offset = [89] + length = [0] + reference = [0] + flst = glymur.jp2box.FragmentListBox(offset, length, reference) + with self.assertRaises(IOError): + with tempfile.TemporaryFile() as tfile: + flst.write(tfile) + + def test_ftbl_boxes_empty(self): + """A fragment table box must have at least one child box.""" + ftbl = glymur.jp2box.FragmentTableBox() + with self.assertRaises(IOError): + with tempfile.TemporaryFile() as tfile: + ftbl.write(tfile) + + def test_ftbl_child_not_flst(self): + """A fragment table box can only contain a fragment list.""" + free = glymur.jp2box.FreeBox() + ftbl = glymur.jp2box.FragmentTableBox(box=[free]) + with self.assertRaises(IOError): + with tempfile.TemporaryFile() as tfile: + ftbl.write(tfile) + def test_jpx_rreq_mask_length_3(self): """There are some JPX files with rreq mask length of 3.""" jpx = Jp2k(self.jpxfile)