diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9434d04 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.3" + +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq python-numpy + - wget http://openjpeg.googlecode.com/files/openjpeg-1.5.0-Linux-x86_64.tar.gz + - sudo tar -xvf openjpeg-1.5.0-Linux-x86_64.tar.gz --strip-components=1 -C / + +# command to install dependencies +install: + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --use-mirrors contextlib2 mock ordereddict unittest2; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --use-mirrors contextlib2 mock; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install --use-mirrors numpy; fi + +# command to run tests +script: + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then unit2 discover; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then python -m unittest discover; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then python -m unittest discover; fi + +notifications: + email: "john.g.evans.ne@gmail.com" diff --git a/CHANGES.txt b/CHANGES.txt index 42b5b4b..40846e4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,32 @@ +Oct 13, 2013 - v0.5.6 Fixed handling of non-ascii chars in XML boxes. Fixed + some docstring errors in jp2box module. + +Oct 03, 2013 - v0.5.5 Fixed pip install error introduced in 0.5.0. + +Sep 24, 2013 - v0.5.4 Fixed test error restricted to v2.0. + +Sep 24, 2013 - v0.5.3 Removed a duplicated channel definition test in + test_jp2box that could cause a segfault in 1.3 if not properly skipped. + +Sep 23, 2013 - v0.5.2 Fixed some tests that have been failing since 0.5. + under various edge cases. + +Sep 19, 2013 - v0.5.1 Added more resiliency to XML box parsing. Fixed tests + that failed if OPJ_DATA_ROOT not set. + +Sep 16, 2013 - v0.5.0 Added write support for 1.5.x. Added version module. + +Aug 21, 2013 - v0.4.1 Fixed segfault with openjpeg 1.x when rlevel=-1 + +Aug 18, 2013 - v0.4.0 Added append method. + +Aug 15, 2013 - v0.3.2 Fixed test bug where missing Pillow package caused test + failures. + +Aug 14, 2013 - v0.3.1 Exposed mantissa, exponent, and guard_bits fields in QCC + and QCD segments. Exposed layers and code_block_size in COD segment. + Exposed precinct_size in COC segment. + Jul 31, 2013 - v0.3.0 Added support for official 2.0.0. Jul 27, 2013 - v0.2.8 Fixed inconsistency regarding configuration diff --git a/docs/source/conf.py b/docs/source/conf.py index 79e33af..44fc9e0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -76,9 +76,9 @@ copyright = u'2013, John Evans' # built documents. # # The short X.Y version. -version = '0.2' +version = '0.5' # The full version, including alpha/beta/rc tags. -release = '0.3.0' +release = '0.5.6' # 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 126efaa..3e4d9dc 100644 --- a/docs/source/detailed_installation.rst +++ b/docs/source/detailed_installation.rst @@ -1,43 +1,47 @@ ---------------------------------- Advanced Installation Instructions ---------------------------------- +Most users won't need to read this! You've been warned... '''''''''''''''''''''' Glymur Configuration '''''''''''''''''''''' -The default glymur installation process relies upon OpenJPEG version -1.X being properly installed on your system. This will, however, only -give you you basic read capabilities, so if you wish to take advantage -of more of glymur's features, you should install version 2.0 or -compile OpenJPEG as a shared library (named *openjp2* instead of -*openjpeg*) from the developmental source that you can retrieve via -subversion. As of this time of writing, svn revision 2345 works. +The default glymur installation process relies upon OpenJPEG +being properly installed on your system. If you have version 1.5 you can +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 2347 works. You should also download the test data for the purpose of configuring -and running OpenJPEG's test suite, check their instructions for all -this. You should set the **OPJ_DATA_ROOT** environment variable -for the purpose of running Glymur's test suite. :: +and running OpenJPEG's test suite, check their instructions for all this. +You should set the **OPJ_DATA_ROOT** environment variable for the purpose +of running Glymur's test suite. :: $ svn co http://openjpeg.googlecode.com/svn/data $ export OPJ_DATA_ROOT=`pwd`/data -Glymur uses ctypes (for the moment) to access the openjp2 library, and -because ctypes access libraries in a platform-dependent manner, it is +Glymur uses ctypes to access the openjp2/openjpeg libraries, +and because ctypes accesses libraries in a platform-dependent manner, it is recommended that you create a configuration file to help Glymur properly find -the openjp2 library. The configuration format is the same as used by Python's -configparser module, i.e. :: +the openjpeg or openjp2 libraries (linux users don't need to bother with this +if you are using OpenJPEG as provided by your package manager). The +configuration format is the same as used by Python's configparser module, +i.e. :: [library] openjp2: /opt/openjp2-svn/lib/libopenjp2.so This assumes, of course, that you've installed OpenJPEG into /opt/openjp2-svn on a linux system. The location of the configuration file -is platform-dependent (of course). If you use either linux or mac, the path +can vary as well (of course). If you use either linux or mac, the path to the configuration file would normally be :: $HOME/.config/glymur/glymurrc -but if you have **$XDG_CONFIG_HOME** defined, the path will be :: +but if you have the **XDG_CONFIG_HOME** environment variable defined, +the path will be :: $XDG_CONFIG_HOME/glymur/glymurrc @@ -52,12 +56,11 @@ You may also include a line for the version 1.x openjpeg library if you have it installed in a non-standard place, i.e. :: [library] - openjp2: /opt/openjp2-svn/lib/libopenjp2.so openjpeg: /not/the/usual/location/lib/libopenjpeg.so -''''''''''''''''''''''''''''''''''''''''''' -Package Management Suggestions for Testing -''''''''''''''''''''''''''''''''''''''''''' +'''''''''''''''''''''''''''''' +Package Management Suggestions +'''''''''''''''''''''''''''''' You only need to read this section if you want detailed platform-specific instructions on running as many tests as possible or wish to @@ -67,8 +70,9 @@ packages/RPMs/ports/whatever without going through pip. Mac OS X -------- -All the necessary packages are available to use glymur with Python 3.3 via -MacPorts. You should install the following set of ports: +All the necessary packages are available to use glymur with Python 2.6, 2.7, +and 3.3 via MacPorts. For python 3.3, you should install the following set of +ports: * python33 * py33-numpy @@ -80,71 +84,38 @@ MacPorts supplies both OpenJPEG 1.5.0 and OpenJPEG 2.0.0. Linux ----- +For the most part, you only need python and numpy to run glymur, so on +just about all distributions you are already set to go (and you don't +need to mess around with a configuration file, as the openjpeg shared +libraries are found in the usual places thanks to your package manager). +In order to run as many tests as possible, however, the following Python +packages may also need to be installed. Consult your package manager +documentation or use pip. -Fedora 19 -''''''''' -Fedora 18 ships with Python 3.3 and all the necessary RPMs are available to -run the maximum number of tests. + * setuptools + * matplotlib + * pillow + * contextlib2 (python 2.6, 2.7 only) + * mock (python 2.6, 2.7 only) + * ordereddict (python 2.6 only) - * python3 - * python3-numpy - * python3-setuptools - * python3-matplotlib (for running tests) - * python3-matplotlib-tk (or whichever matplotlib backend you prefer) - * python3-pillow (for running tests) - -Fedora 18 -''''''''' -Fedora 18 ships with Python 3.3 and the following RPMs are available to -meet the minimal set of requirements for running glymur. - - * python3 - * python3-numpy - * python3-setuptools - -For running the maximal number of tests, you also need - - * python3-matplotlib - * python3-matplotlib-tk (or whichever matplotlib backend you prefer) - -Pillow is also needed in order to run the maximum number of tests, so -go ahead and install Pillow via pip since Pillow is not available -in Fedora 18 default repositories:: - - $ yum install python3-devel # pip needs this in order to compile Pillow - $ yum install python3-pip - $ pip-python3 install Pillow --user - $ export PYTHONPATH=$HOME/.local/lib/python3.3/site-packages:$PYTHONPATH - -Fedora 17 -''''''''' -Fedora 17 ships with Python 2.7 and OpenJPEG 1.4. You should have the -following RPMs installed. - - * python - * python-mock - * python-pip - * python-setuptools - * numpy - * matplotlib (optional) - -In addition, you must install contextlib2 and Pillow via pip. :: - - $ yum install python-devel # pip needs this in order to compile Pillow - $ pip-python install Pillow --user - $ pip-python install contextlib2 --user - $ export PYTHONPATH=$HOME/.local/lib/python2.7/site-packages:$PYTHONPATH +Glymur's been tested on the following linux platforms without any unexpected +difficulties: + + * OpenSUSE 12.3 + * Fedora 17, 18, 19 + * Raspian + * Travis CI (currently Ubuntu 12.04?) + * CentOS 6.4 Windows ------- -32-bit WinPython 2.7.5 seems to work with OpenJPEG 1.X, 2.0, and the -development version, but still requires contextlib2 and mock to be +32-bit WinPython 2.7.5 seemed to work with OpenJPEG 1.X, 2.0, and the +development version, but still required contextlib2 and mock to be installed via pip. WinPython 3.3.2, however, seems to have trouble -with OpenJPEG 2.0, so I would suggest using the development version -there (I'm unwilling to spend ANY more time trying to figure out what -the problem is there). - -64-bit windows is completely untested. +with OpenJPEG 2.0, so I would suggest using the development version with +that configuration. I no longer have any access to a windows machine, +so I cannot currently offer much guidance here. ''''''' @@ -154,8 +125,8 @@ Testing There are two environment variables you may wish to set before running the tests. - * **OPJ_DATA_ROOT** - points to directory for OpenJPEG test data - * **FORMAT_CORPUS_ROOT** - points to directory for format-corpus repository (see https://github.com/openplanets/format-corpus if you wish, but you really don't need to bother with this) + * **OPJ_DATA_ROOT** - points to directory for OpenJPEG test data (see above) + * **FORMAT_CORPUS_DATA_ROOT** - points to directory for format-corpus repository (see https://github.com/openplanets/format-corpus if you wish, but you really don't need to bother with this) Setting these two environment variables is not required, as any tests using either of them will be skipped. @@ -174,7 +145,7 @@ or from the command line. :: Quite a few tests are currently skipped. These include tests whose OpenJPEG counterparts are already failing, and others which do pass but still produce heaps of output on stderr. Rather than let this swamp -the signal (that most of the tests are actually passing), they've been +the signal (that most of those tests are actually passing), they've been filtered out for now. There are also more skipped tests on Python 2.7 than on Python 3.3. The important part is whether or not any test errors are reported at the end. diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index eee558c..212862d 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -3,7 +3,7 @@ How do I...? ------------ -Read the lowest resolution thumbnail? +... read the lowest resolution thumbnail? ===================================== Printing the Jp2k object should reveal the number of resolutions (look in the COD segment section), but you can take a shortcut by supplying -1 as the @@ -14,7 +14,7 @@ resolution level. :: >>> j = glymur.Jp2k(file) >>> thumbnail = j.read(rlevel=-1) -Display metadata? +... display metadata? ================= There are two ways. From the unix command line, the script *jp2dump* is available. :: @@ -34,8 +34,40 @@ codestream box, only the main header is printed. It is possible to print >>> print(j.get_codestream()) -Add XML Metadata? +... add XML metadata? ================= +You can append any number of XML boxes to a JP2 file (not to a raw codestream). +Consider the following XML file `data.xml` : :: + + + + + Boston + 24.9 inches + + + Portland + 31.9 inches + + + New York City + 11.4 inches + + + +The **append** method can add an XML box as shown below:: + + >>> import shutil + >>> import glymur + >>> shutil.copyfile(glymur.data.nemo(), 'myfile.jp2') + >>> from xml.etree import cElementTree as ET + >>> jp2 = glymur.Jp2k('myfile.jp2') + >>> xmlbox = glymur.jp2box.XMLBox(filename='data.xml') + >>> jp2.append(xmlbox) + >>> print(jp2) + +... add metadata in a more general fashion? +======================================= 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` (a file consisting of a raw codestream), @@ -75,8 +107,8 @@ 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, +in order to add an XML box into the mix before the codestream box, we'll need to +re-specify all of the boxes. If you already have a JP2 jacket in place, you can just reuse that, though. Take the following example content in an XML file `favorites.xml` : :: @@ -84,10 +116,11 @@ 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 :: +In order to add the XML after the JP2 header box, but before the codestream box, +the following will work. :: >>> boxes = jp2.box # The box attribute is the list of JP2 boxes - >>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml') + >>> xmlbox = glymur.jp2box.XMLBox(filename='favorites.xml') >>> boxes.insert(3, xmlbox) >>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes) >>> print(jp2_xml) @@ -119,18 +152,23 @@ and add it after the JP2 header box, but before the codestream box :: . (truncated) . -Create an image with an alpha layer? +As to the question of which method you should use, **append** or **wrap**, +to add metadata, you should keep in mind that **wrap** produces a new JP2 file, +while **append** modifies an existing file and is currently limited to XML +boxes. + +... create an image with an alpha layer? ==================================== OpenJPEG can create JP2 files with more than 3 components (requires -the development version), but by default, any extra components are +the development version of OpenJPEG), but by default, any extra components are not described as such. In order to do so, we need to rewrap such an image in a set of boxes that includes a channel definition box. This example is based on SciPy example code found at http://scipy-lectures.github.io/advanced/image_processing/#basic-manipulations . -Instead of a circular mask, however, we'll make it an ellipse since the source -image isn't square. +Instead of a circular mask we'll make it an ellipse since the source +image isn't square. :: >>> import numpy as np >>> import glymur @@ -181,7 +219,7 @@ Here's how the Preview application on the mac shows the RGBA image. .. image:: goodstuff_alpha.png -Work with XMP UUIDs? +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 0a59ebd..e1a3b3b 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -4,9 +4,8 @@ Glymur: a Python interface for JPEG 2000 **Glymur** is an interface to the OpenJPEG library which allows one to read and write JPEG 2000 files from within Python. -Glymur supports both reading and writing of JPEG 2000 images. Writing -JPEG 2000 images is currently limited to images that can fit in memory, -however. +Glymur supports both reading and writing of JPEG 2000 images, but writing +JPEG 2000 images is currently limited to images that can fit in memory Of particular focus is retrieval of metadata. Reading Exif UUIDs is supported, as is reading XMP UUIDs as the XMP data packet is just XML. There is @@ -20,15 +19,14 @@ 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 2.0 series, however, and the trunk/development +only supported with the 1.5 or better, however, and the trunk/development version is strongly recommended. For more information about OpenJPEG, please consult http://www.openjpeg.org. -If you use MacPorts on the mac or if you have a sufficiently recent -version of Linux, your package manager should already provide you -with a version of OpenJPEG 1.X with which glymur can already use -for read-only purposes. If your platform is windows, I suggest -using the windows installers provided to you by the OpenJPEG +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. If your platform is windows, +I suggest using the windows installers provided to you by the OpenJPEG folks at https://code.google.com/p/openjpeg/downloads/list . Glymur Installation @@ -47,14 +45,5 @@ 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; in fact most of them are skipped if you -are relying on OpenJPEG 1.X. The important thing, though, is whether or -not any tests fail. diff --git a/docs/source/roadmap.rst b/docs/source/roadmap.rst index 7f50a87..f9216a3 100644 --- a/docs/source/roadmap.rst +++ b/docs/source/roadmap.rst @@ -1,9 +1,3 @@ ------------- -Known Issues ------------- - - * WinPython 3.3.2 and OpenJPEG 2.0 don't seem to want to play well together. If you do not need write support, just use OpenJPEG 1.5 instead. If you do need write support, try the development version of OpenJPEG. - ------- Roadmap ------- diff --git a/glymur/__init__.py b/glymur/__init__.py index fb1c504..bf2f625 100644 --- a/glymur/__init__.py +++ b/glymur/__init__.py @@ -2,12 +2,17 @@ """ import sys +from glymur import version +__version__ = version.version + from .jp2k import Jp2k from .jp2dump import jp2dump from . import data +# unittest2 only in python-2.6 (pylint/python2.7 issue) +# pylint: disable=F0401 def runtests(): """Discover and run all tests for the glymur package. """ diff --git a/glymur/codestream.py b/glymur/codestream.py index 637f40e..e83c5cc 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -3,7 +3,19 @@ The module contains classes used to store information parsed from JPEG 2000 codestreams. """ -# pylint: disable=C0302,R0902,R0903,R0913 + +# The number of lines in the module is long and that's ok. It would not help +# matters to move anything out to another file. +# pylint: disable=C0302 + +# "Too many instance attributes", "Too many arguments" +# Some segments just have a lot of information. +# It doesn't make sense to subclass just for that. +# pylint: disable=R0902,R0913 + +# "Too few public methods" Some segments don't define any new methods from +# the base Segment class. +# pylint: disable=R0903 import math import struct @@ -77,6 +89,64 @@ class Codestream(object): If True, only marker segments in the main header are parsed. Supplying False may impose a large performance penalty. """ + # Map each of the known markers to a method that processes them. + process_marker_segment = {0xff00: self._parse_reserved_segment, + 0xff01: self._parse_reserved_segment, + 0xff30: self._parse_reserved_marker, + 0xff31: self._parse_reserved_marker, + 0xff32: self._parse_reserved_marker, + 0xff33: self._parse_reserved_marker, + 0xff34: self._parse_reserved_marker, + 0xff35: self._parse_reserved_marker, + 0xff36: self._parse_reserved_marker, + 0xff37: self._parse_reserved_marker, + 0xff38: self._parse_reserved_marker, + 0xff39: self._parse_reserved_marker, + 0xff3a: self._parse_reserved_marker, + 0xff3b: self._parse_reserved_marker, + 0xff3c: self._parse_reserved_marker, + 0xff3d: self._parse_reserved_marker, + 0xff3e: self._parse_reserved_marker, + 0xff3f: self._parse_reserved_marker, + 0xff4f: self._parse_reserved_segment, + 0xff50: self._parse_reserved_segment, + 0xff51: self._parse_siz_segment, + 0xff52: self._parse_cod_segment, + 0xff53: self._parse_coc_segment, + 0xff54: self._parse_reserved_segment, + 0xff55: self._parse_tlm_segment, + 0xff56: self._parse_reserved_segment, + 0xff57: self._parse_reserved_segment, + 0xff58: self._parse_plt_segment, + 0xff59: self._parse_reserved_segment, + 0xff5a: self._parse_reserved_segment, + 0xff5b: self._parse_reserved_segment, + 0xff5c: self._parse_qcd_segment, + 0xff5d: self._parse_qcc_segment, + 0xff5e: self._parse_rgn_segment, + 0xff5f: self._parse_pod_segment, + 0xff60: self._parse_ppm_segment, + 0xff61: self._parse_ppt_segment, + 0xff62: self._parse_reserved_segment, + 0xff63: self._parse_crg_segment, + 0xff64: self._parse_cme_segment, + 0xff65: self._parse_reserved_segment, + 0xff66: self._parse_reserved_segment, + 0xff67: self._parse_reserved_segment, + 0xff68: self._parse_reserved_segment, + 0xff69: self._parse_reserved_segment, + 0xff6a: self._parse_reserved_segment, + 0xff6b: self._parse_reserved_segment, + 0xff6c: self._parse_reserved_segment, + 0xff6d: self._parse_reserved_segment, + 0xff6e: self._parse_reserved_segment, + 0xff6f: self._parse_reserved_segment, + 0xff79: self._parse_unrecognized_segment, + 0xff90: self._parse_sot_segment, + 0xff91: self._parse_unrecognized_segment, + 0xff92: self._parse_unrecognized_segment, + 0xff93: self._parse_sod_segment, + 0xffd9: self._parse_eoc_segment} self.offset = fptr.tell() self.length = length @@ -90,9 +160,8 @@ class Codestream(object): self.segment = [] - # First two bytes are the SOC marker + # First two bytes are the SOC marker. We already know that. read_buffer = fptr.read(2) - marker_id, = struct.unpack('>H', read_buffer) segment = SOCsegment(offset=fptr.tell() - 2, length=0) self.segment.append(segment) @@ -103,7 +172,7 @@ class Codestream(object): read_buffer = fptr.read(2) try: - marker_id, = struct.unpack('>H', read_buffer) + self._marker_id, = struct.unpack('>H', read_buffer) except struct.error: # Treat this as a warning. msg = "Marker had length {0} instead of expected length of 2 " @@ -111,26 +180,29 @@ class Codestream(object): warnings.warn(msg.format(len(read_buffer))) break - if marker_id == 0xff90 and header_only: + self._offset = fptr.tell() - 2 + + if self._marker_id == 0xff90 and header_only: # Start-of-tile (SOT) means that we are out of the main header # and there is no need to go further. break try: - segment = self._process_marker_segment(fptr, marker_id) - except Exception as error: - # Treat this as a warning. - msg = str(error) + segment = process_marker_segment[self._marker_id](fptr) + except KeyError: + msg = 'Invalid marker id encountered at byte {0:d} ' + msg += 'in codestream: "0x{1:x}"' + msg = msg.format(self._offset, self._marker_id) warnings.warn(msg) break self.segment.append(segment) - if marker_id == 0xffd9: + if self._marker_id == 0xffd9: # end of codestream, should break. break - if marker_id == 0xff93: + if self._marker_id == 0xff93: # If SOD, then we need to seek past the tile part bit stream. if self._parse_tpart_flag and not header_only: # But first parse the tile part bit stream for SOP and @@ -140,115 +212,44 @@ class Codestream(object): fptr.seek(self._tile_offset[-1] + self._tile_length[-1]) - def _process_marker_segment(self, fptr, marker_id): - """Process and return a segment from the codestream. + def _parse_unrecognized_segment(self, fptr): + """Looks like a valid marker, but not sure from reading the specs. + """ + msg = "Unrecognized marker id: 0x{0:x}".format(self._marker_id) + warnings.warn(msg) + cpos = fptr.tell() + read_buffer = fptr.read(2) + next_item, = struct.unpack('>H', read_buffer) + fptr.seek(cpos) + if ((next_item & 0xff00) >> 8) == 255: + # No segment associated with this marker, so reset + # to two bytes after it. + segment = Segment(id='0x{0:x}'.format(self._marker_id), + offset=self._offset, length=0) + else: + segment = self._parse_reserved_segment(fptr) + return segment + + def _parse_reserved_segment(self, fptr): + """Parse valid marker segment, segment description is unknown. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + Segment instance. """ offset = fptr.tell() - 2 - if marker_id >= 0xff30 and marker_id <= 0xff3f: - the_id = '0x{0:x}'.format(marker_id) - segment = Segment(marker_id=the_id, offset=offset, length=0) - - elif marker_id == 0xff51: - # Need to keep track of the number of components from SIZ for - # other markers - segment = _parse_siz_segment(fptr) - self._csiz = len(segment.ssiz) - - elif marker_id == 0xff52: - segment = _parse_cod_segment(fptr) - - sop = (segment.scod & 2) > 0 - eph = (segment.scod & 4) > 0 - - if sop or eph: - self._parse_tpart_flag = True - else: - self._parse_tpart_flag = False - - elif marker_id == 0xff53: - segment = self._parse_coc_segment(fptr) - - elif marker_id == 0xff55: - segment = _parse_tlm_segment(fptr) - - elif marker_id == 0xff58: - segment = _parse_plt_segment(fptr) - - elif marker_id == 0xff5c: - segment = _parse_qcd_segment(fptr) - - elif marker_id == 0xff5d: - segment = self._parse_qcc_segment(fptr) - - elif marker_id == 0xff5e: - segment = self._parse_rgn_segment(fptr) - - elif marker_id == 0xff5f: - segment = self._parse_pod_segment(fptr) - - elif marker_id == 0xff60: - segment = _parse_ppm_segment(fptr) - - elif marker_id == 0xff61: - segment = _parse_ppt_segment(fptr) - - elif marker_id == 0xff63: - segment = _parse_crg_segment(fptr, self._csiz) - - elif marker_id == 0xff64: - segment = _parse_cme_segment(fptr) - - elif marker_id == 0xff90: - # Need to keep easy access to tile offsets and lengths for when - # we encounter start-of-data marker segments. - - segment = _parse_sot_segment(fptr) - self._tile_offset.append(segment.offset) - if segment.psot == 0: - tile_part_length = (self.offset + self.length - - segment.offset - 2) - else: - tile_part_length = segment.psot - self._tile_length.append(tile_part_length) - - elif marker_id == 0xff93: - # start of data. Need to seek past the current tile part. - # The last SOT marker segment has the info that we need. - segment = _parse_sod_segment(fptr) - - elif marker_id == 0xffd9: - # end of codestream - segment = _parse_eoc_segment(fptr) - - elif marker_id in _VALID_MARKERS: - # It's a reserved marker that I don't know anything about. - # See table A-1 in ISO/IEC FCD15444-1. - segment = _parse_generic_segment(fptr, marker_id) - - elif ((marker_id & 0xff00) >> 8) == 255: - # Peek ahead to see if the next two bytes are a marker or not. - # Then seek back. - msg = "Unrecognized marker id: 0x{0:x}".format(marker_id) - warnings.warn(msg) - cpos = fptr.tell() - read_buffer = fptr.read(2) - next_item, = struct.unpack('>H', read_buffer) - fptr.seek(cpos) - if ((next_item & 0xff00) >> 8) == 255: - # No segment associated with this marker, so reset - # to two bytes after it. - segment = Segment(id='0x{0:x}'.format(marker_id), - offset=offset, length=0) - else: - segment = _parse_generic_segment(fptr, marker_id) - - else: - msg = 'Invalid marker id encountered at byte {0:d} ' - msg += 'in codestream: "0x{1:x}"' - msg = msg.format(offset, marker_id) - raise IOError(msg) + read_buffer = fptr.read(2) + length, = struct.unpack('>H', read_buffer) + data = fptr.read(length-2) + segment = Segment(marker_id='0x{0:x}'.format(self._marker_id), + offset=offset, length=length, data=data) return segment def _parse_tile_part_bit_stream(self, fptr, sod_marker, tile_length): @@ -288,6 +289,29 @@ class Codestream(object): msg += ''.join(strs) return msg + # pylint: disable=R0201 + def _parse_cme_segment(self, fptr): + """Parse the CME marker segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + CME segment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(4) + data = struct.unpack('>HH', read_buffer) + length = data[0] + rcme = data[1] + ccme = fptr.read(length - 4) + + return CMEsegment(rcme, ccme, length, offset) + def _parse_coc_segment(self, fptr): """Parse the COC marker segment. @@ -326,6 +350,118 @@ class Codestream(object): return COCsegment(ccoc, scoc, spcoc, length, offset) + def _parse_cod_segment(self, fptr): + """Parse the COD segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + COD segment instance. + """ + offset = fptr.tell() - 2 + offset = fptr.tell() - 2 + + read_buffer = fptr.read(3) + length, scod = struct.unpack('>HB', read_buffer) + + numbytes = offset + 2 + length - fptr.tell() + spcod = fptr.read(numbytes) + spcod = np.frombuffer(spcod, dtype=np.uint8) + + sop = (scod & 2) > 0 + eph = (scod & 4) > 0 + + if sop or eph: + self._parse_tpart_flag = True + else: + self._parse_tpart_flag = False + + return CODsegment(scod, spcod, length, offset) + + def _parse_crg_segment(self, fptr): + """Parse the CRG marker segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + CRG segment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(2) + length, = struct.unpack('>H', read_buffer) + + read_buffer = fptr.read(4 * self._csiz) + data = struct.unpack('>' + 'HH' * self._csiz, read_buffer) + xcrg = data[0::2] + ycrg = data[1::2] + + return CRGsegment(xcrg, ycrg, length, offset) + + def _parse_eoc_segment(self, fptr): + """Parse the EOC (end-of-codestream) marker segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + EOC Segment instance. + """ + offset = fptr.tell() - 2 + length = 0 + + return EOCsegment(length, offset) + + def _parse_plt_segment(self, fptr): + """Parse the PLT segment. + + The packet headers are not parsed, i.e. they remain "uninterpreted" + raw data beffers. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + PLT segment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(3) + length, zplt = struct.unpack('>HB', read_buffer) + + numbytes = length - 3 + read_buffer = fptr.read(numbytes) + iplt = np.frombuffer(read_buffer, dtype=np.uint8) + + packet_len = [] + plen = 0 + for byte in iplt: + plen |= (byte & 0x7f) + if byte & 0x80: + # Continue by or-ing in the next byte. + plen <<= 7 + else: + packet_len.append(plen) + plen = 0 + + iplt = packet_len + + return PLTsegment(zplt, iplt, length, offset) + def _parse_pod_segment(self, fptr): """Parse the POD segment. @@ -356,6 +492,55 @@ class Codestream(object): return PODsegment(pod_params, length, offset) + def _parse_ppm_segment(self, fptr): + """Parse the PPM segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + PPM segment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(3) + length, zppm = struct.unpack('>HB', read_buffer) + + numbytes = length - 3 + read_buffer = fptr.read(numbytes) + + return PPMsegment(zppm, read_buffer, length, offset) + + def _parse_ppt_segment(self, fptr): + """Parse the PPT segment. + + The packet headers are not parsed, i.e. they remain "uninterpreted" + raw data beffers. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + PPT segment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(3) + length, zppt = struct.unpack('>HB', read_buffer) + length = length + zppt = zppt + + numbytes = length - 3 + ippt = fptr.read(numbytes) + + return PPTsegment(zppt, ippt, length, offset) + def _parse_qcc_segment(self, fptr): """Parse the QCC segment. @@ -383,8 +568,8 @@ class Codestream(object): mantissa_exponent_buffer_length = length - 4 cqcc, sqcc = struct.unpack(fmt, read_buffer) if cqcc >= self._csiz: - msg = "Invalid component number (%d), " - msg += "number of components is only %d." + msg = "Invalid component number ({0}), " + msg += "number of components is only {1}." msg = msg.format(cqcc, self._csiz) warnings.warn(msg) @@ -392,6 +577,26 @@ class Codestream(object): return QCCsegment(cqcc, sqcc, spqcc, length, offset) + def _parse_qcd_segment(self, fptr): + """Parse the QCD segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + QCD Segment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(3) + length, sqcd = struct.unpack('>HB', read_buffer) + spqcd = fptr.read(length - 3) + + return QCDsegment(sqcd, spqcd, length, offset) + def _parse_rgn_segment(self, fptr): """Parse the RGN segment. @@ -423,6 +628,152 @@ class Codestream(object): return RGNsegment(length, offset, crgn, srgn, sprgn) + def _parse_siz_segment(self, fptr): + """Parse the SIZ segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + SIZsegment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(2) + length, = struct.unpack('>H', read_buffer) + + xy_buffer = fptr.read(36) + + num_components, = struct.unpack('>H', xy_buffer[-2:]) + + component_buffer = fptr.read(num_components * 3) + + segment = SIZsegment(xy_buffer, component_buffer, length, offset) + + # Need to keep track of the number of components from SIZ for + # other markers + self._csiz = len(segment.ssiz) + + return segment + + def _parse_sod_segment(self, fptr): + """Parse the SOD (start-of-data) segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + SOD segment instance. + """ + offset = fptr.tell() - 2 + length = 0 + + return SODsegment(length, offset) + + def _parse_sot_segment(self, fptr): + """Parse the SOT segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + SOT segment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(10) + data = struct.unpack('>HHIBB', read_buffer) + + length = data[0] + isot = data[1] + psot = data[2] + tpsot = data[3] + tnsot = data[4] + + segment = SOTsegment(isot, psot, tpsot, tnsot, length, offset) + + # Need to keep easy access to tile offsets and lengths for when + # we encounter start-of-data marker segments. + + self._tile_offset.append(segment.offset) + if segment.psot == 0: + tile_part_length = (self.offset + self.length - + segment.offset - 2) + else: + tile_part_length = segment.psot + self._tile_length.append(tile_part_length) + + return segment + + def _parse_tlm_segment(self, fptr): + """Parse the TLM segment. + + Parameters + ---------- + fptr : file + Open file object. + + Returns + ------- + TLM segment instance. + """ + offset = fptr.tell() - 2 + + read_buffer = fptr.read(2) + length, = struct.unpack('>H', read_buffer) + + read_buffer = fptr.read(2) + ztlm, stlm = struct.unpack('>BB', read_buffer) + ttlm_st = (stlm >> 4) & 0x3 + ptlm_sp = (stlm >> 6) & 0x1 + + nbytes = length - 4 + if ttlm_st == 0: + ntiles = nbytes / ((ptlm_sp + 1) * 2) + else: + ntiles = nbytes / (ttlm_st + (ptlm_sp + 1) * 2) + + read_buffer = fptr.read(nbytes) + if ttlm_st == 0: + ttlm = None + fmt = '' + elif ttlm_st == 1: + fmt = 'B' + elif ttlm_st == 2: + fmt = 'H' + + if ptlm_sp == 0: + fmt += 'H' + else: + fmt += 'I' + + data = struct.unpack('>' + fmt * int(ntiles), read_buffer) + if ttlm_st == 0: + ttlm = None + ptlm = data + else: + ttlm = data[0::2] + ptlm = data[1::2] + + return TLMsegment(length, offset, ztlm, ttlm, ptlm) + + # pylint: disable=W0613 + def _parse_reserved_marker(self, fptr): + """Marker range between 0xff30 and 0xff39. + """ + the_id = '0x{0:x}'.format(self._marker_id) + segment = Segment(marker_id=the_id, offset=self._offset, length=0) + return segment + class Segment(object): """Segment information. @@ -436,12 +787,15 @@ class Segment(object): length : int Length of marker segment in bytes. This number does not include the two bytes constituting the marker. + data : bytes iterable or None + Uninterpreted buffer of raw bytes, only used where a segment is not + well understood. """ def __init__(self, marker_id='', offset=-1, length=-1, data=None): self.marker_id = marker_id self.offset = offset self.length = length - self._data = data + self.data = data def __str__(self): msg = '{0} marker segment @ ({1}, {2})'.format(self.marker_id, @@ -468,6 +822,8 @@ class COCsegment(Segment): Coding style for this component. spcoc : byte array Coding style parameters for this component. + precinct_size : list of tuples + Dimensions of precinct. References ---------- @@ -481,13 +837,13 @@ class COCsegment(Segment): self.scoc = scoc self.spcoc = spcoc - self._code_block_size = (4 * math.pow(2, self.spcoc[2]), - 4 * math.pow(2, self.spcoc[1])) + self.code_block_size = (4 * math.pow(2, self.spcoc[2]), + 4 * math.pow(2, self.spcoc[1])) if len(self.spcoc) > 5: - self._precinct_size = _parse_precinct_size(self.spcoc[5:]) + self.precinct_size = _parse_precinct_size(self.spcoc[5:]) else: - self._precinct_size = None + self.precinct_size = None self.length = length self.offset = offset @@ -508,16 +864,16 @@ class COCsegment(Segment): msg += '\n Code block height, width: ({1} x {2})' msg += '\n Wavelet transform: {3}' msg = msg.format(self.spcoc[0] + 1, - int(self._code_block_size[0]), - int(self._code_block_size[1]), + int(self.code_block_size[0]), + int(self.code_block_size[1]), _WAVELET_TRANSFORM_DISPLAY[self.spcoc[4]]) msg += '\n ' msg += _context_string(self.spcoc[3]) - if self._precinct_size is not None: + if self.precinct_size is not None: msg += '\n Precinct size: ' - for pps in self._precinct_size: + for pps in self.precinct_size: msg += '(%d, %d)'.format(pps) return msg @@ -537,10 +893,16 @@ class CODsegment(Segment): two bytes constituting the marker. scod : int Default coding style. + layers : int + Quality layers. + code_block_size : tuple + Size of code block. spcod : bytes - Coding style parameters, including quality layers, multicomponent - transform usage, decomposition levels, code block size, style of code- - block passes, and which wavelet transform is used. + Encoded coding style parameters, including quality layers, + multi component transform usage, decomposition levels, code block size, + style of code-block passes, and which wavelet transform is used. + precinct_size : list of tuples + Dimensions of precinct. References ---------- @@ -556,7 +918,7 @@ class CODsegment(Segment): self.offset = offset params = struct.unpack('>BHBBBBBB', self.spcod[0:9]) - self._layers = params[1] + self.layers = params[1] self._numresolutions = params[3] if params[3] > opj2.J2K_MAXRLVLS: @@ -567,12 +929,12 @@ class CODsegment(Segment): cblk_width = 4 * math.pow(2, params[4]) cblk_height = 4 * math.pow(2, params[5]) code_block_size = (cblk_height, cblk_width) - self._code_block_size = code_block_size + self.code_block_size = code_block_size if len(self.spcod) > 9: - self._precinct_size = _parse_precinct_size(self.spcod[9:]) + self.precinct_size = _parse_precinct_size(self.spcod[9:]) else: - self._precinct_size = None + self.precinct_size = None def __str__(self): msg = Segment.__str__(self) @@ -605,18 +967,18 @@ class CODsegment(Segment): msg += '\n '.join(lines) msg = msg.format(_PROGRESSION_ORDER_DISPLAY[self.spcod[0]], - self._layers, + self.layers, mct, self.spcod[4] + 1, - int(self._code_block_size[0]), - int(self._code_block_size[1]), + int(self.code_block_size[0]), + int(self.code_block_size[1]), _WAVELET_TRANSFORM_DISPLAY[self.spcod[8]]) msg += '\n Precinct size: ' - if self._precinct_size is None: + if self.precinct_size is None: msg += 'default, 2^15 x 2^15' else: - for pps in self._precinct_size: + for pps in self.precinct_size: msg += '({0}, {1})'.format(pps[0], pps[1]) msg += '\n ' @@ -856,8 +1218,8 @@ class PPMsegment(Segment): Segment.__init__(self, marker_id='PPM') self.zppm = zppm - # both Nppm and Ippms information stored in _data - self._data = data + # both Nppm and Ippms information stored in data + self.data = data self.length = length self.offset = offset @@ -866,7 +1228,7 @@ class PPMsegment(Segment): msg = Segment.__str__(self) msg += '\n Index: {0}' msg += '\n Data: {1} uninterpreted bytes' - msg = msg.format(self.zppm, len(self._data)) + msg = msg.format(self.zppm, len(self.data)) return msg @@ -926,6 +1288,10 @@ class QCCsegment(Segment): Quantization style for this component. spqcc : iterable bytes Quantization value for each sub-band. + mantissa, exponent : iterable + Defines quantization factors. + guard_bits : int + Number of guard bits. References ---------- @@ -941,18 +1307,18 @@ class QCCsegment(Segment): self.length = length self.offset = offset - self._mantissa, self._exponent = parse_quantization(self.spqcc, - self.sqcc) - self._guard_bits = (self.sqcc & 0xe0) >> 5 + self.mantissa, self.exponent = parse_quantization(self.spqcc, + self.sqcc) + self.guard_bits = (self.sqcc & 0xe0) >> 5 def __str__(self): msg = Segment.__str__(self) msg += '\n Associated Component: {0}'.format(self.cqcc) msg += _print_quantization_style(self.sqcc) - msg += '{0} guard bits'.format(self._guard_bits) + msg += '{0} guard bits'.format(self.guard_bits) - step_size = zip(self._mantissa, self._exponent) + step_size = zip(self.mantissa, self.exponent) msg += '\n Step size: ' + str(list(step_size)) return msg @@ -973,6 +1339,10 @@ class QCDsegment(Segment): Quantization style for all components. spqcd : iterable bytes Quantization step size values (uninterpreted). + mantissa, exponent : iterable + Defines quantization factors. + guard_bits : int + Number of guard bits. References ---------- @@ -989,18 +1359,18 @@ class QCDsegment(Segment): self.offset = offset mantissa, exponent = parse_quantization(self.spqcd, self.sqcd) - self._mantissa = mantissa - self._exponent = exponent - self._guard_bits = (self.sqcd & 0xe0) >> 5 + self.mantissa = mantissa + self.exponent = exponent + self.guard_bits = (self.sqcd & 0xe0) >> 5 def __str__(self): msg = Segment.__str__(self) msg += _print_quantization_style(self.sqcd) - msg += '{0} guard bits'.format(self._guard_bits) + msg += '{0} guard bits'.format(self.guard_bits) - step_size = zip(self._mantissa, self._exponent) + step_size = zip(self.mantissa, self.exponent) msg += '\n Step size: ' + str(list(step_size)) return msg @@ -1072,7 +1442,11 @@ class SIZsegment(Segment): xtosiz, ytosiz : int Horizontal and vertical offsets of tile from origin of reference grid. ssiz : iterable bytes - Precision (depth) in bits and sign of each component. + Encoded precision (depth) in bits and sign of each component. + bitdepth : iterable bytes + Precision (depth) in bits of each component. + signed : iterable bool + Signedness of each component. xrsiz, yrsiz : int Horizontal and vertical sample separations with respect to reference grid. @@ -1120,8 +1494,8 @@ class SIZsegment(Segment): self.xrsiz = data[1::3] self.yrsiz = data[2::3] - self._bitdepth = tuple(((x & 0x7f) + 1) for x in self.ssiz) - self._signed = tuple(((x & 0xb0) > 0) for x in self.ssiz) + self.bitdepth = tuple(((x & 0x7f) + 1) for x in self.ssiz) + self.signed = tuple(((x & 0xb0) > 0) for x in self.ssiz) self.length = length self.offset = offset @@ -1144,8 +1518,8 @@ class SIZsegment(Segment): self.yosiz, self.xosiz, self.ytsiz, self.xtsiz, self.ytosiz, self.xtosiz, - self._bitdepth, - self._signed, + self.bitdepth, + self.signed, tuple(zip(self.yrsiz, self.xrsiz))) return msg @@ -1433,352 +1807,3 @@ def _print_quantization_style(sqcc): elif sqcc & 0x1f == 2: msg += 'scalar explicit, ' return msg - - -def _parse_tlm_segment(fptr): - """Parse the TLM segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - TLM segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(2) - length, = struct.unpack('>H', read_buffer) - - read_buffer = fptr.read(2) - ztlm, stlm = struct.unpack('>BB', read_buffer) - ttlm_st = (stlm >> 4) & 0x3 - ptlm_sp = (stlm >> 6) & 0x1 - - nbytes = length - 4 - if ttlm_st == 0: - ntiles = nbytes / ((ptlm_sp + 1) * 2) - else: - ntiles = nbytes / (ttlm_st + (ptlm_sp + 1) * 2) - - read_buffer = fptr.read(nbytes) - if ttlm_st == 0: - ttlm = None - fmt = '' - elif ttlm_st == 1: - fmt = 'B' - elif ttlm_st == 2: - fmt = 'H' - - if ptlm_sp == 0: - fmt += 'H' - else: - fmt += 'I' - - data = struct.unpack('>' + fmt * int(ntiles), read_buffer) - if ttlm_st == 0: - ttlm = None - ptlm = data - else: - ttlm = data[0::2] - ptlm = data[1::2] - - return TLMsegment(length, offset, ztlm, ttlm, ptlm) - - -def _parse_sot_segment(fptr): - """Parse the SOT segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - SOT segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(10) - data = struct.unpack('>HHIBB', read_buffer) - - length = data[0] - isot = data[1] - psot = data[2] - tpsot = data[3] - tnsot = data[4] - - return SOTsegment(isot, psot, tpsot, tnsot, length, offset) - - -def _parse_sod_segment(fptr): - """Parse the SOD segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - SOD segment instance. - """ - offset = fptr.tell() - 2 - length = 0 - - return SODsegment(length, offset) - - -def _parse_qcd_segment(fptr): - """Parse the QCD segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - QCD Segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(3) - length, sqcd = struct.unpack('>HB', read_buffer) - spqcd = fptr.read(length - 3) - - return QCDsegment(sqcd, spqcd, length, offset) - - -def _parse_ppt_segment(fptr): - """Parse the PPT segment. - - The packet headers are not parsed, i.e. they remain "uninterpreted" - raw data beffers. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - PPT segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(3) - length, zppt = struct.unpack('>HB', read_buffer) - length = length - zppt = zppt - - numbytes = length - 3 - ippt = fptr.read(numbytes) - - return PPTsegment(zppt, ippt, length, offset) - - -def _parse_plt_segment(fptr): - """Parse the PLT segment. - - The packet headers are not parsed, i.e. they remain "uninterpreted" - raw data beffers. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - PLT segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(3) - length, zplt = struct.unpack('>HB', read_buffer) - - numbytes = length - 3 - read_buffer = fptr.read(numbytes) - iplt = np.frombuffer(read_buffer, dtype=np.uint8) - - packet_len = [] - plen = 0 - for byte in iplt: - plen |= (byte & 0x7f) - if byte & 0x80: - # Continue by or-ing in the next byte. - plen <<= 7 - else: - packet_len.append(plen) - plen = 0 - - iplt = packet_len - - return PLTsegment(zplt, iplt, length, offset) - - -def _parse_ppm_segment(fptr): - """Parse the PPM segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - PPM segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(3) - length, zppm = struct.unpack('>HB', read_buffer) - - numbytes = length - 3 - read_buffer = fptr.read(numbytes) - - return PPMsegment(zppm, read_buffer, length, offset) - - -def _parse_crg_segment(fptr, csiz): - """Parse the CRG marker segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - CRG segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(2) - length, = struct.unpack('>H', read_buffer) - - read_buffer = fptr.read(4 * csiz) - data = struct.unpack('>' + 'HH' * csiz, read_buffer) - xcrg = data[0::2] - ycrg = data[1::2] - - return CRGsegment(xcrg, ycrg, length, offset) - - -def _parse_eoc_segment(fptr): - """Parse the EOC marker segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - EOC Segment instance. - """ - offset = fptr.tell() - 2 - length = 0 - - return EOCsegment(length, offset) - - -def _parse_cme_segment(fptr): - """Parse the CME marker segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - CME segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(4) - data = struct.unpack('>HH', read_buffer) - length = data[0] - rcme = data[1] - ccme = fptr.read(length - 4) - - return CMEsegment(rcme, ccme, length, offset) - - -def _parse_siz_segment(fptr): - """Parse the SIZ segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - SIZsegment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(2) - length, = struct.unpack('>H', read_buffer) - - xy_buffer = fptr.read(36) - - num_components, = struct.unpack('>H', xy_buffer[-2:]) - - component_buffer = fptr.read(num_components * 3) - - return SIZsegment(xy_buffer, component_buffer, length, offset) - - -def _parse_cod_segment(fptr): - """Parse the COD segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - COD segment instance. - """ - offset = fptr.tell() - 2 - offset = fptr.tell() - 2 - - read_buffer = fptr.read(3) - length, scod = struct.unpack('>HB', read_buffer) - - numbytes = offset + 2 + length - fptr.tell() - spcod = fptr.read(numbytes) - spcod = np.frombuffer(spcod, dtype=np.uint8) - - return CODsegment(scod, spcod, length, offset) - - -def _parse_generic_segment(fptr, marker_id): - """Parse a generic marker segment. - - Parameters - ---------- - fptr : file - Open file object. - - Returns - ------- - Segment instance. - """ - offset = fptr.tell() - 2 - - read_buffer = fptr.read(2) - length, = struct.unpack('>H', read_buffer) - data = fptr.read(length-2) - - segment = Segment(marker_id='0x{0:x}'.format(marker_id), offset=offset, - length=length, data=data) - return segment diff --git a/glymur/jp2box.py b/glymur/jp2box.py index c264b7b..bf88e4d 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -24,6 +24,7 @@ import uuid import warnings import xml.etree.cElementTree as ET if sys.hexversion < 0x02070000: + # pylint: disable=F0401,E0611 from ordereddict import OrderedDict from xml.etree.cElementTree import XMLParserError as ParseError else: @@ -361,7 +362,8 @@ class _ICCProfile(object): header['Connection Space'] = data data = struct.unpack('>HHHHHH', self._raw_buffer[24:36]) - header['Datetime'] = datetime.datetime(*data) + header['Datetime'] = datetime.datetime(data[0], data[1], data[2], + data[3], data[4], data[5]) header['File Signature'] = read_buffer[36:40].decode('utf-8') if read_buffer[40:44] == b'\x00\x00\x00\x00': header['Platform'] = 'unrecognized' @@ -369,10 +371,9 @@ class _ICCProfile(object): header['Platform'] = read_buffer[40:44].decode('utf-8') fval, = struct.unpack('>I', read_buffer[44:48]) - flags = 'embedded, ' if fval & 0x01 else 'not embedded, ' - flags += 'cannot ' if fval & 0x02 else 'can ' - flags += 'be used independently' - header['Flags'] = flags + flags = "{0}embedded, {1} be used independently" + header['Flags'] = flags.format('' if fval & 0x01 else 'not ', + 'cannot' if fval & 0x02 else 'can') header['Device Manufacturer'] = read_buffer[48:52].decode('utf-8') if read_buffer[52:56] == b'\x00\x00\x00\x00': @@ -382,11 +383,11 @@ class _ICCProfile(object): header['Device Model'] = device_model val, = struct.unpack('>Q', read_buffer[56:64]) - attr = 'transparency, ' if val & 0x01 else 'reflective, ' - attr += 'matte, ' if val & 0x02 else 'glossy, ' - attr += 'negative ' if val & 0x04 else 'positive ' - attr += 'media polarity, ' - attr += 'black and white media' if val & 0x08 else 'color media' + attr = "{0}, {1}, {2} media polarity, {3} media" + attr = attr.format('transparency' if val & 0x01 else 'reflective', + 'matte' if val & 0x02 else 'glossy', + 'negative' if val & 0x04 else 'positive', + 'black and white' if val & 0x08 else 'color') header['Device Attributes'] = attr rval, = struct.unpack('>I', read_buffer[64:68]) @@ -567,7 +568,7 @@ class CodestreamHeaderBox(Jp2kBox): Returns ------- - AssociationBox instance + CodestreamHeaderBox instance """ box = CodestreamHeaderBox(length=length, offset=offset) @@ -626,7 +627,7 @@ class CompositingLayerHeaderBox(Jp2kBox): Returns ------- - AssociationBox instance + CompositingLayerHeaderBox instance """ box = CompositingLayerHeaderBox(length=length, offset=offset) @@ -637,7 +638,7 @@ class CompositingLayerHeaderBox(Jp2kBox): class ComponentMappingBox(Jp2kBox): - """Container for channel identification information. + """Container for component mapping information. Attributes ---------- @@ -1237,43 +1238,68 @@ class PaletteBox(Jp2kBox): bps = [((x & 0x07f) + 1) for x in data] signed = [((x & 0x80) > 1) for x in data] + # 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]) + # Form the format string so that we can intelligently unpack the # colormap. We have to do this because it is possible that the # colormap columns could have different datatypes. # # This means that we store the palette as a list of 1D arrays, # which reverses the usual indexing scheme. - palette = [] - fmt = '>' - row_nbytes = 0 - for j in range(num_columns): - if bps[j] <= 8: - fmt += 'B' - row_nbytes += 1 - palette.append(np.zeros(num_entries, dtype=np.uint8)) - elif bps[j] <= 16: - fmt += 'H' - row_nbytes += 2 - palette.append(np.zeros(num_entries, dtype=np.uint16)) - elif bps[j] <= 32: - fmt += 'I' - row_nbytes += 4 - palette.append(np.zeros(num_entries, dtype=np.uint32)) - else: - msg = 'Unsupported palette bitdepth (%d).'.format(bps[j]) - raise IOError(msg) read_buffer = fptr.read(num_entries * row_nbytes) + palette = _buffer2palette(read_buffer, num_entries, num_columns, bps) - for j in range(num_entries): - row_buffer = read_buffer[(row_nbytes * j):(row_nbytes * (j + 1))] - row = struct.unpack(fmt, row_buffer) - for k in range(num_columns): - palette[k][j] = row[k] - - box = PaletteBox(palette, bps, signed, length=length, - offset=offset) + box = PaletteBox(palette, bps, signed, length=length, offset=offset) return box + +def _buffer2palette(read_buffer, num_rows, num_cols, bps): + """Construct the palette from the buffer read from file. + + Parameters + ---------- + read_buffer : iterable + Byte array of palette information read from file. + num_rows, num_cols : int + Size of palette. + bps : iterable + Bits per sample for each channel. + + Returns + ------- + palette : list of 1D arrays + Each 1D array corresponds to a channel. + """ + row_nbytes = 0 + palette = [] + fmt = '>' + for j in range(num_cols): + if bps[j] <= 8: + row_nbytes += 1 + fmt += 'B' + palette.append(np.zeros(num_rows, dtype=np.uint8)) + elif bps[j] <= 16: + row_nbytes += 2 + fmt += 'H' + palette.append(np.zeros(num_rows, dtype=np.uint16)) + elif bps[j] <= 32: + row_nbytes += 4 + fmt += 'I' + palette.append(np.zeros(num_rows, dtype=np.uint32)) + else: + msg = 'Unsupported palette bitdepth (%d).'.format(bps[j]) + raise IOError(msg) + + for j in range(num_rows): + row_buffer = read_buffer[(row_nbytes * j):(row_nbytes * (j + 1))] + row = struct.unpack(fmt, row_buffer) + for k in range(num_cols): + palette[k][j] = row[k] + + return palette + # Map rreq codes to display text. _READER_REQUIREMENTS_DISPLAY = { 0: 'File not completely understood', @@ -1434,52 +1460,18 @@ class ReaderRequirementsBox(Jp2kBox): """ read_buffer = fptr.read(1) mask_length, = struct.unpack('>B', read_buffer) - if mask_length == 1: - mask_format = 'B' - elif mask_length == 2: - mask_format = 'H' - elif mask_length == 4: - mask_format = 'I' - else: - msg = 'Unhandled reader requirements box mask length (%d).' - msg %= mask_length - raise RuntimeError(msg) # Fully Understands Aspect Mask # Decodes Completely Mask read_buffer = fptr.read(2 * mask_length) - data = struct.unpack('>' + mask_format * 2, read_buffer) - fuam = data[0] - dcm = data[1] - read_buffer = fptr.read(2) - num_standard_flags, = struct.unpack('>H', read_buffer) + # The mask length tells us the format string to use when unpacking + # from the buffer read from file. + mask_format = {1: 'B', 2: 'H', 4: 'I'}[mask_length] + fuam, dcm = struct.unpack('>' + mask_format * 2, read_buffer) - # Read in standard flags and standard masks. Each standard flag should - # be two bytes, but the standard mask flag is as long as specified by - # the mask length. - read_buffer = fptr.read(num_standard_flags * (2 + mask_length)) - data = struct.unpack('>' + ('H' + mask_format) * num_standard_flags, - read_buffer) - standard_flag = data[0:num_standard_flags * 2:2] - standard_mask = data[1:num_standard_flags * 2:2] - - # Vendor features - read_buffer = fptr.read(2) - num_vendor_features, = struct.unpack('>H', read_buffer) - - # Each vendor feature consists of a 16-byte UUID plus a mask whose - # length is specified by, you guessed it, "mask_length". - entry_length = 16 + mask_length - read_buffer = fptr.read(num_vendor_features * entry_length) - vendor_feature = [] - vendor_mask = [] - for j in range(num_vendor_features): - ubuffer = read_buffer[j * entry_length:(j + 1) * entry_length] - vendor_feature.append(uuid.UUID(bytes=ubuffer[0:16])) - - vmask = struct.unpack('>' + mask_format, ubuffer[16:]) - vendor_mask.append(vmask) + standard_flag, standard_mask = _parse_standard_flag(fptr, mask_length) + vendor_feature, vendor_mask = _parse_vendor_features(fptr, mask_length) box = ReaderRequirementsBox(fuam, dcm, standard_flag, standard_mask, vendor_feature, vendor_mask, @@ -1487,6 +1479,74 @@ class ReaderRequirementsBox(Jp2kBox): return box +def _parse_standard_flag(fptr, mask_length): + """Construct standard flag, standard mask data from the file. + + Specifically working on Reader Requirements box. + + Parameters + ---------- + fptr : file object + File object for JP2K file. + mask_length : int + Length of standard mask flag + """ + # The mask length tells us the format string to use when unpacking + # from the buffer read from file. + mask_format = {1: 'B', 2: 'H', 4: 'I'}[mask_length] + + read_buffer = fptr.read(2) + num_standard_flags, = struct.unpack('>H', read_buffer) + + # Read in standard flags and standard masks. Each standard flag should + # be two bytes, but the standard mask flag is as long as specified by + # the mask length. + read_buffer = fptr.read(num_standard_flags * (2 + mask_length)) + + fmt = '>' + ('H' + mask_format) * num_standard_flags + data = struct.unpack(fmt, read_buffer) + + standard_flag = data[0:num_standard_flags * 2:2] + standard_mask = data[1:num_standard_flags * 2:2] + + return standard_flag, standard_mask + + +def _parse_vendor_features(fptr, mask_length): + """Construct vendor features, vendor mask data from the file. + + Specifically working on Reader Requirements box. + + Parameters + ---------- + fptr : file object + File object for JP2K file. + mask_length : int + Length of vendor mask flag + """ + # The mask length tells us the format string to use when unpacking + # from the buffer read from file. + mask_format = {1: 'B', 2: 'H', 4: 'I'}[mask_length] + + read_buffer = fptr.read(2) + num_vendor_features, = struct.unpack('>H', read_buffer) + + # Each vendor feature consists of a 16-byte UUID plus a mask whose + # length is specified by, you guessed it, "mask_length". + entry_length = 16 + mask_length + read_buffer = fptr.read(num_vendor_features * entry_length) + vendor_feature = [] + vendor_mask = [] + for j in range(num_vendor_features): + ubuffer = read_buffer[j * entry_length:(j + 1) * entry_length] + vendor_feature.append(uuid.UUID(bytes=ubuffer[0:16])) + + vmask = struct.unpack('>' + mask_format, ubuffer[16:]) + vendor_mask.append(vmask) + + return vendor_feature, vendor_mask + + class ResolutionBox(Jp2kBox): """Container for Resolution superbox information. @@ -1577,7 +1637,7 @@ class CaptureResolutionBox(Jp2kBox): @staticmethod def parse(fptr, offset, length): - """Parse Resolution box. + """Parse CaptureResolutionBox. Parameters ---------- @@ -1634,7 +1694,7 @@ class DisplayResolutionBox(Jp2kBox): @staticmethod def parse(fptr, offset, length): - """Parse Resolution box. + """Parse display resolution box. Parameters ---------- @@ -1789,16 +1849,33 @@ class XMLBox(Jp2kBox): """ num_bytes = offset + length - fptr.tell() read_buffer = fptr.read(num_bytes) - text = read_buffer.decode('utf-8') + try: + text = read_buffer.decode('utf-8') + except UnicodeDecodeError as ude: + # Possibly bad string of bytes to begin with. + # Try to search for -1: + text = read_buffer[decl_start:].decode('utf-8') + else: + raise - # Strip out any trailing nulls. - text = text.rstrip('\0') + # 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) + warnings.warn(msg, UserWarning) + + + # Strip out any trailing nulls, as they can foul up XML parsing. + text = text.rstrip(chr(0)) try: - elt = ET.fromstring(text) + elt = ET.fromstring(text.encode('utf-8')) xml = ET.ElementTree(elt) except ParseError as parse_error: - msg = 'A problem was encountered while parsing an XML box: "{0}"' + msg = 'A problem was encountered while parsing an XML box:' + msg += '\n\n\t"{0}"\n\nNo XML was retrieved.' msg = msg.format(str(parse_error)) warnings.warn(msg, UserWarning) xml = None @@ -1968,7 +2045,7 @@ class DataEntryURLBox(Jp2kBox): @staticmethod def parse(fptr, offset, length): - """Parse Data Entry URL box. + """Parse data entry URL box. Parameters ---------- @@ -2670,9 +2747,18 @@ def _pretty_print_xml(xml, level=0): """ xml = copy.deepcopy(xml) _indent(xml.getroot(), level=level) - xmltext = ET.tostring(xml.getroot()).decode('utf-8') + xmltext = ET.tostring(xml.getroot(), encoding='utf-8').decode('utf-8') # Indent it a bit. lst = [(' ' + x) for x in xmltext.split('\n')] - xml = '\n'.join(lst) - return '\n{0}'.format(xml) + try: + xml = '\n'.join(lst) + return '\n{0}'.format(xml) + except UnicodeEncodeError: + # This can happen on python 2.x if the character set contains certain + # non-ascii characters. Just print out the corresponding xml char + # entities instead. + xml = u'\n'.join(lst) + text = u'\n{0}'.format(xml) + text = text.encode('ascii', 'xmlcharrefreplace') + return text diff --git a/glymur/jp2dump.py b/glymur/jp2dump.py index 0e751e2..c0f0f7f 100644 --- a/glymur/jp2dump.py +++ b/glymur/jp2dump.py @@ -1,6 +1,7 @@ """ Entry point for jp2dump script. """ +import warnings from .jp2k import Jp2k @@ -15,8 +16,21 @@ def jp2dump(filename, codestream=False): codestream : optional, logical scalar Whether or not to dump codestream contents. """ - j = Jp2k(filename) - if codestream: - print(j.get_codestream(header_only=False)) - else: - print(j) + with warnings.catch_warnings(record=True) as wctx: + + # JP2 metadata can be extensive, so don't print any warnings until we + # are done with the metadata. + j = Jp2k(filename) + if codestream: + print(j.get_codestream(header_only=False)) + else: + print(j) + + # Re-emit any warnings that may have been suppressed. + if len(wctx) > 0: + print("\n") + for warning in wctx: + print("{0}:{1}: {2}: {3}".format(warning.filename, + warning.lineno, + warning.category.__name__, + warning.message)) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 8dba499..1bd36a4 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -1,15 +1,21 @@ -"""Access to JPEG2000 files. +"""This file is part of glymur, a Python interface for accessing JPEG 2000. + +http://glymur.readthedocs.org + +Copyright 2013 John Evans License: MIT """ -# pylint: disable=C0302 - import sys + +# Exitstack not found in contextlib in 2.7 +# pylint: disable=E0611 if sys.hexversion >= 0x03030000: from contextlib import ExitStack else: from contextlib2 import ExitStack + import ctypes import math import os @@ -19,67 +25,17 @@ import warnings import numpy as np from .codestream import Codestream -from .core import SRGB -from .core import GREYSCALE +from .core import SRGB, GREYSCALE from .core import PROGRESSION_ORDER from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE from .jp2box import Jp2kBox -from .jp2box import JPEG2000SignatureBox -from .jp2box import FileTypeBox -from .jp2box import JP2HeaderBox -from .jp2box import ContiguousCodestreamBox +from .jp2box import JPEG2000SignatureBox, FileTypeBox, JP2HeaderBox +from .jp2box import ColourSpecificationBox, ContiguousCodestreamBox from .jp2box import ImageHeaderBox -from .jp2box import ColourSpecificationBox -from .lib import _openjpeg as _opj -from .lib import _openjp2 as _opj2 -from .lib import c as _libc - -# Need to known if openjp2 library is the officially release v2.0.0 or not. -_OPENJP2_IS_OFFICIAL_V2 = False -if _opj2.OPENJP2 is not None: - if _opj2.version() == '2.0.0': - if not hasattr(_opj2.OPENJP2, - 'opj_stream_create_default_file_stream_v3'): - _OPENJP2_IS_OFFICIAL_V2 = True - -_COLORSPACE_MAP = {'rgb': _opj2.CLRSPC_SRGB, - 'gray': _opj2.CLRSPC_GRAY, - 'grey': _opj2.CLRSPC_GRAY, - 'ycc': _opj2.CLRSPC_YCC} - -# Setup the default callback handlers. See the callback functions subsection -# in the ctypes section of the Python documentation for a solid explanation of -# what's going on here. -_CMPFUNC = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p) - - -def _default_error_handler(msg, _): - """Default error handler callback for openjpeg library.""" - msg = "OpenJPEG library error: {0}".format(msg.decode('utf-8').rstrip()) - _opj2.set_error_message(msg) - - -def _default_info_handler(msg, _): - """Default info handler callback for openjpeg library.""" - print("[INFO] {0}".format(msg.decode('utf-8').rstrip())) - - -def _default_warning_handler(library_msg, _): - """Default warning handler callback for openjpeg library.""" - library_msg = library_msg.decode('utf-8').rstrip() - msg = "OpenJPEG library warning: {0}".format(library_msg) - warnings.warn(msg) - -_ERROR_CALLBACK = _CMPFUNC(_default_error_handler) -_INFO_CALLBACK = _CMPFUNC(_default_info_handler) -_WARNING_CALLBACK = _CMPFUNC(_default_warning_handler) - - -class LibraryNotFoundError(IOError): - """Raised if functionality is requested without the necessary library. - """ - def __init__(self, msg): - IOError.__init__(self, msg) +from .lib import openjpeg as opj +from .lib import openjp2 as opj2 +from . import version +from .lib import c as libc class Jp2k(Jp2kBox): @@ -143,12 +99,12 @@ class Jp2k(Jp2kBox): read_buffer = fptr.read(2) signature, = struct.unpack('>H', read_buffer) if signature == 0xff4f: - self._codec_format = _opj2.CODEC_J2K + self._codec_format = opj2.CODEC_J2K # That's it, we're done. The codestream object is only # produced upon explicit request. return - self._codec_format = _opj2.CODEC_JP2 + self._codec_format = opj2.CODEC_JP2 # Should be JP2. # First 4 bytes should be 12, the length of the 'jP ' box. @@ -187,99 +143,181 @@ class Jp2k(Jp2kBox): msg += "profile if the file type box brand is 'jp2 '." warnings.warn(msg) - def _validate_write_parameters(self, img_array, code_block_size, - precinct_sizes, cratios, psnr, colorspace, - codec_fmt): - """Check that the input parameters to the write function are valid. + def _populate_cparams(self, **kwargs): + """Populate compression parameters structure from input arguments. + + Parameters + ---------- + cbsize : tuple, optional + Code block size (DY, DX). + cratios : iterable + Compression ratios for successive layers. + eph : bool, optional + 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) + 2 = RESET + 4 = RESTART(TERMALL) + 8 = VSC + 16 = ERTERM(SEGTERM) + 32 = SEGMARK(SEGSYM) + numres : int, optional + Number of resolutions. + prog : str, optional + Progression order, one of "LRCP" "RLCP", "RPCL", "PCRL", "CPRL". + psnr : iterable, optional + Different PSNR for successive layers. + psizes : list, optional + List of precinct sizes. Each precinct size tuple is defined in + (height x width). + sop : bool, optional + If true, write SOP marker before each packet. + subsam : tuple, optional + Subsampling factors (dy, dx). + tilesize : tuple, optional + Numeric tuple specifying tile size in terms of (numrows, numcols), + not (X, Y). + + Returns + ------- + cparams : CompressionParametersType(ctypes.Structure) + Corresponds to cparameters_t type in openjp2 headers. + """ + if version.openjpeg_version_tuple[0] == 1: + cparams = opj.set_default_encoder_parameters() + else: + cparams = opj2.set_default_encoder_parameters() + + outfile = self.filename.encode() + num_pad_bytes = opj2.PATH_LEN - len(outfile) + outfile += b'0' * num_pad_bytes + cparams.outfile = outfile + + if self.filename[-4:].endswith(('.jp2', '.JP2')): + cparams.codec_fmt = opj2.CODEC_JP2 + else: + cparams.codec_fmt = opj2.CODEC_J2K + + # Set defaults to lossless to begin. + cparams.tcp_rates[0] = 0 + cparams.tcp_numlayers = 1 + cparams.cp_disto_alloc = 1 + + if 'cbsize' in kwargs: + cparams.cblockw_init = kwargs['cbsize'][1] + cparams.cblockh_init = kwargs['cbsize'][0] + + if 'cratios' in kwargs: + cparams.tcp_numlayers = len(kwargs['cratios']) + for j, cratio in enumerate(kwargs['cratios']): + cparams.tcp_rates[j] = cratio + cparams.cp_disto_alloc = 1 + + if 'eph' in kwargs: + cparams.csty |= 0x04 + + if 'grid_offset' in kwargs: + cparams.image_offset_x0 = kwargs['grid_offset'][1] + cparams.image_offset_y0 = kwargs['grid_offset'][0] + + if 'modesw' in kwargs: + for shift in range(6): + power_of_two = 1 << shift + if kwargs['modesw'] & power_of_two: + cparams.mode |= power_of_two + + if 'numres' in kwargs: + cparams.numresolution = kwargs['numres'] + + if 'prog' in kwargs: + prog = kwargs['prog'].upper() + cparams.prog_order = PROGRESSION_ORDER[prog] + + if 'psnr' in kwargs: + cparams.tcp_numlayers = len(kwargs['psnr']) + for j, snr_layer in enumerate(kwargs['psnr']): + cparams.tcp_distoratio[j] = snr_layer + cparams.cp_fixed_quality = 1 + + if 'psizes' in kwargs: + for j, (prch, prcw) in enumerate(kwargs['psizes']): + cparams.prcw_init[j] = prcw + cparams.prch_init[j] = prch + cparams.csty |= 0x01 + cparams.res_spec = len(kwargs['psizes']) + + if 'sop' in kwargs: + cparams.csty |= 0x02 + + if 'subsam' in kwargs: + cparams.subsampling_dy = kwargs['subsam'][0] + cparams.subsampling_dx = kwargs['subsam'][1] + + if 'tilesize' in kwargs: + cparams.cp_tdx = kwargs['tilesize'][1] + cparams.cp_tdy = kwargs['tilesize'][0] + cparams.tile_size_on = opj2.TRUE + + return cparams + + def _process_write_inputs(self, img_array, colorspace=None, **kwargs): + """Directs processing of write method arguments. + + It's somewhat awkward to process all the kwargs arguments at once. + The "colorspace" is not a parameter that gets processed into the + compression parameters structure, and it unfortunately must be handled + in the middle of the compression parameter processing. Parameters ---------- img_array : ndarray Image data to be written to file. - code_block_size : tuple - Code block size (DY, DX). - precinct_sizes : list - List of precinct sizes. Each precinct size tuple is defined in - (height x width). - cratios : iterable - Compression ratios for successive layers. - psnr : iterable - Different PSNR for successive layers. - mct : bool - Specifies usage of the multi component transform. If not - specified, defaults to True if the colorspace is RGB. colorspace : str, optional Either 'rgb' or 'gray'. - codec_fmt : int - Are we writing a JP2 file or a J2K file? + + Returns + ------- + cparams : CompressionParametersType(ctypes.Structure) + Corresponds to cparameters_t type in openjp2 headers. + colorspace : int + Either CLRSPC_SRGB or CLRSPC_GRAY """ - # Validate code block size and precinct sizes. - if code_block_size is not None: - width = code_block_size[1] - height = code_block_size[0] - if height * width > 4096 or height < 4 or width < 4: - msg = "Code block area cannot exceed 4096. " - msg += "Code block height and width must be larger than 4." - raise IOError(msg) - if ((math.log(height, 2) != math.floor(math.log(height, 2)) or - math.log(width, 2) != math.floor(math.log(width, 2)))): - msg = "Bad code block size ({0}, {1}), " - msg += "must be powers of 2." - raise IOError(msg.format(height, width)) - if precinct_sizes is not None: - for j, (prch, prcw) in enumerate(precinct_sizes): - if j == 0 and code_block_size is not None: - cblkh, cblkw = code_block_size - if cblkh * 2 > prch or cblkw * 2 > prcw: - msg = "Highest Resolution precinct size must be at " - msg += "least twice that of the code block dimensions." - raise IOError(msg) - if ((math.log(prch, 2) != math.floor(math.log(prch, 2)) or - math.log(prcw, 2) != math.floor(math.log(prcw, 2)))): - msg = "Bad precinct sizes ({0}, {1}), " - msg += "must be powers of 2." - raise IOError(msg.format(prch, prcw)) - - if cratios is not None and psnr is not None: + if 'cratios' in kwargs and 'psnr' in kwargs: msg = "Cannot specify cratios and psnr together." raise IOError(msg) - # What would the point of 1D images be? - if img_array.ndim == 1 or img_array.ndim > 3: - msg = "{0}D imagery is not allowed.".format(img_array.ndim) - raise IOError(msg) + cparams = self._populate_cparams(**kwargs) + _validate_compression_params(img_array, cparams) - if _OPENJP2_IS_OFFICIAL_V2: - if (((img_array.ndim != 2) and - (img_array.shape[2] != 1 and img_array.shape[2] != 3))): - msg = "Writing images is restricted to single-channel " - msg += "greyscale images or three-channel RGB images when " - msg += "the OpenJPEG library version is the official 2.0.0 " - msg += "release." - raise IOError(msg) + colorspace = _unpack_colorspace(colorspace, img_array, cparams) - if colorspace is not None: - if codec_fmt == _opj2.CODEC_J2K: - msg = 'Do not specify a colorspace when writing a raw ' - msg += 'codestream.' - raise IOError(msg) - if colorspace.lower() not in ('rgb', 'grey', 'gray'): - msg = 'Invalid colorspace "{0}"'.format(colorspace) - raise IOError(msg) - elif colorspace.lower() == 'rgb' and img_array.shape[2] < 3: - msg = 'RGB colorspace requires at least 3 components.' + try: + mct = kwargs['mct'] + if mct and colorspace == opj2.CLRSPC_GRAY: + # Cannot check for this in the validate routine, as we need + # to know what the target colorspace has been determined to be. + msg = "Cannot specify usage of the multi component transform " + msg += "if the colorspace is gray." raise IOError(msg) + cparams.tcp_mct = 1 if mct else 0 + except KeyError: + # If the multi component transform was not specified, we infer + # that it should be used if the color space is RGB. + if colorspace == opj2.CLRSPC_SRGB: + cparams.tcp_mct = 1 + else: + cparams.tcp_mct = 0 - if img_array.dtype != np.uint8 and img_array.dtype != np.uint16: - msg = "Only uint8 and uint16 images are currently supported." - raise RuntimeError(msg) + return cparams, colorspace - # pylint: disable-msg=W0221 - def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None, - cbsize=None, psizes=None, grid_offset=None, sop=False, - subsam=None, tilesize=None, prog=None, modesw=None, - colorspace=None, verbose=False, mct=None): + def write(self, img_array, verbose=False, **kwargs): """Write image data to a JP2/JPX/J2k file. Intended usage of the various parameters follows that of OpenJPEG's opj_compress utility. @@ -290,11 +328,6 @@ class Jp2k(Jp2kBox): ---------- img_array : ndarray Image data to be written to file. - callbacks : bool, optional - If true, enable default info handler such that INFO messages - produced by the OpenJPEG library are output to the console. By - default, OpenJPEG warning and error messages are captured by - Python's own warning and error mechanisms. cbsize : tuple, optional Code block size (DY, DX). colorspace : str, optional @@ -351,197 +384,166 @@ class Jp2k(Jp2kBox): glymur.LibraryNotFoundError If glymur is unable to load the openjp2 library. """ - if _opj2.OPENJP2 is None: - raise LibraryNotFoundError("You must have the openjp2 library " - "installed before using this " + if opj2.OPENJP2 is not None: + self._write_openjp2(img_array, verbose=verbose, **kwargs) + elif opj.OPENJPEG is not None: + self._write_openjpeg(img_array, verbose=verbose, **kwargs) + else: + raise LibraryNotFoundError("You must have at least version 1.5 of " + "OpenJPEG before using this " "functionality.") - if self.filename[-4:].lower() == '.jp2': - codec_fmt = _opj2.CODEC_JP2 - else: - codec_fmt = _opj2.CODEC_J2K - - self._validate_write_parameters(img_array, cbsize, psizes, cratios, - psnr, colorspace, codec_fmt) - - cparams = _opj2.set_default_encoder_parameters() - - outfile = self.filename.encode() - num_pad_bytes = _opj2.PATH_LEN - len(outfile) - outfile += b'0' * num_pad_bytes - cparams.outfile = outfile - - cparams.cod_format = codec_fmt - - # Set defaults to lossless to begin. - cparams.tcp_rates[0] = 0 - cparams.tcp_numlayers = 1 - cparams.cp_disto_alloc = 1 - - if cbsize is not None: - width = cbsize[1] - height = cbsize[0] - cparams.cblockw_init = width - cparams.cblockh_init = height - - if cratios is not None: - cparams.tcp_numlayers = len(cratios) - for j, cratio in enumerate(cratios): - cparams.tcp_rates[j] = cratio - cparams.cp_disto_alloc = 1 - - if eph: - cparams.csty |= 0x04 - - if grid_offset is not None: - cparams.image_offset_x0 = grid_offset[1] - cparams.image_offset_y0 = grid_offset[0] - - if modesw is not None: - for shift in range(6): - power_of_two = 1 << shift - if modesw & power_of_two: - cparams.mode |= power_of_two - - if numres is not None: - cparams.numresolution = numres - - if prog is not None: - prog = prog.upper() - cparams.prog_order = PROGRESSION_ORDER[prog] - - if psnr is not None: - cparams.tcp_numlayers = len(psnr) - for j, snr_layer in enumerate(psnr): - cparams.tcp_distoratio[j] = snr_layer - cparams.cp_fixed_quality = 1 - - if psizes is not None: - for j, (prch, prcw) in enumerate(psizes): - cparams.prcw_init[j] = prcw - cparams.prch_init[j] = prch - cparams.csty |= 0x01 - cparams.res_spec = len(psizes) - - if sop: - cparams.csty |= 0x02 - - if subsam is not None: - cparams.subsampling_dy = subsam[0] - cparams.subsampling_dx = subsam[1] - - if tilesize is not None: - cparams.cp_tdx = tilesize[1] - cparams.cp_tdy = tilesize[0] - cparams.tile_size_on = _opj2.TRUE + def _write_openjpeg(self, img_array, verbose=False, **kwargs): + """ + Write JPEG 2000 file using OpenJPEG 1.5 interface. + """ + cparams, colorspace = self._process_write_inputs(img_array, **kwargs) if img_array.ndim == 2: - # Force it to be 3D. Just makes things easier later on. + # Force the image to be 3D. Just makes things easier later on. + img_array = img_array.reshape(img_array.shape[0], + img_array.shape[1], + 1) + + comptparms = _populate_comptparms(img_array, cparams) + + with ExitStack() as stack: + image = opj.image_create(comptparms, colorspace) + stack.callback(opj.image_destroy, image) + + numrows, numcols, numlayers = img_array.shape + + # set image offset and reference grid + image.contents.x0 = cparams.image_offset_x0 + image.contents.y0 = cparams.image_offset_y0 + image.contents.x1 = image.contents.x0 \ + + (numcols - 1) * cparams.subsampling_dx + 1 + image.contents.y1 = image.contents.y0 \ + + (numrows - 1) * cparams.subsampling_dy + 1 + + # Stage the image data to the openjpeg data structure. + for k in range(0, numlayers): + layer = np.ascontiguousarray(img_array[:, :, k], + dtype=np.int32) + dest = image.contents.comps[k].data + src = layer.ctypes.data + ctypes.memmove(dest, src, layer.nbytes) + + cinfo = opj.create_compress(cparams.codec_fmt) + stack.callback(opj.destroy_compress, cinfo) + + # Setup the info, warning, and error handlers. + # Always use the warning and error handler. Use of an info + # handler is optional. + event_mgr = opj.EventMgrType() + _info_handler = _INFO_CALLBACK if verbose else None + event_mgr.info_handler = _info_handler + event_mgr.warning_handler = ctypes.cast(_WARNING_CALLBACK, + ctypes.c_void_p) + event_mgr.error_handler = ctypes.cast(_ERROR_CALLBACK, + ctypes.c_void_p) + + opj.setup_encoder(cinfo, ctypes.byref(cparams), image) + + cio = opj.cio_open(cinfo) + stack.callback(opj.cio_close, cio) + + if not opj.encode(cinfo, cio, image): + raise IOError("Encode error.") + + pos = opj.cio_tell(cio) + + blob = ctypes.string_at(cio.contents.buffer, pos) + fptr = open(self.filename, 'wb') + stack.callback(fptr.close) + fptr.write(blob) + + self.parse() + + + def _write_openjp2(self, img_array, verbose=False, **kwargs): + """ + Write JPEG 2000 file using OpenJPEG 1.5 interface. + """ + cparams, colorspace = self._process_write_inputs(img_array, **kwargs) + + if img_array.ndim == 2: + # Force the image to be 3D. Just makes things easier later on. numrows, numcols = img_array.shape img_array = img_array.reshape(numrows, numcols, 1) - numrows, numcols, num_comps = img_array.shape + comptparms = _populate_comptparms(img_array, cparams) - if colorspace is None: - # Must infer the colorspace from the image dimensions. - if img_array.shape[2] == 1 or img_array.shape[2] == 2: - # A single channel image or an image with two channels is going - # to be greyscale. - colorspace = _opj2.CLRSPC_GRAY + with ExitStack() as stack: + image = opj2.image_create(comptparms, colorspace) + stack.callback(opj2.image_destroy, image) + + _populate_image_struct(cparams, image, img_array) + + codec = opj2.create_compress(cparams.codec_fmt) + stack.callback(opj2.destroy_codec, codec) + + info_handler = _INFO_CALLBACK if verbose else None + opj2.set_info_handler(codec, info_handler) + opj2.set_warning_handler(codec, _WARNING_CALLBACK) + opj2.set_error_handler(codec, _ERROR_CALLBACK) + + opj2.setup_encoder(codec, cparams, image) + + if _OPENJP2_IS_OFFICIAL_V2: + fptr = libc.fopen(self.filename, 'wb') + strm = opj2.stream_create_default_file_stream(fptr, False) + stack.callback(opj2.stream_destroy, strm) + stack.callback(libc.fclose, fptr) else: - # Anything else must be RGB, right? - colorspace = _opj2.CLRSPC_SRGB - else: - # Turn the colorspace from a string to the enumerated value that - # the library expects. - colorspace = _COLORSPACE_MAP[colorspace.lower()] - - if mct is None: - # If the multi component transform was not specified, we infer - # that it should be used if the color space is RGB. - if colorspace == _opj2.CLRSPC_SRGB: - cparams.tcp_mct = 1 - else: - cparams.tcp_mct = 0 - else: - if mct and colorspace == _opj2.CLRSPC_GRAY: - # Cannot check for this in the validate routine, as we need - # to know what the target colorspace has been determined to be. - msg = "Cannot specify usage of the multi component transform " - msg += "if the colorspace is gray." - raise IOError(msg) - cparams.tcp_mct = 1 if mct else 0 - - if img_array.dtype == np.uint8: - comp_prec = 8 - else: - # We already know it cannot be anything else than uint16. - comp_prec = 16 - - comptparms = (_opj2.ImageComptParmType * num_comps)() - for j in range(num_comps): - comptparms[j].dx = cparams.subsampling_dx - comptparms[j].dy = cparams.subsampling_dy - comptparms[j].w = numcols - comptparms[j].h = numrows - comptparms[j].x0 = cparams.image_offset_x0 - comptparms[j].y0 = cparams.image_offset_y0 - comptparms[j].prec = comp_prec - comptparms[j].bpp = comp_prec - comptparms[j].sgnd = 0 - - image = _opj2.image_create(comptparms, colorspace) - - # set image offset and reference grid - image.contents.x0 = cparams.image_offset_x0 - image.contents.y0 = cparams.image_offset_y0 - image.contents.x1 = (image.contents.x0 + - (numcols - 1) * cparams.subsampling_dx + 1) - image.contents.y1 = (image.contents.y0 + - (numrows - 1) * cparams.subsampling_dy + 1) - - # Stage the image data to the openjpeg data structure. - for k in range(0, num_comps): - layer = np.ascontiguousarray(img_array[:, :, k], dtype=np.int32) - dest = image.contents.comps[k].data - src = layer.ctypes.data - ctypes.memmove(dest, src, layer.nbytes) - - codec = _opj2.create_compress(codec_fmt) - - if verbose: - _opj2.set_info_handler(codec, _INFO_CALLBACK) - else: - _opj2.set_info_handler(codec, None) - - _opj2.set_warning_handler(codec, _WARNING_CALLBACK) - _opj2.set_error_handler(codec, _ERROR_CALLBACK) - _opj2.setup_encoder(codec, cparams, image) - - if _OPENJP2_IS_OFFICIAL_V2: - fptr = _libc.fopen(self.filename, 'wb') - strm = _opj2.stream_create_default_file_stream(fptr, False) - else: - strm = _opj2.stream_create_default_file_stream_v3(self.filename, - False) - - # Start to clean up after ourselves. - _opj2.start_compress(codec, image, strm) - _opj2.encode(codec, strm) - _opj2.end_compress(codec, strm) - - if _OPENJP2_IS_OFFICIAL_V2: - _opj2.stream_destroy(strm) - _libc.fclose(fptr) - else: - _opj2.stream_destroy_v3(strm) - - _opj2.destroy_codec(codec) - _opj2.image_destroy(image) - + # This routine introduced in 2.0 devel series. + strm = opj2.stream_create_default_file_stream_v3(self.filename, + False) + stack.callback(opj2.stream_destroy_v3, strm) + + opj2.start_compress(codec, image, strm) + opj2.encode(codec, strm) + opj2.end_compress(codec, strm) + # Refresh the metadata. self.parse() + def append(self, box): + """Append a JP2 box to the file in-place. + + Parameters + ---------- + box : Jp2Box + Instance of a JP2 box. Currently only XML boxes are allowed. + """ + if self._codec_format == opj2.CODEC_J2K: + msg = "Only JP2 files can currently have boxes appended to them." + raise IOError(msg) + + if box.box_id != 'xml ': + raise IOError("Only XML boxes can currently be appended.") + + # Check the last box. If the length field is zero, then rewrite + # the length field to reflect the true length of the box. + with open(self.filename, 'rb') as ifile: + offset = self.box[-1].offset + ifile.seek(offset) + read_buffer = ifile.read(4) + box_length, = struct.unpack('>I', read_buffer) + if box_length == 0: + # Reopen the file in write mode and rewrite the length field. + true_box_length = os.path.getsize(ifile.name) - offset + with open(self.filename, 'r+b') as ofile: + ofile.seek(offset) + write_buffer = struct.pack('>I', true_box_length) + ofile.write(write_buffer) + + # Can now safely append the box. + with open(self.filename, 'ab') as ofile: + box.write(ofile) + + self.parse() + def wrap(self, filename, boxes=None): """Write the codestream back out to file, wrapped in new JP2 jacket. @@ -582,72 +584,7 @@ class Jp2k(Jp2kBox): 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].box_id != 'jP ' or boxes[1].box_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.box_id == 'jp2h'] - jp2h_idx = jp2h_lst[0] - jp2c_lst = [idx for (idx, box) in enumerate(boxes) - if box.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].box_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. - colr_lst = [j for (j, box) in enumerate(jp2h.box) - if box.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.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.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 - 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) + _validate_jp2_box_sequence(boxes) with open(filename, 'wb') as ofile: for box in boxes: @@ -729,9 +666,9 @@ class Jp2k(Jp2kBox): >>> thumbnail.shape (728, 1296, 3) """ - if _opj2.OPENJP2 is not None: + if opj2.OPENJP2 is not None: img = self._read_openjp2(**kwargs) - elif _opj.OPENJPEG is not None: + elif opj.OPENJPEG is not None: img = self._read_openjpeg(**kwargs) else: raise LibraryNotFoundError("You must have either a recent version " @@ -740,6 +677,18 @@ class Jp2k(Jp2kBox): "using this functionality.") return img + def _subsampling_sanity_check(self): + """Check for differing subsample factors. + """ + codestream = self.get_codestream(header_only=True) + dxs = np.array(codestream.segment[1].xrsiz) + dys = np.array(codestream.segment[1].yrsiz) + if np.any(dxs - dxs[0]) or np.any(dys - dys[0]): + msg = "Components must all have the same subsampling factors " + msg += "to use this method. Please consider using OPENJP2 and " + msg += "the read_bands method instead." + raise RuntimeError(msg) + def _read_openjpeg(self, rlevel=0, verbose=False): """Read a JPEG 2000 image using libopenjpeg. @@ -761,96 +710,65 @@ class Jp2k(Jp2kBox): RuntimeError If the image has differing subsample factors. """ - # Check for differing subsample factors. - codestream = self.get_codestream(header_only=True) - dxs = np.array(codestream.segment[1].xrsiz) - dys = np.array(codestream.segment[1].yrsiz) - if np.any(dxs - dxs[0]) or np.any(dys - dys[0]): - msg = "Components must all have the same subsampling factors " - msg += "to use this method with OpenJPEG 1.5.1. Please consider " - msg += "using OPENJP2 instead." - raise RuntimeError(msg) + 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) with ExitStack() as stack: - # Set decoding parameters. - dparameters = _opj.DecompressionParametersType() - _opj.set_default_decoder_parameters(ctypes.byref(dparameters)) - dparameters.cp_reduce = rlevel - dparameters.decod_format = self._codec_format + 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 + infile = self.filename.encode() + nelts = opj.PATH_LEN - len(infile) + infile += b'0' * nelts + dparameters.infile = infile - dinfo = _opj.create_decompress(dparameters.decod_format) + dinfo = opj.create_decompress(dparameters.decod_format) - event_mgr = _opj.EventMgrType() - info_handler = ctypes.cast(_INFO_CALLBACK, ctypes.c_void_p) - event_mgr.info_handler = info_handler if verbose else None - event_mgr.warning_handler = ctypes.cast(_WARNING_CALLBACK, - ctypes.c_void_p) - event_mgr.error_handler = ctypes.cast(_ERROR_CALLBACK, - ctypes.c_void_p) - _opj.set_event_mgr(dinfo, ctypes.byref(event_mgr)) + event_mgr = opj.EventMgrType() + info_handler = ctypes.cast(_INFO_CALLBACK, ctypes.c_void_p) + event_mgr.info_handler = info_handler if verbose else None + event_mgr.warning_handler = ctypes.cast(_WARNING_CALLBACK, + ctypes.c_void_p) + event_mgr.error_handler = ctypes.cast(_ERROR_CALLBACK, + ctypes.c_void_p) + opj.set_event_mgr(dinfo, ctypes.byref(event_mgr)) - _opj.setup_decoder(dinfo, dparameters) + opj.setup_decoder(dinfo, dparameters) - with open(self.filename, 'rb') as fptr: - src = fptr.read() - cio = _opj.cio_open(dinfo, src) + with open(self.filename, 'rb') as fptr: + src = fptr.read() + cio = opj.cio_open(dinfo, src) - image = _opj.decode(dinfo, cio) + image = opj.decode(dinfo, cio) - stack.callback(_opj.image_destroy, image) - stack.callback(_opj.destroy_decompress, dinfo) - stack.callback(_opj.cio_close, cio) + stack.callback(opj.image_destroy, image) + stack.callback(opj.destroy_decompress, dinfo) + stack.callback(opj.cio_close, cio) - ncomps = image.contents.numcomps - component = image.contents.comps[0] - if component.sgnd: - if component.prec <= 8: - dtype = np.int8 - elif component.prec <= 16: - dtype = np.int16 - else: - raise RuntimeError("Unhandled precision, datatype") - else: - if component.prec <= 8: - dtype = np.uint8 - elif component.prec <= 16: - dtype = np.uint16 - else: - raise RuntimeError("Unhandled precision, datatype") + data = extract_image_cube(image) - nrows = image.contents.comps[0].h - ncols = image.contents.comps[0].w - ncomps = image.contents.numcomps - data = np.zeros((nrows, ncols, ncomps), dtype) - - for k in range(image.contents.numcomps): - component = image.contents.comps[k] - nrows = component.h - ncols = component.w - - if nrows == 0 or ncols == 0: - # Letting this situation continue would segfault - # Python. - msg = "Component {0} has dimensions {1} x {2}" - msg = msg.format(k, nrows, ncols) - raise IOError(msg) - - addr = ctypes.addressof(component.data.contents) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - nelts = nrows * ncols - band = np.ctypeslib.as_array( - (ctypes.c_int32 * nelts).from_address(addr)) - data[:, :, k] = np.reshape(band.astype(dtype), - (nrows, ncols)) + except ValueError: + opj2.check_error(0) if data.shape[2] == 1: - data = data.view() + # The third dimension has just a single layer. Make the image + # data 2D instead of 3D. data.shape = data.shape[0:2] return data @@ -884,30 +802,54 @@ class Jp2k(Jp2kBox): RuntimeError If the image has differing subsample factors. """ - # Check for differing subsample factors. - codestream = self.get_codestream(header_only=True) - dxs = np.array(codestream.segment[1].xrsiz) - dys = np.array(codestream.segment[1].yrsiz) - if np.any(dxs - dxs[0]) or np.any(dys - dys[0]): - msg = "Components must all have the same subsampling factors." - raise RuntimeError(msg) + self._subsampling_sanity_check() - img_array = self._read_common(rlevel=rlevel, - layer=layer, - area=area, - tile=tile, - verbose=verbose, - as_bands=False) + dparam = self._populate_dparam(layer, rlevel, area, tile) + + with ExitStack() as stack: + if hasattr(opj2.OPENJP2, + 'opj_stream_create_default_file_stream_v3'): + filename = self.filename + stream = opj2.stream_create_default_file_stream_v3(filename, + True) + stack.callback(opj2.stream_destroy_v3, stream) + else: + fptr = libc.fopen(self.filename, 'rb') + stack.callback(libc.fclose, fptr) + stream = opj2.stream_create_default_file_stream(fptr, True) + stack.callback(opj2.stream_destroy, stream) + codec = opj2.create_decompress(self._codec_format) + stack.callback(opj2.destroy_codec, codec) + + opj2.set_error_handler(codec, _ERROR_CALLBACK) + opj2.set_warning_handler(codec, _WARNING_CALLBACK) + if verbose: + opj2.set_info_handler(codec, _INFO_CALLBACK) + else: + opj2.set_info_handler(codec, None) + + opj2.setup_decoder(codec, dparam) + image = opj2.read_header(stream, codec) + stack.callback(opj2.image_destroy, image) + + if dparam.nb_tile_to_decode: + opj2.get_decoded_tile(codec, stream, image, dparam.tile_index) + else: + opj2.set_decode_area(codec, image, + dparam.DA_x0, dparam.DA_y0, + dparam.DA_x1, dparam.DA_y1) + opj2.decode(codec, stream, image) + opj2.end_decompress(codec, stream) + + img_array = extract_image_cube(image) if img_array.shape[2] == 1: - img_array = img_array.view() img_array.shape = img_array.shape[0:2] return img_array - def _read_common(self, rlevel=0, layer=0, area=None, tile=None, - verbose=False, as_bands=False): - """Read a JPEG 2000 image. + def _populate_dparam(self, layer, rlevel, area, tile): + """Populate decompression structure with appropriate input parameters. Parameters ---------- @@ -920,20 +862,16 @@ class Jp2k(Jp2kBox): (first_row, first_col, last_row, last_col) tile : int, optional Number of tile to decode. - verbose : bool, optional - Print informational messages produced by the OpenJPEG library. - as_bands : bool, optional - If true, return the individual 2D components in a list. Returns ------- - img_array : ndarray - The individual image components or a single array. + dparam : DecompressionParametersType (ctypes) + Corresponds to openjp2 decompression parameters structure. """ - dparam = _opj2.set_default_decoder_parameters() + dparam = opj2.set_default_decoder_parameters() infile = self.filename.encode() - nelts = _opj2.PATH_LEN - len(infile) + nelts = opj2.PATH_LEN - len(infile) infile += b'0' * nelts dparam.infile = infile @@ -945,17 +883,13 @@ class Jp2k(Jp2kBox): # Get the lowest resolution thumbnail. codestream = self.get_codestream() rlevel = codestream.segment[2].spcod[4] - dparam.cp_reduce = rlevel + if area is not None: - if area[0] < 0 or area[1] < 0: - msg = "Upper left corner coordinates must be nonnegative: {0}" - msg = msg.format(area) - raise IOError(msg) - if area[2] <= 0 or area[3] <= 0: - msg = "Lower right corner coordinates must be positive: {0}" - msg = msg.format(area) - raise IOError(msg) + if area[0] < 0 or area[1] < 0 or area[2] <= 0 or area[3] <= 0: + msg = "Upper left corner coordinates must be nonnegative and " + msg += "lower right corner coordinates must be positive: {0}" + raise IOError(msg.format(area)) dparam.DA_y0 = area[0] dparam.DA_x0 = area[1] dparam.DA_y1 = area[2] @@ -965,87 +899,7 @@ class Jp2k(Jp2kBox): dparam.tile_index = tile dparam.nb_tile_to_decode = 1 - with ExitStack() as stack: - if hasattr(_opj2.OPENJP2, 'opj_stream_create_default_file_stream_v3'): - stream = _opj2.stream_create_default_file_stream_v3(self.filename, - True) - stack.callback(_opj2.stream_destroy_v3, stream) - else: - fptr = _libc.fopen(self.filename, 'rb') - stack.callback(_libc.fclose, fptr) - stream = _opj2.stream_create_default_file_stream(fptr, True) - stack.callback(_opj2.stream_destroy, stream) - codec = _opj2.create_decompress(self._codec_format) - stack.callback(_opj2.destroy_codec, codec) - - _opj2.set_error_handler(codec, _ERROR_CALLBACK) - _opj2.set_warning_handler(codec, _WARNING_CALLBACK) - if verbose: - _opj2.set_info_handler(codec, _INFO_CALLBACK) - else: - _opj2.set_info_handler(codec, None) - - _opj2.setup_decoder(codec, dparam) - image = _opj2.read_header(stream, codec) - stack.callback(_opj2.image_destroy, image) - - if dparam.nb_tile_to_decode: - _opj2.get_decoded_tile(codec, stream, image, dparam.tile_index) - else: - _opj2.set_decode_area(codec, image, - dparam.DA_x0, dparam.DA_y0, - dparam.DA_x1, dparam.DA_y1) - _opj2.decode(codec, stream, image) - _opj2.end_decompress(codec, stream) - - component = image.contents.comps[0] - if component.sgnd: - if component.prec <= 8: - dtype = np.int8 - elif component.prec <= 16: - dtype = np.int16 - else: - raise RuntimeError("Unhandled precision, datatype") - else: - if component.prec <= 8: - dtype = np.uint8 - elif component.prec <= 16: - dtype = np.uint16 - else: - raise RuntimeError("Unhandled precision, datatype") - - if as_bands: - data = [] - else: - nrows = image.contents.comps[0].h - ncols = image.contents.comps[0].w - ncomps = image.contents.numcomps - data = np.zeros((nrows, ncols, ncomps), dtype) - - for k in range(image.contents.numcomps): - component = image.contents.comps[k] - nrows = component.h - ncols = component.w - - if nrows == 0 or ncols == 0: - # Letting this situation continue would segfault - # Python. - msg = "Component {0} has dimensions {1} x {2}" - msg = msg.format(k, nrows, ncols) - raise IOError(msg) - - addr = ctypes.addressof(component.data.contents) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - band = np.ctypeslib.as_array( - (ctypes.c_int32 * nrows * ncols).from_address(addr)) - if as_bands: - data.append(np.reshape(band.astype(dtype), (nrows, ncols))) - else: - data[:, :, k] = np.reshape(band.astype(dtype), - (nrows, ncols)) - - return data + return dparam def read_bands(self, rlevel=0, layer=0, area=None, tile=None, verbose=False): @@ -1090,17 +944,49 @@ class Jp2k(Jp2kBox): glymur.LibraryNotFoundError If glymur is unable to load the openjp2 library. """ - if _opj2.OPENJP2 is None: - raise LibraryNotFoundError("You must have the development version " + if version.openjpeg_version_tuple[0] < 2: + raise LibraryNotFoundError("You must have at least version 2.0.0 " "of OpenJP2 installed before using " "this functionality.") - lst = self._read_common(rlevel=rlevel, - layer=layer, - area=area, - tile=tile, - verbose=verbose, - as_bands=True) + dparam = self._populate_dparam(layer, rlevel, area, tile) + + with ExitStack() as stack: + if hasattr(opj2.OPENJP2, + 'opj_stream_create_default_file_stream_v3'): + filename = self.filename + stream = opj2.stream_create_default_file_stream_v3(filename, + True) + stack.callback(opj2.stream_destroy_v3, stream) + else: + fptr = libc.fopen(self.filename, 'rb') + stack.callback(libc.fclose, fptr) + stream = opj2.stream_create_default_file_stream(fptr, True) + stack.callback(opj2.stream_destroy, stream) + codec = opj2.create_decompress(self._codec_format) + stack.callback(opj2.destroy_codec, codec) + + opj2.set_error_handler(codec, _ERROR_CALLBACK) + opj2.set_warning_handler(codec, _WARNING_CALLBACK) + if verbose: + opj2.set_info_handler(codec, _INFO_CALLBACK) + else: + opj2.set_info_handler(codec, None) + + opj2.setup_decoder(codec, dparam) + image = opj2.read_header(stream, codec) + stack.callback(opj2.image_destroy, image) + + if dparam.nb_tile_to_decode: + opj2.get_decoded_tile(codec, stream, image, dparam.tile_index) + else: + opj2.set_decode_area(codec, image, + dparam.DA_x0, dparam.DA_y0, + dparam.DA_x1, dparam.DA_y1) + opj2.decode(codec, stream, image) + opj2.end_decompress(codec, stream) + + lst = extract_image_bands(image) return lst @@ -1140,7 +1026,7 @@ class Jp2k(Jp2kBox): If the file is JPX with more than one codestream. """ with open(self.filename, 'rb') as fptr: - if self._codec_format == _opj2.CODEC_J2K: + if self._codec_format == opj2.CODEC_J2K: codestream = Codestream(fptr, self.length, header_only=header_only) else: @@ -1163,3 +1049,402 @@ class Jp2k(Jp2kBox): header_only=header_only) return codestream + + +def component2dtype(component): + """Take an OpenJPEG component structure and determine the numpy datatype. + + Parameters + ---------- + component : ctypes pointer to ImageCompType (image_comp_t) + single image component structure. + + Returns + ------- + dtype : builtins.type + numpy datatype to be used to construct an image array. + """ + if component.sgnd: + if component.prec <= 8: + dtype = np.int8 + elif component.prec <= 16: + dtype = np.int16 + else: + raise RuntimeError("Unhandled precision, datatype") + else: + if component.prec <= 8: + dtype = np.uint8 + elif component.prec <= 16: + dtype = np.uint16 + else: + raise RuntimeError("Unhandled precision, datatype") + + return dtype + + +def _validate_nonzero_image_size(nrows, ncols, component_index): + """The image cannot have area of zero. + """ + if nrows == 0 or ncols == 0: + # Letting this situation continue would segfault Python. + msg = "Component {0} has dimensions {1} x {2}" + msg = msg.format(component_index, nrows, ncols) + raise IOError(msg) + + +def _validate_jp2_box_sequence(boxes): + """Run through series of tests for JP2 box legality. + + This is non-exhaustive. + """ + # Check for a bad sequence of boxes. + # 1st two boxes must be 'jP ' and 'ftyp' + if boxes[0].box_id != 'jP ' or boxes[1].box_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.box_id == 'jp2h'] + jp2h_idx = jp2h_lst[0] + jp2c_lst = [idx for (idx, box) in enumerate(boxes) + if box.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].box_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. + colr_lst = [j for (j, box) in enumerate(jp2h.box) + if box.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.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.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]] + if colr.colorspace == SRGB: + if any([chan + 1 not in cdef.association + or cdef.channel_type[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 cdef.channel_type: + msg = "All color channels must be defined in the " + msg += "channel definition box." + raise IOError(msg) + + +def extract_image_cube(image): + """Extract 3D image from openjpeg data structure. + """ + ncomps = image.contents.numcomps + component = image.contents.comps[0] + dtype = component2dtype(component) + + nrows = component.h + ncols = component.w + data = np.zeros((nrows, ncols, ncomps), dtype) + + for k in range(image.contents.numcomps): + component = image.contents.comps[k] + nrows = component.h + ncols = component.w + + _validate_nonzero_image_size(nrows, ncols, k) + + addr = ctypes.addressof(component.data.contents) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + nelts = nrows * ncols + band = np.ctypeslib.as_array( + (ctypes.c_int32 * nelts).from_address(addr)) + data[:, :, k] = np.reshape(band.astype(dtype), (nrows, ncols)) + + return data + + +def extract_image_bands(image): + """Extract unequally-sized image bands. + + This routine need only be called when subsampling differs across image + components, such as is often the case with YCbCr imagery. + """ + data = [] + for k in range(image.contents.numcomps): + component = image.contents.comps[k] + + dtype = component2dtype(component) + nrows = component.h + ncols = component.w + + _validate_nonzero_image_size(nrows, ncols, k) + + addr = ctypes.addressof(component.data.contents) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + band = np.ctypeslib.as_array( + (ctypes.c_int32 * nrows * ncols).from_address(addr)) + data.append(np.reshape(band.astype(dtype), (nrows, ncols))) + + return data + + +def _unpack_colorspace(colorspace, img_array, cparams): + """Determine the colorspace from the supplied inputs. + + Parameters + ---------- + colorspace : int + Either CLRSPC_SRGB or CLRSPC_GRAY + img_array : ndarray + Image data to be written to file. + cparams : CompressionParametersType(ctypes.Structure) + Corresponds to cparameters_t type in openjp2 headers. + """ + if colorspace is None: + # Must infer the colorspace from the image dimensions. + if img_array.ndim < 3: + # A single channel image is grayscale. + colorspace = opj2.CLRSPC_GRAY + elif img_array.shape[2] == 1 or img_array.shape[2] == 2: + # A single channel image or an image with two channels is going + # to be greyscale. + colorspace = opj2.CLRSPC_GRAY + else: + # Anything else must be RGB, right? + colorspace = opj2.CLRSPC_SRGB + else: + # Supplied a string colorspace, so we must validate it. + if cparams.codec_fmt == opj2.CODEC_J2K: + msg = 'Do not specify a colorspace when writing a raw ' + msg += 'codestream.' + raise IOError(msg) + if colorspace.lower() not in ('rgb', 'grey', 'gray'): + msg = 'Invalid colorspace "{0}"'.format(colorspace) + raise IOError(msg) + elif colorspace.lower() == 'rgb' and img_array.shape[2] < 3: + msg = 'RGB colorspace requires at least 3 components.' + raise IOError(msg) + + # Turn the colorspace from a string to the enumerated value that + # the library expects. + colorspace = _COLORSPACE_MAP[colorspace.lower()] + + return colorspace + + +def _populate_comptparms(img_array, cparams): + """Instantiate and populate comptparms structure. + + This structure defines the image components. + + Parameters + ---------- + img_array : ndarray + Image data to be written to file. + cparams : CompressionParametersType(ctypes.Structure) + Corresponds to cparameters_t type in openjp2 headers. + + Returns + ------- + comptparms : ImageCompType(ctypes.Structure) + Corresponds to image_comp_t type in openjp2 headers. + """ + # Only two precisions are possible. + if img_array.dtype == np.uint8: + comp_prec = 8 + else: + comp_prec = 16 + + numrows, numcols, num_comps = img_array.shape + if version.openjpeg_version_tuple[0] == 1: + comptparms = (opj.ImageComptParmType * num_comps)() + else: + comptparms = (opj2.ImageComptParmType * num_comps)() + for j in range(num_comps): + comptparms[j].dx = cparams.subsampling_dx + comptparms[j].dy = cparams.subsampling_dy + comptparms[j].w = numcols + comptparms[j].h = numrows + comptparms[j].x0 = cparams.image_offset_x0 + comptparms[j].y0 = cparams.image_offset_y0 + comptparms[j].prec = comp_prec + comptparms[j].bpp = comp_prec + comptparms[j].sgnd = 0 + + return comptparms + + +def _populate_image_struct(cparams, image, imgdata): + """Populates image struct needed for compression. + + Parameters + ---------- + cparams : CompressionParametersType(ctypes.Structure) + Corresponds to cparameters_t type in openjp2 headers. + image : ImageType(ctypes.Structure) + Corresponds to image_t type in openjp2 headers. + imgarray : ndarray + Image data to be written to file. + """ + + numrows, numcols, num_comps = imgdata.shape + + # set image offset and reference grid + image.contents.x0 = cparams.image_offset_x0 + image.contents.y0 = cparams.image_offset_y0 + image.contents.x1 = (image.contents.x0 + + (numcols - 1) * cparams.subsampling_dx + 1) + image.contents.y1 = (image.contents.y0 + + (numrows - 1) * cparams.subsampling_dy + 1) + + # Stage the image data to the openjpeg data structure. + for k in range(0, num_comps): + layer = np.ascontiguousarray(imgdata[:, :, k], dtype=np.int32) + dest = image.contents.comps[k].data + src = layer.ctypes.data + ctypes.memmove(dest, src, layer.nbytes) + + return image + + +def _validate_compression_params(img_array, cparams): + """Check that the compression parameters are valid. + + Parameters + ---------- + img_array : ndarray + Image data to be written to file. + cparams : CompressionParametersType(ctypes.Structure) + Corresponds to cparameters_t type in openjp2 headers. + """ + + # Code block size + code_block_specified = False + if cparams.cblockw_init != 0 and cparams.cblockh_init != 0: + # These fields ARE zero if uninitialized. + width = cparams.cblockw_init + height = cparams.cblockh_init + code_block_specified = True + if height * width > 4096 or height < 4 or width < 4: + msg = "Code block area cannot exceed 4096. " + msg += "Code block height and width must be larger than 4." + raise IOError(msg) + if ((math.log(height, 2) != math.floor(math.log(height, 2)) or + math.log(width, 2) != math.floor(math.log(width, 2)))): + msg = "Bad code block size ({0}, {1}), " + msg += "must be powers of 2." + raise IOError(msg.format(height, width)) + + # Precinct size + if cparams.res_spec != 0: + # precinct size was not specified if this field is zero. + for j in range(cparams.res_spec): + prch = cparams.prch_init[j] + prcw = cparams.prcw_init[j] + if j == 0 and code_block_specified: + height, width = cparams.cblockh_init, cparams.cblockw_init + if height * 2 > prch or width * 2 > prcw: + msg = "Highest Resolution precinct size must be at " + msg += "least twice that of the code block dimensions." + raise IOError(msg) + if ((math.log(prch, 2) != math.floor(math.log(prch, 2)) or + math.log(prcw, 2) != math.floor(math.log(prcw, 2)))): + msg = "Bad precinct sizes ({0}, {1}), " + msg += "must be powers of 2." + raise IOError(msg.format(prch, prcw)) + + # What would the point of 1D images be? + if img_array.ndim == 1 or img_array.ndim > 3: + msg = "{0}D imagery is not allowed.".format(img_array.ndim) + raise IOError(msg) + + if _OPENJP2_IS_OFFICIAL_V2: + if (((img_array.ndim != 2) and + (img_array.shape[2] != 1 and img_array.shape[2] != 3))): + msg = "Writing images is restricted to single-channel " + msg += "greyscale images or three-channel RGB images when " + msg += "the OpenJPEG library version is the official 2.0.0 " + msg += "release." + raise IOError(msg) + + if img_array.dtype != np.uint8 and img_array.dtype != np.uint16: + msg = "Only uint8 and uint16 images are currently supported." + raise RuntimeError(msg) + +# Need to known if openjp2 library is the officially release v2.0.0 or not. +_OPENJP2_IS_OFFICIAL_V2 = False +if opj2.OPENJP2 is not None: + if opj2.version() == '2.0.0': + if not hasattr(opj2.OPENJP2, + 'opj_stream_create_default_file_stream_v3'): + _OPENJP2_IS_OFFICIAL_V2 = True + +_COLORSPACE_MAP = {'rgb': opj2.CLRSPC_SRGB, + 'gray': opj2.CLRSPC_GRAY, + 'grey': opj2.CLRSPC_GRAY, + 'ycc': opj2.CLRSPC_YCC} + +# Setup the default callback handlers. See the callback functions subsection +# in the ctypes section of the Python documentation for a solid explanation of +# what's going on here. +_CMPFUNC = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p) + + +def _default_error_handler(msg, _): + """Default error handler callback for libopenjp2.""" + msg = "OpenJPEG library error: {0}".format(msg.decode('utf-8').rstrip()) + opj2.set_error_message(msg) + + +def _default_info_handler(msg, _): + """Default info handler callback.""" + print("[INFO] {0}".format(msg.decode('utf-8').rstrip())) + + +def _default_warning_handler(library_msg, _): + """Default warning handler callback.""" + library_msg = library_msg.decode('utf-8').rstrip() + msg = "OpenJPEG library warning: {0}".format(library_msg) + warnings.warn(msg) + +_ERROR_CALLBACK = _CMPFUNC(_default_error_handler) +_INFO_CALLBACK = _CMPFUNC(_default_info_handler) +_WARNING_CALLBACK = _CMPFUNC(_default_warning_handler) + + +class LibraryNotFoundError(IOError): + """Raised if functionality is requested without the necessary library. + """ + def __init__(self, msg): + IOError.__init__(self, msg) diff --git a/glymur/lib/__init__.py b/glymur/lib/__init__.py index 20fee5f..a283f7f 100644 --- a/glymur/lib/__init__.py +++ b/glymur/lib/__init__.py @@ -1,4 +1,4 @@ """This package organizes individual libraries employed by glymur.""" -from . import openjp2 as _openjp2 -from . import openjpeg as _openjpeg +from . import openjp2 as openjp2 +from . import openjpeg as openjpeg from . import c diff --git a/glymur/lib/config.py b/glymur/lib/config.py index e2b780a..e3a5c1d 100644 --- a/glymur/lib/config.py +++ b/glymur/lib/config.py @@ -1,6 +1,9 @@ """ Configure glymur to use installed libraries if possible. """ +# configparser is new in python3 (pylint/python-2.7) +# pylint: disable=F0401 + import ctypes from ctypes.util import find_library import os diff --git a/glymur/lib/openjp2.py b/glymur/lib/openjp2.py index 66a3cba..f056db0 100644 --- a/glymur/lib/openjp2.py +++ b/glymur/lib/openjp2.py @@ -2,7 +2,7 @@ Wraps individual functions in openjp2 library. """ -# pylint: disable=C0302,R0903 +# pylint: disable=C0302,R0903,W0201 import ctypes import sys @@ -553,119 +553,6 @@ class CodestreamInfoV2(ctypes.Structure): # information regarding tiles inside of image ("tile_info", ctypes.POINTER(TileInfoV2))] -# Restrict the input and output argument types for each function used in the -# API. -if OPENJP2 is not None: - OPENJP2.opj_create_compress.restype = CODEC_TYPE - OPENJP2.opj_create_compress.argtypes = [CODEC_FORMAT_TYPE] - - OPENJP2.opj_create_decompress.argtypes = [CODEC_FORMAT_TYPE] - OPENJP2.opj_create_decompress.restype = CODEC_TYPE - - ARGTYPES = [CODEC_TYPE, STREAM_TYPE_P, ctypes.POINTER(ImageType)] - OPENJP2.opj_decode.argtypes = ARGTYPES - - ARGTYPES = [CODEC_TYPE, ctypes.c_uint32, - ctypes.POINTER(ctypes.c_uint8), - ctypes.c_uint32, - STREAM_TYPE_P] - OPENJP2.opj_decode_tile_data.argtypes = ARGTYPES - - ARGTYPES = [ctypes.POINTER(ctypes.POINTER(CodestreamInfoV2))] - OPENJP2.opj_destroy_cstr_info.argtypes = ARGTYPES - OPENJP2.opj_destroy_cstr_info.restype = ctypes.c_void_p - - ARGTYPES = [CODEC_TYPE, STREAM_TYPE_P] - OPENJP2.opj_encode.argtypes = ARGTYPES - - OPENJP2.opj_get_cstr_info.argtypes = [CODEC_TYPE] - OPENJP2.opj_get_cstr_info.restype = ctypes.POINTER(CodestreamInfoV2) - - ARGTYPES = [CODEC_TYPE, - STREAM_TYPE_P, - ctypes.POINTER(ImageType), - ctypes.c_uint32] - OPENJP2.opj_get_decoded_tile.argtypes = ARGTYPES - - ARGTYPES = [ctypes.c_uint32, - ctypes.POINTER(ImageComptParmType), - COLOR_SPACE_TYPE] - OPENJP2.opj_image_create.argtypes = ARGTYPES - OPENJP2.opj_image_create.restype = ctypes.POINTER(ImageType) - - ARGTYPES = [ctypes.c_uint32, - ctypes.POINTER(ImageComptParmType), - COLOR_SPACE_TYPE] - OPENJP2.opj_image_tile_create.argtypes = ARGTYPES - OPENJP2.opj_image_tile_create.restype = ctypes.POINTER(ImageType) - - OPENJP2.opj_image_destroy.argtypes = [ctypes.POINTER(ImageType)] - - ARGTYPES = [STREAM_TYPE_P, CODEC_TYPE, - ctypes.POINTER(ctypes.POINTER(ImageType))] - OPENJP2.opj_read_header.argtypes = ARGTYPES - - ARGTYPES = [CODEC_TYPE, - STREAM_TYPE_P, - ctypes.POINTER(ctypes.c_uint32), - ctypes.POINTER(ctypes.c_uint32), - ctypes.POINTER(ctypes.c_int32), - ctypes.POINTER(ctypes.c_int32), - ctypes.POINTER(ctypes.c_int32), - ctypes.POINTER(ctypes.c_int32), - ctypes.POINTER(ctypes.c_uint32), - ctypes.POINTER(BOOL_TYPE)] - OPENJP2.opj_read_tile_header.argtypes = ARGTYPES - - ARGTYPES = [CODEC_TYPE, ctypes.POINTER(ImageType), ctypes.c_int32, - ctypes.c_int32, ctypes.c_int32, ctypes.c_int32] - OPENJP2.opj_set_decode_area.argtypes = ARGTYPES - - ARGTYPES = [ctypes.POINTER(CompressionParametersType)] - OPENJP2.opj_set_default_encoder_parameters.argtypes = ARGTYPES - - ARGTYPES = [ctypes.POINTER(DecompressionParametersType)] - OPENJP2.opj_set_default_decoder_parameters.argtypes = ARGTYPES - - ARGTYPES = [CODEC_TYPE, ctypes.c_void_p, ctypes.c_void_p] - OPENJP2.opj_set_error_handler.argtypes = ARGTYPES - OPENJP2.opj_set_info_handler.argtypes = ARGTYPES - OPENJP2.opj_set_warning_handler.argtypes = ARGTYPES - - ARGTYPES = [CODEC_TYPE, ctypes.POINTER(DecompressionParametersType)] - OPENJP2.opj_setup_decoder.argtypes = ARGTYPES - - ARGTYPES = [CODEC_TYPE, - ctypes.POINTER(CompressionParametersType), - ctypes.POINTER(ImageType)] - OPENJP2.opj_setup_encoder.argtypes = ARGTYPES - - if hasattr(OPENJP2, 'opj_stream_create_default_file_stream_v3'): - ARGTYPES = [ctypes.c_char_p, ctypes.c_int32] - OPENJP2.opj_stream_create_default_file_stream_v3.argtypes = ARGTYPES - OPENJP2.opj_stream_create_default_file_stream_v3.restype = STREAM_TYPE_P - OPENJP2.opj_stream_destroy_v3.argtypes = [STREAM_TYPE_P] - else: - ARGTYPES = [ctypes.c_void_p, ctypes.c_int32] - OPENJP2.opj_stream_create_default_file_stream.argtypes = ARGTYPES - OPENJP2.opj_stream_create_default_file_stream.restype = STREAM_TYPE_P - OPENJP2.opj_stream_destroy.argtypes = [STREAM_TYPE_P] - - ARGTYPES = [CODEC_TYPE, ctypes.POINTER(ImageType), STREAM_TYPE_P] - OPENJP2.opj_start_compress.argtypes = ARGTYPES - - OPENJP2.opj_end_compress.argtypes = [CODEC_TYPE, STREAM_TYPE_P] - OPENJP2.opj_end_decompress.argtypes = [CODEC_TYPE, STREAM_TYPE_P] - - OPENJP2.opj_destroy_codec.argtypes = [CODEC_TYPE] - - ARGTYPES = [CODEC_TYPE, - ctypes.c_uint32, - ctypes.POINTER(ctypes.c_uint8), - ctypes.c_uint32, - STREAM_TYPE_P] - OPENJP2.opj_write_tile.argtypes = ARGTYPES - def check_error(status): """Set a generic function as the restype attribute of all OpenJPEG @@ -684,19 +571,6 @@ def check_error(status): else: raise IOError("OpenJPEG function failure.") -# These library functions all return an error status. Circumvent that and -# force # them to raise an exception. -FCNS = ['opj_decode', 'opj_decode_tile_data', 'opj_end_compress', - 'opj_encode', 'opj_end_decompress', 'opj_get_decoded_tile', - 'opj_read_header', 'opj_read_tile_header', 'opj_set_decode_area', - 'opj_set_error_handler', 'opj_set_info_handler', - 'opj_set_warning_handler', - 'opj_setup_decoder', 'opj_setup_encoder', 'opj_start_compress', - 'opj_write_tile'] -if OPENJP2 is not None: - for fcn in FCNS: - setattr(getattr(OPENJP2, fcn), 'restype', check_error) - def create_compress(codec_format): """Creates a J2K/JP2 compress structure. @@ -712,6 +586,9 @@ def create_compress(codec_format): ------- codec : Reference to CODEC_TYPE instance. """ + OPENJP2.opj_create_compress.restype = CODEC_TYPE + OPENJP2.opj_create_compress.argtypes = [CODEC_FORMAT_TYPE] + codec = OPENJP2.opj_create_compress(codec_format) return codec @@ -735,6 +612,10 @@ def decode(codec, stream, image): RuntimeError If the OpenJPEG library routine opj_decode fails. """ + OPENJP2.opj_decode.argtypes = [CODEC_TYPE, STREAM_TYPE_P, + ctypes.POINTER(ImageType)] + OPENJP2.opj_decode.restype = check_error + OPENJP2.opj_decode(codec, stream, image) @@ -761,13 +642,19 @@ def decode_tile_data(codec, tidx, data, data_size, stream): RuntimeError If the OpenJPEG library routine opj_decode fails. """ + OPENJP2.opj_decode_tile_data.argtypes = [CODEC_TYPE, + ctypes.c_uint32, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32, + STREAM_TYPE_P] + OPENJP2.opj_decode_tile_data.restype = check_error + datap = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)) OPENJP2.opj_decode_tile_data(codec, ctypes.c_uint32(tidx), datap, ctypes.c_uint32(data_size), stream) - return codec def create_decompress(codec_format): @@ -784,6 +671,9 @@ def create_decompress(codec_format): ------- codec : Reference to CODEC_TYPE instance. """ + OPENJP2.opj_create_decompress.argtypes = [CODEC_FORMAT_TYPE] + OPENJP2.opj_create_decompress.restype = CODEC_TYPE + codec = OPENJP2.opj_create_decompress(codec_format) return codec @@ -798,6 +688,8 @@ def destroy_codec(codec): codec : CODEC_TYPE Decompressor handle to destroy. """ + OPENJP2.opj_destroy_codec.argtypes = [CODEC_TYPE] + OPENJP2.opj_destroy_codec.restype = ctypes.c_void_p OPENJP2.opj_destroy_codec(codec) @@ -818,6 +710,9 @@ def encode(codec, stream): RuntimeError If the OpenJPEG library routine opj_encode fails. """ + OPENJP2.opj_encode.argtypes = [CODEC_TYPE, STREAM_TYPE_P] + OPENJP2.opj_encode.restype = check_error + OPENJP2.opj_encode(codec, stream) @@ -836,6 +731,9 @@ def get_cstr_info(codec): cstr_info_p : CodestreamInfoV2 Reference to codestream information. """ + OPENJP2.opj_get_cstr_info.argtypes = [CODEC_TYPE] + OPENJP2.opj_get_cstr_info.restype = ctypes.POINTER(CodestreamInfoV2) + cstr_info_p = OPENJP2.opj_get_cstr_info(codec) return cstr_info_p @@ -861,6 +759,12 @@ def get_decoded_tile(codec, stream, imagep, tile_index): RuntimeError If the OpenJPEG library routine opj_get_decoded_tile fails. """ + OPENJP2.opj_get_decoded_tile.argtypes = [CODEC_TYPE, + STREAM_TYPE_P, + ctypes.POINTER(ImageType), + ctypes.c_uint32] + OPENJP2.opj_get_decoded_tile.restype = check_error + OPENJP2.opj_get_decoded_tile(codec, stream, imagep, tile_index) @@ -874,6 +778,10 @@ def destroy_cstr_info(cstr_info_p): cstr_info_p : CodestreamInfoV2 pointer Pointer to codestream info structure. """ + ARGTYPES = [ctypes.POINTER(ctypes.POINTER(CodestreamInfoV2))] + OPENJP2.opj_destroy_cstr_info.argtypes = ARGTYPES + OPENJP2.opj_destroy_cstr_info.restype = ctypes.c_void_p + OPENJP2.opj_destroy_cstr_info(ctypes.byref(cstr_info_p)) @@ -894,6 +802,8 @@ def end_compress(codec, stream): RuntimeError If the OpenJPEG library routine opj_end_compress fails. """ + OPENJP2.opj_end_compress.argtypes = [CODEC_TYPE, STREAM_TYPE_P] + OPENJP2.opj_end_compress.restype = check_error OPENJP2.opj_end_compress(codec, stream) @@ -914,6 +824,8 @@ def end_decompress(codec, stream): RuntimeError If the OpenJPEG library routine opj_end_decompress fails. """ + OPENJP2.opj_end_decompress.argtypes = [CODEC_TYPE, STREAM_TYPE_P] + OPENJP2.opj_end_decompress.restype = check_error OPENJP2.opj_end_decompress(codec, stream) @@ -927,6 +839,9 @@ def image_destroy(image): image : ImageType pointer Image resource to be disposed. """ + OPENJP2.opj_image_destroy.argtypes = [ctypes.POINTER(ImageType)] + OPENJP2.opj_image_destroy.restype = ctypes.c_void_p + OPENJP2.opj_image_destroy(image) @@ -947,6 +862,11 @@ def image_create(comptparms, clrspc): image : ImageType Reference to ImageType instance. """ + OPENJP2.opj_image_create.argtypes = [ctypes.c_uint32, + ctypes.POINTER(ImageComptParmType), + COLOR_SPACE_TYPE] + OPENJP2.opj_image_create.restype = ctypes.POINTER(ImageType) + image = OPENJP2.opj_image_create(len(comptparms), comptparms, clrspc) @@ -970,6 +890,12 @@ def image_tile_create(comptparms, clrspc): image : ImageType Reference to ImageType instance. """ + ARGTYPES = [ctypes.c_uint32, + ctypes.POINTER(ImageComptParmType), + COLOR_SPACE_TYPE] + OPENJP2.opj_image_tile_create.argtypes = ARGTYPES + OPENJP2.opj_image_tile_create.restype = ctypes.POINTER(ImageType) + image = OPENJP2.opj_image_tile_create(len(comptparms), comptparms, clrspc) @@ -998,6 +924,11 @@ def read_header(stream, codec): RuntimeError If the OpenJPEG library routine opj_read_header fails. """ + ARGTYPES = [STREAM_TYPE_P, CODEC_TYPE, + ctypes.POINTER(ctypes.POINTER(ImageType))] + OPENJP2.opj_read_header.argtypes = ARGTYPES + OPENJP2.opj_read_header.restype = check_error + imagep = ctypes.POINTER(ImageType)() OPENJP2.opj_read_header(stream, codec, ctypes.byref(imagep)) return imagep @@ -1035,6 +966,19 @@ def read_tile_header(codec, stream): RuntimeError If the OpenJPEG library routine opj_read_tile_header fails. """ + ARGTYPES = [CODEC_TYPE, + STREAM_TYPE_P, + ctypes.POINTER(ctypes.c_uint32), + ctypes.POINTER(ctypes.c_uint32), + ctypes.POINTER(ctypes.c_int32), + ctypes.POINTER(ctypes.c_int32), + ctypes.POINTER(ctypes.c_int32), + ctypes.POINTER(ctypes.c_int32), + ctypes.POINTER(ctypes.c_uint32), + ctypes.POINTER(BOOL_TYPE)] + OPENJP2.opj_read_tile_header.argtypes = ARGTYPES + OPENJP2.opj_read_tile_header.restype = check_error + tile_index = ctypes.c_uint32() data_size = ctypes.c_uint32() col0 = ctypes.c_int32() @@ -1086,6 +1030,14 @@ def set_decode_area(codec, image, start_x=0, start_y=0, end_x=0, end_y=0): RuntimeError If the OpenJPEG library routine opj_set_decode_area fails. """ + OPENJP2.opj_set_decode_area.argtypes = [CODEC_TYPE, + ctypes.POINTER(ImageType), + ctypes.c_int32, + ctypes.c_int32, + ctypes.c_int32, + ctypes.c_int32] + OPENJP2.opj_set_decode_area.restype = check_error + OPENJP2.opj_set_decode_area(codec, image, ctypes.c_int32(start_x), ctypes.c_int32(start_y), @@ -1103,6 +1055,10 @@ def set_default_decoder_parameters(): dparam : DecompressionParametersType Decompression parameters. """ + ARGTYPES = [ctypes.POINTER(DecompressionParametersType)] + OPENJP2.opj_set_default_decoder_parameters.argtypes = ARGTYPES + OPENJP2.opj_set_default_decoder_parameters.restype = ctypes.c_void_p + dparams = DecompressionParametersType() OPENJP2.opj_set_default_decoder_parameters(ctypes.byref(dparams)) return dparams @@ -1138,6 +1094,10 @@ def set_default_encoder_parameters(): cparameters : CompressionParametersType Compression parameters. """ + ARGTYPES = [ctypes.POINTER(CompressionParametersType)] + OPENJP2.opj_set_default_encoder_parameters.argtypes = ARGTYPES + OPENJP2.opj_set_default_encoder_parameters.restype = ctypes.c_void_p + cparams = CompressionParametersType() OPENJP2.opj_set_default_encoder_parameters(ctypes.byref(cparams)) return cparams @@ -1162,6 +1122,10 @@ def set_error_handler(codec, handler, data=None): RuntimeError If the OpenJPEG library routine opj_set_error_handler fails. """ + OPENJP2.opj_set_error_handler.argtypes = [CODEC_TYPE, + ctypes.c_void_p, + ctypes.c_void_p] + OPENJP2.opj_set_error_handler.restype = check_error OPENJP2.opj_set_error_handler(codec, handler, data) @@ -1184,6 +1148,10 @@ def set_info_handler(codec, handler, data=None): RuntimeError If the OpenJPEG library routine opj_set_info_handler fails. """ + OPENJP2.opj_set_info_handler.argtypes = [CODEC_TYPE, + ctypes.c_void_p, + ctypes.c_void_p] + OPENJP2.opj_set_info_handler.restype = check_error OPENJP2.opj_set_info_handler(codec, handler, data) @@ -1206,6 +1174,11 @@ def set_warning_handler(codec, handler, data=None): RuntimeError If the OpenJPEG library routine opj_set_warning_handler fails. """ + OPENJP2.opj_set_warning_handler.argtypes = [CODEC_TYPE, + ctypes.c_void_p, + ctypes.c_void_p] + OPENJP2.opj_set_warning_handler.restype = check_error + OPENJP2.opj_set_warning_handler(codec, handler, data) @@ -1226,6 +1199,10 @@ def setup_decoder(codec, dparams): RuntimeError If the OpenJPEG library routine opj_setup_decoder fails. """ + ARGTYPES = [CODEC_TYPE, ctypes.POINTER(DecompressionParametersType)] + OPENJP2.opj_setup_decoder.argtypes = ARGTYPES + OPENJP2.opj_setup_decoder.restype = check_error + OPENJP2.opj_setup_decoder(codec, ctypes.byref(dparams)) @@ -1249,6 +1226,11 @@ def setup_encoder(codec, cparams, image): RuntimeError If the OpenJPEG library routine opj_setup_encoder fails. """ + ARGTYPES = [CODEC_TYPE, + ctypes.POINTER(CompressionParametersType), + ctypes.POINTER(ImageType)] + OPENJP2.opj_setup_encoder.argtypes = ARGTYPES + OPENJP2.opj_setup_encoder.restype = check_error OPENJP2.opj_setup_encoder(codec, ctypes.byref(cparams), image) @@ -1271,6 +1253,11 @@ def start_compress(codec, image, stream): RuntimeError If the OpenJPEG library routine opj_start_compress fails. """ + OPENJP2.opj_start_compress.argtypes = [CODEC_TYPE, + ctypes.POINTER(ImageType), + STREAM_TYPE_P] + OPENJP2.opj_start_compress.restype = check_error + OPENJP2.opj_start_compress(codec, image, stream) @@ -1292,6 +1279,9 @@ def stream_create_default_file_stream(fptr, isa_read_stream): stream : stream_t An OpenJPEG file stream. """ + ARGTYPES = [ctypes.c_void_p, ctypes.c_int32] + OPENJP2.opj_stream_create_default_file_stream.argtypes = ARGTYPES + OPENJP2.opj_stream_create_default_file_stream.restype = STREAM_TYPE_P read_stream = 1 if isa_read_stream else 0 stream = OPENJP2.opj_stream_create_default_file_stream(fptr, read_stream) return stream @@ -1315,6 +1305,9 @@ def stream_create_default_file_stream_v3(fname, isa_read_stream): stream : stream_t An OpenJPEG file stream. """ + ARGTYPES = [ctypes.c_char_p, ctypes.c_int32] + OPENJP2.opj_stream_create_default_file_stream_v3.argtypes = ARGTYPES + OPENJP2.opj_stream_create_default_file_stream_v3.restype = STREAM_TYPE_P read_stream = 1 if isa_read_stream else 0 file_argument = ctypes.c_char_p(fname.encode()) stream = OPENJP2.opj_stream_create_default_file_stream_v3(file_argument, @@ -1332,6 +1325,8 @@ def stream_destroy(stream): stream : STREAM_TYPE_P The file stream. """ + OPENJP2.opj_stream_destroy.argtypes = [STREAM_TYPE_P] + OPENJP2.opj_stream_destroy.restype = ctypes.c_void_p OPENJP2.opj_stream_destroy(stream) @@ -1345,6 +1340,8 @@ def stream_destroy_v3(stream): stream : STREAM_TYPE_P The file stream. """ + OPENJP2.opj_stream_destroy_v3.argtypes = [STREAM_TYPE_P] + OPENJP2.opj_stream_destroy_v3.restype = ctypes.c_void_p OPENJP2.opj_stream_destroy_v3(stream) @@ -1371,6 +1368,13 @@ def write_tile(codec, tile_index, data, data_size, stream): RuntimeError If the OpenJPEG library routine opj_write_tile fails. """ + OPENJP2.opj_write_tile.argtypes = [CODEC_TYPE, + ctypes.c_uint32, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32, + STREAM_TYPE_P] + OPENJP2.opj_write_tile.restype = check_error + datap = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)) OPENJP2.opj_write_tile(codec, ctypes.c_uint32(int(tile_index)), @@ -1381,7 +1385,6 @@ def write_tile(codec, tile_index, data, data_size, stream): def set_error_message(msg): """The openjpeg error handler has recorded an error message.""" - global ERROR_MSG_LST ERROR_MSG_LST.append(msg) diff --git a/glymur/lib/openjpeg.py b/glymur/lib/openjpeg.py index 704cf9c..5b1183f 100644 --- a/glymur/lib/openjpeg.py +++ b/glymur/lib/openjpeg.py @@ -6,10 +6,16 @@ import ctypes import sys +import numpy as np + from .config import glymur_config _, OPENJPEG = glymur_config() -PATH_LEN = 4096 # maximum allowed size for filenames +# Maximum number of tile parts expected by JPWL: increase at your will +JPWL_MAX_NO_TILESPECS = 16 + +J2K_MAXRLVLS = 33 # Number of maximum resolution level authorized +PATH_LEN = 4096 # maximum allowed size for filenames def version(): @@ -52,14 +58,8 @@ class CommonStructType(ctypes.Structure): ("mj2_handle", ctypes.c_void_p)] -class DecompressionInfoType(ctypes.Structure): - """This is for decompression contexts. - - Corresponds to dinfo_t type in openjpeg headers. - """ - pass - - +STREAM_READ = 0x0001 # The stream was opened for reading. +STREAM_WRITE = 0x0002 # The stream was opened for writing. class CioType(ctypes.Structure): """Byte input-output stream (CIO) @@ -80,6 +80,266 @@ class CioType(ctypes.Structure): ("bp", ctypes.c_char_p)] +class CompressionInfoType(CommonStructType): + """Common fields between JPEG-2000 compression and decompression contexts. + This is for compression contexts. Corresponds to common_struct_t. + """ + pass + + +class PocType(ctypes.Structure): + """Progression order changes.""" + _fields_ = [("resno", ctypes.c_int), + # Resolution num start, Component num start, given by POC + ("compno0", ctypes.c_int), + + # Layer num end,Resolution num end, Component num end, given by POC + ("layno1", ctypes.c_int), + ("resno1", ctypes.c_int), + ("compno1", ctypes.c_int), + + # Layer num start,Precinct num start, Precinct num end + ("layno0", ctypes.c_int), + ("precno0", ctypes.c_int), + ("precno1", ctypes.c_int), + + # Progression order enum + # OPJ_PROG_ORDER prg1,prg; + ("prg1", ctypes.c_int), + ("prg", ctypes.c_int), + + # Progression order string + # char progorder[5]; + ("progorder", ctypes.c_char * 5), + + # Tile number + # int tile; + ("tile", ctypes.c_int), + + # /** Start and end values for Tile width and height*/ + # int tx0,tx1,ty0,ty1; + ("tx0", ctypes.c_int), + ("tx1", ctypes.c_int), + ("ty0", ctypes.c_int), + ("ty1", ctypes.c_int), + + # /** Start value, initialised in pi_initialise_encode*/ + # int layS, resS, compS, prcS; + ("layS", ctypes.c_int), + ("resS", ctypes.c_int), + ("compS", ctypes.c_int), + ("prcS", ctypes.c_int), + + # /** End value, initialised in pi_initialise_encode */ + # int layE, resE, compE, prcE; + ("layE", ctypes.c_int), + ("resE", ctypes.c_int), + ("compE", ctypes.c_int), + ("prcE", ctypes.c_int), + + # Start and end values of Tile width and height, initialised in + # pi_initialise_encode int txS,txE,tyS,tyE,dx,dy; + ("txS", ctypes.c_int), + ("txE", ctypes.c_int), + ("tyS", ctypes.c_int), + ("tyE", ctypes.c_int), + ("dx", ctypes.c_int), + ("dy", ctypes.c_int), + + # Temporary values for Tile parts, initialised in pi_create_encode + # int lay_t, res_t, comp_t, prc_t,tx0_t,ty0_t; + ("lay_t", ctypes.c_int), + ("res_t", ctypes.c_int), + ("comp_t", ctypes.c_int), + ("prc_t", ctypes.c_int), + ("tx0_t", ctypes.c_int), + ("ty0_t", ctypes.c_int)] + + +class CompressionParametersType(ctypes.Structure): + """Compression parameters. + + Corresponds to cparameters_t type in openjp2 headers. + """ + _fields_ = [ + # size of tile: + # tile_size_on = false (not in argument) or + # = true (in argument) + ("tile_size_on", ctypes.c_int), + + # XTOsiz, YTOsiz + ("cp_tx0", ctypes.c_int), + ("cp_ty0", ctypes.c_int), + + # XTsiz, YTsiz + ("cp_tdx", ctypes.c_int), + ("cp_tdy", ctypes.c_int), + + # allocation by rate/distortion + ("cp_disto_alloc", ctypes.c_int), + + # allocation by fixed layer + ("cp_fixed_alloc", ctypes.c_int), + + # add fixed_quality + ("cp_fixed_quality", ctypes.c_int), + + # fixed layer + ("cp_matrice", ctypes.c_void_p), + + # comment for coding + ("cp_comment", ctypes.c_char_p), + + # csty : coding style + ("csty", ctypes.c_int), + + # progression order (default OPJ_LRCP) + ("prog_order", ctypes.c_int), + + # progression order changes + ("poc", PocType * 32), + + # number of progression order changes (POC), default to 0 + ("numpocs", ctypes.c_uint), + + # number of layers + ("tcp_numlayers", ctypes.c_int), + + # rates of layers + ("tcp_rates", ctypes.c_float * 100), + + # different psnr for successive layers + ("tcp_distoratio", ctypes.c_float * 100), + + # number of resolutions + ("numresolution", ctypes.c_int), + + # initial code block width, default to 64 + ("cblockw_init", ctypes.c_int), + + # initial code block height, default to 64 + ("cblockh_init", ctypes.c_int), + + # mode switch (cblk_style) + ("mode", ctypes.c_int), + + # 1 : use the irreversible DWT 9-7 + # 0 : use lossless compression (default) + ("irreversible", ctypes.c_int), + + # region of interest: affected component in [0..3], -1 means no ROI + ("roi_compno", ctypes.c_int), + + # region of interest: upshift value + ("roi_shift", ctypes.c_int), + + # number of precinct size specifications + ("res_spec", ctypes.c_int), + + # initial precinct width + ("prcw_init", ctypes.c_int * J2K_MAXRLVLS), + + # initial precinct height + ("prch_init", ctypes.c_int * J2K_MAXRLVLS), + + # input file name + ("infile", ctypes.c_char * PATH_LEN), + + # output file name + ("outfile", ctypes.c_char * PATH_LEN), + + # DEPRECATED. + ("index_on", ctypes.c_int), + + # DEPRECATED. + ("index", ctypes.c_char * PATH_LEN), + + # subimage encoding: origin image offset in x direction + # subimage encoding: origin image offset in y direction + ("image_offset_x0", ctypes.c_int), + ("image_offset_y0", ctypes.c_int), + + # subsampling value for dx + # subsampling value for dy + ("subsampling_dx", ctypes.c_int), + ("subsampling_dy", ctypes.c_int), + + # input file format 0: PGX, 1: PxM, 2: BMP 3:TIF + # output file format 0: J2K, 1: JP2, 2: JPT + ("decod_format", ctypes.c_int), + ("cod_format", ctypes.c_int), + + # JPWL encoding parameters + # enables writing of EPC in MH, thus activating JPWL + ("jpwl_epc_on", ctypes.c_int), + + # error protection method for MH (0,1,16,32,37-128) + ("jpwl_hprot_mh", ctypes.c_int), + + # tile number of header protection specification (>=0) + ("jpwl_hprot_tph_tileno", ctypes.c_int * JPWL_MAX_NO_TILESPECS), + + # error protection methods for TPHs (0,1,16,32,37-128) + ("jpwl_hprot_tph", ctypes.c_int * JPWL_MAX_NO_TILESPECS), + + # tile number of packet protection specification (>=0) + ("jpwl_pprot_tileno", ctypes.c_int * JPWL_MAX_NO_TILESPECS), + + # packet number of packet protection specification (>=0) + ("jpwl_pprot_packno", ctypes.c_int * JPWL_MAX_NO_TILESPECS), + + # error protection methods for packets (0,1,16,32,37-128) + ("jpwl_pprot", ctypes.c_int * JPWL_MAX_NO_TILESPECS), + + # enables writing of ESD, (0=no/1/2 bytes) + ("jpwl_sens_size", ctypes.c_int), + + # sensitivity addressing size (0=auto/2/4 bytes) + ("jpwl_sens_addr", ctypes.c_int), + + # sensitivity range (0-3) + ("jpwl_sens_range", ctypes.c_int), + + # sensitivity method for MH (-1=no,0-7) + ("jpwl_sens_mh", ctypes.c_int), + + # tile number of sensitivity specification (>=0) + ("jpwl_sens_tph_tileno", ctypes.c_int * JPWL_MAX_NO_TILESPECS), + + # sensitivity methods for TPHs (-1=no,0-7) + ("jpwl_sens_tph", ctypes.c_int * JPWL_MAX_NO_TILESPECS), + + # Digital Cinema compliance 0-not compliant, 1-compliant + ("cp_cinema", ctypes.c_int), + + # Maximum rate for each component. + # If == 0, component size limitation is not considered + ("max_comp_size", ctypes.c_int), + + # Profile name + ("cp_rsiz", ctypes.c_int), + + # Tile part generation + ("tp_on", ctypes.c_uint8), + + # Flag for Tile part generation + ("tp_flag", ctypes.c_uint8), + + # MCT (multiple component transform) + ("tcp_mct", ctypes.c_uint8), + + # Enable JPIP indexing + ("jpip_on", ctypes.c_int)] + + +class DecompressionInfoType(ctypes.Structure): + """This is for decompression contexts. + + Corresponds to dinfo_t type in openjpeg headers. + """ + pass + + class DecompressionParametersType(ctypes.Structure): """Decompression parameters. @@ -111,23 +371,51 @@ class DecompressionParametersType(ctypes.Structure): _fields_.append(("flags", ctypes.c_uint)) -class ImageCompType(ctypes.Structure): - """Defines a single image component. - - Corresponds to image_comp_t type in openjpeg. +class ImageComptParmType(ctypes.Structure): + """Component parameters structure used by the opj_image_create function. """ - _fields_ = [("dx", ctypes.c_int), - ("dy", ctypes.c_int), - ("w", ctypes.c_int), - ("h", ctypes.c_int), - ("x0", ctypes.c_int), - ("y0", ctypes.c_int), - ("prec", ctypes.c_int), - ("bpp", ctypes.c_int), - ("sgnd", ctypes.c_int), - ("resno_decoded", ctypes.c_int), - ("factor", ctypes.c_int), - ("data", ctypes.POINTER(ctypes.c_int))] + _fields_ = [ + # XRsiz: horizontal separation of a sample of ith component with + # respect to the reference grid + ("dx", ctypes.c_int), + + # YRsiz: vertical separation of a sample of ith component with + # respect to the reference grid */ + ("dy", ctypes.c_int), + + # data width, height + ("w", ctypes.c_int), + ("h", ctypes.c_int), + + # x component offset compared to the whole image + # y component offset compared to the whole image + ("x0", ctypes.c_int), + ("y0", ctypes.c_int), + + # precision + ('prec', ctypes.c_int), + + # image depth in bits + ('bpp', ctypes.c_int), + + # signed (1) / unsigned (0) + ('sgnd', ctypes.c_int)] + + +class ImageCompType(ctypes.Structure): + """Defines a single image component. """ + _fields_ = [("dx", ctypes.c_int), + ("dy", ctypes.c_int), + ("w", ctypes.c_int), + ("h", ctypes.c_int), + ("x0", ctypes.c_int), + ("y0", ctypes.c_int), + ("prec", ctypes.c_int), + ("bpp", ctypes.c_int), + ("sgnd", ctypes.c_int), + ("resno_decoded", ctypes.c_int), + ("factor", ctypes.c_int), + ("data", ctypes.POINTER(ctypes.c_int))] class ImageType(ctypes.Structure): @@ -146,16 +434,22 @@ class ImageType(ctypes.Structure): ("icc_profile_len", ctypes.c_int)] -def cio_open(cinfo, src): +def cio_open(cinfo, src=None): """Wrapper for openjpeg library function opj_cio_open.""" argtypes = [ctypes.POINTER(CommonStructType), ctypes.c_char_p, ctypes.c_int] OPENJPEG.opj_cio_open.argtypes = argtypes OPENJPEG.opj_cio_open.restype = ctypes.POINTER(CioType) + if src is None: + length = 0 + else: + length = len(src) + cio = OPENJPEG.opj_cio_open(ctypes.cast(cinfo, ctypes.POINTER(CommonStructType)), - src, len(src)) + src, + length) return cio @@ -166,6 +460,24 @@ def cio_close(cio): OPENJPEG.opj_cio_close(cio) +def cio_tell(cio): + """Get position in byte stream.""" + OPENJPEG.cio_tell.argtypes = [ctypes.POINTER(CioType)] + OPENJPEG.cio_tell.restype = ctypes.c_int + pos = OPENJPEG.cio_tell(cio) + return pos + +def create_compress(fmt): + """Wrapper for openjpeg library function opj_create_compress. + + Creates a J2K/JPT/JP2 compression structure. + """ + OPENJPEG.opj_create_compress.argtypes = [ctypes.c_int] + OPENJPEG.opj_create_compress.restype = ctypes.POINTER(CompressionInfoType) + cinfo = OPENJPEG.opj_create_compress(fmt) + return cinfo + + def create_decompress(fmt): """Wraps openjpeg library function opj_create_decompress. """ @@ -186,6 +498,38 @@ def decode(dinfo, cio): return image +def destroy_compress(cinfo): + """Wrapper for openjpeg library function opj_destroy_compress. + + Release resources for a compressor handle. + """ + argtypes = [ctypes.POINTER(CompressionInfoType)] + OPENJPEG.opj_destroy_compress.argtypes = argtypes + OPENJPEG.opj_destroy_compress(cinfo) + + +def encode(cinfo, cio, image): + """Wrapper for openjpeg library function opj_encode. + + Encodes an image into a JPEG-2000 codestream. + + Parameters + ---------- + cinfo : compression handle + + cio : output buffer stream + + image : image to encode + """ + argtypes = [ctypes.POINTER(CompressionInfoType), + ctypes.POINTER(CioType), + ctypes.POINTER(ImageType)] + OPENJPEG.opj_encode.argtypes = argtypes + OPENJPEG.opj_encode.restype = ctypes.c_int + status = OPENJPEG.opj_encode(cinfo, cio, image) + return status + + def destroy_decompress(dinfo): """Wraps openjpeg library function opj_destroy_decompress.""" argtypes = [ctypes.POINTER(DecompressionInfoType)] @@ -193,12 +537,78 @@ def destroy_decompress(dinfo): OPENJPEG.opj_destroy_decompress(dinfo) +def image_cmptparm_t_from_np(np_image): + """Return appropriate image_cmptparm_t based on given numpy array. + """ + try: + num_comps = np_image.shape[2] + except IndexError: + num_comps = 1 + + cmpt_parm_array_t = ImageCmptparmType * num_comps + tarr = cmpt_parm_array_t() + + if np_image.dtype == np.uint8: + prec = 8 + bpp = 8 + sgnd = 0 + elif np_image.dtype == np.int8: + prec = 8 + bpp = 8 + sgnd = 1 + elif np_image.dtype == np.uint16: + prec = 16 + bpp = 16 + sgnd = 0 + elif np_image.dtype == np.int16: + prec = 16 + bpp = 16 + sgnd = 1 + else: + raise(TypeError("unhandled")) + + for j in range(0, num_comps): + tarr[j].dx = 1 + tarr[j].dy = 1 + tarr[j].w = np_image.shape[1] + tarr[j].h = np_image.shape[0] + tarr[j].x0 = 0 + tarr[j].y0 = 0 + tarr[j].prec = prec + tarr[j].bpp = bpp + tarr[j].sgnd = sgnd + + return(tarr) + + +def image_create(cmptparms, cspace): + """Wrapper for openjpeg library function opj_image_create. + """ + OPENJPEG.opj_image_create.argtypes = [ctypes.c_int, + ctypes.POINTER(ImageComptParmType), + ctypes.c_int] + OPENJPEG.opj_image_create.restype = ctypes.POINTER(ImageType) + + image = OPENJPEG.opj_image_create(len(cmptparms), cmptparms, cspace) + return(image) + + def image_destroy(image): """Wraps openjpeg library function opj_image_destroy.""" OPENJPEG.opj_image_destroy.argtypes = [ctypes.POINTER(ImageType)] OPENJPEG.opj_image_destroy(image) +def set_default_encoder_parameters(): + """Wrapper for openjpeg library function opj_set_default_encoder_parameters. + """ + cparams = CompressionParametersType() + argtypes = [ctypes.POINTER(CompressionParametersType)] + OPENJPEG.opj_set_default_encoder_parameters.argtypes = argtypes + OPENJPEG.opj_set_default_encoder_parameters(ctypes.byref(cparams)) + return cparams + + def set_default_decoder_parameters(dparams_p): """Wrapper for opj_set_default_decoder_parameters. """ @@ -219,6 +629,15 @@ def set_event_mgr(dinfo, event_mgr, context=None): event_mgr, context) +def setup_encoder(cinfo, cparameters, image): + """Wrapper for openjpeg library function opj_setup_decoder.""" + argtypes = [ctypes.POINTER(CompressionInfoType), + ctypes.POINTER(CompressionParametersType), + ctypes.POINTER(ImageType)] + OPENJPEG.opj_setup_encoder.argtypes = argtypes + OPENJPEG.opj_setup_encoder(cinfo, cparameters, image) + + def setup_decoder(dinfo, dparams): """Wrapper for openjpeg library function opj_setup_decoder.""" argtypes = [ctypes.POINTER(DecompressionInfoType), diff --git a/glymur/lib/test/__init__.py b/glymur/lib/test/__init__.py index 5aea3ab..47a3d86 100644 --- a/glymur/lib/test/__init__.py +++ b/glymur/lib/test/__init__.py @@ -1 +1,3 @@ -#from .test_openjp2 import TestOpenJP2 as openjp2 +""" +Test suite for openjp2, openjpeg low-level functionality. +""" diff --git a/glymur/lib/test/test_openjp2.py b/glymur/lib/test/test_openjp2.py index 710f265..54d8254 100644 --- a/glymur/lib/test/test_openjp2.py +++ b/glymur/lib/test/test_openjp2.py @@ -1,9 +1,14 @@ -#pylint: disable-all -import doctest +""" +Tests for libopenjp2 wrapping functions. +""" +# R0904: Seems like pylint is fooled in this situation +# W0142: using kwargs is ok in this context +# pylint: disable=R0904,W0142 + +# unittest2 is python-2.6 only (pylint/python-2.7) +# pylint: disable=F0401 + import os -import pkg_resources -import shutil -import struct import sys import tempfile @@ -15,28 +20,28 @@ else: import numpy as np import glymur +from glymur.lib import openjp2 OPENJP2_IS_V2_OFFICIAL = False -if glymur.lib.openjp2.OPENJP2 is not None: - if not hasattr(glymur.lib.openjp2.OPENJP2, +if openjp2.OPENJP2 is not None: + if not hasattr(openjp2.OPENJP2, 'opj_stream_create_default_file_stream_v3'): OPENJP2_IS_V2_OFFICIAL = True @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, +@unittest.skipIf(openjp2.OPENJP2 is None, "Missing openjp2 library.") @unittest.skipIf(OPENJP2_IS_V2_OFFICIAL, "API followed here specific to V2.0+") class TestOpenJP2(unittest.TestCase): + """Test openjp2 library functionality. - def setUp(self): - pass + Some tests correspond to those in the openjpeg test suite. + """ - def tearDown(self): - pass - - def test_set_default_encoder_parameters(self): - cparams = glymur.lib._openjp2.set_default_encoder_parameters() + def test_default_encoder_parameters(self): + """Ensure that the encoder structure is clean upon init.""" + cparams = openjp2.set_default_encoder_parameters() self.assertEqual(cparams.res_spec, 0) self.assertEqual(cparams.cblockw_init, 64) @@ -52,8 +57,9 @@ class TestOpenJP2(unittest.TestCase): self.assertEqual(cparams.irreversible, 0) - def test_set_default_decoder_parameters(self): - dparams = glymur.lib._openjp2.set_default_decoder_parameters() + def test_default_decoder_parameters(self): + """Tests that the structure is clean upon initialization""" + dparams = openjp2.set_default_decoder_parameters() self.assertEqual(dparams.DA_x0, 0) self.assertEqual(dparams.DA_y0, 0) @@ -61,35 +67,34 @@ class TestOpenJP2(unittest.TestCase): self.assertEqual(dparams.DA_y1, 0) def tile_macro(self, codec, stream, imagep, tidx): - # called only by j2k_random_tile_access - glymur.lib._openjp2.get_decoded_tile(codec, stream, imagep, tidx) + """called only by j2k_random_tile_access""" + openjp2.get_decoded_tile(codec, stream, imagep, tidx) for j in range(imagep.contents.numcomps): self.assertIsNotNone(imagep.contents.comps[j].data) def j2k_random_tile_access(self, filename, codec_format=None): - # called by the test_rtaX methods - dparam = glymur.lib._openjp2.set_default_decoder_parameters() + """fixture called by the test_rtaX methods""" + dparam = openjp2.set_default_decoder_parameters() infile = filename.encode() - nelts = glymur.lib._openjp2.PATH_LEN - len(infile) + nelts = openjp2.PATH_LEN - len(infile) infile += b'0' * nelts dparam.infile = infile dparam.decod_format = codec_format - codec = glymur.lib._openjp2.create_decompress(codec_format) + codec = openjp2.create_decompress(codec_format) - glymur.lib._openjp2.set_info_handler(codec, None) - glymur.lib._openjp2.set_warning_handler(codec, None) - glymur.lib._openjp2.set_error_handler(codec, None) + openjp2.set_info_handler(codec, None) + openjp2.set_warning_handler(codec, None) + openjp2.set_error_handler(codec, None) - x = (filename, True) - stream = glymur.lib._openjp2.stream_create_default_file_stream_v3(*x) + stream = openjp2.stream_create_default_file_stream_v3(filename, True) - glymur.lib._openjp2.setup_decoder(codec, dparam) - image = glymur.lib._openjp2.read_header(stream, codec) + openjp2.setup_decoder(codec, dparam) + image = openjp2.read_header(stream, codec) - cstr_info = glymur.lib._openjp2.get_cstr_info(codec) + cstr_info = openjp2.get_cstr_info(codec) tile_ul = 0 tile_ur = cstr_info.contents.tw - 1 @@ -101,159 +106,39 @@ class TestOpenJP2(unittest.TestCase): self.tile_macro(codec, stream, image, tile_lr) self.tile_macro(codec, stream, image, tile_ll) - glymur.lib._openjp2.destroy_cstr_info(cstr_info) + openjp2.destroy_cstr_info(cstr_info) - glymur.lib._openjp2.end_decompress(codec, stream) - glymur.lib._openjp2.destroy_codec(codec) - glymur.lib._openjp2.stream_destroy_v3(stream) - glymur.lib._openjp2.image_destroy(image) - - def tile_decoder(self, x0=None, y0=None, x1=None, y1=None, filename=None, - codec_format=None): - x = (filename, True) - stream = glymur.lib._openjp2.stream_create_default_file_stream_v3(*x) - dparam = glymur.lib._openjp2.set_default_decoder_parameters() - - dparam.decod_format = codec_format - - # Do not use layer decoding limitation. - dparam.cp_layer = 0 - - # do not use resolution reductions. - dparam.cp_reduce = 0 - - codec = glymur.lib._openjp2.create_decompress(codec_format) - - glymur.lib._openjp2.set_info_handler(codec, None) - glymur.lib._openjp2.set_warning_handler(codec, None) - glymur.lib._openjp2.set_error_handler(codec, None) - - glymur.lib._openjp2.setup_decoder(codec, dparam) - image = glymur.lib._openjp2.read_header(stream, codec) - glymur.lib._openjp2.set_decode_area(codec, image, x0, y0, x1, y1) - - data = np.zeros((1150, 2048, 3), dtype=np.uint8) - while True: - rargs = glymur.lib._openjp2.read_tile_header(codec, stream) - tidx = rargs[0] - sz = rargs[1] - go_on = rargs[-1] - if not go_on: - break - glymur.lib._openjp2.decode_tile_data(codec, tidx, data, sz, stream) - - glymur.lib._openjp2.end_decompress(codec, stream) - glymur.lib._openjp2.destroy_codec(codec) - glymur.lib._openjp2.stream_destroy_v3(stream) - glymur.lib._openjp2.image_destroy(image) - - def tile_encoder(self, num_comps=None, tile_width=None, tile_height=None, - filename=None, codec=None, comp_prec=None, - image_width=None, image_height=None, - irreversible=None): - num_tiles = (image_width / tile_width) * (image_height / tile_height) - tile_size = tile_width * tile_height * num_comps * comp_prec / 8 - - data = np.random.random((tile_height, tile_width, num_comps)) - data = (data * 255).astype(np.uint8) - - l_param = glymur.lib._openjp2.set_default_encoder_parameters() - - l_param.tcp_numlayers = 1 - l_param.cp_fixed_quality = 1 - l_param.tcp_distoratio[0] = 20 - - # position of the tile grid aligned with the image - l_param.cp_tx0 = 0 - l_param.cp_ty0 = 0 - - # tile size, we are using tile based encoding - l_param.tile_size_on = 1 - l_param.cp_tdx = tile_width - l_param.cp_tdy = tile_height - - # use irreversible encoding - l_param.irreversible = irreversible - - l_param.numresolution = 6 - - l_param.prog_order = glymur.core.LRCP - - l_params = (glymur.lib._openjp2.ImageComptParmType * num_comps)() - for j in range(num_comps): - l_params[j].dx = 1 - l_params[j].dy = 1 - l_params[j].h = image_height - l_params[j].w = image_width - l_params[j].sgnd = 0 - l_params[j].prec = comp_prec - l_params[j].x0 = 0 - l_params[j].y0 = 0 - - codec = glymur.lib._openjp2.create_compress(codec) - - glymur.lib._openjp2.set_info_handler(codec, None) - glymur.lib._openjp2.set_warning_handler(codec, None) - glymur.lib._openjp2.set_error_handler(codec, None) - - cspace = glymur.lib._openjp2.CLRSPC_SRGB - l_image = glymur.lib._openjp2.image_tile_create(l_params, cspace) - - l_image.contents.x0 = 0 - l_image.contents.y0 = 0 - l_image.contents.x1 = image_width - l_image.contents.y1 = image_height - l_image.contents.color_space = glymur.lib._openjp2.CLRSPC_SRGB - - glymur.lib._openjp2.setup_encoder(codec, l_param, l_image) - - x = (filename, False) - stream = glymur.lib._openjp2.stream_create_default_file_stream_v3(*x) - glymur.lib._openjp2.start_compress(codec, l_image, stream) - - for j in np.arange(num_tiles): - glymur.lib._openjp2.write_tile(codec, j, data, tile_size, stream) - - glymur.lib._openjp2.end_compress(codec, stream) - glymur.lib._openjp2.stream_destroy_v3(stream) - glymur.lib._openjp2.destroy_codec(codec) - glymur.lib._openjp2.image_destroy(l_image) - - def tte0_setup(self, filename): - kwargs = {'filename': filename, - 'codec': glymur.lib._openjp2.CODEC_J2K, - 'comp_prec': 8, - 'irreversible': 1, - 'num_comps': 3, - 'image_height': 200, - 'image_width': 200, - 'tile_height': 100, - 'tile_width': 100} - self.tile_encoder(**kwargs) + openjp2.end_decompress(codec, stream) + openjp2.destroy_codec(codec) + openjp2.stream_destroy_v3(stream) + openjp2.image_destroy(image) def test_tte0(self): - # Runs test designated tte0 in OpenJPEG test suite. + """Runs test designated tte0 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - self.tte0_setup(tfile.name) + ttx0_setup(tfile.name) + self.assertTrue(True) def test_ttd0(self): - # Runs test designated ttd0 in OpenJPEG test suite. + """Runs test designated ttd0 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: # Produce the tte0 output file for ttd0 input. - self.tte0_setup(tfile.name) + ttx0_setup(tfile.name) kwargs = {'x0': 0, 'y0': 0, 'x1': 1000, 'y1': 1000, 'filename': tfile.name, - 'codec_format': glymur.lib._openjp2.CODEC_J2K} - self.tile_decoder(**kwargs) + 'codec_format': openjp2.CODEC_J2K} + tile_decoder(**kwargs) + self.assertTrue(True) - def tte1_setup(self, filename): + def xtx1_setup(self, filename): + """Runs tests tte1, rta1.""" kwargs = {'filename': filename, - 'codec': glymur.lib._openjp2.CODEC_J2K, + 'codec': openjp2.CODEC_J2K, 'comp_prec': 8, 'irreversible': 1, 'num_comps': 3, @@ -261,149 +146,298 @@ class TestOpenJP2(unittest.TestCase): 'image_width': 256, 'tile_height': 128, 'tile_width': 128} - self.tile_encoder(**kwargs) + tile_encoder(**kwargs) + self.assertTrue(True) def test_tte1(self): + """Runs test designated tte1 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - # Runs test designated tte1 in OpenJPEG test suite. - self.tte1_setup(tfile.name) + self.xtx1_setup(tfile.name) def test_ttd1(self): - # Runs test designated ttd1 in OpenJPEG test suite. + """Runs test designated ttd1 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: # Produce the tte0 output file for ttd0 input. - self.tte1_setup(tfile.name) + self.xtx1_setup(tfile.name) kwargs = {'x0': 0, 'y0': 0, 'x1': 128, 'y1': 128, 'filename': tfile.name, - 'codec_format': glymur.lib._openjp2.CODEC_J2K} - self.tile_decoder(**kwargs) + 'codec_format': openjp2.CODEC_J2K} + tile_decoder(**kwargs) + self.assertTrue(True) def test_rta1(self): + """Runs test designated rta1 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - # Runs test designated rta1 in OpenJPEG test suite. - self.tte1_setup(tfile.name) + self.xtx1_setup(tfile.name) - kwargs = {'codec_format': glymur.lib._openjp2.CODEC_J2K} - self.j2k_random_tile_access(tfile.name, **kwargs) - - def tte2_setup(self, filename): - kwargs = {'filename': filename, - 'codec': glymur.lib._openjp2.CODEC_JP2, - 'comp_prec': 8, - 'irreversible': 1, - 'num_comps': 3, - 'image_height': 256, - 'image_width': 256, - 'tile_height': 128, - 'tile_width': 128} - self.tile_encoder(**kwargs) + codec_format = openjp2.CODEC_J2K + self.j2k_random_tile_access(tfile.name, codec_format) + self.assertTrue(True) def test_tte2(self): - # Runs test designated tte2 in OpenJPEG test suite. + """Runs test designated tte2 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - self.tte2_setup(tfile.name) + xtx2_setup(tfile.name) + self.assertTrue(True) def test_ttd2(self): - # Runs test designated ttd2 in OpenJPEG test suite. + """Runs test designated ttd2 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: # Produce the tte0 output file for ttd0 input. - self.tte2_setup(tfile.name) + xtx2_setup(tfile.name) kwargs = {'x0': 0, 'y0': 0, 'x1': 128, 'y1': 128, 'filename': tfile.name, - 'codec_format': glymur.lib._openjp2.CODEC_JP2} - self.tile_decoder(**kwargs) + 'codec_format': openjp2.CODEC_JP2} + tile_decoder(**kwargs) + self.assertTrue(True) def test_rta2(self): + """Runs test designated rta2 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - # Runs test designated rta2 in OpenJPEG test suite. - self.tte2_setup(tfile.name) + xtx2_setup(tfile.name) - kwargs = {'codec_format': glymur.lib._openjp2.CODEC_JP2} - self.j2k_random_tile_access(tfile.name, **kwargs) - - def tte3_setup(self, filename): - kwargs = {'filename': filename, - 'codec': glymur.lib._openjp2.CODEC_J2K, - 'comp_prec': 8, - 'irreversible': 1, - 'num_comps': 1, - 'image_height': 256, - 'image_width': 256, - 'tile_height': 128, - 'tile_width': 128} - self.tile_encoder(**kwargs) + codec_format = openjp2.CODEC_JP2 + self.j2k_random_tile_access(tfile.name, codec_format) def test_tte3(self): + """Runs test designated tte3 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - # Runs test designated tte3 in OpenJPEG test suite. - self.tte3_setup(tfile.name) + xtx3_setup(tfile.name) + self.assertTrue(True) def test_rta3(self): - # Runs test designated rta3 in OpenJPEG test suite. + """Runs test designated rta3 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - self.tte3_setup(tfile.name) + xtx3_setup(tfile.name) - kwargs = {'codec_format': glymur.lib._openjp2.CODEC_J2K} - self.j2k_random_tile_access(tfile.name, **kwargs) - - def tte4_setup(self, filename): - kwargs = {'filename': filename, - 'codec': glymur.lib._openjp2.CODEC_J2K, - 'comp_prec': 8, - 'irreversible': 0, - 'num_comps': 1, - 'image_height': 256, - 'image_width': 256, - 'tile_height': 128, - 'tile_width': 128} - self.tile_encoder(**kwargs) + codec_format = openjp2.CODEC_J2K + self.j2k_random_tile_access(tfile.name, codec_format) + self.assertTrue(True) def test_tte4(self): - # Runs test designated tte4 in OpenJPEG test suite. + """Runs test designated tte4 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - self.tte4_setup(tfile.name) + xtx4_setup(tfile.name) + self.assertTrue(True) def test_rta4(self): - # Runs test designated rta4 in OpenJPEG test suite. + """Runs test designated rta4 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - self.tte4_setup(tfile.name) + xtx4_setup(tfile.name) - kwargs = {'codec_format': glymur.lib._openjp2.CODEC_J2K} - self.j2k_random_tile_access(tfile.name, **kwargs) - - def tte5_setup(self, filename): - kwargs = {'filename': filename, - 'codec': glymur.lib._openjp2.CODEC_J2K, - 'comp_prec': 8, - 'irreversible': 0, - 'num_comps': 1, - 'image_height': 512, - 'image_width': 512, - 'tile_height': 256, - 'tile_width': 256} - self.tile_encoder(**kwargs) + codec_format = openjp2.CODEC_J2K + self.j2k_random_tile_access(tfile.name, codec_format) def test_tte5(self): - # Runs test designated tte5 in OpenJPEG test suite. + """Runs test designated tte5 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - self.tte5_setup(tfile.name) + xtx5_setup(tfile.name) + self.assertTrue(True) def test_rta5(self): - # Runs test designated rta5 in OpenJPEG test suite. + """Runs test designated rta5 in OpenJPEG test suite.""" with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: - self.tte5_setup(tfile.name) + xtx5_setup(tfile.name) - kwargs = {'codec_format': glymur.lib._openjp2.CODEC_J2K} - self.j2k_random_tile_access(tfile.name, **kwargs) + codec_format = openjp2.CODEC_J2K + self.j2k_random_tile_access(tfile.name, codec_format) + + +#def tile_encoder(num_comps=None, tile_width=None, tile_height=None, +# filename=None, codec=None, comp_prec=None, +# image_width=None, image_height=None, +# irreversible=None): +def tile_encoder(**kwargs): + """Fixture used by many tests.""" + num_tiles = ((kwargs['image_width'] / kwargs['tile_width']) * + (kwargs['image_height'] / kwargs['tile_height'])) + tile_size = ((kwargs['tile_width'] * kwargs['tile_height']) * + (kwargs['num_comps'] * kwargs['comp_prec'] / 8)) + + data = np.random.random((kwargs['tile_height'], + kwargs['tile_width'], + kwargs['num_comps'])) + data = (data * 255).astype(np.uint8) + + l_param = openjp2.set_default_encoder_parameters() + + l_param.tcp_numlayers = 1 + l_param.cp_fixed_quality = 1 + l_param.tcp_distoratio[0] = 20 + + # position of the tile grid aligned with the image + l_param.cp_tx0 = 0 + l_param.cp_ty0 = 0 + + # tile size, we are using tile based encoding + l_param.tile_size_on = 1 + l_param.cp_tdx = kwargs['tile_width'] + l_param.cp_tdy = kwargs['tile_height'] + + # use irreversible encoding + l_param.irreversible = kwargs['irreversible'] + + l_param.numresolution = 6 + + l_param.prog_order = glymur.core.LRCP + + l_params = (openjp2.ImageComptParmType * kwargs['num_comps'])() + for j in range(kwargs['num_comps']): + l_params[j].dx = 1 + l_params[j].dy = 1 + l_params[j].h = kwargs['image_height'] + l_params[j].w = kwargs['image_width'] + l_params[j].sgnd = 0 + l_params[j].prec = kwargs['comp_prec'] + l_params[j].x0 = 0 + l_params[j].y0 = 0 + + codec = openjp2.create_compress(kwargs['codec']) + + openjp2.set_info_handler(codec, None) + openjp2.set_warning_handler(codec, None) + openjp2.set_error_handler(codec, None) + + cspace = openjp2.CLRSPC_SRGB + l_image = openjp2.image_tile_create(l_params, cspace) + + l_image.contents.x0 = 0 + l_image.contents.y0 = 0 + l_image.contents.x1 = kwargs['image_width'] + l_image.contents.y1 = kwargs['image_height'] + l_image.contents.color_space = openjp2.CLRSPC_SRGB + + openjp2.setup_encoder(codec, l_param, l_image) + + stream = openjp2.stream_create_default_file_stream_v3(kwargs['filename'], + False) + openjp2.start_compress(codec, l_image, stream) + + for j in np.arange(num_tiles): + openjp2.write_tile(codec, j, data, tile_size, stream) + + openjp2.end_compress(codec, stream) + openjp2.stream_destroy_v3(stream) + openjp2.destroy_codec(codec) + openjp2.image_destroy(l_image) + +def tile_decoder(**kwargs): + """Fixture called with various configurations by many tests. + + Reads a tile. That's all it does. + """ + stream = openjp2.stream_create_default_file_stream_v3(kwargs['filename'], + True) + dparam = openjp2.set_default_decoder_parameters() + + dparam.decod_format = kwargs['codec_format'] + + # Do not use layer decoding limitation. + dparam.cp_layer = 0 + + # do not use resolution reductions. + dparam.cp_reduce = 0 + + codec = openjp2.create_decompress(kwargs['codec_format']) + + openjp2.set_info_handler(codec, None) + openjp2.set_warning_handler(codec, None) + openjp2.set_error_handler(codec, None) + + openjp2.setup_decoder(codec, dparam) + image = openjp2.read_header(stream, codec) + openjp2.set_decode_area(codec, image, + kwargs['x0'], kwargs['y0'], + kwargs['x1'], kwargs['y1']) + + data = np.zeros((1150, 2048, 3), dtype=np.uint8) + while True: + rargs = openjp2.read_tile_header(codec, stream) + tidx = rargs[0] + size = rargs[1] + go_on = rargs[-1] + if not go_on: + break + openjp2.decode_tile_data(codec, tidx, data, size, stream) + + openjp2.end_decompress(codec, stream) + openjp2.destroy_codec(codec) + openjp2.stream_destroy_v3(stream) + openjp2.image_destroy(image) + +def ttx0_setup(filename): + """Runs tests tte0, tte0.""" + kwargs = {'filename': filename, + 'codec': openjp2.CODEC_J2K, + 'comp_prec': 8, + 'irreversible': 1, + 'num_comps': 3, + 'image_height': 200, + 'image_width': 200, + 'tile_height': 100, + 'tile_width': 100} + tile_encoder(**kwargs) + +def xtx2_setup(filename): + """Runs tests rta2, tte2, ttd2.""" + kwargs = {'filename': filename, + 'codec': openjp2.CODEC_JP2, + 'comp_prec': 8, + 'irreversible': 1, + 'num_comps': 3, + 'image_height': 256, + 'image_width': 256, + 'tile_height': 128, + 'tile_width': 128} + tile_encoder(**kwargs) + +def xtx3_setup(filename): + """Runs tests tte3, rta3.""" + kwargs = {'filename': filename, + 'codec': openjp2.CODEC_J2K, + 'comp_prec': 8, + 'irreversible': 1, + 'num_comps': 1, + 'image_height': 256, + 'image_width': 256, + 'tile_height': 128, + 'tile_width': 128} + tile_encoder(**kwargs) + +def xtx4_setup(filename): + """Runs tests rta4, tte4.""" + kwargs = {'filename': filename, + 'codec': openjp2.CODEC_J2K, + 'comp_prec': 8, + 'irreversible': 0, + 'num_comps': 1, + 'image_height': 256, + 'image_width': 256, + 'tile_height': 128, + 'tile_width': 128} + tile_encoder(**kwargs) + +def xtx5_setup(filename): + """Runs tests rta5, tte5.""" + kwargs = {'filename': filename, + 'codec': openjp2.CODEC_J2K, + 'comp_prec': 8, + 'irreversible': 0, + 'num_comps': 1, + 'image_height': 512, + 'image_width': 512, + 'tile_height': 256, + 'tile_width': 256} + tile_encoder(**kwargs) if __name__ == "__main__": unittest.main() diff --git a/glymur/lib/test/test_openjpeg.py b/glymur/lib/test/test_openjpeg.py index 3dc7628..91e6d84 100644 --- a/glymur/lib/test/test_openjpeg.py +++ b/glymur/lib/test/test_openjpeg.py @@ -1,4 +1,11 @@ -#pylint: disable-all +""" +Tests for OpenJPEG module. +""" +# unittest2 is python2.6 only (pylint/python-2.7) +# pylint: disable=F0401 + +# pylint: disable=E1101,R0904 + import ctypes import re import sys @@ -10,43 +17,38 @@ else: import glymur - -@unittest.skipIf(glymur.lib._openjpeg.OPENJPEG is None, +@unittest.skipIf(glymur.lib.openjpeg.OPENJPEG is None, "Missing openjpeg library.") class TestOpenJPEG(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass + """Test suite for openjpeg functions we choose to expose.""" def test_version(self): - version = glymur.lib._openjpeg.version() + """Only versions 1.3, 1.4, and 1.5 are supported.""" + version = glymur.lib.openjpeg.version() regex = re.compile('1.[345].[0-9]') if sys.hexversion <= 0x03020000: self.assertRegexpMatches(version, regex) else: self.assertRegex(version, regex) - def test_set_default_decoder_parameters(self): - # Verify that we properly set the default decode parameters. - version = glymur.lib._openjpeg.version() + def test_default_decoder_parameters(self): + """Verify that we properly set the default decode parameters.""" + version = glymur.lib.openjpeg.version() minor = int(version.split('.')[1]) - dp = glymur.lib._openjpeg.DecompressionParametersType() - glymur.lib._openjpeg.set_default_decoder_parameters(ctypes.byref(dp)) + dcp = glymur.lib.openjpeg.DecompressionParametersType() + glymur.lib.openjpeg.set_default_decoder_parameters(ctypes.byref(dcp)) - self.assertEqual(dp.cp_reduce, 0) - self.assertEqual(dp.cp_layer, 0) - self.assertEqual(dp.infile, b'') - self.assertEqual(dp.outfile, b'') - self.assertEqual(dp.decod_format, -1) - self.assertEqual(dp.cod_format, -1) - self.assertEqual(dp.jpwl_correct, 0) - self.assertEqual(dp.jpwl_exp_comps, 0) - self.assertEqual(dp.jpwl_max_tiles, 0) - self.assertEqual(dp.cp_limit_decoding, 0) + self.assertEqual(dcp.cp_reduce, 0) + self.assertEqual(dcp.cp_layer, 0) + self.assertEqual(dcp.infile, b'') + self.assertEqual(dcp.outfile, b'') + self.assertEqual(dcp.decod_format, -1) + self.assertEqual(dcp.cod_format, -1) + self.assertEqual(dcp.jpwl_correct, 0) + self.assertEqual(dcp.jpwl_exp_comps, 0) + self.assertEqual(dcp.jpwl_max_tiles, 0) + self.assertEqual(dcp.cp_limit_decoding, 0) if minor > 4: # Introduced in 1.5.x - self.assertEqual(dp.flags, 0) + self.assertEqual(dcp.flags, 0) diff --git a/glymur/test/__init__.py b/glymur/test/__init__.py index e69de29..b61a5e1 100644 --- a/glymur/test/__init__.py +++ b/glymur/test/__init__.py @@ -0,0 +1,3 @@ +""" +Test suite for glymur high-level functionality. +""" diff --git a/glymur/test/fixtures.py b/glymur/test/fixtures.py index 39b4ac5..b872f1d 100644 --- a/glymur/test/fixtures.py +++ b/glymur/test/fixtures.py @@ -1,15 +1,15 @@ +""" +Test fixtures common to more than one test point. +""" +import os import re import sys +import warnings import numpy as np import glymur -# Need to know the openjpeg version. If openjpeg is not installed, we use -# '0.0.0' -OPENJPEG_VERSION = '0.0.0' -if glymur.lib.openjpeg.OPENJPEG is not None: - OPENJPEG_VERSION = glymur.lib.openjpeg.version() # Need to know of the libopenjp2 version is the official 2.0.0 release and NOT # the 2.0+ development version. @@ -20,6 +20,47 @@ if glymur.lib.openjp2.OPENJP2 is not None: OPENJP2_IS_V2_OFFICIAL = True +NO_READ_BACKEND_MSG = "Matplotlib with the PIL backend must be available in " +NO_READ_BACKEND_MSG += "order to run the tests in this suite." + +try: + OPJ_DATA_ROOT = os.environ['OPJ_DATA_ROOT'] +except KeyError: + OPJ_DATA_ROOT = None +except: + raise + + +def opj_data_file(relative_file_name): + """Compact way of forming a full filename from OpenJPEG's test suite.""" + jfile = os.path.join(OPJ_DATA_ROOT, relative_file_name) + return jfile + +try: + from matplotlib.pyplot import imread + + # The whole point of trying to import PIL is to determine if it's there + # or not. We won't use it directly. + # pylint: disable=F0401,W0611 + import PIL + + NO_READ_BACKEND = False +except ImportError: + NO_READ_BACKEND = True + + +def read_image(infile): + """Read image using matplotlib backend. + + Hopefully PIL(low) is installed as matplotlib's backend. It issues + warnings which we do not care about, so suppress them. + """ + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + data = imread(infile) + return data + + def mse(amat, bmat): """Mean Square Error""" diff = amat.astype(np.double) - bmat.astype(np.double) @@ -36,28 +77,10 @@ def peak_tolerance(amat, bmat): def read_pgx(pgx_file): """Helper function for reading the PGX comparison files. - - Open the file in ascii mode and read the header line. - Will look something like - - PG ML + 8 128 128 - PG%[ \t]%c%c%[ \t+-]%d%[ \t]%d%[ \t]%d" """ - header = '' - with open(pgx_file, 'rb') as fptr: - while True: - char = fptr.read(1) - if char[0] == 10 or char == '\n': - pos = fptr.tell() - break - else: - if sys.hexversion < 0x03000000: - header += char - else: - header += chr(char[0]) + header, pos = read_pgx_header(pgx_file) - header = header.rstrip() - tokens = re.split('\s', header) + tokens = re.split(r'\s', header) if (tokens[1][0] == 'M') and (sys.byteorder == 'little'): swapbytes = True @@ -81,6 +104,29 @@ def read_pgx(pgx_file): nrows = int(tokens[4]) ncols = int(tokens[3]) + dtype = determine_pgx_datatype(signed, bitdepth) + + shape = [nrows, ncols] + + # Reopen the file in binary mode and seek to the start of the binary + # data + with open(pgx_file, 'rb') as fptr: + fptr.seek(pos) + data = np.fromfile(file=fptr, dtype=dtype).reshape(shape) + + return(data.byteswap(swapbytes)) + + +def determine_pgx_datatype(signed, bitdepth): + """Determine the datatype of the PGX file. + + Parameters + ---------- + signed : bool + True if the datatype is signed, false otherwise + bitdepth : int + How many bits are used to make up an image plane. Should be 8 or 16. + """ if signed: if bitdepth <= 8: dtype = np.int8 @@ -96,12 +142,28 @@ def read_pgx(pgx_file): else: raise RuntimeError("unhandled bitdepth") - shape = [nrows, ncols] + return dtype - # Reopen the file in binary mode and seek to the start of the binary - # data + +def read_pgx_header(pgx_file): + """Open the file in ascii mode (not really) and read the header line. + Will look something like + + PG ML + 8 128 128 + PG%[ \t]%c%c%[ \t+-]%d%[ \t]%d%[ \t]%d" + """ + header = '' with open(pgx_file, 'rb') as fptr: - fptr.seek(pos) - data = np.fromfile(file=fptr, dtype=dtype).reshape(shape) + while True: + char = fptr.read(1) + if char[0] == 10 or char == '\n': + pos = fptr.tell() + break + else: + if sys.hexversion < 0x03000000: + header += char + else: + header += chr(char[0]) - return(data.byteswap(swapbytes)) + header = header.rstrip() + return header, pos diff --git a/glymur/test/test_callbacks.py b/glymur/test/test_callbacks.py index 47ec100..722c5da 100644 --- a/glymur/test/test_callbacks.py +++ b/glymur/test/test_callbacks.py @@ -1,6 +1,13 @@ -#pylint: disable-all +""" +Test suite for openjpeg's callback functions. +""" +# R0904: Seems like pylint is fooled in this situation +# pylint: disable=R0904 + +# 'mock' most certainly is in unittest (Python 3.3) +# pylint: disable=E0611,F0401 + import os -import pkg_resources import re import sys import tempfile @@ -24,6 +31,7 @@ import glymur @unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, "Missing openjp2 library.") class TestCallbacks(unittest.TestCase): + """Test suite for callbacks.""" def setUp(self): self.jp2file = glymur.data.nemo() @@ -34,7 +42,7 @@ class TestCallbacks(unittest.TestCase): @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_info_callback_on_write(self): - # Verify the messages printed when writing an image in verbose mode. + """Verify messages printed when writing an image in verbose mode.""" j = glymur.Jp2k(self.jp2file) with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -47,12 +55,14 @@ class TestCallbacks(unittest.TestCase): expected = '[INFO] tile number 1 / 1' self.assertEqual(actual, expected) - def test_info_warning_callbacks_on_read(self): + def test_info_callbacks_on_read(self): + """stdio output when info callback handler is enabled""" + # Verify that we get the expected stdio output when our internal info # callback handler is enabled. j = glymur.Jp2k(self.j2kfile) with patch('sys.stdout', new=StringIO()) as fake_out: - d = j.read(rlevel=1, verbose=True, area=(0, 0, 200, 150)) + j.read(rlevel=1, verbose=True, area=(0, 0, 200, 150)) actual = fake_out.getvalue().strip() lines = ['[INFO] Start to read j2k main header (0).', @@ -80,15 +90,18 @@ class TestCallbacks15(unittest.TestCase): pass def test_info_callbacks_on_read(self): - # Verify that we get the expected stdio output when our internal info - # callback handler is enabled. + """Verify stdout when reading. + + Verify that we get the expected stdio output when our internal info + callback handler is enabled. + """ with patch('glymur.lib.openjp2.OPENJP2', new=None): # Force to use OPENJPEG instead of OPENJP2. j = glymur.Jp2k(self.j2kfile) with patch('sys.stdout', new=StringIO()) as fake_out: - d = j.read(rlevel=1, verbose=True) + j.read(rlevel=1, verbose=True) actual = fake_out.getvalue().strip() - + regex = re.compile(r"""\[INFO\]\stile\s1\sof\s1\s+ \[INFO\]\s-\stiers-1\stook\s [0-9]+\.[0-9]+\ss\s+ @@ -97,6 +110,9 @@ class TestCallbacks15(unittest.TestCase): \[INFO\]\s-\stile\sdecoded\sin\s [0-9]+\.[0-9]+\ss""", re.VERBOSE) + + # assertRegex in Python 3.3 (python2.7/pylint issue) + # pylint: disable=E1101 if sys.hexversion <= 0x03020000: self.assertRegexpMatches(actual, regex) else: diff --git a/glymur/test/test_codestream.py b/glymur/test/test_codestream.py index 31c674c..8db5039 100644 --- a/glymur/test/test_codestream.py +++ b/glymur/test/test_codestream.py @@ -1,4 +1,16 @@ -#pylint: disable-all +""" +Test suite for codestream parsing. +""" + +# unittest doesn't work well with R0904. +# pylint: disable=R0904 + +# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2 +# pylint: disable=E1101 + +# unittest2 is python2.6 only (pylint/python-2.7) +# pylint: disable=F0401 + import os import struct import sys @@ -9,23 +21,21 @@ if sys.hexversion < 0x02070000: else: import unittest -import numpy as np -import pkg_resources - from glymur import Jp2k import glymur try: - data_root = os.environ['OPJ_DATA_ROOT'] + DATA_ROOT = os.environ['OPJ_DATA_ROOT'] except KeyError: - data_root = None + DATA_ROOT = None except: raise -@unittest.skipIf(data_root is None, +@unittest.skipIf(DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") class TestCodestream(unittest.TestCase): + """Test suite for unusual codestream cases.""" def setUp(self): self.jp2file = glymur.data.nemo() @@ -35,75 +45,76 @@ class TestCodestream(unittest.TestCase): @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_reserved_marker_segment(self): + """Reserved marker segments are ok.""" + # Some marker segments were reserved in FCD15444-1. Since that # standard is old, some of them may have come into use. # # 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(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. - buffer = ifile.read(45) - tfile.write(buffer) + read_buffer = ifile.read(45) + tfile.write(read_buffer) # Write the new marker segment, 0xff6f = 65391 - buffer = struct.pack('>HHB', int(65391), int(3), int(0)) - tfile.write(buffer) + read_buffer = struct.pack('>HHB', int(65391), int(3), int(0)) + tfile.write(read_buffer) # Get the rest of the input file. - buffer = ifile.read() - tfile.write(buffer) + read_buffer = ifile.read() + tfile.write(read_buffer) tfile.flush() - j = Jp2k(tfile.name) - c = j.get_codestream() + codestream = Jp2k(tfile.name).get_codestream() - self.assertEqual(c.segment[2].marker_id, '0xff6f') - self.assertEqual(c.segment[2].length, 3) - self.assertEqual(c.segment[2]._data, b'\x00') + self.assertEqual(codestream.segment[2].marker_id, '0xff6f') + self.assertEqual(codestream.segment[2].length, 3) + self.assertEqual(codestream.segment[2].data, b'\x00') - @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") @unittest.skipIf(sys.hexversion < 0x03020000, "Uses features introduced in 3.2.") + @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_unknown_marker_segment(self): + """Should warn for an unknown marker.""" # 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(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. - buffer = ifile.read(45) - tfile.write(buffer) + read_buffer = ifile.read(45) + tfile.write(read_buffer) # Write the new marker segment, 0xff79 = 65401 - buffer = struct.pack('>HHB', int(65401), int(3), int(0)) - tfile.write(buffer) + read_buffer = struct.pack('>HHB', int(65401), int(3), int(0)) + tfile.write(read_buffer) # Get the rest of the input file. - buffer = ifile.read() - tfile.write(buffer) + read_buffer = ifile.read() + tfile.write(read_buffer) tfile.flush() - with self.assertWarns(UserWarning) as cw: - j = Jp2k(tfile.name) - c = j.get_codestream() + with self.assertWarns(UserWarning): + codestream = Jp2k(tfile.name).get_codestream() - self.assertEqual(c.segment[2].marker_id, '0xff79') - self.assertEqual(c.segment[2].length, 3) - self.assertEqual(c.segment[2]._data, b'\x00') + self.assertEqual(codestream.segment[2].marker_id, '0xff79') + self.assertEqual(codestream.segment[2].length, 3) + self.assertEqual(codestream.segment[2].data, b'\x00') def test_psot_is_zero(self): - # Psot=0 in SOT is perfectly legal. Issue #78. - filename = os.path.join(data_root, + """Psot=0 in SOT is perfectly legal. Issue #78.""" + filename = os.path.join(DATA_ROOT, 'input/nonregression/123.j2c') j = Jp2k(filename) - c = j.get_codestream(header_only=False) + codestream = j.get_codestream(header_only=False) # The codestream is valid, so we should be able to get the entire # codestream, so the last one is EOC. - self.assertEqual(c.segment[-1].marker_id, 'EOC') + self.assertEqual(codestream.segment[-1].marker_id, 'EOC') if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_config.py b/glymur/test/test_config.py index 7833865..32f067a 100644 --- a/glymur/test/test_config.py +++ b/glymur/test/test_config.py @@ -1,7 +1,15 @@ """These tests are for edge cases where OPENJPEG does not exist, but OPENJP2 may be present in some form or other. """ -#pylint: disable-all +# unittest doesn't work well with R0904. +# pylint: disable=R0904 + +# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2 +# pylint: disable=E1101 + +# unittest.mock only in Python 3.3 (python2.7/pylint import issue) +# pylint: disable=E0611,F0401 + import imp import os @@ -17,13 +25,9 @@ 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, @@ -31,6 +35,7 @@ from glymur.lib import openjp2 as opj2 @unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, "Needs openjp2 library first before these tests make sense.") class TestSuite(unittest.TestCase): + """Test suite for configuration file operation.""" @classmethod def setUpClass(cls): @@ -56,30 +61,79 @@ class TestSuite(unittest.TestCase): filename = os.path.join(configdir, 'glymurrc') with open(filename, 'wt') as tfile: tfile.write('[library]\n') + + # Need to reliably recover the location of the openjp2 library, + # so using '_name' appears to be the only way to do it. + # pylint: disable=W0212 libloc = glymur.lib.openjp2.OPENJP2._name line = 'openjp2: {0}\n'.format(libloc) tfile.write(line) tfile.flush() with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}): imp.reload(glymur.lib.openjp2) - j = Jp2k(self.jp2file) + Jp2k(self.jp2file) - def test_config_file_via_environ_is_wrong(self): - # A non-existant library location should be rejected. + def test_xdg_env_config_file_is_bad(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 open(fname, 'w') as fptr: with tempfile.NamedTemporaryFile(suffix='.dylib') as tfile: - fp.write('[library]\n') - fp.write('openjp2: {0}.not.there\n'.format(tfile.name)) - fp.flush() + fptr.write('[library]\n') + fptr.write('openjp2: {0}.not.there\n'.format(tfile.name)) + fptr.flush() with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}): # Misconfigured new configuration file should # be rejected. - with self.assertWarns(UserWarning) as cw: + with self.assertWarns(UserWarning): imp.reload(glymur.lib.openjp2) + +@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and + glymur.lib.openjpeg.OPENJPEG is None, + "Missing openjp2 library.") +class TestConfig(unittest.TestCase): + """Test suite for reading without proper library in place.""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + def tearDown(self): + pass + + def test_read_without_library(self): + """Don't have either openjp2 or openjpeg libraries? Must error out. + """ + with patch('glymur.lib.openjp2.OPENJP2', new=None): + with patch('glymur.lib.openjpeg.OPENJPEG', new=None): + with self.assertRaises(glymur.jp2k.LibraryNotFoundError): + glymur.Jp2k(self.jp2file).read() + + def test_read_bands_without_library(self): + """Don't have openjp2 library? Must error out. + """ + with patch('glymur.lib.openjp2.OPENJP2', new=None): + with patch('glymur.lib.openjpeg.OPENJPEG', new=None): + with patch('glymur.version.openjpeg_version_tuple', + new=(0, 0, 0)): + with self.assertRaises(glymur.jp2k.LibraryNotFoundError): + glymur.Jp2k(self.jp2file).read_bands() + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_write_without_library(self): + """Don't have openjpeg libraries? Must error out. + """ + data = glymur.Jp2k(self.j2kfile).read() + with patch('glymur.lib.openjp2.OPENJP2', new=None): + with patch('glymur.lib.openjpeg.OPENJPEG', new=None): + with self.assertRaises(glymur.jp2k.LibraryNotFoundError): + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + ofile = Jp2k(tfile.name, 'wb') + ofile.write(data) + + if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_conformance.py b/glymur/test/test_conformance.py index 9e1ad1a..5f53b5f 100644 --- a/glymur/test/test_conformance.py +++ b/glymur/test/test_conformance.py @@ -1,9 +1,18 @@ """ These tests deal with JPX/JP2/J2K images in the format-corpus repository. """ -#pylint: disable-all +# R0904: Not too many methods in unittest. +# pylint: disable=R0904 + +# E1101: assertWarns introduced in python 3.2 +# pylint: disable=E1101 + +# unittest2 is python2.6 only (pylint/python-2.7) +# pylint: disable=F0401 import os +from os.path import join +import re import sys if sys.hexversion < 0x02070000: @@ -11,106 +20,105 @@ if sys.hexversion < 0x02070000: else: import unittest -import warnings - -from glymur import Jp2k import glymur +from glymur import Jp2k try: - format_corpus_data_root = os.environ['FORMAT_CORPUS_DATA_ROOT'] + FORMAT_CORPUS_DATA_ROOT = os.environ['FORMAT_CORPUS_DATA_ROOT'] except KeyError: - format_corpus_data_root = None + FORMAT_CORPUS_DATA_ROOT = None try: - opj_data_root = os.environ['OPJ_DATA_ROOT'] + OPJ_DATA_ROOT = os.environ['OPJ_DATA_ROOT'] except KeyError: - opj_data_root = None + OPJ_DATA_ROOT = None -@unittest.skipIf(format_corpus_data_root is 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.""" - def setUp(self): - pass - - def tearDown(self): - pass - + @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, + """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): - c = j2k.get_codestream(header_only=False) + codestream = j2k.get_codestream(header_only=False) # The last segment is truncated, so there should not be an EOC marker. - self.assertNotEqual(c.segment[-1].marker_id, 'EOC') + 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, + """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): - c = j2k.get_codestream(header_only=False) + codestream = j2k.get_codestream(header_only=False) # The last segment is truncated, so there should not be an EOC marker. - self.assertNotEqual(c.segment[-1].marker_id, 'EOC') + 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, + """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): - c = j2k.get_codestream(header_only=False) + codestream = j2k.get_codestream(header_only=False) # The last segment is truncated, so there should not be an EOC marker. - self.assertNotEqual(c.segment[-1].marker_id, 'EOC') + 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_vs_any_icc_profile(self): - # If 'jp2 ', then the method cannot be any icc profile. - jfile = os.path.join(format_corpus_data_root, + 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): - j2k = Jp2k(jfile) + Jp2k(jfile) - def test_jp2_brand_vs_any_icc_profile_multiple_colr(self): - # Has colr box, one that conforms, one that does not. + 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. - lst = [format_corpus_data_root, 'jp2k-test', 'icc', - 'balloon_eciRGBv2_ps_adobeplugin_jp2compatible.jpf'] - jfile = os.path.join(*lst) + jfile = join(FORMAT_CORPUS_DATA_ROOT, 'jp2k-test', 'icc', + 'balloon_eciRGBv2_ps_adobeplugin_jp2compatible.jpf') with self.assertWarns(UserWarning): - j2k = Jp2k(jfile) + Jp2k(jfile) -@unittest.skipIf(opj_data_root is None, +@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 @@ -118,12 +126,12 @@ class TestSuiteOpj(unittest.TestCase): def tearDown(self): pass - def test_jp2_brand_vs_any_icc_profile(self): - # If 'jp2 ', then the method cannot be any icc profile. - filename = os.path.join(opj_data_root, + 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): - j2k = Jp2k(filename) + Jp2k(filename) if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_icc.py b/glymur/test/test_icc.py index 4334645..8668999 100644 --- a/glymur/test/test_icc.py +++ b/glymur/test/test_icc.py @@ -1,35 +1,32 @@ -#pylint: disable-all +""" +ICC profile tests. +""" + +# unittest doesn't work well with R0904. +# pylint: disable=R0904 + +# unittest2 is python2.6 only (pylint/python-2.7) +# pylint: disable=F0401 + import datetime import os -import struct import sys -import tempfile if sys.hexversion < 0x02070000: import unittest2 as unittest else: import unittest -import warnings -from xml.etree import cElementTree as ET - import numpy as np -import pkg_resources 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_ROOT, opj_data_file -@unittest.skipIf(data_root is None, +@unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") class TestICC(unittest.TestCase): + """ICC profile tests""" def setUp(self): pass @@ -38,7 +35,8 @@ class TestICC(unittest.TestCase): pass def test_file5(self): - filename = os.path.join(data_root, 'input/conformance/file5.jp2') + """basic ICC profile""" + filename = opj_data_file('input/conformance/file5.jp2') j = Jp2k(filename) profile = j.box[3].box[1].icc_profile self.assertEqual(profile['Size'], 546) @@ -70,10 +68,13 @@ class TestICC(unittest.TestCase): @unittest.skipIf(sys.hexversion < 0x03020000, "Uses features introduced in 3.2.") def test_invalid_profile_header(self): - jfile = os.path.join(data_root, - 'input/nonregression/orb-blue10-lin-jp2.jp2') - with self.assertWarns(UserWarning) as cw: - j = Jp2k(jfile) + """invalid ICC header data should cause UserWarning""" + jfile = opj_data_file('input/nonregression/orb-blue10-lin-jp2.jp2') + + # assertWarns in Python 3.3 (python2.7/pylint issue) + # pylint: disable=E1101 + with self.assertWarns(UserWarning): + Jp2k(jfile) if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index a563b58..ef62460 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -1,8 +1,28 @@ -#pylint: disable-all +""" +Test suite specifically targeting JP2 box layout. +""" +# E1103: return value from read may be list or np array +# pylint: disable=E1103 + +# F0401: unittest2 is needed on python-2.6 (pylint on 2.7) +# pylint: disable=F0401 + +# R0902: More than 7 instance attributes are just fine for testing. +# pylint: disable=R0902 + +# R0904: Seems like pylint is fooled in this situation +# pylint: disable=R0904 + +# W0613: load_tests doesn't need to use ignore or loader arguments. +# pylint: disable=W0613 + import doctest import os +import shutil +import struct import sys import tempfile +import uuid import xml.etree.cElementTree as ET if sys.hexversion < 0x02070000: @@ -11,37 +31,37 @@ else: import unittest import numpy as np -import pkg_resources import glymur from glymur import Jp2k -from glymur.jp2box import * +from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox +from glymur.jp2box import FileTypeBox, ImageHeaderBox, JP2HeaderBox +from glymur.jp2box import JPEG2000SignatureBox from glymur.core import COLOR, OPACITY from glymur.core import RED, GREEN, BLUE, GREY, WHOLE_IMAGE from .fixtures import OPENJP2_IS_V2_OFFICIAL try: - format_corpus_data_root = os.environ['FORMAT_CORPUS_DATA_ROOT'] + FORMAT_CORPUS_DATA_ROOT = os.environ['FORMAT_CORPUS_DATA_ROOT'] except KeyError: - format_corpus_data_root = None + FORMAT_CORPUS_DATA_ROOT = None -# Doc tests should be run as well. def load_tests(loader, tests, ignore): + """Run doc tests as well.""" if os.name == "nt": # Can't do it on windows, temporary file issue. return tests tests.addTests(doctest.DocTestSuite('glymur.jp2box')) return tests - -@unittest.skipIf(OPENJP2_IS_V2_OFFICIAL, - "Requires v2.0.0+ in order to run.") +@unittest.skipIf(glymur.version.openjpeg_version_tuple[0] < 2 or + OPENJP2_IS_V2_OFFICIAL, + "Not supported until 2.0+.") @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, - "Missing openjp2 library.") class TestChannelDefinition(unittest.TestCase): + """Test suite for channel definition boxes.""" @classmethod def setUpClass(cls): @@ -78,12 +98,12 @@ class TestChannelDefinition(unittest.TestCase): 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) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) - self.jP = JPEG2000SignatureBox() + self.jp2b = JPEG2000SignatureBox() self.ftyp = FileTypeBox() self.jp2h = JP2HeaderBox() self.jp2c = ContiguousCodestreamBox() @@ -110,7 +130,7 @@ class TestChannelDefinition(unittest.TestCase): association=association) boxes = [self.ihdr, self.colr_rgb, cdef] self.jp2h.box = boxes - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name, boxes=boxes) @@ -133,7 +153,7 @@ class TestChannelDefinition(unittest.TestCase): association=association) boxes = [self.ihdr, self.colr_rgb, cdef] self.jp2h.box = boxes - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name, boxes=boxes) @@ -156,7 +176,7 @@ class TestChannelDefinition(unittest.TestCase): association=association) boxes = [self.ihdr, self.colr_rgb, cdef] self.jp2h.box = boxes - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name, boxes=boxes) @@ -177,9 +197,9 @@ class TestChannelDefinition(unittest.TestCase): association=association) boxes = [self.ihdr, self.colr_rgb, cdef] self.jp2h.box = boxes - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - with self.assertRaises(IOError) as ce: + with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) def test_grey(self): @@ -191,7 +211,7 @@ class TestChannelDefinition(unittest.TestCase): association=association) boxes = [self.ihdr, self.colr_gr, cdef] self.jp2h.box = boxes - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name, boxes=boxes) @@ -212,7 +232,7 @@ class TestChannelDefinition(unittest.TestCase): association=association) boxes = [self.ihdr, self.colr_gr, cdef] self.jp2h.box = boxes - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name, boxes=boxes) @@ -236,12 +256,12 @@ class TestChannelDefinition(unittest.TestCase): association=association) boxes = [self.ihdr, self.colr_gr, cdef] self.jp2h.box = boxes - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: - with self.assertRaises((OSError, IOError)) as ce: + with self.assertRaises((OSError, IOError)): j2k.wrap(tfile.name, boxes=boxes) - def test_only_one_cdef_in_jp2_header(self): + def test_only_one_cdef_in_jp2h(self): """There can only be one channel definition box in the jp2 header.""" j2k = Jp2k(self.j2kfile) @@ -253,13 +273,14 @@ class TestChannelDefinition(unittest.TestCase): boxes = [self.ihdr, cdef, self.colr_rgb, cdef] self.jp2h.box = boxes - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, 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): + def test_not_in_jp2h(self): + """need cdef in jp2h""" j2k = Jp2k(self.j2kfile) boxes = [self.ihdr, self.colr_rgb] self.jp2h.box = boxes @@ -269,151 +290,47 @@ class TestChannelDefinition(unittest.TestCase): cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, association=association) - boxes = [self.jP, self.ftyp, self.jp2h, cdef, self.jp2c] + boxes = [self.jp2b, 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. + """Channel types are limited to 0, 1, 2, 65535 + Should reject if not all of index, channel_type, association the + same length. + """ channel_type = (COLOR, COLOR, 3) association = (RED, GREEN, BLUE) with self.assertRaises(IOError): - box = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, - association=association) + glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) def test_wrong_lengths(self): - # Should reject if not all of index, channel_type, association the - # same length. + """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): - box = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, - association=association) - - -@unittest.skipIf(os.name == "nt", "Temporary file issue on window.") -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) - - @unittest.skipIf(os.name == "nt", - "Problems using NamedTemporaryFile on windows.") - 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].box_id, 'xml ') - self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()), - b'0') - - @unittest.skipIf(os.name == "nt", - "Problems using NamedTemporaryFile on windows.") - 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.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') + glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) class TestColourSpecificationBox(unittest.TestCase): + """Test suite for colr box instantiation.""" 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) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) - self.jP = JPEG2000SignatureBox() + self.jp2b = JPEG2000SignatureBox() self.ftyp = FileTypeBox() self.jp2h = JP2HeaderBox() self.jp2c = ContiguousCodestreamBox() @@ -425,10 +342,11 @@ class TestColourSpecificationBox(unittest.TestCase): @unittest.skipIf(os.name == "nt", "Problems using NamedTemporaryFile on windows.") - def test_color_specification_box_with_out_enumerated_colorspace(self): + def test_colr_with_out_enum_cspace(self): + """must supply an enumerated colorspace when writing""" j2k = Jp2k(self.j2kfile) - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, 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): @@ -436,47 +354,142 @@ class TestColourSpecificationBox(unittest.TestCase): @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_missing_colr_box(self): + """jp2h must have a colr box""" j2k = Jp2k(self.j2kfile) - boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + boxes = [self.jp2b, 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_default_colr(self): + """basic colr instantiation""" + colr = ColourSpecificationBox(colorspace=glymur.core.SRGB) + self.assertEqual(colr.method, glymur.core.ENUMERATED_COLORSPACE) + self.assertEqual(colr.precedence, 0) + self.assertEqual(colr.approximation, 0) + self.assertEqual(colr.colorspace, glymur.core.SRGB) + self.assertIsNone(colr.icc_profile) - def test_ColourSpecificationBox_with_colorspace_and_icc(self): - # Colour specification boxes can't have both. + def test_colr_with_cspace_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) + rawb = b'\x01\x02\x03\x04' + glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + icc_profile=rawb) - def test_ColourSpecificationBox_with_bad_method(self): + def test_colr_with_bad_method(self): + """colr must have a valid method field""" colorspace = glymur.core.SRGB method = -1 with self.assertRaises(IOError): - b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, - method=method) + glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + method=method) - def test_ColourSpecificationBox_with_bad_approximation(self): + def test_colr_with_bad_approx(self): + """colr must have a valid approximation field""" colorspace = glymur.core.SRGB approx = -1 with self.assertRaises(IOError): - b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, - approximation=approx) + glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, + approximation=approx) + + +class TestAppend(unittest.TestCase): + """Tests for append method.""" + + def setUp(self): + self.j2kfile = glymur.data.goodstuff() + self.jp2file = glymur.data.nemo() + + def tearDown(self): + pass + + def test_append_xml(self): + """Should be able to append an XML box.""" + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + shutil.copyfile(self.jp2file, tfile.name) + + jp2 = Jp2k(tfile.name) + the_xml = ET.fromstring('0') + xmlbox = glymur.jp2box.XMLBox(xml=the_xml) + jp2.append(xmlbox) + + # The sequence of box IDs should be the same as before, but with an + # xml box at the end. + box_ids = [box.box_id for box in jp2.box] + expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml '] + self.assertEqual(box_ids, expected) + self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()), + b'0') + + def test_only_jp2_allowed_to_append(self): + """Only JP2 files are allowed to be appended.""" + with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile: + shutil.copyfile(self.j2kfile, tfile.name) + + jp2 = Jp2k(tfile.name) + + # Make a UUID box. + uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000') + data = b'0123456789' + uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data) + with self.assertRaises(IOError): + jp2.append(uuidbox) + + def test_length_field_is_zero(self): + """L=0 (length field in box header) is handled. + + L=0 implies that the containing box is the last box. If this is not + handled properly, the appended box is never seen. + """ + baseline_jp2 = Jp2k(self.jp2file) + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + with open(self.jp2file, 'rb') as ifile: + # Everything up until the jp2c box. + offset = baseline_jp2.box[-1].offset + tfile.write(ifile.read(offset)) + + # Write the L, T fields of the jp2c box such that L == 0 + write_buffer = struct.pack('>I4s', int(0), b'jp2c') + tfile.write(write_buffer) + + # Write out the rest of the codestream. + ifile.seek(offset+8) + tfile.write(ifile.read()) + tfile.flush() + + jp2 = Jp2k(tfile.name) + the_xml = ET.fromstring('0') + xmlbox = glymur.jp2box.XMLBox(xml=the_xml) + jp2.append(xmlbox) + + # The sequence of box IDs should be the same as before, but with an + # xml box at the end. + box_ids = [box.box_id for box in jp2.box] + expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml '] + self.assertEqual(box_ids, expected) + self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()), + b'0') + + def test_only_xml_allowed_to_append(self): + """Only XML boxes are allowed to be appended.""" + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + shutil.copyfile(self.jp2file, tfile.name) + + jp2 = Jp2k(tfile.name) + + # Make a UUID box. + uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000') + data = b'0123456789' + uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data) + with self.assertRaises(IOError): + jp2.append(uuidbox) -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, - "Missing openjp2 library.") class TestWrap(unittest.TestCase): + """Tests for wrap method.""" def setUp(self): self.j2kfile = glymur.data.goodstuff() @@ -486,7 +499,7 @@ class TestWrap(unittest.TestCase): pass def verify_wrapped_raw(self, jp2file): - # Shared method by at least two tests. + """Shared fixture""" jp2 = Jp2k(jp2file) self.assertEqual(len(jp2.box), 4) @@ -536,6 +549,7 @@ class TestWrap(unittest.TestCase): @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_wrap(self): + """basic test for rewrapping a j2c file, no specified boxes""" j2k = Jp2k(self.j2kfile) with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: j2k.wrap(tfile.name) @@ -543,6 +557,7 @@ class TestWrap(unittest.TestCase): @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_wrap_jp2(self): + """basic test for rewrapping a jp2 file, no specified boxes""" j2k = Jp2k(self.jp2file) with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: jp2 = j2k.wrap(tfile.name) @@ -550,16 +565,17 @@ class TestWrap(unittest.TestCase): self.assertEqual(boxes, ['jP ', 'ftyp', 'jp2h', 'jp2c']) @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") - def test_default_layout_but_with_specified_boxes(self): + def test_default_layout_with_boxes(self): + """basic test for rewrapping a jp2 file, boxes specified""" 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) + codestream = j2k.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), @@ -569,17 +585,17 @@ class TestWrap(unittest.TestCase): self.verify_wrapped_raw(tfile.name) @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") - def test_image_header_box_not_first_in_jp2_header(self): - # The specification says that ihdr must be the first box in jp2h. + def test_ihdr_not_first_in_jp2h(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) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) boxes[2].box = [ColourSpecificationBox(colorspace=glymur.core.SRGB), ImageHeaderBox(height=height, width=width, @@ -589,14 +605,15 @@ class TestWrap(unittest.TestCase): j2k.wrap(tfile.name, boxes=boxes) @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") - def test_first_2_boxes_not_jP_and_ftyp(self): + def test_first_boxes_jp_and_ftyp(self): + """first two boxes must be jP followed by ftyp""" 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) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) - jP = JPEG2000SignatureBox() + jp2b = JPEG2000SignatureBox() ftyp = FileTypeBox() jp2h = JP2HeaderBox() jp2c = ContiguousCodestreamBox() @@ -604,20 +621,21 @@ class TestWrap(unittest.TestCase): ihdr = ImageHeaderBox(height=height, width=width, num_components=num_components) jp2h.box = [ihdr, colr] - boxes = [ftyp, jP, jp2h, jp2c] + boxes = [ftyp, jp2b, jp2h, jp2c] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_jp2h_not_preceeding_jp2c(self): + """jp2h must precede jp2c""" 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) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) - jP = JPEG2000SignatureBox() + jp2b = JPEG2000SignatureBox() ftyp = FileTypeBox() jp2h = JP2HeaderBox() jp2c = ContiguousCodestreamBox() @@ -625,68 +643,74 @@ class TestWrap(unittest.TestCase): ihdr = ImageHeaderBox(height=height, width=width, num_components=num_components) jp2h.box = [ihdr, colr] - boxes = [jP, ftyp, jp2c, jp2h] + boxes = [jp2b, ftyp, jp2c, jp2h] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) @unittest.skipIf(os.name == "nt", "Temporary file issue on window.") def test_missing_codestream(self): + """Need a codestream box in order to call wrap method.""" 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) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) - jP = JPEG2000SignatureBox() + jp2k = JPEG2000SignatureBox() ftyp = FileTypeBox() jp2h = JP2HeaderBox() ihdr = ImageHeaderBox(height=height, width=width, num_components=num_components) jp2h.box = [ihdr] - boxes = [jP, ftyp, jp2h] + boxes = [jp2k, ftyp, jp2h] with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: with self.assertRaises(IOError): j2k.wrap(tfile.name, boxes=boxes) class TestJp2Boxes(unittest.TestCase): + """Tests for canonical JP2 boxes.""" - 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_jp2k(self): + """Should be able to instantiate a JPEG2000SignatureBox""" + jp2k = glymur.jp2box.JPEG2000SignatureBox() + self.assertEqual(jp2k.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_ftyp(self): + """Should be able to instantiate a FileTypeBox""" + ftyp = glymur.jp2box.FileTypeBox() + self.assertEqual(ftyp.brand, 'jp2 ') + self.assertEqual(ftyp.minor_version, 0) + self.assertEqual(ftyp.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_ihdr(self): + """Should be able to instantiate an image header box.""" + ihdr = glymur.jp2box.ImageHeaderBox(height=512, width=256, + num_components=3) + self.assertEqual(ihdr.height, 512) + self.assertEqual(ihdr.width, 256) + self.assertEqual(ihdr.num_components, 3) + self.assertEqual(ihdr.bits_per_component, 8) + self.assertFalse(ihdr.signed) + self.assertFalse(ihdr.colorspace_unknown) - def test_default_JP2HeaderBox(self): - b1 = JP2HeaderBox() - b1.box = [ImageHeaderBox(height=512, width=256), - ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)] + def test_default_jp2headerbox(self): + """Should be able to set jp2h boxes.""" + box = JP2HeaderBox() + box.box = [ImageHeaderBox(height=512, width=256), + ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)] + self.assertTrue(True) - def test_default_ContiguousCodestreamBox(self): - b = ContiguousCodestreamBox() - self.assertEqual(b.box_id, 'jp2c') - self.assertIsNone(b.main_header) + def test_default_ccodestreambox(self): + """Raw instantiation should not produce a main_header.""" + box = ContiguousCodestreamBox() + self.assertEqual(box.box_id, 'jp2c') + self.assertIsNone(box.main_header) class TestJpxBoxes(unittest.TestCase): + """Tests for JPX boxes.""" def setUp(self): pass @@ -694,11 +718,11 @@ class TestJpxBoxes(unittest.TestCase): def tearDown(self): pass - @unittest.skipIf(format_corpus_data_root is None, + @unittest.skipIf(FORMAT_CORPUS_DATA_ROOT is None, "FORMAT_CORPUS_DATA_ROOT environment variable not set") def test_codestream_header(self): - # Should recognize codestream header box. - jfile = os.path.join(format_corpus_data_root, + """Should recognize codestream header box.""" + jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT, 'jp2k-formats/balloon.jpf') jpx = Jp2k(jfile) @@ -706,11 +730,11 @@ class TestJpxBoxes(unittest.TestCase): self.assertEqual(jpx.box[4].box_id, 'jpch') self.assertEqual(len(jpx.box[4].box), 0) - @unittest.skipIf(format_corpus_data_root is None, + @unittest.skipIf(FORMAT_CORPUS_DATA_ROOT is None, "FORMAT_CORPUS_DATA_ROOT environment variable not set") def test_compositing_layer_header(self): - # Should recognize compositing layer header box. - jfile = os.path.join(format_corpus_data_root, + """Should recognize compositing layer header box.""" + jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT, 'jp2k-formats/balloon.jpf') jpx = Jp2k(jfile) diff --git a/glymur/test/test_jp2box_xml.py b/glymur/test/test_jp2box_xml.py new file mode 100644 index 0000000..b875188 --- /dev/null +++ b/glymur/test/test_jp2box_xml.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +""" +Test suite specifically targeting JP2 box layout. +""" +# E1103: return value from read may be list or np array +# pylint: disable=E1103 + +# F0401: unittest2 is needed on python-2.6 (pylint on 2.7) +# pylint: disable=F0401 + +# R0902: More than 7 instance attributes are just fine for testing. +# pylint: disable=R0902 + +# R0904: Seems like pylint is fooled in this situation +# pylint: disable=R0904 + +# W0613: load_tests doesn't need to use ignore or loader arguments. +# pylint: disable=W0613 + +import os +import struct +import sys +import tempfile +import warnings +import xml.etree.cElementTree as ET + +if sys.hexversion < 0x03000000: + from StringIO import StringIO +else: + from io import StringIO + +if sys.hexversion <= 0x03030000: + from mock import patch +else: + from unittest.mock import patch + +if sys.hexversion < 0x02070000: + import unittest2 as unittest +else: + import unittest + +import glymur +from glymur import Jp2k +from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox +from glymur.jp2box import FileTypeBox, ImageHeaderBox, JP2HeaderBox +from glymur.jp2box import JPEG2000SignatureBox + + +@unittest.skipIf(os.name == "nt", "Temporary file issue on window.") +class TestXML(unittest.TestCase): + """Test suite for XML boxes.""" + + 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) + codestream = j2k.get_codestream() + height = codestream.segment[1].ysiz + width = codestream.segment[1].xsiz + num_components = len(codestream.segment[1].xrsiz) + + self.jp2b = 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) + + def test_negative_file_and_xml(self): + """The XML should come from only one source.""" + xml_object = ET.parse(self.xmlfile) + with self.assertRaises((IOError, OSError)): + glymur.jp2box.XMLBox(filename=self.xmlfile, xml=xml_object) + + def test_basic_xml(self): + """Should be able to write a basic 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.jp2b, 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].box_id, 'xml ') + self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()), + b'0') + + def test_xml_from_file(self): + """Must be able to create an XML box from an XML file.""" + j2k = Jp2k(self.j2kfile) + + self.jp2h.box = [self.ihdr, self.colr] + + xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile) + boxes = [self.jp2b, 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.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') + + def test_utf8_xml(self): + """Should be able to write/read an XMLBox with utf-8 encoding.""" + # 'Россия' is 'Russia' in Cyrillic, not that it matters. + xml = u""" + Россия""" + with tempfile.NamedTemporaryFile(suffix=".xml") as xmlfile: + xmlfile.write(xml.encode('utf-8')) + xmlfile.flush() + + j2k = glymur.Jp2k(self.j2kfile) + with tempfile.NamedTemporaryFile(suffix=".jp2") as jfile: + jp2 = j2k.wrap(jfile.name) + xmlbox = glymur.jp2box.XMLBox(filename=xmlfile.name) + jp2.append(xmlbox) + + box_xml = jp2.box[-1].xml.getroot() + box_xml_str = ET.tostring(box_xml, + encoding='utf-8').decode('utf-8') + self.assertEqual(box_xml_str, + u'Россия') + + + +@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") +class TestJp2kBadXmlFile(unittest.TestCase): + """Test suite for bad XML box situations""" + + @classmethod + def setUpClass(cls): + """Setup a JP2 file with a bad XML box. We only need to do this once + per class rather than once per test. + """ + jp2file = glymur.data.nemo() + with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: + cls._bad_xml_file = tfile.name + with open(jp2file, 'rb') as ifile: + # Everything up until the UUID box. + write_buffer = ifile.read(77) + tfile.write(write_buffer) + + # Write the xml box with bad xml + # Length = 28, id is 'xml '. + write_buffer = struct.pack('>I4s', int(28), b'xml ') + tfile.write(write_buffer) + + write_buffer = 'this is a test' + write_buffer = write_buffer.encode() + tfile.write(write_buffer) + + # Get the rest of the input file. + write_buffer = ifile.read() + tfile.write(write_buffer) + tfile.flush() + + @classmethod + def tearDownClass(cls): + os.unlink(cls._bad_xml_file) + + def setUp(self): + self.jp2file = glymur.data.nemo() + + def tearDown(self): + pass + + @unittest.skipIf(sys.hexversion < 0x03020000, + "Uses features introduced in 3.2.") + def test_invalid_xml_box_warning(self): + """Should warn in case of bad XML""" + with self.assertWarns(UserWarning): + Jp2k(self._bad_xml_file) + + def test_invalid_xml_box(self): + """Should be able to recover info from xml box with bad xml.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + jp2k = Jp2k(self._bad_xml_file) + + self.assertEqual(jp2k.box[3].box_id, 'xml ') + self.assertEqual(jp2k.box[3].offset, 77) + self.assertEqual(jp2k.box[3].length, 28) + self.assertIsNone(jp2k.box[3].xml) + + +@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") +class TestBadButRecoverableXmlFile(unittest.TestCase): + """Test suite for XML box that is bad, but we can still recover the XML.""" + + @classmethod + def setUpClass(cls): + """Setup a JP2 file with bad bytes preceding the XML. We only need + to do this once per class rather than once per test. + """ + jp2file = glymur.data.nemo() + with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: + cls._bad_xml_file = tfile.name + with open(jp2file, 'rb') as ifile: + # Everything up until the UUID box. + write_buffer = ifile.read(77) + tfile.write(write_buffer) + + # Write the xml box with bad xml + # Length = 64, id is 'xml '. + write_buffer = struct.pack('>I4s', int(64), b'xml ') + tfile.write(write_buffer) + + # Write out 8 bad bytes. + write_buffer = b'\x00\x00\x07\x90xml ' + tfile.write(write_buffer) + + # Write out 48 good bytes constituting the XML payload. + write_buffer = b'' + tfile.write(write_buffer) + write_buffer = b'this is a test' + tfile.write(write_buffer) + + # Get the rest of the input file. + write_buffer = ifile.read() + tfile.write(write_buffer) + tfile.flush() + + @classmethod + def tearDownClass(cls): + os.unlink(cls._bad_xml_file) + + @unittest.skipIf(sys.hexversion < 0x03020000, + "Uses features introduced in 3.2.") + def test_bad_xml_box_warning(self): + """Should warn in case of bad XML""" + with self.assertWarns(UserWarning): + Jp2k(self._bad_xml_file) + + def test_recover_from_bad_xml(self): + """Should be able to recover info from xml box with bad xml.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + jp2 = Jp2k(self._bad_xml_file) + + self.assertEqual(jp2.box[3].box_id, 'xml ') + self.assertEqual(jp2.box[3].offset, 77) + self.assertEqual(jp2.box[3].length, 64) + self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()), + b'this is a test') + + diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index f0db4b7..636ea9a 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -1,4 +1,15 @@ -# pylint: disable-all +""" +Tests for general glymur functionality. +""" +# E1101: assertWarns introduced in python 3.2 +# pylint: disable=E1101 + +# R0904: Not too many methods in unittest. +# pylint: disable=R0904 + +# E0611: unittest.mock is unknown to python2.7/pylint +# pylint: disable=E0611,F0401 + import doctest import os import re @@ -7,16 +18,13 @@ import struct import sys import tempfile import uuid +from xml.etree import cElementTree as ET if sys.hexversion < 0x02070000: import unittest2 as unittest else: import unittest -if sys.hexversion <= 0x03030000: - from mock import patch -else: - from unittest.mock import patch import warnings import numpy as np @@ -24,20 +32,18 @@ import pkg_resources import glymur from glymur import Jp2k -from glymur.lib import openjp2 as opj2 from .fixtures import OPENJP2_IS_V2_OFFICIAL - -try: - data_root = os.environ['OPJ_DATA_ROOT'] -except KeyError: - data_root = None -except: - raise +from .fixtures import OPJ_DATA_ROOT, opj_data_file # Doc tests should be run as well. def load_tests(loader, tests, ignore): + # W0613: "loader" and "ignore" are necessary for the protocol + # They are unused here, however. + # pylint: disable=W0613 + + """Should run doc tests as well""" if os.name == "nt": # Can't do it on windows, temporary file issue. return tests @@ -49,113 +55,10 @@ def load_tests(loader, tests, ignore): return tests -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and - glymur.lib.openjpeg.OPENJPEG is None, - "Missing openjp2 library.") -class TestConfig(unittest.TestCase): - - def setUp(self): - self.jp2file = glymur.data.nemo() - self.j2kfile = glymur.data.goodstuff() - - def tearDown(self): - pass - - def test_read_without_library_backing_us_up(self): - """Don't have either openjp2 or openjpeg libraries? Must error out. - """ - with patch('glymur.lib.openjp2.OPENJP2', new=None): - with patch('glymur.lib.openjpeg.OPENJPEG', new=None): - with self.assertRaises(glymur.jp2k.LibraryNotFoundError): - d = glymur.Jp2k(self.jp2file).read() - - def test_read_bands_without_library_backing_us_up(self): - """Don't have openjp2 library? Must error out. - """ - with patch('glymur.lib.openjp2.OPENJP2', new=None): - with patch('glymur.lib.openjpeg.OPENJPEG', new=None): - with self.assertRaises(glymur.jp2k.LibraryNotFoundError): - d = glymur.Jp2k(self.jp2file).read_bands() - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_write_without_library_backing_us_up(self): - """Don't have openjp2 library? Must error out. - """ - data = glymur.Jp2k(self.j2kfile).read() - with patch('glymur.lib.openjp2.OPENJP2', new=None): - with patch('glymur.lib.openjpeg.OPENJPEG', new=None): - with self.assertRaises(glymur.jp2k.LibraryNotFoundError): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - ofile = Jp2k(tfile.name, 'wb') - ofile.write(data) - - -@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, - "Missing openjp2 library.") -class TestJp2kBadXmlFile(unittest.TestCase): - - @classmethod - def setUpClass(cls): - # Setup a JP2 file with a bad XML box. We only need to do this once - # per class rather than once per test. - jp2file = pkg_resources.resource_filename(glymur.__name__, - "data/nemo.jp2") - with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: - cls._bad_xml_file = tfile.name - with open(jp2file, 'rb') as ifile: - # Everything up until the jp2c box. - buffer = ifile.read(77) - tfile.write(buffer) - - # Write the xml box with bad xml - # Length = 28, id is 'xml '. - buffer = struct.pack('>I4s', int(28), b'xml ') - tfile.write(buffer) - - buffer = 'this is a test' - buffer = buffer.encode() - tfile.write(buffer) - - # Get the rest of the input file. - buffer = ifile.read() - tfile.write(buffer) - tfile.flush() - - @classmethod - def tearDownClass(cls): - os.unlink(cls._bad_xml_file) - - def setUp(self): - self.jp2file = glymur.data.nemo() - self.j2kfile = glymur.data.goodstuff() - - def tearDown(self): - pass - - @unittest.skipIf(sys.hexversion < 0x03020000, - "Uses features introduced in 3.2.") - def test_invalid_xml_box_warning(self): - # Should be able to recover from xml box with bad xml. - # Just verify that a warning is issued on 3.3+ - with self.assertWarns(UserWarning) as cw: - jp2k = Jp2k(self._bad_xml_file) - - def test_invalid_xml_box(self): - # Should be able to recover from xml box with bad xml. - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - jp2k = Jp2k(self._bad_xml_file) - - self.assertEqual(jp2k.box[3].box_id, 'xml ') - self.assertEqual(jp2k.box[3].offset, 77) - self.assertEqual(jp2k.box[3].length, 28) - self.assertIsNone(jp2k.box[3].xml) - - -@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, - "Missing openjp2 library.") class TestJp2k(unittest.TestCase): + """Test suite for openjpeg software starting at 1.3""" + + # These tests should be run by just about all configuration. def setUp(self): self.jp2file = glymur.data.nemo() @@ -165,88 +68,35 @@ class TestJp2k(unittest.TestCase): pass def test_rlevel_max(self): - # Verify that rlevel=-1 gets us the lowest resolution image + """Verify that rlevel=-1 gets us the lowest resolution image""" j = Jp2k(self.j2kfile) thumbnail1 = j.read(rlevel=-1) thumbnail2 = j.read(rlevel=5) np.testing.assert_array_equal(thumbnail1, thumbnail2) self.assertEqual(thumbnail1.shape, (25, 15, 3)) - def test_bad_area_parameter(self): - # Verify that we error out appropriately if given a bad area parameter. - j = Jp2k(self.jp2file) - with self.assertRaises(IOError): - # Start corner must be >= 0 - d = j.read(area=(-1, -1, 1, 1)) - with self.assertRaises(IOError): - # End corner must be > 0 - d = j.read(area=(10, 10, 0, 0)) - with self.assertRaises(IOError): - # End corner must be >= start corner - d = j.read(area=(10, 10, 8, 8)) - def test_rlevel_too_high(self): - # Verify that we error out appropriately if not given a JPEG 2000 file. + """Should error out appropriately if reduce level too high""" j = Jp2k(self.jp2file) with self.assertRaises(IOError): - d = j.read(rlevel=6) + j.read(rlevel=6) - def test_not_JPEG2000(self): - # Verify that we error out appropriately if not given a JPEG 2000 file. + def test_not_jpeg2000(self): + """Should error out appropriately if not given a JPEG 2000 file.""" filename = pkg_resources.resource_filename(glymur.__name__, "jp2k.py") with self.assertRaises(IOError): - jp2k = Jp2k(filename) + Jp2k(filename) def test_file_not_present(self): + """Should error out if reading from a file that does not exist""" # Verify that we error out appropriately if not given an existing file # at all. - if sys.hexversion < 0x03030000: - error = OSError - else: - error = IOError - with self.assertRaises(error): + with self.assertRaises(OSError): filename = 'this file does not actually exist on the file system.' - jp2k = Jp2k(filename) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - 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 - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - 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) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_write_cprl(self): - # Issue 17 - j = Jp2k(self.jp2file) - expdata = j.read(rlevel=1) - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - ofile = Jp2k(tfile.name, 'wb') - ofile.write(expdata, prog='CPRL') - actdata = ofile.read() - np.testing.assert_array_equal(actdata, expdata) - - c = ofile.get_codestream() - self.assertEqual(c.segment[2].spcod[0], glymur.core.CPRL) + Jp2k(filename) def test_jp2_boxes(self): - # Verify the boxes of a JP2 file. + """Verify the boxes of a JP2 file. Basic jp2 test.""" jp2k = Jp2k(self.jp2file) # top-level boxes @@ -304,39 +154,38 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[2].box[1].colorspace, glymur.core.SRGB) self.assertIsNone(jp2k.box[2].box[1].icc_profile) - @unittest.skipIf(data_root is None, - "OPJ_DATA_ROOT environment variable not set") def test_j2k_box(self): + """A J2K/J2C file must not have any boxes.""" # Verify that a J2K file has no boxes. - filename = os.path.join(data_root, 'input/conformance/p0_01.j2k') - jp2k = Jp2k(filename) + jp2k = Jp2k(self.j2kfile) self.assertEqual(len(jp2k.box), 0) @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_64bit_XL_field(self): + def test_64bit_xl_field(self): + """XL field should be supported""" # Verify that boxes with the XL field are properly read. # Don't have such a file on hand, so we create one. Copy our example # file, but making the codestream have a 64-bit XL field. with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: with open(self.jp2file, 'rb') as ifile: # Everything up until the jp2c box. - buffer = ifile.read(3127) - tfile.write(buffer) + write_buffer = ifile.read(3127) + tfile.write(write_buffer) # The L field must be 1 in order to signal the presence of the # XL field. The actual length of the jp2c box increased by 8 # (8 bytes for the XL field). - L = 1 - T = b'jp2c' - XL = 1133427 + 8 - buffer = struct.pack('>I4sQ', int(L), T, XL) - tfile.write(buffer) + length = 1 + typ = b'jp2c' + xlen = 1133427 + 8 + write_buffer = struct.pack('>I4sQ', int(length), typ, xlen) + tfile.write(write_buffer) # Get the rest of the input file (minus the 8 bytes for L and # T. ifile.seek(8, 1) - buffer = ifile.read() - tfile.write(buffer) + write_buffer = ifile.read() + tfile.write(write_buffer) tfile.flush() jp2k = Jp2k(tfile.name) @@ -346,7 +195,8 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[5].length, 1133427 + 8) @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_L_is_zero(self): + def test_length_field_is_zero(self): + """L=0 (length field in box header) is allowed""" # Verify that boxes with the L field as zero are correctly read. # This should only happen in the last box of a JPEG 2000 file. # Our example image has its last box at byte 588458. @@ -354,19 +204,19 @@ class TestJp2k(unittest.TestCase): with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: with open(self.jp2file, 'rb') as ifile: # Everything up until the jp2c box. - buffer = ifile.read(588458) - tfile.write(buffer) + write_buffer = ifile.read(588458) + tfile.write(write_buffer) - L = 0 - T = b'uuid' - buffer = struct.pack('>I4s', int(L), T) - tfile.write(buffer) + length = 0 + typ = b'uuid' + write_buffer = struct.pack('>I4s', int(length), typ) + tfile.write(write_buffer) # Get the rest of the input file (minus the 8 bytes for L and # T. ifile.seek(8, 1) - buffer = ifile.read() - tfile.write(buffer) + write_buffer = ifile.read() + tfile.write(write_buffer) tfile.flush() new_jp2 = Jp2k(tfile.name) @@ -380,198 +230,77 @@ class TestJp2k(unittest.TestCase): self.assertEqual(new_jp2.box[j].length, baseline_jp2.box[j].length) - @unittest.skipIf(data_root is None, + def test_basic_jp2(self): + """Just a very basic test that reading a JP2 file does not error out. + """ + j2k = Jp2k(self.jp2file) + j2k.read(rlevel=1) + + def test_basic_j2k(self): + """This test is only useful when openjp2 is not available + and OPJ_DATA_ROOT is not set. We need at least one + working J2K test. + """ + 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 = os.path.join(data_root, 'input/conformance/p0_05.j2k') + filename = opj_data_file('input/conformance/p0_05.j2k') j = Jp2k(filename) with self.assertRaises(RuntimeError): j.read() - @unittest.skipIf(data_root is None, - "OPJ_DATA_ROOT environment variable not set") def test_empty_box_with_j2k(self): - # Verify that the list of boxes in a J2C/J2K file is present, but - # empty. - filename = os.path.join(data_root, 'input/conformance/p0_05.j2k') - j = Jp2k(filename) + """Verify that the list of boxes in a J2C/J2K file is present, but + empty. + """ + j = Jp2k(self.j2kfile) self.assertEqual(j.box, []) - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_code_block_height_different_than_width(self): - # Verify that we can set a code block size where height does not equal - # width. - data = np.zeros((128, 128), dtype=np.uint8) - with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') - - # The code block dimensions are given as rows x columns. - j.write(data, cbsize=(16, 32)) - - c = j.get_codestream() - - # Code block size is reported as XY in the codestream. - self.assertEqual(tuple(c.segment[2].spcod[5:7]), (3, 2)) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_negative_too_many_dimensions(self): - # OpenJP2 only allows 2D or 3D images. - with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') - with self.assertRaises(IOError) as ce: - data = np.zeros((128, 128, 2, 2), dtype=np.uint8) - j.write(data) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_unrecognized_jp2_colorspace(self): - # We only allow RGB and GRAYSCALE. - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - with self.assertRaises(IOError) as ce: - data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='cmyk') - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_2D_rgb(self): - # RGB must have at least 3 components. - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - with self.assertRaises(IOError) as ce: - data = np.zeros((128, 128, 2), dtype=np.uint8) - j.write(data, colorspace='rgb') - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_colorspace_with_j2k(self): - # Specifying a colorspace with J2K does not make sense. - with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') - with self.assertRaises(IOError) as ce: - data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='rgb') - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_specify_rgb(self): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='rgb') - self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_specify_gray(self): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - data = np.zeros((128, 128), dtype=np.uint8) - j.write(data, colorspace='gray') - self.assertEqual(j.box[2].box[1].colorspace, - glymur.core.GREYSCALE) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_specify_grey(self): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - data = np.zeros((128, 128), dtype=np.uint8) - j.write(data, colorspace='grey') - self.assertEqual(j.box[2].box[1].colorspace, - glymur.core.GREYSCALE) - - @unittest.skipIf(OPENJP2_IS_V2_OFFICIAL, - "Does not seem to work on official v2.0.0 release.") - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_grey_with_extra_component(self): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - data = np.zeros((128, 128, 2), dtype=np.uint8) - j.write(data) - self.assertEqual(j.box[2].box[0].height, 128) - self.assertEqual(j.box[2].box[0].width, 128) - self.assertEqual(j.box[2].box[0].num_components, 2) - self.assertEqual(j.box[2].box[1].colorspace, - glymur.core.GREYSCALE) - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_grey_with_two_extra_components(self): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='gray') - self.assertEqual(j.box[2].box[0].height, 128) - self.assertEqual(j.box[2].box[0].width, 128) - self.assertEqual(j.box[2].box[0].num_components, 3) - self.assertEqual(j.box[2].box[1].colorspace, - glymur.core.GREYSCALE) - - @unittest.skipIf(OPENJP2_IS_V2_OFFICIAL, - "Does not seem to work on official v2.0.0 release.") - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_rgb_with_extra_component(self): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - data = np.zeros((128, 128, 4), dtype=np.uint8) - j.write(data) - self.assertEqual(j.box[2].box[0].height, 128) - self.assertEqual(j.box[2].box[0].width, 128) - self.assertEqual(j.box[2].box[0].num_components, 4) - self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB) - - @unittest.skipIf(OPENJP2_IS_V2_OFFICIAL is False, - "Test is specific for v2.0.0 release") - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_extra_components_on_v2_official(self): - # Extra components seems to require 2.0+. Verify that we error out. - with self.assertRaises(IOError): - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - data = np.zeros((128, 128, 4), dtype=np.uint8) - j.write(data) - - def test_specify_ycc(self): - # We don't support writing YCC at the moment. - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - with self.assertRaises(IOError) as ce: - data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='ycc') - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") def test_uinf_ulst_url_boxes(self): + """Verify that we can read UINF, ULST, and URL boxes""" # Verify that we can read UINF, ULST, and URL boxes. I don't have # easy access to such a file, and there's no such file in the # openjpeg repository, so I'll fake one. with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: with open(self.jp2file, 'rb') as ifile: # Everything up until the jp2c box. - buffer = ifile.read(77) - tfile.write(buffer) + write_buffer = ifile.read(77) + tfile.write(write_buffer) # Write the UINF superbox # Length = 50, id is uinf. - buffer = struct.pack('>I4s', int(50), b'uinf') - tfile.write(buffer) + write_buffer = struct.pack('>I4s', int(50), b'uinf') + tfile.write(write_buffer) # Write the ULST box. # Length is 26, 1 UUID, hard code that UUID as zeros. - buffer = struct.pack('>I4sHIIII', int(26), b'ulst', int(1), - int(0), int(0), int(0), int(0)) - tfile.write(buffer) + write_buffer = struct.pack('>I4sHIIII', int(26), b'ulst', + int(1), int(0), int(0), int(0), + int(0)) + tfile.write(write_buffer) # Write the URL box. # Length is 16, version is one byte, flag is 3 bytes, url # is the rest. - buffer = struct.pack('>I4sBBBB', - int(16), b'url ', - int(0), int(0), int(0), int(0)) - tfile.write(buffer) - buffer = struct.pack('>ssss', b'a', b'b', b'c', b'd') - tfile.write(buffer) + write_buffer = struct.pack('>I4sBBBB', + int(16), b'url ', + int(0), int(0), int(0), int(0)) + tfile.write(write_buffer) + write_buffer = struct.pack('>ssss', b'a', b'b', b'c', b'd') + tfile.write(write_buffer) # Get the rest of the input file. - buffer = ifile.read() - tfile.write(buffer) + write_buffer = ifile.read() + tfile.write(write_buffer) tfile.flush() jp2k = Jp2k(tfile.name) @@ -595,27 +324,26 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[3].box[1].url, 'abcd') @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_xml_box_with_trailing_nulls(self): - # ElementTree does not like trailing null chars after valid XML - # text. + def test_xml_with_trailing_nulls(self): + """ElementTree doesn't like trailing null chars after valid XML text""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: with open(self.jp2file, 'rb') as ifile: # Everything up until the jp2c box. - buffer = ifile.read(77) - tfile.write(buffer) + write_buffer = ifile.read(77) + tfile.write(write_buffer) # Write the xml box # Length = 36, id is 'xml '. - buffer = struct.pack('>I4s', int(36), b'xml ') - tfile.write(buffer) + write_buffer = struct.pack('>I4s', int(36), b'xml ') + tfile.write(write_buffer) - buffer = 'this is a test' + chr(0) - buffer = buffer.encode() - tfile.write(buffer) + write_buffer = 'this is a test' + chr(0) + write_buffer = write_buffer.encode() + tfile.write(write_buffer) # Get the rest of the input file. - buffer = ifile.read() - tfile.write(buffer) + write_buffer = ifile.read() + tfile.write(write_buffer) tfile.flush() jp2k = Jp2k(tfile.name) @@ -623,9 +351,297 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[3].box_id, 'xml ') self.assertEqual(jp2k.box[3].offset, 77) self.assertEqual(jp2k.box[3].length, 36) + self.assertEqual(ET.tostring(jp2k.box[3].xml.getroot()), + b'this is a test') + + def test_xmp_attribute(self): + """Verify the XMP packet in the shipping example file can be read.""" + j = Jp2k(self.jp2file) + xmp = j.box[4].data + ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' + ns1 = '{http://ns.adobe.com/xap/1.0/}' + name = '{0}RDF/{0}Description'.format(ns0) + elt = xmp.find(name) + attr_value = elt.attrib['{0}CreatorTool'.format(ns1)] + self.assertEqual(attr_value, 'glymur') + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_unrecognized_exif_tag(self): + """An unrecognized exif tag should be handled gracefully.""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + shutil.copyfile(self.jp2file, tfile.name) + + # The Exif UUID starts at byte 77. There are 8 bytes for the L and + # T fields, then 16 bytes for the UUID identifier, then 6 exif + # header bytes, then 8 bytes for the TIFF header, then 2 bytes + # the the Image IFD number of tags, where we finally find the first + # tag, "Make" (271). We'll corrupt it by changing it into 171, + # which does not correspond to any known Exif Image tag. + with open(tfile.name, 'r+b') as fptr: + fptr.seek(117) + write_buffer = struct.pack('= 2, + "Negative tests only for version 1.x") +class TestJp2k_1_x(unittest.TestCase): + """Test suite for openjpeg 1.x, not appropriate for 2.x""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + def tearDown(self): + pass + + def test_area(self): + """Area option not allowed for 1.x. + """ + j2k = Jp2k(self.j2kfile) + with self.assertRaises(TypeError): + j2k.read(area=(0, 0, 100, 100)) + + def test_tile(self): + """tile option not allowed for 1.x. + """ + j2k = Jp2k(self.j2kfile) + with self.assertRaises(TypeError): + j2k.read(tile=0) + + def test_layer(self): + """layer option not allowed for 1.x. + """ + j2k = Jp2k(self.j2kfile) + with self.assertRaises(TypeError): + j2k.read(layer=1) + + +@unittest.skipIf(not OPENJP2_IS_V2_OFFICIAL, + "Tests only to be run on 2.0 official.") +class TestJp2k_2_0_official(unittest.TestCase): + """Test suite to only be run on v2.0 official.""" + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_extra_components_on_v2(self): + """Can only write 4 components on 2.0+, should error out otherwise.""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 4), dtype=np.uint8) + with self.assertRaises(IOError): + j.write(data) + + +@unittest.skipIf(glymur.version.openjpeg_version_tuple[0] < 2, + "Requires as least v2.0") +class TestJp2k_2_0(unittest.TestCase): + """Test suite requiring at least version 2.0""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + def tearDown(self): + pass + + def test_bad_area_parameter(self): + """Should error out appropriately if given a bad area parameter.""" + j = Jp2k(self.jp2file) + with self.assertRaises(IOError): + # Start corner must be >= 0 + j.read(area=(-1, -1, 1, 1)) + with self.assertRaises(IOError): + # End corner must be > 0 + j.read(area=(10, 10, 0, 0)) + with self.assertRaises(IOError): + # End corner must be >= start corner + j.read(area=(10, 10, 8, 8)) + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_unrecognized_jp2_clrspace(self): + """We only allow RGB and GRAYSCALE. Should error out with others""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError): + data = np.zeros((128, 128, 3), dtype=np.uint8) + j.write(data, colorspace='cmyk') @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") def test_asoc_label_box(self): + """Test asoc and label box""" # Construct a fake file with an asoc and a label box, as # OpenJPEG doesn't have such a file. data = Jp2k(self.jp2file).read(rlevel=1) @@ -636,30 +652,30 @@ class TestJp2k(unittest.TestCase): with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile2: # Offset of the codestream is where we start. - buffer = tfile.read(77) - tfile2.write(buffer) + read_buffer = tfile.read(77) + tfile2.write(read_buffer) # read the rest of the file, it's the codestream. codestream = tfile.read() # Write the asoc superbox. # Length = 36, id is 'asoc'. - buffer = struct.pack('>I4s', int(56), b'asoc') - tfile2.write(buffer) + write_buffer = struct.pack('>I4s', int(56), b'asoc') + tfile2.write(write_buffer) # Write the contained label box - buffer = struct.pack('>I4s', int(13), b'lbl ') - tfile2.write(buffer) + write_buffer = struct.pack('>I4s', int(13), b'lbl ') + tfile2.write(write_buffer) tfile2.write('label'.encode()) # Write the xml box # Length = 36, id is 'xml '. - buffer = struct.pack('>I4s', int(35), b'xml ') - tfile2.write(buffer) + write_buffer = struct.pack('>I4s', int(35), b'xml ') + tfile2.write(write_buffer) - buffer = 'this is a test' - buffer = buffer.encode() - tfile2.write(buffer) + write_buffer = 'this is a test' + write_buffer = write_buffer.encode() + tfile2.write(write_buffer) # Now append the codestream. tfile2.write(codestream) @@ -671,14 +687,51 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jasoc.box[3].box[0].label, 'label') self.assertEqual(jasoc.box[3].box[1].box_id, 'xml ') + +@unittest.skipIf(glymur.version.openjpeg_version_tuple[0] < 2 or + OPENJP2_IS_V2_OFFICIAL, + "Missing openjp2 library version 2.0+.") +class TestJp2k_2_1(unittest.TestCase): + """Only to be run in 2.0+.""" + + def setUp(self): + self.jp2file = glymur.data.nemo() + self.j2kfile = glymur.data.goodstuff() + + def tearDown(self): + pass + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_grey_with_extra_component(self): + """version 2.0 cannot write gray + extra""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 2), dtype=np.uint8) + j.write(data) + self.assertEqual(j.box[2].box[0].height, 128) + self.assertEqual(j.box[2].box[0].width, 128) + self.assertEqual(j.box[2].box[0].num_components, 2) + self.assertEqual(j.box[2].box[1].colorspace, + glymur.core.GREYSCALE) + + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") + def test_rgb_with_extra_component(self): + """v2.0+ should be able to write extra components""" + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 4), dtype=np.uint8) + j.write(data) + self.assertEqual(j.box[2].box[0].height, 128) + self.assertEqual(j.box[2].box[0].width, 128) + self.assertEqual(j.box[2].box[0].num_components, 4) + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB) + @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - @unittest.skipIf(OPENJP2_IS_V2_OFFICIAL, - "Segfault on official v2.0.0 release.") def test_openjpeg_library_message(self): - # Verify the error message produced by the openjpeg library. + """Verify the error message produced by the openjpeg library""" # This will confirm that the error callback mechanism is working. - with open(self.jp2file, 'rb') as fp: - data = fp.read() + with open(self.jp2file, 'rb') as fptr: + data = fptr.read() with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: # Codestream starts at byte 3127. SIZ marker at 3137. # COD marker at 3186. Subsampling at 3180. @@ -702,113 +755,10 @@ class TestJp2k(unittest.TestCase): :\sdx=1\sdy=0''', re.VERBOSE) if sys.hexversion < 0x03020000: with self.assertRaisesRegexp((IOError, OSError), regexp): - d = j.read(rlevel=1) + j.read(rlevel=1) else: with self.assertRaisesRegex((IOError, OSError), regexp): - d = j.read(rlevel=1) - - def test_xmp_attribute(self): - # Verify that we can read the XMP packet in our shipping example file. - j = Jp2k(self.jp2file) - xmp = j.box[4].data - ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}' - ns1 = '{http://ns.adobe.com/xap/1.0/}' - name = '{0}RDF/{0}Description'.format(ns0) - elt = xmp.find(name) - attr_value = elt.attrib['{0}CreatorTool'.format(ns1)] - self.assertEqual(attr_value, 'glymur') - - @unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows") - def test_unrecognized_exif_tag(self): - # An unrecognized exif tag should be handled gracefully. - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - shutil.copyfile(self.jp2file, tfile.name) - - # The Exif UUID starts at byte 77. There are 8 bytes for the L and - # T fields, then 16 bytes for the UUID identifier, then 6 exif - # header bytes, then 8 bytes for the TIFF header, then 2 bytes - # the the Image IFD number of tags, where we finally find the first - # tag, "Make" (271). We'll corrupt it by changing it into 171, - # which does not correspond to any known Exif Image tag. - with open(tfile.name, 'r+b') as fp: - fp.seek(117) - buffer = struct.pack('I4s', int(56), b'asoc') - tfile2.write(buffer) + wbuffer = struct.pack('>I4s', int(56), b'asoc') + tfile2.write(wbuffer) # Write the contained label box - buffer = struct.pack('>I4s', int(13), b'lbl ') - tfile2.write(buffer) + wbuffer = struct.pack('>I4s', int(13), b'lbl ') + tfile2.write(wbuffer) tfile2.write('label'.encode()) # Write the xml box # Length = 36, id is 'xml '. - buffer = struct.pack('>I4s', int(35), b'xml ') - tfile2.write(buffer) + wbuffer = struct.pack('>I4s', int(35), b'xml ') + tfile2.write(wbuffer) - buffer = 'this is a test' - buffer = buffer.encode() - tfile2.write(buffer) + wbuffer = 'this is a test' + wbuffer = wbuffer.encode() + tfile2.write(wbuffer) # Now append the codestream. tfile2.write(codestream) @@ -179,6 +186,7 @@ class TestPrintingNeedsLib(unittest.TestCase): self.assertEqual(actual, expected) def test_jp2dump(self): + """basic jp2dump test""" with patch('sys.stdout', new=StringIO()) as fake_out: glymur.jp2dump(self._plain_nemo_file) actual = fake_out.getvalue().strip() @@ -187,9 +195,10 @@ class TestPrintingNeedsLib(unittest.TestCase): lst = actual.split('\n') lst = lst[1:] actual = '\n'.join(lst) - self.assertEqual(actual, self.expectedPlain) + self.assertEqual(actual, self.expected_plain) def test_entire_file(self): + """verify output from printing entire file""" j = glymur.Jp2k(self._plain_nemo_file) with patch('sys.stdout', new=StringIO()) as fake_out: print(j) @@ -200,10 +209,11 @@ class TestPrintingNeedsLib(unittest.TestCase): lst = lst[1:] actual = '\n'.join(lst) - self.assertEqual(actual, self.expectedPlain) + self.assertEqual(actual, self.expected_plain) class TestPrinting(unittest.TestCase): + """Test suite for printing where the libraries are not needed""" def setUp(self): # Save sys.stdout. @@ -212,7 +222,8 @@ class TestPrinting(unittest.TestCase): def tearDown(self): pass - def test_COC_segment(self): + def test_coc_segment(self): + """verify printing of COC segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream(header_only=False) with patch('sys.stdout', new=StringIO()) as fake_out: @@ -239,7 +250,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_COD_segment(self): + def test_cod_segment(self): + """verify printing of COD segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream() with patch('sys.stdout', new=StringIO()) as fake_out: @@ -270,14 +282,13 @@ class TestPrinting(unittest.TestCase): ' Segmentation symbols: False'] expected = '\n'.join(lines) - self.actual = actual - self.expected = expected self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") def test_icc_profile(self): - filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') + """verify printing of colr box with ICC profile""" + filename = opj_data_file('input/nonregression/text_GBR.jp2') with warnings.catch_warnings(): # brand is 'jp2 ', but has any icc profile. warnings.simplefilter("ignore") @@ -342,24 +353,26 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_CRG(self): - filename = os.path.join(data_root, 'input/conformance/p0_03.j2k') + 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 at (87, 6)', + lines = ['CRG marker segment @ (87, 6)', ' Vertical, Horizontal offset: (0.50, 1.00)'] expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_RGN(self): - filename = os.path.join(data_root, 'input/conformance/p0_03.j2k') + 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: @@ -372,10 +385,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_SOP(self): - filename = os.path.join(data_root, 'input/conformance/p0_03.j2k') + 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: @@ -386,11 +400,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @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 = os.path.join(data_root, 'input/conformance/p0_02.j2k') + 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 @@ -402,7 +416,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_EOC_segment(self): + def test_eoc_segment(self): + """verify printing of eoc segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream(header_only=False) with patch('sys.stdout', new=StringIO()) as fake_out: @@ -413,10 +428,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_PLT_segment(self): - filename = os.path.join(data_root, 'input/conformance/p0_07.j2k') + 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: @@ -431,10 +447,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_POD_segment(self): - filename = os.path.join(data_root, 'input/conformance/p0_13.j2k') + 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: @@ -460,10 +477,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_PPM_segment(self): - filename = os.path.join(data_root, 'input/conformance/p1_03.j2k') + 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: @@ -477,10 +495,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_PPT_segment(self): - filename = os.path.join(data_root, 'input/conformance/p1_06.j2k') + 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: @@ -494,7 +513,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_QCC_segment(self): + def test_qcc_segment(self): + """verify printing of qcc segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream(header_only=False) with patch('sys.stdout', new=StringIO()) as fake_out: @@ -509,7 +529,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_QCD_segment_5x3_transform(self): + def test_qcd_segment_5x3_transform(self): + """verify printing of qcd segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream() with patch('sys.stdout', new=StringIO()) as fake_out: @@ -523,7 +544,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_SIZ_segment(self): + def test_siz_segment(self): + """verify printing of SIZ segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream() with patch('sys.stdout', new=StringIO()) as fake_out: @@ -544,7 +566,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_SOC_segment(self): + def test_soc_segment(self): + """verify printing of SOC segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream() with patch('sys.stdout', new=StringIO()) as fake_out: @@ -555,7 +578,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_SOD_segment(self): + def test_sod_segment(self): + """verify printing of SOD segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream(header_only=False) with patch('sys.stdout', new=StringIO()) as fake_out: @@ -566,7 +590,8 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_SOT_segment(self): + def test_sot_segment(self): + """verify printing of SOT segment""" j = glymur.Jp2k(self.jp2file) codestream = j.get_codestream(header_only=False) with patch('sys.stdout', new=StringIO()) as fake_out: @@ -580,13 +605,13 @@ class TestPrinting(unittest.TestCase): ' Number of tile parts: 1'] expected = '\n'.join(lines) - self.maxDiff = None self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_TLM_segment(self): - filename = os.path.join(data_root, 'input/conformance/p0_15.j2k') + 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: @@ -604,7 +629,7 @@ class TestPrinting(unittest.TestCase): @unittest.skipIf(sys.hexversion < 0x02070000, "Differences in XML printing between 2.6 and 2.7") def test_xmp(self): - # Verify the printing of a UUID/XMP box. + """Verify the printing of a UUID/XMP box.""" j = glymur.Jp2k(self.jp2file) with patch('sys.stdout', new=StringIO()) as fake_out: print(j.box[4]) @@ -626,6 +651,7 @@ class TestPrinting(unittest.TestCase): self.assertEqual(actual, expected) def test_codestream(self): + """verify printing of entire codestream""" j = glymur.Jp2k(self.jp2file) with patch('sys.stdout', new=StringIO()) as fake_out: print(j.get_codestream()) @@ -671,15 +697,15 @@ class TestPrinting(unittest.TestCase): ' CME marker segment @ (3209, 37)', ' "Created by OpenJPEG version 2.0.0"'] expected = '\n'.join(lst) - self.maxDiff = None self.assertEqual(actual, expected) @unittest.skipIf(sys.hexversion < 0x02070000, "Differences in XML printing between 2.6 and 2.7") - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") def test_xml(self): - filename = os.path.join(data_root, 'input/conformance/file1.jp2') + """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]) @@ -706,10 +732,70 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(sys.hexversion < 0x03000000, + "Only trusting python3 for printing non-ascii chars") + def test_xml_latin1(self): + """Should be able to print an XMLBox with utf-8 encoding (latin1).""" + # Seems to be inconsistencies between different versions of python2.x + # as to what gets printed. + # + # 2.7.5 (fedora 19) prints xml entities. + # 2.7.3 seems to want to print hex escapes. + text = u""" + Strömung""" + if sys.hexversion < 0x03000000: + xml = ET.parse(StringIO(text.encode('utf-8'))) + else: + xml = ET.parse(StringIO(text)) + + xmlbox = glymur.jp2box.XMLBox(xml=xml) + with patch('sys.stdout', new=StringIO()) as fake_out: + print(xmlbox) + actual = fake_out.getvalue().strip() + if sys.hexversion < 0x03000000: + lines = ["XML Box (xml ) @ (-1, 0)", + " Str\xc3\xb6mung"] + else: + lines = ["XML Box (xml ) @ (-1, 0)", + " Strömung"] + expected = '\n'.join(lines) + self.assertEqual(actual, expected) + + @unittest.skipIf(sys.hexversion < 0x03000000, + "Only trusting python3 for printing non-ascii chars") + def test_xml_cyrrilic(self): + """Should be able to print an XMLBox with utf-8 encoding (cyrrillic).""" + # Seems to be inconsistencies between different versions of python2.x + # as to what gets printed. + # + # 2.7.5 (fedora 19) prints xml entities. + # 2.7.3 seems to want to print hex escapes. + text = u""" + Россия""" + if sys.hexversion < 0x03000000: + xml = ET.parse(StringIO(text.encode('utf-8'))) + else: + xml = ET.parse(StringIO(text)) + + xmlbox = glymur.jp2box.XMLBox(xml=xml) + with patch('sys.stdout', new=StringIO()) as fake_out: + print(xmlbox) + actual = fake_out.getvalue().strip() + if sys.hexversion < 0x03000000: + lines = ["XML Box (xml ) @ (-1, 0)", + " Россия"] + else: + lines = ["XML Box (xml ) @ (-1, 0)", + " Россия"] + + 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): - filename = os.path.join(data_root, 'input/conformance/file2.jp2') + """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]) @@ -721,10 +807,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") def test_component_mapping(self): - filename = os.path.join(data_root, 'input/conformance/file9.jp2') + """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]) @@ -736,10 +823,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_palette(self): - filename = os.path.join(data_root, 'input/conformance/file9.jp2') + 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]) @@ -749,10 +837,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_palette(self): - filename = os.path.join(data_root, 'input/conformance/file7.jp2') + 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]) @@ -772,25 +861,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, - "OPJ_DATA_ROOT environment variable not set") - def test_CRG(self): - filename = os.path.join(data_root, '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[6]) - 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(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") def test_differing_subsamples(self): - # Issue 86. - filename = os.path.join(data_root, 'input/conformance/p0_05.j2k') + """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: @@ -809,11 +884,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @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 = os.path.join(data_root, 'input/conformance/file9.jp2') + """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]) @@ -825,54 +900,56 @@ class TestPrinting(unittest.TestCase): @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""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: with open(self.jp2file, 'rb') as ifile: # Everything up until the jp2c box. - buffer = ifile.read(77) - tfile.write(buffer) + wbuffer = ifile.read(77) + tfile.write(wbuffer) # Write the UINF superbox # Length = 50, id is uinf. - buffer = struct.pack('>I4s', int(50), b'uinf') - tfile.write(buffer) + wbuffer = struct.pack('>I4s', int(50), b'uinf') + tfile.write(wbuffer) # Write the ULST box. # Length is 26, 1 UUID, hard code that UUID as zeros. - buffer = struct.pack('>I4sHIIII', int(26), b'ulst', int(1), - int(0), int(0), int(0), int(0)) - tfile.write(buffer) + wbuffer = struct.pack('>I4sHIIII', int(26), b'ulst', int(1), + int(0), int(0), int(0), int(0)) + tfile.write(wbuffer) # Write the URL box. # Length is 16, version is one byte, flag is 3 bytes, url # is the rest. - buffer = struct.pack('>I4sBBBB', - int(16), b'url ', - int(0), int(0), int(0), int(0)) - tfile.write(buffer) - buffer = struct.pack('>ssss', b'a', b'b', b'c', b'd') - tfile.write(buffer) + wbuffer = struct.pack('>I4sBBBB', + int(16), b'url ', + int(0), int(0), int(0), int(0)) + tfile.write(wbuffer) + + wbuffer = struct.pack('>ssss', b'a', b'b', b'c', b'd') + tfile.write(wbuffer) # Start the resolution superbox. - buffer = struct.pack('>I4s', int(44), b'res ') - tfile.write(buffer) + wbuffer = struct.pack('>I4s', int(44), b'res ') + tfile.write(wbuffer) # Write the capture resolution box. - buffer = struct.pack('>I4sHHHHBB', - int(18), b'resc', - int(1), int(1), int(1), int(1), - int(0), int(1)) - tfile.write(buffer) + wbuffer = struct.pack('>I4sHHHHBB', + int(18), b'resc', + int(1), int(1), int(1), int(1), + int(0), int(1)) + tfile.write(wbuffer) # Write the display resolution box. - buffer = struct.pack('>I4sHHHHBB', - int(18), b'resd', - int(1), int(1), int(1), int(1), - int(1), int(0)) - tfile.write(buffer) + wbuffer = struct.pack('>I4sHHHHBB', + int(18), b'resd', + int(1), int(1), int(1), int(1), + int(1), int(0)) + tfile.write(wbuffer) # Get the rest of the input file. - buffer = ifile.read() - tfile.write(buffer) + wbuffer = ifile.read() + tfile.write(wbuffer) tfile.flush() jp2k = glymur.Jp2k(tfile.name) @@ -900,12 +977,13 @@ class TestPrinting(unittest.TestCase): @unittest.skipIf(sys.hexversion < 0x03000000, "Ordered dicts not printing well in 2.7") - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") - def test_jpx_approximation_with_icc_profile(self): + def test_jpx_approx_icc_profile(self): + """verify jpx with approx field equal to zero""" # ICC profiles may be used in JP2, but the approximation field should # be zero unless we have jpx. This file does both. - filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') + filename = opj_data_file('input/nonregression/text_GBR.jp2') with warnings.catch_warnings(): # brand is 'jp2 ', but has any icc profile. warnings.simplefilter("ignore") @@ -944,11 +1022,11 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - @unittest.skipIf(data_root is None, + @unittest.skipIf(OPJ_DATA_ROOT is None, "OPJ_DATA_ROOT environment variable not set") def test_uuid(self): - # UUID box - filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') + """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") @@ -967,6 +1045,7 @@ class TestPrinting(unittest.TestCase): @unittest.skipIf(sys.hexversion < 0x03000000, "Ordered dicts not printing well in 2.7") def test_exif_uuid(self): + """Verify printing of exif information""" j = glymur.Jp2k(self.jp2file) with patch('sys.stdout', new=StringIO()) as fake_out: @@ -1026,5 +1105,6 @@ class TestPrinting(unittest.TestCase): self.assertEqual(actual, expected) + if __name__ == "__main__": unittest.main() diff --git a/glymur/version.py b/glymur/version.py new file mode 100644 index 0000000..af7523e --- /dev/null +++ b/glymur/version.py @@ -0,0 +1,55 @@ +# This file is part of glymur, a Python interface for accessing JPEG 2000. +# +# http://glymur.readthedocs.org +# +# Copyright 2013 John Evans +# +# License: MIT + +import sys +import numpy as np +from distutils.version import LooseVersion + +from .lib import openjpeg as opj +from .lib import openjp2 as opj2 + +# Do not change the format of this next line! Doing so risks breaking +# setup.py +version = "0.5.6" +_sv = LooseVersion(version) +version_tuple = _sv.version + + +if opj.OPENJPEG is None and opj2.OPENJP2 is None: + openjpeg_version = '0.0.0' +elif opj2.OPENJP2 is None: + openjpeg_version = opj.version() +else: + openjpeg_version = opj2.version() + +_sv = LooseVersion(openjpeg_version) +openjpeg_version_tuple = _sv.version + +__doc__ = """\ +This is glymur **{glymur_version}** + +* OPENJPEG version: **{openjpeg}** +""".format(glymur_version=version, + openjpeg=openjpeg_version) + +info = """\ +Summary of glymur configuration +------------------------------- + +glymur {glymur} +OPENJPEG {openjpeg} +Python {python} +sys.platform {platform} +sys.maxsize {maxsize} +numpy {numpy} +""".format(glymur=version, + openjpeg=openjpeg_version, + python=sys.version, + platform=sys.platform, + maxsize=sys.maxsize, + numpy=np.__version__) diff --git a/release.txt b/release.txt deleted file mode 100644 index 17d55e0..0000000 --- a/release.txt +++ /dev/null @@ -1,59 +0,0 @@ -| OS | Python | Python | Python | Notes | -| | 2.6 | 2.7 | 3.3 | | -+-----------+--------+--------+--------+--------------------------------------+ -| Windows | | X | | WinPython with OpenJPEG 1.5.1 and | -| | | | | OpenJPEG 2.0.0. 267 of 455 tests | -| | | | | pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Windows | | | X | WinPython with OpenJPEG 1.5.1 and | -| | | | | OpenJPEG svn. 307 of 455 tests | -| | | | | pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Mac | X | | | MacPorts with both OpenJPEG 1.5.1 | -| 10.6.8 | | | | and OpenJPEG svn. 354 of 456 tests | -| | | | | should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Mac | X | | | MacPorts with both OpenJPEG 1.5.1 | -| 10.6.8 | | | | and OpenJPEG 2.0. 315 of 456 tests | -| | | | | should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Mac | | X | | MacPorts with both OpenJPEG 1.5.1 | -| 10.6.8 | | | | and OpenJPEG svn. 379 of 461 tests | -| | | | | should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Mac | | X | | MacPorts with both OpenJPEG 1.5.1 | -| 10.6.8 | | | | and OpenJPEG 2.0. 340 of 461 tests | -| | | | | should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Mac | | | X | MacPorts with both OpenJPEG 1.5.1 | -| 10.6.8 | | | | and OpenJPEG svn. 407 of 461 | -| | | | | tests should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Mac | | | X | MacPorts with both OpenJPEG 1.5.1 | -| 10.6.8 | | | | and OpenJPEG 2.0. 355 of 461 | -| | | | | tests should pass. | -+-----------+--------+--------+-----------------------------------------------+ -| Fedora 19 | | | X | Ships with 1.5.1, openjp2 svn built, | -| | | | | too. 407 of 461 tests should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Fedora 18 | | X | | Ships with 1.5.1. 173 of 456 tests | -| | | | | should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Fedora 17 | | X | | Ships with 1.4.0. 171 of 456 tests | -| | | | | should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| CentOS | X | | | Ships with 1.3.0. 169 of 456 tests | -| 6.3 | | | | should pass. | -+-----------+--------+--------+--------+--------------------------------------+ -| Raspberry | | X | | Ships with 1.3.0. 171 of 456 tests | -| Pi | | | | should pass. | -| Debian 7 | | | | | -+-----------+--------+--------+--------+--------------------------------------+ - -Make release branch, qualify on all platforms. -Pylint on entire package should exceed 0.95. -pep8 should be pass cleanly. -Coverage should exceed 95%. -Make release candidate, push to Pypi, qualify in virtual environment. -Merge release branch to master, tag, push to master, push to Pypi. - diff --git a/setup.py b/setup.py index f725631..89e69f7 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ from setuptools import setup, find_packages +import os +import re import sys kwargs = {'name': 'Glymur', - 'version': '0.3.0', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans', @@ -12,9 +13,8 @@ kwargs = {'name': 'Glymur', 'glymur.lib.test'], 'package_data': {'glymur': ['data/*.jp2', 'data/*.j2k']}, 'scripts': ['bin/jp2dump'], - 'license': 'LICENSE.txt', - 'test_suite': 'glymur.test', - 'platforms': ['darwin']} + 'license': 'MIT', + 'test_suite': 'glymur.test'} instllrqrs = ['numpy>=1.4.1'] if sys.hexversion < 0x03030000: @@ -39,4 +39,12 @@ clssfrs = ["Programming Language :: Python", "Intended Audience :: Information Technology", "Topic :: Software Development :: Libraries :: Python Modules"] kwargs['classifiers'] = clssfrs + +# Get the version string. Cannot do this by importing glymur! +version_file = os.path.join('glymur', 'version.py') +with open('glymur/version.py', 'rt') as fptr: + contents = fptr.read() + match = re.search('version\s*=\s*"(?P\d*.\d*.\d*.*)"\n', contents) + kwargs['version'] = match.group('version') + setup(**kwargs)