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 .jp2box import ColourSpecificationBox
|
||||||
from .lib import _openjpeg as _opj
|
from .lib import _openjpeg as _opj
|
||||||
from .lib import _openjp2 as _opj2
|
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.
|
# Need to known if openjp2 library is the officially release v2.0.0 or not.
|
||||||
_OPENJP2_IS_OFFICIAL_V2 = False
|
_OPENJP2_IS_OFFICIAL_V2 = False
|
||||||
|
|
@ -187,6 +187,94 @@ class Jp2k(Jp2kBox):
|
||||||
msg += "profile if the file type box brand is 'jp2 '."
|
msg += "profile if the file type box brand is 'jp2 '."
|
||||||
warnings.warn(msg)
|
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
|
# pylint: disable-msg=W0221
|
||||||
def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None,
|
def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None,
|
||||||
cbsize=None, psizes=None, grid_offset=None, sop=False,
|
cbsize=None, psizes=None, grid_offset=None, sop=False,
|
||||||
|
|
@ -211,7 +299,7 @@ class Jp2k(Jp2kBox):
|
||||||
Code block size (DY, DX).
|
Code block size (DY, DX).
|
||||||
colorspace : str, optional
|
colorspace : str, optional
|
||||||
Either 'rgb' or 'gray'.
|
Either 'rgb' or 'gray'.
|
||||||
cratios : sequence, optional
|
cratios : iterable
|
||||||
Compression ratios for successive layers.
|
Compression ratios for successive layers.
|
||||||
eph : bool, optional
|
eph : bool, optional
|
||||||
If true, write SOP marker after each header packet.
|
If true, write SOP marker after each header packet.
|
||||||
|
|
@ -232,7 +320,7 @@ class Jp2k(Jp2kBox):
|
||||||
Number of resolutions.
|
Number of resolutions.
|
||||||
prog : str, optional
|
prog : str, optional
|
||||||
Progression order, one of "LRCP" "RLCP", "RPCL", "PCRL", "CPRL".
|
Progression order, one of "LRCP" "RLCP", "RPCL", "PCRL", "CPRL".
|
||||||
psnr : list, optional
|
psnr : iterable, optional
|
||||||
Different PSNR for successive layers.
|
Different PSNR for successive layers.
|
||||||
psizes : list, optional
|
psizes : list, optional
|
||||||
List of precinct sizes. Each precinct size tuple is defined in
|
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 glymur is unable to load the openjp2 library.
|
||||||
"""
|
"""
|
||||||
if _opj2.OPENJP2 is None:
|
if _opj2.OPENJP2 is None:
|
||||||
raise LibraryNotFoundError("You must have the development version "
|
raise LibraryNotFoundError("You must have the openjp2 library "
|
||||||
"of OpenJP2 installed before using "
|
"installed before using this "
|
||||||
"this functionality.")
|
"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()
|
cparams = _opj2.set_default_encoder_parameters()
|
||||||
|
|
||||||
|
|
@ -275,11 +371,6 @@ class Jp2k(Jp2kBox):
|
||||||
outfile += b'0' * num_pad_bytes
|
outfile += b'0' * num_pad_bytes
|
||||||
cparams.outfile = outfile
|
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
|
cparams.cod_format = codec_fmt
|
||||||
|
|
||||||
# Set defaults to lossless to begin.
|
# Set defaults to lossless to begin.
|
||||||
|
|
@ -290,15 +381,6 @@ class Jp2k(Jp2kBox):
|
||||||
if cbsize is not None:
|
if cbsize is not None:
|
||||||
width = cbsize[1]
|
width = cbsize[1]
|
||||||
height = cbsize[0]
|
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.cblockw_init = width
|
||||||
cparams.cblockh_init = height
|
cparams.cblockh_init = height
|
||||||
|
|
||||||
|
|
@ -336,18 +418,6 @@ class Jp2k(Jp2kBox):
|
||||||
|
|
||||||
if psizes is not None:
|
if psizes is not None:
|
||||||
for j, (prch, prcw) in enumerate(psizes):
|
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.prcw_init[j] = prcw
|
||||||
cparams.prch_init[j] = prch
|
cparams.prch_init[j] = prch
|
||||||
cparams.csty |= 0x01
|
cparams.csty |= 0x01
|
||||||
|
|
@ -365,54 +435,38 @@ class Jp2k(Jp2kBox):
|
||||||
cparams.cp_tdy = tilesize[0]
|
cparams.cp_tdy = tilesize[0]
|
||||||
cparams.tile_size_on = _opj2.TRUE
|
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:
|
if img_array.ndim == 2:
|
||||||
|
# Force it to be 3D. Just makes things easier later on.
|
||||||
numrows, numcols = img_array.shape
|
numrows, numcols = img_array.shape
|
||||||
img_array = img_array.reshape(numrows, numcols, 1)
|
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
|
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:
|
if colorspace is None:
|
||||||
|
# Must infer the colorspace from the image dimensions.
|
||||||
if img_array.shape[2] == 1 or img_array.shape[2] == 2:
|
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
|
colorspace = _opj2.CLRSPC_GRAY
|
||||||
else:
|
else:
|
||||||
# No YCC unless specifically told to do so.
|
# Anything else must be RGB, right?
|
||||||
colorspace = _opj2.CLRSPC_SRGB
|
colorspace = _opj2.CLRSPC_SRGB
|
||||||
else:
|
else:
|
||||||
if codec_fmt == _opj2.CODEC_J2K:
|
# Turn the colorspace from a string to the enumerated value that
|
||||||
raise IOError('Do not specify a colorspace with J2K.')
|
# the library expects.
|
||||||
colorspace = colorspace.lower()
|
colorspace = _COLORSPACE_MAP[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]
|
|
||||||
|
|
||||||
if mct is None:
|
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:
|
if colorspace == _opj2.CLRSPC_SRGB:
|
||||||
cparams.tcp_mct = 1
|
cparams.tcp_mct = 1
|
||||||
else:
|
else:
|
||||||
cparams.tcp_mct = 0
|
cparams.tcp_mct = 0
|
||||||
else:
|
else:
|
||||||
if mct and colorspace == _opj2.CLRSPC_GRAY:
|
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 = "Cannot specify usage of the multi component transform "
|
||||||
msg += "if the colorspace is gray."
|
msg += "if the colorspace is gray."
|
||||||
raise IOError(msg)
|
raise IOError(msg)
|
||||||
|
|
@ -420,10 +474,9 @@ class Jp2k(Jp2kBox):
|
||||||
|
|
||||||
if img_array.dtype == np.uint8:
|
if img_array.dtype == np.uint8:
|
||||||
comp_prec = 8
|
comp_prec = 8
|
||||||
elif img_array.dtype == np.uint16:
|
|
||||||
comp_prec = 16
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("unhandled datatype")
|
# We already know it cannot be anything else than uint16.
|
||||||
|
comp_prec = 16
|
||||||
|
|
||||||
comptparms = (_opj2.ImageComptParmType * num_comps)()
|
comptparms = (_opj2.ImageComptParmType * num_comps)()
|
||||||
for j in range(num_comps):
|
for j in range(num_comps):
|
||||||
|
|
@ -465,26 +518,28 @@ class Jp2k(Jp2kBox):
|
||||||
_opj2.set_error_handler(codec, _ERROR_CALLBACK)
|
_opj2.set_error_handler(codec, _ERROR_CALLBACK)
|
||||||
_opj2.setup_encoder(codec, cparams, image)
|
_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,
|
strm = _opj2.stream_create_default_file_stream_v3(self.filename,
|
||||||
False)
|
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.start_compress(codec, image, strm)
|
||||||
_opj2.encode(codec, strm)
|
_opj2.encode(codec, strm)
|
||||||
_opj2.end_compress(codec, strm)
|
_opj2.end_compress(codec, strm)
|
||||||
|
|
||||||
if hasattr(_opj2.OPENJP2, 'opj_stream_create_default_file_stream_v3'):
|
if _OPENJP2_IS_OFFICIAL_V2:
|
||||||
_opj2.stream_destroy_v3(strm)
|
|
||||||
else:
|
|
||||||
_opj2.stream_destroy(strm)
|
_opj2.stream_destroy(strm)
|
||||||
c.fclose(fptr)
|
_libc.fclose(fptr)
|
||||||
|
else:
|
||||||
|
_opj2.stream_destroy_v3(strm)
|
||||||
|
|
||||||
_opj2.destroy_codec(codec)
|
_opj2.destroy_codec(codec)
|
||||||
_opj2.image_destroy(image)
|
_opj2.image_destroy(image)
|
||||||
|
|
||||||
|
# Refresh the metadata.
|
||||||
self.parse()
|
self.parse()
|
||||||
|
|
||||||
def wrap(self, filename, boxes=None):
|
def wrap(self, filename, boxes=None):
|
||||||
|
|
@ -916,8 +971,8 @@ class Jp2k(Jp2kBox):
|
||||||
True)
|
True)
|
||||||
stack.callback(_opj2.stream_destroy_v3, stream)
|
stack.callback(_opj2.stream_destroy_v3, stream)
|
||||||
else:
|
else:
|
||||||
fptr = c.fopen(self.filename, 'rb')
|
fptr = _libc.fopen(self.filename, 'rb')
|
||||||
stack.callback(c.fclose, fptr)
|
stack.callback(_libc.fclose, fptr)
|
||||||
stream = _opj2.stream_create_default_file_stream(fptr, True)
|
stream = _opj2.stream_create_default_file_stream(fptr, True)
|
||||||
stack.callback(_opj2.stream_destroy, stream)
|
stack.callback(_opj2.stream_destroy, stream)
|
||||||
codec = _opj2.create_decompress(self._codec_format)
|
codec = _opj2.create_decompress(self._codec_format)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class TestSuiteNegative(unittest.TestCase):
|
||||||
data = read_image(infile)
|
data = read_image(infile)
|
||||||
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
|
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
|
||||||
j = Jp2k(tfile.name, 'wb')
|
j = Jp2k(tfile.name, 'wb')
|
||||||
with self.assertRaises(RuntimeError):
|
with self.assertRaises(IOError):
|
||||||
j.write(data, psnr=[30, 35, 40], cratios=[2, 3, 4])
|
j.write(data, psnr=[30, 35, 40], cratios=[2, 3, 4])
|
||||||
|
|
||||||
def test_NR_MarkerIsNotCompliant_j2k_dump(self):
|
def test_NR_MarkerIsNotCompliant_j2k_dump(self):
|
||||||
|
|
@ -109,13 +109,13 @@ class TestSuiteNegative(unittest.TestCase):
|
||||||
j = Jp2k(tfile.name, 'wb')
|
j = Jp2k(tfile.name, 'wb')
|
||||||
|
|
||||||
# opj_compress doesn't allow code block area to exceed 4096.
|
# 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))
|
j.write(data, cbsize=(256, 256))
|
||||||
|
|
||||||
# opj_compress doesn't allow either dimension to be less than 4.
|
# 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))
|
j.write(data, cbsize=(2048, 2))
|
||||||
with self.assertRaises(RuntimeError) as cr:
|
with self.assertRaises(IOError) as cr:
|
||||||
j.write(data, cbsize=(2, 2048))
|
j.write(data, cbsize=(2, 2048))
|
||||||
|
|
||||||
@unittest.skipIf(sys.hexversion < 0x03020000,
|
@unittest.skipIf(sys.hexversion < 0x03020000,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue