diff --git a/CHANGES.txt b/CHANGES.txt
index ef8ef6c..efc70fc 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,7 @@
+Jun 27, 2013 - v0.1.10 Can wrap codestreams in custom JP2 jackets. Exposing
+ parameter to specify multi component transform. Added a raw codestream
+ file.
+
Jun 16, 2013 - v0.1.9 Reading ICC profile headers as ordered dicts. Exif
dictionaries changed to ordered dicts. Honoring XDG_CONFIG_HOME
environment variable.
diff --git a/docs/source/api.rst b/docs/source/api.rst
index e1efdac..7584718 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -5,7 +5,7 @@ API
Jp2k
----
.. autoclass:: glymur.Jp2k
- :members: read, write, read_bands, get_codestream
+ :members: read, write, wrap, read_bands, get_codestream
Individual Boxes
----------------
@@ -24,9 +24,9 @@ ColourSpecificationBox
.. autoclass:: glymur.jp2box.ColourSpecificationBox
:members:
-ComponentDefinitionBox
+ChannelDefinitionBox
''''''''''''''''''''''
-.. autoclass:: glymur.jp2box.ComponentDefinitionBox
+.. autoclass:: glymur.jp2box.ChannelDefinitionBox
:members:
ComponentMappingBox
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 0e3563a..6ddfcfb 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -78,7 +78,7 @@ copyright = u'2013, John Evans'
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
-release = '0.1.9'
+release = '0.1.10'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/docs/source/detailed_installation.rst b/docs/source/detailed_installation.rst
index 1771722..5090cdc 100644
--- a/docs/source/detailed_installation.rst
+++ b/docs/source/detailed_installation.rst
@@ -56,6 +56,27 @@ repositories::
$ pip-python3 install Pillow --user
$ export PYTHONPATH=$HOME/.local/lib/python3.3/site-packages:$PYTHONPATH
+Raspbian
+''''''''
+Yeah, this was the first thing I tried after getting my new Raspberry Pi hooked
+up (couldn't help myself :-) Raspbian ships with Python 3.2 and 2.7, so these steps detail working with 2.7.
+
+Additional required OS packages include::
+
+ * python-pip
+ * python-pkg-resources
+ * python-mock
+
+You must install contextlib2 via pip, and then you can run at least
+a minimal number of tests. To attempt to run more of the tests,
+install the following debs::
+
+ * python-dev
+ * python-matplotlib
+
+and then install Pillow via pip. The tests take about 30 minutes to run, with
+one unexpected failure as of the time of writing.
+
Fedora 17
'''''''''
Fedora 17 ships with Python 3.2 and 2.7, so these steps detail working with
@@ -77,8 +98,8 @@ combinations of RPMs / Python packages is installed.
* scikit-image and either Pillow or freeimage
* matplotlib and Pillow
-scikit-image is not available in the Fedora 17 default repositories, but
-it may be installed via pip::
+scikit-image was not available in the Fedora 17 default repositories, but
+it was installable via pip::
$ yum install Cython # pip needs this in order to compile scikit-image
$ yum install python-devel # pip needs this in order to compile scikit-image
@@ -96,7 +117,13 @@ Not currently supported.
Testing
'''''''
-If you wish to run the tests (strongly suggested :-), try the following
+If you wish to run the tests (strongly suggested :-), you can either run them
+from within python as follows ... ::
+
+ >>> import glymur
+ >>> glymur.runtests()
+
+or from the unix command line. ::
$ cd /to/where/you/unpacked/glymur
$ python -m unittest discover
diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst
index 355ff14..a15b865 100644
--- a/docs/source/how_do_i.rst
+++ b/docs/source/how_do_i.rst
@@ -34,6 +34,91 @@ codestream box, only the main header is printed. It is possible to print
>>> print(j.get_codestream())
+Add XML Metadata?
+=================
+An existing raw codestream (or JP2 file) can be wrapped (re-wrapped) in a
+user-defined set of JP2 boxes. To get just a minimal JP2 jacket on the
+codestream provided by `goodstuff.j2k`, you can use the **wrap** method with
+no box argument: ::
+
+ >>> import glymur
+ >>> jfile = glymur.data.goodstuff()
+ >>> j2k = glymur.Jp2k(jfile)
+ >>> jp2 = j2k.wrap("newfile.jp2")
+ >>> print(jp2)
+ File: newfile.jp2
+ JPEG 2000 Signature Box (jP ) @ (0, 12)
+ Signature: 0d0a870a
+ File Type Box (ftyp) @ (12, 20)
+ Brand: jp2
+ Compatibility: ['jp2 ']
+ JP2 Header Box (jp2h) @ (32, 45)
+ Image Header Box (ihdr) @ (40, 22)
+ Size: [800 480 3]
+ Bitdepth: 8
+ Signed: False
+ Compression: wavelet
+ Colorspace Unknown: False
+ Colour Specification Box (colr) @ (62, 15)
+ Method: enumerated colorspace
+ Precedence: 0
+ Colorspace: sRGB
+ Contiguous Codestream Box (jp2c) @ (77, 115228)
+ Main header:
+ .
+ . (truncated)
+ .
+
+The raw codestream was wrapped in a JP2 jacket with four boxes in the outer
+layer (the signature, file type, JP2 header, and contiguous codestream), with
+two additional boxes (image header and color specification) contained in the
+JP2 header superbox.
+
+XML boxes are not in the minimal set of box requirements for the JP2 format, so
+in order to add an XML box into the mix, we'll need to specify all of the
+boxes. If you already have a JP2 jacket in place, you can just reuse it,
+though. Take the following example content in an XML file `favorites.xml` : ::
+
+
+
+ Light Ale
+
+
+and add it after the JP2 header box, but before the codestream box ::
+
+ >>> boxes = jp2.box # The box attribute is the list of JP2 boxes
+ >>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml')
+ >>> boxes.insert(3, xmlbox)
+ >>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes)
+ >>> print(jp2_xml)
+ File: newfile_with_xml.jp2
+ JPEG 2000 Signature Box (jP ) @ (0, 12)
+ Signature: 0d0a870a
+ File Type Box (ftyp) @ (12, 20)
+ Brand: jp2
+ Compatibility: ['jp2 ']
+ JP2 Header Box (jp2h) @ (32, 45)
+ Image Header Box (ihdr) @ (40, 22)
+ Size: [800 480 3]
+ Bitdepth: 8
+ Signed: False
+ Compression: wavelet
+ Colorspace Unknown: False
+ Colour Specification Box (colr) @ (62, 15)
+ Method: enumerated colorspace
+ Precedence: 0
+ Colorspace: sRGB
+ XML Box (xml ) @ (77, 76)
+
+ Light Ale
+
+
+ Contiguous Codestream Box (jp2c) @ (153, 115236)
+ Main header:
+ .
+ . (truncated)
+ .
+
Work with XMP UUIDs?
====================
The example JP2 file shipped with glymur has an XMP UUID. ::
diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst
index d21d7eb..f8d4c65 100644
--- a/docs/source/introduction.rst
+++ b/docs/source/introduction.rst
@@ -50,7 +50,7 @@ the openjp2 library. You may create the configuration file as follows::
> EOF
That assumes, of course, that you've installed OpenJPEG into /opt/openjp2-svn.
-You may also replace **$HOME/.config** with **$XDG_CONFIG_HOME**.
+You may also substitute **$XDG_CONFIG_HOME** for **$HOME/.config**.
Glymur Installation
===================
@@ -59,14 +59,22 @@ You can retrieve the source for Glymur from either of
* https://pypi.python.org/pypi/Glymur/ (stable releases)
* http://github.com/quintusdias/glymur (bleeding edge)
-but you should now be able to get a functional installation of Glymur via
-pip ::
+but you should also be able to install Glymur via pip ::
$ pip install glymur
-This will install the **jp2dump** script, so you should adjust your **$PATH**
+This will install the **jp2dump** script that can be used from the unix command
+line, so you should adjust your **$PATH**
to take advantage of it. For example, if you install with pip's
`--user` option on linux ::
$ export PYTHONPATH=$HOME/.local/lib/python3.3/site-packages
$ export PATH=$HOME/.local/bin:$PATH
+
+You can run the tests from within python as follows::
+
+ >>> import glymur
+ >>> glymur.runtests()
+
+Many tests are currently skipped; the important thing is whether or not any
+tests fail.
diff --git a/docs/source/roadmap.rst b/docs/source/roadmap.rst
index a6f967a..ea4a5bd 100644
--- a/docs/source/roadmap.rst
+++ b/docs/source/roadmap.rst
@@ -6,7 +6,7 @@ Here's an incomplete list of what I'd like to focus on in the near future.
* continue to monitor upstream changes in the openjp2 library
* investigate using CFFI or cython instead of ctypes to wrap openjp2
+ * investigate swapping out ElementTree for LXML
* eventually expose the openjp2 API
- * add metadata support for ICC profiles
* investigate JPIP
diff --git a/glymur/__init__.py b/glymur/__init__.py
index 473bb65..c720c16 100644
--- a/glymur/__init__.py
+++ b/glymur/__init__.py
@@ -1,92 +1,16 @@
"""glymur - read, write, and interrogate JPEG 2000 files
"""
-
-def _glymurrc_fname():
- """Return the path to the configuration file.
-
- Search order:
- 1) current working directory
- 2) environ var XDG_CONFIG_HOME
- 3) $HOME/.config/glymur/glymurrc
- """
-
- # Current directory.
- fname = os.path.join(os.getcwd(), 'glymurrc')
- if os.path.exists(fname):
- return fname
-
- # Either GLYMURCONFIGDIR/glymurrc or $HOME/.glymur/glymurrc
- confdir = _get_configdir()
- if confdir is not None:
- fname = os.path.join(confdir, 'glymurrc')
- if os.path.exists(fname):
- return fname
- else:
- msg = "Configuration file '{0}' does not exist.".format(confdir)
- warnings.warn(msg, UserWarning)
-
- # didn't find a configuration file.
- return None
-
-
-def _config():
- """Read configuration file.
-
- Based on matplotlib.
- """
- filename = _glymurrc_fname()
- if filename is not None:
- # Read the configuration file for the library location.
- parser = ConfigParser()
- parser.read(filename)
- libopenjp2_path = parser.get('library', 'openjp2')
- else:
- # No help from the config file, try to find it ourselves.
- from ctypes.util import find_library
- libopenjp2_path = find_library('openjp2')
-
- if libopenjp2_path is None:
- return None
-
- try:
- _OPENJP2 = ctypes.CDLL(libopenjp2_path)
- except OSError:
- msg = '"Library {0}" could not be loaded. Operating in degraded mode.'
- msg = msg.format(libopenjp2_path)
- warnings.warn(msg, UserWarning)
- _OPENJP2 = None
- return _OPENJP2
-
-
-def _get_configdir():
- """Return string representing the configuration directory.
-
- Default is $HOME/.config/glymur. You can override this with the
- XDG_CONFIG_HOME environment variable.
- """
-
- if 'XDG_CONFIG_HOME' in os.environ:
- return os.path.join(os.environ['XDG_CONFIG_HOME'], 'glymur')
-
- if 'HOME' in os.environ:
- return os.path.join(os.environ['HOME'], '.config', 'glymur')
-
-import warnings
-import sys
-if sys.hexversion <= 0x03000000:
- from ConfigParser import SafeConfigParser as ConfigParser
- from ConfigParser import NoOptionError
-else:
- from configparser import ConfigParser
- from configparser import NoOptionError
-import ctypes
-import os
-
-_OPENJP2 = _config()
-
from .jp2k import Jp2k
from .jp2dump import jp2dump
from . import data
from . import test
+
+
+def runtests():
+ """Discover and run all tests for the glymur package.
+ """
+ import unittest
+ suite = unittest.defaultTestLoader.discover(__path__[0])
+ unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/glymur/codestream.py b/glymur/codestream.py
index 04c208f..327dd19 100644
--- a/glymur/codestream.py
+++ b/glymur/codestream.py
@@ -175,7 +175,7 @@ class Codestream(object):
segment = self._parseGenericSegment(f, marker_id)
else:
- msg = 'Invalid marker id encountered at byte {0:d}'
+ msg = 'Invalid marker id encountered at byte {0:d} '
msg += 'in codestream: "0x{1:x}"'
msg = msg.format(offset, marker_id)
raise IOError(msg)
diff --git a/glymur/core.py b/glymur/core.py
index 73a286e..6483558 100644
--- a/glymur/core.py
+++ b/glymur/core.py
@@ -74,6 +74,12 @@ _color_type_map_display = {
_PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity',
_UNSPECIFIED: 'unspecified'}
+# color channel definitions.
+RED = 1
+GREEN = 2
+BLUE = 3
+GREY = 1
+
# enumerated color channel associations
_rgb_colorspace = {"R": 1, "G": 2, "B": 3}
_greyscale_colorspace = {"Y": 1}
diff --git a/glymur/data/__init__.py b/glymur/data/__init__.py
index ff39c0e..bebfb08 100644
--- a/glymur/data/__init__.py
+++ b/glymur/data/__init__.py
@@ -1,8 +1,10 @@
"""Shipping JPEG 2000 files.
-There is only one JP2 file at the moment, "nemo.jp2", converted from the
-original JPEG photo of the aftermath of NEMO, the nor'easter that shutdown
-Boston in February of 2013.
+These include:
+ nemo.jp2: converted from the original JPEG photo of the aftermath of NEMO,
+ the nor'easter that shutdown Boston in February of 2013.
+ goodstuff.j2k: my favorite bevorage.
+
"""
import pkg_resources
@@ -17,3 +19,15 @@ def nemo():
"""
file = pkg_resources.resource_filename(__name__, "nemo.jp2")
return file
+
+
+def goodstuff():
+ """Shortcut for specifying path to goodstuff.j2k.
+
+ Returns
+ -------
+ file : str
+ Platform-independent path to goodstuff.j2k.
+ """
+ file = pkg_resources.resource_filename(__name__, "goodstuff.j2k")
+ return file
diff --git a/glymur/data/goodstuff.j2k b/glymur/data/goodstuff.j2k
new file mode 100644
index 0000000..3e3c1b6
Binary files /dev/null and b/glymur/data/goodstuff.j2k differ
diff --git a/glymur/jp2box.py b/glymur/jp2box.py
index d4a44e9..e0107f3 100644
--- a/glymur/jp2box.py
+++ b/glymur/jp2box.py
@@ -31,6 +31,7 @@ from .core import _colorspace_map_display
from .core import _color_type_map_display
from .core import _method_display
from .core import _reader_requirements_display
+from .core import *
class Jp2kBox(object):
@@ -59,6 +60,12 @@ class Jp2kBox(object):
msg += " @ ({0}, {1})".format(self.offset, self.length)
return msg
+ def _write(self, f):
+ """Must be implemented in a subclass.
+ """
+ msg = "Not supported for {0} box.".format(self.longname)
+ raise NotImplementedError(msg)
+
def _parse_superbox(self, f):
"""Parse a superbox (box consisting of nothing but other boxes.
@@ -159,9 +166,22 @@ class ColourSpecificationBox(Jp2kBox):
ICC profile header according to ICC profile specification. If
colorspace is not None, then icc_profile must be empty.
"""
- def __init__(self, **kwargs):
- Jp2kBox.__init__(self, id='', longname='Colour Specification')
+ def __init__(self, method=ENUMERATED_COLORSPACE, precedence=0,
+ approximation=0, colorspace=None, icc_profile=None, **kwargs):
+ Jp2kBox.__init__(self, id='colr', longname='Colour Specification')
+
+ if colorspace is not None and icc_profile is not None:
+ raise IOError("colorspace and icc_profile cannot both be set.")
+ if method not in (1, 2, 3, 4):
+ raise IOError("Invalid method.")
+ if approximation not in (0, 1, 2, 3, 4):
+ raise IOError("Invalid approximation.")
self.__dict__.update(**kwargs)
+ self.method = method
+ self.precedence = precedence
+ self.approximation = approximation
+ self.colorspace = colorspace
+ self.icc_profile = icc_profile
def __str__(self):
msg = Jp2kBox.__str__(self)
@@ -188,6 +208,24 @@ class ColourSpecificationBox(Jp2kBox):
return msg
+ def _write(self, f):
+ """Write an Colour Specification box to file.
+ """
+ if self.colorspace is None:
+ msg = "Writing Colour Specification boxes without enumerated "
+ msg += "colorspaces is not supported at this time."
+ raise NotImplementedError(msg)
+ length = 15 if self.icc_profile is None else 11 + len(self.icc_profile)
+ f.write(struct.pack('>I', length))
+ f.write('colr'.encode())
+
+ buffer = struct.pack('>BBBI',
+ self.method,
+ self.precedence,
+ self.approximation,
+ self.colorspace)
+ f.write(buffer)
+
@staticmethod
def _parse(f, id, offset, length):
"""Parse JPEG 2000 color specification box.
@@ -354,7 +392,7 @@ class _ICCProfile(object):
self.header = header
-class ComponentDefinitionBox(Jp2kBox):
+class ChannelDefinitionBox(Jp2kBox):
"""Container for component definition box information.
Attributes
@@ -367,29 +405,58 @@ class ComponentDefinitionBox(Jp2kBox):
offset of the box from the start of the file.
longname : str
more verbose description of the box.
- component_number : int
- number of the component
- component_type : int
- type of the component
+ index : int
+ number of the channel
+ channel_type : int
+ type of the channel
association : int
- number of the associated color
+ index of the associated color
"""
- def __init__(self, **kwargs):
- Jp2kBox.__init__(self, id='', longname='Component Definition')
+ def __init__(self, index, channel_type, association, **kwargs):
+ Jp2kBox.__init__(self, id='cdef', longname='Channel Definition')
+ if len(index) != len(channel_type) or len(index) != len(association):
+ msg = "Length of channel definition box inputs must be the same."
+ raise IOError(msg)
+
+ # channel types must be one of 0, 1, 2, 65535
+ if any(x not in [0, 1, 2, 65535] for x in channel_type):
+ msg = "Channel types must be in the set of\n\n"
+ msg += " 0 - colour image data for associated color\n"
+ msg += " 1 - opacity\n"
+ msg += " 2 - premultiplied opacity\n"
+ msg += " 65535 - unspecified"
+ raise IOError(msg)
+
+ self.index = index
+ self.channel_type = channel_type
+ self.association = association
self.__dict__.update(**kwargs)
def __str__(self):
msg = Jp2kBox.__str__(self)
for j in range(len(self.association)):
- color_type_string = _color_type_map_display[self.component_type[j]]
+ color_type_string = _color_type_map_display[self.channel_type[j]]
if self.association[j] == 0:
assn = 'whole image'
else:
assn = str(self.association[j])
- msg += '\n Component {0} ({1}) ==> ({2})'
- msg = msg.format(self.component_number[j], color_type_string, assn)
+ msg += '\n Channel {0} ({1}) ==> ({2})'
+ msg = msg.format(self.index[j], color_type_string, assn)
return msg
+ def _write(self, f):
+ """Write a channel definition box to file.
+ """
+ N = len(self.association)
+ f.write(struct.pack('>I', 8 + 2 + N * 6))
+ f.write('cdef'.encode('utf-8'))
+ f.write(struct.pack('>H', N))
+ for j in range(N):
+ f.write(struct.pack('>' + 'H' * 3,
+ self.index[j],
+ self.channel_type[j],
+ self.association[j]))
+
@staticmethod
def _parse(f, id, offset, length):
"""Parse component definition box.
@@ -418,17 +485,17 @@ class ComponentDefinitionBox(Jp2kBox):
buffer = f.read(2)
N, = struct.unpack('>H', buffer)
- component_number = []
- component_type = []
+ index = []
+ chan_type = []
association = []
buffer = f.read(N * 6)
data = struct.unpack('>' + 'HHH' * N, buffer)
- kwargs['component_number'] = data[0:N * 6:3]
- kwargs['component_type'] = data[1:N * 6:3]
- kwargs['association'] = data[2:N * 6:3]
+ index = data[0:N * 6:3]
+ channel_type = data[1:N * 6:3]
+ association = data[2:N * 6:3]
- box = ComponentDefinitionBox(**kwargs)
+ box = ChannelDefinitionBox(index, channel_type, association, **kwargs)
return box
@@ -524,9 +591,10 @@ class ContiguousCodestreamBox(Jp2kBox):
List of segments in the codestream header.
"""
- def __init__(self, **kwargs):
+ def __init__(self, main_header=[], **kwargs):
Jp2kBox.__init__(self, id='jp2c', longname='Contiguous Codestream')
self.__dict__.update(**kwargs)
+ self.main_header = main_header
def __str__(self):
msg = Jp2kBox.__str__(self)
@@ -591,8 +659,14 @@ class FileTypeBox(Jp2kBox):
List of file conformance profiles.
"""
def __init__(self, **kwargs):
- Jp2kBox.__init__(self, id='', longname='File Type')
+ Jp2kBox.__init__(self, id='ftyp', longname='File Type')
self.__dict__.update(**kwargs)
+ if 'brand' not in kwargs.keys():
+ self.brand = 'jp2 '
+ if 'minor_version' not in kwargs.keys():
+ self.minor_version = 0
+ if 'compatibility_list' not in kwargs.keys():
+ self.compatibility_list = ['jp2 ']
def __str__(self):
lst = [Jp2kBox.__str__(self),
@@ -603,6 +677,18 @@ class FileTypeBox(Jp2kBox):
return msg
+ def _write(self, f):
+ """Write a File Type box to file.
+ """
+ length = 16 + 4*len(self.compatibility_list)
+ f.write(struct.pack('>I', length))
+ f.write('ftyp'.encode())
+ f.write(self.brand.encode())
+ f.write(struct.pack('>I', self.minor_version))
+
+ for item in self.compatibility_list:
+ f.write(item.encode())
+
@staticmethod
def _parse(f, id, offset, length):
"""Parse JPEG 2000 file type box.
@@ -673,30 +759,68 @@ class ImageHeaderBox(Jp2kBox):
Bits per component.
signed : bool
False if the image components are unsigned.
- compression : nt
+ compression : int
The compression type, should be 7 if JP2.
- cspace_unknown : int
- 0 if the color space is known and correctly specified.
- ip_provided : int
- 0 if the file does not contain intellectual propery rights information.
+ colorspace_unknown : bool
+ False if the color space is known and correctly specified.
+ ip_provided : bool
+ False if the file does not contain intellectual propery rights
+ information.
"""
- def __init__(self, **kwargs):
- Jp2kBox.__init__(self, id='', longname='Image Header')
+ def __init__(self, height, width, num_components=1, signed=False,
+ bits_per_component=8, compression=7, colorspace_unknown=False,
+ ip_provided=False, **kwargs):
+ """
+ Examples
+ --------
+ >>> import glymur
+ >>> box = glymur.jp2box.ImageHeaderBox(height=512, width=256)
+ """
+ Jp2kBox.__init__(self, id='ihdr', longname='Image Header')
+ self.height = height
+ self.width = width
+ self.num_components = num_components
+ self.signed = signed
+ self.bits_per_component = bits_per_component
+ self.compression = compression
+ self.colorspace_unknown = colorspace_unknown
+ self.ip_provided = False
self.__dict__.update(**kwargs)
def __str__(self):
- lst = [Jp2kBox.__str__(self)]
- lst.append('Size: [{0} {1} {2}]'.format(self.height, self.width,
- self.num_components))
- lst.append('Bitdepth: {0}'.format(self.bits_per_component))
- lst.append('Signed: {0}'.format(self.signed))
- if self.compression == 7:
- lst.append('Compression: wavelet')
- if self.cspace_unknown:
- lst.append('Colorspace Unknown: True')
- else:
- lst.append('Colorspace Unknown: False')
- return '\n '.join(lst)
+ msg = Jp2kBox.__str__(self)
+ msg = "{0}"
+ msg += '\n Size: [{1} {2} {3}]'
+ msg += '\n Bitdepth: {4}'
+ msg += '\n Signed: {5}'
+ msg += '\n Compression: {6}'
+ msg += '\n Colorspace Unknown: {7}'
+ msg = msg.format(Jp2kBox.__str__(self),
+ self.height, self.width, self.num_components,
+ self.bits_per_component,
+ self.signed,
+ 'wavelet' if self.compression == 7 else 'unknown',
+ self.colorspace_unknown)
+ return msg
+
+ def _write(self, f):
+ """Write an Image Header box to file.
+ """
+ f.write(struct.pack('>I', 22))
+ f.write('ihdr'.encode())
+
+ # signedness and bps are stored together in a single byte
+ bit_depth_signedness = 0x80 if self.signed else 0x00
+ bit_depth_signedness |= self.bits_per_component - 1
+ buffer = struct.pack('>IIHBBBB',
+ self.height,
+ self.width,
+ self.num_components,
+ bit_depth_signedness,
+ self.compression,
+ 1 if self.colorspace_unknown else 0,
+ 1 if self.ip_provided else 0)
+ f.write(buffer)
@staticmethod
def _parse(f, id, offset, length):
@@ -724,16 +848,16 @@ class ImageHeaderBox(Jp2kBox):
# Read the box information
buffer = f.read(14)
params = struct.unpack('>IIHBBBB', buffer)
- kwargs['height'] = params[0]
- kwargs['width'] = params[1]
+ height = params[0]
+ width = params[1]
kwargs['num_components'] = params[2]
kwargs['bits_per_component'] = (params[3] & 0x7f) + 1
kwargs['signed'] = (params[3] & 0x80) > 1
kwargs['compression'] = params[4]
- kwargs['cspace_unknown'] = params[5]
- kwargs['ip_provided'] = params[6]
+ kwargs['colorspace_unknown'] = True if params[5] else False
+ kwargs['ip_provided'] = True if params[6] else False
- box = ImageHeaderBox(**kwargs)
+ box = ImageHeaderBox(height, width, **kwargs)
return box
@@ -818,7 +942,7 @@ class JP2HeaderBox(Jp2kBox):
List of boxes contained in this superbox.
"""
def __init__(self, **kwargs):
- Jp2kBox.__init__(self, id='', longname='JP2 Header')
+ Jp2kBox.__init__(self, id='jp2h', longname='JP2 Header')
self.__dict__.update(**kwargs)
def __str__(self):
@@ -831,6 +955,21 @@ class JP2HeaderBox(Jp2kBox):
msg += ''.join(strs)
return msg
+ def _write(self, f):
+ """Write a JP2 Header box to file.
+ """
+ # Write the contained boxes, then come back and write the length.
+ orig_pos = f.tell()
+ f.write(struct.pack('>I', 0))
+ f.write('jp2h'.encode())
+ for box in self.box:
+ box._write(f)
+
+ end_pos = f.tell()
+ f.seek(orig_pos)
+ f.write(struct.pack('>I', end_pos - orig_pos))
+ f.seek(end_pos)
+
@staticmethod
def _parse(f, id, offset, length):
"""Parse JPEG 2000 header box.
@@ -882,8 +1021,10 @@ class JPEG2000SignatureBox(Jp2kBox):
Four-byte tuple identifying the file as JPEG 2000.
"""
def __init__(self, **kwargs):
- Jp2kBox.__init__(self, id='', longname='JPEG 2000 Signature')
+ Jp2kBox.__init__(self, id='jP ', longname='JPEG 2000 Signature')
self.__dict__.update(**kwargs)
+ if 'signature' not in kwargs.keys():
+ self.signature = (13, 10, 135, 10)
def __str__(self):
msg = Jp2kBox.__str__(self)
@@ -891,6 +1032,13 @@ class JPEG2000SignatureBox(Jp2kBox):
msg = msg.format(*self.signature)
return msg
+ def _write(self, f):
+ """Write a JPEG 2000 Signature box to file.
+ """
+ f.write(struct.pack('>I', 12))
+ f.write(self.id.encode())
+ f.write(struct.pack('>BBBB', *self.signature))
+
@staticmethod
def _parse(f, id, offset, length):
"""Parse JPEG 2000 signature box.
@@ -1412,19 +1560,50 @@ class XMLBox(Jp2kBox):
xml : ElementTree.Element
XML section.
"""
- def __init__(self, **kwargs):
- Jp2kBox.__init__(self, id='', longname='XML')
+ def __init__(self, xml=None, filename=None, **kwargs):
+ """
+ Parameters
+ ----------
+ xml : ElementTree
+ An ElementTree object already existing in python.
+ filename : str
+ File from which to read XML. If filename is not None, then the xml
+ keyword argument must be None.
+ """
+ Jp2kBox.__init__(self, id='xml ', longname='XML')
+ if filename is not None and xml is not None:
+ msg = "Only one of either filename or xml should be provided."
+ raise IOError(msg)
+ if filename is not None:
+ self.xml = ET.parse(filename)
+ else:
+ self.xml = xml
self.__dict__.update(**kwargs)
def __str__(self):
msg = Jp2kBox.__str__(self)
xml = self.xml
if self.xml is not None:
- msg += _pretty_print_xml(self.xml)
+ try:
+ msg += _pretty_print_xml(self.xml)
+ except TypeError:
+ msg += _pretty_print_xml(self.xml.getroot())
else:
msg += '\n {0}'.format(xml)
return msg
+ def _write(self, f):
+ """Write an XML box to file.
+ """
+ try:
+ buffer = ET.tostring(self.xml, encoding='utf-8')
+ except AttributeError:
+ buffer = ET.tostring(self.xml.getroot(), encoding='utf-8')
+
+ f.write(struct.pack('>I', len(buffer) + 8))
+ f.write(self.id.encode())
+ f.write(buffer)
+
@staticmethod
def _parse(f, id, offset, length):
"""Parse XML box.
@@ -2273,7 +2452,7 @@ class _ExifInteroperabilityIfd(_Ifd):
# Map each box ID to the corresponding class.
_box_with_id = {
'asoc': AssociationBox,
- 'cdef': ComponentDefinitionBox,
+ 'cdef': ChannelDefinitionBox,
'cmap': ComponentMappingBox,
'colr': ColourSpecificationBox,
'jP ': JPEG2000SignatureBox,
diff --git a/glymur/jp2k.py b/glymur/jp2k.py
index b09271e..96ead49 100644
--- a/glymur/jp2k.py
+++ b/glymur/jp2k.py
@@ -16,8 +16,8 @@ import warnings
import numpy as np
from .codestream import Codestream
-from .core import progression_order
-from .jp2box import Jp2kBox
+from .core import *
+from .jp2box import *
from .lib import openjp2 as opj2
_cspace_map = {'rgb': opj2._CLRSPC_SRGB,
@@ -141,7 +141,7 @@ class Jp2k(Jp2kBox):
def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None,
cbsize=None, psizes=None, grid_offset=None, sop=False,
subsam=None, tilesize=None, prog=None, modesw=None,
- colorspace=None, verbose=False):
+ colorspace=None, verbose=False, mct=None):
"""Write image data to a JP2/JPX/J2k file. Intended usage of the
various parameters follows that of OpenJPEG's opj_compress utility.
@@ -167,6 +167,9 @@ class Jp2k(Jp2kBox):
If true, write SOP marker after each header packet.
grid_offset : tuple, optional
Offset (DY, DX) of the origin of the image in the reference grid.
+ mct : bool, optional
+ Specifies usage of the multi component transform. If not
+ specified, defaults to True if the colorspace is RGB.
modesw : int, optional
Mode switch.
1 = BYPASS(LAZY)
@@ -336,6 +339,18 @@ class Jp2k(Jp2kBox):
else:
colorspace = _cspace_map[colorspace]
+ if mct is None:
+ if colorspace == opj2._CLRSPC_SRGB:
+ cparams.tcp_mct = 1
+ else:
+ cparams.tcp_mct = 0
+ else:
+ if mct and colorspace == opj2._CLRSPC_GRAY:
+ msg = "Cannot specify usage of the multi component transform "
+ msg += "if the colorspace is gray."
+ raise IOError(msg)
+ cparams.tcp_mct = 1 if mct else 0
+
if img_array.dtype == np.uint8:
comp_prec = 8
elif img_array.dtype == np.uint16:
@@ -372,12 +387,6 @@ class Jp2k(Jp2kBox):
src = layer.ctypes.data
ctypes.memmove(dest, src, layer.nbytes)
- # set multi-component transform?
- if image.contents.numcomps == 3:
- cparams.tcp_mct = 1
- else:
- cparams.tcp_mct = 0
-
codec = opj2._create_compress(codec_fmt)
if verbose:
@@ -398,6 +407,143 @@ class Jp2k(Jp2kBox):
self._parse()
+ def wrap(self, filename, boxes=None):
+ """Write the codestream back out to file, wrapped in new JP2 jacket.
+
+ Parameters
+ ----------
+ filename : str
+ JP2 file to be created from a raw codestream.
+ boxes : list
+ 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.
+
+ Returns
+ -------
+ jp2 : Jp2k object
+ Newly wrapped Jp2k object.
+
+ Examples
+ --------
+ >>> import glymur, tempfile
+ >>> jfile = glymur.data.goodstuff()
+ >>> j2k = glymur.Jp2k(jfile)
+ >>> tfile = tempfile.NamedTemporaryFile(suffix='jp2')
+ >>> jp2 = j2k.wrap(tfile.name)
+ """
+ if boxes is None:
+ # Try to create a reasonable default.
+ boxes = [JPEG2000SignatureBox(),
+ FileTypeBox(),
+ JP2HeaderBox(),
+ ContiguousCodestreamBox()]
+ c = self.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+ boxes[2].box = [ImageHeaderBox(height=height,
+ width=width,
+ num_components=num_components),
+ ColourSpecificationBox(colorspace=SRGB)]
+
+ # Check for a bad sequence of boxes.
+ # 1st two boxes must be 'jP ' and 'ftyp'
+ if boxes[0].id != 'jP ' or boxes[1].id != 'ftyp':
+ msg = "The first box must be the signature box and the second "
+ msg += "must be the file type box."
+ raise IOError(msg)
+
+ # jp2c must be preceeded by jp2h
+ jp2h_lst = [idx for (idx, box) in enumerate(boxes) if box.id == 'jp2h']
+ jp2h_idx = jp2h_lst[0]
+ jp2c_lst = [idx for (idx, box) in enumerate(boxes) if box.id == 'jp2c']
+ if len(jp2c_lst) == 0:
+ msg = "A codestream box must be defined in the outermost "
+ msg += "list of boxes."
+ raise IOError(msg)
+
+ jp2c_idx = jp2c_lst[0]
+ if jp2h_idx >= jp2c_idx:
+ msg = "The codestream box must be preceeded by a jp2 header box."
+ raise IOError(msg)
+
+ # 1st jp2 header box must be ihdr
+ jp2h = boxes[jp2h_idx]
+ if jp2h.box[0].id != 'ihdr':
+ msg = "The first box in the jp2 header box must be the image "
+ msg += "header box."
+ raise IOError(msg)
+
+ # colr must be present in jp2 header box.
+ jp2hb = jp2h.box
+ colr_lst = [j for (j, box) in enumerate(jp2h.box) if box.id == 'colr']
+ if len(colr_lst) == 0:
+ msg = "The jp2 header box must contain a color definition box."
+ raise IOError(msg)
+ colr = jp2h.box[colr_lst[0]]
+
+ # Any cdef box must be in the jp2 header following the image header.
+ cdef_lst = [j for (j, box) in enumerate(boxes) if box.id == 'cdef']
+ if len(cdef_lst) != 0:
+ msg = "Any channel defintion box must be in the JP2 header "
+ msg += "following the image header."
+ raise IOError(msg)
+
+ cdef_lst = [j for (j, box) in enumerate(jp2h.box) if box.id == 'cdef']
+ if len(cdef_lst) > 1:
+ msg = "Only one channel definition box is allowed in the "
+ msg += "JP2 header."
+ raise IOError(msg)
+ elif len(cdef_lst) == 1:
+ cdef = jp2h.box[cdef_lst[0]]
+ assn = cdef.association
+ typ = cdef.channel_type
+ index = cdef.index
+ if colr.colorspace == SRGB:
+ if any([chan + 1 not in assn or typ[chan] != 0
+ for chan in [0, 1, 2]]):
+ msg = "All color channels must be defined in the "
+ msg += "channel definition box."
+ raise IOError(msg)
+ elif colr.colorspace == GREYSCALE:
+ if 0 not in typ:
+ msg = "All color channels must be defined in the "
+ msg += "channel definition box."
+ raise IOError(msg)
+
+ with open(filename, 'wb') as ofile:
+ for box in boxes:
+ if 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.id == 'jp2c']
+ jp2c = jp2c[0]
+ ofile.write(struct.pack('>I', jp2c.length + 8))
+ 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))
+
+ ofile.flush()
+
+ jp2 = Jp2k(filename)
+ return jp2
+
def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False):
"""Read a JPEG 2000 image.
diff --git a/glymur/lib/openjp2.py b/glymur/lib/openjp2.py
index 798f23d..d7578d0 100644
--- a/glymur/lib/openjp2.py
+++ b/glymur/lib/openjp2.py
@@ -2,12 +2,99 @@
Wraps individual functions in openjp2 library.
"""
+
+def _glymurrc_fname():
+ """Return the path to the configuration file.
+
+ Search order:
+ 1) current working directory
+ 2) environ var XDG_CONFIG_HOME
+ 3) $HOME/.config/glymur/glymurrc
+ """
+
+ # Current directory.
+ fname = os.path.join(os.getcwd(), 'glymurrc')
+ if os.path.exists(fname):
+ return fname
+
+ # Either GLYMURCONFIGDIR/glymurrc or $HOME/.glymur/glymurrc
+ confdir = _get_configdir()
+ if confdir is not None:
+ fname = os.path.join(confdir, 'glymurrc')
+ if os.path.exists(fname):
+ return fname
+ else:
+ msg = "Configuration file '{0}' does not exist.".format(confdir)
+ warnings.warn(msg, UserWarning)
+
+ # didn't find a configuration file.
+ return None
+
+
+def _config():
+ """Read configuration file.
+
+ Based on matplotlib.
+ """
+ filename = _glymurrc_fname()
+ if filename is not None:
+ # Read the configuration file for the library location.
+ parser = ConfigParser()
+ parser.read(filename)
+ libopenjp2_path = parser.get('library', 'openjp2')
+ else:
+ # No help from the config file, try to find it ourselves.
+ from ctypes.util import find_library
+ libopenjp2_path = find_library('openjp2')
+
+ if libopenjp2_path is None:
+ return None
+
+ try:
+ _OPENJP2 = ctypes.CDLL(libopenjp2_path)
+ except OSError:
+ msg = '"Library {0}" could not be loaded. Operating in degraded mode.'
+ msg = msg.format(libopenjp2_path)
+ warnings.warn(msg, UserWarning)
+ _OPENJP2 = None
+ return _OPENJP2
+
+
+def _get_configdir():
+ """Return string representing the configuration directory.
+
+ Default is $HOME/.config/glymur. You can override this with the
+ XDG_CONFIG_HOME environment variable.
+ """
+
+ if 'XDG_CONFIG_HOME' in os.environ:
+ return os.path.join(os.environ['XDG_CONFIG_HOME'], 'glymur')
+
+ if 'HOME' in os.environ:
+ return os.path.join(os.environ['HOME'], '.config', 'glymur')
+
import ctypes
+import os
+import warnings
+
+import sys
+if sys.hexversion <= 0x03000000:
+ from ConfigParser import SafeConfigParser as ConfigParser
+ from ConfigParser import NoOptionError
+else:
+ from configparser import ConfigParser
+ from configparser import NoOptionError
+
+_OPENJP2 = _config()
import numpy as np
-from .. import _OPENJP2
-from ..core import LRCP, RLCP, RPCL, PCRL, CPRL
+# Progression order
+LRCP = 0
+RLCP = 1
+RPCL = 2
+PCRL = 3
+CPRL = 4
_ERROR_MSG_LST = []
diff --git a/glymur/test/__init__.py b/glymur/test/__init__.py
index 1e19d88..aec2d72 100644
--- a/glymur/test/__init__.py
+++ b/glymur/test/__init__.py
@@ -6,3 +6,4 @@ from .test_printing import TestPrinting as printing
from .test_opj_suite import TestSuite as suite
from .test_opj_suite_write import TestSuiteWrite as suitew
from .test_opj_suite_neg import TestSuiteNegative as suiteneg
+from .test_jp2box import TestJp2Boxes as box
diff --git a/glymur/test/test_config.py b/glymur/test/test_config.py
new file mode 100644
index 0000000..1abf02f
--- /dev/null
+++ b/glymur/test/test_config.py
@@ -0,0 +1,101 @@
+import imp
+import os
+import sys
+import tempfile
+import unittest
+if sys.hexversion <= 0x03030000:
+ from mock import patch
+else:
+ from unittest.mock import patch
+import warnings
+
+import pkg_resources
+
+import glymur
+from glymur import Jp2k
+from glymur.lib import openjp2 as opj2
+
+
+@unittest.skipIf(sys.hexversion < 0x03020000,
+ "Uses features introduced in 3.2.")
+class TestJp2k(unittest.TestCase):
+ def setUp(self):
+ self.jp2file = pkg_resources.resource_filename(glymur.__name__,
+ "data/nemo.jp2")
+ imp.reload(glymur)
+ imp.reload(glymur.lib.openjp2)
+
+ def tearDown(self):
+ imp.reload(glymur)
+ imp.reload(glymur.lib.openjp2)
+
+ def test_config_file_via_environ(self):
+ """Verify that we can read a configuration file set via environ var."""
+ with tempfile.TemporaryDirectory() as tdir:
+ configdir = os.path.join(tdir, 'glymur')
+ os.mkdir(configdir)
+ filename = os.path.join(configdir, 'glymurrc')
+ with open(filename, 'wb') as tfile:
+ tfile.write('[library]\n'.encode())
+ libloc = glymur.lib.openjp2._OPENJP2._name
+ line = 'openjp2: {0}\n'.format(libloc)
+ tfile.write(line.encode())
+ tfile.flush()
+ with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}):
+ imp.reload(glymur.lib.openjp2)
+ j = Jp2k(self.jp2file)
+
+ def test_config_file_via_environ_is_wrong(self):
+ # A non-existant library location should be rejected.
+ with tempfile.TemporaryDirectory() as tdir:
+ configdir = os.path.join(tdir, 'glymur')
+ os.mkdir(configdir)
+ fname = os.path.join(configdir, 'glymurrc')
+ with open(fname, 'w') as fp:
+ with tempfile.NamedTemporaryFile(suffix='.dylib') as tfile:
+ fp.write('[library]\n')
+ fp.write('openjp2: {0}.not.there\n'.format(tfile.name))
+ fp.flush()
+ with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}):
+ # Misconfigured new configuration file should
+ # be rejected.
+ with self.assertWarns(UserWarning) as cw:
+ imp.reload(glymur.lib.openjp2)
+
+ def test_missing_config_file_via_environ(self):
+ # Verify that we error out properly if the configuration file
+ # specified via environment variable is not found.
+ with tempfile.TemporaryDirectory() as tdir:
+ with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}):
+ # Misconfigured new configuration file should
+ # be rejected.
+ with self.assertWarns(UserWarning) as cw:
+ imp.reload(glymur.lib.openjp2)
+
+ def test_home_dir_missing_config_dir(self):
+ # Verify no exception is raised if $HOME is missing .config directory.
+ with tempfile.TemporaryDirectory() as tdir:
+ with patch.dict('os.environ', {'HOME': tdir}):
+ # Misconfigured new configuration file should
+ # be rejected.
+ with self.assertWarns(UserWarning) as cw:
+ imp.reload(glymur.lib.openjp2)
+
+ def test_home_dir_missing_glymur_rc_dir(self):
+ # Should warn but not error if $HOME/.config but no glymurrc dir.
+ with tempfile.TemporaryDirectory() as tdir:
+ # We need the subdirectory to be specifically named as ".config"
+ # in order for this test to work. A specifically-named temporary
+ # directory does not seem to be possible, so try to symlink it.
+ # Supposedly the symlink gets cleaned up with tdir gets cleaned up.
+ with tempfile.TemporaryDirectory(suffix=".config", dir=tdir) \
+ as tdir_config:
+ os.symlink(tdir_config, os.path.join(tdir, '.config'))
+ with patch.dict('os.environ', {'HOME': tdir}):
+ # Misconfigured new configuration file should
+ # be rejected.
+ with self.assertWarns(UserWarning) as cw:
+ imp.reload(glymur.lib.openjp2)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py
new file mode 100644
index 0000000..e0500f5
--- /dev/null
+++ b/glymur/test/test_jp2box.py
@@ -0,0 +1,604 @@
+import doctest
+import tempfile
+import xml.etree.cElementTree as ET
+import unittest
+
+import numpy as np
+import pkg_resources
+
+import glymur
+from glymur import Jp2k
+from glymur.jp2box import *
+
+
+# Doc tests should be run as well.
+def load_tests(loader, tests, ignore):
+ tests.addTests(doctest.DocTestSuite('glymur.jp2box'))
+ return tests
+
+
+class TestChannelDefinition(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ """Need a one_plane plane image for greyscale testing."""
+ j2k = Jp2k(glymur.data.goodstuff())
+ data = j2k.read()
+ # Write the first component back out to file.
+ with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile:
+ grey_j2k = Jp2k(tfile.name, 'wb')
+ grey_j2k.write(data[:, :, 0])
+ cls.one_plane = tfile.name
+ # Write the first two components back out to file.
+ with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile:
+ grey_j2k = Jp2k(tfile.name, 'wb')
+ grey_j2k.write(data[:, :, 0:1])
+ cls.two_planes = tfile.name
+ # Write four components back out to file.
+ with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile:
+ rgba_jp2 = Jp2k(tfile.name, 'wb')
+ shape = (data.shape[0], data.shape[1], 1)
+ alpha = np.zeros((shape), dtype=data.dtype)
+ data4 = np.concatenate((data, alpha), axis=2)
+ rgba_jp2.write(data4)
+ cls.four_planes = tfile.name
+
+ @classmethod
+ def tearDownClass(cls):
+ os.unlink(cls.one_plane)
+ os.unlink(cls.two_planes)
+ os.unlink(cls.four_planes)
+
+ def setUp(self):
+ self.jp2file = glymur.data.nemo()
+ self.j2kfile = glymur.data.goodstuff()
+
+ j2k = Jp2k(self.j2kfile)
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+
+ self.jP = JPEG2000SignatureBox()
+ self.ftyp = FileTypeBox()
+ self.jp2h = JP2HeaderBox()
+ self.jp2c = ContiguousCodestreamBox()
+ self.ihdr = ImageHeaderBox(height=height, width=width,
+ num_components=num_components)
+ self.colr_rgb = ColourSpecificationBox(colorspace=glymur.core.SRGB)
+ self.colr_gr = ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)
+
+ def tearDown(self):
+ pass
+
+ def test_rgb(self):
+ """Just regular RGB."""
+ j2k = Jp2k(self.j2kfile)
+ cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
+ channel_type=[0, 0, 0],
+ association=[1, 2, 3])
+ boxes = [self.ihdr, self.colr_rgb, cdef]
+ self.jp2h.box = boxes
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ jp2 = Jp2k(tfile.name)
+ jp2h = jp2.box[2]
+ boxes = [box.id for box in jp2h.box]
+ self.assertEqual(boxes, ['ihdr', 'colr', 'cdef'])
+ self.assertEqual(jp2h.box[2].index, (0, 1, 2))
+ self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0))
+ self.assertEqual(jp2h.box[2].association, (1, 2, 3))
+
+ def test_rgba(self):
+ """Just regular RGBA."""
+ j2k = Jp2k(self.four_planes)
+ cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2, 3],
+ channel_type=[0, 0, 0, 1],
+ association=[1, 2, 3, 0])
+ boxes = [self.ihdr, self.colr_rgb, cdef]
+ self.jp2h.box = boxes
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ jp2 = Jp2k(tfile.name)
+ jp2h = jp2.box[2]
+ boxes = [box.id for box in jp2h.box]
+ self.assertEqual(boxes, ['ihdr', 'colr', 'cdef'])
+ self.assertEqual(jp2h.box[2].index, (0, 1, 2, 3))
+ self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0, 1))
+ self.assertEqual(jp2h.box[2].association, (1, 2, 3, 0))
+
+ def test_bad_rgba(self):
+ """R, G, and B must be specified."""
+ j2k = Jp2k(self.four_planes)
+ cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2, 3],
+ channel_type=[0, 0, 1, 1],
+ association=[1, 2, 3, 0])
+ boxes = [self.ihdr, self.colr_rgb, cdef]
+ self.jp2h.box = boxes
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError) as ce:
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ def test_grey(self):
+ """Just regular greyscale."""
+ j2k = Jp2k(self.one_plane)
+ cdef = glymur.jp2box.ChannelDefinitionBox(index=[0],
+ channel_type=[0],
+ association=[1])
+ boxes = [self.ihdr, self.colr_gr, cdef]
+ self.jp2h.box = boxes
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ jp2 = Jp2k(tfile.name)
+ jp2h = jp2.box[2]
+ boxes = [box.id for box in jp2h.box]
+ self.assertEqual(boxes, ['ihdr', 'colr', 'cdef'])
+ self.assertEqual(jp2h.box[2].index, (0,))
+ self.assertEqual(jp2h.box[2].channel_type, (0,))
+ self.assertEqual(jp2h.box[2].association, (1,))
+
+ def test_grey_alpha(self):
+ """Just regular greyscale plus alpha."""
+ j2k = Jp2k(self.two_planes)
+ cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1],
+ channel_type=[0, 1],
+ association=[1, 0])
+ boxes = [self.ihdr, self.colr_gr, cdef]
+ self.jp2h.box = boxes
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ jp2 = Jp2k(tfile.name)
+ jp2h = jp2.box[2]
+ boxes = [box.id for box in jp2h.box]
+ self.assertEqual(boxes, ['ihdr', 'colr', 'cdef'])
+ self.assertEqual(jp2h.box[2].index, (0, 1))
+ self.assertEqual(jp2h.box[2].channel_type, (0, 1))
+ self.assertEqual(jp2h.box[2].association, (1, 0))
+
+ def test_bad_grey_alpha(self):
+ """A greyscale image with alpha layer must specify Y"""
+ j2k = Jp2k(self.two_planes)
+
+ # This cdef box
+ cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1],
+ channel_type=[1, 1],
+ association=[0, 1])
+ boxes = [self.ihdr, self.colr_gr, cdef]
+ self.jp2h.box = boxes
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises((OSError, IOError)) as ce:
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ def test_only_one_cdef_in_jp2_header(self):
+ """There can only be one channel definition box in the jp2 header."""
+ j2k = Jp2k(self.j2kfile)
+
+ cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
+ channel_type=[0, 0, 0],
+ association=[1, 2, 3])
+
+ boxes = [self.ihdr, cdef, self.colr_rgb, cdef]
+ self.jp2h.box = boxes
+
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ def test_not_in_jp2_header(self):
+ j2k = Jp2k(self.j2kfile)
+ boxes = [self.ihdr, self.colr_rgb]
+ self.jp2h.box = boxes
+
+ cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
+ channel_type=[0, 0, 0],
+ association=[1, 2, 3])
+
+ boxes = [self.jP, self.ftyp, self.jp2h, cdef, self.jp2c]
+
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ 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
+ # same length.
+ with self.assertRaises(IOError):
+ box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
+ channel_type=[0, 0, 3],
+ association=[1, 2, 3])
+
+ def test_wrong_lengths(self):
+ # Should reject if not all of index, channel_type, association the
+ # same length.
+ with self.assertRaises(IOError):
+ box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
+ channel_type=[0, 0],
+ association=[1, 2, 3])
+
+
+class TestXML(unittest.TestCase):
+
+ def setUp(self):
+ self.jp2file = glymur.data.nemo()
+ self.j2kfile = glymur.data.goodstuff()
+
+ raw_xml = b"""
+
+
+ 1
+ 2008
+ 141100
+
+
+
+
+ 4
+ 2011
+ 59900
+
+
+
+ 68
+ 2011
+ 13600
+
+
+
+ """
+ with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile:
+ tfile.write(raw_xml)
+ tfile.flush()
+ self.xmlfile = tfile.name
+
+ j2k = Jp2k(self.j2kfile)
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+
+ self.jP = JPEG2000SignatureBox()
+ self.ftyp = FileTypeBox()
+ self.jp2h = JP2HeaderBox()
+ self.jp2c = ContiguousCodestreamBox()
+ self.ihdr = ImageHeaderBox(height=height, width=width,
+ num_components=num_components)
+ self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
+
+ def tearDown(self):
+ os.unlink(self.xmlfile)
+ pass
+
+ def test_negative_both_file_and_xml_provided(self):
+ """The XML should come from only one source."""
+ j2k = Jp2k(self.j2kfile)
+ xml_object = ET.parse(self.xmlfile)
+ with self.assertRaises((IOError, OSError)) as ce:
+ xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile, xml=xml_object)
+
+ def test_basic_xml(self):
+ # Should be able to write an XMLBox.
+ j2k = Jp2k(self.j2kfile)
+
+ self.jp2h.box = [self.ihdr, self.colr]
+
+ the_xml = ET.fromstring('0')
+ xmlb = glymur.jp2box.XMLBox(xml=the_xml)
+ self.assertEqual(ET.tostring(xmlb.xml),
+ b'0')
+
+ boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
+
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+ jp2 = Jp2k(tfile.name)
+ self.assertEqual(jp2.box[3].id, 'xml ')
+ self.assertEqual(ET.tostring(jp2.box[3].xml),
+ b'0')
+
+ def test_xml_from_file(self):
+ j2k = Jp2k(self.j2kfile)
+
+ self.jp2h.box = [self.ihdr, self.colr]
+
+ xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile)
+ boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+ jp2 = Jp2k(tfile.name)
+
+ output_boxes = [box.id for box in jp2.box]
+ self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ',
+ 'jp2c'])
+
+ elts = jp2.box[3].xml.findall('country')
+ self.assertEqual(len(elts), 3)
+
+ neighbor = elts[1].find('neighbor')
+ self.assertEqual(neighbor.attrib['name'], 'Malaysia')
+ self.assertEqual(neighbor.attrib['direction'], 'N')
+
+
+class TestColourSpecificationBox(unittest.TestCase):
+
+ def setUp(self):
+ self.j2kfile = glymur.data.goodstuff()
+
+ j2k = Jp2k(self.j2kfile)
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+
+ self.jP = JPEG2000SignatureBox()
+ self.ftyp = FileTypeBox()
+ self.jp2h = JP2HeaderBox()
+ self.jp2c = ContiguousCodestreamBox()
+ self.ihdr = ImageHeaderBox(height=height, width=width,
+ num_components=num_components)
+
+ def tearDown(self):
+ pass
+
+ def test_color_specification_box_with_out_enumerated_colorspace(self):
+ j2k = Jp2k(self.j2kfile)
+
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+ boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(NotImplementedError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ def test_missing_colr_box(self):
+ j2k = Jp2k(self.j2kfile)
+ boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
+ boxes[2].box = [self.ihdr]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ def test_default_ColourSpecificationBox(self):
+ b = glymur.jp2box.ColourSpecificationBox(colorspace=glymur.core.SRGB)
+ self.assertEqual(b.method, glymur.core.ENUMERATED_COLORSPACE)
+ self.assertEqual(b.precedence, 0)
+ self.assertEqual(b.approximation, 0)
+ self.assertEqual(b.colorspace, glymur.core.SRGB)
+ self.assertIsNone(b.icc_profile)
+
+ def test_ColourSpecificationBox_with_colorspace_and_icc(self):
+ # Colour specification boxes can't have both.
+ with self.assertRaises((OSError, IOError)):
+ colorspace = glymur.core.SRGB
+ icc_profile = b'\x01\x02\x03\x04'
+ b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
+ icc_profile=icc_profile)
+
+ def test_ColourSpecificationBox_with_bad_method(self):
+ colorspace = glymur.core.SRGB
+ method = -1
+ with self.assertRaises(IOError):
+ b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
+ method=method)
+
+ def test_ColourSpecificationBox_with_bad_approximation(self):
+ colorspace = glymur.core.SRGB
+ approx = -1
+ with self.assertRaises(IOError):
+ b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
+ approximation=approx)
+
+
+@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None,
+ "Missing openjp2 library.")
+class TestJp2Boxes(unittest.TestCase):
+
+ def setUp(self):
+ self.j2kfile = glymur.data.goodstuff()
+
+ def tearDown(self):
+ pass
+
+ def test_default_JPEG2000SignatureBox(self):
+ # Should be able to instantiate a JPEG2000SignatureBox
+ b = glymur.jp2box.JPEG2000SignatureBox()
+ self.assertEqual(b.signature, (13, 10, 135, 10))
+
+ def test_default_FileTypeBox(self):
+ # Should be able to instantiate a FileTypeBox
+ b = glymur.jp2box.FileTypeBox()
+ self.assertEqual(b.brand, 'jp2 ')
+ self.assertEqual(b.minor_version, 0)
+ self.assertEqual(b.compatibility_list, ['jp2 '])
+
+ def test_default_ImageHeaderBox(self):
+ # Should be able to instantiate an image header box.
+ b = glymur.jp2box.ImageHeaderBox(height=512, width=256,
+ num_components=3)
+ self.assertEqual(b.height, 512)
+ self.assertEqual(b.width, 256)
+ self.assertEqual(b.num_components, 3)
+ self.assertEqual(b.bits_per_component, 8)
+ self.assertFalse(b.signed)
+ self.assertFalse(b.colorspace_unknown)
+
+ def test_default_JP2HeaderBox(self):
+ b1 = JP2HeaderBox()
+ b1.box = [ImageHeaderBox(height=512, width=256),
+ ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
+
+ def test_default_ContiguousCodestreamBox(self):
+ b = ContiguousCodestreamBox()
+ self.assertEqual(b.id, 'jp2c')
+ self.assertEqual(b.main_header, [])
+
+ def verify_wrapped_raw(self, jp2file):
+ # Shared method by at least two tests.
+ jp2 = Jp2k(jp2file)
+ self.assertEqual(len(jp2.box), 4)
+
+ self.assertEqual(jp2.box[0].id, 'jP ')
+ self.assertEqual(jp2.box[0].offset, 0)
+ self.assertEqual(jp2.box[0].length, 12)
+ self.assertEqual(jp2.box[0].longname, 'JPEG 2000 Signature')
+
+ self.assertEqual(jp2.box[1].id, 'ftyp')
+ self.assertEqual(jp2.box[1].offset, 12)
+ self.assertEqual(jp2.box[1].length, 20)
+ self.assertEqual(jp2.box[1].longname, 'File Type')
+
+ self.assertEqual(jp2.box[2].id, 'jp2h')
+ self.assertEqual(jp2.box[2].offset, 32)
+ self.assertEqual(jp2.box[2].length, 45)
+ self.assertEqual(jp2.box[2].longname, 'JP2 Header')
+
+ self.assertEqual(jp2.box[3].id, 'jp2c')
+ self.assertEqual(jp2.box[3].offset, 77)
+ self.assertEqual(jp2.box[3].length, 115228)
+
+ # jp2h super box
+ self.assertEqual(len(jp2.box[2].box), 2)
+
+ self.assertEqual(jp2.box[2].box[0].id, 'ihdr')
+ self.assertEqual(jp2.box[2].box[0].offset, 40)
+ self.assertEqual(jp2.box[2].box[0].length, 22)
+ self.assertEqual(jp2.box[2].box[0].longname, 'Image Header')
+ self.assertEqual(jp2.box[2].box[0].height, 800)
+ 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, 8)
+ self.assertEqual(jp2.box[2].box[0].signed, False)
+ self.assertEqual(jp2.box[2].box[0].compression, 7)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].ip_provided, False)
+
+ self.assertEqual(jp2.box[2].box[1].id, 'colr')
+ self.assertEqual(jp2.box[2].box[1].offset, 62)
+ self.assertEqual(jp2.box[2].box[1].length, 15)
+ self.assertEqual(jp2.box[2].box[1].longname, 'Colour Specification')
+ self.assertEqual(jp2.box[2].box[1].precedence, 0)
+ self.assertEqual(jp2.box[2].box[1].approximation, 0)
+ self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB)
+ self.assertIsNone(jp2.box[2].box[1].icc_profile)
+
+ def test_wrap(self):
+ j2k = Jp2k(self.j2kfile)
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name)
+ self.verify_wrapped_raw(tfile.name)
+
+ def test_wrap_jp2(self):
+ j2k = Jp2k(self.j2kfile)
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ jp2 = j2k.wrap(tfile.name)
+ boxes = [box.id for box in jp2.box]
+ self.assertEqual(boxes, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
+
+ def test_default_layout_but_with_specified_boxes(self):
+ j2k = Jp2k(self.j2kfile)
+ boxes = [JPEG2000SignatureBox(),
+ FileTypeBox(),
+ JP2HeaderBox(),
+ ContiguousCodestreamBox()]
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+ boxes[2].box = [ImageHeaderBox(height=height,
+ width=width,
+ num_components=num_components),
+ ColourSpecificationBox(colorspace=glymur.core.SRGB)]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ j2k.wrap(tfile.name, boxes=boxes)
+ self.verify_wrapped_raw(tfile.name)
+
+ def test_image_header_box_not_first_in_jp2_header(self):
+ # The specification says that ihdr must be the first box in jp2h.
+ j2k = Jp2k(self.j2kfile)
+ boxes = [JPEG2000SignatureBox(),
+ FileTypeBox(),
+ JP2HeaderBox(),
+ ContiguousCodestreamBox()]
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+ boxes[2].box = [ColourSpecificationBox(colorspace=glymur.core.SRGB),
+ ImageHeaderBox(height=height,
+ width=width,
+ num_components=num_components)]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ def test_first_2_boxes_not_jP_and_ftyp(self):
+ j2k = Jp2k(self.j2kfile)
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+
+ jP = JPEG2000SignatureBox()
+ ftyp = FileTypeBox()
+ jp2h = JP2HeaderBox()
+ jp2c = ContiguousCodestreamBox()
+ colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
+ ihdr = ImageHeaderBox(height=height, width=width,
+ num_components=num_components)
+ jp2h.box = [ihdr, colr]
+ boxes = [ftyp, jP, jp2h, jp2c]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ def test_jp2h_not_preceeding_jp2c(self):
+ j2k = Jp2k(self.j2kfile)
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+
+ jP = JPEG2000SignatureBox()
+ ftyp = FileTypeBox()
+ jp2h = JP2HeaderBox()
+ jp2c = ContiguousCodestreamBox()
+ colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
+ ihdr = ImageHeaderBox(height=height, width=width,
+ num_components=num_components)
+ jp2h.box = [ihdr, colr]
+ boxes = [jP, ftyp, jp2c, jp2h]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
+ def test_missing_codestream(self):
+ j2k = Jp2k(self.j2kfile)
+ c = j2k.get_codestream()
+ height = c.segment[1].Ysiz
+ width = c.segment[1].Xsiz
+ num_components = len(c.segment[1].XRsiz)
+
+ jP = JPEG2000SignatureBox()
+ ftyp = FileTypeBox()
+ jp2h = JP2HeaderBox()
+ ihdr = ImageHeaderBox(height=height, width=width,
+ num_components=num_components)
+ jp2h.box = [ihdr]
+ boxes = [jP, ftyp, jp2h]
+ with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
+ with self.assertRaises(IOError):
+ j2k.wrap(tfile.name, boxes=boxes)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py
index 3c25cca..d9549c5 100644
--- a/glymur/test/test_jp2k.py
+++ b/glymur/test/test_jp2k.py
@@ -15,14 +15,13 @@ if sys.hexversion <= 0x03030000:
else:
from unittest.mock import patch
import warnings
-from xml.etree import cElementTree as ET
import numpy as np
import pkg_resources
+import glymur
from glymur import Jp2k
from glymur.lib import openjp2 as opj2
-import glymur
try:
data_root = os.environ['OPJ_DATA_ROOT']
@@ -77,6 +76,8 @@ class TestJp2k(unittest.TestCase):
def setUp(self):
self.jp2file = pkg_resources.resource_filename(glymur.__name__,
"data/nemo.jp2")
+ self.j2kfile = pkg_resources.resource_filename(glymur.__name__,
+ "data/goodstuff.j2k")
def tearDown(self):
pass
@@ -153,6 +154,27 @@ class TestJp2k(unittest.TestCase):
subsetdata = j.read(area=(0, 0, 512, 512))
np.testing.assert_array_equal(tiledata, subsetdata)
+ def test_write_srgb_without_mct(self):
+ j2k = Jp2k(self.j2kfile)
+ expdata = j2k.read()
+ with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
+ ofile = Jp2k(tfile.name, 'wb')
+ ofile.write(expdata, mct=False)
+ actdata = ofile.read()
+ np.testing.assert_array_equal(actdata, expdata)
+
+ c = ofile.get_codestream()
+ self.assertEqual(c.segment[2].SPcod[3], 0) # no mct
+
+ def test_write_grayscale_with_mct(self):
+ # MCT usage makes no sense for grayscale images.
+ j2k = Jp2k(self.j2kfile)
+ expdata = j2k.read()
+ with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
+ ofile = Jp2k(tfile.name, 'wb')
+ with self.assertRaises(IOError):
+ ofile.write(expdata[:, :, 0], mct=True)
+
def test_write_cprl(self):
# Issue 17
j = Jp2k(self.jp2file)
@@ -213,7 +235,7 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(jp2k.box[2].box[0].bits_per_component, 8)
self.assertEqual(jp2k.box[2].box[0].signed, False)
self.assertEqual(jp2k.box[2].box[0].compression, 7)
- self.assertEqual(jp2k.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2k.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2k.box[2].box[0].ip_provided, False)
self.assertEqual(jp2k.box[2].box[1].id, 'colr')
@@ -594,83 +616,6 @@ class TestJp2k(unittest.TestCase):
with self.assertRaisesRegex(IOError, regexp) as ce:
d = j.read(reduce=3)
- @unittest.skipIf(sys.hexversion < 0x03020000,
- "Uses features introduced in 3.2.")
- def test_config_file_via_environ(self):
- """Verify that we can read a configuration file set via environ var."""
- with tempfile.TemporaryDirectory() as tdir:
- configdir = os.path.join(tdir, 'glymur')
- os.mkdir(configdir)
- filename = os.path.join(configdir, 'glymurrc')
- with open(filename, 'wb') as tfile:
- tfile.write('[library]\n'.encode())
- line = 'openjp2: {0}\n'.format(glymur._OPENJP2._name)
- tfile.write(line.encode())
- tfile.flush()
- with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}):
- imp.reload(glymur)
- j = Jp2k(self.jp2file)
-
- @unittest.skipIf(sys.hexversion < 0x03020000,
- "Uses features introduced in 3.2.")
- def test_config_file_via_environ_is_wrong(self):
- # A non-existant library location should be rejected.
- with tempfile.TemporaryDirectory() as tdir:
- configdir = os.path.join(tdir, 'glymur')
- os.mkdir(configdir)
- fname = os.path.join(configdir, 'glymurrc')
- with open(fname, 'w') as fp:
- with tempfile.NamedTemporaryFile(suffix='.dylib') as tfile:
- fp.write('[library]\n')
- fp.write('openjp2: {0}.not.there\n'.format(tfile.name))
- fp.flush()
- with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}):
- # Misconfigured new configuration file should
- # be rejected.
- with self.assertWarns(UserWarning) as cw:
- imp.reload(glymur)
-
- @unittest.skipIf(sys.hexversion < 0x03020000,
- "Uses features introduced in 3.2.")
- def test_missing_config_file_via_environ(self):
- # Verify that we error out properly if the configuration file
- # specified via environment variable is not found.
- with tempfile.TemporaryDirectory() as tdir:
- with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}):
- # Misconfigured new configuration file should
- # be rejected.
- with self.assertWarns(UserWarning) as cw:
- imp.reload(glymur)
-
- @unittest.skipIf(sys.hexversion < 0x03020000,
- "Uses features introduced in 3.2.")
- def test_home_dir_missing_config_dir(self):
- # Verify no exception is raised if $HOME is missing .config directory.
- with tempfile.TemporaryDirectory() as tdir:
- with patch.dict('os.environ', {'HOME': tdir}):
- # Misconfigured new configuration file should
- # be rejected.
- with self.assertWarns(UserWarning) as cw:
- imp.reload(glymur)
-
- @unittest.skipIf(sys.hexversion < 0x03020000,
- "Uses features introduced in 3.2.")
- def test_home_dir_missing_glymur_rc_dir(self):
- # Should warn but not error if $HOME/.config but no glymurrc dir.
- with tempfile.TemporaryDirectory() as tdir:
- # We need the subdirectory to be specifically named as ".config"
- # in order for this test to work. A specifically-named temporary
- # directory does not seem to be possible, so try to symlink it.
- # Supposedly the symlink gets cleaned up with tdir gets cleaned up.
- with tempfile.TemporaryDirectory(suffix=".config", dir=tdir) \
- as tdir_config:
- os.symlink(tdir_config, os.path.join(tdir, '.config'))
- with patch.dict('os.environ', {'HOME': tdir}):
- # Misconfigured new configuration file should
- # be rejected.
- with self.assertWarns(UserWarning) as cw:
- imp.reload(glymur)
-
def test_xmp_attribute(self):
# Verify that we can read the XMP packet in our shipping example file.
j = Jp2k(self.jp2file)
diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py
index 9a7efde..4e47971 100644
--- a/glymur/test/test_opj_suite.py
+++ b/glymur/test/test_opj_suite.py
@@ -4116,7 +4116,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 8)
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].cspace_unknown, False)
+ self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@@ -4158,7 +4158,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -4170,8 +4170,8 @@ class TestSuiteDump(unittest.TestCase):
# Jp2 Header
# Channel Definition
- self.assertEqual(jp2.box[2].box[2].component_number, (0, 1, 2))
- self.assertEqual(jp2.box[2].box[2].component_type, (0, 0, 0)) # color
+ self.assertEqual(jp2.box[2].box[2].index, (0, 1, 2))
+ self.assertEqual(jp2.box[2].box[2].channel_type, (0, 0, 0)) # color
self.assertEqual(jp2.box[2].box[2].association, (3, 2, 1)) # reverse
def test_NR_file3_dump(self):
@@ -4204,7 +4204,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -4250,7 +4250,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -4292,7 +4292,7 @@ class TestSuiteDump(unittest.TestCase):
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].cspace_unknown, False)
+ self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@@ -4338,7 +4338,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[2].box[0].bits_per_component, 12)
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].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -4387,7 +4387,7 @@ class TestSuiteDump(unittest.TestCase):
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].cspace_unknown, False)
+ self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@@ -4436,7 +4436,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -4488,7 +4488,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Palette box.
@@ -5985,7 +5985,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -6118,7 +6118,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -6246,7 +6246,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -6386,7 +6386,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 8)
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].cspace_unknown, True)
+ self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@@ -6497,7 +6497,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, True)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -6597,7 +6597,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 8)
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].cspace_unknown, True)
+ self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@@ -6694,7 +6694,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, True)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -6706,8 +6706,8 @@ class TestSuiteDump(unittest.TestCase):
# Jp2 Header
# Channel Definition
- self.assertEqual(jp2.box[2].box[2].component_number, (0, 1))
- self.assertEqual(jp2.box[2].box[2].component_type, (0, 1)) # opacity
+ self.assertEqual(jp2.box[2].box[2].index, (0, 1))
+ self.assertEqual(jp2.box[2].box[2].channel_type, (0, 1)) # opacity
self.assertEqual(jp2.box[2].box[2].association, (0, 0)) # both main
c = jp2.box[3].main_header
@@ -6813,7 +6813,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 1)
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].cspace_unknown, True)
+ self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@@ -6927,7 +6927,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 4)
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].cspace_unknown, True)
+ self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@@ -7035,7 +7035,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -7142,7 +7142,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -7241,7 +7241,7 @@ class TestSuiteDump(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@@ -7343,7 +7343,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 8)
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].cspace_unknown, True)
+ self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
diff --git a/glymur/test/test_opj_suite_neg.py b/glymur/test/test_opj_suite_neg.py
index 03cb437..5754e16 100644
--- a/glymur/test/test_opj_suite_neg.py
+++ b/glymur/test/test_opj_suite_neg.py
@@ -11,7 +11,7 @@ import warnings
import numpy as np
import pkg_resources
-from ..lib import openjp2 as opj2
+from glymur.lib import openjp2 as opj2
# Need some combination of matplotlib, PIL, or scikits-image for reading
# other image formats.
diff --git a/glymur/test/test_opj_suite_write.py b/glymur/test/test_opj_suite_write.py
index 0526fc3..50e0e48 100644
--- a/glymur/test/test_opj_suite_write.py
+++ b/glymur/test/test_opj_suite_write.py
@@ -11,7 +11,7 @@ import warnings
import numpy as np
-from ..lib import openjp2 as opj2
+from glymur.lib import openjp2 as opj2
# Need some combination of matplotlib, PIL, or scikits-image for reading
# other image formats.
@@ -686,7 +686,7 @@ class TestSuiteWrite(unittest.TestCase):
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
- self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
+ self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py
index f948c3e..4090a2d 100644
--- a/glymur/test/test_printing.py
+++ b/glymur/test/test_printing.py
@@ -30,8 +30,7 @@ class TestPrintingNeedsLib(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Setup a plain JP2 file without the two UUID boxes.
- jp2file = pkg_resources.resource_filename(glymur.__name__,
- "data/nemo.jp2")
+ jp2file = glymur.data.nemo()
with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile:
cls._plain_nemo_file = tfile.name
ijfile = Jp2k(jp2file)
@@ -676,15 +675,15 @@ class TestPrinting(unittest.TestCase):
@unittest.skipIf(data_root is None,
"OPJ_DATA_ROOT environment variable not set")
- def test_component_definition(self):
+ def test_channel_definition(self):
filename = os.path.join(data_root, 'input/conformance/file2.jp2')
j = glymur.Jp2k(filename)
print(j.box[2].box[2])
actual = sys.stdout.getvalue().strip()
- lines = ['Component Definition Box (cdef) @ (81, 28)',
- ' Component 0 (color) ==> (3)',
- ' Component 1 (color) ==> (2)',
- ' Component 2 (color) ==> (1)']
+ 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)
diff --git a/setup.py b/setup.py
index 0566e21..0e213a3 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
import sys
kwargs = {'name': 'Glymur',
- 'version': '0.1.9',
+ 'version': '0.1.10',
'description': 'Tools for accessing JPEG2000 files',
'long_description': open('README.md').read(),
'author': 'John Evans',
@@ -10,7 +10,7 @@ kwargs = {'name': 'Glymur',
'url': 'https://github.com/quintusdias/glymur',
'packages': ['glymur', 'glymur.data', 'glymur.test', 'glymur.lib',
'glymur.lib.test'],
- 'package_data': {'glymur': ['data/*.jp2']},
+ 'package_data': {'glymur': ['data/*.jp2', 'data/*.j2k']},
'scripts': ['bin/jp2dump'],
'license': 'LICENSE.txt',
'platforms': ['darwin']}