From 506e4fff9220b93aa471417a25ad89f5925dd255 Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 10 Apr 2014 21:17:26 -0400 Subject: [PATCH] Improved code coverage to over 95% --- glymur/_uuid_io.py | 7 +- glymur/codestream.py | 10 +-- glymur/jp2box.py | 56 ++++++++-------- glymur/jp2k.py | 45 ++++--------- glymur/test/fixtures.py | 54 +++++++++++++++ glymur/test/test_jp2box.py | 106 ++++++++++++++++++++++++++++++ glymur/test/test_jp2box_jpx.py | 90 +++++++++++++++++++++++-- glymur/test/test_jp2k.py | 11 ++++ glymur/test/test_opj_suite_neg.py | 22 +++++++ glymur/test/test_printing.py | 81 ++++++++++++++++++++++- 10 files changed, 399 insertions(+), 83 deletions(-) diff --git a/glymur/_uuid_io.py b/glymur/_uuid_io.py index 8cd5bcf..daa1994 100644 --- a/glymur/_uuid_io.py +++ b/glymur/_uuid_io.py @@ -2,6 +2,7 @@ """ Part of glymur. """ +from collections import OrderedDict import pprint import re import struct @@ -10,12 +11,6 @@ import warnings import lxml.etree as ET -if sys.hexversion < 0x02070000: - # pylint: disable=F0401,E0611 - from ordereddict import OrderedDict -else: - from collections import OrderedDict - def xml(raw_data): """ XMP data to be parsed as XML. diff --git a/glymur/codestream.py b/glymur/codestream.py index 6830fab..acc7791 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -188,15 +188,7 @@ class Codestream(object): while True: read_buffer = fptr.read(2) - try: - self._marker_id, = struct.unpack('>H', read_buffer) - except struct.error: - # Treat this as a warning. - msg = "Marker had length {0} instead of expected length of 2 " - msg += "bytes. Codestream parsing terminated." - warnings.warn(msg.format(len(read_buffer))) - break - + self._marker_id, = struct.unpack('>H', read_buffer) self._offset = fptr.tell() - 2 if self._marker_id == 0xff90 and header_only: diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 5887864..b20895b 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1011,7 +1011,7 @@ class ContiguousCodestreamBox(Jp2kBox): if self._main_header is None: if self._filename is not None: with open(self._filename, 'rb') as fptr: - fptr.seek(self._offset + 8) + fptr.seek(self.main_header_offset) main_header = Codestream(fptr, self._length, header_only=True) self._main_header = main_header return self._main_header @@ -1059,7 +1059,6 @@ class ContiguousCodestreamBox(Jp2kBox): length=length, offset=offset) box._filename = fptr.name box._length = length - box._offset = offset return box @@ -1110,7 +1109,7 @@ class DataReferenceBox(Jp2kBox): """ self._write_validate() - # Very similar to the say a superbox is written. + # Very similar to the way a superbox is written. orig_pos = fptr.tell() fptr.write(struct.pack('>I4s', 0, b'dtbl')) @@ -1176,7 +1175,7 @@ class DataReferenceBox(Jp2kBox): box = DataEntryURLBox.parse(box_fptr, 0, box_length) # Need to adjust the box start to that of the "real" file. - box.start = offset + box_offset + box.offset = offset + 8 + box_offset data_entry_url_box_list.append(box) # Point to the next embedded URL box. @@ -1341,7 +1340,10 @@ class FragmentListBox(Jp2kBox): self._dispatch_validation_error(msg, writing=writing) def __repr__(self): - msg = "glymur.jp2box.FragmentListBox()" + msg = "glymur.jp2box.FragmentListBox({0}, {1}, {2})" + msg = msg.format(str(self.fragment_offset), + str(self.fragment_length), + str(self.data_reference)) return msg def __str__(self): @@ -1426,7 +1428,8 @@ class FragmentTableBox(Jp2kBox): self.box = box if box is not None else [] def __repr__(self): - msg = "glymur.jp2box.FragmentTableBox()" + msg = "glymur.jp2box.FragmentTableBox(box={0})" + msg = msg.format(None) if (len(self.box) == 0) else msg.format(self.box) return msg def __str__(self): @@ -1884,6 +1887,12 @@ class PaletteBox(Jp2kBox): msg = "The length of the 'bits_per_component' and the 'signed' " msg += "members must equal the number of columns of the palette." self._dispatch_validation_error(msg, writing=writing) + bps = self.bits_per_component + if writing and not all(b == bps[0] for b in bps): + # We don't support writing palettes with bit depths that are + # different. + msg = "Writing palettes with varying bit depths is not supported." + self._dispatch_validation_error(msg, writing=writing) def __repr__(self): msg = "glymur.jp2box.PaletteBox({0}, bits_per_component={1}, " @@ -1925,26 +1934,14 @@ class PaletteBox(Jp2kBox): fptr.write(write_buffer) bps = self.bits_per_component - if all(b == bps[0] for b in bps): - # All components are the same. Writing is straightforward. - if self.bits_per_component[0] <= 8: - write_buffer = memoryview(self.palette.astype(np.uint8)) - elif self.bits_per_component[0] <= 16: - write_buffer = memoryview(self.palette.astype(np.uint16)) - elif self.bits_per_component[0] <= 32: - write_buffer = memoryview(self.palette.astype(np.uint32)) - fptr.write(write_buffer) - else: - # Not all the components are the same. More general, but much rarer - # case. Does this even happen. - code_dict = {8: 'B', 16: 'H', 32: 'I'} - codes = '' - for width in bps: - codes += code_dict[width] - fmt = '>' + codes - for row in self.palette: - write_buffer = struct.pack(fmt, *row) - fptr.write(write_buffer) + # All components are the same. Writing is straightforward. + if self.bits_per_component[0] <= 8: + write_buffer = memoryview(self.palette.astype(np.uint8)) + elif self.bits_per_component[0] <= 16: + write_buffer = memoryview(self.palette.astype(np.uint16)) + elif self.bits_per_component[0] <= 32: + write_buffer = memoryview(self.palette.astype(np.uint32)) + fptr.write(write_buffer) @classmethod def parse(cls, fptr, offset, length): @@ -2635,18 +2632,19 @@ class NumberListBox(Jp2kBox): msg += 'the rendered result' elif (association >> 24) == 1: idx = association & 0x00FFFFFF - msg += 'Codestream {0}' + msg += 'codestream {0}' msg = msg.format(idx) elif (association >> 24) == 2: idx = association & 0x00FFFFFF - msg += 'Compositing Layer {0}' + msg += 'compositing layer {0}' msg = msg.format(idx) else: msg += 'unrecognized' return msg def __repr__(self): - msg = 'glymur.jp2box.NumberListBox()' + msg = 'glymur.jp2box.NumberListBox(associations={0})' + msg = msg.format(self.associations) return msg @classmethod diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 8110c50..3fffea1 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -547,13 +547,13 @@ class Jp2k(Jp2kBox): opj2.setup_encoder(codec, cparams, image) - if _OPENJP2_IS_OFFICIAL_V2: + if re.match("2.0", version.openjpeg_version) is not None: fptr = libc.fopen(self.filename, 'wb') strm = opj2.stream_create_default_file_stream(fptr, False) stack.callback(opj2.stream_destroy, strm) stack.callback(libc.fclose, fptr) else: - # This routine introduced in 2.0 devel series. + # Introduced in 2.1 devel series. strm = opj2.stream_create_default_file_stream_v3(self.filename, False) stack.callback(opj2.stream_destroy_v3, strm) @@ -1080,17 +1080,17 @@ class Jp2k(Jp2kBox): layer=layer, tile=tile, area=area) with ExitStack() as stack: - if hasattr(opj2.OPENJP2, - 'opj_stream_create_default_file_stream_v3'): - filename = self.filename - stream = opj2.stream_create_default_file_stream_v3(filename, - True) - stack.callback(opj2.stream_destroy_v3, stream) - else: + if re.match("2.0", version.openjpeg_version): fptr = libc.fopen(self.filename, 'rb') stack.callback(libc.fclose, fptr) stream = opj2.stream_create_default_file_stream(fptr, True) stack.callback(opj2.stream_destroy, stream) + else: + # API change in 2.1+ + filename = self.filename + stream = opj2.stream_create_default_file_stream_v3(filename, + True) + stack.callback(opj2.stream_destroy_v3, stream) codec = opj2.create_decompress(self._codec_format) stack.callback(opj2.destroy_codec, codec) @@ -1147,12 +1147,6 @@ class Jp2k(Jp2kBox): Bitdepth: (8, 8, 8) Signed: (False, False, False) Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1)) - - Raises - ------ - IOError - If the file is JPX with more than one codestream and no JP2 - compatibility is advertised. """ with open(self.filename, 'rb') as fptr: if self._codec_format == opj2.CODEC_J2K: @@ -1161,11 +1155,6 @@ class Jp2k(Jp2kBox): else: ftyp = self.box[1] box = [x for x in self.box if x.box_id == 'jp2c'] - if len(box) > 1 and 'jp2 ' not in ftyp.compatibility_list: - msg = "If more than one codestream exists, JP2 " - msg += "compatibiltity must be advertised by the FileType " - msg += "box." - raise RuntimeError(msg) fptr.seek(box[0].offset) read_buffer = fptr.read(8) (box_length, _) = struct.unpack('>I4s', read_buffer) @@ -1183,7 +1172,7 @@ class Jp2k(Jp2kBox): return codestream -def component2dtype(component): +def _component2dtype(component): """Take an OpenJPEG component structure and determine the numpy datatype. Parameters @@ -1474,7 +1463,7 @@ def extract_image_cube(image): """ ncomps = image.contents.numcomps component = image.contents.comps[0] - dtype = component2dtype(component) + dtype = _component2dtype(component) nrows = component.h ncols = component.w @@ -1508,7 +1497,7 @@ def extract_image_bands(image): for k in range(image.contents.numcomps): component = image.contents.comps[k] - dtype = component2dtype(component) + dtype = _component2dtype(component) nrows = component.h ncols = component.w @@ -1698,7 +1687,7 @@ def _validate_compression_params(img_array, cparams): msg = "{0}D imagery is not allowed.".format(img_array.ndim) raise IOError(msg) - if _OPENJP2_IS_OFFICIAL_V2: + if re.match("2.0", version.openjpeg_version) is not None: if (((img_array.ndim != 2) and (img_array.shape[2] != 1 and img_array.shape[2] != 3))): msg = "Writing images is restricted to single-channel " @@ -1711,14 +1700,6 @@ def _validate_compression_params(img_array, cparams): msg = "Only uint8 and uint16 images are currently supported." raise RuntimeError(msg) -# Need to known if openjp2 library is the officially release v2.0.0 or not. -_OPENJP2_IS_OFFICIAL_V2 = False -if opj2.OPENJP2 is not None: - if opj2.version() == '2.0.0': - if not hasattr(opj2.OPENJP2, - 'opj_stream_create_default_file_stream_v3'): - _OPENJP2_IS_OFFICIAL_V2 = True - _COLORSPACE_MAP = {'rgb': opj2.CLRSPC_SRGB, 'gray': opj2.CLRSPC_GRAY, 'grey': opj2.CLRSPC_GRAY, diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index 1ee7aa1..683ecca 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -622,3 +622,57 @@ cinema2k_profile = """SIZ marker segment @ (2, 47) Bitdepth: (12, 12, 12) Signed: (False, False, False) Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1))""" + +jplh_color_group_box = r"""Compositing Layer Header Box (jplh) @ (314227, 31) + Colour Group Box (cgrp) @ (314235, 23) + Colour Specification Box (colr) @ (314243, 15) + Method: enumerated colorspace + Precedence: 0 + Colorspace: sRGB""" + +fragment_list_box = r"""Fragment List Box (flst) @ (-1, 0) + Offset 0: 89 + Fragment Length 0: 1132288 + Data Reference 0: 0""" + +number_list_box = r"""Number List Box (nlst) @ (-1, 0) + Association[0]: the rendered result + Association[1]: codestream 0 + Association[2]: compositing layer 0""" + + +goodstuff = r"""Codestream: + SOC marker segment @ (0, 0) + SIZ marker segment @ (2, 47) + Profile: no profile + Reference Grid Height, Width: (800 x 480) + Vertical, Horizontal Reference Grid Offset: (0 x 0) + Reference Tile Height, Width: (800 x 480) + 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 @ (51, 12) + Coding style: + Entropy coder, without partitions + SOP marker segments: False + EPH marker segments: False + Coding style parameters: + Progression order: LRCP + Number of layers: 1 + Multiple component transformation usage: reversible + Number of resolutions: 6 + 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 @ (65, 19) + Quantization style: no quantization, 2 guard bits + Step size: [(0, 8), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10)]""" + diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 90f205d..e19a097 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -543,6 +543,16 @@ class TestPaletteBox(unittest.TestCase): pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps, signed=signed) + def test_writing_with_different_bitdepths(self): + """Bitdepths must be the same when writing.""" + palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint16) + bps = (8, 16, 8) + signed = (False, False, False) + pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps, + signed=signed) + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + with self.assertRaises(IOError): + pclr.write(tfile) class TestAppend(unittest.TestCase): """Tests for append method.""" @@ -733,6 +743,49 @@ class TestWrap(unittest.TestCase): boxes = [box.box_id for box in jp2.box] self.assertEqual(boxes, ['jP ', 'ftyp', 'jp2h', 'jp2c']) + def test_wrap_jp2_Lzero(self): + """Wrap jp2 with jp2c box length is zero""" + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with open(self.jp2file, 'rb') as ifile: + tfile.write(ifile.read()) + # Rewrite with codestream length as zero. + tfile.seek(3223) + tfile.write(struct.pack('>I', 0)) + tfile.flush() + jp2 = Jp2k(tfile.name) + + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile2: + jp2 = jp2.wrap(tfile2.name) + boxes = [box for box in jp2.box] + self.assertEqual(boxes[3].length, 1132296) + + def test_wrap_jp2_Lone(self): + """Wrap jp2 with jp2c box length is 1, implies Q field""" + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with open(self.jp2file, 'rb') as ifile: + tfile.write(ifile.read(3223)) + # Write new L, T, Q fields + tfile.write(struct.pack('>I4sQ', 1, b'jp2c', 1132296 + 8)) + # skip over the old L, T fields + ifile.seek(3231) + tfile.write(ifile.read()) + tfile.flush() + jp2 = Jp2k(tfile.name) + + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile2: + jp2 = jp2.wrap(tfile2.name) + boxes = [box for box in jp2.box] + self.assertEqual(boxes[3].length, 1132296 + 8) + + def test_wrap_compatibility_not_jp2(self): + """File type compatibility must contain jp2""" + jp2 = Jp2k(self.jp2file) + boxes = [box for box in jp2.box] + boxes[1].compatibility_list = ['jpx '] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + with self.assertRaises(IOError): + jp2.wrap(tfile.name, boxes=boxes) + def test_empty_jp2h(self): """JP2H box list cannot be empty.""" jp2 = Jp2k(self.jp2file) @@ -992,6 +1045,59 @@ class TestRepr(unittest.TestCase): self.assertTrue(isinstance(newbox, glymur.jp2box.JPEG2000SignatureBox)) self.assertEqual(newbox.signature, (13, 10, 135, 10)) + def test_free(self): + """Should be able to instantiate a free box""" + free = glymur.jp2box.FreeBox() + + # Test the representation instantiation. + newbox = eval(repr(free)) + self.assertTrue(isinstance(newbox, glymur.jp2box.FreeBox)) + + def test_nlst(self): + """Should be able to instantiate a number list box""" + assn = (0, 1, 2) + nlst = glymur.jp2box.NumberListBox(assn) + + # Test the representation instantiation. + newbox = eval(repr(nlst)) + self.assertTrue(isinstance(newbox, glymur.jp2box.NumberListBox)) + self.assertEqual(newbox.associations, (0, 1, 2)) + + def test_ftbl(self): + """Should be able to instantiate a fragment table box""" + ftbl = glymur.jp2box.FragmentTableBox() + + # Test the representation instantiation. + newbox = eval(repr(ftbl)) + self.assertTrue(isinstance(newbox, glymur.jp2box.FragmentTableBox)) + + def test_dref(self): + """Should be able to instantiate a data reference box""" + dref = glymur.jp2box.DataReferenceBox() + + # Test the representation instantiation. + newbox = eval(repr(dref)) + self.assertTrue(isinstance(newbox, glymur.jp2box.DataReferenceBox)) + + def test_flst(self): + """Should be able to instantiate a fragment list box""" + flst = glymur.jp2box.FragmentListBox([89], [1132288], [0]) + + # Test the representation instantiation. + newbox = eval(repr(flst)) + self.assertTrue(isinstance(newbox, glymur.jp2box.FragmentListBox)) + self.assertEqual(newbox.fragment_offset, [89]) + self.assertEqual(newbox.fragment_length, [1132288]) + self.assertEqual(newbox.data_reference, [0]) + + def test_default_cgrp(self): + """Should be able to instantiate a color group box""" + cgrp = glymur.jp2box.ColourGroupBox() + + # Test the representation instantiation. + newbox = eval(repr(cgrp)) + self.assertTrue(isinstance(newbox, glymur.jp2box.ColourGroupBox)) + def test_default_ftyp(self): """Should be able to instantiate a FileTypeBox""" ftyp = glymur.jp2box.FileTypeBox() diff --git a/glymur/test/test_jp2box_jpx.py b/glymur/test/test_jp2box_jpx.py index 7fe407a..3e09c83 100644 --- a/glymur/test/test_jp2box_jpx.py +++ b/glymur/test/test_jp2box_jpx.py @@ -3,6 +3,7 @@ Test suite specifically targeting JPX box layout. """ +import ctypes import os import struct import sys @@ -52,6 +53,7 @@ class TestJPXWrap(unittest.TestCase): tfile1.write(fptr.read()) tfile1.flush() jp2_1 = Jp2k(tfile1.name) + jp2h = jp2_1.box[2] jp2c = [box for box in jp2_1.box if box.box_id == 'jp2c'][0] @@ -61,12 +63,13 @@ class TestJPXWrap(unittest.TestCase): clen = [] dr_idx = [] - coff.append(jp2c.offset + 8) + coff.append(jp2c.main_header_offset) clen.append(jp2c.length - (coff[0] - jp2c.offset)) dr_idx.append(1) # Make the url box for this codestream. url1 = DataEntryURLBox(0, [0, 0, 0], 'file://' + tfile1.name) + url1_name_len = len(url1.url) + 1 with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile2: @@ -74,7 +77,7 @@ class TestJPXWrap(unittest.TestCase): jp2_2 = j2k.wrap(tfile2.name) jp2c = [box for box in jp2_2.box if box.box_id == 'jp2c'][0] - coff.append(jp2c.offset + 8) + coff.append(jp2c.main_header_offset) clen.append(jp2c.length - (coff[0] - jp2c.offset)) dr_idx.append(2) @@ -85,7 +88,7 @@ class TestJPXWrap(unittest.TestCase): FileTypeBox(brand='jpx ', compatibility_list=['jpx ', 'jp2 ', 'jpxb']), - jp2_1.box[2]] + jp2h] with tempfile.NamedTemporaryFile(suffix='.jpx') as tjpx: for box in boxes: box.write(tjpx) @@ -99,6 +102,15 @@ class TestJPXWrap(unittest.TestCase): dtbl.write(tjpx) tjpx.flush() + jpx_no_jp2c = Jp2k(tjpx.name) + jpx_boxes = [box.box_id for box in jpx_no_jp2c.box] + self.assertEqual(jpx_boxes, ['jP ', 'ftyp', 'jp2h', + 'ftbl', 'dtbl']) + self.assertEqual(jpx_no_jp2c.box[4].DR[0].offset, 141) + + offset = 141 + 8 + 4 + url1_name_len + self.assertEqual(jpx_no_jp2c.box[4].DR[1].offset, offset) + def test_jp2_with_jpx_box(self): """If the brand is jp2, then no jpx boxes are allowed.""" jp2 = Jp2k(self.jp2file) @@ -154,8 +166,8 @@ class TestJPXWrap(unittest.TestCase): self.assertEqual(jpx.box[-1].box[0].box_id, 'colr') self.assertEqual(jpx.box[-1].box[1].box_id, 'colr') - def test_cgrp_neg(self): - """Can't write a cgrp with anything but colr sub boxes""" + def test_label_neg(self): + """Can't write a label box embedded in any old box.""" jp2 = Jp2k(self.jp2file) boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]] @@ -173,6 +185,26 @@ class TestJPXWrap(unittest.TestCase): with self.assertRaises(IOError): jpx = jp2.wrap(tfile.name, boxes=boxes) + def test_cgrp_neg(self): + """Can't write a cgrp with anything but colr sub boxes""" + 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'] + + the_xml = ET.fromstring('0') + xmlb = glymur.jp2box.XMLBox(xml=the_xml) + box = [xmlb] + + cgrp = glymur.jp2box.ColourGroupBox(box=box) + boxes.append(cgrp) + + with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile: + with self.assertRaises(IOError): + jpx = jp2.wrap(tfile.name, boxes=boxes) + def test_ftbl(self): """Write a fragment table box.""" # Add a negative test where offset < 0 @@ -459,7 +491,7 @@ class TestJPX(unittest.TestCase): self.assertEqual(jpx.box[2].box_id, 'rreq') self.assertEqual(type(jpx.box[2]), glymur.jp2box.ReaderRequirementsBox) - self.assertEqual(jpx.box[2].standard_flag, + self.asserwrite_buffertEqual(jpx.box[2].standard_flag, (5, 42, 45, 2, 18, 19, 1, 8, 12, 31, 20)) @unittest.skip("Requires unnecessarily complicated code") @@ -552,6 +584,52 @@ class TestJPX(unittest.TestCase): self.assertEqual(jpx.box[-1].box[0].fragment_length, (170246,)) self.assertEqual(jpx.box[-1].box[0].data_reference, (3,)) + def test_rreq3(self): + """Verify that we can read a rreq box with mask length 3 bytes""" + rreq_buffer = ctypes.create_string_buffer(74) + struct.pack_into('>I4s', rreq_buffer, 0, 74, b'rreq') + + # mask length + struct.pack_into('>B', rreq_buffer, 8, 3) + + # fuam, dcm. 6 bytes, two sets of 3. + lst = (255, 224, 0, 0, 31, 252) + struct.pack_into('>BBBBBB', rreq_buffer, 9, *lst) + + # number of standard features: 11 + struct.pack_into('>H', rreq_buffer, 15, 11) + + standard_flags = [5, 42, 45, 2, 18, 19, 1, 8, 12, 31, 20] + standard_masks = [8388608, 4194304, 2097152, 1048576, 524288, 262144, + 131072, 65536, 32768, 16384, 8192] + for j in range(len(standard_flags)): + mask = (standard_masks[j] >> 16, + standard_masks[j] & 0x0000ffff>> 8, + standard_masks[j] & 0x000000ff) + struct.pack_into('>HBBB', rreq_buffer, 17 + j * 5, + standard_flags[j], *mask) + + # num vendor features: 0 + struct.pack_into('>H', rreq_buffer, 72, 0) + + # Ok, done with the box, we can now insert it into the jpx file after + # the ftyp box. + with tempfile.NamedTemporaryFile(suffix=".jpx") as ofile: + with open(self.jpxfile, 'rb') as ifile: + ofile.write(ifile.read(40)) + ofile.write(rreq_buffer) + ofile.write(ifile.read()) + ofile.flush() + + jpx = Jp2k(ofile.name) + + self.assertEqual(jpx.box[2].box_id, 'rreq') + self.assertEqual(type(jpx.box[2]), + glymur.jp2box.ReaderRequirementsBox) + self.assertEqual(jpx.box[2].standard_flag, + (5, 42, 45, 2, 18, 19, 1, 8, 12, 31, 20)) + + def test_nlst(self): """Verify that we can handle a number list box.""" j = Jp2k(self.jpxfile) diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 3901cc5..7d2c911 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -779,6 +779,17 @@ class TestParsing(unittest.TestCase): with self.assertWarns(UserWarning): jp2 = Jp2k(filename) + @unittest.skip("blah") + def test_main_header(self): + """Verify that the main header is not loaded when parsing turned off.""" + # The hidden _main_header attribute should show up after accessing it. + glymur.set_parseoptions(codestream=False) + jp2 = Jp2k(self.jp2file) + jp2c = jp2.box[4] + self.assertIsNone(jp2c._main_header) + main_header = jp2c.main_header + self.assertIsNotNone(jp2c._main_header) + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") class TestJp2kOpjDataRoot(unittest.TestCase): diff --git a/glymur/test/test_opj_suite_neg.py b/glymur/test/test_opj_suite_neg.py index c9a7e8c..f6ef098 100644 --- a/glymur/test/test_opj_suite_neg.py +++ b/glymur/test/test_opj_suite_neg.py @@ -16,6 +16,13 @@ import unittest import numpy as np +try: + import skimage.io + skimage.io.use_plugin('freeimage', 'imread') + _HAS_SKIMAGE_FREEIMAGE_SUPPORT = True +except ((ImportError, RuntimeError)): + _HAS_SKIMAGE_FREEIMAGE_SUPPORT = False + from .fixtures import OPJ_DATA_ROOT, opj_data_file, read_image from .fixtures import NO_READ_BACKEND, NO_READ_BACKEND_MSG @@ -35,6 +42,21 @@ class TestSuiteNegative(unittest.TestCase): def tearDown(self): pass + + @unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT, + "Cannot read input image without scikit-image/freeimage") + @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") + def test_cinema2K_bad_frame_rate(self): + """Cinema2k frame rate must be either 24 or 48.""" + relfile = 'input/nonregression/X_5_2K_24_235_CBR_STEM24_000.tif' + infile = opj_data_file(relfile) + data = skimage.io.imread(infile) + with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: + j = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError): + j.write(data, cinema2k=36) + + @unittest.skipIf(NO_READ_BACKEND, NO_READ_BACKEND_MSG) @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_psnr_with_cratios(self): diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 5286419..db40530 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -50,6 +50,18 @@ class TestPrinting(unittest.TestCase): def tearDown(self): pass + def test_codestream(self): + """Should be able to print a raw codestream.""" + j = glymur.Jp2k(self.j2kfile) + with patch('sys.stdout', new=StringIO()) as fake_out: + print(j) + actual = fake_out.getvalue().strip() + # Remove the file line, as that is filesystem-dependent. + lines = actual.split('\n') + actual = '\n'.join(lines[1:]) + + self.assertEqual(actual, fixtures.codestream) + def test_version_info(self): """Should be able to print(glymur.version.info)""" with patch('sys.stdout', new=StringIO()) as fake_out: @@ -598,6 +610,63 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) + def test_flst(self): + """Verify printing of fragment list box.""" + flst = glymur.jp2box.FragmentListBox([89], [1132288], [0]) + with patch('sys.stdout', new=StringIO()) as fake_out: + print(flst) + actual = fake_out.getvalue().strip() + self.assertEqual(actual, fixtures.fragment_list_box) + + def test_dref(self): + """Verify printing of data reference box.""" + dref = glymur.jp2box.DataReferenceBox() + with patch('sys.stdout', new=StringIO()) as fake_out: + print(dref) + actual = fake_out.getvalue().strip() + self.assertEqual(actual, 'Data Reference Box (dtbl) @ (-1, 0)') + + def test_jplh_cgrp(self): + """Verify printing of compositing layer header box, color group box.""" + jpx = glymur.Jp2k(self.jpxfile) + with patch('sys.stdout', new=StringIO()) as fake_out: + print(jpx.box[7]) + actual = fake_out.getvalue().strip() + self.assertEqual(actual, fixtures.jplh_color_group_box) + + def test_free(self): + """Verify printing of Free box.""" + free = glymur.jp2box.FreeBox() + with patch('sys.stdout', new=StringIO()) as fake_out: + print(free) + actual = fake_out.getvalue().strip() + self.assertEqual(actual, 'Free Box (free) @ (-1, 0)') + + def test_nlst(self): + """Verify printing of number list box.""" + assn = (0, 16777216, 33554432) + nlst = glymur.jp2box.NumberListBox(assn) + with patch('sys.stdout', new=StringIO()) as fake_out: + print(nlst) + actual = fake_out.getvalue().strip() + self.assertEqual(actual, fixtures.number_list_box) + + def test_ftbl(self): + """Verify printing of fragment table box.""" + ftbl = glymur.jp2box.FragmentTableBox() + with patch('sys.stdout', new=StringIO()) as fake_out: + print(ftbl) + actual = fake_out.getvalue().strip() + self.assertEqual(actual, 'Fragment Table Box (ftbl) @ (-1, 0)') + + def test_jpch(self): + """Verify printing of JPCH box.""" + jpx = glymur.Jp2k(self.jpxfile) + with patch('sys.stdout', new=StringIO()) as fake_out: + print(jpx.box[3]) + actual = fake_out.getvalue().strip() + self.assertEqual(actual, 'Codestream Header Box (jpch) @ (887, 8)') + @unittest.skipIf(sys.hexversion < 0x03000000, "Ordered dicts not printing well in 2.7") def test_exif_uuid(self): @@ -893,6 +962,17 @@ class TestPrintingOpjDataRoot(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) + def test_componentmapping_box_alpha(self): + """Verify __repr__ method on cmap box.""" + cmap = glymur.jp2box.ComponentMappingBox(component_index=(0, 0, 0), + mapping_type=(1, 1, 1), + palette_index=(0, 1, 2)) + newbox = eval(repr(cmap)) + self.assertEqual(newbox.box_id, 'cmap') + self.assertEqual(newbox.component_index, (0, 0, 0)) + self.assertEqual(newbox.mapping_type, (1, 1, 1)) + self.assertEqual(newbox.palette_index, (0, 1, 2)) + def test_palette7(self): """verify printing of pclr box""" filename = opj_data_file('input/conformance/file9.jp2') @@ -905,7 +985,6 @@ class TestPrintingOpjDataRoot(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skip("file7 no longer has a rreq") def test_rreq(self): """verify printing of reader requirements box""" filename = opj_data_file('input/nonregression/text_GBR.jp2')