diff --git a/docs/source/whatsnew/0.8.rst b/docs/source/whatsnew/0.8.rst index f36b593..5c87f5a 100644 --- a/docs/source/whatsnew/0.8.rst +++ b/docs/source/whatsnew/0.8.rst @@ -6,5 +6,7 @@ Changes in 0.8.0 ================= * Simplified writing images by moving data and options into the - constructor. This is backwards-incompatible with 0.7.x. + constructor. + * The main_header attribute of the ContiguousCodestream class is now called + codestream. * Deprecated :py:meth:`read` method in favor of array-style slicing. diff --git a/glymur/command_line.py b/glymur/command_line.py index ff442f3..3d1d57e 100644 --- a/glymur/command_line.py +++ b/glymur/command_line.py @@ -5,7 +5,7 @@ import argparse import os import warnings -from . import Jp2k, set_printoptions, lib +from . import Jp2k, set_printoptions, set_parseoptions, lib def main(): @@ -13,23 +13,25 @@ def main(): Entry point for console script jp2dump. """ - description = 'Print JPEG2000 metadata.' - parser = argparse.ArgumentParser(description=description) + kwargs = {'description': 'Print JPEG2000 metadata.', + 'formatter_class': argparse.ArgumentDefaultsHelpFormatter} + parser = argparse.ArgumentParser(**kwargs) parser.add_argument('-x', '--noxml', - help='Suppress XML.', + help='suppress XML', action='store_true') parser.add_argument('-s', '--short', - help='Only print box id, offset, and length.', + help='only print box id, offset, and length', action='store_true') - chelp = 'Level of codestream information. 0 suppressed all details, ' - chelp += '1 prints headers, 2 prints the full codestream' + chelp = 'Level of codestream information. 0 suppresses all details, ' + chelp += '1 prints the main header, 2 prints the full codestream.' parser.add_argument('-c', '--codestream', help=chelp, + metavar='LEVEL', nargs=1, type=int, - default=[0]) + default=[1]) parser.add_argument('filename') @@ -45,11 +47,8 @@ def main(): if codestream_level == 0: set_printoptions(codestream=False) - print_full_codestream = False - elif codestream_level == 1: - print_full_codestream = False - else: - print_full_codestream = True + elif codestream_level == 2: + set_parseoptions(full_codestream=True) filename = args.filename @@ -58,11 +57,14 @@ def main(): # JP2 metadata can be extensive, so don't print any warnings until we # are done with the metadata. jp2 = Jp2k(filename) - if (((jp2._codec_format == lib.openjp2.CODEC_J2K) and - (codestream_level == 0))): - print('File: {0}'.format(os.path.basename(filename))) - elif print_full_codestream: - print(jp2.get_codestream(header_only=False)) + if jp2._codec_format == lib.openjp2.CODEC_J2K: + if codestream_level == 0: + print('File: {0}'.format(os.path.basename(filename))) + elif codestream_level == 1: + print(jp2) + elif codestream_level == 2: + print('File: {0}'.format(os.path.basename(filename))) + print(jp2.get_codestream(header_only=False)) else: print(jp2) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index e29df28..bc23ebe 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1001,18 +1001,19 @@ class ContiguousCodestreamBox(Jp2kBox): offset of the box from the start of the file. longname : str more verbose description of the box. - main_header : Codestream object - contains list of main header marker/segments + codestream : Codestream object + Contains list of codestream marker/segments. By default, only the main + header is retrieved. main_header_offset : int offset of main header from start of file """ box_id = 'jp2c' longname = 'Contiguous Codestream' - def __init__(self, main_header=None, main_header_offset=None, length=0, + def __init__(self, codestream=None, main_header_offset=None, length=0, offset=-1): Jp2kBox.__init__(self) - self._main_header = main_header + self._codestream = codestream self.length = length self.offset = offset self.main_header_offset = main_header_offset @@ -1021,20 +1022,23 @@ class ContiguousCodestreamBox(Jp2kBox): self._filename = None @property - def main_header(self): - if self._main_header is None: + def codestream(self): + if _parseoptions['full_codestream'] is True: + header_only = False + else: + header_only = True + if self._codestream is None: if self._filename is not None: with open(self._filename, 'rb') as fptr: fptr.seek(self.main_header_offset) - main_header = Codestream(fptr, - self._length, - header_only=True) - self._main_header = main_header - return self._main_header + codestream = Codestream(fptr, self._length, + header_only=header_only) + self._codestream = codestream + return self._codestream def __repr__(self): - msg = "glymur.jp2box.ContiguousCodeStreamBox(main_header={0})" - return msg.format(repr(self.main_header)) + msg = "glymur.jp2box.ContiguousCodeStreamBox(codestream={0})" + return msg.format(repr(self.codestream)) def __str__(self): msg = Jp2kBox.__str__(self) @@ -1043,9 +1047,8 @@ class ContiguousCodestreamBox(Jp2kBox): if _printoptions['codestream'] is False: return msg - msg += '\n Main header:' - for segment in self.main_header.segment: - msg += '\n' + self._indent(str(segment), indent_level=8) + for segment in self.codestream.segment: + msg += '\n' + self._indent(str(segment), indent_level=4) return msg @@ -1067,11 +1070,11 @@ class ContiguousCodestreamBox(Jp2kBox): ContiguousCodestreamBox instance """ main_header_offset = fptr.tell() - if _parseoptions['codestream'] is True: - main_header = Codestream(fptr, length, header_only=True) + if _parseoptions['full_codestream'] is True: + codestream = Codestream(fptr, length, header_only=False) else: - main_header = None - box = cls(main_header, main_header_offset=main_header_offset, + codestream = None + box = cls(codestream, main_header_offset=main_header_offset, length=length, offset=offset) box._filename = fptr.name box._length = length @@ -1320,7 +1323,7 @@ class FileTypeBox(Jp2kBox): if sys.hexversion >= 0x03000000: try: entry = entry.decode('utf-8') - except UnicodeDecodeError as err: + except UnicodeDecodeError: # The entry is invalid, but we've got code to catch this # later on. pass @@ -3314,19 +3317,20 @@ _BOX_WITH_ID = { b'uuid': UUIDBox, b'xml ': XMLBox} -_parseoptions = {'codestream': True} +_parseoptions = {'full_codestream': False} -def set_parseoptions(codestream=True): +def set_parseoptions(full_codestream=True): """Set parsing options. These options determine the way JPEG 2000 boxes are parsed. Parameters ---------- - codestream : bool, defaults to True - When False, the codestream header is only parsed when accessed. This - can results in faster JP2/JPX parsing. + full_codestream : bool, defaults to True + When False, only the codestream header is parsed for metadata. This + can results in faster JP2/JPX parsing. When True, the entire + codestream is parsed for metadata. See also -------- @@ -3337,9 +3341,9 @@ def set_parseoptions(codestream=True): To put back the default options, you can use: >>> import glymur - >>> glymur.set_parseoptions(codestream=True) + >>> glymur.set_parseoptions(full_codestream=True) """ - _parseoptions['codestream'] = codestream + _parseoptions['full_codestream'] = full_codestream def get_parseoptions(): @@ -3378,7 +3382,9 @@ def set_printoptions(**kwargs): When False, printing of the XML contents of any XML boxes or UUID XMP boxes is suppressed. codestream : bool, optional - When False, printing of the codestream contents is suppressed. + When False, only the codestream header is printed. When True, the + entire codestream is printed. This option has no effect when the + 'short' option is set to True. See also -------- diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index c7bab78..dc27b0e 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -2,6 +2,7 @@ Test fixtures common to more than one test point. """ import os +import platform import re import sys import textwrap @@ -33,6 +34,14 @@ elif re.match('1.[0-6]', six.__version__) is not None: WARNING_INFRASTRUCTURE_ISSUE = True msg = "Cannot run test with version {0} of python-six" WARNING_INFRASTRUCTURE_MSG = msg.format(six.__version__) +elif ((re.match('1.8', six.__version__) is not None) and + (sys.platform.startswith('linux')) and + (platform.linux_distribution() == ('LinuxMint', '17', 'qiana'))): + WARNING_INFRASTRUCTURE_ISSUE = True + linux_distribution = platform.linux_distribution() + msg = "Cannot run test with version {0} of python-six on {1}" + WARNING_INFRASTRUCTURE_MSG = msg.format(six.__version__, + platform.linux_distribution) # Cannot reopen a named temporary file in windows. WINDOWS_TMP_FILE_MSG = "cannot use NamedTemporaryFile like this in windows" @@ -709,6 +718,190 @@ UUID Box (uuid) @ (77, 3146) UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) Contiguous Codestream Box (jp2c) @ (3223, 1132296)""" +nemo = """JPEG 2000 Signature Box (jP ) @ (0, 12) + Signature: 0d0a870a +File Type Box (ftyp) @ (12, 20) + Brand: jp2 + Compatibility: ['jp2 '] +JP2 Header Box (jp2h) @ (32, 45) + Image Header Box (ihdr) @ (40, 22) + Size: [1456 2592 3] + Bitdepth: 8 + Signed: False + Compression: wavelet + Colorspace Unknown: False + Colour Specification Box (colr) @ (62, 15) + Method: enumerated colorspace + Precedence: 0 + Colorspace: sRGB +UUID Box (uuid) @ (77, 3146) + UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) + UUID Data: + + + + + Google + 2013-02-09T14:47:53 + + + 1 + 72/1 + 72/1 + 2 + HTC + HTC Glacier + 2592 + 1456 + + + 8 + 8 + 8 + + + 2 + 3 + + + 1343036288/4294967295 + 1413044224/4294967295 + + + + + 2748779008/4294967295 + 1417339264/4294967295 + 1288490240/4294967295 + 2576980480/4294967295 + 644245120/4294967295 + 257698032/4294967295 + + + + + 1 + 2528 + 1424 + 353/100 + 0 + 0/1 + WGS-84 + 2013-02-09T14:47:53 + + + 76 + + + 0220 + 0100 + + + 1 + 2 + 3 + 0 + + + 42,20.56N + 71,5.29W + 2013-02-09T19:47:53Z + NETWORK + + + 2013-02-09T14:47:53 + + + + + Glymur + Python XMP Toolkit + + + + + + +Contiguous Codestream Box (jp2c) @ (3223, 1132296) + SOC marker segment @ (3231, 0) + SIZ marker segment @ (3233, 47) + Profile: no profile + Reference Grid Height, Width: (1456 x 2592) + Vertical, Horizontal Reference Grid Offset: (0 x 0) + Reference Tile Height, Width: (1456 x 2592) + Vertical, Horizontal Reference Tile Offset: (0 x 0) + Bitdepth: (8, 8, 8) + Signed: (False, False, False) + Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1)) + COD marker segment @ (3282, 12) + Coding style: + Entropy coder, without partitions + SOP marker segments: False + EPH marker segments: False + Coding style parameters: + Progression order: LRCP + Number of layers: 2 + Multiple component transformation usage: reversible + Number of resolutions: 2 + Code block height, width: (64 x 64) + Wavelet transform: 5-3 reversible + Precinct size: default, 2^15 x 2^15 + Code block context: + Selective arithmetic coding bypass: False + Reset context probabilities on coding pass boundaries: False + Termination on each coding pass: False + Vertically stripe causal context: False + Predictable termination: False + Segmentation symbols: False + QCD marker segment @ (3296, 7) + Quantization style: no quantization, 2 guard bits + Step size: [(0, 8), (0, 9), (0, 9), (0, 10)] + CME marker segment @ (3305, 37) + "Created by OpenJPEG version 2.0.0" + SOT marker segment @ (3344, 10) + Tile part index: 0 + Tile part length: 1132173 + Tile part instance: 0 + Number of tile parts: 1 + COC marker segment @ (3356, 9) + Associated component: 1 + Coding style for this component: Entropy coder, PARTITION = 0 + Coding style parameters: + Number of resolutions: 2 + Code block height, width: (64 x 64) + Wavelet transform: 5-3 reversible + Code block context: + Selective arithmetic coding bypass: False + Reset context probabilities on coding pass boundaries: False + Termination on each coding pass: False + Vertically stripe causal context: False + Predictable termination: False + Segmentation symbols: False + QCC marker segment @ (3367, 8) + Associated Component: 1 + Quantization style: no quantization, 2 guard bits + Step size: [(0, 8), (0, 9), (0, 9), (0, 10)] + COC marker segment @ (3377, 9) + Associated component: 2 + Coding style for this component: Entropy coder, PARTITION = 0 + Coding style parameters: + Number of resolutions: 2 + Code block height, width: (64 x 64) + Wavelet transform: 5-3 reversible + Code block context: + Selective arithmetic coding bypass: False + Reset context probabilities on coding pass boundaries: False + Termination on each coding pass: False + Vertically stripe causal context: False + Predictable termination: False + Segmentation symbols: False + QCC marker segment @ (3388, 8) + Associated Component: 2 + Quantization style: no quantization, 2 guard bits + Step size: [(0, 8), (0, 9), (0, 9), (0, 10)] + SOD marker segment @ (3398, 0) + EOC marker segment @ (1135517, 0)""" + # Output of reader requirements printing for text_GBR.jp2 text_GBR_rreq = r"""Reader Requirements Box (rreq) @ (40, 109) Fully Understands Aspect Mask: 0xffff diff --git a/glymur/test/test_glymur_warnings.py b/glymur/test/test_glymur_warnings.py index ff18c6e..8086004 100644 --- a/glymur/test/test_glymur_warnings.py +++ b/glymur/test/test_glymur_warnings.py @@ -20,6 +20,13 @@ from .fixtures import WARNING_INFRASTRUCTURE_ISSUE, WARNING_INFRASTRUCTURE_MSG class TestWarnings(unittest.TestCase): """Test suite for warnings issued by glymur.""" + def test_invalid_compatibility_list_entry(self): + """should not error out with invalid compatibility list entry""" + filename = opj_data_file('input/nonregression/issue397.jp2') + with self.assertWarns(UserWarning): + Jp2k(filename) + self.assertTrue(True) + def test_exceeded_box_length(self): """ should warn if reading past end of a box @@ -62,7 +69,7 @@ class TestWarnings(unittest.TestCase): \(\d+\)\.""", re.VERBOSE) with self.assertWarnsRegex(UserWarning, regex): - Jp2k(jfile) + Jp2k(jfile).get_codestream() @unittest.skipIf(re.match("1.5|2.0.0", glymur.version.openjpeg_version), "Test not passing on 1.5.x, not introduced until 2.x") @@ -77,7 +84,7 @@ class TestWarnings(unittest.TestCase): \(\d+\)\.""", re.VERBOSE) with self.assertWarnsRegex(UserWarning, regex): - Jp2k(jfile) + Jp2k(jfile).get_codestream() def test_NR_gdal_fuzzer_check_comp_dx_dy_jp2_dump(self): """ @@ -90,7 +97,7 @@ class TestWarnings(unittest.TestCase): dx=\d+,\s*dy=\d+""", re.VERBOSE) with self.assertWarnsRegex(UserWarning, regex): - Jp2k(jfile) + Jp2k(jfile).get_codestream() def test_NR_gdal_fuzzer_assert_in_opj_j2k_read_SQcd_SQcc_patch_jp2(self): lst = ['input', 'nonregression', @@ -100,32 +107,32 @@ class TestWarnings(unittest.TestCase): number\sof\scomponents\sis\sonly\s\d+""", re.VERBOSE) with self.assertWarnsRegex(UserWarning, regex): - Jp2k(jfile) + Jp2k(jfile).get_codestream() def test_bad_rsiz(self): """Should warn if RSIZ is bad. Issue196""" filename = opj_data_file('input/nonregression/edf_c2_1002767.jp2') with self.assertWarnsRegex(UserWarning, 'Invalid profile'): - Jp2k(filename) + Jp2k(filename).get_codestream() def test_bad_wavelet_transform(self): """Should warn if wavelet transform is bad. Issue195""" filename = opj_data_file('input/nonregression/edf_c2_10025.jp2') with self.assertWarnsRegex(UserWarning, 'Invalid wavelet transform'): - Jp2k(filename) + Jp2k(filename).get_codestream() def test_invalid_progression_order(self): """Should still be able to parse even if prog order is invalid.""" jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2') with self.assertWarnsRegex(UserWarning, 'Invalid progression order'): - Jp2k(jfile) + Jp2k(jfile).get_codestream() def test_tile_height_is_zero(self): """Zero tile height should not cause an exception.""" filename = 'input/nonregression/2539.pdf.SIGFPE.706.1712.jp2' filename = opj_data_file(filename) with self.assertWarnsRegex(UserWarning, 'Invalid tile dimensions'): - Jp2k(filename) + Jp2k(filename).get_codestream() @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_unknown_marker_segment(self): diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 9ccd75b..e43eae6 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -1016,7 +1016,7 @@ class TestJp2Boxes(unittest.TestCase): """Raw instantiation should not produce a main_header.""" box = ContiguousCodestreamBox() self.assertEqual(box.box_id, 'jp2c') - self.assertIsNone(box.main_header) + self.assertIsNone(box.codestream) def test_codestream_main_header_offset(self): """main_header_offset is an attribute of the CCS box""" @@ -1318,7 +1318,7 @@ class TestRepr(MetadataBase): # Difficult to eval(repr()) this, so just match the general pattern. regexp = "glymur.jp2box.ContiguousCodeStreamBox" - regexp += "[(]main_header=