From 14743d6633d02c60384816981db6ced0828a72e3 Mon Sep 17 00:00:00 2001 From: jevans Date: Sat, 8 Feb 2014 14:56:50 -0500 Subject: [PATCH] Added write support for dtbl boxes. --- CHANGES.txt | 16 ++++---- glymur/jp2box.py | 21 ++++++++++ glymur/jp2k.py | 52 +++++++++++++++++++++++ glymur/test/test_jp2box_jpx.py | 75 +++++++++++++++++++++------------- 4 files changed, 128 insertions(+), 36 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 25dc20c..bc823bd 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,12 +1,12 @@ -Feb 03, 2014 - Removed support for Python 2.6. Added write support for +Feb 08, 2014 - Removed support for Python 2.6. Added write support for JP2 DataEntryURL, 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. + Association, NumberList and DataReference 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. Jan 28, 2014 - v0.5.10 Fixed bad warning when reader requirements box mask length is unsupported. diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 47cfa30..d535af9 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -863,6 +863,27 @@ class DataReferenceBox(Jp2kBox): self.length = length self.offset = offset + def write(self, fptr): + """Write a Data Reference box to file. + """ + + # Very similar to the say a superbox is written. + orig_pos = fptr.tell() + fptr.write(struct.pack('>I', 0)) + fptr.write(self.box_id.encode()) + + # Write the number of data entry url boxes. + write_buffer = struct.pack('>H', len(self.DR)) + fptr.write(write_buffer) + + for box in self.DR: + box.write(fptr) + + end_pos = fptr.tell() + fptr.seek(orig_pos) + fptr.write(struct.pack('>I', end_pos - orig_pos)) + fptr.seek(end_pos) + def __str__(self): msg = Jp2kBox.__str__(self) for box in self.DR: diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 732a79f..f9e1030 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -16,6 +16,7 @@ if sys.hexversion >= 0x03030000: else: from contextlib2 import ExitStack +from collections import Counter import ctypes import math import os @@ -1190,6 +1191,57 @@ def _validate_jp2_box_sequence(boxes): _asoc_check(boxes) _jpx_brand(boxes, boxes[1].brand) _jpx_compatibility(boxes, boxes[1].compatibility_list) + _check_for_singletons(boxes) + _check_top_level(boxes) + +def _collect_box_count(boxes): + """Count the occurences of each box type.""" + count = Counter([box.box_id for box in boxes]) + + # Add the counts in the superboxes. + for box in boxes: + if hasattr(box, 'box'): + count.update(_collect_box_count(box.box)) + + return count + +TOP_LEVEL_ONLY_BOXES = set(['dtbl']) + +def _check_superbox_for_top_levels(boxes): + """Several boxes can only occur at the top level.""" + # We are only looking at the boxes contained in a superbox, so if any of + # the blacklisted boxes show up here, it's an error. + box_ids = set([box.box_id for box in boxes]) + intersection = box_ids.intersection(TOP_LEVEL_ONLY_BOXES) + if len(intersection) > 0: + msg = "A '{0}' box cannot be nested in a superbox." + raise IOError(msg.format(list(intersection)[0])) + + # Recursively check any contained superboxes. + for box in boxes: + if hasattr(box, 'box'): + _check_superbox_for_top_levels(box.box) + +def _check_top_level(boxes): + """Several boxes can only occur at the top level.""" + # Add the counts in the superboxes. + for box in boxes: + if hasattr(box, 'box'): + _check_superbox_for_top_levels(box.box) + + count = _collect_box_count(boxes) + # Which boxes occur more than once? + multiples = [box_id for box_id, bcount in count.items() if bcount > 1] + if 'dtbl' in multiples: + raise IOError('There can only be one dtbl box in a file.') + +def _check_for_singletons(boxes): + """Several boxes can only occur once.""" + count = _collect_box_count(boxes) + # Which boxes occur more than once? + multiples = [box_id for box_id, bcount in count.items() if bcount > 1] + if 'dtbl' in multiples: + raise IOError('There can only be one dtbl box in a file.') def _jpx_brand(boxes, brand): """ diff --git a/glymur/test/test_jp2box_jpx.py b/glymur/test/test_jp2box_jpx.py index dbbabb7..39151ba 100644 --- a/glymur/test/test_jp2box_jpx.py +++ b/glymur/test/test_jp2box_jpx.py @@ -67,6 +67,41 @@ class TestJPXWrap(unittest.TestCase): self.assertEqual(ET.tostring(jpx.box[-1].box[1].xml.getroot()), b'0') + def test_only_one_data_reference(self): + """Data reference boxes cannot be inside a superbox .""" + jp2 = Jp2k(self.jp2file) + boxes = [jp2.box[idx] for idx in [0, 1, 2, 5]] + + flag = 0 + version = (0, 0, 0) + url = 'file:////usr/local/bin' + deurl = glymur.jp2box.DataEntryURLBox(flag, version, url) + dref = glymur.jp2box.DataReferenceBox([deurl]) + boxes.append(dref) + boxes.append(dref) + + with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile: + with self.assertRaises(IOError): + jpx = jp2.wrap(tfile.name, boxes=boxes) + + def test_data_reference_not_at_top_level(self): + """Data reference boxes cannot be inside a superbox .""" + jp2 = Jp2k(self.jp2file) + boxes = [jp2.box[idx] for idx in [0, 1, 2, 5]] + + flag = 0 + version = (0, 0, 0) + url = 'file:////usr/local/bin' + deurl = glymur.jp2box.DataEntryURLBox(flag, version, url) + dref = glymur.jp2box.DataReferenceBox([deurl]) + + # Put it inside the jp2 header box. + boxes[2].box.append(dref) + + with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile: + with self.assertRaises(IOError): + jpx = jp2.wrap(tfile.name, boxes=boxes) + def test_jp2_to_jpx_sans_jp2_compatibility(self): """jp2 wrapped to jpx not including jp2 compatibility is wrong.""" jp2 = Jp2k(self.jp2file) @@ -133,46 +168,30 @@ class TestJPX(unittest.TestCase): def test_dtbl(self): """Verify that we can interpret Data Reference boxes.""" # Copy the existing JPX file, add a data reference box onto the end. + flag = 0 + version = (0, 0, 0) + url1 = 'file:////usr/local/bin' + url2 = 'http://glymur.readthedocs.org' + chr(0) * 3 with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile: with open(self.jpxfile, 'rb') as ifile: tfile.write(ifile.read()) - # 8 + 2 + 20 + 36 - boxlen = 66 - write_buffer = struct.pack('>I4s', boxlen, b'dtbl') - tfile.write(write_buffer) - - # Just two boxes. - write_buffer = struct.pack('>H', 2) - tfile.write(write_buffer) - - # First data entry url box. - # This one will have a URL with 3 null chars at the end. - # They should be stripped off. - write_buffer = struct.pack('>I4s', 36, b'url ') - tfile.write(write_buffer) - url1 = 'file:////usr/local/bin' - write_buffer = struct.pack('>BBBB24s', 0, 0, 0, 0, - (url1 + chr(0) * 3).encode()) - tfile.write(write_buffer) - - # 2nd data entry url box. - write_buffer = struct.pack('>I4s', 20, b'url ') - tfile.write(write_buffer) - url2 = 'file:///' - write_buffer = struct.pack('>BBBB8s', 0, 0, 0, 0, url2.encode()) - tfile.write(write_buffer) + deurl1 = glymur.jp2box.DataEntryURLBox(flag, version, url1) + deurl2 = glymur.jp2box.DataEntryURLBox(flag, version, url2) + dref = glymur.jp2box.DataReferenceBox([deurl1, deurl2]) + dref.write(tfile) tfile.flush() - with self.assertWarns(UserWarning): + with warnings.catch_warnings(): + # This file has a rreq mask length that we do not recognize. + warnings.simplefilter("ignore") jpx = Jp2k(tfile.name) self.assertEqual(jpx.box[-1].box_id, 'dtbl') self.assertEqual(len(jpx.box[-1].DR), 2) self.assertEqual(jpx.box[-1].DR[0].url, url1) - self.assertEqual(jpx.box[-1].DR[1].url, url2) - + self.assertEqual(jpx.box[-1].DR[1].url, url2.rstrip('\0')) def test_ftbl(self): """Verify that we can interpret Fragment Table boxes."""