From 429d0f8f1fc4a26fe13d4a726c7a49b2ae9102be Mon Sep 17 00:00:00 2001 From: John Evans Date: Mon, 29 Jul 2013 11:26:32 -0400 Subject: [PATCH] Abstracted out all the write parameter validation. #90 RuntimeErrors swapped out for IOError. --- glymur/jp2k.py | 201 +++++++++++++++++++----------- glymur/test/test_opj_suite_neg.py | 8 +- 2 files changed, 132 insertions(+), 77 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index beb0788..1d751e0 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -32,7 +32,7 @@ from .jp2box import ImageHeaderBox from .jp2box import ColourSpecificationBox from .lib import _openjpeg as _opj from .lib import _openjp2 as _opj2 -from .lib import c +from .lib import c as _libc # Need to known if openjp2 library is the officially release v2.0.0 or not. _OPENJP2_IS_OFFICIAL_V2 = False @@ -187,6 +187,94 @@ class Jp2k(Jp2kBox): msg += "profile if the file type box brand is 'jp2 '." warnings.warn(msg) + def _validate_write_parameters(self, img_array, code_block_size, precinct_sizes, cratios, + psnr, mct, colorspace, codec_fmt): + """Check that the input parameters to the write function are valid. + + Parameters + ---------- + img_array : ndarray + Image data to be written to file. + code_block_size : tuple + Code block size (DY, DX). + precinct_sizes : list + List of precinct sizes. Each precinct size tuple is defined in + (height x width). + cratios : iterable + Compression ratios for successive layers. + psnr : iterable + Different PSNR for successive layers. + mct : bool + Specifies usage of the multi component transform. If not + specified, defaults to True if the colorspace is RGB. + colorspace : str, optional + Either 'rgb' or 'gray'. + codec_fmt : int + Are we writing a JP2 file or a J2K file? + """ + # Validate code block size and precinct sizes. + if code_block_size is not None: + width = code_block_size[1] + height = code_block_size[0] + 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." + raise IOError(msg) + if ((math.log(height, 2) != math.floor(math.log(height, 2)) or + math.log(width, 2) != math.floor(math.log(width, 2)))): + msg = "Bad code block size ({0}, {1}), " + msg += "must be powers of 2." + raise IOError(msg.format(height, width)) + + if precinct_sizes is not None: + for j, (prch, prcw) in enumerate(precinct_sizes): + if j == 0 and code_block_size is not None: + cblkh, cblkw = code_block_size + if cblkh * 2 > prch or cblkw * 2 > prcw: + msg = "Highest Resolution precinct size must be at " + msg += "least twice that of the code block dimensions." + raise IOError(msg) + if ((math.log(prch, 2) != math.floor(math.log(prch, 2)) or + math.log(prcw, 2) != math.floor(math.log(prcw, 2)))): + msg = "Bad precinct sizes ({0}, {1}), " + msg += "must be powers of 2." + raise IOError(msg.format(prch, prcw)) + + if cratios is not None and psnr is not None: + msg = "Cannot specify cratios and psnr together." + raise IOError(msg) + + # What would the point of 1D images be? + if img_array.ndim == 1 or img_array.ndim > 3: + msg = "{0}D imagery is not allowed.".format(img_array.ndim) + raise IOError(msg) + + if _OPENJP2_IS_OFFICIAL_V2: + 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 " + msg += "greyscale images or three-channel RGB images when " + msg += "the OpenJPEG library version is the official 2.0.0 " + msg += "release." + raise IOError(msg) + + if colorspace is not None: + if codec_fmt == _opj2.CODEC_J2K: + msg = 'Do not specify a colorspace when writing a raw ' + msg += 'codestream.' + raise IOError(msg) + if colorspace.lower() not in ('rgb', 'grey', 'gray'): + msg = 'Invalid colorspace "{0}"'.format(colorspace) + raise IOError(msg) + elif colorspace.lower() == 'rgb' and img_array.shape[2] < 3: + msg = 'RGB colorspace requires at least 3 components.' + raise IOError(msg) + + if img_array.dtype != np.uint8 and img_array.dtype != np.uint16: + msg = "Only uint8 and uint16 images are currently supported." + raise RuntimeError(msg) + + # pylint: disable-msg=W0221 def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None, cbsize=None, psizes=None, grid_offset=None, sop=False, @@ -211,7 +299,7 @@ class Jp2k(Jp2kBox): Code block size (DY, DX). colorspace : str, optional Either 'rgb' or 'gray'. - cratios : sequence, optional + cratios : iterable Compression ratios for successive layers. eph : bool, optional If true, write SOP marker after each header packet. @@ -232,7 +320,7 @@ class Jp2k(Jp2kBox): Number of resolutions. prog : str, optional Progression order, one of "LRCP" "RLCP", "RPCL", "PCRL", "CPRL". - psnr : list, optional + psnr : iterable, optional Different PSNR for successive layers. psizes : list, optional List of precinct sizes. Each precinct size tuple is defined in @@ -264,9 +352,17 @@ class Jp2k(Jp2kBox): If glymur is unable to load the openjp2 library. """ if _opj2.OPENJP2 is None: - raise LibraryNotFoundError("You must have the development version " - "of OpenJP2 installed before using " - "this functionality.") + raise LibraryNotFoundError("You must have the openjp2 library " + "installed before using this " + "functionality.") + + if self.filename[-4:].lower() == '.jp2': + codec_fmt = _opj2.CODEC_JP2 + else: + codec_fmt = _opj2.CODEC_J2K + + self._validate_write_parameters(img_array, cbsize, psizes, cratios, + psnr, mct, colorspace, codec_fmt) cparams = _opj2.set_default_encoder_parameters() @@ -275,11 +371,6 @@ class Jp2k(Jp2kBox): outfile += b'0' * num_pad_bytes cparams.outfile = outfile - if self.filename[-4:].lower() == '.jp2': - codec_fmt = _opj2.CODEC_JP2 - else: - codec_fmt = _opj2.CODEC_J2K - cparams.cod_format = codec_fmt # Set defaults to lossless to begin. @@ -290,15 +381,6 @@ class Jp2k(Jp2kBox): if cbsize is not None: width = cbsize[1] height = cbsize[0] - 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." - raise RuntimeError(msg) - if ((math.log(height, 2) != math.floor(math.log(height, 2)) or - math.log(width, 2) != math.floor(math.log(width, 2)))): - msg = "Bad code block size ({0}, {1}), " - msg += "must be powers of 2." - raise IOError(msg.format(height, width)) cparams.cblockw_init = width cparams.cblockh_init = height @@ -336,18 +418,6 @@ class Jp2k(Jp2kBox): if psizes is not None: for j, (prch, prcw) in enumerate(psizes): - if j == 0 and cbsize is not None: - cblkh, cblkw = cbsize - if cblkh * 2 > prch or cblkw * 2 > prcw: - msg = "Highest Resolution precinct size must be at " - msg += "least twice that of the code block dimensions." - raise IOError(msg) - if ((math.log(prch, 2) != math.floor(math.log(prch, 2)) or - math.log(prcw, 2) != math.floor(math.log(prcw, 2)))): - msg = "Bad precinct sizes ({0}, {1}), " - msg += "must be powers of 2." - raise IOError(msg.format(prch, prcw)) - cparams.prcw_init[j] = prcw cparams.prch_init[j] = prch cparams.csty |= 0x01 @@ -365,54 +435,38 @@ class Jp2k(Jp2kBox): cparams.cp_tdy = tilesize[0] cparams.tile_size_on = _opj2.TRUE - if cratios is not None and psnr is not None: - msg = "Cannot specify cratios and psnr together." - raise RuntimeError(msg) - if img_array.ndim == 2: + # Force it to be 3D. Just makes things easier later on. numrows, numcols = img_array.shape img_array = img_array.reshape(numrows, numcols, 1) - elif img_array.ndim == 3: - pass - else: - msg = "{0}D imagery is not allowed.".format(img_array.ndim) - raise IOError(msg) numrows, numcols, num_comps = img_array.shape - if _OPENJP2_IS_OFFICIAL_V2: - if img_array.shape[2] != 1 and img_array.shape[2] != 3: - msg = "Writing images is restricted to single-channel " - msg += "greyscale images or three-channel RGB images when " - msg += "the OpenJPEG library version is the official 2.0.0 " - msg += "release." - raise IOError(msg) if colorspace is None: + # Must infer the colorspace from the image dimensions. if img_array.shape[2] == 1 or img_array.shape[2] == 2: + # A single channel image or an image with two channels is going + # to be greyscale. colorspace = _opj2.CLRSPC_GRAY else: - # No YCC unless specifically told to do so. + # Anything else must be RGB, right? colorspace = _opj2.CLRSPC_SRGB else: - if codec_fmt == _opj2.CODEC_J2K: - raise IOError('Do not specify a colorspace with J2K.') - colorspace = colorspace.lower() - if colorspace not in ('rgb', 'grey', 'gray'): - msg = 'Invalid colorspace "{0}"'.format(colorspace) - raise IOError(msg) - elif colorspace == 'rgb' and img_array.shape[2] < 3: - msg = 'RGB colorspace requires at least 3 components.' - raise IOError(msg) - else: - colorspace = _COLORSPACE_MAP[colorspace] + # Turn the colorspace from a string to the enumerated value that + # the library expects. + colorspace = _COLORSPACE_MAP[colorspace.lower()] 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. if colorspace == _opj2.CLRSPC_SRGB: cparams.tcp_mct = 1 else: cparams.tcp_mct = 0 else: if mct and colorspace == _opj2.CLRSPC_GRAY: + # Cannot check for this in the validate routine, as we need + # to know what the target colorspace has been determined to be. msg = "Cannot specify usage of the multi component transform " msg += "if the colorspace is gray." raise IOError(msg) @@ -420,10 +474,9 @@ class Jp2k(Jp2kBox): if img_array.dtype == np.uint8: comp_prec = 8 - elif img_array.dtype == np.uint16: - comp_prec = 16 else: - raise RuntimeError("unhandled datatype") + # We already know it cannot be anything else than uint16. + comp_prec = 16 comptparms = (_opj2.ImageComptParmType * num_comps)() for j in range(num_comps): @@ -465,26 +518,28 @@ class Jp2k(Jp2kBox): _opj2.set_error_handler(codec, _ERROR_CALLBACK) _opj2.setup_encoder(codec, cparams, image) - if hasattr(_opj2.OPENJP2, 'opj_stream_create_default_file_stream_v3'): + if _OPENJP2_IS_OFFICIAL_V2: + fptr = _libc.fopen(self.filename, 'wb') + strm = _opj2.stream_create_default_file_stream(fptr, False) + else: strm = _opj2.stream_create_default_file_stream_v3(self.filename, False) - else: - fptr = c.fopen(self.filename, 'wb') - strm = _opj2.stream_create_default_file_stream(fptr, False) + # Start to clean up after ourselves. _opj2.start_compress(codec, image, strm) _opj2.encode(codec, strm) _opj2.end_compress(codec, strm) - if hasattr(_opj2.OPENJP2, 'opj_stream_create_default_file_stream_v3'): - _opj2.stream_destroy_v3(strm) - else: + if _OPENJP2_IS_OFFICIAL_V2: _opj2.stream_destroy(strm) - c.fclose(fptr) + _libc.fclose(fptr) + else: + _opj2.stream_destroy_v3(strm) _opj2.destroy_codec(codec) _opj2.image_destroy(image) + # Refresh the metadata. self.parse() def wrap(self, filename, boxes=None): @@ -916,8 +971,8 @@ class Jp2k(Jp2kBox): True) stack.callback(_opj2.stream_destroy_v3, stream) else: - fptr = c.fopen(self.filename, 'rb') - stack.callback(c.fclose, fptr) + 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) codec = _opj2.create_decompress(self._codec_format) diff --git a/glymur/test/test_opj_suite_neg.py b/glymur/test/test_opj_suite_neg.py index 48b8117..3dfda15 100644 --- a/glymur/test/test_opj_suite_neg.py +++ b/glymur/test/test_opj_suite_neg.py @@ -70,7 +70,7 @@ class TestSuiteNegative(unittest.TestCase): data = read_image(infile) with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: j = Jp2k(tfile.name, 'wb') - with self.assertRaises(RuntimeError): + with self.assertRaises(IOError): j.write(data, psnr=[30, 35, 40], cratios=[2, 3, 4]) def test_NR_MarkerIsNotCompliant_j2k_dump(self): @@ -109,13 +109,13 @@ class TestSuiteNegative(unittest.TestCase): j = Jp2k(tfile.name, 'wb') # opj_compress doesn't allow code block area to exceed 4096. - with self.assertRaises(RuntimeError) as cr: + with self.assertRaises(IOError) as cr: j.write(data, cbsize=(256, 256)) # opj_compress doesn't allow either dimension to be less than 4. - with self.assertRaises(RuntimeError) as cr: + with self.assertRaises(IOError) as cr: j.write(data, cbsize=(2048, 2)) - with self.assertRaises(RuntimeError) as cr: + with self.assertRaises(IOError) as cr: j.write(data, cbsize=(2, 2048)) @unittest.skipIf(sys.hexversion < 0x03020000,