From cbdd6200b6feee7daf65e83ffca7a6525d3141b0 Mon Sep 17 00:00:00 2001 From: John Evans Date: Tue, 13 Jan 2015 14:44:03 -0500 Subject: [PATCH 1/5] reduce cyclomatic complexity where possible --- glymur/jp2k.py | 242 ++++++++++++++++++++++++++++++------------------- 1 file changed, 147 insertions(+), 95 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index ee6eedc..3633657 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -363,7 +363,11 @@ class Jp2k(Jp2kBox): # 2.1 API self._cparams.rsiz = core.OPJ_PROFILE_CINEMA_4K - def _populate_cparams(self, img_array, **kwargs): + def _populate_cparams(self, img_array, mct=None, cratios=None, psnr=None, + cinema2k=None, cinema4k=None, irreversible=None, + cbsize=None, eph=None, grid_offset=None, modesw=None, + numres=None, prog=None, psizes=None, sop=None, + subsam=None, tilesize=None, colorspace=None): """Directs processing of write method arguments. Parameters @@ -373,12 +377,14 @@ class Jp2k(Jp2kBox): kwargs : dictionary non-image keyword inputs provided to write method """ - if ((('cinema2k' in kwargs or 'cinema4k' in kwargs) and - (len(set(kwargs)) > 1))): + other_args = (mct, cratios, psnr, irreversible, cbsize, eph, + grid_offset, modesw, numres, prog, psizes, sop, subsam) + if (((cinema2k is not None or cinema4k is not None) and + (not all([arg is None for arg in other_args])))): msg = "Cannot specify cinema2k/cinema4k along with other options." raise IOError(msg) - if 'cratios' in kwargs and 'psnr' in kwargs: + if cratios is not None and psnr is not None: msg = "Cannot specify cratios and psnr together." raise IOError(msg) @@ -402,90 +408,81 @@ class Jp2k(Jp2kBox): cparams.tcp_numlayers = 1 cparams.cp_disto_alloc = 1 - if 'irreversible' in kwargs and kwargs['irreversible'] is True: - cparams.irreversible = 1 + cparams.irreversible = 1 if irreversible else 0 - if 'cinema2k' in kwargs: + if cinema2k is not None: self._cparams = cparams - self._set_cinema_params('cinema2k', kwargs['cinema2k']) + self._set_cinema_params('cinema2k', cinema2k) return - if 'cinema4k' in kwargs: + if cinema4k is not None: self._cparams = cparams - self._set_cinema_params('cinema4k', kwargs['cinema4k']) + self._set_cinema_params('cinema4k', cinema4k) return - if 'cbsize' in kwargs: - cparams.cblockw_init = kwargs['cbsize'][1] - cparams.cblockh_init = kwargs['cbsize'][0] + if cbsize is not None: + cparams.cblockw_init = cbsize[1] + cparams.cblockh_init = cbsize[0] - if 'cratios' in kwargs: - cparams.tcp_numlayers = len(kwargs['cratios']) - for j, cratio in enumerate(kwargs['cratios']): + if cratios is not None: + cparams.tcp_numlayers = len(cratios) + for j, cratio in enumerate(cratios): cparams.tcp_rates[j] = cratio cparams.cp_disto_alloc = 1 - if 'eph' in kwargs: - cparams.csty |= 0x04 + cparams.csty |= 0x02 if sop else 0 + cparams.csty |= 0x04 if eph else 0 - if 'grid_offset' in kwargs: - cparams.image_offset_x0 = kwargs['grid_offset'][1] - cparams.image_offset_y0 = kwargs['grid_offset'][0] + if grid_offset is not None: + cparams.image_offset_x0 = grid_offset[1] + cparams.image_offset_y0 = grid_offset[0] - if 'modesw' in kwargs: + if modesw is not None: for shift in range(6): power_of_two = 1 << shift - if kwargs['modesw'] & power_of_two: + if modesw & power_of_two: cparams.mode |= power_of_two - if 'numres' in kwargs: - cparams.numresolution = kwargs['numres'] + if numres is not None: + cparams.numresolution = numres - if 'prog' in kwargs: - prog = kwargs['prog'].upper() - cparams.prog_order = core.PROGRESSION_ORDER[prog] + if prog is not None: + cparams.prog_order = core.PROGRESSION_ORDER[prog.upper()] - if 'psnr' in kwargs: - cparams.tcp_numlayers = len(kwargs['psnr']) - for j, snr_layer in enumerate(kwargs['psnr']): + if psnr is not None: + cparams.tcp_numlayers = len(psnr) + for j, snr_layer in enumerate(psnr): cparams.tcp_distoratio[j] = snr_layer cparams.cp_fixed_quality = 1 - if 'psizes' in kwargs: - for j, (prch, prcw) in enumerate(kwargs['psizes']): + if psizes is not None: + for j, (prch, prcw) in enumerate(psizes): cparams.prcw_init[j] = prcw cparams.prch_init[j] = prch cparams.csty |= 0x01 - cparams.res_spec = len(kwargs['psizes']) + cparams.res_spec = len(psizes) - if 'sop' in kwargs: - cparams.csty |= 0x02 + if subsam is not None: + cparams.subsampling_dy = subsam[0] + cparams.subsampling_dx = subsam[1] - if 'subsam' in kwargs: - cparams.subsampling_dy = kwargs['subsam'][0] - cparams.subsampling_dx = kwargs['subsam'][1] - - if 'tilesize' in kwargs: - cparams.cp_tdx = kwargs['tilesize'][1] - cparams.cp_tdy = kwargs['tilesize'][0] + if tilesize is not None: + cparams.cp_tdx = tilesize[1] + cparams.cp_tdy = tilesize[0] cparams.tile_size_on = opj2.TRUE - try: - mct = kwargs['mct'] - if mct and self._colorspace == opj2.CLRSPC_GRAY: + if mct is None: + # If the multi component transform was not specified, we infer + # that it should be used if the color space is RGB. + cparams.tcp_mct = 1 if self._colorspace == opj2.CLRSPC_SRGB else 0 + else: + if self._colorspace == opj2.CLRSPC_GRAY: msg = "Cannot specify usage of the multi component transform " msg += "if the colorspace is gray." raise IOError(msg) cparams.tcp_mct = 1 if mct else 0 - except KeyError: - # If the multi component transform was not specified, we infer - # that it should be used if the color space is RGB. - if self._colorspace == opj2.CLRSPC_SRGB: - cparams.tcp_mct = 1 - else: - cparams.tcp_mct = 0 - self._validate_compression_params(img_array, cparams, **kwargs) + self._validate_compression_params(img_array, cparams, colorspace) self._cparams = cparams @@ -576,29 +573,26 @@ class Jp2k(Jp2kBox): self.parse() - def _validate_compression_params(self, img_array, cparams, **kwargs): - """Check that the compression parameters are valid. - - Parameters - ---------- - img_array : ndarray - Image data to be written to file. - cparams : CompressionParametersType(ctypes.Structure) - Corresponds to cparameters_t type in openjp2 headers. + def _validate_j2k_colorspace(self, cparams, colorspace): """ - # Cannot specify a colorspace with J2K. - if cparams.codec_fmt == opj2.CODEC_J2K and 'colorspace' in kwargs: + Cannot specify a colorspace with J2K. + """ + if cparams.codec_fmt == opj2.CODEC_J2K and colorspace is not None: msg = 'Do not specify a colorspace when writing a raw ' msg += 'codestream.' raise IOError(msg) - # Code block size - code_block_specified = False + def _validate_codeblock_size(self, cparams): + """ + Code block dimensions must satisfy certain restrictions. + + They must both be a power of 2 and the total area defined by the width + and height cannot be either too great or too small for the codec. + """ if cparams.cblockw_init != 0 and cparams.cblockh_init != 0: # These fields ARE zero if uninitialized. width = cparams.cblockw_init height = cparams.cblockh_init - code_block_specified = True if height * width > 4096 or height < 4 or width < 4: msg = "Code block area cannot exceed 4096. " msg += "Code block height and width must be larger than 4." @@ -609,7 +603,17 @@ class Jp2k(Jp2kBox): msg += "must be powers of 2." raise IOError(msg.format(height, width)) - # Precinct size + def _validate_precinct_size(self, cparams): + """ + Precinct dimensions must satisfy certain restrictions if specified. + + They must both be a power of 2 and must both be at least twice the + size of their codeblock size counterparts. + """ + code_block_specified = False + if cparams.cblockw_init != 0 and cparams.cblockh_init != 0: + code_block_specified = True + if cparams.res_spec != 0: # precinct size was not specified if this field is zero. for j in range(cparams.res_spec): @@ -627,11 +631,18 @@ class Jp2k(Jp2kBox): msg += "must be powers of 2." raise IOError(msg.format(prch, prcw)) - # What would the point of 1D images be? + def _validate_image_rank(self, img_array): + """ + Images must be either 2D or 3D. + """ if img_array.ndim == 1 or img_array.ndim > 3: msg = "{0}D imagery is not allowed.".format(img_array.ndim) raise IOError(msg) + def _validate_v2_0_0_images(self, img_array): + """ + Version 2.0.0 is restricted to only the most common images. + """ if re.match("2.0.0", version.openjpeg_version) is not None: if (((img_array.ndim != 2) and (img_array.shape[2] != 1 and img_array.shape[2] != 3))): @@ -641,11 +652,32 @@ class Jp2k(Jp2kBox): msg += "release." raise IOError(msg) + def _validate_image_datatype(self, img_array): + """ + Only uint8 and uint16 images are currently supported. + """ if img_array.dtype != np.uint8 and img_array.dtype != np.uint16: msg = "Only uint8 and uint16 datatypes are currently supported " msg += "when writing." raise RuntimeError(msg) + def _validate_compression_params(self, img_array, cparams, colorspace): + """Check that the compression parameters are valid. + + Parameters + ---------- + img_array : ndarray + Image data to be written to file. + cparams : CompressionParametersType(ctypes.Structure) + Corresponds to cparameters_t type in openjp2 headers. + """ + self._validate_j2k_colorspace(cparams, colorspace) + self._validate_codeblock_size(cparams) + self._validate_precinct_size(cparams) + self._validate_image_rank(img_array) + self._validate_v2_0_0_images(img_array) + self._validate_image_datatype(img_array) + def _determine_colorspace(self, colorspace=None, **kwargs): """Determine the colorspace from the supplied inputs. @@ -916,6 +948,47 @@ class Jp2k(Jp2kBox): msg = "Partial write operations are currently not allowed." raise TypeError(msg) + def _remove_ellipsis(self, index, numrows, numcols, numbands): + """ + resolve the first ellipsis in the index so that it references the image + + Parameters + ---------- + index : tuple + tuple of index arguments, presumably one of them is the Ellipsis + numrows, numcols, numbands : int + image dimensions + + Returns + ------- + newindex : tuple + Same as index, except that the first Ellipsis is replaced with + a proper slice whose start and stop members are not None + """ + # Remove the first ellipsis we find. + rows = slice(0, numrows) + cols = slice(0, numcols) + bands = slice(0, numbands) + if index[0] is Ellipsis: + if len(index) == 2: + # jp2k[..., other_slice] + newindex = (rows, cols, index[1]) + else: + # jp2k[..., cols, bands] + newindex = (rows, index[1], index[2]) + elif index[1] is Ellipsis: + if len(index) == 2: + # jp2k[rows, ...] + newindex = (index[0], cols, bands) + else: + # jp2k[rows, ..., bands] + newindex = (index[0], cols, index[2]) + else: + # Assume that we don't have 4D imagery, of course. + newindex = (index[0], index[1], bands) + + return newindex + def __getitem__(self, pargs): """ Slicing protocol. @@ -950,23 +1023,7 @@ class Jp2k(Jp2kBox): pargs = (pargs, Ellipsis) if isinstance(pargs, tuple) and any(x is Ellipsis for x in pargs): - # Remove the first ellipsis we find. - rows = slice(0, numrows) - cols = slice(0, numcols) - bands = slice(0, numbands) - if pargs[0] is Ellipsis: - if len(pargs) == 2: - newindex = (rows, cols, pargs[1]) - else: - newindex = (rows, pargs[1], pargs[2]) - elif pargs[1] is Ellipsis: - if len(pargs) == 2: - newindex = (pargs[0], cols, bands) - else: - newindex = (pargs[0], cols, pargs[2]) - else: - # Assume that we don't have 4D imagery, of course. - newindex = (pargs[0], pargs[1], bands) + newindex = self._remove_ellipsis(pargs, numrows, numcols, numbands) # Run once again because it is possible that there's another # Ellipsis object in the 2nd or 3rd position. @@ -1279,12 +1336,11 @@ class Jp2k(Jp2kBox): infile += b'0' * nelts dparam.infile = infile - if self.ignore_pclr_cmap_cdef: - # Return raw codestream components. - dparam.flags |= 1 + # Return raw codestream components instead of "interpolating" the + # colormap? + dparam.flags |= 1 if self.ignore_pclr_cmap_cdef else 0 dparam.decod_format = self._codec_format - dparam.cp_layer = self._layer # Must check the specified rlevel against the maximum. @@ -1315,10 +1371,6 @@ class Jp2k(Jp2kBox): dparam.tile_index = tile dparam.nb_tile_to_decode = 1 - if self.ignore_pclr_cmap_cdef: - # Return raw codestream components. - dparam.flags |= 1 - self._dparams = dparam def read_bands(self, rlevel=0, layer=None, area=None, tile=None, From 2d99154e582ea5211077fa86e73116a3b97cfac5 Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 26 Jan 2015 19:13:29 -0500 Subject: [PATCH 2/5] Use textwrap for indenting Use textwrap to replace hardcoding of spaces for indenting purposes. Closes-Issue: #318 --- glymur/jp2box.py | 427 ++++++++++++++++++++++------------- glymur/test/test_printing.py | 3 +- 2 files changed, 272 insertions(+), 158 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 98b0e6a..698711a 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -356,35 +356,45 @@ class ColourSpecificationBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg += '\n Method: {0}'.format(_METHOD_DISPLAY[self.method]) - msg += '\n Precedence: {0}'.format(self.precedence) + lst = [] + text = 'Method: {0}'.format(_METHOD_DISPLAY[self.method]) + lst.append(text) + text = 'Precedence: {0}'.format(self.precedence) + lst.append(text) if self.approximation is not 0: dispvalue = _APPROX_DISPLAY[self.approximation] - msg += '\n Approximation: {0}'.format(dispvalue) + text = 'Approximation: {0}'.format(dispvalue) + lst.append(text) if self.colorspace is not None: dispvalue = _COLORSPACE_MAP_DISPLAY[self.colorspace] - msg += '\n Colorspace: {0}'.format(dispvalue) + text = 'Colorspace: {0}'.format(dispvalue) else: # 2.7 has trouble pretty-printing ordered dicts so we just have # to print as a regular dict in this case. if self.icc_profile is None: - msg += '\n ICC Profile: None' + text = 'ICC Profile: None' else: if sys.hexversion < 0x03000000: icc_profile = dict(self.icc_profile) else: icc_profile = self.icc_profile - dispvalue = pprint.pformat(icc_profile) - lines = [' ' * 8 + y for y in dispvalue.split('\n')] - msg += '\n ICC Profile:\n{0}'.format('\n'.join(lines)) + text = pprint.pformat(icc_profile) + text = self._indent(text) + text = '\n'.join(['ICC Profile:', text]) - return msg + lst.append(text) + + text = '\n'.join(lst) + + text = '\n'.join([title, self._indent(text)]) + + return text def write(self, fptr): """Write an Colour Specification box to file. @@ -620,19 +630,26 @@ class ChannelDefinitionBox(Jp2kBox): self._dispatch_validation_error(msg, writing=writing) def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title + lst = [] for j in range(len(self.association)): color_type_string = _COLOR_TYPE_MAP_DISPLAY[self.channel_type[j]] if self.association[j] == 0: assn = 'whole image' else: assn = str(self.association[j]) - msg += '\n Channel {0} ({1}) ==> ({2})' - msg = msg.format(self.index[j], color_type_string, assn) - return msg + text = 'Channel {0} ({1}) ==> ({2})'.format(self.index[j], + color_type_string, + assn) + lst.append(text) + + text = '\n'.join(lst) + text = self._indent(text) + text = '\n'.join([title, text]) + return text def __repr__(self): msg = "glymur.jp2box.ChannelDefinitionBox(" @@ -929,19 +946,26 @@ class ComponentMappingBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title + lst = [] for k in range(len(self.component_index)): if self.mapping_type[k] == 1: - msg += '\n Component {0} ==> palette column {1}' - msg = msg.format(self.component_index[k], - self.palette_index[k]) + text = 'Component {0} ==> palette column {1}' + text = text.format(self.component_index[k], + self.palette_index[k]) else: - msg += '\n Component {0} ==> {1}' - msg = msg.format(self.component_index[k], k) - return msg + text = 'Component {0} ==> {1}' + text = text.format(self.component_index[k], k) + lst.append(text) + + text = '\n'.join(lst) + text = self._indent(text) + text = '\n'.join([title, text]) + + return text def write(self, fptr): """Write a Component Mapping box to file. @@ -1041,16 +1065,20 @@ class ContiguousCodestreamBox(Jp2kBox): return msg.format(repr(self.codestream)) def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title if _printoptions['codestream'] is False: - return msg + return title + lst = [] for segment in self.codestream.segment: - msg += '\n' + self._indent(str(segment), indent_level=4) + lst.append(str(segment)) - return msg + text = '\n'.join(lst) + text = self._indent(text) + text = '\n'.join([title, text]) + return text @classmethod def parse(cls, fptr, offset=0, length=0): @@ -1149,13 +1177,18 @@ class DataReferenceBox(Jp2kBox): fptr.seek(end_pos) def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title + lst = [] for box in self.DR: - msg += '\n ' + str(box) - return msg + lst.append(str(box)) + text = '\n'.join(lst) + text = self._indent(text) + + text = '\n'.join([title, text]) + return text def __repr__(self): msg = 'glymur.jp2box.DataReferenceBox()' @@ -1252,17 +1285,22 @@ class FileTypeBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - lst = [msg, - ' Brand: {0}', - ' Compatibility: {1}'] - msg = '\n'.join(lst) - msg = msg.format(self.brand, self.compatibility_list) + lst = [] + text = 'Brand: {0}'.format(self.brand) + lst.append(text) + text = 'Compatibility: {0}'.format(self.compatibility_list) + lst.append(text) - return msg + text = '\n'.join(lst) + text = self._indent(text) + + text = '\n'.join([title, text]) + + return text def _validate(self, writing=False): """Validate the box before writing to file.""" @@ -1384,19 +1422,24 @@ class FragmentListBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title + lst = [] for j in range(len(self.fragment_offset)): - msg += "\n Offset {0}: {1}" - msg += "\n Fragment Length {2}: {3}" - msg += "\n Data Reference {4}: {5}" - msg = msg.format(j, self.fragment_offset[j], - j, self.fragment_length[j], - j, self.data_reference[j]) + text = "Offset {0}: {1}".format(j, self.fragment_offset[j]) + lst.append(text) + text = "Fragment Length {0}: {1}".format(j, + self.fragment_length[j]) + lst.append(text) + text = "Data Reference {0}: {1}".format(j, self.data_reference[j]) + lst.append(text) - return msg + text = '\n'.join(lst) + text = self._indent(text) + text = '\n'.join([title, text]) + return text def write(self, fptr): """Write a fragment list box to file. @@ -1546,11 +1589,7 @@ class FreeBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) - if _printoptions['short'] is True: - return msg - - return msg + return Jp2kBox.__str__(self) @classmethod def parse(cls, fptr, offset, length): @@ -1642,23 +1681,34 @@ class ImageHeaderBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg = "{0}" - msg += '\n Size: [{1} {2} {3}]' - msg += '\n Bitdepth: {4}' - msg += '\n Signed: {5}' - msg += '\n Compression: {6}' - msg += '\n Colorspace Unknown: {7}' - msg = msg.format(Jp2kBox.__str__(self), - self.height, self.width, self.num_components, - self.bits_per_component, - self.signed, - 'wavelet' if self.compression == 7 else 'unknown', - self.colorspace_unknown) - return msg + lst = [] + + text = 'Size: [{0} {1} {2}]' + text = text.format(self.height, self.width, self.num_components) + lst.append(text) + + text = 'Bitdepth: {0}'.format(self.bits_per_component) + lst.append(text) + + text = 'Signed: {0}'.format(self.signed) + lst.append(text) + + text = 'Compression: {0}' + text = text.format('wavelet' if self.compression == 7 else 'unknown') + lst.append(text) + + text = 'Colorspace Unknown: {0}'.format(self.colorspace_unknown) + lst.append(text) + + text = '\n'.join(lst) + text = self._indent(text) + text = '\n'.join([title, text]) + + return text def write(self, fptr): """Write an Image Header box to file. @@ -1873,14 +1923,16 @@ class JPEG2000SignatureBox(Jp2kBox): return 'glymur.jp2box.JPEG2000SignatureBox()' def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg += '\n Signature: {0:02x}{1:02x}{2:02x}{3:02x}' - msg = msg.format(self.signature[0], self.signature[1], - self.signature[2], self.signature[3]) - return msg + body = 'Signature: {0:02x}{1:02x}{2:02x}{3:02x}' + body = body.format(self.signature[0], self.signature[1], + self.signature[2], self.signature[3]) + body = self._indent(body) + text = '\n'.join([title, body]) + return text def write(self, fptr): """Write a JPEG 2000 Signature box to file. @@ -1962,12 +2014,15 @@ class PaletteBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg += '\n Size: ({0} x {1})'.format(*self.palette.shape) - return msg + body = 'Size: ({0} x {1})'.format(*self.palette.shape) + body = self._indent(body) + + text = '\n'.join([title, body]) + return text def write(self, fptr): """Write a Palette box to file. @@ -2211,25 +2266,47 @@ class ReaderRequirementsBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg += '\n Fully Understands Aspect Mask: 0x{0:x}' - msg = msg.format(self.fuam) - msg += '\n Display Completely Mask: 0x{0:x}'.format(self.dcm) + lst = [] - msg += '\n Standard Features and Masks:' + text = 'Fully Understands Aspect Mask: 0x{0:x}'.format(self.fuam) + lst.append(text) + + text = 'Display Completely Mask: 0x{0:x}'.format(self.dcm) + lst.append(text) + + text = 'Standard Features and Masks:' + lst.append(text) + + lst2 = [] for j in range(len(self.standard_flag)): args = (self.standard_flag[j], self.standard_mask[j], _READER_REQUIREMENTS_DISPLAY[self.standard_flag[j]]) - msg += '\n Feature {0:03d}: 0x{1:x} {2}'.format(*args) + text = 'Feature {0:03d}: 0x{1:x} {2}'.format(*args) + lst2.append(text) + text = '\n'.join(lst2) + text = self._indent(text) + lst.append(text) - msg += '\n Vendor Features:' + text = 'Vendor Features:' + lst.append(text) + + lst2 = [] for j in range(len(self.vendor_feature)): - msg += '\n UUID {0}'.format(self.vendor_feature[j]) + text = 'UUID {0}'.format(self.vendor_feature[j]) + lst2.append(text) + text = '\n'.join(lst2) + text = self._indent(text) + lst.append(text) - return msg + text = '\n'.join(lst) + text = self._indent(text) + text = '\n'.join([title, text]) + + return text @classmethod def parse(cls, fptr, offset, length): @@ -2500,13 +2577,21 @@ class CaptureResolutionBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg += '\n VCR: {0}'.format(self.vertical_resolution) - msg += '\n HCR: {0}'.format(self.horizontal_resolution) - return msg + lst = [] + text = 'VCR: {0}'.format(self.vertical_resolution) + lst.append(text) + text = 'HCR: {0}'.format(self.horizontal_resolution) + lst.append(text) + + text = '\n'.join(lst) + body = self._indent(text) + + text = '\n'.join([title, body]) + return text @classmethod def parse(cls, fptr, offset, length): @@ -2566,13 +2651,21 @@ class DisplayResolutionBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg += '\n VDR: {0}'.format(self.vertical_resolution) - msg += '\n HDR: {0}'.format(self.horizontal_resolution) - return msg + lst = [] + text = 'VDR: {0}'.format(self.vertical_resolution) + lst.append(text) + text = 'HDR: {0}'.format(self.horizontal_resolution) + lst.append(text) + + text = '\n'.join(lst) + body = self._indent(text) + + text = '\n'.join([title, body]) + return text @classmethod def parse(cls, fptr, offset, length): @@ -2626,12 +2719,15 @@ class LabelBox(Jp2kBox): self.offset = offset def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg += '\n Label: {0}'.format(self.label) - return msg + text = 'Label: {0}'.format(self.label) + body = self._indent(text) + + text = '\n'.join([title, body]) + return text def __repr__(self): msg = 'glymur.jp2box.LabelBox("{0}")'.format(self.label) @@ -2694,25 +2790,30 @@ class NumberListBox(Jp2kBox): self.offset = offset def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title + lst = [] for j, association in enumerate(self.associations): - msg += '\n Association[{0}]: '.format(j) + text = 'Association[{0}]: '.format(j) if association == 0: - msg += 'the rendered result' + text += 'the rendered result' elif (association >> 24) == 1: idx = association & 0x00FFFFFF - msg += 'codestream {0}' - msg = msg.format(idx) + text += 'codestream {0}'.format(idx) elif (association >> 24) == 2: idx = association & 0x00FFFFFF - msg += 'compositing layer {0}' - msg = msg.format(idx) + text += 'compositing layer {0}'.format(idx) else: - msg += 'unrecognized' - return msg + text += 'unrecognized' + lst.append(text) + + body = '\n'.join(lst) + body = self._indent(body) + + text = '\n'.join([title, body]) + return text def __repr__(self): msg = 'glymur.jp2box.NumberListBox(associations={0})' @@ -2797,21 +2898,22 @@ class XMLBox(Jp2kBox): return "glymur.jp2box.XMLBox(xml={0})".format(self.xml) def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title if _printoptions['xml'] is False: - return msg + return title - msg += '\n' if self.xml is not None: - xmlstring = ET.tostring(self.xml, - encoding='utf-8', - pretty_print=True).decode('utf-8') + body = ET.tostring(self.xml, + encoding='utf-8', + pretty_print=True).decode('utf-8') else: - xmlstring = 'None' - msg += self._indent(xmlstring) - return msg + body = 'None' + body = self._indent(body) + + text = '\n'.join([title, body]) + return text def write(self, fptr): """Write an XML box to file. @@ -2918,13 +3020,19 @@ class UUIDListBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title + lst = [] for j, uuid_item in enumerate(self.ulst): - msg += '\n UUID[{0}]: {1}'.format(j, uuid_item) - return msg + text = 'UUID[{0}]: {1}'.format(j, uuid_item) + lst.append(text) + body = '\n'.join(lst) + body = self._indent(body) + + text = '\n'.join([title, body]) + return text @classmethod def parse(cls, fptr, offset, length): @@ -3070,20 +3178,21 @@ class DataEntryURLBox(Jp2kBox): return msg def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg += '\n ' + lst = ['Version: {0}', + 'Flag: {1} {2} {3}', + 'URL: "{4}"'] + body = '\n'.join(lst) + body = body.format(self.version, + self.flag[0], self.flag[1], self.flag[2], + self.url) + body = self._indent(body) - lines = ['Version: {0}', - 'Flag: {1} {2} {3}', - 'URL: "{4}"'] - msg += '\n '.join(lines) - msg = msg.format(self.version, - self.flag[0], self.flag[1], self.flag[2], - self.url) - return msg + text = '\n'.join([title, body]) + return text @classmethod def parse(cls, fptr, offset, length): @@ -3222,38 +3331,44 @@ class UUIDBox(Jp2kBox): return msg.format(repr(self.uuid), len(self.raw_data)) def __str__(self): - msg = Jp2kBox.__str__(self) + title = Jp2kBox.__str__(self) if _printoptions['short'] is True: - return msg + return title - msg = '{0}\n UUID: {1}'.format(msg, self.uuid) + text = 'UUID: {0}'.format(self.uuid) if self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - msg += ' (XMP)' + text += ' (XMP)' elif self.uuid.bytes == b'JpgTiffExif->JP2': - msg += ' (EXIF)' + text += ' (EXIF)' else: - msg += ' (unknown)' + text += ' (unknown)' + + lst = [text] if (((_printoptions['xml'] is False) and (self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')))): # If it's an XMP UUID, don't print the XML contents. - return msg + pass - if self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - line = '\n UUID Data:\n{0}' + elif self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): + line = 'UUID Data:\n{0}' xmlstring = ET.tostring(self.data, encoding='utf-8', - pretty_print=True).decode('utf-8') - # indent it a bit - xmlstring = self._indent(xmlstring.rstrip()) - msg += line.format(xmlstring) + pretty_print=True).decode('utf-8').rstrip() + text = line.format(xmlstring) + lst.append(text) elif self.uuid.bytes == b'JpgTiffExif->JP2': - msg += '\n UUID Data: {0}'.format(str(self.data)) + text = 'UUID Data: {0}'.format(str(self.data)) + lst.append(text) else: - line = '\n UUID Data: {0} bytes' - msg += line.format(len(self.raw_data)) + text = 'UUID Data: {0} bytes'.format(len(self.raw_data)) + lst.append(text) - return msg + body = '\n'.join(lst) + body = self._indent(body) + + text = '\n'.join([title, body]) + return text def write(self, fptr): """Write a UUID box to file. diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index bccaa00..ba5dd09 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -1088,8 +1088,6 @@ class TestJp2dump(unittest.TestCase): def test_jp2_codestream_2(self): """Verify dumping with -c 2, print entire jp2 jacket, codestream.""" actual = self.run_jp2dump(['', '-c', '2', self.jp2file]) - - # shave off the non-main-header segments expected = fixtures.nemo self.assertEqual(actual, expected) @@ -1125,6 +1123,7 @@ class TestJp2dump(unittest.TestCase): def test_suppress_xml(self): """Verify dumping with -x, suppress XML.""" + self.maxDiff = None actual = self.run_jp2dump(['', '-x', self.jp2file]) # shave off the XML and non-main-header segments From 858ecbbac808dc73d6be2d109fbf88185a46fe7b Mon Sep 17 00:00:00 2001 From: jevans Date: Wed, 18 Mar 2015 20:30:50 -0400 Subject: [PATCH 3/5] Refactor image extraction routines Able to combine nearly identical routines for extract imagery from openjpeg data structures regardless of whether the image is ycbcr or not. Closes-Issue: #320 --- glymur/jp2k.py | 147 ++++++++++++++++++++++++------------------------- 1 file changed, 73 insertions(+), 74 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 3633657..d28b0f7 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -1167,7 +1167,7 @@ class Jp2k(Jp2kBox): Returns ------- - img_array : ndarray + image : ndarray The image data. Raises @@ -1203,21 +1203,21 @@ class Jp2k(Jp2kBox): src = fptr.read() cio = opj.cio_open(dinfo, src) - image = opj.decode(dinfo, cio) + raw_image = opj.decode(dinfo, cio) - stack.callback(opj.image_destroy, image) + stack.callback(opj.image_destroy, raw_image) stack.callback(opj.destroy_decompress, dinfo) stack.callback(opj.cio_close, cio) - data = extract_image_cube(image) + image = self._extract_image(raw_image) except ValueError: opj2.check_error(0) - if data.shape[2] == 1: + if image.shape[2] == 1: # The third dimension has just a single layer. Make the image # data 2D instead of 3D. - data.shape = data.shape[0:2] + image.shape = image.shape[0:2] if area is not None: x0, y0, x1, y1 = area @@ -1229,9 +1229,9 @@ class Jp2k(Jp2kBox): area = [int(round(float(x)/extent + 2 ** -20)) for x in area] rows = slice(area[0], area[2], None) cols = slice(area[1], area[3], None) - data = data[rows, cols] + image = image[rows, cols] - return data + return image def _read_openjp2(self, rlevel=0, layer=None, area=None, tile=None, verbose=False): @@ -1254,7 +1254,7 @@ class Jp2k(Jp2kBox): Returns ------- - img_array : ndarray + image : ndarray The image data. Raises @@ -1291,26 +1291,26 @@ class Jp2k(Jp2kBox): opj2.set_info_handler(codec, None) opj2.setup_decoder(codec, self._dparams) - image = opj2.read_header(stream, codec) - stack.callback(opj2.image_destroy, image) + raw_image = opj2.read_header(stream, codec) + stack.callback(opj2.image_destroy, raw_image) if self._dparams.nb_tile_to_decode: - opj2.get_decoded_tile(codec, stream, image, + opj2.get_decoded_tile(codec, stream, raw_image, self._dparams.tile_index) else: - opj2.set_decode_area(codec, image, + opj2.set_decode_area(codec, raw_image, self._dparams.DA_x0, self._dparams.DA_y0, self._dparams.DA_x1, self._dparams.DA_y1) - opj2.decode(codec, stream, image) + opj2.decode(codec, stream, raw_image) opj2.end_decompress(codec, stream) - img_array = extract_image_cube(image) + image = self._extract_image(raw_image) - if img_array.shape[2] == 1: - img_array.shape = img_array.shape[0:2] + if image.shape[2] == 1: + image.shape = image.shape[0:2] - return img_array + return image def _populate_dparams(self, rlevel, tile=None, area=None): """Populate decompression structure with appropriate input parameters. @@ -1459,10 +1459,64 @@ class Jp2k(Jp2kBox): opj2.decode(codec, stream, image) opj2.end_decompress(codec, stream) - lst = extract_image_bands(image) + lst = self._extract_image(image) return lst + def _extract_image(self, raw_image): + """ + Extract unequally-sized image bands. + + Parameters + ---------- + raw_image : reference to openjpeg ImageType instance + The image structure initialized with image characteristics. + + Returns + ------- + image : list or numpy array + If the JPEG 2000 image has unequally-sized images, they are + extracted into a list, otherwise a numpy array. + + """ + ncomps = raw_image.contents.numcomps + + # Make a pass thru the image, see if any of the band datatypes or + # dimensions differ. + dtypes, nrows, ncols = [], [], [] + for k in range(raw_image.contents.numcomps): + component = raw_image.contents.comps[k] + dtypes.append(_component2dtype(component)) + nrows.append(component.h) + ncols.append(component.w) + is_cube = all(r == nrows[0] and c == ncols[0] and d == dtypes[0] + for r, c, d in zip(nrows, ncols, dtypes)) + + if is_cube: + image = np.zeros((nrows[0], ncols[0], ncomps), dtypes[0]) + else: + image = [] + + for k in range(raw_image.contents.numcomps): + component = raw_image.contents.comps[k] + + _validate_nonzero_image_size(nrows[k], ncols[k], k) + + addr = ctypes.addressof(component.data.contents) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + nelts = nrows[k] * ncols[k] + band = np.ctypeslib.as_array( + (ctypes.c_int32 * nelts).from_address(addr)) + if is_cube: + image[:, :, k] = np.reshape(band.astype(dtypes[k]), + (nrows[k], ncols[k])) + else: + image.append(np.reshape(band.astype(dtypes[k]), + (nrows[k], ncols[k]))) + + return image + def get_codestream(self, header_only=True): """Returns a codestream object. @@ -1893,61 +1947,6 @@ def _validate_label(boxes): _validate_label(box.box) -def extract_image_cube(image): - """Extract 3D image from openjpeg data structure. - """ - ncomps = image.contents.numcomps - component = image.contents.comps[0] - dtype = _component2dtype(component) - - nrows = component.h - ncols = component.w - data = np.zeros((nrows, ncols, ncomps), dtype) - - for k in range(image.contents.numcomps): - component = image.contents.comps[k] - nrows = component.h - ncols = component.w - - _validate_nonzero_image_size(nrows, ncols, k) - - addr = ctypes.addressof(component.data.contents) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - nelts = nrows * ncols - band = np.ctypeslib.as_array( - (ctypes.c_int32 * nelts).from_address(addr)) - data[:, :, k] = np.reshape(band.astype(dtype), (nrows, ncols)) - - return data - - -def extract_image_bands(image): - """Extract unequally-sized image bands. - - This routine need only be called when subsampling differs across image - components, such as is often the case with YCbCr imagery. - """ - data = [] - for k in range(image.contents.numcomps): - component = image.contents.comps[k] - - dtype = _component2dtype(component) - nrows = component.h - ncols = component.w - - _validate_nonzero_image_size(nrows, ncols, k) - - addr = ctypes.addressof(component.data.contents) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - band = np.ctypeslib.as_array( - (ctypes.c_int32 * nrows * ncols).from_address(addr)) - data.append(np.reshape(band.astype(dtype), (nrows, ncols))) - - return data - - # Setup the default callback handlers. See the callback functions subsection # in the ctypes section of the Python documentation for a solid explanation of # what's going on here. From d7a3c4df275d3ff78e24db34e8f8a7ad16984a5d Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 19 Mar 2015 18:50:00 -0400 Subject: [PATCH 4/5] Refactor box validation when wrapping These became functions only at the behest of pylint. I don't really agree with that justification anymore, and flake8 is perfectly happy with them as methods, so back to methods they go. --- glymur/jp2k.py | 544 ++++++++++++++++++++++++------------------------- 1 file changed, 265 insertions(+), 279 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index d28b0f7..56a3438 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -840,7 +840,7 @@ class Jp2k(Jp2kBox): if boxes is None: boxes = self._get_default_jp2_boxes() - _validate_jp2_box_sequence(boxes) + self._validate_jp2_box_sequence(boxes) with open(filename, 'wb') as ofile: for box in boxes: @@ -1486,7 +1486,7 @@ class Jp2k(Jp2kBox): dtypes, nrows, ncols = [], [], [] for k in range(raw_image.contents.numcomps): component = raw_image.contents.comps[k] - dtypes.append(_component2dtype(component)) + dtypes.append(self._component2dtype(component)) nrows.append(component.h) ncols.append(component.w) is_cube = all(r == nrows[0] and c == ncols[0] and d == dtypes[0] @@ -1500,7 +1500,7 @@ class Jp2k(Jp2kBox): for k in range(raw_image.contents.numcomps): component = raw_image.contents.comps[k] - _validate_nonzero_image_size(nrows[k], ncols[k], k) + self._validate_nonzero_image_size(nrows[k], ncols[k], k) addr = ctypes.addressof(component.data.contents) with warnings.catch_warnings(): @@ -1517,6 +1517,38 @@ class Jp2k(Jp2kBox): return image + def _component2dtype(self, component): + """Determin the appropriate numpy datatype for an OpenJPEG component. + + Parameters + ---------- + component : ctypes pointer to ImageCompType (image_comp_t) + single image component structure. + + Returns + ------- + dtype : builtins.type + numpy datatype to be used to construct an image array. + """ + if component.sgnd: + if component.prec <= 8: + dtype = np.int8 + elif component.prec <= 16: + dtype = np.int16 + else: + msg = "Unhandled precision: {0} bits.".format(component.prec) + raise RuntimeError(msg) + else: + if component.prec <= 8: + dtype = np.uint8 + elif component.prec <= 16: + dtype = np.uint16 + else: + msg = "Unhandled precision: {0} bits.".format(component.prec) + raise RuntimeError(msg) + + return dtype + def get_codestream(self, header_only=True): """Returns a codestream object. @@ -1645,306 +1677,260 @@ class Jp2k(Jp2kBox): self._comptparms = comptparms - -def _component2dtype(component): - """Take an OpenJPEG component structure and determine the numpy datatype. - - Parameters - ---------- - component : ctypes pointer to ImageCompType (image_comp_t) - single image component structure. - - Returns - ------- - dtype : builtins.type - numpy datatype to be used to construct an image array. - """ - if component.sgnd: - if component.prec <= 8: - dtype = np.int8 - elif component.prec <= 16: - dtype = np.int16 - else: - raise RuntimeError("Unhandled precision, datatype") - else: - if component.prec <= 8: - dtype = np.uint8 - elif component.prec <= 16: - dtype = np.uint16 - else: - raise RuntimeError("Unhandled precision, datatype") - - return dtype - - -def _validate_nonzero_image_size(nrows, ncols, component_index): - """The image cannot have area of zero. - """ - if nrows == 0 or ncols == 0: - # Letting this situation continue would segfault Python. - msg = "Component {0} has dimensions {1} x {2}" - msg = msg.format(component_index, nrows, ncols) - raise IOError(msg) - - -JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ', - 'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ', - 'uuid'] - - -def _validate_jp2_box_sequence(boxes): - """Run through series of tests for JP2 box legality. - - This is non-exhaustive. - """ - _validate_signature_compatibility(boxes) - _validate_jp2h(boxes) - _validate_jp2c(boxes) - if boxes[1].brand == 'jpx ': - _validate_jpx_box_sequence(boxes) - else: - # Validate the JP2 box IDs. - count = _collect_box_count(boxes) - for box_id in count.keys(): - if box_id not in JP2_IDS: - msg = "The presence of a '{0}' box requires that the file " - msg += "type brand be set to 'jpx '." - raise IOError(msg.format(box_id)) - - _validate_jp2_colr(boxes) - - -def _validate_jp2_colr(boxes): - """ - Validate JP2 requirements on colour specification boxes. - """ - lst = [box for box in boxes if box.box_id == 'jp2h'] - jp2h = lst[0] - for colr in [box for box in jp2h.box if box.box_id == 'colr']: - if colr.approximation != 0: - msg = "A JP2 colr box cannot have a non-zero approximation field." + def _validate_nonzero_image_size(self, nrows, ncols, component_index): + """The image cannot have area of zero. + """ + if nrows == 0 or ncols == 0: + # Letting this situation continue would segfault openjpeg. + msg = "Component {0} has dimensions {1} x {2}" + msg = msg.format(component_index, nrows, ncols) raise IOError(msg) + def _validate_jp2_box_sequence(self, boxes): + """Run through series of tests for JP2 box legality. -def _validate_jpx_box_sequence(boxes): - """Run through series of tests for JPX box legality.""" - _validate_label(boxes) - _validate_jpx_brand(boxes, boxes[1].brand) - _validate_jpx_compatibility(boxes, boxes[1].compatibility_list) - _validate_singletons(boxes) - _validate_top_level(boxes) + This is non-exhaustive. + """ + JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', + 'jP ', 'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', + 'uinf', 'url ', 'uuid'] + self._validate_signature_compatibility(boxes) + self._validate_jp2h(boxes) + self._validate_jp2c(boxes) + if boxes[1].brand == 'jpx ': + self._validate_jpx_box_sequence(boxes) + else: + # Validate the JP2 box IDs. + count = self._collect_box_count(boxes) + for box_id in count.keys(): + if box_id not in JP2_IDS: + msg = "The presence of a '{0}' box requires that the file " + msg += "type brand be set to 'jpx '." + raise IOError(msg.format(box_id)) -def _validate_signature_compatibility(boxes): - """Validate the file signature and compatibility status.""" - # Check for a bad sequence of boxes. - # 1st two boxes must be 'jP ' and 'ftyp' - if boxes[0].box_id != 'jP ' or boxes[1].box_id != 'ftyp': - msg = "The first box must be the signature box and the second " - msg += "must be the file type box." - raise IOError(msg) + self._validate_jp2_colr(boxes) - # The compatibility list must contain at a minimum 'jp2 '. - if 'jp2 ' not in boxes[1].compatibility_list: - msg = "The ftyp box must contain 'jp2 ' in the compatibility list." - raise IOError(msg) - - -def _validate_jp2c(boxes): - """Validate the codestream box in relation to other boxes.""" - # jp2c must be preceeded by jp2h - jp2h_lst = [idx for (idx, box) in enumerate(boxes) - if box.box_id == 'jp2h'] - jp2h_idx = jp2h_lst[0] - jp2c_lst = [idx for (idx, box) in enumerate(boxes) - if box.box_id == 'jp2c'] - if len(jp2c_lst) == 0: - msg = "A codestream box must be defined in the outermost " - msg += "list of boxes." - raise IOError(msg) - - jp2c_idx = jp2c_lst[0] - if jp2h_idx >= jp2c_idx: - msg = "The codestream box must be preceeded by a jp2 header box." - raise IOError(msg) - - -def _validate_jp2h(boxes): - """Validate the JP2 Header box.""" - _check_jp2h_child_boxes(boxes, 'top-level') - - jp2h_lst = [box for box in boxes if box.box_id == 'jp2h'] - jp2h = jp2h_lst[0] - - # 1st jp2 header box cannot be empty. - if len(jp2h.box) == 0: - msg = "The JP2 header superbox cannot be empty." - raise IOError(msg) - - # 1st jp2 header box must be ihdr - if jp2h.box[0].box_id != 'ihdr': - msg = "The first box in the jp2 header box must be the image " - msg += "header box." - raise IOError(msg) - - # colr must be present in jp2 header box. - colr_lst = [j for (j, box) in enumerate(jp2h.box) - if box.box_id == 'colr'] - if len(colr_lst) == 0: - msg = "The jp2 header box must contain a color definition box." - raise IOError(msg) - colr = jp2h.box[colr_lst[0]] - - _validate_channel_definition(jp2h, colr) - - -def _validate_channel_definition(jp2h, colr): - """Validate the channel definition box.""" - cdef_lst = [j for (j, box) in enumerate(jp2h.box) if box.box_id == 'cdef'] - if len(cdef_lst) > 1: - msg = "Only one channel definition box is allowed in the " - msg += "JP2 header." - raise IOError(msg) - elif len(cdef_lst) == 1: - cdef = jp2h.box[cdef_lst[0]] - if colr.colorspace == core.SRGB: - if any([chan + 1 not in cdef.association - or cdef.channel_type[chan] != 0 - for chan in [0, 1, 2]]): - msg = "All color channels must be defined in the " - msg += "channel definition box." - raise IOError(msg) - elif colr.colorspace == core.GREYSCALE: - if 0 not in cdef.channel_type: - msg = "All color channels must be defined in the " - msg += "channel definition box." + def _validate_jp2_colr(self, boxes): + """ + Validate JP2 requirements on colour specification boxes. + """ + lst = [box for box in boxes if box.box_id == 'jp2h'] + jp2h = lst[0] + for colr in [box for box in jp2h.box if box.box_id == 'colr']: + if colr.approximation != 0: + msg = "A JP2 colr box cannot have a non-zero approximation " + msg += "field." raise IOError(msg) + def _validate_jpx_box_sequence(self, boxes): + """Run through series of tests for JPX box legality.""" + self._validate_label(boxes) + self._validate_jpx_brand(boxes, boxes[1].brand) + self._validate_jpx_compatibility(boxes, boxes[1].compatibility_list) + self._validate_singletons(boxes) + self._validate_top_level(boxes) -JP2H_CHILDREN = set(['bpcc', 'cdef', 'cmap', 'ihdr', 'pclr']) + def _validate_signature_compatibility(self, boxes): + """Validate the file signature and compatibility status.""" + # Check for a bad sequence of boxes. + # 1st two boxes must be 'jP ' and 'ftyp' + if boxes[0].box_id != 'jP ' or boxes[1].box_id != 'ftyp': + msg = "The first box must be the signature box and the second " + msg += "must be the file type box." + raise IOError(msg) + # The compatibility list must contain at a minimum 'jp2 '. + if 'jp2 ' not in boxes[1].compatibility_list: + msg = "The ftyp box must contain 'jp2 ' in the compatibility list." + raise IOError(msg) -def _check_jp2h_child_boxes(boxes, parent_box_name): - """Certain boxes can only reside in the JP2 header.""" - box_ids = set([box.box_id for box in boxes]) - intersection = box_ids.intersection(JP2H_CHILDREN) - if len(intersection) > 0 and parent_box_name not in ['jp2h', 'jpch']: - msg = "A '{0}' box can only be nested in a JP2 header box." - raise IOError(msg.format(list(intersection)[0])) + def _validate_jp2c(self, boxes): + """Validate the codestream box in relation to other boxes.""" + # jp2c must be preceeded by jp2h + jp2h_lst = [idx for (idx, box) in enumerate(boxes) + if box.box_id == 'jp2h'] + jp2h_idx = jp2h_lst[0] + jp2c_lst = [idx for (idx, box) in enumerate(boxes) + if box.box_id == 'jp2c'] + if len(jp2c_lst) == 0: + msg = "A codestream box must be defined in the outermost " + msg += "list of boxes." + raise IOError(msg) - # Recursively check any contained superboxes. - for box in boxes: - if hasattr(box, 'box'): - _check_jp2h_child_boxes(box.box, box.box_id) + jp2c_idx = jp2c_lst[0] + if jp2h_idx >= jp2c_idx: + msg = "The codestream box must be preceeded by a jp2 header box." + raise IOError(msg) + def _validate_jp2h(self, boxes): + """Validate the JP2 Header box.""" + self._check_jp2h_child_boxes(boxes, 'top-level') -def _collect_box_count(boxes): - """Count the occurences of each box type.""" - count = Counter([box.box_id for box in boxes]) + jp2h_lst = [box for box in boxes if box.box_id == 'jp2h'] + jp2h = jp2h_lst[0] - # Add the counts in the superboxes. - for box in boxes: - if hasattr(box, 'box'): - count.update(_collect_box_count(box.box)) + # 1st jp2 header box cannot be empty. + if len(jp2h.box) == 0: + msg = "The JP2 header superbox cannot be empty." + raise IOError(msg) - return count + # 1st jp2 header box must be ihdr + if jp2h.box[0].box_id != 'ihdr': + msg = "The first box in the jp2 header box must be the image " + msg += "header box." + raise IOError(msg) -TOP_LEVEL_ONLY_BOXES = set(['dtbl']) + # colr must be present in jp2 header box. + colr_lst = [j for (j, box) in enumerate(jp2h.box) + if box.box_id == 'colr'] + if len(colr_lst) == 0: + msg = "The jp2 header box must contain a color definition box." + raise IOError(msg) + colr = jp2h.box[colr_lst[0]] + self._validate_channel_definition(jp2h, colr) -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])) + def _validate_channel_definition(self, jp2h, colr): + """Validate the channel definition box.""" + cdef_lst = [j for (j, box) in enumerate(jp2h.box) + if box.box_id == 'cdef'] + if len(cdef_lst) > 1: + msg = "Only one channel definition box is allowed in the " + msg += "JP2 header." + raise IOError(msg) + elif len(cdef_lst) == 1: + cdef = jp2h.box[cdef_lst[0]] + if colr.colorspace == core.SRGB: + if any([chan + 1 not in cdef.association + or cdef.channel_type[chan] != 0 + for chan in [0, 1, 2]]): + msg = "All color channels must be defined in the " + msg += "channel definition box." + raise IOError(msg) + elif colr.colorspace == core.GREYSCALE: + if 0 not in cdef.channel_type: + msg = "All color channels must be defined in the " + msg += "channel definition box." + raise IOError(msg) - # Recursively check any contained superboxes. - for box in boxes: - if hasattr(box, 'box'): - _check_superbox_for_top_levels(box.box) + def _check_jp2h_child_boxes(self, boxes, parent_box_name): + """Certain boxes can only reside in the JP2 header.""" + JP2H_CHILDREN = set(['bpcc', 'cdef', 'cmap', 'ihdr', 'pclr']) + box_ids = set([box.box_id for box in boxes]) + intersection = box_ids.intersection(JP2H_CHILDREN) + if len(intersection) > 0 and parent_box_name not in ['jp2h', 'jpch']: + msg = "A '{0}' box can only be nested in a JP2 header box." + raise IOError(msg.format(list(intersection)[0])) -def _validate_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.') - - # If there is one data reference box, then there must also be one ftbl. - if 'dtbl' in count and 'ftbl' not in count: - msg = 'The presence of a data reference box requires the presence of ' - msg += 'a fragment table box as well.' - raise IOError(msg) - - -def _validate_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.') - -JPX_IDS = ['asoc', 'nlst'] - - -def _validate_jpx_brand(boxes, brand): - """ - If there is a JPX box then the brand must be 'jpx '. - """ - for box in boxes: - if box.box_id in JPX_IDS: - if brand != 'jpx ': - msg = "A JPX box requires that the file type box brand be " - msg += "'jpx '." - raise RuntimeError(msg) - if hasattr(box, 'box') != 0: - # Same set of checks on any child boxes. - _validate_jpx_brand(box.box, brand) - - -def _validate_jpx_compatibility(boxes, compatibility_list): - """ - If there is a JPX box then the compatibility list must also contain 'jpx '. - """ - jpx_cl = set(compatibility_list) - for box in boxes: - if box.box_id in JPX_IDS: - if len(set(['jpx ', 'jpxb']).intersection(jpx_cl)) == 0: - msg = "A JPX box requires that either 'jpx ' or 'jpxb' be " - msg += "present in the ftype compatibility list." - raise RuntimeError(msg) - if hasattr(box, 'box') != 0: - # Same set of checks on any child boxes. - _validate_jpx_compatibility(box.box, compatibility_list) - - -def _validate_label(boxes): - """ - Label boxes can only be inside association, codestream headers, or - compositing layer header boxes. - """ - for box in boxes: - if box.box_id != 'asoc': + # Recursively check any contained superboxes. + for box in boxes: if hasattr(box, 'box'): - for boxi in box.box: - if boxi.box_id == 'lbl ': - msg = "A label box cannot be nested inside a {0} box." - msg = msg.format(box.box_id) - raise IOError(msg) + self._check_jp2h_child_boxes(box.box, box.box_id) + + def _collect_box_count(self, 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(self._collect_box_count(box.box)) + + return count + + def _check_superbox_for_top_levels(self, 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. + TOP_LEVEL_ONLY_BOXES = set(['dtbl']) + 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'): + self._check_superbox_for_top_levels(box.box) + + def _validate_top_level(self, boxes): + """Several boxes can only occur at the top level.""" + # Add the counts in the superboxes. + for box in boxes: + if hasattr(box, 'box'): + self._check_superbox_for_top_levels(box.box) + + count = self._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.') + + # If there is one data reference box, then there must also be one ftbl. + if 'dtbl' in count and 'ftbl' not in count: + msg = 'The presence of a data reference box requires the presence ' + msg += 'of a fragment table box as well.' + raise IOError(msg) + + def _validate_singletons(self, boxes): + """Several boxes can only occur once.""" + count = self._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 _validate_jpx_brand(self, boxes, brand): + """ + If there is a JPX box then the brand must be 'jpx '. + """ + JPX_IDS = ['asoc', 'nlst'] + for box in boxes: + if box.box_id in JPX_IDS: + if brand != 'jpx ': + msg = "A JPX box requires that the file type box brand be " + msg += "'jpx '." + raise RuntimeError(msg) + if hasattr(box, 'box') != 0: # Same set of checks on any child boxes. - _validate_label(box.box) + self._validate_jpx_brand(box.box, brand) + + def _validate_jpx_compatibility(self, boxes, compatibility_list): + """ + If there is a JPX box then the compatibility list must also contain + 'jpx '. + """ + JPX_IDS = ['asoc', 'nlst'] + jpx_cl = set(compatibility_list) + for box in boxes: + if box.box_id in JPX_IDS: + if len(set(['jpx ', 'jpxb']).intersection(jpx_cl)) == 0: + msg = "A JPX box requires that either 'jpx ' or 'jpxb' be " + msg += "present in the ftype compatibility list." + raise RuntimeError(msg) + if hasattr(box, 'box') != 0: + # Same set of checks on any child boxes. + self._validate_jpx_compatibility(box.box, compatibility_list) + + def _validate_label(self, boxes): + """ + Label boxes can only be inside association, codestream headers, or + compositing layer header boxes. + """ + for box in boxes: + if box.box_id != 'asoc': + if hasattr(box, 'box'): + for boxi in box.box: + if boxi.box_id == 'lbl ': + msg = "A label box cannot be nested inside a " + msg += "{0} box." + msg = msg.format(box.box_id) + raise IOError(msg) + # Same set of checks on any child boxes. + self._validate_label(box.box) # Setup the default callback handlers. See the callback functions subsection From 528fb3492d610bce9f433fd7f533bcd7c96ffbc4 Mon Sep 17 00:00:00 2001 From: jevans Date: Thu, 19 Mar 2015 19:42:06 -0400 Subject: [PATCH 5/5] Added support for JP2 bits per component box Closes-Issue: #321 --- CHANGES.txt | 2 + glymur/jp2box.py | 74 ++++++++++++++++++++++++++++++++++++ glymur/test/fixtures.py | 5 +++ glymur/test/test_printing.py | 11 ++++++ 4 files changed, 92 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 042e351..1bdd8ed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,5 @@ +Mar 18, 2015 - Added support for JP2 bits per component box. + Jan 10, 2015 - v0.8.0 Reduced number of steps required for writing images. Deprecated old read and write methods in favor of array-style slicing. Added ignore_pclr_cmap_cdef, verbose, diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 698711a..9bf8e01 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1830,6 +1830,79 @@ class AssociationBox(Jp2kBox): self._write_superbox(fptr, b'asoc') +class BitsPerComponentBox(Jp2kBox): + """Container for bits per component 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. + bpc : list + bits per component for each component + signed : list + True if signed, false if not, for each component + """ + box_id = 'bpcc' + longname = 'Bits Per Component' + + def __init__(self, bpc, signed, length=0, offset=-1): + Jp2kBox.__init__(self) + self.length = length + self.offset = offset + self.bpc = bpc + self.signed = signed + + def __repr__(self): + msg = "glymur.jp2box.BitsPerComponentBox(box={0})".format(self.box) + return msg + + def __str__(self): + title = Jp2kBox.__str__(self) + if _printoptions['short'] is True: + return title + + body = 'Bits per component: [' + body += ', '.join(str(x) for x in self.bpc) + body += ']' + body += '\n' + body += 'Signed: [' + ', '.join(str(x) for x in self.signed) + ']' + + body = self._indent(body) + + text = '\n'.join([title, body]) + return text + + @classmethod + def parse(cls, fptr, offset, length): + """Parse bits per component box. + + Parameters + ---------- + fptr : file + Open file object. + offset : int + Start position of box in bytes. + length : int + Length of the box in bytes. + + Returns + ------- + AssociationBox instance + """ + nbytes = length - 8 + data = fptr.read(nbytes) + bpc = tuple(((x & 0x7f) + 1) for x in data) + signed = tuple(((x & 0x80) > 0) for x in data) + + return cls(bpc, signed, length=length, offset=offset) + + class JP2HeaderBox(Jp2kBox): """Container for JP2 header box information. @@ -3404,6 +3477,7 @@ class UUIDBox(Jp2kBox): # Map each box ID to the corresponding class. _BOX_WITH_ID = { b'asoc': AssociationBox, + b'bpcc': BitsPerComponentBox, b'cdef': ChannelDefinitionBox, b'cgrp': ColourGroupBox, b'cmap': ComponentMappingBox, diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index 34327fc..11b9d16 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -1092,3 +1092,8 @@ goodstuff_with_full_header = r"""Codestream: 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)] SOD marker segment @ (164, 0) EOC marker segment @ (115218, 0)""" + +bpcc = """Bits Per Component Box (bpcc) @ (62, 12) + Bits per component: [5, 5, 5, 1] + Signed: [False, False, False, False]""" + diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index ba5dd09..f17414d 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -618,6 +618,17 @@ class TestPrintingOpjDataRoot(unittest.TestCase): def tearDown(self): pass + def test_bpcc(self): + """BPCC boxes are rare :-)""" + self.maxDiff = None + filename = opj_data_file('input/nonregression/issue458.jp2') + jp2 = Jp2k(filename) + with patch('sys.stdout', new=StringIO()) as fake_out: + box = jp2.box[2].box[1] + print(box) + actual = fake_out.getvalue().strip() + self.assertEqual(actual, fixtures.bpcc) + def test_cinema_profile(self): """Should print Cinema 2K when the profile is 3.""" filename = opj_data_file('input/nonregression/_00042.j2k')