Abstracted out all the write parameter validation. #90
RuntimeErrors swapped out for IOError.
This commit is contained in:
parent
c2a94b92f1
commit
429d0f8f1f
2 changed files with 132 additions and 77 deletions
201
glymur/jp2k.py
201
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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue