diff --git a/CHANGES.txt b/CHANGES.txt index 2495697..056f3a9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,20 @@ -May 27, 2013 - v0.1.1, Changed write example to not rely on matplotlib. Fixed - readthedocs.org setup to build documentation automatically. Can import - glymur without libopenjp2 actually being present. +Jun 06, 2013 - v0.1.6 Exif classes made private. Refactored IFD post + processing. Corrected omission of Exif in UUIDBox docstring. -May 27, 2013 - Changed write example to not rely on matplotlib. +Jun 06, 2013 - v0.1.5 Changed ColourSpecificationBox attribute "color_space" + to "colorspace". Fixed MANIFEST issue affecting PyPI installs. + +Jun 05, 2013 - v0.1.4 Added Exif UUID read support. + +Jun 02, 2013 - v0.1.3p1 Raising IOErrors when code block size and precinct + sizes are not in harmony. Added statement to docs about upstream library + dependence. Added roadmap to docs. + +May 30, 2013 - v0.1.2. Added XMP UUID read support. Added jp2 boxes to rst + docs, XMLBox.indent method made into a private module method. Precinct + sizes restricted to be multiples of two. + +May 27, 2013 - v0.1.1. Changed write example to not rely on matplotlib. Fixed + readthedocs.org setup to build documentation automatically. Can import + glymur without libopenjp2 actually being present. Changed write example + to not rely on matplotlib. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0820575 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 John Evans + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index 49ff165..0000000 --- a/MANIFEST +++ /dev/null @@ -1,14 +0,0 @@ -# file GENERATED by distutils, do NOT edit -README -setup.py -jptools/__init__.py -jptools/codestream.py -jptools/core.py -jptools/jp2box.py -jptools/jp2k.py -jptools/data/emneck.jp2 -jptools/test/__init__.py -jptools/test/test_codestream.py -jptools/test/test_conformance.py -jptools/test/test_jp2k.py -jptools/test/test_printing.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..160d333 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include *.txt *.md +recursive-include *.txt *.py +prune build +exclude readthedocs-pip-requirements.txt diff --git a/README b/README deleted file mode 100644 index 1428afa..0000000 --- a/README +++ /dev/null @@ -1,187 +0,0 @@ -========================================= -glymur: a Python interface for JPEG 2000 -========================================= - -**glymur** contains a Python interface to the OpenJPEG library -which allows linux and mac users to read and write JPEG 2000 files. For more -information about OpenJPEG, please consult http://www.openjpeg.org. **glymur** -should be considered to be alpha-quality software. - -**glymur** tries to support reading (including all metadata) and writing of -JP2 and J2C files. Writing J2C/JP2 files is currently limited to images that -can fit in memory, however. - -There is some very limited support for reading JPX -metadata. For instance, **asoc** and **labl** boxes are recognized, so GMLJP2 -metadata can be retrieved from such JPX files. - ------------- -Requirements ------------- -**glymur** works on Python 2.7 and 3.3. Python 3.3 is strongly recommended. - -OpenJPEG -======== -OpenJPEG must be built as a shared library. In addition, you -currently must compile OpenJPEG from the developmental source that -you can retrieve via subversion. As of this time of writing, svn -revision 2345 works. In addition, you should also retrieve their test data, as -you will need it when running **glymur**'s test suite. - -Earlier versions of OpenJPEG through the 2.0 official release are not supported. - -Be sure to have the following ports/RPMs/debs installed. - - * gcc - * gcc-c++ - * cmake - -You should build OpenJPEG with testing turned on. Consult the OpenJPEG -documentation on how to do this. If you use linux, make sure that you -have the following development packages installed - - * zlib-devel - * png-devel - * libtiff-devel - * lcms2-devel - -OS -== - -Mac OS X --------- -All the necessary packages are available to use **glymur** with Python 3.3 via -MacPorts. A minimal set of ports includes - - * python33 - * py33-numpy - * py33-distribute - -To run all the testing, one of the following combinations of ports must -additionally be installed: - - * py33-scikit-image and either py33-Pillow or freeimage - * py33-matplotlib and py33-Pillow - -Linux ------ - -Fedora 18 -''''''''' -Fedora 18 ships with Python 3.3, so all the necessary RPMs are available to -meet the minimal set of requirements. - - * python3 - * python3-numpy - * python3-setuptools - * python3-matplotlib (for running tests) - * python3-matplotlib-tk (or whichever matplotlib backend you prefer) - -A few tests still will not run, however, unless one of the following -combinations of RPMs / Python packages is installed. - - * scikit-image and either Pillow or freeimage - * matplotlib and Pillow - -The 2nd route is probably the easiest, so go ahead and install **Pillow** -via **pip** since **Pillow** is not yet available in Fedora 18 default -repositories:: - - $ yum install python3-devel # pip needs this in order to compile Pillow - $ yum install python3-pip # First one must install pip of course! - $ pip-python3 install Pillow --user - $ export PYTHONPATH=$HOME/.local/lib/python3.3/site-packages:$PYTHONPATH - -Fedora 17 -''''''''' -Fedora 17 ships with Python 3.2 and 2.7, so these steps detail working with -2.7. - -Required RPMs include:: - - * python - * python-mock - * python-pip - * python-setuptools - * numpy - -In addition, you must install **contextlib2** via pip. - -A few tests still will not run, however, unless one of the following -combinations of RPMs / Python packages is installed. - - * scikit-image and either Pillow or freeimage - * matplotlib and Pillow - -**scikit-image** is not available in the Fedora 17 default repositories, but -it may be installed via **pip**:: - - $ yum install Cython # pip needs this in order to compile scikit-image - $ yum install python-devel # pip needs this in order to compile scikit-image - $ yum install freeimage # scikit-image uses this as a backend - $ yum install scipy # needed by scikit-image - $ pip-python install scikit-image --user - $ pip-python install contextlib2 --user - $ export PYTHONPATH=$HOME/.local/lib/python2.7/site-packages:$PYTHONPATH - -Windows -------- -Not currently supported. - ------------------------------------- -Installation, Testing, Configuration ------------------------------------- - -From this point forward, python3 will be referred to as just "python". - -Installation -============ - -In addition to merely installing **glymur**, you should adjust your **$PATH** -environment variable in order to be able to use the **jp2dump** script from -the unix command line. - -:: - - $ python setup.py install --prefix=/install/path - $ export PYTHONPATH=/install/path/lib/python3.3/site-packages - $ export PATH=/install/path/bin:$PATH - - -Configuration -============= -glymur uses **ctypes** (for the moment) to access the openjp2 library, and -because **ctypes** access libraries in a platform-dependent manner, it is -recommended that you create a configuration file to help glymur properly find -the openjp2 library. You may create the configuration file as follows:: - - $ mkdir ~/.glymur - $ cd ~/.glymur - $ cat > glymurrc << EOF - > [library] - > openjp2: /opt/openjp2-svn/lib/libopenjp2.so - > EOF - -That assumes, of course, that you've installed OpenJPEG into /opt/openjp2-svn. - - -Testing -======= -In order to run all of the test suite, you will first need the OpenJPEG test -data that you previously retrieved. -Then you should set the **OPJ_DATA_ROOT** environment variable to -point to this directory, e.g.:: - - $ cd /somewhere/outside/the/glymur/unpacking/directory - $ svn co http://openjpeg.googlecode.com/svn/data - $ export OPJ_DATA_ROOT=`pwd`/data - -The test suite may then be run with:: - - $ cd /back/to/glymur/unpacking/directory - $ python -m unittest discover - -At the moment, the development version of the library prints quite a few -warnings to stderr, which you may ignore. There are also more skipped tests -on Python 2.7 than on Python 3.3. The important thing will be whether -or not any test errors are reported at the end. diff --git a/TODO b/TODO deleted file mode 100644 index 48883ad..0000000 --- a/TODO +++ /dev/null @@ -1,4 +0,0 @@ -Test -c options with multiple values. -Test -b options with h*w>4096 -Test -b options with h or w > 1024 -Test -b options with h or w < 4 diff --git a/docs/source/api.rst b/docs/source/api.rst index 8c9284e..e1efdac 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,5 +1,116 @@ +--- API -=== +--- +Jp2k +---- .. autoclass:: glymur.Jp2k + :members: read, write, read_bands, get_codestream + +Individual Boxes +---------------- +Jp2kbox +''''''' +.. autoclass:: glymur.jp2box.Jp2kBox :members: + +AssociationBox +'''''''''''''' +.. autoclass:: glymur.jp2box.AssociationBox + :members: + +ColourSpecificationBox +'''''''''''''''''''''' +.. autoclass:: glymur.jp2box.ColourSpecificationBox + :members: + +ComponentDefinitionBox +'''''''''''''''''''''' +.. autoclass:: glymur.jp2box.ComponentDefinitionBox + :members: + +ComponentMappingBox +''''''''''''''''''' +.. autoclass:: glymur.jp2box.ComponentMappingBox + :members: + +ContiguousCodestreamBox +''''''''''''''''''''''' +.. autoclass:: glymur.jp2box.ContiguousCodestreamBox + :members: + +DataEntryURLBox +''''''''''''''' +.. autoclass:: glymur.jp2box.DataEntryURLBox + :members: + +FileTypeBox +''''''''''' +.. autoclass:: glymur.jp2box.FileTypeBox + :members: + +ImageHeaderBox +'''''''''''''' +.. autoclass:: glymur.jp2box.ImageHeaderBox + :members: + +JP2HeaderBox +'''''''''''' +.. autoclass:: glymur.jp2box.JP2HeaderBox + :members: + +JPEG2000SignatureBox +'''''''''''''''''''' +.. autoclass:: glymur.jp2box.JPEG2000SignatureBox + :members: + +LabelBox +'''''''' +.. autoclass:: glymur.jp2box.LabelBox + :members: + +PaletteBox +'''''''''' +.. autoclass:: glymur.jp2box.PaletteBox + :members: + +ReaderRequirementsBox +''''''''''''''''''''' +.. autoclass:: glymur.jp2box.ReaderRequirementsBox + :members: + +ResolutionBox +''''''''''''' +.. autoclass:: glymur.jp2box.ResolutionBox + :members: + +CaptureResolutionBox +'''''''''''''''''''' +.. autoclass:: glymur.jp2box.CaptureResolutionBox + :members: + +DisplayResolutionBox +'''''''''''''''''''' +.. autoclass:: glymur.jp2box.DisplayResolutionBox + :members: + +UUIDBox +''''''' +.. autoclass:: glymur.jp2box.UUIDBox + :members: + +UUIDInfoBox +''''''''''' +.. autoclass:: glymur.jp2box.UUIDInfoBox + :members: + +UUIDListBox +''''''''''' +.. autoclass:: glymur.jp2box.UUIDListBox + :members: + +XMLBox +'''''' +.. autoclass:: glymur.jp2box.XMLBox + :members: + diff --git a/docs/source/conf.py b/docs/source/conf.py index a467ed2..31eba3d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,7 +3,8 @@ # glymur documentation build configuration file, created by # sphinx-quickstart on Sun May 26 11:29:54 2013. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its containing +# dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,7 +12,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os + class Mock(object): def __init__(self, *args, **kwargs): @@ -41,13 +44,13 @@ for mod_name in MOCK_MODULES: # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'numpydoc'] @@ -75,7 +78,7 @@ copyright = u'2013, John Evans' # The short X.Y version. version = '0.1' # The full version, including alpha/beta/rc tags. -release = '0.1.1' +release = '0.1.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -91,7 +94,8 @@ release = '0.1.1' # directories to ignore when looking for source files. exclude_patterns = [] -# The reST default role (used for this markup: `text`) to use for all documents. +# The reST default role (used for this markup: `text`) to use for all +# documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. @@ -112,7 +116,7 @@ pygments_style = 'sphinx' #modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -192,9 +196,8 @@ html_show_sourcelink = True htmlhelp_basename = 'glymurdoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- -latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', @@ -203,14 +206,13 @@ latex_elements = { # Additional stuff for the LaTeX preamble. #'preamble': '', -} +latex_elements = {} # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'glymur.tex', u'glymur Documentation', - u'John Evans', 'manual'), -] +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [('index', 'glymur.tex', u'glymur Documentation', + u'John Evans', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. @@ -233,7 +235,7 @@ latex_documents = [ #latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). @@ -246,16 +248,14 @@ man_pages = [ #man_show_urls = False -# -- Options for Texinfo output ------------------------------------------------ +# -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) -texinfo_documents = [ - ('index', 'glymur', u'glymur Documentation', - u'John Evans', 'glymur', 'One line description of project.', - 'Miscellaneous'), -] +texinfo_documents = [('index', 'glymur', u'glymur Documentation', + u'John Evans', 'glymur', + 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 2938255..82f03bd 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -1,15 +1,16 @@ -************ +------------ How do I...? -************ +------------ + Display metadata? ================= -There are two ways. From the unix command line, the script **jp2dump** is -available.:: +There are two ways. From the unix command line, the script *jp2dump* is +available. :: $ jp2dump /path/to/glymur/installation/data/nemo.jp2 -From within Python, it is as simple as printing the Jp2k object, i.e.:: +From within Python, it is as simple as printing the Jp2k object, i.e. :: >>> import pkg_resources >>> from glymur import Jp2k @@ -17,8 +18,40 @@ From within Python, it is as simple as printing the Jp2k object, i.e.:: >>> j = Jp2k(file) >>> print(j) -The primary emphasis is on JP2 metadata, but it is possible to -display raw codestream as well:: +This prints the metadata found in the JP2 boxes, but in the case of the +codestream box, only the main header is printed. It is possible to print +**only** the codestream information as well, i.e. :: >>> print(j.get_codestream()) +Work with XMP UUIDs? +==================== +The example JP2 file shipped with glymur has an XMP UUID:: + + >>> import pkg_resources + >>> from glymur import Jp2k + >>> file = pkg_resources.resource_filename(glymur.__name__, "data/nemo.jp2") + >>> j = Jp2k(file) + >>> print(j.box[4]) + UUID Box (uuid) @ (715, 2412) + UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) + UUID Data: + + + + + + +Since the UUID data in this case is returned as an ElementTree Element, one can +use ElementTree to access the data. For example, to extract the +**CreatorTool** attribute value, the following would work:: + + >>> 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) + >>> elt + + >>> elt.attrib['{0}CreatorTool'.format(ns1)] + 'glymur' diff --git a/docs/source/index.rst b/docs/source/index.rst index 43632b4..1a7ddda 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,8 +3,9 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +================================== Welcome to glymur's documentation! -=================================== +================================== Contents: @@ -14,10 +15,11 @@ Contents: introduction api how_do_i + roadmap - +------------------ Indices and tables -================== +------------------ * :ref:`genindex` * :ref:`modindex` diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 5f4ccb3..7f3a5ed 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -1,23 +1,27 @@ -========================================= -glymur: a Python interface for JPEG 2000 -========================================= +---------------------------------------- +Glymur: a Python interface for JPEG 2000 +---------------------------------------- -**glymur** contains a Python interface to the OpenJPEG library +**Glymur** contains a Python interface to the OpenJPEG library which allows linux and mac users to read and write JPEG 2000 files. For more -information about OpenJPEG, please consult http://www.openjpeg.org. glymur -should be considered to be alpha-quality software. +information about OpenJPEG, please consult http://www.openjpeg.org. Glymur +currently relies upon a development version of the OpenJPEG library, and so, +while useable, it is totally at the mercy of any upstream changes +made to the development version of OpenJPEG. -glymur tries to support reading (including all metadata) and writing of -JP2 and J2C files. Writing J2C/JP2 files is currently limited to images that -can fit in memory, however. +Glymur supports both reading and writing of JPEG 2000 images (part 1). Writing +JPEG 2000 images is currently limited to images that can fit in memory, +however. -There is some very limited support for reading JPX -metadata. For instance, **asoc** and **labl** boxes are recognized, so GMLJP2 -metadata can be retrieved from such JPX files. +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 +some very limited support for reading JPX metadata. For instance, +**asoc** and **labl** boxes are recognized, so GMLJP2 metadata can +be retrieved from such JPX files. ------------- +'''''''''''' Requirements ------------- +'''''''''''' glymur works on Python 2.7 and 3.3. Python 3.3 is strongly recommended. OpenJPEG @@ -28,7 +32,8 @@ you can retrieve via subversion. As of this time of writing, svn revision 2345 works. In addition, you should also retrieve their test data, as you will need it when running glymur's test suite. -Earlier versions of OpenJPEG through the 2.0 official release are not supported. +Earlier versions of OpenJPEG through the 2.0 official release will **NOT** +work and are not supported. Be sure to have the following ports/RPMs/debs installed. @@ -128,22 +133,34 @@ Windows ------- Not currently supported. ------------------------------------- +'''''''''''''''''''''''''''''''''''' Installation, Testing, Configuration ------------------------------------- +'''''''''''''''''''''''''''''''''''' From this point forward, python3 will be referred to as just "python". Installation ============ +You can install glymur via pip from the command line:: + + $ pip install glymur + +or manually retrieve the code from either of GitHub or PyPI + + * https://pypi.python.org/pypi/Glymur/ (stable releases) + * http://github.com/quintusdias/glymur (bleeding edge) + +and then unpack and install with:: + + $ python setup.py install --prefix=/install/path + In addition to merely installing glymur, you should adjust your **$PATH** -environment variable in order to be able to use the **jp2dump** script from +environment variable in order to be able to use the *jp2dump* script from the unix command line. :: - $ python setup.py install --prefix=/install/path $ export PYTHONPATH=/install/path/lib/python3.3/site-packages $ export PATH=/install/path/bin:$PATH @@ -181,7 +198,10 @@ The test suite may then be run with:: $ cd /back/to/glymur/unpacking/directory $ python -m unittest discover -At the moment, the development version of the library prints quite a few -warnings to stderr, which you may ignore. There are also more skipped tests -on Python 2.7 than on Python 3.3. The important thing will be whether -or not any test errors are reported at the end. +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 +filtered out for now. There are also more skipped tests on Python 2.7 +than on Python 3.3. The important point to remember is whether or not any test +errors are reported at the end. diff --git a/docs/source/roadmap.rst b/docs/source/roadmap.rst new file mode 100644 index 0000000..a6f967a --- /dev/null +++ b/docs/source/roadmap.rst @@ -0,0 +1,12 @@ +------- +Roadmap +------- + +Here's an incomplete list of what I'd like to focus on in the near future. + + * continue to monitor upstream changes in the openjp2 library + * investigate using CFFI or cython instead of ctypes to wrap openjp2 + * eventually expose the openjp2 API + * add metadata support for ICC profiles + * investigate JPIP + diff --git a/glymur/__init__.py b/glymur/__init__.py index 29e8cc4..3525b6a 100644 --- a/glymur/__init__.py +++ b/glymur/__init__.py @@ -16,24 +16,15 @@ def _glymurrc_fname(): if os.path.exists(fname): return fname - # environ var GLYMURCONFIGDIR - if 'GLYMURCONFIGDIR' in os.environ: - path = os.environ['GLYMURCONFIGDIR'] - if os.path.exists(path): - fname = os.path.join(path, 'glymurrc') - if os.path.exists(fname): - return fname - else: - msg = "glymurrc file hinted at by GLYMURCONFIGDIR does not " - msg += "exist." - warnings.warn(msg, UserWarning) - - # HOME/.glymur/glymurrc + # Either GLYMURCONFIGDIR/glymurrc or $HOME/.glymur/glymurrc confdir = _get_configdir() if confdir is not None: - fname = os.path.join(_get_configdir(), 'glymurrc') + fname = os.path.join(confdir, 'glymurrc') if os.path.exists(fname): return fname + else: + msg = "Configuration file '{0}' does not exist.".format(confdir) + warnings.warn(msg, UserWarning) # didn't find a configuration file. return None diff --git a/glymur/codestream.py b/glymur/codestream.py index 506450d..f433eca 100644 --- a/glymur/codestream.py +++ b/glymur/codestream.py @@ -472,8 +472,6 @@ class Codestream: SPcod = f.read(n) kwargs['SPcod'] = np.frombuffer(SPcod, dtype=np.uint8) - # 0: - # 1: layers params = struct.unpack('>BHBBBBBB', SPcod[0:9]) kwargs['_layers'] = params[1] kwargs['_numresolutions'] = params[3] diff --git a/glymur/data/nemo.jp2 b/glymur/data/nemo.jp2 index 1b97d0d..2bb8638 100644 Binary files a/glymur/data/nemo.jp2 and b/glymur/data/nemo.jp2 differ diff --git a/glymur/jp2box.py b/glymur/jp2box.py index b2ca564..789c803 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1,11 +1,20 @@ """Classes for individual JPEG 2000 boxes. -Contains classes for each individual JPEG2000 boxes. +References +---------- +.. [JP2K15444-1i] International Organization for Standardication. ISO/IEC + 15444-1:2004 - Information technology -- JPEG 2000 image coding system: + Core coding system" + +.. [JP2K15444-2m] International Organization for Standardication. ISO/IEC + 15444-2:2004 - Information technology -- JPEG 2000 image coding system: + Extensions """ import copy import math import os +import pprint import struct import sys import uuid @@ -28,7 +37,7 @@ class Jp2kBox: Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -127,7 +136,7 @@ class ColourSpecificationBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int Length of the box in bytes. offset : int @@ -141,18 +150,12 @@ class ColourSpecificationBox(Jp2kBox): boxes. approximation : int Measure of colorspace accuracy. - color_space : int or None + colorspace : int or None Enumerated colorspace, corresponds to one of 'sRGB', 'greyscale', or 'YCC'. If not None, then icc_profile must be None. icc_profile : byte array or None ICC profile according to ICC profile specification. If not None, then color_space must be None. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Colour Specification') @@ -167,8 +170,8 @@ class ColourSpecificationBox(Jp2kBox): if self.approximation is not 0: x = _approximation_display[self.approximation] msg += '\n Approximation: {0}'.format(x) - if self.color_space is not None: - x = _colorspace_map_display[self.color_space] + if self.colorspace is not None: + x = _colorspace_map_display[self.colorspace] msg += '\n Colorspace: {0}'.format(x) else: x = len(self.icc_profile) @@ -211,12 +214,12 @@ class ColourSpecificationBox(Jp2kBox): if method == 1: # enumerated colour space buffer = f.read(4) - kwargs['color_space'], = struct.unpack('>I', buffer) + kwargs['colorspace'], = struct.unpack('>I', buffer) kwargs['icc_profile'] = None else: # ICC profile - kwargs['color_space'] = None + kwargs['colorspace'] = None n = offset + length - f.tell() kwargs['icc_profile'] = f.read(n) @@ -230,7 +233,7 @@ class ComponentDefinitionBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : numeric scalar length of the box in bytes. offset : int @@ -243,12 +246,6 @@ class ComponentDefinitionBox(Jp2kBox): type of the component association : int number of the associated color - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Component Definition') @@ -314,7 +311,7 @@ class ComponentMappingBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : numeric scalar Length of the box in bytes. offset : int @@ -327,12 +324,6 @@ class ComponentMappingBox(Jp2kBox): mapping type, either direct use (0) or palette (1) palette_index : int Index component from palette - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Component Mapping') @@ -395,7 +386,7 @@ class ContiguousCodestreamBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -404,12 +395,6 @@ class ContiguousCodestreamBox(Jp2kBox): more verbose description of the box. main_header : list List of segments in the codestream header. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): @@ -463,7 +448,7 @@ class FileTypeBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -477,12 +462,6 @@ class FileTypeBox(Jp2kBox): Minor version number identifying the JP2 specification used. compatibility_list: list List of file conformance profiles. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='File Type') @@ -552,7 +531,7 @@ class ImageHeaderBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -573,12 +552,6 @@ class ImageHeaderBox(Jp2kBox): 0 if the color space is known and correctly specified. ip_provided : int 0 if the file does not contain intellectual propery rights information. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Image Header') @@ -643,7 +616,7 @@ class AssociationBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -652,12 +625,6 @@ class AssociationBox(Jp2kBox): more verbose description of the box. box : list List of boxes contained in this superbox. - - References - ---------- - .. [JP2K15444-2m] International Organization for Standardication. ISO/IEC - 15444-2:2004 - Information technology -- JPEG 2000 image coding system: - Extensions """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Association') @@ -713,7 +680,7 @@ class JP2HeaderBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -722,12 +689,6 @@ class JP2HeaderBox(Jp2kBox): more verbose description of the box. box : list List of boxes contained in this superbox. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='JP2 Header') @@ -783,7 +744,7 @@ class JPEG2000SignatureBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -792,12 +753,6 @@ class JPEG2000SignatureBox(Jp2kBox): more verbose description of the box. signature : byte Four-byte tuple identifying the file as JPEG 2000. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='JPEG 2000 Signature') @@ -846,7 +801,7 @@ class PaletteBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -855,12 +810,6 @@ class PaletteBox(Jp2kBox): more verbose description of the box. palette : list Colormap represented as list of 1D arrays, one per color component. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Palette') @@ -953,7 +902,7 @@ class ReaderRequirementsBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -974,12 +923,6 @@ class ReaderRequirementsBox(Jp2kBox): vendor_mask : list Specifies the compatibility mask for each corresponding vendor feature. - - References - ---------- - .. [JP2K15444-2m] International Organization for Standardication. ISO/IEC - 15444-2:2004 - Information technology -- JPEG 2000 image coding system: - Extensions """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Reader Requirements') @@ -1090,7 +1033,7 @@ class ResolutionBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1099,12 +1042,6 @@ class ResolutionBox(Jp2kBox): more verbose description of the box. box : list List of boxes contained in this superbox. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Resolution') @@ -1160,7 +1097,7 @@ class CaptureResolutionBox(ResolutionBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1169,12 +1106,6 @@ class CaptureResolutionBox(ResolutionBox): more verbose description of the box. VR, HR : float Vertical, horizontal resolution. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): ResolutionBox.__init__(self, id='', longname='Capture Resolution') @@ -1227,7 +1158,7 @@ class DisplayResolutionBox(ResolutionBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1236,12 +1167,6 @@ class DisplayResolutionBox(ResolutionBox): more verbose description of the box. VR, HR : float Vertical, horizontal resolution. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): ResolutionBox.__init__(self, id='', longname='Display Resolution') @@ -1294,7 +1219,7 @@ class LabelBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1303,12 +1228,6 @@ class LabelBox(Jp2kBox): more verbose description of the box. label : str Label - - References - ---------- - .. [JP2K15444-2m] International Organization for Standardication. ISO/IEC - 15444-2:2004 - Information technology -- JPEG 2000 image coding system: - Extensions """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Label') @@ -1356,7 +1275,7 @@ class XMLBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1365,48 +1284,16 @@ class XMLBox(Jp2kBox): more verbose description of the box. xml : ElementTree.Element XML section. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='XML') self.__dict__.update(**kwargs) - def indent(self, elem, level=0): - """recipe for pretty printing XML. Please see - - http://effbot.org/zone/element-lib.htm#prettyprint - """ - i = "\n" + level * " " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - self.indent(elem, level + 1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - def __str__(self): msg = Jp2kBox.__str__(self) xml = self.xml if self.xml is not None: - xml = copy.deepcopy(self.xml) - self.indent(xml) - xmltext = ET.tostring(xml).decode('utf-8') - - # Indent it a bit. - lst = [(' ' + x) for x in xmltext.split('\n')] - xml = '\n'.join(lst) - msg += '\n{0}'.format(xml) + msg += _pretty_print_xml(self.xml) else: msg += '\n {0}'.format(xml) return msg @@ -1460,7 +1347,7 @@ class UUIDListBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1469,12 +1356,6 @@ class UUIDListBox(Jp2kBox): more verbose description of the box. ulst : list List of UUIDs. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='UUID List') @@ -1528,7 +1409,7 @@ class UUIDInfoBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1537,12 +1418,6 @@ class UUIDInfoBox(Jp2kBox): more verbose description of the box. box : list List of boxes contained in this superbox. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='UUIDInfo') @@ -1599,7 +1474,7 @@ class DataEntryURLBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1612,12 +1487,6 @@ class DataEntryURLBox(Jp2kBox): Particular attributes of this box, consists of three bytes. URL : str Associated URL. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='Data Entry URL') @@ -1678,7 +1547,7 @@ class UUIDBox(Jp2kBox): Attributes ---------- id : str - 4-byte identifier for the box. + 4-character identifier for the box. length : int length of the box in bytes. offset : int @@ -1687,23 +1556,34 @@ class UUIDBox(Jp2kBox): more verbose description of the box. uuid : uuid.UUID 16-byte UUID - data : bytes - Vendor-specific UUID data. - - References - ---------- - .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC - 15444-1:2004 - Information technology -- JPEG 2000 image coding system: - Core coding system" + data : bytes or dictionary or ElementTree.Element + Vendor-specific UUID data. Exif UUIDs are interpreted as dictionaries. + XMP UUIDs are interpreted as standard XML. """ def __init__(self, **kwargs): Jp2kBox.__init__(self, id='', longname='UUID') self.__dict__.update(**kwargs) def __str__(self): - msg = Jp2kBox.__str__(self) - msg += '\n UUID: {0}'.format(self.uuid) - msg += '\n UUID Data: {0} bytes'.format(len(self.data)) + msg = '{0}\n' + msg += ' UUID: {1}{2}\n' + msg += ' UUID Data: {3}' + + if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): + uuid_type = ' (XMP)' + uuid_data = _pretty_print_xml(self.data) + elif self.uuid.bytes == b'JpgTiffExif->JP2': + uuid_type = ' (Exif)' + uuid_data = '\n' + pprint.pformat(self.data) + else: + uuid_type = '' + uuid_data = '{0} bytes'.format(len(self.data)) + + msg = msg.format(Jp2kBox.__str__(self), + self.uuid, + uuid_type, + uuid_data) + return msg @staticmethod @@ -1734,11 +1614,520 @@ class UUIDBox(Jp2kBox): n = offset + length - f.tell() buffer = f.read(n) - kwargs['data'] = buffer + if kwargs['uuid'] == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): + # XMP data. Parse as XML. Seems to be a difference between + # ElementTree in version 2.7 and 3.3. + if sys.hexversion < 0x03000000: + parser = ET.XMLParser(encoding='utf-8') + kwargs['data'] = ET.fromstringlist(buffer, parser=parser) + else: + text = buffer.decode('utf-8') + kwargs['data'] = ET.fromstring(text) + elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2': + e = Exif(buffer) + d = {} + d['Exif'] = e.exif_image + d['Photo'] = e.exif_photo + d['GPSInfo'] = e.exif_gpsinfo + d['Iop'] = e.exif_iop + kwargs['data'] = d + else: + kwargs['data'] = buffer box = UUIDBox(**kwargs) return box +class Exif: + """ + Attributes + ---------- + buffer : bytes + Raw byte stream consisting of the UUID data. + endian : str + Either '<' for big-endian, or '>' for little-endian. + """ + + def __init__(self, buffer): + """Interpret raw buffer consisting of Exif IFD. + """ + self.exif_image = None + self.exif_photo = None + self.exif_gpsinfo = None + self.exif_iop = None + + self.buffer = buffer + + # Ignore the first six bytes. + # Next 8 should be (73, 73, 42, 8) + data = struct.unpack('' for little-endian. + num_tags : int + Number of tags in the IFD. + raw_ifd : dictionary + Maps tag number to "mildly-interpreted" tag value. + processed_ifd : dictionary + Maps tag name to "mildly-interpreted" tag value. + """ + datatype2fmt = {1: ('B', 1), + 2: ('B', 1), + 3: ('H', 2), + 4: ('I', 4), + 5: ('II', 8), + 7: ('B', 1), + 9: ('i', 4), + 10: ('ii', 8)} + + def __init__(self, endian, buffer, offset): + self.endian = endian + self.buffer = buffer + self.processed_ifd = {} + + self.num_tags, = struct.unpack(endian + 'H', + buffer[offset:offset + 2]) + + fmt = self.endian + 'HHII' * self.num_tags + ifd_buffer = buffer[offset + 2:offset + 2 + self.num_tags * 12] + data = struct.unpack(fmt, ifd_buffer) + self.raw_ifd = {} + for j, tag in enumerate(data[0::4]): + # The offset to the tag offset/payload is the offset to the IFD + # plus 2 bytes for the number of tags plus 12 bytes for each + # tag entry plus 8 bytes to the offset/payload itself. + toffp = buffer[offset + 10 + j * 12:offset + 10 + j * 12 + 4] + tag_data = self.parse_tag(data[j * 4 + 1], + data[j * 4 + 2], + toffp) + self.raw_ifd[tag] = tag_data + + def parse_tag(self, dtype, count, offset_buf): + """Interpret an Exif image tag data payload. + """ + fmt = self.datatype2fmt[dtype][0] * count + payload_size = self.datatype2fmt[dtype][1] * count + + if payload_size <= 4: + # Interpret the payload from the 4 bytes in the tag entry. + target_buffer = offset_buf[:payload_size] + else: + # Interpret the payload at the offset specified by the 4 bytes in + # the tag entry. + offset, = struct.unpack(self.endian + 'I', offset_buf) + target_buffer = self.buffer[offset:offset + payload_size] + + if dtype == 2: + # ASCII + if sys.hexversion < 0x03000000: + payload = target_buffer.rstrip('\x00') + else: + payload = target_buffer.decode('utf-8').rstrip('\x00') + + else: + payload = struct.unpack(self.endian + fmt, target_buffer) + if dtype == 5 or dtype == 10: + # Rational or Signed Rational. Construct the list of values. + rational_payload = [] + for j in range(count): + value = float(payload[j * 2]) / float(payload[j * 2 + 1]) + rational_payload.append(value) + payload = rational_payload + if count == 1: + # If just a single value, then return a scalar instead of a + # tuple. + payload = payload[0] + + return payload + + def post_process(self, tagnum2name): + for tag, value in self.raw_ifd.items(): + tag_name = tagnum2name[tag] + self.processed_ifd[tag_name] = value + + +class _ExifImageIfd(_Ifd): + """ + Attributes + ---------- + tagnum2name : dict + Maps Exif image tag numbers to the tag names. + ifd : dict + Maps tag names to tag values. + """ + tagnum2name = {11: 'ProcessingSoftware', + 254: 'NewSubfileType', + 255: 'SubfileType', + 256: 'ImageWidth', + 257: 'ImageLength', + 258: 'BitsPerSample', + 259: 'Compression', + 262: 'PhotometricInterpretation', + 263: 'Threshholding', + 264: 'CellWidth', + 265: 'CellLength', + 266: 'FillOrder', + 269: 'DocumentName', + 270: 'ImageDescription', + 271: 'Make', + 272: 'Model', + 273: 'StripOffsets', + 274: 'Orientation', + 277: 'SamplesPerPixel', + 278: 'RowsPerStrip', + 279: 'StripByteCounts', + 282: 'XResolution', + 283: 'YResolution', + 284: 'PlanarConfiguration', + 290: 'GrayResponseUnit', + 291: 'GrayResponseCurve', + 292: 'T4Options', + 293: 'T6Options', + 296: 'ResolutionUnit', + 301: 'TransferFunction', + 305: 'Software', + 306: 'DateTime', + 315: 'Artist', + 316: 'HostComputer', + 317: 'Predictor', + 318: 'WhitePoint', + 319: 'PrimaryChromaticities', + 320: 'ColorMap', + 321: 'HalftoneHints', + 322: 'TileWidth', + 323: 'TileLength', + 324: 'TileOffsets', + 325: 'TileByteCounts', + 330: 'SubIFDs', + 332: 'InkSet', + 333: 'InkNames', + 334: 'NumberOfInks', + 336: 'DotRange', + 337: 'TargetPrinter', + 338: 'ExtraSamples', + 339: 'SampleFormat', + 340: 'SMinSampleValue', + 341: 'SMaxSampleValue', + 342: 'TransferRange', + 343: 'ClipPath', + 344: 'XClipPathUnits', + 345: 'YClipPathUnits', + 346: 'Indexed', + 347: 'JPEGTables', + 351: 'OPIProxy', + 512: 'JPEGProc', + 513: 'JPEGInterchangeFormat', + 514: 'JPEGInterchangeFormatLength', + 515: 'JPEGRestartInterval', + 517: 'JPEGLosslessPredictors', + 518: 'JPEGPointTransforms', + 519: 'JPEGQTables', + 520: 'JPEGDCTables', + 521: 'JPEGACTables', + 529: 'YCbCrCoefficients', + 530: 'YCbCrSubSampling', + 531: 'YCbCrPositioning', + 532: 'ReferenceBlackWhite', + 700: 'XMLPacket', + 18246: 'Rating', + 18249: 'RatingPercent', + 32781: 'ImageID', + 33421: 'CFARepeatPatternDim', + 33422: 'CFAPattern', + 33423: 'BatteryLevel', + 33432: 'Copyright', + 33434: 'ExposureTime', + 33437: 'FNumber', + 33723: 'IPTCNAA', + 34377: 'ImageResources', + 34665: 'ExifTag', + 34675: 'InterColorProfile', + 34850: 'ExposureProgram', + 34852: 'SpectralSensitivity', + 34853: 'GPSTag', + 34855: 'ISOSpeedRatings', + 34856: 'OECF', + 34857: 'Interlace', + 34858: 'TimeZoneOffset', + 34859: 'SelfTimerMode', + 36867: 'DateTimeOriginal', + 37122: 'CompressedBitsPerPixel', + 37377: 'ShutterSpeedValue', + 37378: 'ApertureValue', + 37379: 'BrightnessValue', + 37380: 'ExposureBiasValue', + 37381: 'MaxApertureValue', + 37382: 'SubjectDistance', + 37383: 'MeteringMode', + 37384: 'LightSource', + 37385: 'Flash', + 37386: 'FocalLength', + 37387: 'FlashEnergy', + 37388: 'SpatialFrequencyResponse', + 37389: 'Noise', + 37390: 'FocalPlaneXResolution', + 37391: 'FocalPlaneYResolution', + 37392: 'FocalPlaneResolutionUnit', + 37393: 'ImageNumber', + 37394: 'SecurityClassification', + 37395: 'ImageHistory', + 37396: 'SubjectLocation', + 37397: 'ExposureIndex', + 37398: 'TIFFEPStandardID', + 37399: 'SensingMethod', + 40091: 'XPTitle', + 40092: 'XPComment', + 40093: 'XPAuthor', + 40094: 'XPKeywords', + 40095: 'XPSubject', + 50341: 'PrintImageMatching', + 50706: 'DNGVersion', + 50707: 'DNGBackwardVersion', + 50708: 'UniqueCameraModel', + 50709: 'LocalizedCameraModel', + 50710: 'CFAPlaneColor', + 50711: 'CFALayout', + 50712: 'LinearizationTable', + 50713: 'BlackLevelRepeatDim', + 50714: 'BlackLevel', + 50715: 'BlackLevelDeltaH', + 50716: 'BlackLevelDeltaV', + 50717: 'WhiteLevel', + 50718: 'DefaultScale', + 50719: 'DefaultCropOrigin', + 50720: 'DefaultCropSize', + 50721: 'ColorMatrix1', + 50722: 'ColorMatrix2', + 50723: 'CameraCalibration1', + 50724: 'CameraCalibration2', + 50725: 'ReductionMatrix1', + 50726: 'ReductionMatrix2', + 50727: 'AnalogBalance', + 50728: 'AsShotNeutral', + 50729: 'AsShotWhiteXY', + 50730: 'BaselineExposure', + 50731: 'BaselineNoise', + 50732: 'BaselineSharpness', + 50733: 'BayerGreenSplit', + 50734: 'LinearResponseLimit', + 50735: 'CameraSerialNumber', + 50736: 'LensInfo', + 50737: 'ChromaBlurRadius', + 50738: 'AntiAliasStrength', + 50739: 'ShadowScale', + 50740: 'DNGPrivateData', + 50741: 'MakerNoteSafety', + 50778: 'CalibrationIlluminant1', + 50779: 'CalibrationIlluminant2', + 50780: 'BestQualityScale', + 50781: 'RawDataUniqueID', + 50827: 'OriginalRawFileName', + 50828: 'OriginalRawFileData', + 50829: 'ActiveArea', + 50830: 'MaskedAreas', + 50831: 'AsShotICCProfile', + 50832: 'AsShotPreProfileMatrix', + 50833: 'CurrentICCProfile', + 50834: 'CurrentPreProfileMatrix', + 50879: 'ColorimetricReference', + 50931: 'CameraCalibrationSignature', + 50932: 'ProfileCalibrationSignature', + 50934: 'AsShotProfileName', + 50935: 'NoiseReductionApplied', + 50936: 'ProfileName', + 50937: 'ProfileHueSatMapDims', + 50938: 'ProfileHueSatMapData1', + 50939: 'ProfileHueSatMapData2', + 50940: 'ProfileToneCurve', + 50941: 'ProfileEmbedPolicy', + 50942: 'ProfileCopyright', + 50964: 'ForwardMatrix1', + 50965: 'ForwardMatrix2', + 50966: 'PreviewApplicationName', + 50967: 'PreviewApplicationVersion', + 50968: 'PreviewSettingsName', + 50969: 'PreviewSettingsDigest', + 50970: 'PreviewColorSpace', + 50971: 'PreviewDateTime', + 50972: 'RawImageDigest', + 50973: 'OriginalRawFileDigest', + 50974: 'SubTileBlockSize', + 50975: 'RowInterleaveFactor', + 50981: 'ProfileLookTableDims', + 50982: 'ProfileLookTableData', + 51008: 'OpcodeList1', + 51009: 'OpcodeList2', + 51022: 'OpcodeList3', + 51041: 'NoiseProfile'} + + def __init__(self, endian, buffer, offset): + _Ifd.__init__(self, endian, buffer, offset) + self.post_process(self.tagnum2name) + + +class _ExifPhotoIfd(_Ifd): + tagnum2name = {33434: 'ExposureTime', + 33437: 'FNumber', + 34850: 'ExposureProgram', + 34852: 'SpectralSensitivity', + 34855: 'ISOSpeedRatings', + 34856: 'OECF', + 34864: 'SensitivityType', + 34865: 'StandardOutputSensitivity', + 34866: 'RecommendedExposureIndex', + 34867: 'ISOSpeed', + 34868: 'ISOSpeedLatitudeyyy', + 34869: 'ISOSpeedLatitudezzz', + 36864: 'ExifVersion', + 36867: 'DateTimeOriginal', + 36868: 'DateTimeDigitized', + 37121: 'ComponentsConfiguration', + 37122: 'CompressedBitsPerPixel', + 37377: 'ShutterSpeedValue', + 37378: 'ApertureValue', + 37379: 'BrightnessValue', + 37380: 'ExposureBiasValue', + 37381: 'MaxApertureValue', + 37382: 'SubjectDistance', + 37383: 'MeteringMode', + 37384: 'LightSource', + 37385: 'Flash', + 37386: 'FocalLength', + 37396: 'SubjectArea', + 37500: 'MakerNote', + 37510: 'UserComment', + 37520: 'SubSecTime', + 37521: 'SubSecTimeOriginal', + 37522: 'SubSecTimeDigitized', + 40960: 'FlashpixVersion', + 40961: 'ColorSpace', + 40962: 'PixelXDimension', + 40963: 'PixelYDimension', + 40964: 'RelatedSoundFile', + 40965: 'InteroperabilityTag', + 41483: 'FlashEnergy', + 41484: 'SpatialFrequencyResponse', + 41486: 'FocalPlaneXResolution', + 41487: 'FocalPlaneYResolution', + 41488: 'FocalPlaneResolutionUnit', + 41492: 'SubjectLocation', + 41493: 'ExposureIndex', + 41495: 'SensingMethod', + 41728: 'FileSource', + 41729: 'SceneType', + 41730: 'CFAPattern', + 41985: 'CustomRendered', + 41986: 'ExposureMode', + 41987: 'WhiteBalance', + 41988: 'DigitalZoomRatio', + 41989: 'FocalLengthIn35mmFilm', + 41990: 'SceneCaptureType', + 41991: 'GainControl', + 41992: 'Contrast', + 41993: 'Saturation', + 41994: 'Sharpness', + 41995: 'DeviceSettingDescription', + 41996: 'SubjectDistanceRange', + 42016: 'ImageUniqueID', + 42032: 'CameraOwnerName', + 42033: 'BodySerialNumber', + 42034: 'LensSpecification', + 42035: 'LensMake', + 42036: 'LensModel', + 42037: 'LensSerialNumber'} + + def __init__(self, endian, buffer, offset): + _Ifd.__init__(self, endian, buffer, offset) + self.post_process(self.tagnum2name) + + +class _ExifGPSInfoIfd(_Ifd): + tagnum2name = {0: 'GPSVersionID', + 1: 'GPSLatitudeRef', + 2: 'GPSLatitude', + 3: 'GPSLongitudeRef', + 4: 'GPSLongitude', + 5: 'GPSAltitudeRef', + 6: 'GPSAltitude', + 7: 'GPSTimeStamp', + 8: 'GPSSatellites', + 9: 'GPSStatus', + 10: 'GPSMeasureMode', + 11: 'GPSDOP', + 12: 'GPSSpeedRef', + 13: 'GPSSpeed', + 14: 'GPSTrackRef', + 15: 'GPSTrack', + 16: 'GPSImgDirectionRef', + 17: 'GPSImgDirection', + 18: 'GPSMapDatum', + 19: 'GPSDestLatitudeRef', + 20: 'GPSDestLatitude', + 21: 'GPSDestLongitudeRef', + 22: 'GPSDestLongitude', + 23: 'GPSDestBearingRef', + 24: 'GPSDestBearing', + 25: 'GPSDestDistanceRef', + 26: 'GPSDestDistance', + 27: 'GPSProcessingMethod', + 28: 'GPSAreaInformation', + 29: 'GPSDateStamp', + 30: 'GPSDifferential'} + + def __init__(self, endian, buffer, offset): + _Ifd.__init__(self, endian, buffer, offset) + self.post_process(self.tagnum2name) + + +class _ExifInteroperabilityIfd(_Ifd): + tagnum2name = {1: 'InteroperabilityIndex', + 2: 'InteroperabilityVersion', + 4096: 'RelatedImageFileFormat', + 4097: 'RelatedImageWidth', + 4098: 'RelatedImageLength'} + + def __init__(self, endian, buffer, offset): + _Ifd.__init__(self, endian, buffer, offset) + self.post_process(self.tagnum2name) + + # Map each box ID to the corresponding class. _box_with_id = { 'asoc': AssociationBox, @@ -1761,3 +2150,36 @@ _box_with_id = { 'url ': DataEntryURLBox, 'uuid': UUIDBox, 'xml ': XMLBox} + + +def _indent(elem, level=0): + """Recipe for pretty printing XML. Please see + + http://effbot.org/zone/element-lib.htm#prettyprint + """ + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + _indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +def _pretty_print_xml(xml, level=0): + """Pretty print XML data. + """ + xml = copy.deepcopy(xml) + _indent(xml, level=level) + xmltext = ET.tostring(xml).decode('utf-8') + + # Indent it a bit. + lst = [(' ' + x) for x in xmltext.split('\n')] + xml = '\n'.join(lst) + return '\n{0}'.format(xml) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index dbfc74d..a42e291 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -8,6 +8,7 @@ if sys.hexversion >= 0x03030000: else: from contextlib2 import ExitStack import ctypes +import math import os import struct import warnings @@ -152,7 +153,7 @@ class Jp2k(Jp2kBox): data : array Image data to be written to file. callbacks : bool, optional - If true, enable default info handler such that INFO messages + 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. @@ -181,7 +182,8 @@ class Jp2k(Jp2kBox): psnr : list, optional Different PSNR for successive layers. psizes : list, optional - List of precinct sizes. + 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 @@ -231,6 +233,11 @@ class Jp2k(Jp2kBox): msg = "Code block area cannot exceed 4096. " msg += "Code block height and width must be larger than 4." raise RuntimeError(msg) + if ((math.log(h, 2) != math.floor(math.log(h, 2)) or + math.log(w, 2) != math.floor(math.log(w, 2)))): + msg = "Bad code block size ({0}, {1}), " + msg += "must be powers of 2." + raise IOError(msg.format(h, w)) cparams.cblockw_init = w cparams.cblockh_init = h @@ -266,9 +273,21 @@ class Jp2k(Jp2kBox): cparams.cp_fixed_quality = 1 if psizes is not None: - for j, precinct in enumerate(psizes): - cparams.prcw_init[j] = precinct[0] - cparams.prch_init[j] = precinct[1] + for j, (prch, prcw) in enumerate(psizes): + if j == 0 and cbsize is not None: + cblkh, cblkw = cbsize + if cblkh * 2 > prch or cblkw * 2 > prcw: + msg = "Highest Resolution precinct size must be at " + msg += "least twice that of the code block dimensions." + raise IOError(msg) + if ((math.log(prch, 2) != math.floor(math.log(prch, 2)) or + math.log(prcw, 2) != math.floor(math.log(prcw, 2)))): + msg = "Bad precinct sizes ({0}, {1}), " + msg += "must be powers of 2." + raise IOError(msg.format(prch, prcw)) + + cparams.prcw_init[j] = prcw + cparams.prch_init[j] = prch cparams.csty |= 0x01 cparams.res_spec = len(psizes) @@ -634,7 +653,7 @@ class Jp2k(Jp2kBox): >>> jp = glymur.Jp2k(jfile) >>> codestream = jp.get_codestream() >>> print(codestream.segment[1]) - SIZ marker segment @ (87, 47) + SIZ marker segment @ (3137, 47) Profile: 2 Reference Grid Height, Width: (1456 x 2592) Vertical, Horizontal Reference Grid Offset: (0 x 0) diff --git a/glymur/test/test_callbacks.py b/glymur/test/test_callbacks.py index 1c70743..aae4ddc 100644 --- a/glymur/test/test_callbacks.py +++ b/glymur/test/test_callbacks.py @@ -53,7 +53,7 @@ class TestCallbacks(unittest.TestCase): d = j.read(reduce=3, verbose=True, area=(0, 0, 512, 1024)) actual = sys.stdout.getvalue().strip() - lines = ['[INFO] Start to read j2k main header (85).', + lines = ['[INFO] Start to read j2k main header (3135).', '[INFO] Main header has been correctly decoded.', '[INFO] Setting decoding area to 0,0,1024,512', '[INFO] Header of tile 0 / 17 has been read.', diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 5234e0b..a32cda8 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -50,19 +50,14 @@ def chdir(dirname=None): class TestJp2k(unittest.TestCase): - def setUp(self): - self.jp2file = pkg_resources.resource_filename(glymur.__name__, - "data/nemo.jp2") - - def tearDown(self): - pass - - @unittest.skipIf(sys.hexversion < 0x03020000, - "Uses features introduced in 3.2.") - def test_invalid_xml_box(self): - # Should be able to recover from xml box with bad xml. - with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - with open(self.jp2file, 'rb') as ifile: + @classmethod + def setUpClass(cls): + # Setup a JP2 file with a bad XML box. + 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) @@ -81,13 +76,35 @@ class TestJp2k(unittest.TestCase): tfile.write(buffer) tfile.flush() - with self.assertWarns(UserWarning) as cw: - jp2k = Jp2k(tfile.name) + @classmethod + def tearDownClass(cls): + os.unlink(cls._bad_xml_file) - self.assertEqual(jp2k.box[3].id, 'xml ') - self.assertEqual(jp2k.box[3].offset, 77) - self.assertEqual(jp2k.box[3].length, 28) - self.assertIsNone(jp2k.box[3].xml) + def setUp(self): + self.jp2file = pkg_resources.resource_filename(glymur.__name__, + "data/nemo.jp2") + + 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.2+ + 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].id, 'xml ') + self.assertEqual(jp2k.box[3].offset, 77) + self.assertEqual(jp2k.box[3].length, 28) + self.assertIsNone(jp2k.box[3].xml) def test_bad_area_parameter(self): # Verify that we error out appropriately if given a bad area parameter. @@ -134,12 +151,25 @@ class TestJp2k(unittest.TestCase): subsetdata = j.read(area=(0, 0, 512, 512)) np.testing.assert_array_equal(tiledata, subsetdata) + def test_write_cprl(self): + # Issue 17 + j = Jp2k(self.jp2file) + expdata = j.read(reduce=2) + 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) + def test_jp2_boxes(self): # Verify the boxes of a JP2 file. jp2k = Jp2k(self.jp2file) # top-level boxes - self.assertEqual(len(jp2k.box), 4) + self.assertEqual(len(jp2k.box), 6) self.assertEqual(jp2k.box[0].id, 'jP ') self.assertEqual(jp2k.box[0].offset, 0) @@ -156,9 +186,17 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[2].length, 45) self.assertEqual(jp2k.box[2].longname, 'JP2 Header') - self.assertEqual(jp2k.box[3].id, 'jp2c') + self.assertEqual(jp2k.box[3].id, 'uuid') self.assertEqual(jp2k.box[3].offset, 77) - self.assertEqual(jp2k.box[3].length, 1133427) + self.assertEqual(jp2k.box[3].length, 638) + + self.assertEqual(jp2k.box[4].id, 'uuid') + self.assertEqual(jp2k.box[4].offset, 715) + self.assertEqual(jp2k.box[4].length, 2412) + + self.assertEqual(jp2k.box[5].id, 'jp2c') + self.assertEqual(jp2k.box[5].offset, 3127) + self.assertEqual(jp2k.box[5].length, 1133427) # jp2h super box self.assertEqual(len(jp2k.box[2].box), 2) @@ -182,7 +220,7 @@ class TestJp2k(unittest.TestCase): self.assertEqual(jp2k.box[2].box[1].longname, 'Colour Specification') self.assertEqual(jp2k.box[2].box[1].precedence, 0) self.assertEqual(jp2k.box[2].box[1].approximation, 0) - self.assertEqual(jp2k.box[2].box[1].color_space, glymur.core.SRGB) + 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, @@ -200,7 +238,7 @@ 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(77) + buffer = ifile.read(3127) tfile.write(buffer) # The L field must be 1 in order to signal the presence of the @@ -221,9 +259,9 @@ class TestJp2k(unittest.TestCase): jp2k = Jp2k(tfile.name) - self.assertEqual(jp2k.box[3].id, 'jp2c') - self.assertEqual(jp2k.box[3].offset, 77) - self.assertEqual(jp2k.box[3].length, 1133427 + 8) + self.assertEqual(jp2k.box[5].id, 'jp2c') + self.assertEqual(jp2k.box[5].offset, 3127) + self.assertEqual(jp2k.box[5].length, 1133427 + 8) def test_L_is_zero(self): # Verify that boxes with the L field as zero are correctly read. @@ -332,14 +370,14 @@ class TestJp2k(unittest.TestCase): 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].color_space, glymur.core.SRGB) + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB) 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].color_space, + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.GREYSCALE) def test_specify_grey(self): @@ -347,7 +385,7 @@ class TestJp2k(unittest.TestCase): 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].color_space, + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.GREYSCALE) def test_grey_with_extra_component(self): @@ -358,7 +396,7 @@ class TestJp2k(unittest.TestCase): 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].color_space, + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.GREYSCALE) def test_grey_with_two_extra_components(self): @@ -369,7 +407,7 @@ class TestJp2k(unittest.TestCase): 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].color_space, + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.GREYSCALE) def test_rgb_with_extra_component(self): @@ -380,7 +418,7 @@ class TestJp2k(unittest.TestCase): 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].color_space, glymur.core.SRGB) + self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB) def test_specify_ycc(self): # We don't support writing YCC at the moment. @@ -513,10 +551,9 @@ class TestJp2k(unittest.TestCase): # Now append the codestream. tfile2.write(codestream) + tfile2.flush() - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - jasoc = Jp2k(tfile2.name) + jasoc = Jp2k(tfile2.name) self.assertEqual(jasoc.box[3].id, 'asoc') self.assertEqual(jasoc.box[3].box[0].id, 'lbl ') self.assertEqual(jasoc.box[3].box[0].label, 'label') @@ -528,17 +565,19 @@ class TestJp2k(unittest.TestCase): with open(self.jp2file, 'rb') as fp: data = fp.read() with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - tfile.write(data[0:129]) + # Codestream starts at byte 3127. SIZ marker at 3137. + # COD marker at 3186. Subsampling at 3180. + tfile.write(data[0:3179]) # Make the DY bytes of the SIZ segment zero. That means that # a subsampling factor is zero, which is illegal. tfile.write(b'\x00') - tfile.write(data[130:132]) + tfile.write(data[3180:3182]) tfile.write(b'\x00') - tfile.write(data[134:136]) + tfile.write(data[3184:3186]) tfile.write(b'\x00') - tfile.write(data[136:]) + tfile.write(data[3186:]) tfile.flush() with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -597,6 +636,27 @@ class TestJp2k(unittest.TestCase): with self.assertWarns(UserWarning) as cw: imp.reload(glymur) + @unittest.skipIf(sys.hexversion < 0x03020000, + "Uses features introduced in 3.2.") + def test_home_dir_missing_config_dir(self): + # Verify no exception is raised if $HOME is missing .glymur directory. + with tempfile.TemporaryDirectory() as tdir: + with patch.dict('os.environ', {'HOME': tdir}): + # Misconfigured new configuration file should + # be rejected. + with self.assertWarns(UserWarning) as cw: + imp.reload(glymur) + + 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') if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index 112fbaf..15913e2 100644 --- a/glymur/test/test_opj_suite.py +++ b/glymur/test/test_opj_suite.py @@ -3,6 +3,7 @@ The tests defined here roughly correspond to what is in the OpenJPEG test suite. """ +from contextlib import contextmanager import os import platform import re @@ -11,6 +12,13 @@ from xml.etree import cElementTree as ET import unittest import warnings +if sys.hexversion <= 0x03030000: + from mock import patch + from StringIO import StringIO +else: + from unittest.mock import patch + from io import StringIO + import numpy as np from glymur import Jp2k @@ -263,6 +271,7 @@ class TestSuite(unittest.TestCase): np.testing.assert_array_equal(jpdata, pgxdata) + @unittest.skip("fprintf stderr output in r2343.") def test_ETS_C0P0_p0_12_j2k(self): jfile = os.path.join(data_root, 'input/conformance/p0_12.j2k') jp2k = Jp2k(jfile) @@ -622,6 +631,7 @@ class TestSuite(unittest.TestCase): pgxdata = read_pgx(pgxfile) np.testing.assert_array_equal(jpdata, pgxdata) + @unittest.skip("fprintf stderr output in r2343.") def test_ETS_C1P0_p0_12_j2k(self): jfile = os.path.join(data_root, 'input/conformance/p0_12.j2k') jp2k = Jp2k(jfile) @@ -631,6 +641,7 @@ class TestSuite(unittest.TestCase): pgxdata = read_pgx(pgxfile) np.testing.assert_array_equal(jpdata, pgxdata) + @unittest.skip("fprintf stderr output in r2343.") def test_ETS_C1P0_p0_13_j2k(self): jfile = os.path.join(data_root, 'input/conformance/p0_13.j2k') jp2k = Jp2k(jfile) @@ -750,6 +761,7 @@ class TestSuite(unittest.TestCase): self.assertTrue(peak_tolerance(jpdata, pgxdata) < 624) self.assertTrue(mse(jpdata, pgxdata) < 3080) + @unittest.skip("fprintf stderr output in r2343.") def test_ETS_C1P1_p1_05_j2k(self): jfile = os.path.join(data_root, 'input/conformance/p1_05.j2k') jp2k = Jp2k(jfile) @@ -770,6 +782,7 @@ class TestSuite(unittest.TestCase): self.assertTrue(peak_tolerance(jpdata[:, :, 2], pgxdata) < 40) self.assertTrue(mse(jpdata[:, :, 2], pgxdata) < 10.154) + @unittest.skip("fprintf stderr output in r2343.") def test_ETS_C1P1_p1_06_j2k(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -790,6 +803,7 @@ class TestSuite(unittest.TestCase): self.assertTrue(peak_tolerance(jpdata[:, :, 2], pgxdata) < 2) self.assertTrue(mse(jpdata[:, :, 2], pgxdata) < 0.6) + @unittest.skip("fprintf stderr output in r2343.") def test_ETS_C1P1_p1_07_j2k(self): jfile = os.path.join(data_root, 'input/conformance/p1_07.j2k') jp2k = Jp2k(jfile) @@ -3389,7 +3403,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].method, 1) self.assertEqual(jp2.box[3].box[1].precedence, 0) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact ?? - self.assertEqual(jp2.box[3].box[1].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[3].box[1].colorspace, glymur.core.SRGB) # XML box tags = [x.tag for x in jp2.box[4].xml] @@ -3431,7 +3445,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 1) # JPX exact?? - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.YCC) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.YCC) # Jp2 Header # Channel Definition @@ -3477,7 +3491,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 1) # JPX exact - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.YCC) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.YCC) # sub-sampling codestream = jp2.get_codestream() @@ -3523,7 +3537,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 1) # JPX exact? - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.GREYSCALE) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.GREYSCALE) def test_NR_file5_dump(self): # Three 8-bit components in the ROMM-RGB colourspace, encapsulated in a @@ -3566,7 +3580,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].precedence, 0) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact self.assertEqual(len(jp2.box[3].box[1].icc_profile), 546) - self.assertIsNone(jp2.box[3].box[1].color_space) + self.assertIsNone(jp2.box[3].box[1].colorspace) # Jp2 Header # Colour specification @@ -3574,7 +3588,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[2].precedence, 1) self.assertEqual(jp2.box[3].box[2].approximation, 1) # JPX exact self.assertIsNone(jp2.box[3].box[2].icc_profile) - self.assertEqual(jp2.box[3].box[2].color_space, + self.assertEqual(jp2.box[3].box[2].colorspace, glymur.core.ROMM_RGB) def test_NR_file6_dump(self): @@ -3612,7 +3626,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 1) # JPX exact self.assertIsNone(jp2.box[2].box[1].icc_profile) - self.assertEqual(jp2.box[2].box[1].color_space, + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.GREYSCALE) def test_NR_file7_dump(self): @@ -3661,7 +3675,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].precedence, 0) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact self.assertEqual(len(jp2.box[3].box[1].icc_profile), 13332) - self.assertIsNone(jp2.box[3].box[1].color_space) + self.assertIsNone(jp2.box[3].box[1].colorspace) # Jp2 Header # Colour specification @@ -3669,7 +3683,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[2].precedence, 1) self.assertEqual(jp2.box[3].box[2].approximation, 1) # JPX exact self.assertIsNone(jp2.box[3].box[2].icc_profile) - self.assertEqual(jp2.box[3].box[2].color_space, + self.assertEqual(jp2.box[3].box[2].colorspace, glymur.core.E_SRGB) def test_NR_file8_dump(self): @@ -3710,7 +3724,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 1) # JPX exact self.assertEqual(len(jp2.box[2].box[1].icc_profile), 414) - self.assertIsNone(jp2.box[2].box[1].color_space) + self.assertIsNone(jp2.box[2].box[1].colorspace) # XML box tags = [x.tag for x in jp2.box[3].xml] @@ -3782,7 +3796,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[3].precedence, 0) self.assertEqual(jp2.box[2].box[3].approximation, 1) # JPX exact self.assertIsNone(jp2.box[2].box[3].icc_profile) - self.assertEqual(jp2.box[2].box[3].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[2].box[3].colorspace, glymur.core.SRGB) def test_NR_00042_j2k_dump(self): # Profile 3. @@ -4313,6 +4327,7 @@ class TestSuite(unittest.TestCase): # Comment value self.assertEqual(c.segment[4].Ccme.decode('latin-1'), "Kakadu-v6.3.1") + @unittest.skip("fprintf stderr output in r2343.") def test_NR_illegalcolortransform_dump(self): jfile = os.path.join(data_root, 'input/nonregression/illegalcolortransform.j2k') @@ -5257,7 +5272,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # not allowed? - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) c = jp2.box[3].main_header @@ -5390,7 +5405,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2 - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) c = jp2.box[3].main_header @@ -5518,7 +5533,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2 - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.YCC) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.YCC) c = jp2.box[3].main_header @@ -5658,7 +5673,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].method, 1) # enumerated self.assertEqual(jp2.box[3].box[1].precedence, 2) self.assertEqual(jp2.box[3].box[1].approximation, 1) # exact - self.assertEqual(jp2.box[3].box[1].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[3].box[1].colorspace, glymur.core.SRGB) # Jp2 Header # Palette box. @@ -5769,7 +5784,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) # Skip the 4th box, it is uknown. @@ -5869,7 +5884,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].method, 1) # ICC self.assertEqual(jp2.box[3].box[1].precedence, 2) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact - self.assertEqual(jp2.box[3].box[1].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[3].box[1].colorspace, glymur.core.SRGB) c = jp2.box[4].main_header @@ -5966,7 +5981,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2 - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.GREYSCALE) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.GREYSCALE) # Jp2 Header # Channel Definition @@ -6085,7 +6100,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].method, 1) # enumerated self.assertEqual(jp2.box[3].box[1].precedence, 2) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact - self.assertEqual(jp2.box[3].box[1].color_space, glymur.core.CMYK) + self.assertEqual(jp2.box[3].box[1].colorspace, glymur.core.CMYK) # Jp2 Header # Palette box. @@ -6199,7 +6214,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].method, 1) # enumerated self.assertEqual(jp2.box[3].box[1].precedence, 2) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact - self.assertEqual(jp2.box[3].box[1].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[3].box[1].colorspace, glymur.core.SRGB) # Jp2 Header # Palette box. @@ -6307,7 +6322,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].method, 1) self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2 - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.YCC) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.YCC) c = jp2.box[3].main_header @@ -6412,7 +6427,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2 self.assertEqual(len(jp2.box[2].box[1].icc_profile), 1) - self.assertIsNone(jp2.box[2].box[1].color_space) + self.assertIsNone(jp2.box[2].box[1].colorspace) c = jp2.box[3].main_header @@ -6508,7 +6523,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) # JP2 self.assertEqual(len(jp2.box[2].box[1].icc_profile), 1) - self.assertIsNone(jp2.box[2].box[1].color_space) + self.assertIsNone(jp2.box[2].box[1].colorspace) c = jp2.box[3].main_header @@ -6610,7 +6625,7 @@ class TestSuite(unittest.TestCase): self.assertEqual(jp2.box[3].box[1].precedence, 2) self.assertEqual(jp2.box[3].box[1].approximation, 1) # JPX exact self.assertEqual(len(jp2.box[3].box[1].icc_profile), 1328) - self.assertIsNone(jp2.box[3].box[1].color_space) + self.assertIsNone(jp2.box[3].box[1].colorspace) # UUID boxes. All mentioned in the RREQ box. self.assertEqual(jp2.box[2].vendor_feature[0], jp2.box[4].uuid) @@ -6690,6 +6705,7 @@ class TestSuite(unittest.TestCase): data = jp2.read() self.assertTrue(True) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_123_j2c_3_decode(self): jfile = os.path.join(data_root, 'input/nonregression/123.j2c') @@ -6735,6 +6751,7 @@ class TestSuite(unittest.TestCase): data = Jp2k(jfile).read() self.assertTrue(True) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_bug_j2c_8_decode(self): jfile = os.path.join(data_root, 'input/nonregression/bug.j2c') @@ -6771,6 +6788,7 @@ class TestSuite(unittest.TestCase): data = Jp2k(jfile).read() self.assertTrue(True) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_illegalcolortransform_j2k_14_decode(self): # Stream too short, expected SOT. jfile = os.path.join(data_root, @@ -7102,6 +7120,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read(reduce=1) np.testing.assert_array_equal(tdata, odata[64:128, 256:320]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_61_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7109,6 +7128,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read() np.testing.assert_array_equal(ssdata, odata[0:12, 0:12]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_62_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7116,6 +7136,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read() np.testing.assert_array_equal(ssdata, odata[1:8, 8:11]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_63_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7123,6 +7144,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read() np.testing.assert_array_equal(ssdata, odata[9:12, 9:12]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_64_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7130,6 +7152,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read() np.testing.assert_array_equal(ssdata, odata[10:12, 4:10]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_65_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7137,6 +7160,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read() np.testing.assert_array_equal(ssdata, odata[3:9, 3:9]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_66_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7144,6 +7168,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read() np.testing.assert_array_equal(ssdata, odata[4:7, 4:7]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_67_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7151,6 +7176,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read() np.testing.assert_array_equal(ssdata, odata[4:5, 4: 5]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_68_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7158,6 +7184,7 @@ class TestSuite(unittest.TestCase): odata = jp2k.read(reduce=1) np.testing.assert_array_equal(ssdata, odata[0:6, 0:6]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_69_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7201,6 +7228,7 @@ class TestSuite(unittest.TestCase): with self.assertRaises((IOError, OSError)) as ce: ssdata = jp2k.read(area=(9, 9, 12, 12), reduce=2) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_76_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7210,6 +7238,7 @@ class TestSuite(unittest.TestCase): tiledata = jp2k.read(tile=0) np.testing.assert_array_equal(tiledata, fulldata[0:3, 0:3]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_77_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7219,6 +7248,7 @@ class TestSuite(unittest.TestCase): tiledata = jp2k.read(tile=5) np.testing.assert_array_equal(tiledata, fulldata[3:6, 3:6]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_78_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7228,6 +7258,7 @@ class TestSuite(unittest.TestCase): tiledata = jp2k.read(tile=9) np.testing.assert_array_equal(tiledata, fulldata[6:9, 3:6]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_79_decode(self): jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) @@ -7237,6 +7268,7 @@ class TestSuite(unittest.TestCase): tiledata = jp2k.read(tile=15) np.testing.assert_array_equal(tiledata, fulldata[9:12, 9:12]) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_80_decode(self): # Just read the data, don't bother verifying. jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') @@ -7245,6 +7277,7 @@ class TestSuite(unittest.TestCase): warnings.simplefilter("ignore") tiledata = jp2k.read(tile=0, reduce=2) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_81_decode(self): # Just read the data, don't bother verifying. jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') @@ -7253,6 +7286,7 @@ class TestSuite(unittest.TestCase): warnings.simplefilter("ignore") tiledata = jp2k.read(tile=5, reduce=2) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_82_decode(self): # Just read the data, don't bother verifying. jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') @@ -7261,6 +7295,7 @@ class TestSuite(unittest.TestCase): warnings.simplefilter("ignore") tiledata = jp2k.read(tile=9, reduce=2) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_83_decode(self): # tile size is 3x3. Reducing two levels results in no data. jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') @@ -7270,11 +7305,12 @@ class TestSuite(unittest.TestCase): with self.assertRaises((IOError, OSError)) as ce: tiledata = jp2k.read(tile=15, reduce=2) + @unittest.skip("fprintf stderr output in r2343.") def test_NR_DEC_p1_06_j2k_84_decode(self): # Just read the data, don't bother verifying. jfile = os.path.join(data_root, 'input/conformance/p1_06.j2k') jp2k = Jp2k(jfile) - tiledata = jp2k.read(reduce=4) + data = jp2k.read(reduce=4) def test_NR_DEC_p0_04_j2k_85_decode(self): jfile = os.path.join(data_root, 'input/conformance/p0_04.j2k') diff --git a/glymur/test/test_opj_suite_neg.py b/glymur/test/test_opj_suite_neg.py index 8296861..75c9c6f 100644 --- a/glymur/test/test_opj_suite_neg.py +++ b/glymur/test/test_opj_suite_neg.py @@ -9,6 +9,7 @@ import unittest import warnings import numpy as np +import pkg_resources from ..lib import openjp2 as opj2 @@ -62,7 +63,8 @@ def read_image(infile): class TestSuiteNegative(unittest.TestCase): def setUp(self): - pass + self.jp2file = pkg_resources.resource_filename(glymur.__name__, + "data/nemo.jp2") def tearDown(self): pass @@ -150,5 +152,35 @@ class TestSuiteNegative(unittest.TestCase): with self.assertWarns(UserWarning) as cw: j = Jp2k(infile) + def test_precinct_size_not_multiple_of_two(self): + # Seems like precinct sizes should be powers of two. + ifile = Jp2k(self.jp2file) + data = ifile.read(reduce=3) + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + ofile = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError) as ce: + ofile.write(data, psizes=[(13, 13)]) + + def test_codeblock_size_not_multiple_of_two(self): + # Seems like code block sizes should be powers of two. + ifile = Jp2k(self.jp2file) + data = ifile.read(reduce=3) + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + ofile = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError) as ce: + ofile.write(data, cbsize=(13, 12)) + + def test_codeblock_size_with_precinct_size(self): + # Seems like code block sizes should never exceed half that of + # precinct size. + ifile = Jp2k(self.jp2file) + data = ifile.read(reduce=3) + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + ofile = Jp2k(tfile.name, 'wb') + with self.assertRaises(IOError) as ce: + ofile.write(data, + cbsize=(64, 64), + psizes=[(64, 64)]) + if __name__ == "__main__": unittest.main() diff --git a/glymur/test/test_opj_suite_write.py b/glymur/test/test_opj_suite_write.py index 486521f..d511768 100644 --- a/glymur/test/test_opj_suite_write.py +++ b/glymur/test/test_opj_suite_write.py @@ -693,7 +693,7 @@ class TestSuiteWrite(unittest.TestCase): self.assertEqual(jp2.box[2].box[1].precedence, 0) self.assertEqual(jp2.box[2].box[1].approximation, 0) self.assertIsNone(jp2.box[2].box[1].icc_profile) - self.assertEqual(jp2.box[2].box[1].color_space, glymur.core.SRGB) + self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB) c = jp2.box[3].main_header diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 299085d..7680377 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -11,6 +11,7 @@ else: from io import StringIO import glymur +from glymur import Jp2k try: data_root = os.environ['OPJ_DATA_ROOT'] @@ -22,6 +23,22 @@ except: class TestPrinting(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Setup a plain JP2 file without the two UUID boxes. + jp2file = pkg_resources.resource_filename(glymur.__name__, + "data/nemo.jp2") + with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile: + cls._plain_nemo_file = tfile.name + ijfile = Jp2k(jp2file) + data = ijfile.read(reduce=3) + ojfile = Jp2k(cls._plain_nemo_file, 'wb') + ojfile.write(data) + + @classmethod + def tearDownClass(cls): + os.unlink(cls._plain_nemo_file) + def setUp(self): # Save sys.stdout. self.stdout = sys.stdout @@ -30,15 +47,14 @@ class TestPrinting(unittest.TestCase): "data/nemo.jp2") # Save the output of dumping nemo.jp2 for more than one test. - lines = ['File: nemo.jp2', - 'JPEG 2000 Signature Box (jP ) @ (0, 12)', + lines = ['JPEG 2000 Signature Box (jP ) @ (0, 12)', ' Signature: 0d0a870a', 'File Type Box (ftyp) @ (12, 20)', ' Brand: jp2 ', " Compatibility: ['jp2 ']", 'JP2 Header Box (jp2h) @ (32, 45)', ' Image Header Box (ihdr) @ (40, 22)', - ' Size: [1456 2592 3]', + ' Size: [182 324 3]', ' Bitdepth: 8', ' Signed: False', ' Compression: wavelet', @@ -47,15 +63,15 @@ class TestPrinting(unittest.TestCase): ' Method: enumerated colorspace', ' Precedence: 0', ' Colorspace: sRGB', - 'Contiguous Codestream Box (jp2c) @ (77, 1133427)', + 'Contiguous Codestream Box (jp2c) @ (77, 112814)', ' Main header:', ' SOC marker segment @ (85, 0)', ' SIZ marker segment @ (87, 47)', ' Profile: 2', - ' Reference Grid Height, Width: (1456 x 2592)', + ' Reference Grid Height, Width: (182 x 324)', ' Vertical, Horizontal Reference Grid Offset: ' + '(0 x 0)', - ' Reference Tile Height, Width: (512 x 512)', + ' Reference Tile Height, Width: (182 x 324)', ' Vertical, Horizontal Reference Tile Offset: ' + '(0 x 0)', ' Bitdepth: (8, 8, 8)', @@ -69,7 +85,7 @@ class TestPrinting(unittest.TestCase): ' EPH marker segments: False', ' Coding style parameters:', ' Progression order: LRCP', - ' Number of layers: 3', + ' Number of layers: 1', ' Multiple component transformation usage: ' + 'reversible', ' Number of resolutions: 6', @@ -93,16 +109,22 @@ class TestPrinting(unittest.TestCase): + '(0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), ' + '(0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), ' + '(0, 10)]'] - self.expectedNemo = '\n'.join(lines) + self.expectedPlain = '\n'.join(lines) def tearDown(self): # Restore stdout. sys.stdout = self.stdout def test_jp2dump(self): - glymur.jp2dump(self.jp2file) + glymur.jp2dump(self._plain_nemo_file) actual = sys.stdout.getvalue().strip() - self.assertEqual(actual, self.expectedNemo) + + # Get rid of the filename line, as it is not set in stone. + lst = actual.split('\n') + lst = lst[1:] + actual = '\n'.join(lst) + + self.assertEqual(actual, self.expectedPlain) def test_COC_segment(self): j = glymur.Jp2k(self.jp2file) @@ -110,7 +132,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[5]) actual = sys.stdout.getvalue().strip() - lines = ['COC marker segment @ (183, 9)', + lines = ['COC marker segment @ (3233, 9)', ' Associated component: 1', ' Coding style for this component: ' + 'Entropy coder, PARTITION = 0', @@ -136,7 +158,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[2]) actual = sys.stdout.getvalue().strip() - lines = ['COD marker segment @ (136, 12)', + lines = ['COD marker segment @ (3186, 12)', ' Coding style:', ' Entropy coder, without partitions', ' SOP marker segments: False', @@ -226,7 +248,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[-1]) actual = sys.stdout.getvalue().strip() - lines = ['EOC marker segment @ (1133502, 0)'] + lines = ['EOC marker segment @ (1136552, 0)'] expected = '\n'.join(lines) self.assertEqual(actual, expected) @@ -313,7 +335,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[6]) actual = sys.stdout.getvalue().strip() - lines = ['QCC marker segment @ (194, 20)', + lines = ['QCC marker segment @ (3244, 20)', ' Associated Component: 1', ' Quantization style: no quantization, 2 guard bits', ' Step size: [(0, 8), (0, 9), (0, 9), (0, 10), (0, 9), ' @@ -329,7 +351,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[3]) actual = sys.stdout.getvalue().strip() - lines = ['QCD marker segment @ (150, 19)', + lines = ['QCD marker segment @ (3200, 19)', ' Quantization style: no quantization, 2 guard bits', ' Step size: [(0, 8), (0, 9), (0, 9), (0, 10), (0, 9), ' + '(0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), ' @@ -344,7 +366,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[1]) actual = sys.stdout.getvalue().strip() - lines = ['SIZ marker segment @ (87, 47)', + lines = ['SIZ marker segment @ (3137, 47)', ' Profile: 2', ' Reference Grid Height, Width: (1456 x 2592)', ' Vertical, Horizontal Reference Grid Offset: (0 x 0)', @@ -364,7 +386,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[0]) actual = sys.stdout.getvalue().strip() - lines = ['SOC marker segment @ (85, 0)'] + lines = ['SOC marker segment @ (3135, 0)'] expected = '\n'.join(lines) self.assertEqual(actual, expected) @@ -374,7 +396,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[9]) actual = sys.stdout.getvalue().strip() - lines = ['SOD marker segment @ (249, 0)'] + lines = ['SOD marker segment @ (3299, 0)'] expected = '\n'.join(lines) self.assertEqual(actual, expected) @@ -384,7 +406,7 @@ class TestPrinting(unittest.TestCase): print(codestream.segment[4]) actual = sys.stdout.getvalue().strip() - lines = ['SOT marker segment @ (171, 10)', + lines = ['SOT marker segment @ (3221, 10)', ' Tile part index: 0', ' Tile part length: 78629', ' Tile part instance: 0', @@ -410,20 +432,45 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) - def test_entire_file(self): + def test_xmp(self): + # Verify the printing of a UUID/XMP box. j = glymur.Jp2k(self.jp2file) + print(j.box[4]) + actual = sys.stdout.getvalue().strip() + lst = ['UUID Box (uuid) @ (715, 2412)', + ' UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)', + ' UUID Data: ', + ' ', + ' ', + ' ', + ' ', + ' '] + expected = '\n'.join(lst) + self.assertEqual(actual, expected) + + def test_entire_file(self): + j = glymur.Jp2k(self._plain_nemo_file) print(j) actual = sys.stdout.getvalue().strip() - self.assertEqual(actual, self.expectedNemo) + + # Get rid of the filename line, as it is not set in stone. + lst = actual.split('\n') + lst = lst[1:] + actual = '\n'.join(lst) + + self.assertEqual(actual, self.expectedPlain) def test_codestream(self): j = glymur.Jp2k(self.jp2file) print(j.get_codestream()) actual = sys.stdout.getvalue().strip() - lst = ['Codestream:', - ' SOC marker segment @ (85, 0)', - ' SIZ marker segment @ (87, 47)', + ' SOC marker segment @ (3135, 0)', + ' SIZ marker segment @ (3137, 47)', ' Profile: 2', ' Reference Grid Height, Width: (1456 x 2592)', ' Vertical, Horizontal Reference Grid Offset: (0 x 0)', @@ -433,7 +480,7 @@ class TestPrinting(unittest.TestCase): ' Signed: (False, False, False)', ' Vertical, Horizontal Subsampling: ' + '((1, 1), (1, 1), (1, 1))', - ' COD marker segment @ (136, 12)', + ' COD marker segment @ (3186, 12)', ' Coding style:', ' Entropy coder, without partitions', ' SOP marker segments: False', @@ -455,7 +502,7 @@ class TestPrinting(unittest.TestCase): ' Vertically stripe causal context: False', ' Predictable termination: False', ' Segmentation symbols: False', - ' QCD marker segment @ (150, 19)', + ' QCD marker segment @ (3200, 19)', ' Quantization style: no quantization, ' + '2 guard bits', ' Step size: [(0, 8), (0, 9), (0, 9), ' @@ -642,6 +689,7 @@ class TestPrinting(unittest.TestCase): # Now append the codestream. tfile2.write(codestream) + tfile2.flush() jasoc = glymur.Jp2k(tfile2.name) print(jasoc.box[3]) @@ -763,5 +811,63 @@ class TestPrinting(unittest.TestCase): expected = '\n'.join(lines) self.assertEqual(actual, expected) + def test_exif_uuid(self): + j = glymur.Jp2k(self.jp2file) + + print(j.box[3]) + actual = sys.stdout.getvalue().strip() + + lines = ["UUID Box (uuid) @ (77, 638)", + " UUID: 4a706754-6966-6645-7869-662d3e4a5032 (Exif)", + " UUID Data: ", + "{'Exif': {'ExifTag': 138,", + " 'GPSTag': 354,", + " 'Make': 'HTC',", + " 'Model': 'HTC Glacier',", + " 'ResolutionUnit': 2,", + " 'XResolution': 72.0,", + " 'YCbCrPositioning': 1,", + " 'YResolution': 72.0},", + " 'GPSInfo': {'GPSAltitude': 0.0,", + " 'GPSAltitudeRef': 0,", + " 'GPSDateStamp': '2013:02:09',", + " 'GPSLatitude': [42.0, 20.0, 33.61],", + " 'GPSLatitudeRef': 'N',", + " 'GPSLongitude': [71.0, 5.0, 17.32],", + " 'GPSLongitudeRef': 'W',", + " 'GPSMapDatum': 'WGS-84',", + " 'GPSProcessingMethod': (65,", + " 83,", + " 67,", + " 73,", + " 73,", + " 0,", + " 0,", + " 0,", + " 78,", + " 69,", + " 84,", + " 87,", + " 79,", + " 82,", + " 75),", + " 'GPSTimeStamp': [19.0, 47.0, 53.0],", + " 'GPSVersionID': (2, 2, 0)},", + " 'Iop': None,", + " 'Photo': {'ColorSpace': 1,", + " 'ComponentsConfiguration': (1, 2, 3, 0),", + " 'DateTimeDigitized': '2013:02:09 14:47:53',", + " 'DateTimeOriginal': '2013:02:09 14:47:53',", + " 'ExifVersion': (48, 50, 50, 48),", + " 'FlashpixVersion': (48, 49, 48, 48),", + " 'FocalLength': 3.53,", + " 'ISOSpeedRatings': 76,", + " 'InteroperabilityTag': 324,", + " 'PixelXDimension': 2528,", + " 'PixelYDimension': 1424}}"] + + expected = '\n'.join(lines) + self.assertEqual(actual, expected) + if __name__ == "__main__": unittest.main() diff --git a/setup.py b/setup.py index 48ef364..3b8e14f 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ from distutils.core import setup -kwargs = {'name': 'glymur', - 'version': '0.1.1', - 'description': 'Tools for manipulating JPEG2000 files', - 'long_description': open('README').read(), +kwargs = {'name': 'Glymur', + 'version': '0.1.6', + 'description': 'Tools for accessing JPEG2000 files', + 'long_description': open('README.md').read(), 'author': 'John Evans', 'author_email': 'johnevans938 at gmail dot com', 'url': 'https://github.com/quintusdias/glymur', @@ -18,10 +18,10 @@ clssfrs = ["Programming Language :: Python", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: Implementation :: CPython", "License :: OSI Approved :: MIT License", - "Development Status :: 1 - Alpha", + "Development Status :: 3 - Alpha", "Operating System :: MacOS", "Operating System :: POSIX :: Linux", - "Intended Audience :: Scientific/Research", + "Intended Audience :: Science/Research", "Intended Audience :: Information Technology", "Topic :: Software Development :: Libraries :: Python Modules"] kwargs['classifiers'] = clssfrs