diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/.travis.yml b/.travis.yml
index c055dce..6a57264 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,8 +11,8 @@ before_install:
# command to install dependencies
install:
- - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --use-mirrors lxml contextlib2 mock; fi
- - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install --use-mirrors lxml numpy; fi
+ - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install lxml contextlib2 mock; fi
+ - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install lxml numpy; fi
# command to run tests
script:
diff --git a/CHANGES.txt b/CHANGES.txt
index 24d9616..44d6832 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
-Feb 09, 2014 - Changed constructor for ChannelDefinition box. Removed support
+Mar 06, 2014 - Added Cinema2K, Cinema4K write support.
+ Changed constructor for ChannelDefinition box. Removed support
for Python 2.6. Added write support for JP2 UUID, DataEntryURL,
Palette and Component Mapping boxes, JPX Association, NumberList
and DataReference boxes. Added read support for JPX free,
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index bbba98e..c3eae6c 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -5,10 +5,12 @@ ChangeLog
0.6.0 (pending)
===============
+ * Added Cinema2K, Cinema4K write support.
* Added lxml requirement.
* added set_printoptions, get_printoptions function
* dropped support for Python 2.6, added support for Python 3.4
- * dropped windows support
+ * dropped support for OpenJPEG versions 1.3 and 1.4
+ * dropped windows support (it might work, it might not, I don't much care)
* added write support for JP2 UUID, dataEntryURL, palette, and component mapping boxes
* added read/write support for JPX free, number list, and data reference boxes
* Added read support for JPX fragment list and fragment table boxes
diff --git a/docs/source/detailed_installation.rst b/docs/source/detailed_installation.rst
index cfbb168..67c230a 100644
--- a/docs/source/detailed_installation.rst
+++ b/docs/source/detailed_installation.rst
@@ -13,7 +13,7 @@ both read and write JPEG 2000 files, but you may wish to install version 2.0
or the 2.0+ version from OpenJPEG's development trunk for better performance.
If you do that, you should compile it 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 r2354 works.
+via subversion. As of this time of writing, svn revision r2691 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
diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst
index 38193eb..10df117 100644
--- a/docs/source/introduction.rst
+++ b/docs/source/introduction.rst
@@ -14,17 +14,7 @@ XMP UUIDs. There is some very limited support for reading JPX metadata.
Glymur 0.6 works on Python versions 2.7, 3.3 and 3.4. If you have Python 2.6,
you should use the 0.5 series of Glymur.
-OpenJPEG Installation
-=====================
-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 1.5 or better, however, and the trunk/development
-version of OpenJPEG is strongly recommended. For more information about
-OpenJPEG, please consult http://www.openjpeg.org.
-
-If you use MacPorts or if you have a sufficiently recent version of
-Linux, your package manager should already provide you with a version of
-OpenJPEG 1.X which glymur can already use.
+For more information about OpenJPEG, please consult http://www.openjpeg.org.
Glymur Installation
===================
diff --git a/glymur/_uuid_io.py b/glymur/_uuid_io.py
index 399fa99..8cd5bcf 100644
--- a/glymur/_uuid_io.py
+++ b/glymur/_uuid_io.py
@@ -43,7 +43,7 @@ def tiff_header(read_buffer):
endian = '>'
else:
msg = "Bad byte order indication: {0}".format(read_buffer[6:8])
- raise RuntimeError(msg)
+ raise IOError(msg)
_, offset = struct.unpack(endian + 'HI', read_buffer[8:14])
diff --git a/glymur/codestream.py b/glymur/codestream.py
index 4259788..544b8a2 100644
--- a/glymur/codestream.py
+++ b/glymur/codestream.py
@@ -27,19 +27,36 @@ import numpy as np
from .core import LRCP, RLCP, RPCL, PCRL, CPRL
from .core import WAVELET_XFORM_9X7_IRREVERSIBLE
from .core import WAVELET_XFORM_5X3_REVERSIBLE
-from .core import _CAPABILITIES_DISPLAY
+from .core import _Keydefaultdict
from .lib import openjp2 as opj2
-_PROGRESSION_ORDER_DISPLAY = {
- LRCP: 'LRCP',
- RLCP: 'RLCP',
- RPCL: 'RPCL',
- PCRL: 'PCRL',
- CPRL: 'CPRL'}
+_factory = lambda x: '{0} (invalid)'.format(x)
+_PROGRESSION_ORDER_DISPLAY = _Keydefaultdict(_factory,
+ { LRCP: 'LRCP',
+ RLCP: 'RLCP',
+ RPCL: 'RPCL',
+ PCRL: 'PCRL',
+ CPRL: 'CPRL'})
-_WAVELET_TRANSFORM_DISPLAY = {
- WAVELET_XFORM_9X7_IRREVERSIBLE: '9-7 irreversible',
- WAVELET_XFORM_5X3_REVERSIBLE: '5-3 reversible'}
+_WAVELET_TRANSFORM_DISPLAY = _Keydefaultdict(_factory,
+ { WAVELET_XFORM_9X7_IRREVERSIBLE: '9-7 irreversible',
+ WAVELET_XFORM_5X3_REVERSIBLE: '5-3 reversible'})
+
+_NO_PROFILE = 0
+_PROFILE_0 = 1
+_PROFILE_1 = 2
+_PROFILE_3 = 3
+_PROFILE_4 = 4
+
+_KNOWN_PROFILES = [_NO_PROFILE, _PROFILE_0, _PROFILE_1, _PROFILE_3, _PROFILE_4]
+
+# How to display the codestream profile.
+_CAPABILITIES_DISPLAY = _Keydefaultdict(_factory,
+ { _NO_PROFILE: 'no profile',
+ _PROFILE_0: '0',
+ _PROFILE_1: '1',
+ _PROFILE_3: 'Cinema 2K',
+ _PROFILE_4: 'Cinema 4K'} )
# Need a catch-all list of valid markers.
# See table A-1 in ISO/IEC FCD15444-1.
@@ -371,6 +388,14 @@ class Codestream(object):
numbytes = offset + 2 + length - fptr.tell()
spcod = fptr.read(numbytes)
spcod = np.frombuffer(spcod, dtype=np.uint8)
+ if spcod[0] not in [LRCP, RLCP, RPCL, PCRL, CPRL]:
+ msg = "Invalid progression order in COD segment: {0}."
+ warnings.warn(msg.format(spcod[0]))
+
+ if spcod[8] not in [WAVELET_XFORM_9X7_IRREVERSIBLE,
+ WAVELET_XFORM_5X3_REVERSIBLE]:
+ msg = "Invalid wavelet transform in COD segment: {0}."
+ warnings.warn(msg.format(spcod[8]))
sop = (scod & 2) > 0
eph = (scod & 4) > 0
@@ -645,10 +670,13 @@ class Codestream(object):
read_buffer = fptr.read(2)
length, = struct.unpack('>H', read_buffer)
- xy_buffer = fptr.read(36)
- data = struct.unpack('>HIIIIIIIIH', xy_buffer)
+ read_buffer = fptr.read(length - 2)
+ data = struct.unpack_from('>HIIIIIIIIH', read_buffer)
rsiz = data[0]
+ if rsiz not in _KNOWN_PROFILES:
+ warnings.warn("Invalid profile: (Rsiz={0}).".format(rsiz))
+
xysiz = (data[1], data[2])
xyosiz = (data[3], data[4])
xytsiz = (data[5], data[6])
@@ -657,9 +685,8 @@ class Codestream(object):
# Csiz is the number of components
Csiz = data[9]
- component_buffer = fptr.read(Csiz * 3)
- data = struct.unpack('>' + 'B' * len(component_buffer),
- component_buffer)
+ data = struct.unpack_from('>' + 'B' * (length - 36 - 2),
+ read_buffer, offset=36)
bitdepth = tuple(((x & 0x7f) + 1) for x in data[0::3])
signed = tuple(((x & 0x80) > 0) for x in data[0::3])
@@ -673,6 +700,18 @@ class Codestream(object):
msg = msg.format(j, subsampling[0], subsampling[1])
warnings.warn(msg)
+ try:
+ num_tiles_x = (xysiz[0] - xyosiz[0]) / (xytsiz[0] - xytosiz[0])
+ num_tiles_y = (xysiz[1] - xyosiz[1]) / (xytsiz[1] - xytosiz[1])
+ except ZeroDivisionError as err:
+ warnings.warn("Invalid tile dimensions.")
+ else:
+ numtiles = math.ceil(num_tiles_x) * math.ceil(num_tiles_y)
+ if numtiles > 65535:
+ msg = "Invalid number of tiles ({0}).".format(numtiles)
+ warnings.warn(msg)
+
+
kwargs = {'rsiz': rsiz,
'xysiz': xysiz,
'xyosiz': xyosiz,
@@ -1514,14 +1553,6 @@ class SIZsegment(Segment):
lst.append(bitdepth - 1)
self.ssiz = tuple(lst)
- num_tiles_x = (self.xsiz - self.xosiz) / (self.xtsiz - self.xtosiz)
- num_tiles_y = (self.ysiz - self.yosiz) / (self.ytsiz - self.ytosiz)
- numtiles = math.ceil(num_tiles_x) * math.ceil(num_tiles_y)
- if numtiles > 65535:
- msg = "Invalid number of tiles ({0}).".format(numtiles)
- warnings.warn(msg)
-
-
def __repr__(self):
msg = "glymur.codestream.SIZsegment(rsiz={rsiz}, xysiz={xysiz}, "
msg += "xyosiz={xyosiz}, xytsiz={xytsiz}, xytosiz={xytosiz}, "
diff --git a/glymur/core.py b/glymur/core.py
index 4e8f950..07949f9 100644
--- a/glymur/core.py
+++ b/glymur/core.py
@@ -1,8 +1,22 @@
"""Core definitions to be shared amongst the modules.
"""
+import collections
import copy
import lxml.etree as ET
+class _Keydefaultdict(collections.defaultdict):
+ """Unlisted keys help form their own error message.
+
+ Normally defaultdict uses a factory function with no input arguments, but
+ that's not quite the behavior we want.
+ """
+ def __missing__(self, key):
+ if self.default_factory is None:
+ raise KeyError(key)
+ else:
+ ret = self[key] = self.default_factory(key)
+ return ret
+
# Progression order
LRCP = 0
RLCP = 1
@@ -10,6 +24,26 @@ RPCL = 2
PCRL = 3
CPRL = 4
+STD = 0
+CINEMA2K = 3
+CINEMA4K = 4
+
+RSIZ = {
+ 'STD': STD,
+ 'CINEMA2K': CINEMA2K,
+ 'CINEMA4K': CINEMA4K}
+
+OFF = 0
+CINEMA2K_24 = 1
+CINEMA2K_48 = 2
+CINEMA4K_24 = 3
+
+CINEMA_MODE = {
+ 'off': OFF,
+ 'cinema2k_24': CINEMA2K_24,
+ 'cinema2k_48': CINEMA2K_48,
+ 'cinema4k_24': CINEMA4K_24, }
+
PROGRESSION_ORDER = {
'LRCP': LRCP,
'RLCP': RLCP,
@@ -37,24 +71,26 @@ YCC = 18
E_SRGB = 20
ROMM_RGB = 21
-_COLORSPACE_MAP_DISPLAY = {
- CMYK: 'CMYK',
- SRGB: 'sRGB',
- GREYSCALE: 'greyscale',
- YCC: 'YCC',
- E_SRGB: 'e-sRGB',
- ROMM_RGB: 'ROMM-RGB'}
+_factory = lambda x: '{0} (unrecognized)'.format(x)
+_COLORSPACE_MAP_DISPLAY = _Keydefaultdict(_factory,
+ { CMYK: 'CMYK',
+ SRGB: 'sRGB',
+ GREYSCALE: 'greyscale',
+ YCC: 'YCC',
+ E_SRGB: 'e-sRGB',
+ ROMM_RGB: 'ROMM-RGB'} )
# enumerated color channel types
COLOR = 0
OPACITY = 1
PRE_MULTIPLIED_OPACITY = 2
_UNSPECIFIED = 65535
-_COLOR_TYPE_MAP_DISPLAY = {
- COLOR: 'color',
- OPACITY: 'opacity',
- PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity',
- _UNSPECIFIED: 'unspecified'}
+_factory = lambda x: '{0} (invalid)'.format(x)
+_COLOR_TYPE_MAP_DISPLAY = _Keydefaultdict(_factory,
+ { COLOR: 'color',
+ OPACITY: 'opacity',
+ PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity',
+ _UNSPECIFIED: 'unspecified'})
# color channel definitions.
RED = 1
@@ -70,9 +106,3 @@ _COLORSPACE = {SRGB: {"R": 1, "G": 2, "B": 3},
E_SRGB: {"R": 1, "G": 2, "B": 3},
ROMM_RGB: {"R": 1, "G": 2, "B": 3}}
-# How to display the codestream profile.
-_CAPABILITIES_DISPLAY = {
- 0: '2',
- 1: '0',
- 2: '1',
- 3: '3'}
diff --git a/glymur/data/12-v6.4.jpx b/glymur/data/12-v6.4.jpx
deleted file mode 100644
index a3e0c60..0000000
Binary files a/glymur/data/12-v6.4.jpx and /dev/null differ
diff --git a/glymur/data/__init__.py b/glymur/data/__init__.py
index 2bcd7f8..de1e62a 100644
--- a/glymur/data/__init__.py
+++ b/glymur/data/__init__.py
@@ -34,13 +34,13 @@ def goodstuff():
def jpxfile():
- """Shortcut for specifying path to 12-v6.4.jpx.
+ """Shortcut for specifying path to heliov.jpx.
Returns
-------
file : str
Platform-independent path to 12-v6.4.jpx
"""
- filename = pkg_resources.resource_filename(__name__, "12-v6.4.jpx")
+ filename = pkg_resources.resource_filename(__name__, "heliov.jpx")
return filename
diff --git a/glymur/data/heliov.jpx b/glymur/data/heliov.jpx
new file mode 100644
index 0000000..c7d5cdb
Binary files /dev/null and b/glymur/data/heliov.jpx differ
diff --git a/glymur/jp2box.py b/glymur/jp2box.py
index a06d87d..701483f 100644
--- a/glymur/jp2box.py
+++ b/glymur/jp2box.py
@@ -33,6 +33,7 @@ from .core import _COLOR_TYPE_MAP_DISPLAY
from .core import SRGB, GREYSCALE, YCC
from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD
+from .core import _Keydefaultdict
from . import _uuid_io
@@ -42,14 +43,12 @@ _METHOD_DISPLAY = {
ANY_ICC_PROFILE: 'any ICC profile',
VENDOR_COLOR_METHOD: 'vendor color method'}
-_APPROX_DISPLAY = {1: 'accurately represents correct colorspace definition',
- 2: 'approximates correct colorspace definition, '
- + 'exceptional quality',
- 3: 'approximates correct colorspace definition, '
- + 'reasonable quality',
- 4: 'approximates correct colorspace definition, '
- + 'poor quality'}
-
+_factory = lambda x: '{0} (invalid)'.format(x)
+_APPROX_DISPLAY = _Keydefaultdict(_factory,
+ {1: 'accurately represents correct colorspace definition',
+ 2: 'approximates correct colorspace definition, exceptional quality',
+ 3: 'approximates correct colorspace definition, reasonable quality',
+ 4: 'approximates correct colorspace definition, poor quality'})
class Jp2kBox(object):
"""Superclass for JPEG 2000 boxes.
@@ -86,6 +85,17 @@ class Jp2kBox(object):
msg += " @ ({0}, {1})".format(self.offset, self.length)
return msg
+ def _dispatch_validation_error(self, msg, writing=False):
+ """Issue either a warning or an error depending on circumstance.
+
+ If writing to file, then error out, as we do not wish to create bad
+ JP2 files. If reading, then we should be more lenient and just warn.
+ """
+ if writing:
+ raise IOError(msg)
+ else:
+ warnings.warn(msg)
+
def write(self, _):
"""Must be implemented in a subclass.
"""
@@ -115,7 +125,7 @@ class Jp2kBox(object):
String to be indented.
indent_level : str
Number of spaces of indentation to add.
-
+
Returns
-------
indented_string : str
@@ -166,34 +176,19 @@ class Jp2kBox(object):
object corresponding to the current box
"""
try:
- box = _BOX_WITH_ID[box_id].parse(fptr, start, num_bytes)
+ parser = _BOX_WITH_ID[box_id].parse
+
except KeyError:
+ # We don't recognize the box ID, so create an UnknownBox and be
+ # done with it.
msg = 'Unrecognized box ({0}) encountered.'.format(box_id)
warnings.warn(msg)
box = UnknownBox(box_id, offset=start, length=num_bytes,
longname='Unknown')
- cpos = fptr.tell()
- if not ((cpos == start + 8) or (cpos == start + 16)):
- # If the file pointer has advanced, then the KeyError
- # ocurred during the parsing of the box.
- pass
- else:
- # Could it be a superbox with recognizable child boxes?
- # Peek ahead to see.
- pos = fptr.tell()
- read_buffer = fptr.read(8)
- _, sub_id = struct.unpack('>I4s', read_buffer)
- sub_id = sub_id.decode('utf-8')
-
- # Regardless of whether or not we recognize the box, rewind back
- # to properly advance to the next box.
- fptr.seek(pos)
-
- # Now process any child boxes if we actually did recognize it.
- if sub_id in _BOX_WITH_ID.keys():
- box.box = box.parse_superbox(fptr)
+ return box
+ box = parser(fptr, start, num_bytes)
return box
def parse_superbox(self, fptr):
@@ -220,10 +215,12 @@ class Jp2kBox(object):
break
read_buffer = fptr.read(8)
- (box_length, box_id) = struct.unpack('>I4s', read_buffer)
- if sys.hexversion >= 0x03000000:
- box_id = box_id.decode('utf-8')
+ if len(read_buffer) < 8:
+ msg = "Extra bytes at end of file ignored."
+ warnings.warn(msg)
+ return superbox
+ (box_length, box_id) = struct.unpack('>I4s', read_buffer)
if box_length == 0:
# The length of the box is presumed to last until the end of
# the file. Compute the effective length of the box.
@@ -295,23 +292,29 @@ class ColourSpecificationBox(Jp2kBox):
approximation=0, colorspace=None, icc_profile=None,
length=0, offset=-1):
Jp2kBox.__init__(self, box_id='colr', longname='Colour Specification')
+
self.method = method
self.precedence = precedence
self.approximation = approximation
+
self.colorspace = colorspace
self.icc_profile = icc_profile
self.length = length
self.offset = offset
- self._validate()
- def _validate(self):
+ self._validate(writing=False)
+
+ def _validate(self, writing=False):
"""Verify that the box obeys the specifications."""
if self.colorspace is not None and self.icc_profile is not None:
- raise IOError("colorspace and icc_profile cannot both be set.")
+ msg = "Colorspace and icc_profile cannot both be set."
+ self._dispatch_validation_error(msg, writing=writing)
if self.method not in (1, 2, 3, 4):
- raise IOError("Invalid method.")
+ msg = "Invalid method.".format(self.method)
+ self._dispatch_validation_error(msg, writing=writing)
if self.approximation not in (0, 1, 2, 3, 4):
- raise IOError("Invalid approximation.")
+ msg = "Invalid approximation: {0}".format(self.approximation)
+ self._dispatch_validation_error(msg, writing=writing)
def _write_validate(self):
"""In addition to constructor validation steps, run validation steps
@@ -319,15 +322,15 @@ class ColourSpecificationBox(Jp2kBox):
if self.colorspace is None:
msg = "Writing Colour Specification boxes without enumerated "
msg += "colorspaces is not supported at this time."
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=True)
if self.icc_profile is None:
if self.colorspace not in [SRGB, GREYSCALE, YCC]:
msg = "Colorspace should correspond to one of SRGB, GREYSCALE, "
msg += "or YCC."
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=True)
- self._validate()
+ self._validate(writing=True)
def __repr__(self):
@@ -359,13 +362,16 @@ class ColourSpecificationBox(Jp2kBox):
else:
# 2.7 has trouble pretty-printing ordered dicts so we just have
# to print as a regular dict in this case.
- if sys.hexversion < 0x03000000:
- icc_profile = dict(self.icc_profile)
+ if self.icc_profile is None:
+ msg += '\n ICC Profile: None'
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))
+ 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))
return msg
@@ -375,7 +381,7 @@ class ColourSpecificationBox(Jp2kBox):
self._write_validate()
length = 15 if self.icc_profile is None else 11 + len(self.icc_profile)
fptr.write(struct.pack('>I', length))
- fptr.write('colr'.encode())
+ fptr.write(b'colr')
read_buffer = struct.pack('>BBBI',
self.method,
@@ -384,8 +390,8 @@ class ColourSpecificationBox(Jp2kBox):
self.colorspace)
fptr.write(read_buffer)
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse JPEG 2000 color specification box.
Parameters
@@ -406,19 +412,20 @@ class ColourSpecificationBox(Jp2kBox):
# Read the brand, minor version.
(method, precedence, approximation) = struct.unpack_from('>BBB',
read_buffer,
- 0)
+ offset=0)
if method == 1:
# enumerated colour space
- colorspace, = struct.unpack_from('>I', read_buffer, 3)
+ colorspace, = struct.unpack_from('>I', read_buffer, offset=3)
+ if colorspace not in _COLORSPACE_MAP_DISPLAY.keys():
+ msg = "Unrecognized colorspace: {0}".format(colorspace)
+ warnings.warn(msg)
icc_profile = None
else:
# ICC profile
colorspace = None
- if num_bytes < 131:
- # If the number of bytes is less than 128 + 3, then there
- # cannot possibly be enough for an ICC profile.
+ if (num_bytes - 3) < 128:
msg = "ICC profile header is corrupt, length is "
msg += "only {0} instead of 128."
warnings.warn(msg.format(num_bytes - 3), UserWarning)
@@ -427,14 +434,13 @@ class ColourSpecificationBox(Jp2kBox):
profile = _ICCProfile(read_buffer[3:])
icc_profile = profile.header
- box = ColourSpecificationBox(method=method,
- precedence=precedence,
- approximation=approximation,
- colorspace=colorspace,
- icc_profile=icc_profile,
- length=length,
- offset=offset)
- return box
+ return cls(method=method,
+ precedence=precedence,
+ approximation=approximation,
+ colorspace=colorspace,
+ icc_profile=icc_profile,
+ length=length,
+ offset=offset)
class _ICCProfile(object):
@@ -581,15 +587,15 @@ class ChannelDefinitionBox(Jp2kBox):
self.channel_type = tuple(channel_type)
self.association = tuple(association)
self.__dict__.update(**kwargs)
- self._validate()
+ self._validate(writing=False)
- def _validate(self):
+ def _validate(self, writing=False):
"""Verify that the box obeys the specifications."""
# channel type and association must be specified.
if not ((len(self.index) == len(self.channel_type)) and
(len(self.channel_type) == len(self.association))):
msg = "Length of channel definition box inputs must be the same."
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=writing)
# channel types must be one of 0, 1, 2, 65535
if any(x not in [0, 1, 2, 65535] for x in self.channel_type):
@@ -598,7 +604,7 @@ class ChannelDefinitionBox(Jp2kBox):
msg += " 1 - opacity\n"
msg += " 2 - premultiplied opacity\n"
msg += " 65535 - unspecified"
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=writing)
def __str__(self):
@@ -625,10 +631,10 @@ class ChannelDefinitionBox(Jp2kBox):
def write(self, fptr):
"""Write a channel definition box to file.
"""
- self._validate()
+ self._validate(writing=True)
num_components = len(self.association)
fptr.write(struct.pack('>I', 8 + 2 + num_components * 6))
- fptr.write('cdef'.encode('utf-8'))
+ fptr.write(b'cdef')
fptr.write(struct.pack('>H', num_components))
for j in range(num_components):
fptr.write(struct.pack('>' + 'H' * 3,
@@ -636,8 +642,8 @@ class ChannelDefinitionBox(Jp2kBox):
self.channel_type[j],
self.association[j]))
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse component definition box.
Parameters
@@ -653,21 +659,22 @@ class ChannelDefinitionBox(Jp2kBox):
-------
ComponentDefinitionBox instance
"""
- # Read the number of components.
- read_buffer = fptr.read(2)
- num_components, = struct.unpack('>H', read_buffer)
+ num_bytes = offset + length - fptr.tell()
+ read_buffer = fptr.read(num_bytes)
- read_buffer = fptr.read(num_components * 6)
- data = struct.unpack('>' + 'HHH' * num_components, read_buffer)
+ # Read the number of components.
+ num_components, = struct.unpack_from('>H', read_buffer)
+
+ data = struct.unpack_from('>' + 'HHH' * num_components, read_buffer,
+ offset=2)
index = data[0:num_components * 6:3]
channel_type = data[1:num_components * 6:3]
association = data[2:num_components * 6:3]
- box = ChannelDefinitionBox(index=tuple(index),
- channel_type=tuple(channel_type),
- association=tuple(association),
- length=length, offset=offset)
- return box
+ return cls(index=tuple(index),
+ channel_type=tuple(channel_type),
+ association=tuple(association),
+ length=length, offset=offset)
class CodestreamHeaderBox(Jp2kBox):
@@ -700,8 +707,13 @@ class CodestreamHeaderBox(Jp2kBox):
msg = self._str_superbox()
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ def write(self, fptr):
+ """Write a codestream header box to file.
+ """
+ self._write_superbox(fptr)
+
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse codestream header box.
Parameters
@@ -717,7 +729,7 @@ class CodestreamHeaderBox(Jp2kBox):
-------
CodestreamHeaderBox instance
"""
- box = CodestreamHeaderBox(length=length, offset=offset)
+ box = cls(length=length, offset=offset)
# The codestream header box is a superbox, so go ahead and parse its
# child boxes.
@@ -726,6 +738,75 @@ class CodestreamHeaderBox(Jp2kBox):
return box
+class ColourGroupBox(Jp2kBox):
+ """Container for colour group 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, box=None, length=0, offset=-1):
+ Jp2kBox.__init__(self, box_id='cgrp', longname='Colour Group')
+ self.length = length
+ self.offset = offset
+ self.box = box if box is not None else []
+
+ def __repr__(self):
+ msg = "glymur.jp2box.ColourGroupBox(box={0})".format(self.box)
+ return msg
+
+ def __str__(self):
+ msg = self._str_superbox()
+ return msg
+
+ def _validate(self, writing=True):
+ """Verify that the box obeys the specifications."""
+ if any([box.box_id != 'colr' for box in self.box]):
+ msg = "Colour group boxes can only contain colour specification "
+ msg += "boxes."
+ self._dispatch_validation_error(msg, writing=writing)
+
+ def write(self, fptr):
+ """Write a colour group box to file.
+ """
+ self._validate(writing=True)
+ self._write_superbox(fptr)
+
+ @classmethod
+ def parse(cls, fptr, offset, length):
+ """Parse colour group box.
+
+ Parameters
+ ----------
+ fptr : file
+ Open file object.
+ offset : int
+ Start position of box in bytes.
+ length : int
+ Length of the box in bytes.
+
+ Returns
+ -------
+ ColourGroupBox instance
+ """
+ box = cls(length=length, offset=offset)
+
+ # The colour group box is a superbox, so go ahead and parse its
+ # child boxes.
+ box.box = box.parse_superbox(fptr)
+
+ return box
+
+
class CompositingLayerHeaderBox(Jp2kBox):
"""Container for compositing layer header box information.
@@ -758,8 +839,13 @@ class CompositingLayerHeaderBox(Jp2kBox):
msg = self._str_superbox()
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ def write(self, fptr):
+ """Write a compositing layer header box to file.
+ """
+ self._write_superbox(fptr)
+
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse compositing layer header box.
Parameters
@@ -775,7 +861,7 @@ class CompositingLayerHeaderBox(Jp2kBox):
-------
CompositingLayerHeaderBox instance
"""
- box = CompositingLayerHeaderBox(length=length, offset=offset)
+ box = cls(length=length, offset=offset)
# This box is a superbox, so go ahead and parse its # child boxes.
box.box = box.parse_superbox(fptr)
@@ -831,7 +917,7 @@ class ComponentMappingBox(Jp2kBox):
msg = msg.format(self.component_index[k],
self.palette_index[k])
else:
- msg += '\n Component %d ==> %d'
+ msg += '\n Component {0} ==> {1}'
msg = msg.format(self.component_index[k], k)
return msg
@@ -839,7 +925,7 @@ class ComponentMappingBox(Jp2kBox):
"""Write a Component Mapping box to file.
"""
length = 8 + 4 * len(self.component_index)
- write_buffer = struct.pack('>I4s', length, self.box_id.encode())
+ write_buffer = struct.pack('>I4s', length, b'cmap')
fptr.write(write_buffer)
for j in range(len(self.component_index)):
@@ -849,8 +935,8 @@ class ComponentMappingBox(Jp2kBox):
self.palette_index[j])
fptr.write(write_buffer)
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse component mapping box.
Parameters
@@ -872,13 +958,12 @@ class ComponentMappingBox(Jp2kBox):
read_buffer = fptr.read(num_bytes)
data = struct.unpack('>' + 'HBB' * num_components, read_buffer)
- component_index = data[0:num_bytes:num_components]
- mapping_type = data[1:num_bytes:num_components]
- palette_index = data[2:num_bytes:num_components]
+ component_index = data[0:num_bytes:3]
+ mapping_type = data[1:num_bytes:3]
+ palette_index = data[2:num_bytes:3]
- box = ComponentMappingBox(component_index, mapping_type, palette_index,
- length=length, offset=offset)
- return box
+ return cls(component_index, mapping_type, palette_index,
+ length=length, offset=offset)
class ContiguousCodestreamBox(Jp2kBox):
@@ -920,8 +1005,8 @@ class ContiguousCodestreamBox(Jp2kBox):
return msg
- @staticmethod
- def parse(fptr, offset=0, length=0):
+ @classmethod
+ def parse(cls, fptr, offset=0, length=0):
"""Parse a codestream box.
Parameters
@@ -938,9 +1023,7 @@ class ContiguousCodestreamBox(Jp2kBox):
ContiguousCodestreamBox instance
"""
main_header = Codestream(fptr, length, header_only=True)
- box = ContiguousCodestreamBox(main_header, length=length,
- offset=offset)
- return box
+ return cls(main_header, length=length, offset=offset)
class DataReferenceBox(Jp2kBox):
@@ -967,23 +1050,23 @@ class DataReferenceBox(Jp2kBox):
self.DR = data_entry_url_boxes
self.length = length
self.offset = offset
- self._validate()
+ self._validate(writing=False)
- def _validate(self):
+ def _validate(self, writing=False):
"""Verify that the box obeys the specifications."""
for box in self.DR:
if box.box_id != 'url ':
msg = 'All child boxes of a data reference box must be data '
msg += 'entry URL boxes.'
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=writing)
def _write_validate(self):
"""Verify that the box obeys the specifications for writing.
"""
if len(self.DR) == 0:
msg = "A data reference box cannot be empty when written to a file."
- raise IOError(msg)
- self._validate()
+ self._dispatch_validation_error(msg, writing=True)
+ self._validate(writing=True)
def write(self, fptr):
"""Write a Data Reference box to file.
@@ -993,7 +1076,7 @@ class DataReferenceBox(Jp2kBox):
# Very similar to the say a superbox is written.
orig_pos = fptr.tell()
fptr.write(struct.pack('>I', 0))
- fptr.write(self.box_id.encode())
+ fptr.write(b'dtbl')
# Write the number of data entry url boxes.
write_buffer = struct.pack('>H', len(self.DR))
@@ -1020,8 +1103,8 @@ class DataReferenceBox(Jp2kBox):
msg = 'glymur.jp2box.DataReferenceBox()'
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse data reference box.
Parameters
@@ -1053,8 +1136,7 @@ class DataReferenceBox(Jp2kBox):
box = DataEntryURLBox.parse(fptr, start, box_length)
data_entry_url_box_list.append(box)
- return DataReferenceBox(data_entry_url_box_list,
- length=length, offset=offset)
+ return cls(data_entry_url_box_list, length=length, offset=offset)
class FileTypeBox(Jp2kBox):
@@ -1089,6 +1171,7 @@ class FileTypeBox(Jp2kBox):
self.compatibility_list = compatibility_list
self.length = length
self.offset = offset
+ self._validate(writing=False)
def __repr__(self):
msg = "glymur.jp2box.FileTypeBox(brand='{0}', minor_version={1}, "
@@ -1110,33 +1193,34 @@ class FileTypeBox(Jp2kBox):
return msg
- def _validate(self):
+ def _validate(self, writing=False):
"""Validate the box before writing to file."""
if self.brand not in ['jp2 ', 'jpx ']:
msg = "The file type brand must be either 'jp2 ' or 'jpx '."
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=writing)
valid_cls = ['jp2 ', 'jpx ', 'jpxb']
for item in self.compatibility_list:
if item not in valid_cls:
msg = "The file type compatibility list item '{0}' is not "
msg += "valid: valid entries are {1}"
- raise IOError(msg.format(item, valid_cls))
+ msg = msg.format(item, valid_cls)
+ self._dispatch_validation_error(msg, writing=writing)
def write(self, fptr):
"""Write a File Type box to file.
"""
- self._validate()
+ self._validate(writing=True)
length = 16 + 4*len(self.compatibility_list)
fptr.write(struct.pack('>I', length))
- fptr.write('ftyp'.encode())
+ fptr.write(b'ftyp')
fptr.write(self.brand.encode())
fptr.write(struct.pack('>I', self.minor_version))
for item in self.compatibility_list:
fptr.write(item.encode())
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse JPEG 2000 file type box.
Parameters
@@ -1169,10 +1253,9 @@ class FileTypeBox(Jp2kBox):
compatibility_list = compatibility_list
- box = FileTypeBox(brand=brand, minor_version=minor_version,
- compatibility_list=compatibility_list,
- length=length, offset=offset)
- return box
+ return cls(brand=brand, minor_version=minor_version,
+ compatibility_list=compatibility_list,
+ length=length, offset=offset)
class FragmentListBox(Jp2kBox):
@@ -1197,18 +1280,21 @@ class FragmentListBox(Jp2kBox):
self.data_reference = data_reference
self.length = length
self.offset = offset
+ self._validate(writing=False)
- def _validate(self):
+ def _validate(self, writing=False):
"""Validate internal correctness."""
if (((len(self.fragment_offset) != len(self.fragment_length)) or
(len(self.fragment_length) != len(self.data_reference)))):
msg = "The lengths of the fragment offsets, fragment lengths, and "
msg += "data reference items must be the same."
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=writing)
if any([x <= 0 for x in self.fragment_offset]):
- raise IOError("Fragment offsets must all be positive.")
+ msg = "Fragment offsets must all be positive."
+ self._dispatch_validation_error(msg, writing=writing)
if any([x <= 0 for x in self.fragment_length]):
- raise IOError("Fragment lengths must all be positive.")
+ msg = "Fragment lengths must all be positive."
+ self._dispatch_validation_error(msg, writing=writing)
def __repr__(self):
msg = "glymur.jp2box.FragmentListBox()"
@@ -1232,11 +1318,11 @@ class FragmentListBox(Jp2kBox):
def write(self, fptr):
"""Write a fragment list box to file.
"""
- self._validate()
+ self._validate(writing=True)
num_items = len(self.fragment_offset)
length = 8 + 2 + num_items * 14
fptr.write(struct.pack('>I', length))
- fptr.write(self.box_id.encode())
+ fptr.write(b'flst')
fptr.write(struct.pack('>H', num_items))
for j in range(num_items):
write_buffer = struct.pack('>QIH',
@@ -1245,9 +1331,9 @@ class FragmentListBox(Jp2kBox):
self.data_reference[j])
fptr.write(write_buffer)
- @staticmethod
- def parse(fptr, offset, length):
- """Parse JPX fragment list box.
+ @classmethod
+ def parse(cls, fptr, offset, length):
+ """Parse JPX free box.
Parameters
----------
@@ -1272,8 +1358,8 @@ class FragmentListBox(Jp2kBox):
frag_offset = lst[0::3]
frag_len = lst[1::3]
data_reference = lst[2::3]
- return FragmentListBox(frag_offset, frag_len, data_reference,
- length=length, offset=offset)
+ return cls(frag_offset, frag_len, data_reference,
+ length=length, offset=offset)
class FragmentTableBox(Jp2kBox):
@@ -1304,8 +1390,8 @@ class FragmentTableBox(Jp2kBox):
msg = self._str_superbox()
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse JPX fragment table superbox box.
Parameters
@@ -1321,7 +1407,7 @@ class FragmentTableBox(Jp2kBox):
-------
FragmentTableBox instance
"""
- box = FragmentTableBox(length=length, offset=offset)
+ box = cls(length=length, offset=offset)
# The FragmentTable box is a superbox, so go ahead and parse its child
# boxes.
@@ -1329,18 +1415,18 @@ class FragmentTableBox(Jp2kBox):
return box
- def _validate(self):
+ def _validate(self, writing=False):
"""Self-validate the box before writing."""
box_ids = [box.box_id for box in self.box]
if len(box_ids) != 1 or box_ids[0] != 'flst':
msg = "Fragment table boxes must have a single fragment list "
msg += "box as a child box."
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=writing)
def write(self, fptr):
"""Write a fragment table box to file.
"""
- self._validate()
+ self._validate(writing=True)
self._write_superbox(fptr)
@@ -1375,8 +1461,8 @@ class FreeBox(Jp2kBox):
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse JPX free box.
Parameters
@@ -1392,7 +1478,7 @@ class FreeBox(Jp2kBox):
-------
FreeBox instance
"""
- return FreeBox(length=length, offset=offset)
+ return cls(length=length, offset=offset)
class ImageHeaderBox(Jp2kBox):
@@ -1484,7 +1570,7 @@ class ImageHeaderBox(Jp2kBox):
"""Write an Image Header box to file.
"""
fptr.write(struct.pack('>I', 22))
- fptr.write('ihdr'.encode())
+ fptr.write(b'ihdr')
# signedness and bps are stored together in a single byte
bit_depth_signedness = 0x80 if self.signed else 0x00
@@ -1499,8 +1585,8 @@ class ImageHeaderBox(Jp2kBox):
1 if self.ip_provided else 0)
fptr.write(read_buffer)
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse JPEG 2000 image header box.
Parameters
@@ -1528,14 +1614,13 @@ class ImageHeaderBox(Jp2kBox):
colorspace_unknown = True if params[5] else False
ip_provided = True if params[6] else False
- box = ImageHeaderBox(height, width, num_components=num_components,
- bits_per_component=bits_per_component,
- signed=signed,
- compression=compression,
- colorspace_unknown=colorspace_unknown,
- ip_provided=ip_provided,
- length=length, offset=offset)
- return box
+ return cls(height, width, num_components=num_components,
+ bits_per_component=bits_per_component,
+ signed=signed,
+ compression=compression,
+ colorspace_unknown=colorspace_unknown,
+ ip_provided=ip_provided,
+ length=length, offset=offset)
class AssociationBox(Jp2kBox):
@@ -1568,8 +1653,8 @@ class AssociationBox(Jp2kBox):
msg = self._str_superbox()
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse association box.
Parameters
@@ -1585,7 +1670,7 @@ class AssociationBox(Jp2kBox):
-------
AssociationBox instance
"""
- box = AssociationBox(length=length, offset=offset)
+ box = cls(length=length, offset=offset)
# The Association box is a superbox, so go ahead and parse its child
# boxes.
@@ -1634,8 +1719,8 @@ class JP2HeaderBox(Jp2kBox):
"""
self._write_superbox(fptr)
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse JPEG 2000 header box.
Parameters
@@ -1651,7 +1736,7 @@ class JP2HeaderBox(Jp2kBox):
-------
JP2HeaderBox instance
"""
- box = JP2HeaderBox(length=length, offset=offset)
+ box = cls(length=length, offset=offset)
# The JP2 header box is a superbox, so go ahead and parse its child
# boxes.
@@ -1699,11 +1784,11 @@ class JPEG2000SignatureBox(Jp2kBox):
"""Write a JPEG 2000 Signature box to file.
"""
fptr.write(struct.pack('>I', 12))
- fptr.write(self.box_id.encode())
+ fptr.write(b'jP ')
fptr.write(struct.pack('>BBBB', *self.signature))
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse JPEG 2000 signature box.
Parameters
@@ -1722,9 +1807,7 @@ class JPEG2000SignatureBox(Jp2kBox):
read_buffer = fptr.read(4)
signature = struct.unpack('>BBBB', read_buffer)
- box = JPEG2000SignatureBox(signature=signature, length=length,
- offset=offset)
- return box
+ return cls(signature=signature, length=length, offset=offset)
class PaletteBox(Jp2kBox):
@@ -1751,15 +1834,15 @@ class PaletteBox(Jp2kBox):
self.signed = signed
self.length = length
self.offset = offset
- self._validate()
+ self._validate(writing=False)
- def _validate(self):
+ def _validate(self, writing=False):
"""Verify that the box obeys the specifications."""
if ((len(self.bits_per_component) != len(self.signed)) or
(len(self.signed) != self.palette.shape[1])):
msg = "The length of the 'bits_per_component' and the 'signed' "
msg += "members must equal the number of columns of the palette."
- raise IOError(msg)
+ self._dispatch_validation_error(msg, writing=writing)
def __repr__(self):
msg = "glymur.jp2box.PaletteBox({0}, bits_per_component={1}, "
@@ -1779,14 +1862,13 @@ class PaletteBox(Jp2kBox):
def write(self, fptr):
"""Write a Palette box to file.
"""
- self._validate()
+ self._validate(writing=True)
bytes_per_row = sum(self.bits_per_component) / 8
bytes_per_palette = bytes_per_row * self.palette.shape[0]
box_length = 8 + 3 + self.palette.shape[1] + bytes_per_palette
# Write the usual header.
- write_buffer = struct.pack('>I4s',
- int(box_length), self.box_id.encode())
+ write_buffer = struct.pack('>I4s', int(box_length), b'pclr')
fptr.write(write_buffer)
write_buffer = struct.pack('>HB', self.palette.shape[0],
@@ -1801,20 +1883,30 @@ class PaletteBox(Jp2kBox):
*bps_signed)
fptr.write(write_buffer)
- if self.bits_per_component[0] <= 8:
- code = 'B'
- elif self.bits_per_component[0] <= 16:
- code = 'H'
- elif self.bits_per_component[0] <= 32:
- code = 'I'
-
- fmt = '>' + code * self.palette.shape[1]
- for row in self.palette:
- write_buffer = struct.pack(fmt, *row)
+ bps = self.bits_per_component
+ if all(b == bps[0] for b in bps):
+ # All components are the same. Writing is straightforward.
+ if self.bits_per_component[0] <= 8:
+ write_buffer = memoryview(self.palette.astype(np.uint8))
+ elif self.bits_per_component[0] <= 16:
+ write_buffer = memoryview(self.palette.astype(np.uint16))
+ elif self.bits_per_component[0] <= 32:
+ write_buffer = memoryview(self.palette.astype(np.uint32))
fptr.write(write_buffer)
+ else:
+ # Not all the components are the same. More general, but much rarer
+ # case. Does this even happen.
+ code_dict = {8: 'B', 16: 'H', 32: 'I'}
+ codes = ''
+ for width in bps:
+ codes += code_dict[width]
+ fmt = '>' + codes
+ for row in self.palette:
+ write_buffer = struct.pack(fmt, *row)
+ fptr.write(write_buffer)
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse palette box.
Parameters
@@ -1835,29 +1927,50 @@ class PaletteBox(Jp2kBox):
(nrows, ncols) = struct.unpack_from('>HB', read_buffer, offset=0)
# Need to determine bps and signed or not
- data = struct.unpack_from('>' + 'B' * ncols, read_buffer, offset=3)
- bps = [((x & 0x7f) + 1) for x in data]
- signed = [((x & 0x80) > 1) for x in data]
+ read_buffer = fptr.read(num_columns)
+ bps_signed = struct.unpack('>' + 'B' * num_columns, read_buffer)
+ bps = [((x & 0x7f) + 1) for x in bps_signed]
+ signed = [((x & 0x80) > 1) for x in bps_signed]
- fmt = '>'
- for bits in bps:
- if bits <= 8:
- fmt += 'B'
- elif bits <= 16:
- fmt += 'H'
- elif bits <= 32:
- fmt += 'I'
+ if all(b == bps_signed[0] for b in bps_signed):
+ # Ok the palette has the same datatype for all columns. We should
+ # be able to efficiently read it.
+ if bps[0] <= 8:
+ nbytes_per_row = num_columns
+ dtype = np.uint8
+ elif bps[0] <= 16:
+ nbytes_per_row = 2 * num_columns
+ dtype = np.uint16
+ elif bps[0] <= 32:
+ nbytes_per_row = 3 * num_columns
+ dtype = np.uint32
- # Each palette component is padded out to the next largest byte.
- # That means a list comprehension does this in one shot.
- row_nbytes = sum([int(math.ceil(x/8.0)) for x in bps])
+ read_buffer = fptr.read(num_entries * nbytes_per_row)
+ palette = np.frombuffer(read_buffer, dtype=dtype)
+ palette = np.reshape(palette, (num_entries, num_columns))
- palette = np.zeros((nrows, ncols), dtype=np.int32)
- for j in range(nrows):
- palette[j] = struct.unpack_from(fmt, read_buffer,
- offset=j * row_nbytes + 3 + ncols)
+ else:
+ # General case where the columns may not be the same width.
+ fmt = '>'
+ for bits in bps:
+ if bits <= 8:
+ fmt += 'B'
+ elif bits <= 16:
+ fmt += 'H'
+ elif bits <= 32:
+ fmt += 'I'
- return PaletteBox(palette, bps, signed, length=length, offset=offset)
+ # Each palette component is padded out to the next largest byte.
+ # That means a list comprehension does this in one shot.
+ row_nbytes = sum([int(math.ceil(x/8.0)) for x in bps])
+
+ read_buffer = fptr.read(num_entries * row_nbytes)
+ palette = np.zeros((num_entries, num_columns), dtype=np.int32)
+ for j in range(num_entries):
+ palette[j] = struct.unpack_from(fmt, read_buffer,
+ offset=j * row_nbytes)
+
+ return cls(palette, bps, signed, length=length, offset=offset)
# Map rreq codes to display text.
@@ -2023,8 +2136,8 @@ class ReaderRequirementsBox(Jp2kBox):
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse reader requirements box.
Parameters
@@ -2070,10 +2183,9 @@ class ReaderRequirementsBox(Jp2kBox):
msg += 'The box contents will not be interpreted.'
warnings.warn(msg.format(mask_length), UserWarning)
- box = ReaderRequirementsBox(fuam, dcm, standard_flag, standard_mask,
- vendor_feature, vendor_mask,
- length=length, offset=offset)
- return box
+ return cls(fuam, dcm, standard_flag, standard_mask,
+ vendor_feature, vendor_mask,
+ length=length, offset=offset)
def _parse_rreq3(fptr, length, offset):
@@ -2231,8 +2343,8 @@ class ResolutionBox(Jp2kBox):
msg = self._str_superbox()
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse Resolution box.
Parameters
@@ -2248,7 +2360,7 @@ class ResolutionBox(Jp2kBox):
-------
ResolutionBox instance
"""
- box = ResolutionBox(length=length, offset=offset)
+ box = cls(length=length, offset=offset)
# The JP2 header box is a superbox, so go ahead and parse its child
# boxes.
@@ -2295,8 +2407,8 @@ class CaptureResolutionBox(Jp2kBox):
msg += '\n HCR: {0}'.format(self.horizontal_resolution)
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse CaptureResolutionBox.
Parameters
@@ -2317,9 +2429,7 @@ class CaptureResolutionBox(Jp2kBox):
vres = rn1 / rd1 * math.pow(10, re1)
hres = rn2 / rd2 * math.pow(10, re2)
- box = CaptureResolutionBox(vres, hres, length=length, offset=offset)
-
- return box
+ return cls(vres, hres, length=length, offset=offset)
class DisplayResolutionBox(Jp2kBox):
@@ -2360,8 +2470,8 @@ class DisplayResolutionBox(Jp2kBox):
msg += '\n HDR: {0}'.format(self.horizontal_resolution)
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse display resolution box.
Parameters
@@ -2383,9 +2493,7 @@ class DisplayResolutionBox(Jp2kBox):
vres = rn1 / rd1 * math.pow(10, re1)
hres = rn2 / rd2 * math.pow(10, re2)
- box = DisplayResolutionBox(vres, hres, length=length, offset=offset)
-
- return box
+ return cls(vres, hres, length=length, offset=offset)
class LabelBox(Jp2kBox):
@@ -2427,11 +2535,11 @@ class LabelBox(Jp2kBox):
"""
length = 8 + len(self.label.encode())
fptr.write(struct.pack('>I', length))
- fptr.write(self.box_id.encode())
+ fptr.write(b'lbl ')
fptr.write(self.label.encode())
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse Label box.
Parameters
@@ -2450,8 +2558,7 @@ class LabelBox(Jp2kBox):
num_bytes = offset + length - fptr.tell()
read_buffer = fptr.read(num_bytes)
label = read_buffer.decode('utf-8')
- box = LabelBox(label, length=length, offset=offset)
- return box
+ return cls(label, length=length, offset=offset)
class NumberListBox(Jp2kBox):
@@ -2493,7 +2600,7 @@ class NumberListBox(Jp2kBox):
elif (association >> 24) == 2:
idx = association & 0x00FFFFFF
msg += 'Compositing Layer {0}'
- msg = msg.format(j, idx)
+ msg = msg.format(idx)
else:
msg += 'unrecognized'
return msg
@@ -2502,9 +2609,9 @@ class NumberListBox(Jp2kBox):
msg = 'glymur.jp2box.NumberListBox()'
return msg
- @staticmethod
- def parse(fptr, offset, length):
- """Parse Label box.
+ @classmethod
+ def parse(cls, fptr, offset, length):
+ """Parse number list box.
Parameters
----------
@@ -2523,14 +2630,13 @@ class NumberListBox(Jp2kBox):
raw_data = fptr.read(num_bytes)
num_associations = int(len(raw_data) / 4)
lst = struct.unpack('>' + 'I' * num_associations, raw_data)
- box = NumberListBox(lst, length=length, offset=offset)
- return box
+ return cls(lst, length=length, offset=offset)
def write(self, fptr):
"""Write a NumberList box to file.
"""
fptr.write(struct.pack('>I', len(self.associations) * 4 + 8))
- fptr.write(self.box_id.encode())
+ fptr.write(b'nlst')
fmt = '>' + 'I' * len(self.associations)
write_buffer = struct.pack(fmt, *self.associations)
@@ -2604,11 +2710,11 @@ class XMLBox(Jp2kBox):
read_buffer = ET.tostring(self.xml.getroot(), encoding='utf-8')
fptr.write(struct.pack('>I', len(read_buffer) + 8))
- fptr.write(self.box_id.encode())
+ fptr.write(b'xml ')
fptr.write(read_buffer)
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse XML box.
Parameters
@@ -2628,26 +2734,39 @@ class XMLBox(Jp2kBox):
read_buffer = fptr.read(num_bytes)
try:
text = read_buffer.decode('utf-8')
- except UnicodeDecodeError as ude:
+ except UnicodeDecodeError as err:
# Possibly bad string of bytes to begin with.
# Try to search for -1:
- text = read_buffer[decl_start:].decode('utf-8')
- else:
- raise
+ if decl_start <= -1:
+ msg = 'A problem was encountered while parsing an XML box:'
+ msg += '\n\n\t"{0}"\n\nNo XML was retrieved.'
+ warnings.warn(msg.format(str(err)))
+ return XMLBox(xml=None, length=length, offset=offset)
+
+ text = read_buffer[decl_start:].decode('utf-8')
# Let the user know that the XML box was problematic.
msg = 'A UnicodeDecodeError was encountered parsing an XML box at '
msg += 'byte position {0} ({1}), but the XML was still recovered.'
- msg = msg.format(offset, ude.reason)
+ msg = msg.format(offset, err.reason)
warnings.warn(msg, UserWarning)
# Strip out any trailing nulls, as they can foul up XML parsing.
+ # Remove any byte order markers.
text = text.rstrip(chr(0))
+ if u'\ufeff' in text:
+ msg = 'An illegal BOM (byte order marker) was detected and '
+ msg += 'removed from the XML contents in the box starting at byte '
+ msg += 'offset {0}'.format(offset)
+ warnings.warn(msg)
+ text = text.replace(u'\ufeff', '')
+ # Remove any encoding declaration.
+ if text.startswith(''):
+ text = text[38:]
try:
- elt = ET.fromstring(text.encode('utf-8'))
+ elt = ET.fromstring(text)
xml = ET.ElementTree(elt)
except ET.ParseError as err:
msg = 'A problem was encountered while parsing an XML box:'
@@ -2656,8 +2775,7 @@ class XMLBox(Jp2kBox):
warnings.warn(msg, UserWarning)
xml = None
- box = XMLBox(xml=xml, length=length, offset=offset)
- return box
+ return cls(xml=xml, length=length, offset=offset)
class UUIDListBox(Jp2kBox):
@@ -2695,8 +2813,8 @@ class UUIDListBox(Jp2kBox):
msg += '\n UUID[{0}]: {1}'.format(j, uuid_item)
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse UUIDList box.
Parameters
@@ -2712,16 +2830,17 @@ class UUIDListBox(Jp2kBox):
-------
UUIDListBox instance
"""
- read_buffer = fptr.read(2)
- num_uuids, = struct.unpack('>H', read_buffer)
+ num_bytes = offset + length - fptr.tell()
+ read_buffer = fptr.read(num_bytes)
+
+ num_uuids, = struct.unpack_from('>H', read_buffer)
ulst = []
- for _ in range(num_uuids):
- read_buffer = fptr.read(16)
- ulst.append(uuid.UUID(bytes=read_buffer))
+ for j in range(num_uuids):
+ uuid_buffer = read_buffer[2 + j * 16 : 2 + (j + 1) * 16]
+ ulst.append(uuid.UUID(bytes=uuid_buffer))
- box = UUIDListBox(ulst, length=length, offset=offset)
- return box
+ return cls(ulst, length=length, offset=offset)
class UUIDInfoBox(Jp2kBox):
@@ -2754,8 +2873,8 @@ class UUIDInfoBox(Jp2kBox):
msg = self._str_superbox()
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse UUIDInfo super box.
Parameters
@@ -2772,7 +2891,7 @@ class UUIDInfoBox(Jp2kBox):
UUIDInfoBox instance
"""
- box = UUIDInfoBox(length=length, offset=offset)
+ box = cls(length=length, offset=offset)
# The UUIDInfo box is a superbox, so go ahead and parse its child
# boxes.
@@ -2819,7 +2938,7 @@ class DataEntryURLBox(Jp2kBox):
length = 8 + 1 + 3 + len(url.encode())
write_buffer = struct.pack('>I4sBBBB',
- length, self.box_id.encode(),
+ length, b'url ',
self.version,
self.flag[0], self.flag[1], self.flag[2])
fptr.write(write_buffer)
@@ -2847,8 +2966,8 @@ class DataEntryURLBox(Jp2kBox):
self.url)
return msg
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse data entry URL box.
Parameters
@@ -2872,8 +2991,7 @@ class DataEntryURLBox(Jp2kBox):
numbytes = offset + length - fptr.tell()
read_buffer = fptr.read(numbytes)
url = read_buffer.decode('utf-8').rstrip(chr(0))
- box = DataEntryURLBox(version, flag, url, length=length, offset=offset)
- return box
+ return cls(version, flag, url, length=length, offset=offset)
class UnknownBox(Jp2kBox):
@@ -2957,7 +3075,11 @@ class UUIDBox(Jp2kBox):
try:
self._parse_raw_data()
- except RuntimeError as error:
+ except KeyError as error:
+ # Such as when an Exif tag is unrecognized.
+ warnings.warn(str(error))
+ except IOError as error:
+ # Such as when Exif byte order is unrecognized.
warnings.warn(str(error))
def _parse_raw_data(self):
@@ -3018,8 +3140,8 @@ class UUIDBox(Jp2kBox):
fptr.write(self.uuid.bytes)
fptr.write(self.raw_data)
- @staticmethod
- def parse(fptr, offset, length):
+ @classmethod
+ def parse(cls, fptr, offset, length):
"""Parse UUID box.
Parameters
@@ -3041,39 +3163,39 @@ class UUIDBox(Jp2kBox):
numbytes = offset + length - fptr.tell()
read_buffer = fptr.read(numbytes)
- box = UUIDBox(the_uuid, read_buffer, length=length, offset=offset)
- return box
+ return cls(the_uuid, read_buffer, length=length, offset=offset)
# Map each box ID to the corresponding class.
_BOX_WITH_ID = {
- 'asoc': AssociationBox,
- 'cdef': ChannelDefinitionBox,
- 'cmap': ComponentMappingBox,
- 'colr': ColourSpecificationBox,
- 'dtbl': DataReferenceBox,
- 'ftyp': FileTypeBox,
- 'ihdr': ImageHeaderBox,
- 'jP ': JPEG2000SignatureBox,
- 'jpch': CodestreamHeaderBox,
- 'jplh': CompositingLayerHeaderBox,
- 'jp2c': ContiguousCodestreamBox,
- 'free': FreeBox,
- 'flst': FragmentListBox,
- 'ftbl': FragmentTableBox,
- 'jp2h': JP2HeaderBox,
- 'lbl ': LabelBox,
- 'nlst': NumberListBox,
- 'pclr': PaletteBox,
- 'res ': ResolutionBox,
- 'resc': CaptureResolutionBox,
- 'resd': DisplayResolutionBox,
- 'rreq': ReaderRequirementsBox,
- 'uinf': UUIDInfoBox,
- 'ulst': UUIDListBox,
- 'url ': DataEntryURLBox,
- 'uuid': UUIDBox,
- 'xml ': XMLBox}
+ b'asoc': AssociationBox,
+ b'cdef': ChannelDefinitionBox,
+ b'cgrp': ColourGroupBox,
+ b'cmap': ComponentMappingBox,
+ b'colr': ColourSpecificationBox,
+ b'dtbl': DataReferenceBox,
+ b'ftyp': FileTypeBox,
+ b'ihdr': ImageHeaderBox,
+ b'jP ': JPEG2000SignatureBox,
+ b'jpch': CodestreamHeaderBox,
+ b'jplh': CompositingLayerHeaderBox,
+ b'jp2c': ContiguousCodestreamBox,
+ b'free': FreeBox,
+ b'flst': FragmentListBox,
+ b'ftbl': FragmentTableBox,
+ b'jp2h': JP2HeaderBox,
+ b'lbl ': LabelBox,
+ b'nlst': NumberListBox,
+ b'pclr': PaletteBox,
+ b'res ': ResolutionBox,
+ b'resc': CaptureResolutionBox,
+ b'resd': DisplayResolutionBox,
+ b'rreq': ReaderRequirementsBox,
+ b'uinf': UUIDInfoBox,
+ b'ulst': UUIDListBox,
+ b'url ': DataEntryURLBox,
+ b'uuid': UUIDBox,
+ b'xml ': XMLBox}
_printoptions = {'short': False, 'xml': True, 'codestream': True}
diff --git a/glymur/jp2k.py b/glymur/jp2k.py
index 6db0198..8110c50 100644
--- a/glymur/jp2k.py
+++ b/glymur/jp2k.py
@@ -20,6 +20,7 @@ from collections import Counter
import ctypes
import math
import os
+import re
import struct
from uuid import UUID
import warnings
@@ -28,7 +29,7 @@ import numpy as np
from .codestream import Codestream
from .core import SRGB, GREYSCALE
-from .core import PROGRESSION_ORDER
+from .core import PROGRESSION_ORDER, CINEMA_MODE
from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
from .jp2box import Jp2kBox
from .jp2box import JPEG2000SignatureBox, FileTypeBox, JP2HeaderBox
@@ -153,6 +154,36 @@ class Jp2k(Jp2kBox):
msg += "profile if the file type box brand is 'jp2 '."
warnings.warn(msg)
+ def _set_cinema_params(self, cparams, cinema_mode, fps):
+ """Populate compression parameters structure for cinema2K.
+
+ Parameters
+ ----------
+ params : ctypes struct
+ Corresponds to compression parameters structure used by the
+ library.
+ cinema_mode : str
+ Either 'cinema2k' or 'cinema4k'
+ fps : int
+ Frames per second, should be either 24 or 48.
+ """
+ if re.match("(1.5|2.0)", version.openjpeg_version) is not None:
+ msg = "Writing Cinema2K or Cinema4K files is not supported with "
+ msg += 'openjpeg library versions less than 2.0.1.'
+ raise IOError(msg)
+
+ if cinema_mode == 'cinema2k':
+ if fps == 24:
+ cparams.cp_cinema = CINEMA_MODE['cinema2k_24']
+ elif fps == 48:
+ cparams.cp_cinema = CINEMA_MODE['cinema2k_48']
+ else:
+ raise IOError('Cinema2K frame rate must be either 24 or 48.')
+ else:
+ cparams.cp_cinema = CINEMA_MODE['cinema4k_24']
+
+ return
+
def _populate_cparams(self, **kwargs):
"""Populate compression parameters structure from input arguments.
@@ -219,6 +250,14 @@ class Jp2k(Jp2kBox):
cparams.tcp_numlayers = 1
cparams.cp_disto_alloc = 1
+ if 'cinema2k' in kwargs:
+ self._set_cinema_params(cparams, 'cinema2k', kwargs['cinema2k'])
+ return cparams
+
+ if 'cinema4k' in kwargs:
+ self._set_cinema_params(cparams, 'cinema4k', kwargs['cinema4k'])
+ return cparams
+
if 'cbsize' in kwargs:
cparams.cblockw_init = kwargs['cbsize'][1]
cparams.cblockh_init = kwargs['cbsize'][0]
@@ -298,6 +337,10 @@ class Jp2k(Jp2kBox):
colorspace : int
Either CLRSPC_SRGB or CLRSPC_GRAY
"""
+ if (('cinema2k' in kwargs or 'cinema4k' in kwargs) and
+ (len(set(kwargs)) > 1)):
+ msg = "Cannot specify cinema2k/cinema4k along with other options."
+ raise IOError(msg)
if 'cratios' in kwargs and 'psnr' in kwargs:
msg = "Cannot specify cratios and psnr together."
@@ -340,6 +383,10 @@ class Jp2k(Jp2kBox):
Image data to be written to file.
cbsize : tuple, optional
Code block size (DY, DX).
+ cinema2k : int, optional
+ frames per second, either 24 or 48
+ cinema4k : bool, optional
+ Set to True to specify Cinema4K mode, defaults to false.
colorspace : str, optional
Either 'rgb' or 'gray'.
cratios : iterable
@@ -395,7 +442,7 @@ class Jp2k(Jp2kBox):
If glymur is unable to load the openjp2 library.
"""
if opj2.OPENJP2 is not None:
- self._write_openjp2(img_array, verbose=verbose, **kwargs)
+ self._write_openjp2(img_array, verbose=verbose, **kwargs)
elif opj.OPENJPEG is not None:
self._write_openjpeg(img_array, verbose=verbose, **kwargs)
else:
@@ -473,7 +520,7 @@ class Jp2k(Jp2kBox):
def _write_openjp2(self, img_array, verbose=False, **kwargs):
"""
- Write JPEG 2000 file using OpenJPEG 1.5 interface.
+ Write JPEG 2000 file using OpenJPEG 2.0 interface.
"""
cparams, colorspace = self._process_write_inputs(img_array, **kwargs)
@@ -559,7 +606,12 @@ class Jp2k(Jp2kBox):
self.parse()
def wrap(self, filename, boxes=None):
- """Write the codestream back out to file, wrapped in new JP2 jacket.
+ """Create a new JP2/JPX file wrapped in a new set of JP2 boxes.
+
+ This method is primarily aimed at wrapping a raw codestream in a set of
+ of JP2 boxes (turning it into a JP2 file instead of just a raw
+ codestream), or rewrapping a codestream in a JP2 file in a new "jacket"
+ of JP2 boxes.
Parameters
----------
@@ -569,6 +621,8 @@ class Jp2k(Jp2kBox):
JP2 box definitions to define the JP2 file format. If not
provided, a default ""jacket" is assumed, consisting of JP2
signature, file type, JP2 header, and contiguous codestream boxes.
+ A JPX file rewrapped without the boxes argument results in a JP2
+ file encompassing the first codestream.
Returns
-------
@@ -584,19 +638,7 @@ class Jp2k(Jp2kBox):
>>> jp2 = j2k.wrap(tfile.name)
"""
if boxes is None:
- # Try to create a reasonable default.
- boxes = [JPEG2000SignatureBox(),
- FileTypeBox(),
- JP2HeaderBox(),
- ContiguousCodestreamBox()]
- codestream = self.get_codestream()
- height = codestream.segment[1].ysiz
- width = codestream.segment[1].xsiz
- num_components = len(codestream.segment[1].xrsiz)
- boxes[2].box = [ImageHeaderBox(height=height,
- width=width,
- num_components=num_components),
- ColourSpecificationBox(colorspace=SRGB)]
+ boxes = self._get_default_jp2_boxes()
_validate_jp2_box_sequence(boxes)
@@ -605,34 +647,92 @@ class Jp2k(Jp2kBox):
if box.box_id != 'jp2c':
box.write(ofile)
else:
- # The codestream gets written last.
- if len(self.box) == 0:
- # Am I a raw codestream? If so, then it is pretty
- # easy, just write the codestream box header plus all
- # of myself out to file.
- ofile.write(struct.pack('>I', self.length + 8))
- ofile.write('jp2c'.encode())
- with open(self.filename, 'rb') as ifile:
- ofile.write(ifile.read())
- else:
- # OK, I'm a jp2 file. Need to find out where the
- # raw codestream actually starts.
- jp2c = [box for box in self.box
- if box.box_id == 'jp2c']
- jp2c = jp2c[0]
- ofile.write(struct.pack('>I', jp2c.length))
- ofile.write('jp2c'.encode())
- with open(self.filename, 'rb') as ifile:
- # Seek 8 bytes past the L, T fields to get to the
- # raw codestream.
- ifile.seek(jp2c.offset + 8)
- ofile.write(ifile.read(jp2c.length - 8))
-
+ self._write_wrapped_codestream(ofile, box)
ofile.flush()
jp2 = Jp2k(filename)
return jp2
+ def _write_wrapped_codestream(self, ofile, box):
+ """Write wrapped codestream."""
+ # Codestreams require a bit more care.
+ # Am I a raw codestream?
+ if len(self.box) == 0:
+ # Yes, just write the codestream box header plus all
+ # of myself out to file.
+ ofile.write(struct.pack('>I', self.length + 8))
+ ofile.write(b'jp2c')
+ with open(self.filename, 'rb') as ifile:
+ ofile.write(ifile.read())
+ return
+
+ # OK, I'm a jp2/jpx file. Need to find out where the raw codestream
+ # actually starts.
+ offset = box.offset
+ if offset == -1:
+ if self.box[1].brand == 'jpx ':
+ msg = "The codestream box must have its offset and "
+ msg += "length attributes fully specified if the file "
+ msg += "type brand is JPX."
+ raise IOError(msg)
+
+ # Find the first codestream in the file.
+ jp2c = [box for box in self.box if box.box_id == 'jp2c']
+ offset = jp2c[0].offset
+
+ # Ready to write the codestream.
+ with open(self.filename, 'rb') as ifile:
+ ifile.seek(offset)
+
+ # Verify that the specified codestream is right.
+ read_buffer = ifile.read(8)
+ L, T = struct.unpack_from('>I4s', read_buffer, 0)
+ if T != b'jp2c':
+ msg = "Unable to locate the specified codestream."
+ raise IOError(msg)
+ if L == 0:
+ # The length of the box is presumed to last until the end of
+ # the file. Compute the effective length of the box.
+ L = os.path.getsize(ifile.name) - ifile.tell() + 8
+
+ elif L == 1:
+ # The length of the box is in the XL field, a 64-bit value.
+ read_buffer = ifile.read(8)
+ L, = struct.unpack('>Q', read_buffer)
+
+ ifile.seek(offset)
+ read_buffer = ifile.read(L)
+ ofile.write(read_buffer)
+
+ def _get_default_jp2_boxes(self):
+ """Create a default set of JP2 boxes."""
+ # Try to create a reasonable default.
+ boxes = [JPEG2000SignatureBox(),
+ FileTypeBox(),
+ JP2HeaderBox(),
+ ContiguousCodestreamBox()]
+ codestream = self.get_codestream()
+ height = codestream.segment[1].ysiz
+ width = codestream.segment[1].xsiz
+ num_components = len(codestream.segment[1].xrsiz)
+ if num_components < 3:
+ colorspace = GREYSCALE
+ else:
+ if len(self.box) == 0:
+ # Best guess is SRGB
+ colorspace = SRGB
+ else:
+ # Take whatever the first jp2 header / color specification
+ # says.
+ jp2hs = [box for box in self.box if box.box_id == 'jp2h']
+ colorspace = jp2hs[0].box[1].colorspace
+
+ boxes[2].box = [ImageHeaderBox(height=height, width=width,
+ num_components=num_components),
+ ColourSpecificationBox(colorspace=colorspace)]
+
+ return boxes
+
def read(self, **kwargs):
"""Read a JPEG 2000 image.
@@ -649,6 +749,9 @@ class Jp2k(Jp2kBox):
(first_row, first_col, last_row, last_col)
tile : int, optional
Number of tile to decode.
+ ignore_pclr_cmap_cdef : bool
+ Whether or not to ignore the pclr, cmap, or cdef boxes during any
+ color transformation. Defaults to False.
verbose : bool, optional
Print informational messages produced by the OpenJPEG library.
@@ -703,7 +806,8 @@ class Jp2k(Jp2kBox):
msg += "the read_bands method instead."
raise RuntimeError(msg)
- def _read_openjpeg(self, rlevel=0, verbose=False):
+ def _read_openjpeg(self, rlevel=0, ignore_pclr_cmap_cdef=False,
+ verbose=False):
"""Read a JPEG 2000 image using libopenjpeg.
Parameters
@@ -711,6 +815,9 @@ class Jp2k(Jp2kBox):
rlevel : int, optional
Factor by which to rlevel output resolution. Use -1 to get the
lowest resolution thumbnail.
+ ignore_pclr_cmap_cdef : bool
+ Whether or not to ignore the pclr, cmap, or cdef boxes during any
+ color transformation. Defaults to False.
verbose : bool, optional
Print informational messages produced by the OpenJPEG library.
@@ -726,32 +833,12 @@ class Jp2k(Jp2kBox):
"""
self._subsampling_sanity_check()
- if rlevel != 0:
- # Must check the specified rlevel against the maximum.
- # OpenJPEG 1.3 will segfault if rlevel is too high.
- codestream = self.get_codestream()
- max_rlevel = codestream.segment[2].spcod[4]
- if rlevel == -1:
- # -1 is shorthand for the largest rlevel
- rlevel = max_rlevel
- if rlevel < -1 or rlevel > max_rlevel:
- msg = "rlevel must be in the range [-1, {0}] for this image."
- msg = msg.format(max_rlevel)
- raise IOError(msg)
+ dparameters = self._populate_dparam(rlevel, ignore_pclr_cmap_cdef)
with ExitStack() as stack:
try:
- # Set decoding parameters.
- dparameters = opj.DecompressionParametersType()
- opj.set_default_decoder_parameters(ctypes.byref(dparameters))
- dparameters.cp_reduce = rlevel
dparameters.decod_format = self._codec_format
- infile = self.filename.encode()
- nelts = opj.PATH_LEN - len(infile)
- infile += b'0' * nelts
- dparameters.infile = infile
-
dinfo = opj.create_decompress(dparameters.decod_format)
event_mgr = opj.EventMgrType()
@@ -788,7 +875,7 @@ class Jp2k(Jp2kBox):
return data
def _read_openjp2(self, rlevel=0, layer=0, area=None, tile=None,
- verbose=False):
+ verbose=False, ignore_pclr_cmap_cdef=False):
"""Read a JPEG 2000 image using libopenjp2.
Parameters
@@ -818,7 +905,8 @@ class Jp2k(Jp2kBox):
"""
self._subsampling_sanity_check()
- dparam = self._populate_dparam(layer, rlevel, area, tile)
+ dparam = self._populate_dparam(rlevel, ignore_pclr_cmap_cdef,
+ layer=layer, tile=tile, area=area)
with ExitStack() as stack:
if hasattr(opj2.OPENJP2,
@@ -862,27 +950,35 @@ class Jp2k(Jp2kBox):
return img_array
- def _populate_dparam(self, layer, rlevel, area, tile):
+ def _populate_dparam(self, rlevel, ignore_pclr_cmap_cdef, tile=None,
+ layer=None, area=None):
"""Populate decompression structure with appropriate input parameters.
Parameters
----------
- layer : int, optional
+ layer : int
Number of quality layer to decode.
- rlevel : int, optional
+ rlevel : int
Factor by which to rlevel output resolution.
- area : tuple, optional
+ area : tuple
Specifies decoding image area,
(first_row, first_col, last_row, last_col)
- tile : int, optional
+ tile : int
Number of tile to decode.
+ ignore_pclr_cmap_cdef : bool
+ Whether or not to ignore the pclr, cmap, or cdef boxes during any
+ color transformation. Defaults to False.
Returns
-------
dparam : DecompressionParametersType (ctypes)
Corresponds to openjp2 decompression parameters structure.
"""
- dparam = opj2.set_default_decoder_parameters()
+ if opj2.OPENJP2 is not None:
+ dparam = opj2.set_default_decoder_parameters()
+ else:
+ dparam = opj.DecompressionParametersType()
+ opj.set_default_decoder_parameters(ctypes.byref(dparam))
infile = self.filename.encode()
nelts = opj2.PATH_LEN - len(infile)
@@ -891,12 +987,22 @@ class Jp2k(Jp2kBox):
dparam.decod_format = self._codec_format
- dparam.cp_layer = layer
+ if layer is not None:
+ dparam.cp_layer = layer
- if rlevel == -1:
- # Get the lowest resolution thumbnail.
+ # Must check the specified rlevel against the maximum.
+ if rlevel != 0:
+ # Must check the specified rlevel against the maximum.
codestream = self.get_codestream()
- rlevel = codestream.segment[2].spcod[4]
+ max_rlevel = codestream.segment[2].spcod[4]
+ if rlevel == -1:
+ # -1 is shorthand for the largest rlevel
+ rlevel = max_rlevel
+ elif rlevel < -1 or rlevel > max_rlevel:
+ msg = "rlevel must be in the range [-1, {0}] for this image."
+ msg = msg.format(max_rlevel)
+ raise IOError(msg)
+
dparam.cp_reduce = rlevel
if area is not None:
@@ -913,10 +1019,14 @@ class Jp2k(Jp2kBox):
dparam.tile_index = tile
dparam.nb_tile_to_decode = 1
+ if ignore_pclr_cmap_cdef is True:
+ # Return raw codestream components.
+ dparam.flags |= 1
+
return dparam
def read_bands(self, rlevel=0, layer=0, area=None, tile=None,
- verbose=False):
+ verbose=False, ignore_pclr_cmap_cdef=False):
"""Read a JPEG 2000 image.
The only time you should use this method is when the image has
@@ -934,6 +1044,9 @@ class Jp2k(Jp2kBox):
(first_row, first_col, last_row, last_col)
tile : int, optional
Number of tile to decode.
+ ignore_pclr_cmap_cdef : bool
+ Whether or not to ignore the pclr, cmap, or cdef boxes during any
+ color transformation. Defaults to False.
verbose : bool, optional
Print informational messages produced by the OpenJPEG library.
@@ -963,7 +1076,8 @@ class Jp2k(Jp2kBox):
"of OpenJP2 installed before using "
"this functionality.")
- dparam = self._populate_dparam(layer, rlevel, area, tile)
+ dparam = self._populate_dparam(rlevel, ignore_pclr_cmap_cdef,
+ layer=layer, tile=tile, area=area)
with ExitStack() as stack:
if hasattr(opj2.OPENJP2,
@@ -1025,7 +1139,7 @@ class Jp2k(Jp2kBox):
>>> codestream = jp2.get_codestream()
>>> print(codestream.segment[1])
SIZ marker segment @ (3233, 47)
- Profile: 2
+ Profile: no profile
Reference Grid Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Grid Offset: (0 x 0)
Reference Tile Height, Width: (1456 x 2592)
@@ -1125,12 +1239,26 @@ def _validate_jp2_box_sequence(boxes):
if boxes[1].brand == 'jpx ':
_validate_jpx_box_sequence(boxes)
else:
+ # Validate the JP2 box IDs.
count = _collect_box_count(boxes)
- for id in count.keys():
- if id not in JP2_IDS:
+ for box_id in count.keys():
+ if box_id not in JP2_IDS:
msg = "The presence of a '{0}' box requires that the file type "
msg += "brand be set to 'jpx '."
- raise IOError(msg.format(id))
+ 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."
+ raise IOError(msg)
def _validate_jpx_box_sequence(boxes):
"""Run through series of tests for JPX box legality."""
@@ -1231,7 +1359,7 @@ 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 != 'jp2h':
+ 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]))
@@ -1507,6 +1635,10 @@ def _populate_image_struct(cparams, image, imgdata):
# Stage the image data to the openjpeg data structure.
for k in range(0, num_comps):
+ if cparams.cp_cinema:
+ image.contents.comps[k].prec = 12
+ image.contents.comps[k].bpp = 12
+
layer = np.ascontiguousarray(imgdata[:, :, k], dtype=np.int32)
dest = image.contents.comps[k].data
src = layer.ctypes.data
diff --git a/glymur/lib/openjp2.py b/glymur/lib/openjp2.py
index f056db0..2da8d75 100644
--- a/glymur/lib/openjp2.py
+++ b/glymur/lib/openjp2.py
@@ -10,6 +10,20 @@ import sys
from .config import glymur_config
OPENJP2, OPENJPEG = glymur_config()
+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
+
+if OPENJP2 is not None:
+ _MAJOR, _MINOR, _PATCH = version().split('.')
+else:
+ _MINOR = 0
+
ERROR_MSG_LST = []
# Map certain atomic OpenJPEG datatypes to the ctypes equivalents.
@@ -35,6 +49,7 @@ CLRSPC_UNSPECIFIED = 0
CLRSPC_SRGB = 1
CLRSPC_GRAY = 2
CLRSPC_YCC = 3
+CLRSPC_EYCC = 4
COLOR_SPACE_TYPE = ctypes.c_int
# supported codec
@@ -392,6 +407,8 @@ class ImageCompType(ctypes.Structure):
# image component data
("data", ctypes.POINTER(ctypes.c_int32))]
+ if _MINOR == '1':
+ _fields_.append(("alpha", ctypes.c_uint16))
class ImageType(ctypes.Structure):
"""Defines image data and characteristics.
diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py
index 666fb6b..1ee7aa1 100644
--- a/glymur/test/fixtures.py
+++ b/glymur/test/fixtures.py
@@ -22,7 +22,7 @@ try:
HAS_PYTHON_XMP_TOOLKIT = True
else:
HAS_PYTHON_XMP_TOOLKIT = False
-except ImportError:
+except:
HAS_PYTHON_XMP_TOOLKIT = False
# Need to know of the libopenjp2 version is the official 2.0.0 release and NOT
@@ -412,7 +412,7 @@ Contiguous Codestream Box (jp2c) @ (3223, 1132296)
Main header:
SOC marker segment @ (3231, 0)
SIZ marker segment @ (3233, 47)
- Profile: 2
+ Profile: no profile
Reference Grid Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Grid Offset: (0 x 0)
Reference Tile Height, Width: (1456 x 2592)
@@ -477,7 +477,7 @@ Contiguous Codestream Box (jp2c) @ (3223, 1132296)
Main header:
SOC marker segment @ (3231, 0)
SIZ marker segment @ (3233, 47)
- Profile: 2
+ Profile: no profile
Reference Grid Height, Width: (1456 x 2592)
Vertical, Horizontal Reference Grid Offset: (0 x 0)
Reference Tile Height, Width: (1456 x 2592)
@@ -554,15 +554,21 @@ UUID Box (uuid) @ (77, 3146)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
Contiguous Codestream Box (jp2c) @ (3223, 1132296)"""
-# Output of reader requirement printing for file7.jp2
-file7_rreq = r"""Reader Requirements Box (rreq) @ (44, 24)
- Fully Understands Aspect Mask: 0xa0
- Display Completely Mask: 0xc0
+# Output of reader requirements printing for text_GBR.jp2
+text_GBR_rreq = r"""Reader Requirements Box (rreq) @ (40, 109)
+ Fully Understands Aspect Mask: 0xffff
+ Display Completely Mask: 0xf8f0
Standard Features and Masks:
- Feature 005: 0x80 Unrestricted JPEG 2000 Part 1 codestream, ITU-T Rec. T.800 | ISO/IEC 15444-1
- Feature 060: 0x60 e-sRGB enumerated colorspace
- Feature 043: 0x40 Deprecated - compositing layer uses restricted ICC profile
- Vendor Features:"""
+ Feature 001: 0x8000 Deprecated - contains no extensions
+ Feature 005: 0x4080 Unrestricted JPEG 2000 Part 1 codestream, ITU-T Rec. T.800 | ISO/IEC 15444-1
+ Feature 012: 0x2040 Deprecated - codestream is contiguous
+ Feature 018: 0x1020 Deprecated - support for compositing is not required
+ Feature 044: 0x810 Compositing layer uses Any ICC profile
+ Vendor Features:
+ UUID 3a0d0218-0ae9-4115-b376-4bca41ce0e71
+ UUID 47c92ccc-d1a1-4581-b904-38bb5467713b
+ UUID bc45a774-dd50-4ec6-a9f6-f3a137f47e90
+ UUID d7c8c5ef-951f-43b2-8757-042500f538e8"""
file1_xml = """XML Box (xml ) @ (36, 439)
@@ -571,3 +577,48 @@ file1_xml = """XML Box (xml ) @ (36, 439)
\t\tProfessional 120 Image
\t
"""
+
+issue_182_cmap = """Component Mapping Box (cmap) @ (130, 24)
+ Component 0 ==> palette column 0
+ Component 0 ==> palette column 1
+ Component 0 ==> palette column 2
+ Component 0 ==> palette column 3"""
+
+issue_183_colr = """Colour Specification Box (colr) @ (62, 12)
+ Method: restricted ICC profile
+ Precedence: 0
+ ICC Profile: None"""
+
+
+# Progression order is invalid.
+issue_186_progression_order = """COD marker segment @ (174, 12)
+ Coding style:
+ Entropy coder, without partitions
+ SOP marker segments: False
+ EPH marker segments: False
+ Coding style parameters:
+ Progression order: 33 (invalid)
+ Number of layers: 1
+ Multiple component transformation usage: reversible
+ Number of resolutions: 6
+ Code block height, width: (32 x 32)
+ Wavelet transform: 9-7 irreversible
+ Precinct size: default, 2^15 x 2^15
+ Code block context:
+ Selective arithmetic coding bypass: False
+ Reset context probabilities on coding pass boundaries: False
+ Termination on each coding pass: False
+ Vertically stripe causal context: False
+ Predictable termination: False
+ Segmentation symbols: False"""
+
+# Cinema 2K profile
+cinema2k_profile = """SIZ marker segment @ (2, 47)
+ Profile: Cinema 2K
+ Reference Grid Height, Width: (1080 x 1920)
+ Vertical, Horizontal Reference Grid Offset: (0 x 0)
+ Reference Tile Height, Width: (1080 x 1920)
+ Vertical, Horizontal Reference Tile Offset: (0 x 0)
+ Bitdepth: (12, 12, 12)
+ Signed: (False, False, False)
+ Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1))"""
diff --git a/glymur/test/test_codestream.py b/glymur/test/test_codestream.py
index 5edd144..8bac19c 100644
--- a/glymur/test/test_codestream.py
+++ b/glymur/test/test_codestream.py
@@ -13,17 +13,12 @@ import struct
import sys
import tempfile
import unittest
+import warnings
from glymur import Jp2k
import glymur
-try:
- DATA_ROOT = os.environ['OPJ_DATA_ROOT']
-except KeyError:
- DATA_ROOT = None
-except:
- raise
-
+from .fixtures import opj_data_file, OPJ_DATA_ROOT
class TestCodestream(unittest.TestCase):
"""Test suite for unusual codestream cases."""
@@ -34,8 +29,73 @@ class TestCodestream(unittest.TestCase):
def tearDown(self):
pass
- @unittest.skipIf(DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
+ def test_siz_segment_ssiz_unsigned(self):
+ """ssiz attribute to be removed in future release"""
+ j = Jp2k(self.jp2file)
+ codestream = j.get_codestream()
+
+ # The ssiz attribute was simply a tuple of raw bytes.
+ # The first 7 bits are interpreted as the bitdepth, the MSB determines
+ # whether or not it is signed.
+ self.assertEqual(codestream.segment[1].ssiz, (7, 7, 7))
+
+
+@unittest.skipIf(OPJ_DATA_ROOT is None,
+ "OPJ_DATA_ROOT environment variable not set")
+class TestCodestreamOpjData(unittest.TestCase):
+ """Test suite for unusual codestream cases. Uses OPJ_DATA_ROOT"""
+
+ def setUp(self):
+ self.jp2file = glymur.data.nemo()
+
+ def tearDown(self):
+ pass
+
+ def test_bad_rsiz(self):
+ """Should warn if RSIZ is bad. Issue196"""
+ filename = opj_data_file('input/nonregression/edf_c2_1002767.jp2')
+ if sys.hexversion < 0x03000000:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ j = Jp2k(filename)
+ else:
+ with self.assertWarns(UserWarning):
+ j = Jp2k(filename)
+
+ def test_bad_wavelet_transform(self):
+ """Should warn if wavelet transform is bad. Issue195"""
+ filename = opj_data_file('input/nonregression/edf_c2_10025.jp2')
+ if sys.hexversion < 0x03000000:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ j = Jp2k(filename)
+ else:
+ with self.assertWarns(UserWarning):
+ j = Jp2k(filename)
+
+ def test_invalid_progression_order(self):
+ """Should still be able to parse even if prog order is invalid."""
+ jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2')
+ if sys.hexversion < 0x03000000:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ Jp2k(jfile)
+ else:
+ with self.assertWarns(UserWarning):
+ Jp2k(jfile)
+
+ def test_tile_height_is_zero(self):
+ """Zero tile height should not cause an exception."""
+ filename = opj_data_file('input/nonregression/2539.pdf.SIGFPE.706.1712.jp2')
+ if sys.hexversion < 0x03000000:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ Jp2k(filename)
+ else:
+ with self.assertWarns(UserWarning):
+ Jp2k(filename)
+
+
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_reserved_marker_segment(self):
"""Reserved marker segments are ok."""
@@ -45,7 +105,7 @@ class TestCodestream(unittest.TestCase):
#
# Let's inject a reserved marker segment into a file that
# we know something about to make sure we can still parse it.
- filename = os.path.join(DATA_ROOT, 'input/conformance/p0_01.j2k')
+ filename = os.path.join(OPJ_DATA_ROOT, 'input/conformance/p0_01.j2k')
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
with open(filename, 'rb') as ifile:
# Everything up until the first QCD marker.
@@ -67,8 +127,6 @@ class TestCodestream(unittest.TestCase):
self.assertEqual(codestream.segment[2].length, 3)
self.assertEqual(codestream.segment[2].data, b'\x00')
- @unittest.skipIf(DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
@unittest.skipIf(sys.hexversion < 0x03020000,
"Uses features introduced in 3.2.")
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
@@ -77,7 +135,7 @@ class TestCodestream(unittest.TestCase):
# Let's inject a marker segment whose marker does not appear to
# be valid. We still parse the file, but warn about the offending
# marker.
- filename = os.path.join(DATA_ROOT, 'input/conformance/p0_01.j2k')
+ filename = os.path.join(OPJ_DATA_ROOT, 'input/conformance/p0_01.j2k')
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
with open(filename, 'rb') as ifile:
# Everything up until the first QCD marker.
@@ -100,11 +158,9 @@ class TestCodestream(unittest.TestCase):
self.assertEqual(codestream.segment[2].length, 3)
self.assertEqual(codestream.segment[2].data, b'\x00')
- @unittest.skipIf(DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
def test_psot_is_zero(self):
"""Psot=0 in SOT is perfectly legal. Issue #78."""
- filename = os.path.join(DATA_ROOT,
+ filename = os.path.join(OPJ_DATA_ROOT,
'input/nonregression/123.j2c')
j = Jp2k(filename)
codestream = j.get_codestream(header_only=False)
@@ -114,22 +170,9 @@ class TestCodestream(unittest.TestCase):
self.assertEqual(codestream.segment[-1].marker_id, 'EOC')
- def test_siz_segment_ssiz_unsigned(self):
- """ssiz attribute to be removed in future release"""
- j = Jp2k(self.jp2file)
- codestream = j.get_codestream()
-
- # The ssiz attribute was simply a tuple of raw bytes.
- # The first 7 bits are interpreted as the bitdepth, the MSB determines
- # whether or not it is signed.
- self.assertEqual(codestream.segment[1].ssiz, (7, 7, 7))
-
-
- @unittest.skipIf(DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
def test_siz_segment_ssiz_signed(self):
"""ssiz attribute to be removed in future release"""
- filename = os.path.join(DATA_ROOT, 'input/conformance/p0_03.j2k')
+ filename = os.path.join(OPJ_DATA_ROOT, 'input/conformance/p0_03.j2k')
j = Jp2k(filename)
codestream = j.get_codestream()
diff --git a/glymur/test/test_conformance.py b/glymur/test/test_conformance.py
deleted file mode 100644
index 8f4496d..0000000
--- a/glymur/test/test_conformance.py
+++ /dev/null
@@ -1,130 +0,0 @@
-"""
-These tests deal with JPX/JP2/J2K images in the format-corpus repository.
-"""
-# R0904: Not too many methods in unittest.
-# pylint: disable=R0904
-
-# E1101: assertWarns introduced in python 3.2
-# pylint: disable=E1101
-
-import os
-from os.path import join
-import re
-import sys
-import unittest
-
-import glymur
-from glymur import Jp2k
-
-try:
- FORMAT_CORPUS_DATA_ROOT = os.environ['FORMAT_CORPUS_DATA_ROOT']
-except KeyError:
- FORMAT_CORPUS_DATA_ROOT = None
-
-try:
- OPJ_DATA_ROOT = os.environ['OPJ_DATA_ROOT']
-except KeyError:
- OPJ_DATA_ROOT = None
-
-
-@unittest.skipIf(FORMAT_CORPUS_DATA_ROOT is None,
- "FORMAT_CORPUS_DATA_ROOT environment variable not set")
-@unittest.skipIf(sys.hexversion < 0x03020000,
- "Requires features introduced in 3.2 (assertWarns)")
-class TestSuiteFormatCorpus(unittest.TestCase):
- """Test suite for files in format corpus repository."""
-
- @unittest.skipIf(re.match(r"""1\.[0123]""",
- glymur.version.openjpeg_version) is not None,
- "Needs 1.3+ to catch this.")
- def test_balloon_trunc1(self):
- """Has one byte shaved off of EOC marker."""
- jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT,
- 'jp2k-test/byteCorruption/balloon_trunc1.jp2')
- j2k = Jp2k(jfile)
- with self.assertWarns(UserWarning):
- codestream = j2k.get_codestream(header_only=False)
-
- # The last segment is truncated, so there should not be an EOC marker.
- self.assertNotEqual(codestream.segment[-1].marker_id, 'EOC')
-
- # The codestream is not as long as claimed.
- with self.assertRaises(OSError):
- j2k.read(rlevel=-1)
-
- @unittest.skipIf(re.match(r"""1\.[01234]""",
- glymur.version.openjpeg_version) is not None,
- "Needs 1.4+ to catch this.")
- def test_balloon_trunc2(self):
- """Shortened by 5000 bytes."""
- jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT,
- 'jp2k-test/byteCorruption/balloon_trunc2.jp2')
- j2k = Jp2k(jfile)
- with self.assertWarns(UserWarning):
- codestream = j2k.get_codestream(header_only=False)
-
- # The last segment is truncated, so there should not be an EOC marker.
- self.assertNotEqual(codestream.segment[-1].marker_id, 'EOC')
-
- # The codestream is not as long as claimed.
- with self.assertRaises(OSError):
- j2k.read(rlevel=-1)
-
- def test_balloon_trunc3(self):
- """Most of last tile is missing."""
- jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT,
- 'jp2k-test/byteCorruption/balloon_trunc3.jp2')
- j2k = Jp2k(jfile)
- with self.assertWarns(UserWarning):
- codestream = j2k.get_codestream(header_only=False)
-
- # The last segment is truncated, so there should not be an EOC marker.
- self.assertNotEqual(codestream.segment[-1].marker_id, 'EOC')
-
- # Should error out, it does not.
- #with self.assertRaises(OSError):
- # j2k.read(rlevel=-1)
-
- def test_jp2_brand_any_icc_profile(self):
- """If 'jp2 ', then the method cannot be any icc profile."""
- jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT,
- 'jp2k-test', 'icc',
- 'balloon_eciRGBv2_ps_adobeplugin.jpf')
- with self.assertWarns(UserWarning):
- Jp2k(jfile)
-
- def test_jp2_brand_iccpr_mult_colr(self):
- """Has colr box, one that conforms, one that does not."""
-
- # Wrong 'brand' field; contains two versions of ICC profile: one
- # embedded using "Any ICC" method; other embedded using "Restricted
- # ICC" method, with description ("Modified eciRGB v2") and profileClass
- # ("Input Device") changed relative to original profile.
- jfile = join(FORMAT_CORPUS_DATA_ROOT, 'jp2k-test', 'icc',
- 'balloon_eciRGBv2_ps_adobeplugin_jp2compatible.jpf')
- with self.assertWarns(UserWarning):
- Jp2k(jfile)
-
-
-@unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
-@unittest.skipIf(sys.hexversion < 0x03020000,
- "Requires features introduced in 3.2 (assertWarns)")
-class TestSuiteOpj(unittest.TestCase):
- """Test suite for files in openjpeg repository."""
-
- def setUp(self):
- pass
-
- def tearDown(self):
- pass
-
- def test_jp2_brand_any_icc_profile(self):
- """If 'jp2 ', then the method cannot be any icc profile."""
- filename = os.path.join(OPJ_DATA_ROOT,
- 'input/nonregression/text_GBR.jp2')
- with self.assertWarns(UserWarning):
- Jp2k(filename)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/glymur/test/test_icc.py b/glymur/test/test_icc.py
index 46d4345..c49055e 100644
--- a/glymur/test/test_icc.py
+++ b/glymur/test/test_icc.py
@@ -9,6 +9,7 @@ import datetime
import os
import sys
import unittest
+import warnings
import numpy as np
@@ -30,8 +31,11 @@ class TestICC(unittest.TestCase):
def test_file5(self):
"""basic ICC profile"""
filename = opj_data_file('input/conformance/file5.jp2')
- j = Jp2k(filename)
- profile = j.box[3].box[1].icc_profile
+ with warnings.catch_warnings():
+ # The file has a bad compatibility list entry. Not important here.
+ warnings.simplefilter("ignore")
+ j = Jp2k(filename)
+ profile = j.box[2].box[1].icc_profile
self.assertEqual(profile['Size'], 546)
self.assertEqual(profile['Preferred CMM Type'], 0)
self.assertEqual(profile['Version'], '2.2.0')
diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py
index 5dedbcc..1a00715 100644
--- a/glymur/test/test_jp2box.py
+++ b/glymur/test/test_jp2box.py
@@ -58,6 +58,23 @@ class TestDataEntryURL(unittest.TestCase):
def setUp(self):
self.jp2file = glymur.data.nemo()
+ def test_wrap_greyscale(self):
+ """A single component should be wrapped as GREYSCALE."""
+ j = Jp2k(self.jp2file)
+ data = j.read()
+ red = data[:, :, 0]
+
+ # Write it back out as a raw codestream.
+ with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile1:
+ j2k = glymur.Jp2k(tfile1.name, 'wb')
+ j2k.write(data[:, :, 0])
+
+ # Ok, now rewrap it as JP2. The colorspace should be GREYSCALE.
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile2:
+ jp2 = j2k.wrap(tfile2.name)
+ self.assertEqual(jp2.box[2].box[1].colorspace,
+ glymur.core.GREYSCALE)
+
def test_basic_url(self):
"""Just your most basic URL box."""
# Wrap our j2k file in a JP2 box along with an interior url box.
@@ -340,6 +357,7 @@ class TestChannelDefinition(unittest.TestCase):
with self.assertRaises((IOError, OSError)):
j2k.wrap(tfile.name, boxes=boxes)
+ @unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
def test_bad_type(self):
"""Channel types are limited to 0, 1, 2, 65535
Should reject if not all of index, channel_type, association the
@@ -347,17 +365,18 @@ class TestChannelDefinition(unittest.TestCase):
"""
channel_type = (COLOR, COLOR, 3)
association = (RED, GREEN, BLUE)
- with self.assertRaises(IOError):
+ with self.assertWarns(UserWarning):
glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type,
association=association)
+ @unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
def test_wrong_lengths(self):
"""Should reject if not all of index, channel_type, association the
same length.
"""
channel_type = (COLOR, COLOR)
association = (RED, GREEN, BLUE)
- with self.assertRaises(IOError):
+ with self.assertWarns(UserWarning):
glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type,
association=association)
@@ -373,14 +392,19 @@ class TestFileTypeBox(unittest.TestCase):
def test_brand_unknown(self):
"""A ftyp box brand must be 'jp2 ' or 'jpx '."""
- ftyp = glymur.jp2box.FileTypeBox(brand='jp3')
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ ftyp = glymur.jp2box.FileTypeBox(brand='jp3')
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
ftyp.write(tfile)
def test_cl_entry_unknown(self):
"""A ftyp box cl list can only contain 'jp2 ', 'jpx ', or 'jpxb'."""
- ftyp = glymur.jp2box.FileTypeBox(compatibility_list=['jp3'])
+ with warnings.catch_warnings():
+ # Bad compatibility list item.
+ warnings.simplefilter("ignore")
+ ftyp = glymur.jp2box.FileTypeBox(compatibility_list=['jp3'])
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
ftyp.write(tfile)
@@ -429,6 +453,18 @@ class TestColourSpecificationBox(unittest.TestCase):
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
+ @unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
+ def test_bad_approx_jp2_field(self):
+ """JP2 has requirements for approx field"""
+ j2k = Jp2k(self.j2kfile)
+ boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c]
+ colr = ColourSpecificationBox(colorspace=glymur.core.SRGB,
+ approximation=1)
+ boxes[2].box = [self.ihdr, colr]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
def test_default_colr(self):
"""basic colr instantiation"""
colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
@@ -438,27 +474,30 @@ class TestColourSpecificationBox(unittest.TestCase):
self.assertEqual(colr.colorspace, glymur.core.SRGB)
self.assertIsNone(colr.icc_profile)
+ @unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
def test_colr_with_cspace_and_icc(self):
"""Colour specification boxes can't have both."""
- with self.assertRaises((OSError, IOError)):
+ with self.assertWarns(UserWarning):
colorspace = glymur.core.SRGB
rawb = b'\x01\x02\x03\x04'
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
icc_profile=rawb)
+ @unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
def test_colr_with_bad_method(self):
"""colr must have a valid method field"""
colorspace = glymur.core.SRGB
method = -1
- with self.assertRaises(IOError):
+ with self.assertWarns(UserWarning):
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
method=method)
+ @unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
def test_colr_with_bad_approx(self):
- """colr must have a valid approximation field"""
+ """colr should have a valid approximation field"""
colorspace = glymur.core.SRGB
approx = -1
- with self.assertRaises(IOError):
+ with self.assertWarns(UserWarning):
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
approximation=approx)
@@ -484,21 +523,23 @@ class TestPaletteBox(unittest.TestCase):
def tearDown(self):
pass
+ @unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
def test_mismatched_bitdepth_signed(self):
"""bitdepth and signed arguments must have equal length"""
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
bps = (8, 8, 8)
signed = (False, False)
- with self.assertRaises(IOError):
+ with self.assertWarns(UserWarning):
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
signed=signed)
+ @unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
def test_mismatched_signed_palette(self):
"""bitdepth and signed arguments must have equal length"""
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
bps = (8, 8, 8, 8)
signed = (False, False, False, False)
- with self.assertRaises(IOError):
+ with self.assertWarns(UserWarning):
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
signed=signed)
@@ -666,8 +707,9 @@ class TestWrap(unittest.TestCase):
def test_jpx_to_jp2(self):
"""basic test for rewrapping a jpx file"""
jpx = Jp2k(self.jpxfile)
- idx = [0, 1, 3, 6]
- boxes = [jpx.box[idx] for idx in [0, 1, 3, 6]]
+ # Use only the signature, file type, header, and 1st codestream.
+ lst = [0, 1, 2, 5]
+ boxes = [jpx.box[idx] for idx in lst]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
jp2 = jpx.wrap(tfile.name, boxes=boxes)
@@ -828,6 +870,67 @@ class TestWrap(unittest.TestCase):
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
+ def test_wrap_jpx_to_jp2_with_unadorned_jpch(self):
+ """A JPX file rewrapped with plain jpch is not allowed."""
+ with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
+ jpx = Jp2k(self.jpxfile)
+ boxes = [jpx.box[0], jpx.box[1], jpx.box[2],
+ glymur.jp2box.ContiguousCodestreamBox()]
+ with self.assertRaises(IOError):
+ jpx.wrap(tfile1.name, boxes=boxes)
+
+ def test_wrap_jpx_to_jp2_with_incorrect_jp2c_offset(self):
+ """Reject A JPX file rewrapped with bad jp2c offset."""
+ with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
+ jpx = Jp2k(self.jpxfile)
+ jpch = jpx.box[5]
+
+ # The offset should be 902.
+ jpch.offset = 901
+ jpch.length = 313274
+ boxes = [jpx.box[0], jpx.box[1], jpx.box[2], jpch]
+ with self.assertRaises(IOError):
+ jpx.wrap(tfile1.name, boxes=boxes)
+
+ def test_wrap_jpx_to_jp2_with_correctly_specified_jp2c(self):
+ """Accept A JPX file rewrapped with good jp2c."""
+ with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
+ jpx = Jp2k(self.jpxfile)
+ jpch = jpx.box[5]
+
+ # This time get it right.
+ jpch.offset = 903
+ jpch.length = 313274
+ boxes = [jpx.box[0], jpx.box[1], jpx.box[2], jpch]
+ jp2 = jpx.wrap(tfile1.name, boxes=boxes)
+
+ act_ids = [box.box_id for box in jp2.box]
+ exp_ids = ['jP ', 'ftyp', 'jp2h', 'jp2c']
+ self.assertEqual(act_ids, exp_ids)
+
+ act_offsets = [box.offset for box in jp2.box]
+ exp_offsets = [0, 12, 40, 887]
+ self.assertEqual(act_offsets, exp_offsets)
+
+ act_lengths = [box.length for box in jp2.box]
+ exp_lengths = [12, 28, 847, 313274]
+ self.assertEqual(act_lengths, exp_lengths)
+
+ def test_full_blown_jpx(self):
+ """Rewrap a jpx file."""
+ with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
+ jpx = Jp2k(self.jpxfile)
+ idx = list(range(5)) + list(range(9, 12)) + list(range(6, 9)) + [12]
+ boxes = [jpx.box[j] for j in idx]
+ jpx2 = jpx.wrap(tfile1.name, boxes=boxes)
+ exp_ids = [box.box_id for box in boxes]
+ lengths = [box.length for box in jpx.box]
+ exp_lengths = [lengths[j] for j in idx]
+ act_ids = [box.box_id for box in jpx2.box]
+ act_lengths = [box.length for box in jpx2.box]
+ self.assertEqual(exp_ids, act_ids)
+ self.assertEqual(exp_lengths, act_lengths)
+
class TestJp2Boxes(unittest.TestCase):
"""Tests for canonical JP2 boxes."""
diff --git a/glymur/test/test_jp2box_jpx.py b/glymur/test/test_jp2box_jpx.py
index f8e9db8..7fe407a 100644
--- a/glymur/test/test_jp2box_jpx.py
+++ b/glymur/test/test_jp2box_jpx.py
@@ -8,18 +8,22 @@ import struct
import sys
import tempfile
import unittest
+import warnings
+
import lxml.etree as ET
import glymur
from glymur import Jp2k
from glymur.jp2box import DataEntryURLBox, FileTypeBox, JPEG2000SignatureBox
from glymur.jp2box import DataReferenceBox, FragmentListBox, FragmentTableBox
+from glymur.jp2box import ColourSpecificationBox
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
class TestJPXWrap(unittest.TestCase):
"""Test suite for wrapping JPX files."""
def setUp(self):
+ self.jpxfile = glymur.data.jpxfile()
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
@@ -107,6 +111,68 @@ class TestJPXWrap(unittest.TestCase):
with self.assertRaises(IOError):
jp2.wrap(tfile.name, boxes=boxes)
+ def test_jpch_jplh(self):
+ """Write a codestream header, compositing layer header box."""
+ jp2 = Jp2k(self.jp2file)
+ boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
+
+ # The ftyp box must be modified to jpx.
+ boxes[1].brand = 'jpx '
+ boxes[1].compatibility_list = ['jp2 ', 'jpxb']
+
+ jpch = glymur.jp2box.CodestreamHeaderBox()
+ boxes.append(jpch)
+ jplh = glymur.jp2box.CompositingLayerHeaderBox()
+ boxes.append(jplh)
+
+ with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
+ jpx = jp2.wrap(tfile.name, boxes=boxes)
+
+ self.assertEqual(jpx.box[-2].box_id, 'jpch')
+ self.assertEqual(jpx.box[-1].box_id, 'jplh')
+
+ def test_cgrp(self):
+ """Write a color group box."""
+ jp2 = Jp2k(self.jp2file)
+ boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
+
+ # The ftyp box must be modified to jpx.
+ boxes[1].brand = 'jpx '
+ boxes[1].compatibility_list = ['jp2 ', 'jpxb']
+
+ colr_rgb = ColourSpecificationBox(colorspace=glymur.core.SRGB)
+ colr_gr = ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)
+ box = [colr_rgb, colr_gr]
+
+ cgrp = glymur.jp2box.ColourGroupBox(box=box)
+ boxes.append(cgrp)
+
+ with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
+ jpx = jp2.wrap(tfile.name, boxes=boxes)
+
+ self.assertEqual(jpx.box[-1].box_id, 'cgrp')
+ self.assertEqual(jpx.box[-1].box[0].box_id, 'colr')
+ self.assertEqual(jpx.box[-1].box[1].box_id, 'colr')
+
+ def test_cgrp_neg(self):
+ """Can't write a cgrp with anything but colr sub boxes"""
+ jp2 = Jp2k(self.jp2file)
+ boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
+
+ # The ftyp box must be modified to jpx.
+ boxes[1].brand = 'jpx '
+ boxes[1].compatibility_list = ['jp2 ', 'jpxb']
+
+ lblb = glymur.jp2box.LabelBox("Just a test")
+ box = [lblb]
+
+ cgrp = glymur.jp2box.ColourGroupBox(box=box)
+ boxes.append(cgrp)
+
+ with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
+ with self.assertRaises(IOError):
+ jpx = jp2.wrap(tfile.name, boxes=boxes)
+
def test_ftbl(self):
"""Write a fragment table box."""
# Add a negative test where offset < 0
@@ -207,13 +273,14 @@ class TestJPXWrap(unittest.TestCase):
with self.assertRaises(IOError):
jp2.wrap(tfile.name, boxes=boxes)
+ @unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
def test_deurl_child_of_dtbl(self):
"""Data reference boxes can only contain data entry url boxes."""
jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
ftyp = glymur.jp2box.FileTypeBox()
- with self.assertRaises(IOError):
+ with self.assertWarns(UserWarning):
dref = glymur.jp2box.DataReferenceBox([ftyp])
# Try to get around it by appending the ftyp box after creation.
@@ -337,7 +404,9 @@ class TestJPX(unittest.TestCase):
offset = [89]
length = [1132288]
reference = [0, 0]
- flst = glymur.jp2box.FragmentListBox(offset, length, reference)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ flst = glymur.jp2box.FragmentListBox(offset, length, reference)
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
flst.write(tfile)
@@ -347,8 +416,10 @@ class TestJPX(unittest.TestCase):
offset = [0]
length = [1132288]
reference = [0]
- flst = glymur.jp2box.FragmentListBox(offset, length, reference)
- with self.assertRaises(IOError):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ flst = glymur.jp2box.FragmentListBox(offset, length, reference)
+ with self.assertRaises((IOError, OSError)):
with tempfile.TemporaryFile() as tfile:
flst.write(tfile)
@@ -357,14 +428,18 @@ class TestJPX(unittest.TestCase):
offset = [89]
length = [0]
reference = [0]
- flst = glymur.jp2box.FragmentListBox(offset, length, reference)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ flst = glymur.jp2box.FragmentListBox(offset, length, reference)
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
flst.write(tfile)
def test_ftbl_boxes_empty(self):
"""A fragment table box must have at least one child box."""
- ftbl = glymur.jp2box.FragmentTableBox()
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ ftbl = glymur.jp2box.FragmentTableBox()
with self.assertRaises(IOError):
with tempfile.TemporaryFile() as tfile:
ftbl.write(tfile)
@@ -377,6 +452,7 @@ class TestJPX(unittest.TestCase):
with tempfile.TemporaryFile() as tfile:
ftbl.write(tfile)
+ @unittest.skip("No such jpx file anymore.")
def test_jpx_rreq_mask_length_3(self):
"""There are some JPX files with rreq mask length of 3."""
jpx = Jp2k(self.jpxfile)
@@ -386,7 +462,7 @@ class TestJPX(unittest.TestCase):
self.assertEqual(jpx.box[2].standard_flag,
(5, 42, 45, 2, 18, 19, 1, 8, 12, 31, 20))
- @unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
+ @unittest.skip("Requires unnecessarily complicated code")
def test_unknown_superbox(self):
"""Verify that we can handle an unknown superbox."""
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
@@ -402,15 +478,9 @@ class TestJPX(unittest.TestCase):
with self.assertWarns(UserWarning):
jpx = Jp2k(tfile.name)
- self.assertEqual(jpx.box[-1].box_id, 'grp ')
+ self.assertEqual(jpx.box[-1].box_id, b'grp ')
self.assertEqual(jpx.box[-1].box[0].box_id, 'free')
- def test_free_box(self):
- """Verify that we can handle a free box."""
- j = Jp2k(self.jpxfile)
- self.assertEqual(j.box[16].box[0].box_id, 'free')
- self.assertEqual(type(j.box[16].box[0]), glymur.jp2box.FreeBox)
-
def test_data_reference_requires_dtbl(self):
"""The existance of a data reference box requires a ftbl box as well."""
flag = 0
@@ -483,17 +553,17 @@ class TestJPX(unittest.TestCase):
self.assertEqual(jpx.box[-1].box[0].data_reference, (3,))
def test_nlst(self):
- """Verify that we can handle a free box."""
+ """Verify that we can handle a number list box."""
j = Jp2k(self.jpxfile)
- self.assertEqual(j.box[16].box[1].box[0].box_id, 'nlst')
- self.assertEqual(type(j.box[16].box[1].box[0]),
- glymur.jp2box.NumberListBox)
+ nlst = j.box[12].box[0].box[0]
+ self.assertEqual(nlst.box_id, 'nlst')
+ self.assertEqual(type(nlst), glymur.jp2box.NumberListBox)
# Two associations.
- self.assertEqual(len(j.box[16].box[1].box[0].associations), 2)
+ self.assertEqual(len(nlst.associations), 2)
# Codestream 0
- self.assertEqual(j.box[16].box[1].box[0].associations[0], 1 << 24)
+ self.assertEqual(nlst.associations[0], 1 << 24)
# Compositing Layer 0
- self.assertEqual(j.box[16].box[1].box[0].associations[1], 2 << 24)
+ self.assertEqual(nlst.associations[1], 2 << 24)
diff --git a/glymur/test/test_jp2box_xml.py b/glymur/test/test_jp2box_xml.py
index facc4df..2531f0d 100644
--- a/glymur/test/test_jp2box_xml.py
+++ b/glymur/test/test_jp2box_xml.py
@@ -39,6 +39,7 @@ from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox
from glymur.jp2box import FileTypeBox, ImageHeaderBox, JP2HeaderBox
from glymur.jp2box import JPEG2000SignatureBox
+from .fixtures import OPJ_DATA_ROOT, opj_data_file
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
class TestXML(unittest.TestCase):
@@ -286,3 +287,40 @@ class TestBadButRecoverableXmlFile(unittest.TestCase):
b'this is a test')
+@unittest.skipIf(OPJ_DATA_ROOT is None,
+ "OPJ_DATA_ROOT environment variable not set")
+class TestXML_OpjDataRoot(unittest.TestCase):
+ """Test suite for XML boxes, requires OPJ_DATA_ROOT."""
+
+ def test_bom(self):
+ """Byte order markers are illegal in UTF-8. Issue 185"""
+ filename = opj_data_file(os.path.join('input',
+ 'nonregression',
+ 'issue171.jp2'))
+ if sys.hexversion < 0x03000000:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+ else:
+ with self.assertWarns(UserWarning):
+ jp2 = Jp2k(filename)
+ self.assertIsNotNone(jp2.box[3].xml)
+
+
+ def test_invalid_utf8(self):
+ """Bad byte sequence that cannot be parsed."""
+ filename = opj_data_file(os.path.join('input',
+ 'nonregression',
+ '26ccf3651020967f7778238ef5af08af.SIGFPE.d25.527.jp2'))
+ if sys.hexversion < 0x03000000:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+ else:
+ with self.assertWarns(UserWarning):
+ jp2 = Jp2k(filename)
+
+ self.assertIsNone(jp2.box[3].box[1].box[1].xml)
+
+
+
diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py
index 386f077..6cd1ffd 100644
--- a/glymur/test/test_jp2k.py
+++ b/glymur/test/test_jp2k.py
@@ -35,7 +35,7 @@ if HAS_PYTHON_XMP_TOOLKIT:
from libxmp import XMPMeta
from .fixtures import OPJ_DATA_ROOT, opj_data_file
-
+from . import fixtures
# Doc tests should be run as well.
def load_tests(loader, tests, ignore):
@@ -53,9 +53,7 @@ def load_tests(loader, tests, ignore):
class TestJp2k(unittest.TestCase):
- """Test suite for openjpeg software starting at 1.3"""
-
- # These tests should be run by just about all configuration.
+ """These tests should be run by just about all configuration."""
def setUp(self):
self.jp2file = glymur.data.nemo()
@@ -65,6 +63,24 @@ class TestJp2k(unittest.TestCase):
def tearDown(self):
pass
+ def test_no_cxform_pclr_jpx(self):
+ """Indices for pclr jpxfile if no color transform"""
+ j = Jp2k(self.jpxfile)
+ rgb = j.read()
+ idx = j.read(ignore_pclr_cmap_cdef=True)
+ nr, nc = 1024, 1024
+ self.assertEqual(rgb.shape, (nr, nc, 3))
+ self.assertEqual(idx.shape, (nr, nc))
+
+ # Should be able to manually reconstruct the RGB image from the palette
+ # and indices.
+ palette = j.box[2].box[2].palette
+ rgb_from_idx = np.zeros(rgb.shape, dtype=np.uint8)
+ for r in np.arange(nr):
+ for c in np.arange(nc):
+ rgb_from_idx[r, c] = palette[idx[r, c]]
+ np.testing.assert_array_equal(rgb, rgb_from_idx)
+
def test_repr(self):
"""Verify that results of __repr__ are eval-able."""
j = Jp2k(self.j2kfile)
@@ -247,19 +263,6 @@ class TestJp2k(unittest.TestCase):
j2k = Jp2k(self.j2kfile)
j2k.read()
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_read_differing_subsamples(self):
- """should error out with read used on differently subsampled images"""
- # Verify that we error out appropriately if we use the read method
- # on an image with differing subsamples
- #
- # Issue 86.
- filename = opj_data_file('input/conformance/p0_05.j2k')
- j = Jp2k(filename)
- with self.assertRaises(RuntimeError):
- j.read()
-
def test_empty_box_with_j2k(self):
"""Verify that the list of boxes in a J2C/J2K file is present, but
empty.
@@ -376,20 +379,18 @@ class TestJp2k(unittest.TestCase):
creator_tool = xmp.get_property(libxmp.consts.XMP_NS_XMP, 'CreatorTool')
self.assertEqual(creator_tool, 'Google')
+ @unittest.skipIf(fixtures.OPENJP2_IS_V2_OFFICIAL,
+ "Feature not supported in 2.0.0 official")
+ @unittest.skipIf(glymur.version.openjpeg_version_tuple[0] == 1,
+ "Feature not supported in 1.5")
def test_jpx_mult_codestreams_jp2_brand(self):
"""Read JPX codestream when jp2-compatible."""
# The file in question has multiple codestreams.
jpx = Jp2k(self.jpxfile)
data = jpx.read()
- if re.match(r"""1\.[0123]""", glymur.version.openjpeg_version):
- # openjpeg 1.3 doesn't apply the palette, so it's a 2D image here
- self.assertEqual(data.shape, (1024, 1024))
- else:
- self.assertEqual(data.shape, (1024, 1024, 3))
+ self.assertEqual(data.shape, (1024, 1024, 3))
-@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
- "Requires at least version 1.5")
class TestJp2k_write(unittest.TestCase):
"""Write tests, can be run by versions 1.5+"""
@@ -753,6 +754,106 @@ class TestJp2k_2_1(unittest.TestCase):
with self.assertRaisesRegex((IOError, OSError), regexp):
j.read(rlevel=1)
+@unittest.skipIf(OPJ_DATA_ROOT is None,
+ "OPJ_DATA_ROOT environment variable not set")
+class TestJp2kOpjDataRoot(unittest.TestCase):
+ """These tests should be run by just about all configuration."""
+
+ def test_undecodeable_box_id(self):
+ """Should warn in case of undecodeable box ID but not error out."""
+ filename = opj_data_file('input/nonregression/edf_c2_1013627.jp2')
+ if sys.hexversion < 0x03000000:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+ else:
+ with self.assertWarns(UserWarning):
+ jp2 = Jp2k(filename)
+
+ # Now make sure we got all of the boxes. Ignore the last, which was
+ # bad.
+ box_ids = [box.box_id for box in jp2.box[:-1]]
+ self.assertEqual(box_ids, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
+
+ def test_invalid_approximation(self):
+ """Should warn in case of bad ftyp brand."""
+ filename = opj_data_file('input/nonregression/edf_c2_1000290.jp2')
+ with self.assertWarns(UserWarning):
+ jp2 = Jp2k(filename)
+
+ @unittest.skipIf(sys.hexversion < 0x03000000, "Test requires Python 3.3+")
+ def test_invalid_approximation(self):
+ """Should warn in case of invalid approximation."""
+ filename = opj_data_file('input/nonregression/edf_c2_1015644.jp2')
+ with self.assertWarns(UserWarning):
+ jp2 = Jp2k(filename)
+
+ @unittest.skipIf(sys.hexversion < 0x03000000, "Test requires Python 3.3+")
+ def test_invalid_colorspace(self):
+ """Should warn in case of invalid colorspace."""
+ filename = opj_data_file('input/nonregression/edf_c2_1103421.jp2')
+ with self.assertWarns(UserWarning):
+ jp2 = Jp2k(filename)
+
+ def test_no_cxform_pclr_jp2(self):
+ """Indices for pclr jpxfile if no color transform"""
+ filename = opj_data_file('input/conformance/file9.jp2')
+ j = Jp2k(filename)
+ rgb = j.read()
+ idx = j.read(ignore_pclr_cmap_cdef=True)
+ self.assertEqual(rgb.shape, (512, 768, 3))
+ self.assertEqual(idx.shape, (512, 768))
+
+ # Should be able to manually reconstruct the RGB image from the palette
+ # and indices.
+ palette = j.box[2].box[1].palette
+ rgb_from_idx = np.zeros(rgb.shape, dtype=np.uint8)
+ for r in np.arange(rgb.shape[0]):
+ for c in np.arange(rgb.shape[1]):
+ rgb_from_idx[r, c] = palette[idx[r, c]]
+ np.testing.assert_array_equal(rgb, rgb_from_idx)
+
+ def test_stupid_windows_eol_at_end(self):
+ """Garbage characters at the end of the file."""
+ filename = opj_data_file('input/nonregression/issue211.jp2')
+ if sys.hexversion < 0x03000000:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+ else:
+ with self.assertWarns(UserWarning):
+ jp2 = Jp2k(filename)
+
+ def test_read_differing_subsamples(self):
+ """should error out with read used on differently subsampled images"""
+ # Verify that we error out appropriately if we use the read method
+ # on an image with differing subsamples
+ #
+ # Issue 86.
+ filename = opj_data_file('input/conformance/p0_05.j2k')
+ j = Jp2k(filename)
+ with self.assertRaises(RuntimeError):
+ j.read()
+
+ def test_no_cxform_cmap(self):
+ """Bands as physically ordered, not as physically intended"""
+ # This file has the components physically reversed. The cmap box
+ # tells the decoder how to order them, but this flag prevents that.
+ filename = opj_data_file('input/conformance/file2.jp2')
+ with warnings.catch_warnings():
+ # The file has a bad compatibility list entry. Not important here.
+ warnings.simplefilter("ignore")
+ j = Jp2k(filename)
+ ycbcr = j.read()
+ crcby = j.read(ignore_pclr_cmap_cdef=True)
+
+ expected = np.zeros(ycbcr.shape, ycbcr.dtype)
+ for k in range(crcby.shape[2]):
+ expected[:,:,crcby.shape[2] - k - 1] = crcby[:,:,k]
+
+ np.testing.assert_array_equal(ycbcr, expected)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py
index abe271a..efbc794 100644
--- a/glymur/test/test_opj_suite.py
+++ b/glymur/test/test_opj_suite.py
@@ -327,7 +327,10 @@ class TestSuite(unittest.TestCase):
def test_ETS_JP2_file1(self):
jfile = opj_data_file('input/conformance/file1.jp2')
- jp2k = Jp2k(jfile)
+ with warnings.catch_warnings():
+ # Bad compatibility list item.
+ warnings.simplefilter("ignore")
+ jp2k = Jp2k(jfile)
jpdata = jp2k.read()
self.assertEqual(jpdata.shape, (512, 768, 3))
@@ -381,11 +384,7 @@ class TestSuite(unittest.TestCase):
jfile = opj_data_file('input/conformance/file9.jp2')
jp2k = Jp2k(jfile)
jpdata = jp2k.read()
- if re.match(r"""1\.3""", glymur.version.openjpeg_version):
- # Version 1.3 reads the indexed image as indices, not as RGB.
- self.assertEqual(jpdata.shape, (512, 768))
- else:
- self.assertEqual(jpdata.shape, (512, 768, 3))
+ self.assertEqual(jpdata.shape, (512, 768, 3))
def test_NR_DEC_Bretagne2_j2k_1_decode(self):
jfile = opj_data_file('input/nonregression/Bretagne2.j2k')
@@ -465,7 +464,9 @@ class TestSuite(unittest.TestCase):
def test_NR_DEC_illegalcolortransform_j2k_14_decode(self):
# Stream too short, expected SOT.
jfile = opj_data_file('input/nonregression/illegalcolortransform.j2k')
- Jp2k(jfile).read()
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ Jp2k(jfile).read()
self.assertTrue(True)
def test_NR_DEC_j2k32_j2k_15_decode(self):
@@ -3116,7 +3117,10 @@ class TestSuiteDump(unittest.TestCase):
def test_NR_file1_dump(self):
jfile = opj_data_file('input/conformance/file1.jp2')
- jp2 = Jp2k(jfile)
+ with warnings.catch_warnings():
+ # Bad compatibility list item.
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(jfile)
ids = [box.box_id for box in jp2.box]
self.assertEqual(ids, ['jP ', 'ftyp', 'xml ', 'jp2h', 'xml ',
@@ -3297,7 +3301,7 @@ class TestSuiteDump(unittest.TestCase):
def test_NR_file5_dump(self):
# Three 8-bit components in the ROMM-RGB colourspace, encapsulated in a
- # JP2 compatible JPX file. The components have been transformed using
+ # JPX file. The components have been transformed using
# the RCT. The colourspace is specified using both a Restricted ICC
# profile and using the JPX-defined enumerated code for the ROMM-RGB
# colourspace.
@@ -3305,49 +3309,37 @@ class TestSuiteDump(unittest.TestCase):
jp2 = Jp2k(jfile)
ids = [box.box_id for box in jp2.box]
- self.assertEqual(ids, ['jP ', 'ftyp', 'rreq', 'jp2h', 'jp2c'])
+ self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
- ids = [box.box_id for box in jp2.box[3].box]
- self.assertEqual(ids, ['ihdr', 'colr', 'colr'])
+ ids = [box.box_id for box in jp2.box[2].box]
+ self.assertEqual(ids, ['ihdr', 'colr'])
# Signature box. Check for corruption.
self.assertEqual(jp2.box[0].signature, (13, 10, 135, 10))
# File type box.
- self.assertEqual(jp2.box[1].brand, 'jpx ')
+ self.assertEqual(jp2.box[1].brand, 'jp2 ')
self.assertEqual(jp2.box[1].minor_version, 0)
self.assertEqual(jp2.box[1].compatibility_list[1], 'jp2 ')
- self.assertEqual(jp2.box[1].compatibility_list[2], 'jpx ')
- self.assertEqual(jp2.box[1].compatibility_list[3], 'jpxb')
# Jp2 Header
# Image header
- self.assertEqual(jp2.box[3].box[0].height, 512)
- self.assertEqual(jp2.box[3].box[0].width, 768)
- self.assertEqual(jp2.box[3].box[0].num_components, 3)
- self.assertEqual(jp2.box[3].box[0].signed, False)
- self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
- self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False)
- self.assertEqual(jp2.box[3].box[0].ip_provided, False)
+ self.assertEqual(jp2.box[2].box[0].height, 512)
+ self.assertEqual(jp2.box[2].box[0].width, 768)
+ self.assertEqual(jp2.box[2].box[0].num_components, 3)
+ self.assertEqual(jp2.box[2].box[0].signed, False)
+ self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
# Colour specification
- self.assertEqual(jp2.box[3].box[1].method,
+ self.assertEqual(jp2.box[2].box[1].method,
glymur.core.RESTRICTED_ICC_PROFILE) # enumerated
- self.assertEqual(jp2.box[3].box[1].precedence, 0)
- self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact
- self.assertEqual(jp2.box[3].box[1].icc_profile['Size'], 546)
- self.assertIsNone(jp2.box[3].box[1].colorspace)
-
- # Jp2 Header
- # Colour specification
- self.assertEqual(jp2.box[3].box[2].method,
- glymur.core.ENUMERATED_COLORSPACE)
- self.assertEqual(jp2.box[3].box[2].precedence, 1)
- self.assertEqual(jp2.box[3].box[2].approximation, 1) # JPX exact
- self.assertIsNone(jp2.box[3].box[2].icc_profile)
- self.assertEqual(jp2.box[3].box[2].colorspace,
- glymur.core.ROMM_RGB)
+ self.assertEqual(jp2.box[2].box[1].precedence, 0)
+ self.assertEqual(jp2.box[2].box[1].approximation, 1) # JPX exact
+ self.assertEqual(jp2.box[2].box[1].icc_profile['Size'], 546)
+ self.assertIsNone(jp2.box[2].box[1].colorspace)
def test_NR_file6_dump(self):
jfile = opj_data_file('input/conformance/file6.jp2')
@@ -3398,54 +3390,37 @@ class TestSuiteDump(unittest.TestCase):
jp2 = Jp2k(jfile)
ids = [box.box_id for box in jp2.box]
- self.assertEqual(ids, ['jP ', 'ftyp', 'rreq', 'jp2h', 'jp2c'])
+ self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
- ids = [box.box_id for box in jp2.box[3].box]
- self.assertEqual(ids, ['ihdr', 'colr', 'colr'])
+ ids = [box.box_id for box in jp2.box[2].box]
+ self.assertEqual(ids, ['ihdr', 'colr'])
# Signature box. Check for corruption.
self.assertEqual(jp2.box[0].signature, (13, 10, 135, 10))
# File type box.
- self.assertEqual(jp2.box[1].brand, 'jpx ')
+ self.assertEqual(jp2.box[1].brand, 'jp2 ')
self.assertEqual(jp2.box[1].compatibility_list[1], 'jp2 ')
- self.assertEqual(jp2.box[1].compatibility_list[2], 'jpx ')
- self.assertEqual(jp2.box[1].compatibility_list[3], 'jpxb')
- self.assertEqual(jp2.box[1].minor_version, 0)
-
- # Reader requirements talk.
- # e-SRGB enumerated colourspace
- self.assertTrue(60 in jp2.box[2].standard_flag)
# Jp2 Header
# Image header
- self.assertEqual(jp2.box[3].box[0].height, 640)
- self.assertEqual(jp2.box[3].box[0].width, 480)
- self.assertEqual(jp2.box[3].box[0].num_components, 3)
- self.assertEqual(jp2.box[3].box[0].bits_per_component, 16)
- self.assertEqual(jp2.box[3].box[0].signed, False)
- self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
- self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False)
- self.assertEqual(jp2.box[3].box[0].ip_provided, False)
+ self.assertEqual(jp2.box[2].box[0].height, 640)
+ self.assertEqual(jp2.box[2].box[0].width, 480)
+ self.assertEqual(jp2.box[2].box[0].num_components, 3)
+ self.assertEqual(jp2.box[2].box[0].bits_per_component, 16)
+ self.assertEqual(jp2.box[2].box[0].signed, False)
+ self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
# Colour specification
- self.assertEqual(jp2.box[3].box[1].method,
+ self.assertEqual(jp2.box[2].box[1].method,
glymur.core.RESTRICTED_ICC_PROFILE)
- self.assertEqual(jp2.box[3].box[1].precedence, 0)
- self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact
- self.assertEqual(jp2.box[3].box[1].icc_profile['Size'], 13332)
- self.assertIsNone(jp2.box[3].box[1].colorspace)
-
- # Jp2 Header
- # Colour specification
- self.assertEqual(jp2.box[3].box[2].method,
- glymur.core.ENUMERATED_COLORSPACE)
- self.assertEqual(jp2.box[3].box[2].precedence, 1)
- self.assertEqual(jp2.box[3].box[2].approximation, 1) # JPX exact
- self.assertIsNone(jp2.box[3].box[2].icc_profile)
- self.assertEqual(jp2.box[3].box[2].colorspace,
- glymur.core.E_SRGB)
+ self.assertEqual(jp2.box[2].box[1].precedence, 0)
+ self.assertEqual(jp2.box[2].box[1].approximation, 1)
+ self.assertEqual(jp2.box[2].box[1].icc_profile['Size'], 13332)
+ self.assertIsNone(jp2.box[2].box[1].colorspace)
def test_NR_file8_dump(self):
# One 8-bit component in a gamma 1.8 space. The colourspace is
@@ -3472,7 +3447,7 @@ class TestSuiteDump(unittest.TestCase):
# Image header
self.assertEqual(jp2.box[2].box[0].height, 400)
self.assertEqual(jp2.box[2].box[0].width, 700)
- self.assertEqual(jp2.box[2].box[0].num_components, 1)
+ self.assertEqual(jp2.box[2].box[0].num_components, 3)
self.assertEqual(jp2.box[2].box[0].bits_per_component, 8)
self.assertEqual(jp2.box[2].box[0].signed, False)
self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet
@@ -5507,7 +5482,7 @@ class TestSuiteDump(unittest.TestCase):
jp2 = Jp2k(jfile)
ids = [box.box_id for box in jp2.box]
- self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', 'XML ', 'jp2c'])
+ self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', b'XML ', 'jp2c'])
ids = [box.box_id for box in jp2.box[2].box]
self.assertEqual(ids, ['ihdr', 'colr'])
@@ -5861,9 +5836,9 @@ class TestSuiteDump(unittest.TestCase):
# Jp2 Header
# Component mapping box
- self.assertEqual(jp2.box[3].box[3].component_index, (0, 1, 2))
- self.assertEqual(jp2.box[3].box[3].mapping_type, (1, 1, 0))
- self.assertEqual(jp2.box[3].box[3].palette_index, (0, 0, 1))
+ self.assertEqual(jp2.box[3].box[3].component_index, (0, 0, 0, 0))
+ self.assertEqual(jp2.box[3].box[3].mapping_type, (1, 1, 1, 1))
+ self.assertEqual(jp2.box[3].box[3].palette_index, (0, 1, 2, 3))
c = jp2.box[4].main_header
@@ -6336,11 +6311,16 @@ class TestSuiteDump(unittest.TestCase):
[8, 9, 9, 10, 9, 9, 10, 9, 9, 10, 9, 9, 10, 9, 9, 10])
def test_NR_text_GBR_dump(self):
+ # brand is 'jp2 ', but has any icc profile.
+ # Verify the warning on python3, but ignore it otherwise.
jfile = opj_data_file('input/nonregression/text_GBR.jp2')
- with warnings.catch_warnings():
- # brand is 'jp2 ', but has any icc profile.
- warnings.simplefilter("ignore")
- jp2 = Jp2k(jfile)
+ if sys.hexversion > 0x03030000:
+ with self.assertWarns(UserWarning):
+ jp2 = Jp2k(jfile)
+ else:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(jfile)
ids = [box.box_id for box in jp2.box]
lst = ['jP ', 'ftyp', 'rreq', 'jp2h',
@@ -6647,6 +6627,7 @@ class TestSuite2point1(unittest.TestCase):
Jp2k(jfile).read()
self.assertTrue(True)
+ @unittest.skip("Failing as of r2436")
def test_NR_DEC_mem_b2ace68c_1381_jp2_34_decode(self):
jfile = opj_data_file('input/nonregression/mem-b2ace68c-1381.jp2')
with warnings.catch_warnings():
diff --git a/glymur/test/test_opj_suite_neg.py b/glymur/test/test_opj_suite_neg.py
index 4b14f4d..c9a7e8c 100644
--- a/glymur/test/test_opj_suite_neg.py
+++ b/glymur/test/test_opj_suite_neg.py
@@ -23,8 +23,6 @@ from glymur import Jp2k
import glymur
-@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
- "Functionality not implemented for 1.3, 1.4")
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_OPJ_DATA_ROOT environment variable not set")
class TestSuiteNegative(unittest.TestCase):
diff --git a/glymur/test/test_opj_suite_write.py b/glymur/test/test_opj_suite_write.py
index 22a99e3..c92c500 100644
--- a/glymur/test/test_opj_suite_write.py
+++ b/glymur/test/test_opj_suite_write.py
@@ -12,25 +12,252 @@ import sys
import tempfile
import unittest
+try:
+ import skimage.io
+ skimage.io.use_plugin('freeimage', 'imread')
+ _HAS_SKIMAGE_FREEIMAGE_SUPPORT = True
+except ((ImportError, RuntimeError)):
+ _HAS_SKIMAGE_FREEIMAGE_SUPPORT = False
+
from .fixtures import read_image, NO_READ_BACKEND, NO_READ_BACKEND_MSG
from .fixtures import OPJ_DATA_ROOT, opj_data_file
+from . import fixtures
from glymur import Jp2k
import glymur
+@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
+ "Cannot read input image without scikit-image/freeimage")
+@unittest.skipIf(os.name == "nt", "no write support on windows, period")
+@unittest.skipIf(fixtures.OPENJP2_IS_V2_OFFICIAL,
+ "Feature not supported in 2.0.0 official")
+@unittest.skipIf(glymur.version.openjpeg_version_tuple[0] == 1,
+ "Feature not supported in 1.5")
+@unittest.skipIf(OPJ_DATA_ROOT is None,
+ "OPJ_DATA_ROOT environment variable not set")
+class TestSuiteWriteCinema(unittest.TestCase):
+ """Tests for writing with openjp2 backend.
+
+ These tests either roughly correspond with those tests with similar names
+ in the OpenJPEG test suite or are closely associated.
+ """
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_cinema2K_with_others(self):
+ """Can't specify cinema2k with any other options."""
+ relfile = 'input/nonregression/X_5_2K_24_235_CBR_STEM24_000.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ with self.assertRaises(IOError):
+ j.write(data, cinema2k=48, cratios=[200, 100, 50])
+
+ def test_cinema4K_with_others(self):
+ """Can't specify cinema4k with any other options."""
+ relfile = 'input/nonregression/ElephantDream_4K.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ with self.assertRaises(IOError):
+ j.write(data, cinema4k=True, cratios=[200, 100, 50])
+
+ def check_cinema4k_codestream(self, codestream, image_size):
+ """Common out for cinema2k tests."""
+ # SIZ: Image and tile size
+ # Profile: "3" means cinema2K
+ self.assertEqual(codestream.segment[1].rsiz, 4)
+ # Reference grid size
+ self.assertEqual((codestream.segment[1].xsiz,
+ codestream.segment[1].ysiz),
+ image_size)
+ # Reference grid offset
+ self.assertEqual((codestream.segment[1].xosiz,
+ codestream.segment[1].yosiz), (0, 0))
+ # Tile size
+ self.assertEqual((codestream.segment[1].xtsiz,
+ codestream.segment[1].ytsiz),
+ image_size)
+ # Tile offset
+ self.assertEqual((codestream.segment[1].xtosiz,
+ codestream.segment[1].ytosiz),
+ (0, 0))
+ # bitdepth
+ self.assertEqual(codestream.segment[1].bitdepth, (12, 12, 12))
+ # signed
+ self.assertEqual(codestream.segment[1].signed,
+ (False, False, False))
+ # subsampling
+ self.assertEqual(list(zip(codestream.segment[1].xrsiz,
+ codestream.segment[1].yrsiz)),
+ [(1, 1)] * 3)
+
+ # COD: Coding style default
+ self.assertFalse(codestream.segment[2].scod & 2) # no sop
+ self.assertFalse(codestream.segment[2].scod & 4) # no eph
+ self.assertEqual(codestream.segment[2].spcod[0], glymur.core.CPRL)
+ self.assertEqual(codestream.segment[2].layers, 1)
+ self.assertEqual(codestream.segment[2].spcod[3], 1) # mct
+ self.assertEqual(codestream.segment[2].spcod[4], 5) # levels
+ self.assertEqual(tuple(codestream.segment[2].code_block_size),
+ (32, 32)) # cblksz
+
+
+
+ def check_cinema2k_codestream(self, codestream, image_size):
+ """Common out for cinema2k tests."""
+ # SIZ: Image and tile size
+ # Profile: "3" means cinema2K
+ self.assertEqual(codestream.segment[1].rsiz, 3)
+ # Reference grid size
+ self.assertEqual((codestream.segment[1].xsiz,
+ codestream.segment[1].ysiz),
+ image_size)
+ # Reference grid offset
+ self.assertEqual((codestream.segment[1].xosiz,
+ codestream.segment[1].yosiz), (0, 0))
+ # Tile size
+ self.assertEqual((codestream.segment[1].xtsiz,
+ codestream.segment[1].ytsiz),
+ image_size)
+ # Tile offset
+ self.assertEqual((codestream.segment[1].xtosiz,
+ codestream.segment[1].ytosiz),
+ (0, 0))
+ # bitdepth
+ self.assertEqual(codestream.segment[1].bitdepth, (12, 12, 12))
+ # signed
+ self.assertEqual(codestream.segment[1].signed,
+ (False, False, False))
+ # subsampling
+ self.assertEqual(list(zip(codestream.segment[1].xrsiz,
+ codestream.segment[1].yrsiz)),
+ [(1, 1)] * 3)
+
+ # COD: Coding style default
+ self.assertFalse(codestream.segment[2].scod & 2) # no sop
+ self.assertFalse(codestream.segment[2].scod & 4) # no eph
+ self.assertEqual(codestream.segment[2].spcod[0], glymur.core.CPRL)
+ self.assertEqual(codestream.segment[2].layers, 1)
+ self.assertEqual(codestream.segment[2].spcod[3], 1) # mct
+ self.assertEqual(codestream.segment[2].spcod[4], 5) # levels
+ self.assertEqual(tuple(codestream.segment[2].code_block_size),
+ (32, 32)) # cblksz
+
+
+
+ def test_NR_ENC_ElephantDream_4K_tif_21_encode(self):
+ relfile = 'input/nonregression/ElephantDream_4K.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ j.write(data, cinema4k=True)
+
+ codestream = j.get_codestream()
+ self.check_cinema4k_codestream(codestream, (4096, 2160))
+
+
+ def test_NR_ENC_X_5_2K_24_235_CBR_STEM24_000_tif_19_encode(self):
+ relfile = 'input/nonregression/X_5_2K_24_235_CBR_STEM24_000.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ j.write(data, cinema2k=48)
+
+ codestream = j.get_codestream()
+ self.check_cinema2k_codestream(codestream, (2048, 857))
+
+
+ def test_NR_ENC_X_6_2K_24_FULL_CBR_CIRCLE_000_tif_20_encode(self):
+ relfile = 'input/nonregression/X_6_2K_24_FULL_CBR_CIRCLE_000.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ j.write(data, cinema2k=48)
+
+ codestream = j.get_codestream()
+ self.check_cinema2k_codestream(codestream, (2048, 1080))
+
+
+ def test_NR_ENC_X_6_2K_24_FULL_CBR_CIRCLE_000_tif_17_encode(self):
+ relfile = 'input/nonregression/X_6_2K_24_FULL_CBR_CIRCLE_000.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ j.write(data, cinema2k=24)
+
+ codestream = j.get_codestream()
+ self.check_cinema2k_codestream(codestream, (2048, 1080))
+
+
+ def test_NR_ENC_X_5_2K_24_235_CBR_STEM24_000_tif_16_encode(self):
+ relfile = 'input/nonregression/X_5_2K_24_235_CBR_STEM24_000.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ j.write(data, cinema2k=24)
+
+ codestream = j.get_codestream()
+ self.check_cinema2k_codestream(codestream, (2048, 857))
+
+
+ def test_NR_ENC_X_4_2K_24_185_CBR_WB_000_tif_18_encode(self):
+ relfile = 'input/nonregression/X_4_2K_24_185_CBR_WB_000.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ j.write(data, cinema2k=48)
+
+ codestream = j.get_codestream()
+ self.check_cinema2k_codestream(codestream, (1998, 1080))
+
+@unittest.skipIf(not _HAS_SKIMAGE_FREEIMAGE_SUPPORT,
+ "Cannot read input image without scikit-image/freeimage")
+@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
+@unittest.skipIf(not re.match("(1.5|2.0)", glymur.version.openjpeg_version),
+ "Functionality implemented for 2.1")
+@unittest.skipIf(OPJ_DATA_ROOT is None,
+ "OPJ_OPJ_DATA_ROOT environment variable not set")
+class TestSuiteNegative2pointzero(unittest.TestCase):
+ """Feature set not supported for versions less than 2.0"""
+
+ def setUp(self):
+ self.jp2file = glymur.data.nemo()
+ self.j2kfile = glymur.data.goodstuff()
+
+ def tearDown(self):
+ pass
+
+ def test_cinema_mode(self):
+ relfile = 'input/nonregression/X_4_2K_24_185_CBR_WB_000.tif'
+ infile = opj_data_file(relfile)
+ data = skimage.io.imread(infile)
+ with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
+ j = Jp2k(tfile.name, 'wb')
+ with self.assertRaises(IOError):
+ j.write(data, cinema2k=48)
+
@unittest.skipIf(os.name == "nt", "no write support on windows, period")
-@unittest.skipIf(re.match(r"""1\.[01234]\.\d""",
- glymur.version.openjpeg_version) is not None,
- "Writing only supported with openjpeg version 1.5+.")
@unittest.skipIf(NO_READ_BACKEND, NO_READ_BACKEND_MSG)
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
class TestSuiteWrite(unittest.TestCase):
"""Tests for writing with openjp2 backend.
- These tests roughly correspond with those tests with similar names in the
- OpenJPEG test suite.
+ These tests either roughly correspond with those tests with similar names
+ in the OpenJPEG test suite or are closely associated.
"""
def setUp(self):
pass
@@ -852,5 +1079,6 @@ class TestSuiteWrite(unittest.TestCase):
glymur.core.WAVELET_XFORM_5X3_REVERSIBLE)
self.assertEqual(len(codestream.segment[2].spcod), 9)
+
if __name__ == "__main__":
unittest.main()
diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py
index f90ade3..5286419 100644
--- a/glymur/test/test_printing.py
+++ b/glymur/test/test_printing.py
@@ -50,6 +50,14 @@ class TestPrinting(unittest.TestCase):
def tearDown(self):
pass
+ def test_version_info(self):
+ """Should be able to print(glymur.version.info)"""
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(glymur.version.info)
+ actual = fake_out.getvalue().strip()
+
+ self.assertTrue(True)
+
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
def test_unknown_superbox(self):
"""Verify that we can handle an unknown superbox."""
@@ -60,6 +68,9 @@ class TestPrinting(unittest.TestCase):
# Add the header for an unknwon superbox.
write_buffer = struct.pack('>I4s', 20, 'grp '.encode())
tfile.write(write_buffer)
+
+ # Add a free box inside of it. We won't be able to identify it,
+ # but it's there.
write_buffer = struct.pack('>I4sI', 12, 'free'.encode(), 0)
tfile.write(write_buffer)
tfile.flush()
@@ -70,8 +81,7 @@ class TestPrinting(unittest.TestCase):
with patch('sys.stdout', new=StringIO()) as fake_out:
print(jpx.box[-1])
actual = fake_out.getvalue().strip()
- lines = ['Unknown Box (grp ) @ (695609, 20)',
- ' Free Box (free) @ (695617, 12)']
+ lines = ["Unknown Box (b'grp ') @ (1399071, 20)"]
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
@@ -133,7 +143,6 @@ class TestPrinting(unittest.TestCase):
lst = lst[1:]
actual = '\n'.join(lst)
expected = fixtures.nemo_dump_no_xml
- self.maxDiff = None
self.assertEqual(actual, expected)
def test_printoptions_short(self):
@@ -290,69 +299,6 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_crg(self):
- """verify printing of CRG segment"""
- filename = opj_data_file('input/conformance/p0_03.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream()
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[-5])
- actual = fake_out.getvalue().strip()
- lines = ['CRG marker segment @ (87, 6)',
- ' Vertical, Horizontal offset: (0.50, 1.00)']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_rgn(self):
- """verify printing of RGN segment"""
- filename = opj_data_file('input/conformance/p0_03.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream(header_only=False)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[12])
- actual = fake_out.getvalue().strip()
- lines = ['RGN marker segment @ (310, 5)',
- ' Associated component: 0',
- ' ROI style: 0',
- ' Parameter: 7']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_sop(self):
- """verify printing of SOP segment"""
- filename = opj_data_file('input/conformance/p0_03.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream(header_only=False)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[-2])
- actual = fake_out.getvalue().strip()
- lines = ['SOP marker segment @ (12836, 4)',
- ' Nsop: 15']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_cme(self):
- """Test printing a CME or comment marker segment."""
- filename = opj_data_file('input/conformance/p0_02.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream()
- # 2nd to last segment in the main header
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[-2])
- actual = fake_out.getvalue().strip()
- lines = ['CME marker segment @ (85, 45)',
- ' "Creator: AV-J2K (c) 2000,2001 Algo Vision"']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
def test_eoc_segment(self):
"""verify printing of eoc segment"""
j = glymur.Jp2k(self.jp2file)
@@ -365,91 +311,6 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_plt_segment(self):
- """verify printing of PLT segment"""
- filename = opj_data_file('input/conformance/p0_07.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream(header_only=False)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[49935])
- actual = fake_out.getvalue().strip()
-
- lines = ['PLT marker segment @ (7871146, 38)',
- ' Index: 0',
- ' Iplt: [9, 122, 19, 30, 27, 9, 41, 62, 18, 29, 261,'
- + ' 55, 82, 299, 93, 941, 951, 687, 1729, 1443, 1008, 2168,'
- + ' 2188, 2223]']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_pod_segment(self):
- """verify printing of POD segment"""
- filename = opj_data_file('input/conformance/p0_13.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream()
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[8])
- actual = fake_out.getvalue().strip()
-
- lines = ['POD marker segment @ (878, 20)',
- ' Progression change 0:',
- ' Resolution index start: 0',
- ' Component index start: 0',
- ' Layer index end: 1',
- ' Resolution index end: 33',
- ' Component index end: 128',
- ' Progression order: RLCP',
- ' Progression change 1:',
- ' Resolution index start: 0',
- ' Component index start: 128',
- ' Layer index end: 1',
- ' Resolution index end: 33',
- ' Component index end: 257',
- ' Progression order: CPRL']
-
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_ppm_segment(self):
- """verify printing of PPM segment"""
- filename = opj_data_file('input/conformance/p1_03.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream()
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[9])
- actual = fake_out.getvalue().strip()
-
- lines = ['PPM marker segment @ (213, 43712)',
- ' Index: 0',
- ' Data: 43709 uninterpreted bytes']
-
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_ppt_segment(self):
- """verify printing of ppt segment"""
- filename = opj_data_file('input/conformance/p1_06.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream(header_only=False)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[6])
- actual = fake_out.getvalue().strip()
-
- lines = ['PPT marker segment @ (155, 109)',
- ' Index: 0',
- ' Packet headers: 106 uninterpreted bytes']
-
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
def test_qcc_segment(self):
"""verify printing of qcc segment"""
j = glymur.Jp2k(self.jp2file)
@@ -490,7 +351,7 @@ class TestPrinting(unittest.TestCase):
actual = fake_out.getvalue().strip()
lines = ['SIZ marker segment @ (3233, 47)',
- ' Profile: 2',
+ ' Profile: no profile',
' Reference Grid Height, Width: (1456 x 2592)',
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
' Reference Tile Height, Width: (1456 x 2592)',
@@ -544,25 +405,6 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_tlm_segment(self):
- """verify printing of TLM segment"""
- filename = opj_data_file('input/conformance/p0_15.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream()
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[10])
- actual = fake_out.getvalue().strip()
-
- lines = ['TLM marker segment @ (268, 28)',
- ' Index: 0',
- ' Tile number: (0, 1, 2, 3)',
- ' Length: (4267, 2117, 4080, 2081)']
-
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
def test_xmp(self):
"""Verify the printing of a UUID/XMP box."""
j = glymur.Jp2k(self.jp2file)
@@ -582,7 +424,7 @@ class TestPrinting(unittest.TestCase):
lst = ['Codestream:',
' SOC marker segment @ (3231, 0)',
' SIZ marker segment @ (3233, 47)',
- ' Profile: 2',
+ ' Profile: no profile',
' Reference Grid Height, Width: (1456 x 2592)',
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
' Reference Tile Height, Width: (1456 x 2592)',
@@ -622,17 +464,6 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lst)
self.assertEqual(actual, expected)
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_xml(self):
- """verify printing of XML box"""
- filename = opj_data_file('input/conformance/file1.jp2')
- j = glymur.Jp2k(filename)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(j.box[2])
- actual = fake_out.getvalue().strip()
- self.assertEqual(actual, fixtures.file1_xml)
-
@unittest.skipIf(sys.hexversion < 0x03000000,
"Only trusting python3 for printing non-ascii chars")
def test_xml_latin1(self):
@@ -690,100 +521,6 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_channel_definition(self):
- """verify printing of cdef box"""
- filename = opj_data_file('input/conformance/file2.jp2')
- j = glymur.Jp2k(filename)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(j.box[2].box[2])
- actual = fake_out.getvalue().strip()
- lines = ['Channel Definition Box (cdef) @ (81, 28)',
- ' Channel 0 (color) ==> (3)',
- ' Channel 1 (color) ==> (2)',
- ' Channel 2 (color) ==> (1)']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_component_mapping(self):
- """verify printing of cmap box"""
- filename = opj_data_file('input/conformance/file9.jp2')
- j = glymur.Jp2k(filename)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(j.box[2].box[2])
- actual = fake_out.getvalue().strip()
- lines = ['Component Mapping Box (cmap) @ (848, 20)',
- ' Component 0 ==> palette column 0',
- ' Component 0 ==> palette column 1',
- ' Component 0 ==> palette column 2']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_palette7(self):
- """verify printing of pclr box"""
- filename = opj_data_file('input/conformance/file9.jp2')
- j = glymur.Jp2k(filename)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(j.box[2].box[1])
- actual = fake_out.getvalue().strip()
- lines = ['Palette Box (pclr) @ (66, 782)',
- ' Size: (256 x 3)']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_rreq(self):
- """verify printing of reader requirements box"""
- filename = opj_data_file('input/conformance/file7.jp2')
- j = glymur.Jp2k(filename)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(j.box[2])
- actual = fake_out.getvalue().strip()
- self.assertEqual(actual, fixtures.file7_rreq)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_differing_subsamples(self):
- """verify printing of SIZ with different subsampling... Issue 86."""
- filename = opj_data_file('input/conformance/p0_05.j2k')
- j = glymur.Jp2k(filename)
- codestream = j.get_codestream()
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(codestream.segment[1])
- actual = fake_out.getvalue().strip()
- lines = ['SIZ marker segment @ (2, 50)',
- ' Profile: 0',
- ' Reference Grid Height, Width: (1024 x 1024)',
- ' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
- ' Reference Tile Height, Width: (1024 x 1024)',
- ' Vertical, Horizontal Reference Tile Offset: (0 x 0)',
- ' Bitdepth: (8, 8, 8, 8)',
- ' Signed: (False, False, False, False)',
- ' Vertical, Horizontal Subsampling: '
- + '((1, 1), (1, 1), (2, 2), (2, 2))']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_palette_box(self):
- """Verify that palette (pclr) boxes are printed without error."""
- filename = opj_data_file('input/conformance/file9.jp2')
- j = glymur.Jp2k(filename)
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(j.box[2].box[1])
- actual = fake_out.getvalue().strip()
- lines = ['Palette Box (pclr) @ (66, 782)',
- ' Size: (256 x 3)']
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_less_common_boxes(self):
"""verify uinf, ulst, url, res, resd, resc box printing"""
@@ -861,50 +598,6 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_icc_profile(self):
- """verify icc profile printing with a jpx"""
- # ICC profiles may be used in JP2, but the approximation field should
- # be zero unless we have jpx. This file does both.
- filename = opj_data_file('input/nonregression/text_GBR.jp2')
- with warnings.catch_warnings():
- # brand is 'jp2 ', but has any icc profile.
- warnings.simplefilter("ignore")
- jp2 = Jp2k(filename)
-
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(jp2.box[3].box[1])
- actual = fake_out.getvalue().strip()
- if sys.hexversion < 0x03000000:
- expected = text_gbr_27
- elif sys.hexversion < 0x03040000:
- expected = text_gbr_33
- else:
- expected = text_gbr_34
-
- self.assertEqual(actual, expected)
-
- @unittest.skipIf(OPJ_DATA_ROOT is None,
- "OPJ_DATA_ROOT environment variable not set")
- def test_uuid(self):
- """verify printing of UUID box"""
- filename = opj_data_file('input/nonregression/text_GBR.jp2')
- with warnings.catch_warnings():
- # brand is 'jp2 ', but has any icc profile.
- warnings.simplefilter("ignore")
- jp2 = Jp2k(filename)
-
- with patch('sys.stdout', new=StringIO()) as fake_out:
- print(jp2.box[4])
- actual = fake_out.getvalue().strip()
- lines = ['UUID Box (uuid) @ (1544, 25)',
- ' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71 (unknown)',
- ' UUID Data: 1 bytes']
-
- expected = '\n'.join(lines)
- self.assertEqual(actual, expected)
-
@unittest.skipIf(sys.hexversion < 0x03000000,
"Ordered dicts not printing well in 2.7")
def test_exif_uuid(self):
@@ -946,5 +639,395 @@ class TestPrinting(unittest.TestCase):
self.assertEqual(actual, expected)
+@unittest.skipIf(OPJ_DATA_ROOT is None,
+ "OPJ_DATA_ROOT environment variable not set")
+@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
+class TestPrintingOpjDataRoot(unittest.TestCase):
+ """Tests for verifying printing. restricted to OPJ_DATA_ROOT files."""
+ def setUp(self):
+ self.jpxfile = glymur.data.jpxfile()
+ self.jp2file = glymur.data.nemo()
+ self.j2kfile = glymur.data.goodstuff()
+
+ # Reset printoptions for every test.
+ glymur.set_printoptions(short=False, xml=True, codestream=True)
+
+ def tearDown(self):
+ pass
+
+ def test_cinema_profile(self):
+ """Should print Cinema 2K when the profile is 3."""
+ filename = opj_data_file('input/nonregression/_00042.j2k')
+ j2k = Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ c = j2k.get_codestream()
+ print(c.segment[1])
+ actual = fake_out.getvalue().strip()
+ self.assertEqual(actual, fixtures.cinema2k_profile)
+
+ def test_invalid_colorspace(self):
+ """An invalid colorspace shouldn't cause an error."""
+ filename = opj_data_file('input/nonregression/edf_c2_1103421.jp2')
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(jp2)
+
+ def test_bad_rsiz(self):
+ """Should still be able to print if rsiz is bad, issue196"""
+ filename = opj_data_file('input/nonregression/edf_c2_1002767.jp2')
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ j = Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(j)
+
+ def test_bad_wavelet_transform(self):
+ """Should still be able to print if wavelet xform is bad, issue195"""
+ filename = opj_data_file('input/nonregression/edf_c2_10025.jp2')
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ j = Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(j)
+
+ def test_invalid_progression_order(self):
+ """Should still be able to print even if prog order is invalid."""
+ jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2')
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(jfile)
+ codestream = jp2.get_codestream()
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[2])
+ actual = fake_out.getvalue().strip()
+ self.assertEqual(actual, fixtures.issue_186_progression_order)
+
+ def test_crg(self):
+ """verify printing of CRG segment"""
+ filename = opj_data_file('input/conformance/p0_03.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream()
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[-5])
+ actual = fake_out.getvalue().strip()
+ lines = ['CRG marker segment @ (87, 6)',
+ ' Vertical, Horizontal offset: (0.50, 1.00)']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_rgn(self):
+ """verify printing of RGN segment"""
+ filename = opj_data_file('input/conformance/p0_03.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream(header_only=False)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[12])
+ actual = fake_out.getvalue().strip()
+ lines = ['RGN marker segment @ (310, 5)',
+ ' Associated component: 0',
+ ' ROI style: 0',
+ ' Parameter: 7']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_sop(self):
+ """verify printing of SOP segment"""
+ filename = opj_data_file('input/conformance/p0_03.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream(header_only=False)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[-2])
+ actual = fake_out.getvalue().strip()
+ lines = ['SOP marker segment @ (12836, 4)',
+ ' Nsop: 15']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_cme(self):
+ """Test printing a CME or comment marker segment."""
+ filename = opj_data_file('input/conformance/p0_02.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream()
+ # 2nd to last segment in the main header
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[-2])
+ actual = fake_out.getvalue().strip()
+ lines = ['CME marker segment @ (85, 45)',
+ ' "Creator: AV-J2K (c) 2000,2001 Algo Vision"']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_plt_segment(self):
+ """verify printing of PLT segment"""
+ filename = opj_data_file('input/conformance/p0_07.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream(header_only=False)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[49935])
+ actual = fake_out.getvalue().strip()
+
+ lines = ['PLT marker segment @ (7871146, 38)',
+ ' Index: 0',
+ ' Iplt: [9, 122, 19, 30, 27, 9, 41, 62, 18, 29, 261,'
+ + ' 55, 82, 299, 93, 941, 951, 687, 1729, 1443, 1008, 2168,'
+ + ' 2188, 2223]']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_pod_segment(self):
+ """verify printing of POD segment"""
+ filename = opj_data_file('input/conformance/p0_13.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream()
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[8])
+ actual = fake_out.getvalue().strip()
+
+ lines = ['POD marker segment @ (878, 20)',
+ ' Progression change 0:',
+ ' Resolution index start: 0',
+ ' Component index start: 0',
+ ' Layer index end: 1',
+ ' Resolution index end: 33',
+ ' Component index end: 128',
+ ' Progression order: RLCP',
+ ' Progression change 1:',
+ ' Resolution index start: 0',
+ ' Component index start: 128',
+ ' Layer index end: 1',
+ ' Resolution index end: 33',
+ ' Component index end: 257',
+ ' Progression order: CPRL']
+
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_ppm_segment(self):
+ """verify printing of PPM segment"""
+ filename = opj_data_file('input/conformance/p1_03.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream()
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[9])
+ actual = fake_out.getvalue().strip()
+
+ lines = ['PPM marker segment @ (213, 43712)',
+ ' Index: 0',
+ ' Data: 43709 uninterpreted bytes']
+
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_ppt_segment(self):
+ """verify printing of ppt segment"""
+ filename = opj_data_file('input/conformance/p1_06.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream(header_only=False)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[6])
+ actual = fake_out.getvalue().strip()
+
+ lines = ['PPT marker segment @ (155, 109)',
+ ' Index: 0',
+ ' Packet headers: 106 uninterpreted bytes']
+
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_tlm_segment(self):
+ """verify printing of TLM segment"""
+ filename = opj_data_file('input/conformance/p0_15.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream()
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[10])
+ actual = fake_out.getvalue().strip()
+
+ lines = ['TLM marker segment @ (268, 28)',
+ ' Index: 0',
+ ' Tile number: (0, 1, 2, 3)',
+ ' Length: (4267, 2117, 4080, 2081)']
+
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_xml(self):
+ """verify printing of XML box"""
+ filename = opj_data_file('input/conformance/file1.jp2')
+ j = glymur.Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(j.box[2])
+ actual = fake_out.getvalue().strip()
+ self.assertEqual(actual, fixtures.file1_xml)
+
+ def test_channel_definition(self):
+ """verify printing of cdef box"""
+ filename = opj_data_file('input/conformance/file2.jp2')
+ with warnings.catch_warnings():
+ # Bad compatibility list item.
+ warnings.simplefilter("ignore")
+ j = glymur.Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(j.box[2].box[2])
+ actual = fake_out.getvalue().strip()
+ lines = ['Channel Definition Box (cdef) @ (81, 28)',
+ ' Channel 0 (color) ==> (3)',
+ ' Channel 1 (color) ==> (2)',
+ ' Channel 2 (color) ==> (1)']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_component_mapping(self):
+ """verify printing of cmap box"""
+ filename = opj_data_file('input/conformance/file9.jp2')
+ j = glymur.Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(j.box[2].box[2])
+ actual = fake_out.getvalue().strip()
+ lines = ['Component Mapping Box (cmap) @ (848, 20)',
+ ' Component 0 ==> palette column 0',
+ ' Component 0 ==> palette column 1',
+ ' Component 0 ==> palette column 2']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_palette7(self):
+ """verify printing of pclr box"""
+ filename = opj_data_file('input/conformance/file9.jp2')
+ j = glymur.Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(j.box[2].box[1])
+ actual = fake_out.getvalue().strip()
+ lines = ['Palette Box (pclr) @ (66, 782)',
+ ' Size: (256 x 3)']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ @unittest.skip("file7 no longer has a rreq")
+ def test_rreq(self):
+ """verify printing of reader requirements box"""
+ filename = opj_data_file('input/nonregression/text_GBR.jp2')
+ j = glymur.Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(j.box[2])
+ actual = fake_out.getvalue().strip()
+ self.assertEqual(actual, fixtures.text_GBR_rreq)
+
+ def test_differing_subsamples(self):
+ """verify printing of SIZ with different subsampling... Issue 86."""
+ filename = opj_data_file('input/conformance/p0_05.j2k')
+ j = glymur.Jp2k(filename)
+ codestream = j.get_codestream()
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(codestream.segment[1])
+ actual = fake_out.getvalue().strip()
+ lines = ['SIZ marker segment @ (2, 50)',
+ ' Profile: 0',
+ ' Reference Grid Height, Width: (1024 x 1024)',
+ ' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
+ ' Reference Tile Height, Width: (1024 x 1024)',
+ ' Vertical, Horizontal Reference Tile Offset: (0 x 0)',
+ ' Bitdepth: (8, 8, 8, 8)',
+ ' Signed: (False, False, False, False)',
+ ' Vertical, Horizontal Subsampling: '
+ + '((1, 1), (1, 1), (2, 2), (2, 2))']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_palette_box(self):
+ """Verify that palette (pclr) boxes are printed without error."""
+ filename = opj_data_file('input/conformance/file9.jp2')
+ j = glymur.Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(j.box[2].box[1])
+ actual = fake_out.getvalue().strip()
+ lines = ['Palette Box (pclr) @ (66, 782)',
+ ' Size: (256 x 3)']
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_icc_profile(self):
+ """verify icc profile printing with a jpx"""
+ # ICC profiles may be used in JP2, but the approximation field should
+ # be zero unless we have jpx. This file does both.
+ filename = opj_data_file('input/nonregression/text_GBR.jp2')
+ with warnings.catch_warnings():
+ # brand is 'jp2 ', but has any icc profile.
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(jp2.box[3].box[1])
+ actual = fake_out.getvalue().strip()
+ if sys.hexversion < 0x03000000:
+ expected = text_gbr_27
+ elif sys.hexversion < 0x03040000:
+ expected = text_gbr_33
+ else:
+ expected = text_gbr_34
+
+ self.assertEqual(actual, expected)
+
+ def test_uuid(self):
+ """verify printing of UUID box"""
+ filename = opj_data_file('input/nonregression/text_GBR.jp2')
+ with warnings.catch_warnings():
+ # brand is 'jp2 ', but has any icc profile.
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(jp2.box[4])
+ actual = fake_out.getvalue().strip()
+ lines = ['UUID Box (uuid) @ (1544, 25)',
+ ' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71 (unknown)',
+ ' UUID Data: 1 bytes']
+
+ expected = '\n'.join(lines)
+ self.assertEqual(actual, expected)
+
+ def test_issue182(self):
+ """Should not show the format string in output."""
+ # The cmap box is wildly broken, but printing was still wrong.
+ # Format strings like %d were showing up in the output.
+ filename = opj_data_file('input/nonregression/mem-b2ace68c-1381.jp2')
+
+ with warnings.catch_warnings():
+ # Ignore warning about bad pclr box.
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(jp2.box[3].box[3])
+ actual = fake_out.getvalue().strip()
+ self.assertEqual(actual, fixtures.issue_182_cmap)
+
+ def test_issue183(self):
+ filename = opj_data_file('input/nonregression/orb-blue10-lin-jp2.jp2')
+
+ with warnings.catch_warnings():
+ # Ignore warning about bad pclr box.
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ print(jp2.box[2].box[1])
+ actual = fake_out.getvalue().strip()
+ self.assertEqual(actual, fixtures.issue_183_colr)
+
+ def test_bom(self):
+ """Byte order markers are illegal in UTF-8. Issue 185"""
+ filename = opj_data_file(os.path.join('input',
+ 'nonregression',
+ 'issue171.jp2'))
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ jp2 = Jp2k(filename)
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ # No need to verify, it's enough that we don't error out.
+ print(jp2)
+
+ self.assertTrue(True)
+
if __name__ == "__main__":
unittest.main()
diff --git a/glymur/version.py b/glymur/version.py
index 4bce9dc..409db9a 100644
--- a/glymur/version.py
+++ b/glymur/version.py
@@ -9,9 +9,11 @@ License: MIT
"""
import sys
-import numpy as np
from distutils.version import LooseVersion
+import lxml.etree
+import numpy as np
+
from .lib import openjpeg as opj
from .lib import openjp2 as opj2
@@ -48,10 +50,12 @@ OPENJPEG {openjpeg}
Python {python}
sys.platform {platform}
sys.maxsize {maxsize}
+lxml {elxml}
numpy {numpy}
""".format(glymur=version,
openjpeg=openjpeg_version,
python=sys.version,
platform=sys.platform,
maxsize=sys.maxsize,
+ elxml=lxml.etree.__version__,
numpy=np.__version__)