From 84a9b39fdbbcead8d1bd43a20b241b152cdaf73b Mon Sep 17 00:00:00 2001 From: John Evans Date: Mon, 3 Jun 2013 09:22:36 -0400 Subject: [PATCH 01/12] Filtering out all the tests where the library is printing directly to stderr. Should investigate suppressing stderr in these cases. #18 --- glymur/test/test_opj_suite.py | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/glymur/test/test_opj_suite.py b/glymur/test/test_opj_suite.py index 112fbaf..d9b5d11 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) @@ -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') @@ -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') From 0f20fc18af7b24bd95d5df1f52ba9c251cf3972d Mon Sep 17 00:00:00 2001 From: jevans Date: Mon, 3 Jun 2013 21:06:57 -0400 Subject: [PATCH 02/12] Starting Exif read support. --- glymur/jp2box.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index a435f67..f11674e 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1618,6 +1618,8 @@ class UUIDBox(Jp2kBox): else: text = buffer.decode('utf-8') kwargs['data'] = ET.fromstring(text) + elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2': + kwargs['data'] = _parse_exif(buffer) else: kwargs['data'] = buffer box = UUIDBox(**kwargs) @@ -1666,6 +1668,73 @@ def _indent(elem, level=0): if level and (not elem.tail or not elem.tail.strip()): elem.tail = i +_tagnum2name = {271: 'Make', 272: 'Model', + 282: 'XResolution', 283: 'YResolution', + 296: 'ResolutionUnit', + 531: 'YCbCrPositioning', + 34665: 'ExifTag', + 34853: 'GPSTag'} + +def _parse_exif(buffer): + """Interpret raw buffer consisting of Exif IFD. + """ + # Ignore the first six bytes. + # Next six should be (73, 73, 42, 8, numtags) + data = struct.unpack(' Date: Tue, 4 Jun 2013 08:25:10 -0400 Subject: [PATCH 03/12] Added XMP UUID example. Closes #21 --- docs/source/how_do_i.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index c5a411d..12c6a04 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -27,3 +27,35 @@ display just raw codestream as well. This will display metadata present in the codestream's main header only. :: >>> 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' From 84c46ea586266b28de9422696ed58c9758ff3f74 Mon Sep 17 00:00:00 2001 From: John Evans Date: Tue, 4 Jun 2013 15:35:35 -0400 Subject: [PATCH 04/12] Fixed #19 --- LICENSE.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE.txt 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. From b2d432770a260d5c8bc21868a861de9f7572181e Mon Sep 17 00:00:00 2001 From: John Evans Date: Tue, 4 Jun 2013 16:05:20 -0400 Subject: [PATCH 05/12] Added XMP test. closes #23 --- glymur/test/test_jp2k.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 41628c2..e05235a 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -646,6 +646,16 @@ class TestJp2k(unittest.TestCase): 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() From 4b07acc3e8726fa428acd985ab29ba7efec0c09c Mon Sep 17 00:00:00 2001 From: John Evans Date: Tue, 4 Jun 2013 16:12:19 -0400 Subject: [PATCH 06/12] Pep 8 work. --- docs/source/conf.py | 44 +++++++++++++++---------------- glymur/jp2box.py | 4 ++- glymur/test/test_opj_suite_neg.py | 2 +- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0b72b85..46d2d3c 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'] @@ -91,7 +94,8 @@ release = '0.1.3p1' # 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/glymur/jp2box.py b/glymur/jp2box.py index a435f67..29acab4 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1610,7 +1610,7 @@ class UUIDBox(Jp2kBox): n = offset + length - f.tell() buffer = f.read(n) if kwargs['uuid'] == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - # XMP data. Parse as XML. Seems to be a difference between + # 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') @@ -1647,6 +1647,7 @@ _box_with_id = { 'uuid': UUIDBox, 'xml ': XMLBox} + def _indent(elem, level=0): """Recipe for pretty printing XML. Please see @@ -1666,6 +1667,7 @@ def _indent(elem, level=0): 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. """ diff --git a/glymur/test/test_opj_suite_neg.py b/glymur/test/test_opj_suite_neg.py index 0862c42..75c9c6f 100644 --- a/glymur/test/test_opj_suite_neg.py +++ b/glymur/test/test_opj_suite_neg.py @@ -171,7 +171,7 @@ class TestSuiteNegative(unittest.TestCase): 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 + # Seems like code block sizes should never exceed half that of # precinct size. ifile = Jp2k(self.jp2file) data = ifile.read(reduce=3) From fd2fd63b287288aaddbef30e11993d7f76dee9c3 Mon Sep 17 00:00:00 2001 From: jevans Date: Tue, 4 Jun 2013 20:52:59 -0400 Subject: [PATCH 07/12] Added Exif classes for preliminary Exif image, photo, iop, gps support. --- glymur/jp2box.py | 295 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 227 insertions(+), 68 deletions(-) diff --git a/glymur/jp2box.py b/glymur/jp2box.py index f11674e..9edf010 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -1619,12 +1619,238 @@ class UUIDBox(Jp2kBox): text = buffer.decode('utf-8') kwargs['data'] = ET.fromstring(text) elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2': - kwargs['data'] = _parse_exif(buffer) + kwargs['data'] = Exif(buffer).exif_image 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. + """ + 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.num_tags, = struct.unpack(endian + 'H', buffer[offset:offset + 2]) + + fmt = self.endian + 'HHII' * self.num_tags + data = struct.unpack(fmt, buffer[offset + 2:offset + 2 + self.num_tags * 12]) + self.raw_ifd = {} + for j, tag in enumerate(data[0::4]): + tag_entry = buffer[offset + 2 + j * 12 + 8:offset + 2 + j * 12 + 8 + 4] + payload = self.parse_tag(data[j * 4 + 1], + data[j * 4 + 2], + tag_entry) + self.raw_ifd[tag] = payload + + 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 + payload = target_buffer.decode('utf-8').rstrip() + 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 + + +class ExifImageIfd(Ifd): + tagnum2name = {271: 'Make', + 272: 'Model', + 282: 'XResolution', + 283: 'YResolution', + 296: 'ResolutionUnit', + 531: 'YCbCrPositioning', + 34665: 'ExifTag', + 34853: 'GPSTag'} + + def __init__(self, endian, buffer, offset): + Ifd.__init__(self, endian, buffer, offset) + + # Now post process the raw IFD. + self.ifd = {} + for tag, value in self.raw_ifd.items(): + tag_name = self.tagnum2name[tag] + self.ifd[tag_name] = value + +class ExifPhotoIfd(Ifd): + tagnum2name = {34855: 'ISOSpeedRatings', + 36864: 'ExifVersion', + 36867: 'DateTimeOriginal', + 36868: 'DateTimeDigitized', + 37121: 'ComponentsConfiguration', + 37386: 'FocalLength', + 40960: 'FlashpixVersion', + 40961: 'ColorSpace', + 40962: 'PixelXDimension', + 40963: 'PixelYDimension', + 40965: 'InteroperabilityTag'} + + def __init__(self, endian, buffer, offset): + Ifd.__init__(self, endian, buffer, offset) + + # Now post process the raw IFD. + self.ifd = {} + for tag, value in self.raw_ifd.items(): + tag_name = self.tagnum2name[tag] + self.ifd[tag_name] = value + +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) + + # Now post process the raw IFD. + self.ifd = {} + for tag, value in self.raw_ifd.items(): + tag_name = self.tagnum2name[tag] + self.ifd[tag_name] = value + +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) + + # Now post process the raw IFD. + self.ifd = {} + for tag, value in self.raw_ifd.items(): + tag_name = self.tagnum2name[tag] + self.ifd[tag_name] = value # Map each box ID to the corresponding class. _box_with_id = { @@ -1668,73 +1894,6 @@ def _indent(elem, level=0): if level and (not elem.tail or not elem.tail.strip()): elem.tail = i -_tagnum2name = {271: 'Make', 272: 'Model', - 282: 'XResolution', 283: 'YResolution', - 296: 'ResolutionUnit', - 531: 'YCbCrPositioning', - 34665: 'ExifTag', - 34853: 'GPSTag'} - -def _parse_exif(buffer): - """Interpret raw buffer consisting of Exif IFD. - """ - # Ignore the first six bytes. - # Next six should be (73, 73, 42, 8, numtags) - data = struct.unpack(' Date: Tue, 4 Jun 2013 20:54:36 -0400 Subject: [PATCH 08/12] Just a comment. --- glymur/test/test_jp2k.py | 1 + 1 file changed, 1 insertion(+) diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 41628c2..f04741d 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -52,6 +52,7 @@ class TestJp2k(unittest.TestCase): @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: From 6407a272086f1c18884ffe39119e3ae244b61699 Mon Sep 17 00:00:00 2001 From: jevans Date: Tue, 4 Jun 2013 20:54:57 -0400 Subject: [PATCH 09/12] No longer testing nemo directly. --- glymur/test/test_printing.py | 100 ++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 36 deletions(-) diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index 0278d63..f0d1f7d 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,45 +63,29 @@ class TestPrinting(unittest.TestCase): ' Method: enumerated colorspace', ' Precedence: 0', ' Colorspace: sRGB', - 'UUID Box (uuid) @ (77, 638)', - ' UUID: 4a706754-6966-6645-7869-662d3e4a5032', - ' UUID Data: 614 bytes', - 'UUID Box (uuid) @ (715, 2412)', - ' UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)', - ' UUID Data: ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - 'Contiguous Codestream Box (jp2c) @ (3127, 1133427)', + 'Contiguous Codestream Box (jp2c) @ (77, 112814)', ' Main header:', - ' SOC marker segment @ (3135, 0)', - ' SIZ marker segment @ (3137, 47)', + ' 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)', ' Signed: (False, False, False)', ' Vertical, Horizontal Subsampling: ' + '((1, 1), (1, 1), (1, 1))', - ' COD marker segment @ (3186, 12)', + ' COD marker segment @ (136, 12)', ' Coding style:', ' Entropy coder, without partitions', ' SOP marker segments: False', ' 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', @@ -102,26 +102,29 @@ class TestPrinting(unittest.TestCase): + 'False', ' Predictable termination: False', ' Segmentation symbols: False', - ' QCD marker segment @ (3200, 19)', + ' QCD marker segment @ (150, 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), (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 - #import pdb; pdb.set_trace() def test_jp2dump(self): - glymur.jp2dump(self.jp2file) + glymur.jp2dump(self._plain_nemo_file) actual = sys.stdout.getvalue().strip() - self.actual = actual - self.expected = self.expectedNemo - 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) @@ -429,17 +432,42 @@ 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 @ (3135, 0)', ' SIZ marker segment @ (3137, 47)', From 42983b7b8e6348ace123accd6fe75bb7af34b20c Mon Sep 17 00:00:00 2001 From: John Evans Date: Wed, 5 Jun 2013 07:45:02 -0400 Subject: [PATCH 10/12] Added PyPI to list of places to get the code. Fixes #24 --- docs/source/how_do_i.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index c5a411d..fdc50ad 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -4,7 +4,10 @@ How do I...? Get the code? ============= -Go to http://github.com/quintusdias/glymur +Go to either of + + * http://github.com/quintusdias/glymur (bleeding edge) + * https://pypi.python.org/pypi/Glymur/ (stable releases) Display metadata? From f4308f1e230d7ac3dc0e7c2041473d2260cfdf9e Mon Sep 17 00:00:00 2001 From: John Evans Date: Wed, 5 Jun 2013 10:26:35 -0400 Subject: [PATCH 11/12] Stronger statement on skipped tests. Minor additional mods. --- docs/source/introduction.rst | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index c090d71..c8102ec 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -13,11 +13,11 @@ 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. -Of particular focus is retrieval of metadata. Reading XMP UUID -boxes is supported, as the data block consists of 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. +Of particular focus is retrieval of metadata. Reading XMP UUID boxes +is supported, as the data block consists of an XMP packet, which 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 @@ -32,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. @@ -185,7 +186,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. From 949671aa12d72b3c666ec933fe5108fae7f45859 Mon Sep 17 00:00:00 2001 From: jevans Date: Wed, 5 Jun 2013 20:42:42 -0400 Subject: [PATCH 12/12] Added Exif support. Closes #11. --- CHANGES.txt | 2 + docs/source/api.rst | 3 +- docs/source/conf.py | 2 +- docs/source/how_do_i.rst | 12 +- docs/source/index.rst | 7 +- docs/source/introduction.rst | 16 +- docs/source/roadmap.rst | 5 +- glymur/jp2box.py | 340 +++++++++++++++++++++++++++--- glymur/test/test_opj_suite_neg.py | 2 +- glymur/test/test_printing.py | 58 +++++ setup.py | 2 +- 11 files changed, 400 insertions(+), 49 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index bd5acf8..b8c6f05 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,5 @@ +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. diff --git a/docs/source/api.rst b/docs/source/api.rst index 3ab4997..fb2fd61 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,5 +1,6 @@ +--- API -=== +--- Jp2k ---- diff --git a/docs/source/conf.py b/docs/source/conf.py index 0b72b85..1e28b1e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -75,7 +75,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.3p1' +release = '0.1.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index c5a411d..724a3b7 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -1,6 +1,6 @@ -************ +------------ How do I...? -************ +------------ Get the code? ============= @@ -9,7 +9,7 @@ Go to http://github.com/quintusdias/glymur Display metadata? ================= -There are two ways. From the unix command line, the script **jp2dump** is +There are two ways. From the unix command line, the script *jp2dump* is available. :: $ jp2dump /path/to/glymur/installation/data/nemo.jp2 @@ -22,8 +22,8 @@ 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 just raw codestream as well. This will display metadata present in the -codestream's main header only. :: +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()) diff --git a/docs/source/index.rst b/docs/source/index.rst index ffc9fa7..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: @@ -16,9 +17,9 @@ Contents: how_do_i roadmap - +------------------ Indices and tables -================== +------------------ * :ref:`genindex` * :ref:`modindex` diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index c090d71..1797017 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -1,6 +1,6 @@ -========================================= +---------------------------------------- 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 @@ -13,15 +13,15 @@ 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. -Of particular focus is retrieval of metadata. Reading XMP UUID -boxes is supported, as the data block consists of XML. There is +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 @@ -132,9 +132,9 @@ Windows ------- Not currently supported. ------------------------------------- +'''''''''''''''''''''''''''''''''''' Installation, Testing, Configuration ------------------------------------- +'''''''''''''''''''''''''''''''''''' From this point forward, python3 will be referred to as just "python". diff --git a/docs/source/roadmap.rst b/docs/source/roadmap.rst index d010885..63036b3 100644 --- a/docs/source/roadmap.rst +++ b/docs/source/roadmap.rst @@ -1,9 +1,10 @@ +------- 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 - * add read support or Exif UUIDs * investigate using CFFI or cython instead of ctypes to wrap **openjp2** + * add read support for ICC profiles diff --git a/glymur/jp2box.py b/glymur/jp2box.py index 9edf010..b8361d3 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -14,6 +14,7 @@ References import copy import math import os +import pprint import struct import sys import uuid @@ -1570,6 +1571,9 @@ class UUIDBox(Jp2kBox): 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)) @@ -1610,7 +1614,7 @@ class UUIDBox(Jp2kBox): n = offset + length - f.tell() buffer = f.read(n) if kwargs['uuid'] == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): - # XMP data. Parse as XML. Seems to be a difference between + # 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') @@ -1619,18 +1623,25 @@ class UUIDBox(Jp2kBox): text = buffer.decode('utf-8') kwargs['data'] = ET.fromstring(text) elif kwargs['uuid'].bytes == b'JpgTiffExif->JP2': - kwargs['data'] = Exif(buffer).exif_image + 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. + Raw byte stream consisting of the UUID data. endian : str Either '<' for big-endian, or '>' for little-endian. """ @@ -1657,7 +1668,7 @@ class Exif: offset = data[3] # This is the 'Exif Image' portion. - exif = ExifImageIfd(self.endian, buffer[6:], offset) + exif = ExifImageIfd(self.endian, buffer[6:], offset) self.exif_image = exif.ifd if 'ExifTag' in self.exif_image.keys(): @@ -1677,12 +1688,13 @@ class Exif: gps = ExifGPSInfoIfd(self.endian, buffer[6:], offset) self.exif_gpsinfo = gps.ifd + class Ifd: """ Attributes ---------- buffer : bytes - Raw byte stream consisting of the UUID data. + Raw byte stream consisting of the UUID data. datatype2fmt : dictionary Class attribute, maps the TIFF enumerated datatype to the python datatype and data width. @@ -1706,36 +1718,45 @@ class Ifd: self.endian = endian self.buffer = buffer - self.num_tags, = struct.unpack(endian + 'H', buffer[offset:offset + 2]) - + self.num_tags, = struct.unpack(endian + 'H', + buffer[offset:offset + 2]) + fmt = self.endian + 'HHII' * self.num_tags - data = struct.unpack(fmt, buffer[offset + 2:offset + 2 + self.num_tags * 12]) + 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]): - tag_entry = buffer[offset + 2 + j * 12 + 8:offset + 2 + j * 12 + 8 + 4] - payload = self.parse_tag(data[j * 4 + 1], - data[j * 4 + 2], - tag_entry) - self.raw_ifd[tag] = payload + # 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. + # 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 - payload = target_buffer.decode('utf-8').rstrip() + 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: @@ -1746,22 +1767,226 @@ class Ifd: rational_payload.append(value) payload = rational_payload if count == 1: - # If just a single value, then return a scalar instead of a + # If just a single value, then return a scalar instead of a # tuple. payload = payload[0] - - return payload + + return payload class ExifImageIfd(Ifd): - tagnum2name = {271: 'Make', + """ + 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', + 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', - 34853: 'GPSTag'} + 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) @@ -1772,18 +1997,77 @@ class ExifImageIfd(Ifd): tag_name = self.tagnum2name[tag] self.ifd[tag_name] = value + class ExifPhotoIfd(Ifd): - tagnum2name = {34855: 'ISOSpeedRatings', + 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', - 40965: 'InteroperabilityTag'} + 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) @@ -1794,6 +2078,7 @@ class ExifPhotoIfd(Ifd): tag_name = self.tagnum2name[tag] self.ifd[tag_name] = value + class ExifGPSInfoIfd(Ifd): tagnum2name = {0: 'GPSVersionID', 1: 'GPSLatitudeRef', @@ -1836,6 +2121,7 @@ class ExifGPSInfoIfd(Ifd): tag_name = self.tagnum2name[tag] self.ifd[tag_name] = value + class ExifInteroperabilityIfd(Ifd): tagnum2name = {1: 'InteroperabilityIndex', 2: 'InteroperabilityVersion', @@ -1875,6 +2161,7 @@ _box_with_id = { 'uuid': UUIDBox, 'xml ': XMLBox} + def _indent(elem, level=0): """Recipe for pretty printing XML. Please see @@ -1894,6 +2181,7 @@ def _indent(elem, level=0): 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. """ diff --git a/glymur/test/test_opj_suite_neg.py b/glymur/test/test_opj_suite_neg.py index 0862c42..75c9c6f 100644 --- a/glymur/test/test_opj_suite_neg.py +++ b/glymur/test/test_opj_suite_neg.py @@ -171,7 +171,7 @@ class TestSuiteNegative(unittest.TestCase): 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 + # Seems like code block sizes should never exceed half that of # precinct size. ifile = Jp2k(self.jp2file) data = ifile.read(reduce=3) diff --git a/glymur/test/test_printing.py b/glymur/test/test_printing.py index f0d1f7d..8b7ba34 100644 --- a/glymur/test/test_printing.py +++ b/glymur/test/test_printing.py @@ -810,5 +810,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 449cb96..5febec4 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from distutils.core import setup kwargs = {'name': 'Glymur', - 'version': '0.1.3p1', + 'version': '0.1.4', 'description': 'Tools for accessing JPEG2000 files', 'long_description': open('README.md').read(), 'author': 'John Evans',