diff --git a/CHANGES.txt b/CHANGES.txt index dd11c27..59ecd8f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,9 @@ Nov 11, 2013 - Palette box now a 2D numpy array instead of a list of 1D arrays. Super box constructors now take optional box list argument. +Jan 28, 2014 - v0.5.10 Fixed bad warning when reader requirements box mask + length is unsupported. + Oct 29, 2013 - v0.5.9 Fixed bad library load on linux as a result of 0.5.8 Oct 29, 2013 - v0.5.8 Fixed unnecessary warnings when default locations for diff --git a/docs/source/conf.py b/docs/source/conf.py index b14af27..4b0eb14 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -78,7 +78,7 @@ copyright = u'2013, John Evans' # The short X.Y version. version = '0.5' # The full version, including alpha/beta/rc tags. -release = '0.5.9' +release = '0.5.10' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/roadmap.rst b/docs/source/roadmap.rst index f9216a3..1e336f1 100644 --- a/docs/source/roadmap.rst +++ b/docs/source/roadmap.rst @@ -9,3 +9,4 @@ Here's an incomplete list of what I'd like to focus on in the future. * investigate adding write support for UUID/XMP boxes (potentially a big project) * investigate JPIP (likely to be an even bigger project) +Support for Python 2.6 will be dropped with the 0.6.0 release. diff --git a/glymur/data/12-v6.4.jpx b/glymur/data/12-v6.4.jpx new file mode 100644 index 0000000..63f5f13 Binary files /dev/null and b/glymur/data/12-v6.4.jpx differ diff --git a/glymur/data/__init__.py b/glymur/data/__init__.py index c95b144..2bcd7f8 100644 --- a/glymur/data/__init__.py +++ b/glymur/data/__init__.py @@ -31,3 +31,16 @@ def goodstuff(): """ filename = pkg_resources.resource_filename(__name__, "goodstuff.j2k") return filename + + +def jpxfile(): + """Shortcut for specifying path to 12-v6.4.jpx. + + Returns + ------- + file : str + Platform-independent path to 12-v6.4.jpx + """ + filename = pkg_resources.resource_filename(__name__, "12-v6.4.jpx") + return filename + diff --git a/glymur/jp2box.py b/glymur/jp2box.py index a66ff68..6e62f88 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -915,6 +915,53 @@ class FileTypeBox(Jp2kBox): return box +class FreeBox(Jp2kBox): + """Container for JPX free box information. + + Attributes + ---------- + box_id : str + 4-character identifier for the box. + length : int + length of the box in bytes. + offset : int + offset of the box from the start of the file. + longname : str + more verbose description of the box. + """ + def __init__(self, length=0, offset=-1): + Jp2kBox.__init__(self, box_id='free', longname='Free') + self.length = length + self.offset = offset + + def __repr__(self): + msg = "glymur.jp2box.FreeBox()" + return msg + + def __str__(self): + msg = Jp2kBox.__str__(self) + return msg + + @staticmethod + def parse(fptr, offset, length): + """Parse JPX free box. + + Parameters + ---------- + f : file + Open file object. + offset : int + Start position of box in bytes. + length : int + Length of the box in bytes. + + Returns + ------- + FreeBox instance + """ + return FreeBox(length=length, offset=offset) + + class ImageHeaderBox(Jp2kBox): """Container for JPEG 2000 image header box information. @@ -1520,13 +1567,24 @@ class ReaderRequirementsBox(Jp2kBox): # Decodes Completely Mask read_buffer = fptr.read(2 * mask_length) + fuam = dcm = standard_flag = standard_mask = [] + vendor_feature = vendor_mask = [] + # The mask length tells us the format string to use when unpacking # from the buffer read from file. - mask_format = {1: 'B', 2: 'H', 4: 'I'}[mask_length] - fuam, dcm = struct.unpack('>' + mask_format * 2, read_buffer) + try: + mask_format = {1: 'B', 2: 'H', 4: 'I', 8: 'Q'}[mask_length] + fuam, dcm = struct.unpack('>' + mask_format * 2, read_buffer) + standard_flag, standard_mask = _parse_standard_flag(fptr, + mask_length) + vendor_feature, vendor_mask = _parse_vendor_features(fptr, + mask_length) - standard_flag, standard_mask = _parse_standard_flag(fptr, mask_length) - vendor_feature, vendor_mask = _parse_vendor_features(fptr, mask_length) + except KeyError: + msg = 'The ReaderRequirements box (rreq) has a mask length of {0} ' + msg += 'bytes, but only values of 1, 2, 4, or 8 are supported. ' + msg += 'The box contents will not be interpreted.' + warnings.warn(msg.format(mask_length), UserWarning) box = ReaderRequirementsBox(fuam, dcm, standard_flag, standard_mask, vendor_feature, vendor_mask, @@ -2804,6 +2862,7 @@ _BOX_WITH_ID = { 'jpch': CodestreamHeaderBox, 'jplh': CompositingLayerHeaderBox, 'jp2c': ContiguousCodestreamBox, + 'free': FreeBox, 'jp2h': JP2HeaderBox, 'lbl ': LabelBox, 'pclr': PaletteBox, diff --git a/glymur/test/test_jp2box_jpx.py b/glymur/test/test_jp2box_jpx.py new file mode 100644 index 0000000..57f5e6f --- /dev/null +++ b/glymur/test/test_jp2box_jpx.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" +Test suite specifically targeting JPX box layout. +""" + +import os +import struct +import sys +import tempfile +import warnings +import xml.etree.cElementTree as ET + +if sys.hexversion < 0x02070000: + import unittest2 as unittest +else: + import unittest + +import glymur +from glymur import Jp2k +from glymur.jp2box import ReaderRequirementsBox + + +@unittest.skipIf(sys.hexversion < 0x03000000, "Warning assert on 2.x.") +@unittest.skipIf(os.name == "nt", "Temporary file issue on window.") +class TestReaderRequirements(unittest.TestCase): + """Test suite for XML boxes.""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + pass + + def tearDown(self): + pass + + def test_mask_length_is_3(self): + """The standard says that the mask length should be 1, 2, 4, or 8.""" + # Rewrite nemo to include this kind of rreq box. + with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile: + with open(self.jp2file, 'rb') as nemof: + # Read the jP and ftyp boxes as-is. + write_buffer = nemof.read(32) + tfile.write(write_buffer) + + # Fake a rreq box with ML = 3. + write_buffer = struct.pack('>I4sB', 74, b'rreq', 3) + tfile.write(write_buffer) + + # pad the rest with zeros + write_buffer = struct.pack('>65s', b'\x00' * 65) + tfile.write(write_buffer) + + # Write the rest of nemo. + tfile.write(nemof.read()) + tfile.flush() + + with self.assertWarns(UserWarning): + j = Jp2k(tfile.name) + self.assertEqual(j.box[2].box_id, 'rreq') + self.assertEqual(type(j.box[2]), + glymur.jp2box.ReaderRequirementsBox) + + +@unittest.skipIf(sys.hexversion < 0x03000000, "Warning assert on 2.x.") +@unittest.skipIf(os.name == "nt", "Temporary file issue on window.") +class TestJPXOther(unittest.TestCase): + """Test suite for other JPX boxes.""" + + def setUp(self): + self.jpxfile = glymur.data.jpxfile() + pass + + def tearDown(self): + pass + + def test_free_box(self): + """Verify that we can handle a free box.""" + with warnings.catch_warnings(): + # This file has a rreq mask length that we do not recognize. + warnings.simplefilter("ignore") + j = Jp2k(self.jpxfile) + self.assertEqual(j.box[16].box[0].box_id, 'free') + self.assertEqual(type(j.box[16].box[0]), glymur.jp2box.FreeBox) + diff --git a/glymur/version.py b/glymur/version.py index 6116920..d3d87db 100644 --- a/glymur/version.py +++ b/glymur/version.py @@ -15,7 +15,7 @@ from .lib import openjp2 as opj2 # Do not change the format of this next line! Doing so risks breaking # setup.py -version = "0.5.9" +version = "0.5.10" _sv = LooseVersion(version) version_tuple = _sv.version diff --git a/setup.py b/setup.py index 89e69f7..2228943 100644 --- a/setup.py +++ b/setup.py @@ -7,11 +7,11 @@ kwargs = {'name': 'Glymur', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans', - 'author_email': 'john.g.evans.ne at gmail dot com', + 'author_email': 'john.g.evans.ne@gmail.com', 'url': 'https://github.com/quintusdias/glymur', 'packages': ['glymur', 'glymur.data', 'glymur.test', 'glymur.lib', 'glymur.lib.test'], - 'package_data': {'glymur': ['data/*.jp2', 'data/*.j2k']}, + 'package_data': {'glymur': ['data/*.jp2', 'data/*.j2k', 'data/*.jpx']}, 'scripts': ['bin/jp2dump'], 'license': 'MIT', 'test_suite': 'glymur.test'}