Merge branch 'release-0.3.0'

This commit is contained in:
jevans 2013-07-31 18:52:32 -04:00
commit d7635bdfef
22 changed files with 458 additions and 252 deletions

View file

@ -1,3 +1,5 @@
Jul 31, 2013 - v0.3.0 Added support for official 2.0.0.
Jul 27, 2013 - v0.2.8 Fixed inconsistency regarding configuration
file directory on windows (issue91).

View file

@ -78,7 +78,7 @@ copyright = u'2013, John Evans'
# The short X.Y version.
version = '0.2'
# The full version, including alpha/beta/rc tags.
release = '0.2.8'
release = '0.3.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -9,13 +9,14 @@ Glymur Configuration
The default glymur installation process relies upon OpenJPEG version
1.X being properly installed on your system. This will, however, only
give you you basic read capabilities, so if you wish to take advantage
of more of glymur's features, you should compile OpenJPEG as a shared
library (named *openjp2* instead of *openjpeg*) from the developmental
source that you can retrieve via subversion. As of this time of writing,
svn revision 2345 works. You should also download the test data for
the purpose of configuring and running OpenJPEG's test suite, check
their instructions for all this. You should set the **OPJ_DATA_ROOT**
environment variable for the purpose of running Glymur's test suite. ::
of more of glymur's features, you should install version 2.0 or
compile OpenJPEG as a shared library (named *openjp2* instead of
*openjpeg*) from the developmental source that you can retrieve via
subversion. As of this time of writing, svn revision 2345 works.
You should also download the test data for the purpose of configuring
and running OpenJPEG's test suite, check their instructions for all
this. You should set the **OPJ_DATA_ROOT** environment variable
for the purpose of running Glymur's test suite. ::
$ svn co http://openjpeg.googlecode.com/svn/data
$ export OPJ_DATA_ROOT=`pwd`/data
@ -40,8 +41,8 @@ but if you have **$XDG_CONFIG_HOME** defined, the path will be ::
$XDG_CONFIG_HOME/glymur/glymurrc
On windows, the path to the configuration file can be determined by starting up Python
and typing ::
On windows, the path to the configuration file can be determined
by starting up Python and typing ::
import os
os.path.join(os.path.expanduser('~'), 'glymur', 'glymurrc')
@ -75,9 +76,7 @@ MacPorts. You should install the following set of ports:
* py33-matplotlib (optional, for running certain tests)
* py33-Pillow (optional, for running certain tests)
MacPorts supplies both OpenJPEG 1.5.0 and OpenJPEG 2.0.0. As previously
mentioned, the 2.0.0 official release is not supported (although the 2.0+
development version via SVN *is* supported).
MacPorts supplies both OpenJPEG 1.5.0 and OpenJPEG 2.0.0.
Linux
-----
@ -138,12 +137,14 @@ In addition, you must install contextlib2 and Pillow via pip. ::
Windows
-------
I would recommend using WinPython, but Python(xy) also seems to work. WinPython 3.3
should work with no additional installations required, but 2.7 versions still require
contextlib2 and mock to be installed via pip.
32-bit WinPython 2.7.5 seems to work with OpenJPEG 1.X, 2.0, and the
development version, but still requires contextlib2 and mock to be
installed via pip. WinPython 3.3.2, however, seems to have trouble
with OpenJPEG 2.0, so I would suggest using the development version
there (I'm unwilling to spend ANY more time trying to figure out what
the problem is there).
Glymur has been tested far less extensively on Windows than on the other
platforms.
64-bit windows is completely untested.
'''''''

View file

@ -122,10 +122,10 @@ and add it after the JP2 header box, but before the codestream box ::
Create an image with an alpha layer?
====================================
OpenJPEG can create JP2 files with more than 3 components, but by default, any
extra components are not described as such. In order to do so,
we need to rewrap such an image in a set of boxes that includes a channel
definition box.
OpenJPEG can create JP2 files with more than 3 components (requires
the development version), but by default, any extra components are
not described as such. In order to do so, we need to rewrap such
an image in a set of boxes that includes a channel definition box.
This example is based on SciPy example code found at
http://scipy-lectures.github.io/advanced/image_processing/#basic-manipulations .

View file

@ -14,22 +14,21 @@ some very limited support for reading JPX metadata. For instance,
**asoc** and **labl** boxes are recognized, so GMLJP2 metadata can
be retrieved from such JPX files.
Glymur works on Python 2.6, 2.7, and 3.3. Python 3.3 is strongly recommended.
Glymur works on Python 2.6, 2.7, and 3.3.
OpenJPEG Installation
=====================
OpenJPEG should be version 1.4, 1.5, or the trunk/development
version of OpenJPEG. The official 2.0.0 release or versions earlier than 1.3.0
are not supported. Furthermore, the 1.X versions of OpenJPEG are
currently only utilized for read-only purposes. In order to write JPEG 2000
images, you must compile the the trunk/development version. For more information
about OpenJPEG, please consult http://www.openjpeg.org.
Glymur will read JPEG 2000 images with versions 1.3, 1.4, 1.5, 2.0,
and the trunk/development version of OpenJPEG. Writing images is
only supported with the 2.0 series, however, and the trunk/development
version is strongly recommended. For more information about OpenJPEG,
please consult http://www.openjpeg.org.
If you use MacPorts on the mac or if you have a sufficiently recent
version of Linux, your package manager should already provide you
with a version of OpenJPEG 1.X with which glymur can already use
for read-only purposes. If your platform is windows, I suggest
using the 1.5.1 windows installer provided to you by the OpenJPEG
using the windows installers provided to you by the OpenJPEG
folks at https://code.google.com/p/openjpeg/downloads/list .
Glymur Installation
@ -57,5 +56,5 @@ You can run the tests from within python as follows::
>>> glymur.runtests()
Many tests are currently skipped; in fact most of them are skipped if you
are relying on OpenJPEG 1.4 or 1.5. The important thing, though, is whether or
are relying on OpenJPEG 1.X. The important thing, though, is whether or
not any tests fail.

View file

@ -1,11 +1,17 @@
------------
Known Issues
------------
* WinPython 3.3.2 and OpenJPEG 2.0 don't seem to want to play well together. If you do not need write support, just use OpenJPEG 1.5 instead. If you do need write support, try the development version of OpenJPEG.
-------
Roadmap
-------
Here's an incomplete list of what I'd like to focus on in the near future.
Here's an incomplete list of what I'd like to focus on in the future.
* continue to monitor upstream changes in the openjp2 library
* investigate using CFFI or cython instead of ctypes to wrap openjp2
* eventually expose the openjp2 API
* investigate JPIP
* investigate adding write support for UUID/XMP boxes (potentially a big project)
* investigate JPIP (likely to be an even bigger project)

View file

@ -636,79 +636,6 @@ class CompositingLayerHeaderBox(Jp2kBox):
return box
class JP2HeaderBox(Jp2kBox):
"""Container for JP2 header 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.
box : list
List of boxes contained in this superbox.
"""
def __init__(self, length=0, offset=-1):
Jp2kBox.__init__(self, box_id='jp2h', longname='JP2 Header')
self.length = length
self.offset = offset
self.box = []
def __str__(self):
msg = Jp2kBox.__str__(self)
for box in self.box:
boxstr = str(box)
# Add indentation.
strs = [('\n ' + x) for x in boxstr.split('\n')]
msg += ''.join(strs)
return msg
def write(self, fptr):
"""Write a JP2 Header box to file.
"""
# Write the contained boxes, then come back and write the length.
orig_pos = fptr.tell()
fptr.write(struct.pack('>I', 0))
fptr.write('jp2h'.encode())
for box in self.box:
box.write(fptr)
end_pos = fptr.tell()
fptr.seek(orig_pos)
fptr.write(struct.pack('>I', end_pos - orig_pos))
fptr.seek(end_pos)
@staticmethod
def parse(fptr, offset, length):
"""Parse JPEG 2000 header box.
Parameters
----------
fptr : file
Open file object.
offset : int
Start position of box in bytes.
length : int
Length of the box in bytes.
Returns
-------
JP2HeaderBox instance
"""
box = JP2HeaderBox(length=length, offset=offset)
# The JP2 header box is a superbox, so go ahead and parse its child
# boxes.
box.box = box.parse_superbox(fptr)
return box
class ComponentMappingBox(Jp2kBox):
"""Container for channel identification information.

View file

@ -32,6 +32,15 @@ from .jp2box import ImageHeaderBox
from .jp2box import ColourSpecificationBox
from .lib import _openjpeg as _opj
from .lib import _openjp2 as _opj2
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
if _opj2.OPENJP2 is not None:
if _opj2.version() == '2.0.0':
if not hasattr(_opj2.OPENJP2,
'opj_stream_create_default_file_stream_v3'):
_OPENJP2_IS_OFFICIAL_V2 = True
_COLORSPACE_MAP = {'rgb': _opj2.CLRSPC_SRGB,
'gray': _opj2.CLRSPC_GRAY,
@ -178,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, 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,
@ -202,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.
@ -223,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
@ -255,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, colorspace, codec_fmt)
cparams = _opj2.set_default_encoder_parameters()
@ -266,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.
@ -281,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
@ -327,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
@ -356,47 +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 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)
@ -404,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):
@ -448,14 +517,29 @@ class Jp2k(Jp2kBox):
_opj2.set_warning_handler(codec, _WARNING_CALLBACK)
_opj2.set_error_handler(codec, _ERROR_CALLBACK)
_opj2.setup_encoder(codec, cparams, image)
strm = _opj2.stream_create_default_file_stream_v3(self.filename, False)
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)
# Start to clean up after ourselves.
_opj2.start_compress(codec, image, strm)
_opj2.encode(codec, strm)
_opj2.end_compress(codec, strm)
_opj2.stream_destroy_v3(strm)
if _OPENJP2_IS_OFFICIAL_V2:
_opj2.stream_destroy(strm)
_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):
@ -882,9 +966,15 @@ class Jp2k(Jp2kBox):
dparam.nb_tile_to_decode = 1
with ExitStack() as stack:
stream = _opj2.stream_create_default_file_stream_v3(self.filename,
True)
stack.callback(_opj2.stream_destroy_v3, stream)
if hasattr(_opj2.OPENJP2, 'opj_stream_create_default_file_stream_v3'):
stream = _opj2.stream_create_default_file_stream_v3(self.filename,
True)
stack.callback(_opj2.stream_destroy_v3, stream)
else:
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)
stack.callback(_opj2.destroy_codec, codec)

View file

@ -1,3 +1,4 @@
"""This package organizes individual libraries employed by glymur."""
from . import openjp2 as _openjp2
from . import openjpeg as _openjpeg
from . import c

46
glymur/lib/c.py Normal file
View file

@ -0,0 +1,46 @@
"""
Wraps fopen and fclose functions in libc.
"""
import ctypes
import ctypes.util
LIBC_PATH = ctypes.util.find_library('c')
C_LIB = ctypes.CDLL(LIBC_PATH)
def fopen(filename, mode):
"""Opens the file with the specified mode.
Parameters
----------
filename : str
Path to filename.
mode : str
Specifies how the file is to be opened.
Returns
-------
fptr : ctypes.c_void_p
File pointer.
"""
C_LIB.fopen.restype = ctypes.c_void_p
C_LIB.fopen.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
fptr = C_LIB.fopen(ctypes.c_char_p(filename.encode()),
ctypes.c_char_p(mode.encode()))
return fptr
def fclose(fptr):
"""Closes a file stream.
Parameters
----------
fptr : ctypes.c_void_p
File pointer.
"""
C_LIB.fclose.argtypes = [ctypes.c_void_p]
C_LIB.fclose.restype = ctypes.c_int32
status = C_LIB.fclose(fptr)
if status != 0:
raise IOError("Unable to close file.")

View file

@ -75,8 +75,6 @@ def load_openjpeg(libopenjpeg_path):
def read_config_file():
"""
We expect to not find openjp2 on the system path since the only version
that we currently care about is still in the svn trunk at openjpeg.org.
We must use a configuration file that the user must write.
"""
lib = {'openjp2': None, 'openjpeg': None}
@ -104,6 +102,17 @@ def load_openjp2(libopenjp2_path):
# No help from the config file, try to find it ourselves.
libopenjp2_path = find_library('openjp2')
if libopenjp2_path is None:
if platform.system() == 'Darwin':
path = '/opt/local/lib/libopenjp2.dylib'
if os.path.exists(path):
libopenjp2_path = path
elif os.name == 'nt':
path = os.path.join('C:\\', 'Program files', 'OpenJPEG 2.0',
'bin', 'openjp2.dll')
if os.path.exists(path):
libopenjp2_path = path
if libopenjp2_path is None:
return None
@ -145,7 +154,7 @@ def get_configdir():
if 'HOME' in os.environ and os.name != 'nt':
# HOME is set by WinPython to something unusual, so we don't
# necessarily want that.
# necessarily want that.
return os.path.join(os.environ['HOME'], '.config', 'glymur')
# Last stand. Should handle windows... others?

View file

@ -5,6 +5,7 @@ Wraps individual functions in openjp2 library.
# pylint: disable=C0302,R0903
import ctypes
import sys
from .config import glymur_config
OPENJP2, OPENJPEG = glymur_config()
@ -639,9 +640,16 @@ if OPENJP2 is not None:
ctypes.POINTER(ImageType)]
OPENJP2.opj_setup_encoder.argtypes = ARGTYPES
ARGTYPES = [ctypes.c_char_p, ctypes.c_int32]
OPENJP2.opj_stream_create_default_file_stream_v3.argtypes = ARGTYPES
OPENJP2.opj_stream_create_default_file_stream_v3.restype = STREAM_TYPE_P
if hasattr(OPENJP2, 'opj_stream_create_default_file_stream_v3'):
ARGTYPES = [ctypes.c_char_p, ctypes.c_int32]
OPENJP2.opj_stream_create_default_file_stream_v3.argtypes = ARGTYPES
OPENJP2.opj_stream_create_default_file_stream_v3.restype = STREAM_TYPE_P
OPENJP2.opj_stream_destroy_v3.argtypes = [STREAM_TYPE_P]
else:
ARGTYPES = [ctypes.c_void_p, ctypes.c_int32]
OPENJP2.opj_stream_create_default_file_stream.argtypes = ARGTYPES
OPENJP2.opj_stream_create_default_file_stream.restype = STREAM_TYPE_P
OPENJP2.opj_stream_destroy.argtypes = [STREAM_TYPE_P]
ARGTYPES = [CODEC_TYPE, ctypes.POINTER(ImageType), STREAM_TYPE_P]
OPENJP2.opj_start_compress.argtypes = ARGTYPES
@ -649,7 +657,6 @@ if OPENJP2 is not None:
OPENJP2.opj_end_compress.argtypes = [CODEC_TYPE, STREAM_TYPE_P]
OPENJP2.opj_end_decompress.argtypes = [CODEC_TYPE, STREAM_TYPE_P]
OPENJP2.opj_stream_destroy_v3.argtypes = [STREAM_TYPE_P]
OPENJP2.opj_destroy_codec.argtypes = [CODEC_TYPE]
ARGTYPES = [CODEC_TYPE,
@ -1267,16 +1274,17 @@ def start_compress(codec, image, stream):
OPENJP2.opj_start_compress(codec, image, stream)
def stream_create_default_file_stream_v3(fname, a_read_stream):
"""Wraps openjp2 library function opj_stream_create_default_vile_stream_v3.
def stream_create_default_file_stream(fptr, isa_read_stream):
"""Wraps openjp2 library function opj_stream_create_default_vile_stream.
Sets the stream to be a file stream.
Sets the stream to be a file stream. This is valid only for version 2.0.0
of OpenJPEG.
Parameters
----------
fname : str
Specifies a file.
a_read_stream: bool
fptr : ctypes.c_void_p
Corresponds to C file pointer. Must be obtained from libc.fopen.
isa_read_stream: bool
True (read) or False (write)
Returns
@ -1284,16 +1292,52 @@ def stream_create_default_file_stream_v3(fname, a_read_stream):
stream : stream_t
An OpenJPEG file stream.
"""
read_stream = 1 if a_read_stream else 0
read_stream = 1 if isa_read_stream else 0
stream = OPENJP2.opj_stream_create_default_file_stream(fptr, read_stream)
return stream
def stream_create_default_file_stream_v3(fname, isa_read_stream):
"""Wraps openjp2 library function opj_stream_create_default_vile_stream_v3.
Sets the stream to be a file stream. This function is only valid for the
trunk/development 2.0+ version of the openjp2 library.
Parameters
----------
fname : str
Specifies a file.
isa_read_stream: bool
True (read) or False (write)
Returns
-------
stream : stream_t
An OpenJPEG file stream.
"""
read_stream = 1 if isa_read_stream else 0
file_argument = ctypes.c_char_p(fname.encode())
stream = OPENJP2.opj_stream_create_default_file_stream_v3(file_argument,
read_stream)
return stream
def stream_destroy_v3(stream):
def stream_destroy(stream):
"""Wraps openjp2 library function opj_stream_destroy.
Destroys the stream created by create_stream.
Parameters
----------
stream : STREAM_TYPE_P
The file stream.
"""
OPENJP2.opj_stream_destroy(stream)
def stream_destroy_v3(stream):
"""Wraps openjp2 library function opj_stream_destroy_v3.
Destroys the stream created by create_stream_v3.
Parameters
@ -1339,3 +1383,13 @@ def set_error_message(msg):
"""The openjpeg error handler has recorded an error message."""
global ERROR_MSG_LST
ERROR_MSG_LST.append(msg)
def version():
"""Wrapper for opj_version library routine."""
OPENJP2.opj_version.restype = ctypes.c_char_p
library_version = OPENJP2.opj_version()
if sys.hexversion >= 0x03000000:
return library_version.decode('utf-8')
else:
return library_version

View file

@ -28,9 +28,6 @@ else:
# Does not really matter. But version should not be called if there is no
# OpenJPEG library found.
_MINOR = 0
# Redefine version so that we can use it.
def version():
return '0.0.0'
class EventMgrType(ctypes.Structure):

View file

@ -16,10 +16,17 @@ import numpy as np
import glymur
OPENJP2_IS_V2_OFFICIAL = False
if glymur.lib.openjp2.OPENJP2 is not None:
if not hasattr(glymur.lib.openjp2.OPENJP2,
'opj_stream_create_default_file_stream_v3'):
OPENJP2_IS_V2_OFFICIAL = True
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
@unittest.skipIf(glymur.lib._openjp2.OPENJP2 is None,
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
"Missing openjp2 library.")
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL, "API followed here specific to V2.0+")
class TestOpenJP2(unittest.TestCase):
def setUp(self):

View file

@ -3,6 +3,22 @@ import sys
import numpy as np
import glymur
# Need to know the openjpeg version. If openjpeg is not installed, we use
# '0.0.0'
OPENJPEG_VERSION = '0.0.0'
if glymur.lib.openjpeg.OPENJPEG is not None:
OPENJPEG_VERSION = glymur.lib.openjpeg.version()
# Need to know of the libopenjp2 version is the official 2.0.0 release and NOT
# the 2.0+ development version.
OPENJP2_IS_V2_OFFICIAL = False
if glymur.lib.openjp2.OPENJP2 is not None:
if not hasattr(glymur.lib.openjp2.OPENJP2,
'opj_stream_create_default_file_stream_v3'):
OPENJP2_IS_V2_OFFICIAL = True
def mse(amat, bmat):
"""Mean Square Error"""

View file

@ -6,16 +6,18 @@ import sys
import tempfile
import warnings
if sys.hexversion < 0x03000000:
from StringIO import StringIO
else:
from io import StringIO
if sys.hexversion < 0x02070000:
import unittest2 as unittest
else:
import unittest
if sys.hexversion <= 0x03030000:
from mock import patch
from StringIO import StringIO
else:
from unittest.mock import patch
from io import StringIO
import glymur
@ -24,15 +26,11 @@ import glymur
class TestCallbacks(unittest.TestCase):
def setUp(self):
# Save sys.stdout.
self.stdout = sys.stdout
sys.stdout = StringIO()
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
def tearDown(self):
# Restore stdout.
sys.stdout = self.stdout
pass
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_info_callback_on_write(self):
@ -43,8 +41,9 @@ class TestCallbacks(unittest.TestCase):
tiledata = j.read(tile=0)
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = glymur.Jp2k(tfile.name, 'wb')
j.write(tiledata, verbose=True)
actual = sys.stdout.getvalue().strip()
with patch('sys.stdout', new=StringIO()) as fake_out:
j.write(tiledata, verbose=True)
actual = fake_out.getvalue().strip()
expected = '[INFO] tile number 1 / 1'
self.assertEqual(actual, expected)
@ -52,8 +51,9 @@ class TestCallbacks(unittest.TestCase):
# Verify that we get the expected stdio output when our internal info
# callback handler is enabled.
j = glymur.Jp2k(self.j2kfile)
d = j.read(rlevel=1, verbose=True, area=(0, 0, 200, 150))
actual = sys.stdout.getvalue().strip()
with patch('sys.stdout', new=StringIO()) as fake_out:
d = j.read(rlevel=1, verbose=True, area=(0, 0, 200, 150))
actual = fake_out.getvalue().strip()
lines = ['[INFO] Start to read j2k main header (0).',
'[INFO] Main header has been correctly decoded.',
@ -66,53 +66,41 @@ class TestCallbacks(unittest.TestCase):
self.assertEqual(actual, expected)
@unittest.skipIf(glymur.lib.openjp2.OPENJPEG is None,
@unittest.skipIf(glymur.lib.openjpeg.OPENJPEG is None,
"Missing openjpeg library.")
class TestCallbacks15(unittest.TestCase):
"""This test suite is for OpenJPEG 1.5.1 properties.
"""
@classmethod
def setUpClass(cls):
# Monkey patch the package so as to use OPENJPEG instead of OPENJP2
cls.openjp2 = glymur.lib.openjp2.OPENJP2
glymur.lib.openjp2.OPENJP2 = None
@classmethod
def tearDownClass(cls):
# Restore OPENJP2
glymur.lib.openjp2.OPENJP2 = cls.openjp2
def setUp(self):
# Save sys.stdout.
self.stdout = sys.stdout
sys.stdout = StringIO()
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
def tearDown(self):
# Restore stdout.
sys.stdout = self.stdout
pass
def test_info_callbacks_on_read(self):
# Verify that we get the expected stdio output when our internal info
# callback handler is enabled.
j = glymur.Jp2k(self.j2kfile)
d = j.read(rlevel=1, verbose=True)
actual = sys.stdout.getvalue().strip()
regex = re.compile(r"""\[INFO\]\stile\s1\sof\s1\s+
\[INFO\]\s-\stiers-1\stook\s
[0-9]+\.[0-9]+\ss\s+
\[INFO\]\s-\sdwt\stook\s
(-){0,1}[0-9]+\.[0-9]+\ss\s+
\[INFO\]\s-\stile\sdecoded\sin\s
[0-9]+\.[0-9]+\ss""",
re.VERBOSE)
if sys.hexversion <= 0x03020000:
self.assertRegexpMatches(actual, regex)
else:
self.assertRegex(actual, regex)
with patch('glymur.lib.openjp2.OPENJP2', new=None):
# Force to use OPENJPEG instead of OPENJP2.
j = glymur.Jp2k(self.j2kfile)
with patch('sys.stdout', new=StringIO()) as fake_out:
d = j.read(rlevel=1, verbose=True)
actual = fake_out.getvalue().strip()
regex = re.compile(r"""\[INFO\]\stile\s1\sof\s1\s+
\[INFO\]\s-\stiers-1\stook\s
[0-9]+\.[0-9]+\ss\s+
\[INFO\]\s-\sdwt\stook\s
(-){0,1}[0-9]+\.[0-9]+\ss\s+
\[INFO\]\s-\stile\sdecoded\sin\s
[0-9]+\.[0-9]+\ss""",
re.VERBOSE)
if sys.hexversion <= 0x03020000:
self.assertRegexpMatches(actual, regex)
else:
self.assertRegex(actual, regex)
if __name__ == "__main__":

View file

@ -19,6 +19,8 @@ from glymur.jp2box import *
from glymur.core import COLOR, OPACITY
from glymur.core import RED, GREEN, BLUE, GREY, WHOLE_IMAGE
from .fixtures import OPENJP2_IS_V2_OFFICIAL
try:
format_corpus_data_root = os.environ['FORMAT_CORPUS_DATA_ROOT']
except KeyError:
@ -34,6 +36,8 @@ def load_tests(loader, tests, ignore):
return tests
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Requires v2.0.0+ in order to run.")
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
"Missing openjp2 library.")

View file

@ -26,6 +26,8 @@ import glymur
from glymur import Jp2k
from glymur.lib import openjp2 as opj2
from .fixtures import OPENJP2_IS_V2_OFFICIAL
try:
data_root = os.environ['OPJ_DATA_ROOT']
except KeyError:
@ -477,6 +479,8 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(j.box[2].box[1].colorspace,
glymur.core.GREYSCALE)
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Does not seem to work on official v2.0.0 release.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_grey_with_extra_component(self):
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
@ -501,6 +505,8 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(j.box[2].box[1].colorspace,
glymur.core.GREYSCALE)
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Does not seem to work on official v2.0.0 release.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_rgb_with_extra_component(self):
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
@ -512,6 +518,17 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(j.box[2].box[0].num_components, 4)
self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB)
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL is False,
"Test is specific for v2.0.0 release")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_extra_components_on_v2_official(self):
# Extra components seems to require 2.0+. Verify that we error out.
with self.assertRaises(IOError):
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
data = np.zeros((128, 128, 4), dtype=np.uint8)
j.write(data)
def test_specify_ycc(self):
# We don't support writing YCC at the moment.
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
@ -655,6 +672,8 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(jasoc.box[3].box[1].box_id, 'xml ')
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Segfault on official v2.0.0 release.")
def test_openjpeg_library_message(self):
# Verify the error message produced by the openjpeg library.
# This will confirm that the error callback mechanism is working.

View file

@ -30,6 +30,9 @@ import numpy as np
from glymur import Jp2k
import glymur
from .fixtures import OPENJPEG_VERSION
from .fixtures import OPENJP2_IS_V2_OFFICIAL
from .fixtures import *
try:
@ -986,6 +989,8 @@ class TestSuite(unittest.TestCase):
data = Jp2k(jfile).read()
self.assertTrue(True)
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test known to fail in v2.0.0 official")
def test_NR_DEC_text_GBR_jp2_29_decode(self):
jfile = os.path.join(data_root,
'input/nonregression/text_GBR.jp2')
@ -1002,12 +1007,16 @@ class TestSuite(unittest.TestCase):
data = Jp2k(jfile).read()
self.assertTrue(True)
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test known to fail in v2.0.0 official")
def test_NR_DEC_kodak_2layers_lrcp_j2c_31_decode(self):
jfile = os.path.join(data_root,
'input/nonregression/kodak_2layers_lrcp.j2c')
data = Jp2k(jfile).read()
self.assertTrue(True)
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test known to fail in v2.0.0 official")
def test_NR_DEC_kodak_2layers_lrcp_j2c_32_decode(self):
jfile = os.path.join(data_root,
'input/nonregression/kodak_2layers_lrcp.j2c')
@ -1020,6 +1029,8 @@ class TestSuite(unittest.TestCase):
data = Jp2k(jfile).read()
self.assertTrue(True)
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test known to fail in v2.0.0 official")
def test_NR_DEC_mem_b2ace68c_1381_jp2_34_decode(self):
jfile = os.path.join(data_root,
'input/nonregression/mem-b2ace68c-1381.jp2')
@ -1030,6 +1041,8 @@ class TestSuite(unittest.TestCase):
data = j.read()
self.assertTrue(True)
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test known to fail in v2.0.0 official")
def test_NR_DEC_mem_b2b86b74_2753_jp2_35_decode(self):
jfile = os.path.join(data_root,
'input/nonregression/mem-b2b86b74-2753.jp2')
@ -1045,6 +1058,8 @@ class TestSuite(unittest.TestCase):
with self.assertRaises(IOError):
data = j.read()
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test not in done in v2.0.0 official")
def test_NR_DEC_jp2_36_decode(self):
lst = ('input',
'nonregression',
@ -1056,6 +1071,8 @@ class TestSuite(unittest.TestCase):
with self.assertRaises(IOError):
data = j.read()
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test not in done in v2.0.0 official")
def test_NR_DEC_gdal_fuzzer_check_number_of_tiles_jp2_38_decode(self):
relpath = 'input/nonregression/gdal_fuzzer_check_number_of_tiles.jp2'
jfile = os.path.join(data_root, relpath)
@ -1065,6 +1082,8 @@ class TestSuite(unittest.TestCase):
with self.assertRaises(IOError):
data = j.read()
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test not in done in v2.0.0 official")
def test_NR_DEC_gdal_fuzzer_check_comp_dx_dy_jp2_39_decode(self):
relpath = 'input/nonregression/gdal_fuzzer_check_comp_dx_dy.jp2'
jfile = os.path.join(data_root, relpath)
@ -1079,6 +1098,8 @@ class TestSuite(unittest.TestCase):
with self.assertRaises(RuntimeError):
data = Jp2k(jfile).read()
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test not in done in v2.0.0 official")
@unittest.skipIf(sys.hexversion < 0x03020000,
"Uses features introduced in 3.2.")
def test_NR_DEC_issue188_beach_64bitsbox_jp2_41_decode(self):
@ -1089,6 +1110,8 @@ class TestSuite(unittest.TestCase):
with self.assertWarns(UserWarning) as cw:
data = Jp2k(jfile).read()
@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL,
"Test not in done in v2.0.0 official")
def test_NR_DEC_issue206_image_000_jp2_42_decode(self):
jfile = os.path.join(data_root,
'input/nonregression/issue206_image-000.jp2')
@ -7779,7 +7802,8 @@ class TestSuite15(unittest.TestCase):
jfile = os.path.join(data_root, 'input/conformance/file9.jp2')
jp2k = Jp2k(jfile)
jpdata = jp2k.read()
if glymur.lib.openjpeg.version().startswith('1.3'):
if re.match('[01]\.3', OPENJPEG_VERSION):
# Version 1.3 reads in the image as the palette indices.
self.assertEqual(jpdata.shape, (512, 768))
else:
self.assertEqual(jpdata.shape, (512, 768, 3))
@ -7818,7 +7842,7 @@ class TestSuite15(unittest.TestCase):
data = jp2.read()
self.assertTrue(True)
@unittest.skipIf(int(glymur.lib.openjpeg.version().split('.')[1]) < 5,
@unittest.skipIf(re.match('[01]\.[34]', OPENJPEG_VERSION),
"Segfaults openjpeg 1.4 and earlier.")
def test_NR_DEC_broken2_jp2_5_decode(self):
# Null pointer access
@ -7841,7 +7865,7 @@ class TestSuite15(unittest.TestCase):
with self.assertRaises(ValueError) as ce:
d = j.read()
@unittest.skipIf(int(glymur.lib.openjpeg.version().split('.')[1]) < 5,
@unittest.skipIf(re.match('[01]\.[34]', OPENJPEG_VERSION),
"Segfaults openjpeg 1.4 and earlier.")
def test_NR_DEC_broken4_jp2_7_decode(self):
# Null pointer access

View file

@ -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,

View file

@ -1,35 +1,51 @@
| OS | Python | Python | Python | Notes |
| | 2.6 | 2.7 | 3.3 | |
+-----------+--------+--------+--------+--------------------------------------+
| Windows | | X | | Python(xy) with OpenJPEG 1.5.1 and |
| | | | | OpenJPEG svn. 285 of 448 tests |
| Windows | | X | | WinPython with OpenJPEG 1.5.1 and |
| | | | | OpenJPEG 2.0.0. 267 of 455 tests |
| | | | | pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Windows | | | X | WinPython with OpenJPEG 1.5.1 and |
| | | | | OpenJPEG svn. 307 of 455 tests |
| | | | | pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Mac | X | | | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG svn. 354 of 455 tests |
| 10.6.8 | | | | and OpenJPEG svn. 354 of 456 tests |
| | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Mac | X | | | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG 2.0. 315 of 456 tests |
| | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Mac | | X | | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG svn. 379 of 460 tests |
| 10.6.8 | | | | and OpenJPEG svn. 379 of 461 tests |
| | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Mac | | X | | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG 2.0. 340 of 461 tests |
| | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Mac | | | X | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG svn. 407 of 460 |
| 10.6.8 | | | | and OpenJPEG svn. 407 of 461 |
| | | | | tests should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Mac | | | X | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG 2.0. 355 of 461 |
| | | | | tests should pass. |
+-----------+--------+--------+-----------------------------------------------+
| Fedora 19 | | | X | Ships with 1.5.1, openjp2 built too. |
| | | | | 402 of 455 tests should pass. |
| Fedora 19 | | | X | Ships with 1.5.1, openjp2 svn built, |
| | | | | too. 407 of 461 tests should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Fedora 18 | | | X | Ships with 1.5.1. 169 of 449 tests |
| Fedora 18 | | X | | Ships with 1.5.1. 173 of 456 tests |
| | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Fedora 17 | | X | | Ships with 1.4.0. 166 of 450 tests |
| Fedora 17 | | X | | Ships with 1.4.0. 171 of 456 tests |
| | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| CentOS | X | | | Ships with 1.3.0. 169 of 455 tests |
| CentOS | X | | | Ships with 1.3.0. 169 of 456 tests |
| 6.3 | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+
| Raspberry | | X | | Ships with 1.3.0. 171 of 455 tests |
| Raspberry | | X | | Ships with 1.3.0. 171 of 456 tests |
| Pi | | | | should pass. |
| Debian 7 | | | | |
+-----------+--------+--------+--------+--------------------------------------+

View file

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
import sys
kwargs = {'name': 'Glymur',
'version': '0.2.8',
'version': '0.3.0',
'description': 'Tools for accessing JPEG2000 files',
'long_description': open('README.md').read(),
'author': 'John Evans',