Merge branch 'release-0.4.1'

This commit is contained in:
John Evans 2013-08-21 11:07:53 -04:00
commit 8b0c8a0198
16 changed files with 903 additions and 73 deletions

17
.travis.yml Normal file
View file

@ -0,0 +1,17 @@
language: python
python:
- "2.7"
- "3.3"
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq libopenjpeg2
- sudo apt-get install -qq python-numpy
- sudo apt-get install -qq python3-numpy
# command to install dependencies
install:
- pip install . --use-mirrors
# command to run tests
script:
- "python -m unittest discover"

View file

@ -1,3 +1,10 @@
Aug 21, 2013 - v0.4.1 Fixed segfault with openjpeg 1.x when rlevel=-1
Aug 18, 2013 - v0.4.0 Added append method.
Aug 15, 2013 - v0.3.2 Fixed test bug where missing Pillow package caused test
failures.
Aug 14, 2013 - v0.3.1 Exposed mantissa, exponent, and guard_bits fields in QCC
and QCD segments. Exposed layers and code_block_size in COD segment.
Exposed precinct_size in COC segment.

View file

@ -76,9 +76,9 @@ copyright = u'2013, John Evans'
# built documents.
#
# The short X.Y version.
version = '0.3'
version = '0.4'
# The full version, including alpha/beta/rc tags.
release = '0.3.1'
release = '0.4.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -137,13 +137,14 @@ In addition, you must install contextlib2 and Pillow via pip. ::
Windows
-------
32-bit WinPython 2.7.5 seems to work with OpenJPEG 1.X, 2.0, and the
development version, but still requires contextlib2 and mock to be
32-bit WinPython 2.7.5 seemed to work with OpenJPEG 1.X, 2.0, and the
development version, but still required contextlib2 and mock to be
installed via pip. WinPython 3.3.2, however, seems to have trouble
with OpenJPEG 2.0, so I would suggest using the development version
there (I'm unwilling to spend ANY more time trying to figure out what
the problem is there).
At the moment I do not have access to a win32 machine, and
64-bit windows is completely untested.

View file

@ -3,7 +3,7 @@ How do I...?
------------
Read the lowest resolution thumbnail?
read the lowest resolution thumbnail?
=====================================
Printing the Jp2k object should reveal the number of resolutions (look in the
COD segment section), but you can take a shortcut by supplying -1 as the
@ -14,7 +14,7 @@ resolution level. ::
>>> j = glymur.Jp2k(file)
>>> thumbnail = j.read(rlevel=-1)
Display metadata?
display metadata?
=================
There are two ways. From the unix command line, the script *jp2dump* is
available. ::
@ -34,8 +34,40 @@ codestream box, only the main header is printed. It is possible to print
>>> print(j.get_codestream())
Add XML Metadata?
add XML metadata?
=================
You can append any number of XML boxes to a JP2 file (not to a raw codestream).
Consider the following XML file `data.xml` : ::
<?xml version="1.0"?>
<info>
<locality>
<city>Boston</city>
<snowfall>24.9 inches</snowfall>
</locality>
<locality>
<city>Portland</city>
<snowfall>31.9 inches</snowfall>
</locality>
<locality>
<city>New York City</city>
<snowfall>11.4 inches</snowfall>
</locality>
</info>
The **append** method can add an XML box as shown below::
>>> import shutil
>>> import glymur
>>> shutil.copyfile(glymur.data.nemo(), 'myfile.jp2')
>>> from xml.etree import cElementTree as ET
>>> jp2 = glymur.Jp2k('myfile.jp2')
>>> xmlbox = glymur.jp2box.XMLBox(filename='data.xml')
>>> jp2.append(xmlbox)
>>> print(jp2)
add metadata in a more general fashion?
=======================================
An existing raw codestream (or JP2 file) can be wrapped (re-wrapped) in a
user-defined set of JP2 boxes. To get just a minimal JP2 jacket on the
codestream provided by `goodstuff.j2k` (a file consisting of a raw codestream),
@ -75,8 +107,8 @@ two additional boxes (image header and color specification) contained in the
JP2 header superbox.
XML boxes are not in the minimal set of box requirements for the JP2 format, so
in order to add an XML box into the mix, we'll need to specify all of the
boxes. If you already have a JP2 jacket in place, you can just reuse it,
in order to add an XML box into the mix before the codestream box, we'll need to
re-specify all of the boxes. If you already have a JP2 jacket in place, you can just reuse that,
though. Take the following example content in an XML file `favorites.xml` : ::
<?xml version="1.0"?>
@ -84,10 +116,11 @@ though. Take the following example content in an XML file `favorites.xml` : ::
<category>Light Ale</category>
</favorite_things>
and add it after the JP2 header box, but before the codestream box ::
In order to add the XML after the JP2 header box, but before the codestream box,
the following will work. ::
>>> boxes = jp2.box # The box attribute is the list of JP2 boxes
>>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml')
>>> xmlbox = glymur.jp2box.XMLBox(filename='favorites.xml')
>>> boxes.insert(3, xmlbox)
>>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes)
>>> print(jp2_xml)
@ -119,7 +152,12 @@ and add it after the JP2 header box, but before the codestream box ::
. (truncated)
.
Create an image with an alpha layer?
As to the question of which method you should use, **append** or **wrap**,
to add metadata, you should keep in mind that **wrap** produces a new JP2 file,
while **append** modifies an existing file and is currently limited to XML
boxes.
create an image with an alpha layer?
====================================
OpenJPEG can create JP2 files with more than 3 components (requires
@ -181,7 +219,7 @@ Here's how the Preview application on the mac shows the RGBA image.
.. image:: goodstuff_alpha.png
Work with XMP UUIDs?
work with XMP UUIDs?
====================
The example JP2 file shipped with glymur has an XMP UUID. ::

View file

@ -1308,7 +1308,7 @@ class QCCsegment(Segment):
self.offset = offset
self.mantissa, self.exponent = parse_quantization(self.spqcc,
self.sqcc)
self.sqcc)
self.guard_bits = (self.sqcc & 0xe0) >> 5
def __str__(self):

View file

@ -432,6 +432,42 @@ class Jp2k(Jp2kBox):
# Refresh the metadata.
self.parse()
def append(self, box):
"""Append a JP2 box to the file in-place.
Parameters
----------
box : Jp2Box
Instance of a JP2 box. Currently only XML boxes are allowed.
"""
if self._codec_format == opj2.CODEC_J2K:
msg = "Only JP2 files can currently have boxes appended to them."
raise IOError(msg)
if box.box_id != 'xml ':
raise IOError("Only XML boxes can currently be appended.")
# Check the last box. If the length field is zero, then rewrite
# the length field to reflect the true length of the box.
with open(self.filename, 'rb') as ifile:
offset = self.box[-1].offset
ifile.seek(offset)
read_buffer = ifile.read(4)
box_length, = struct.unpack('>I', read_buffer)
if box_length == 0:
# Reopen the file in write mode and rewrite the length field.
true_box_length = os.path.getsize(ifile.name) - offset
with open(self.filename, 'r+b') as ofile:
ofile.seek(offset)
write_buffer = struct.pack('>I', true_box_length)
ofile.write(write_buffer)
# Can now safely append the box.
with open(self.filename, 'ab') as ofile:
box.write(ofile)
self.parse()
def wrap(self, filename, boxes=None):
"""Write the codestream back out to file, wrapped in new JP2 jacket.
@ -600,6 +636,19 @@ class Jp2k(Jp2kBox):
"""
self._subsampling_sanity_check()
if rlevel != 0:
# Must check the specified rlevel against the maximum.
# OpenJPEG 1.3 will segfault if rlevel is too high.
codestream = self.get_codestream()
max_rlevel = codestream.segment[2].spcod[4]
if rlevel == -1:
# -1 is shorthand for the largest rlevel
rlevel = max_rlevel
if rlevel < -1 or rlevel > max_rlevel:
msg = "rlevel must be in the range [-1, {0}] for this image."
msg = msg.format(max_rlevel)
raise IOError(msg)
with ExitStack() as stack:
# Set decoding parameters.
dparameters = opj.DecompressionParametersType()
@ -1133,6 +1182,7 @@ def _unpack_colorspace(colorspace, img_array, cparams):
return colorspace
def _populate_comptparms(img_array, cparams):
"""Instantiate and populate comptparms structure.
@ -1171,6 +1221,7 @@ def _populate_comptparms(img_array, cparams):
return comptparms
def _populate_image_struct(cparams, image, imgdata):
"""Populates image struct needed for compression.
@ -1203,6 +1254,7 @@ def _populate_image_struct(cparams, image, imgdata):
return image
def _validate_compression_params(img_array, cparams):
"""Check that the compression parameters are valid.
@ -1313,4 +1365,3 @@ class LibraryNotFoundError(IOError):
"""
def __init__(self, msg):
IOError.__init__(self, msg)

View file

@ -29,6 +29,12 @@ NO_READ_BACKEND_MSG += "order to run the tests in this suite."
try:
from matplotlib.pyplot import imread
# The whole point of trying to import PIL is to determine if it's there
# or not. We won't use it directly.
# pylint: disable=F0401,W0611
import PIL
NO_READ_BACKEND = False
except ImportError:
NO_READ_BACKEND = True

View file

@ -91,7 +91,7 @@ class TestCallbacks15(unittest.TestCase):
def test_info_callbacks_on_read(self):
"""Verify stdout when reading.
Verify that we get the expected stdio output when our internal info
callback handler is enabled.
"""

View file

@ -5,7 +5,7 @@ Test suite for codestream parsing.
# unittest doesn't work well with R0904.
# pylint: disable=R0904
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
# pylint: disable=E1101
# unittest2 is python2.6 only (pylint/python-2.7)

View file

@ -4,7 +4,7 @@ OPENJP2 may be present in some form or other.
# unittest doesn't work well with R0904.
# pylint: disable=R0904
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
# tempfile.TemporaryDirectory, unittest.assertWarns introduced in 3.2
# pylint: disable=E1101
# unittest.mock only in Python 3.3 (python2.7/pylint import issue)
@ -90,5 +90,48 @@ class TestSuite(unittest.TestCase):
with self.assertWarns(UserWarning):
imp.reload(glymur.lib.openjp2)
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and
glymur.lib.openjpeg.OPENJPEG is None,
"Missing openjp2 library.")
class TestConfig(unittest.TestCase):
"""Test suite for reading without proper library in place."""
def setUp(self):
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
def tearDown(self):
pass
def test_read_without_library(self):
"""Don't have either openjp2 or openjpeg libraries? Must error out.
"""
with patch('glymur.lib.openjp2.OPENJP2', new=None):
with patch('glymur.lib.openjpeg.OPENJPEG', new=None):
with self.assertRaises(glymur.jp2k.LibraryNotFoundError):
glymur.Jp2k(self.jp2file).read()
def test_read_bands_without_library(self):
"""Don't have openjp2 library? Must error out.
"""
with patch('glymur.lib.openjp2.OPENJP2', new=None):
with patch('glymur.lib.openjpeg.OPENJPEG', new=None):
with self.assertRaises(glymur.jp2k.LibraryNotFoundError):
glymur.Jp2k(self.jp2file).read_bands()
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_write_without_library(self):
"""Don't have openjp2 library? Must error out.
"""
data = glymur.Jp2k(self.j2kfile).read()
with patch('glymur.lib.openjp2.OPENJP2', new=None):
with patch('glymur.lib.openjpeg.OPENJPEG', new=None):
with self.assertRaises(glymur.jp2k.LibraryNotFoundError):
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
ofile = Jp2k(tfile.name, 'wb')
ofile.write(data)
if __name__ == "__main__":
unittest.main()

View file

@ -18,8 +18,11 @@ Test suite specifically targeting JP2 box layout.
import doctest
import os
import shutil
import struct
import sys
import tempfile
import uuid
import xml.etree.cElementTree as ET
if sys.hexversion < 0x02070000:
@ -502,6 +505,100 @@ class TestColourSpecificationBox(unittest.TestCase):
approximation=approx)
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
"Missing openjp2 library.")
class TestAppend(unittest.TestCase):
"""Tests for append method."""
def setUp(self):
self.j2kfile = glymur.data.goodstuff()
self.jp2file = glymur.data.nemo()
def tearDown(self):
pass
def test_append_xml(self):
"""Should be able to append an XML box."""
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
shutil.copyfile(self.jp2file, tfile.name)
jp2 = Jp2k(tfile.name)
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
xmlbox = glymur.jp2box.XMLBox(xml=the_xml)
jp2.append(xmlbox)
# The sequence of box IDs should be the same as before, but with an
# xml box at the end.
box_ids = [box.box_id for box in jp2.box]
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml ']
self.assertEqual(box_ids, expected)
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
b'<data>0</data>')
def test_only_jp2_allowed_to_append(self):
"""Only JP2 files are allowed to be appended."""
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile:
shutil.copyfile(self.j2kfile, tfile.name)
jp2 = Jp2k(tfile.name)
# Make a UUID box.
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
data = b'0123456789'
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
with self.assertRaises(IOError):
jp2.append(uuidbox)
def test_length_field_is_zero(self):
"""L=0 (length field in box header) is handled.
L=0 implies that the containing box is the last box. If this is not
handled properly, the appended box is never seen.
"""
baseline_jp2 = Jp2k(self.jp2file)
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
with open(self.jp2file, 'rb') as ifile:
# Everything up until the jp2c box.
offset = baseline_jp2.box[-1].offset
tfile.write(ifile.read(offset))
# Write the L, T fields of the jp2c box such that L == 0
write_buffer = struct.pack('>I4s', int(0), b'jp2c')
tfile.write(write_buffer)
# Write out the rest of the codestream.
ifile.seek(offset+8)
tfile.write(ifile.read())
tfile.flush()
jp2 = Jp2k(tfile.name)
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
xmlbox = glymur.jp2box.XMLBox(xml=the_xml)
jp2.append(xmlbox)
# The sequence of box IDs should be the same as before, but with an
# xml box at the end.
box_ids = [box.box_id for box in jp2.box]
expected = ['jP ', 'ftyp', 'jp2h', 'uuid', 'uuid', 'jp2c', 'xml ']
self.assertEqual(box_ids, expected)
self.assertEqual(ET.tostring(jp2.box[-1].xml.getroot()),
b'<data>0</data>')
def test_only_xml_allowed_to_append(self):
"""Only XML boxes are allowed to be appended."""
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
shutil.copyfile(self.jp2file, tfile.name)
jp2 = Jp2k(tfile.name)
# Make a UUID box.
uuid_instance = uuid.UUID('00000000-0000-0000-0000-000000000000')
data = b'0123456789'
uuidbox = glymur.jp2box.UUIDBox(uuid_instance, data)
with self.assertRaises(IOError):
jp2.append(uuidbox)
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
"Missing openjp2 library.")
class TestWrap(unittest.TestCase):
@ -703,7 +800,7 @@ class TestJp2Boxes(unittest.TestCase):
def test_default_ihdr(self):
"""Should be able to instantiate an image header box."""
ihdr = glymur.jp2box.ImageHeaderBox(height=512, width=256,
num_components=3)
num_components=3)
self.assertEqual(ihdr.height, 512)
self.assertEqual(ihdr.width, 256)
self.assertEqual(ihdr.num_components, 3)
@ -715,7 +812,7 @@ class TestJp2Boxes(unittest.TestCase):
"""Should be able to set jp2h boxes."""
box = JP2HeaderBox()
box.box = [ImageHeaderBox(height=512, width=256),
ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
self.assertTrue(True)
def test_default_ccodestreambox(self):

View file

@ -38,6 +38,7 @@ import glymur
from glymur import Jp2k
from .fixtures import OPENJP2_IS_V2_OFFICIAL
from .fixtures import OPENJPEG_VERSION
try:
DATA_ROOT = os.environ['OPJ_DATA_ROOT']
@ -65,48 +66,6 @@ def load_tests(loader, tests, ignore):
return tests
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None and
glymur.lib.openjpeg.OPENJPEG is None,
"Missing openjp2 library.")
class TestConfig(unittest.TestCase):
"""Test suite for reading without proper library in place."""
def setUp(self):
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
def tearDown(self):
pass
def test_read_without_library(self):
"""Don't have either openjp2 or openjpeg libraries? Must error out.
"""
with patch('glymur.lib.openjp2.OPENJP2', new=None):
with patch('glymur.lib.openjpeg.OPENJPEG', new=None):
with self.assertRaises(glymur.jp2k.LibraryNotFoundError):
glymur.Jp2k(self.jp2file).read()
def test_read_bands_without_library(self):
"""Don't have openjp2 library? Must error out.
"""
with patch('glymur.lib.openjp2.OPENJP2', new=None):
with patch('glymur.lib.openjpeg.OPENJPEG', new=None):
with self.assertRaises(glymur.jp2k.LibraryNotFoundError):
glymur.Jp2k(self.jp2file).read_bands()
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_write_without_library(self):
"""Don't have openjp2 library? Must error out.
"""
data = glymur.Jp2k(self.j2kfile).read()
with patch('glymur.lib.openjp2.OPENJP2', new=None):
with patch('glymur.lib.openjpeg.OPENJPEG', new=None):
with self.assertRaises(glymur.jp2k.LibraryNotFoundError):
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
ofile = Jp2k(tfile.name, 'wb')
ofile.write(data)
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
"Missing openjp2 library.")
@ -333,13 +292,10 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(jp2k.box[2].box[1].colorspace, glymur.core.SRGB)
self.assertIsNone(jp2k.box[2].box[1].icc_profile)
@unittest.skipIf(DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_j2k_box(self):
"""A J2K/J2C file must not have any boxes."""
# Verify that a J2K file has no boxes.
filename = os.path.join(DATA_ROOT, 'input/conformance/p0_01.j2k')
jp2k = Jp2k(filename)
jp2k = Jp2k(self.j2kfile)
self.assertEqual(len(jp2k.box), 0)
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
@ -412,6 +368,18 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(new_jp2.box[j].length,
baseline_jp2.box[j].length)
def test_basic_jp2(self):
"""Just a very basic test that reading a JP2 file does not error out.
"""
j2k = Jp2k(self.jp2file)
j2k.read(rlevel=1)
def test_basic_j2k(self):
"""Just a very basic test that reading a J2K file does not error out.
"""
j2k = Jp2k(self.j2kfile)
j2k.read()
@unittest.skipIf(DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_read_differing_subsamples(self):
@ -425,14 +393,11 @@ class TestJp2k(unittest.TestCase):
with self.assertRaises(RuntimeError):
j.read()
@unittest.skipIf(DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_empty_box_with_j2k(self):
"""Verify that the list of boxes in a J2C/J2K file is present, but
empty.
"""
filename = os.path.join(DATA_ROOT, 'input/conformance/p0_05.j2k')
j = Jp2k(filename)
j = Jp2k(self.j2kfile)
self.assertEqual(j.box, [])
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
@ -820,6 +785,14 @@ class TestJp2k15(unittest.TestCase):
def tearDown(self):
pass
def test_rlevel_max(self):
"""Verify that rlevel=-1 gets us the lowest resolution image"""
j = Jp2k(self.j2kfile)
thumbnail2 = j.read(rlevel=5)
thumbnail1 = j.read(rlevel=-1)
np.testing.assert_array_equal(thumbnail1, thumbnail2)
self.assertEqual(thumbnail1.shape, (25, 15, 3))
def test_area(self):
"""Area option not allowed for 1.5.1.
"""
@ -841,6 +814,215 @@ class TestJp2k15(unittest.TestCase):
with self.assertRaises(TypeError):
j2k.read(layer=1)
def test_rlevel_too_high(self):
"""Should error out appropriately if reduce level too high"""
j = Jp2k(self.jp2file)
with self.assertRaises(IOError):
j.read(rlevel=6)
def test_not_jpeg2000(self):
"""Should error out appropriately if not given a JPEG 2000 file."""
filename = pkg_resources.resource_filename(glymur.__name__, "jp2k.py")
with self.assertRaises(IOError):
Jp2k(filename)
def test_file_not_present(self):
"""Should error out if reading from a file that does not exist"""
# Verify that we error out appropriately if not given an existing file
# at all.
with self.assertRaises(OSError):
filename = 'this file does not actually exist on the file system.'
Jp2k(filename)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_write_with_jp2_in_caps(self):
"""should be able to write with JP2 suffix."""
j2k = Jp2k(self.j2kfile)
expdata = j2k.read()
with tempfile.NamedTemporaryFile(suffix='.JP2') as tfile:
ofile = Jp2k(tfile.name, 'wb')
ofile.write(expdata)
actdata = ofile.read()
np.testing.assert_array_equal(actdata, expdata)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_write_srgb_without_mct(self):
"""should be able to write RGB without specifying mct"""
j2k = Jp2k(self.j2kfile)
expdata = j2k.read()
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
ofile = Jp2k(tfile.name, 'wb')
ofile.write(expdata, mct=False)
actdata = ofile.read()
np.testing.assert_array_equal(actdata, expdata)
codestream = ofile.get_codestream()
self.assertEqual(codestream.segment[2].spcod[3], 0) # no mct
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_write_grayscale_with_mct(self):
"""MCT usage makes no sense for grayscale images."""
j2k = Jp2k(self.j2kfile)
expdata = j2k.read()
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
ofile = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
ofile.write(expdata[:, :, 0], mct=True)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_write_cprl(self):
"""Must be able to write a CPRL progression order file"""
# Issue 17
j = Jp2k(self.jp2file)
expdata = j.read(rlevel=1)
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
ofile = Jp2k(tfile.name, 'wb')
ofile.write(expdata, prog='CPRL')
actdata = ofile.read()
np.testing.assert_array_equal(actdata, expdata)
codestream = ofile.get_codestream()
self.assertEqual(codestream.segment[2].spcod[0], glymur.core.CPRL)
def test_jp2_boxes(self):
"""Verify the boxes of a JP2 file. Basic jp2 test."""
jp2k = Jp2k(self.jp2file)
# top-level boxes
self.assertEqual(len(jp2k.box), 6)
self.assertEqual(jp2k.box[0].box_id, 'jP ')
self.assertEqual(jp2k.box[0].offset, 0)
self.assertEqual(jp2k.box[0].length, 12)
self.assertEqual(jp2k.box[0].longname, 'JPEG 2000 Signature')
self.assertEqual(jp2k.box[1].box_id, 'ftyp')
self.assertEqual(jp2k.box[1].offset, 12)
self.assertEqual(jp2k.box[1].length, 20)
self.assertEqual(jp2k.box[1].longname, 'File Type')
self.assertEqual(jp2k.box[2].box_id, 'jp2h')
self.assertEqual(jp2k.box[2].offset, 32)
self.assertEqual(jp2k.box[2].length, 45)
self.assertEqual(jp2k.box[2].longname, 'JP2 Header')
self.assertEqual(jp2k.box[3].box_id, 'uuid')
self.assertEqual(jp2k.box[3].offset, 77)
self.assertEqual(jp2k.box[3].length, 638)
self.assertEqual(jp2k.box[4].box_id, 'uuid')
self.assertEqual(jp2k.box[4].offset, 715)
self.assertEqual(jp2k.box[4].length, 2412)
self.assertEqual(jp2k.box[5].box_id, 'jp2c')
self.assertEqual(jp2k.box[5].offset, 3127)
self.assertEqual(jp2k.box[5].length, 1132296)
# jp2h super box
self.assertEqual(len(jp2k.box[2].box), 2)
self.assertEqual(jp2k.box[2].box[0].box_id, 'ihdr')
self.assertEqual(jp2k.box[2].box[0].offset, 40)
self.assertEqual(jp2k.box[2].box[0].length, 22)
self.assertEqual(jp2k.box[2].box[0].longname, 'Image Header')
self.assertEqual(jp2k.box[2].box[0].height, 1456)
self.assertEqual(jp2k.box[2].box[0].width, 2592)
self.assertEqual(jp2k.box[2].box[0].num_components, 3)
self.assertEqual(jp2k.box[2].box[0].bits_per_component, 8)
self.assertEqual(jp2k.box[2].box[0].signed, False)
self.assertEqual(jp2k.box[2].box[0].compression, 7)
self.assertEqual(jp2k.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2k.box[2].box[0].ip_provided, False)
self.assertEqual(jp2k.box[2].box[1].box_id, 'colr')
self.assertEqual(jp2k.box[2].box[1].offset, 62)
self.assertEqual(jp2k.box[2].box[1].length, 15)
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].colorspace, glymur.core.SRGB)
self.assertIsNone(jp2k.box[2].box[1].icc_profile)
def test_j2k_box(self):
"""A J2K/J2C file must not have any boxes."""
# Verify that a J2K file has no boxes.
jp2k = Jp2k(self.j2kfile)
self.assertEqual(len(jp2k.box), 0)
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_64bit_xl_field(self):
"""XL field should be supported"""
# Verify that boxes with the XL field are properly read.
# Don't have such a file on hand, so we create one. Copy our example
# file, but making the codestream have a 64-bit XL field.
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
with open(self.jp2file, 'rb') as ifile:
# Everything up until the jp2c box.
write_buffer = ifile.read(3127)
tfile.write(write_buffer)
# The L field must be 1 in order to signal the presence of the
# XL field. The actual length of the jp2c box increased by 8
# (8 bytes for the XL field).
length = 1
typ = b'jp2c'
xlen = 1133427 + 8
write_buffer = struct.pack('>I4sQ', int(length), typ, xlen)
tfile.write(write_buffer)
# Get the rest of the input file (minus the 8 bytes for L and
# T.
ifile.seek(8, 1)
write_buffer = ifile.read()
tfile.write(write_buffer)
tfile.flush()
jp2k = Jp2k(tfile.name)
self.assertEqual(jp2k.box[5].box_id, 'jp2c')
self.assertEqual(jp2k.box[5].offset, 3127)
self.assertEqual(jp2k.box[5].length, 1133427 + 8)
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_length_field_is_zero(self):
"""L=0 (length field in box header) is allowed"""
# Verify that boxes with the L field as zero are correctly read.
# This should only happen in the last box of a JPEG 2000 file.
# Our example image has its last box at byte 588458.
baseline_jp2 = Jp2k(self.jp2file)
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
with open(self.jp2file, 'rb') as ifile:
# Everything up until the jp2c box.
write_buffer = ifile.read(588458)
tfile.write(write_buffer)
length = 0
typ = b'uuid'
write_buffer = struct.pack('>I4s', int(length), typ)
tfile.write(write_buffer)
# Get the rest of the input file (minus the 8 bytes for L and
# T.
ifile.seek(8, 1)
write_buffer = ifile.read()
tfile.write(write_buffer)
tfile.flush()
new_jp2 = Jp2k(tfile.name)
# The top level boxes in each file should match.
for j in range(len(baseline_jp2.box)):
self.assertEqual(new_jp2.box[j].box_id,
baseline_jp2.box[j].box_id)
self.assertEqual(new_jp2.box[j].offset,
baseline_jp2.box[j].offset)
self.assertEqual(new_jp2.box[j].length,
baseline_jp2.box[j].length)
def test_basic_jp2(self):
"""This test is only useful when openjp2 is not available
and OPJ_DATA_ROOT is not set. We need at least one
@ -857,6 +1039,393 @@ class TestJp2k15(unittest.TestCase):
j2k = Jp2k(self.j2kfile)
j2k.read()
@unittest.skipIf(DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_read_differing_subsamples(self):
"""should error out with read used on differently subsampled images"""
# Verify that we error out appropriately if we use the read method
# on an image with differing subsamples
#
# Issue 86.
filename = os.path.join(DATA_ROOT, 'input/conformance/p0_05.j2k')
j = Jp2k(filename)
with self.assertRaises(RuntimeError):
j.read()
def test_empty_box_with_j2k(self):
"""Verify that the list of boxes in a J2C/J2K file is present, but
empty.
"""
j = Jp2k(self.j2kfile)
self.assertEqual(j.box, [])
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_cblkh_different_than_width(self):
"""Verify that we can set a code block size where height does not equal
width.
"""
data = np.zeros((128, 128), dtype=np.uint8)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
# The code block dimensions are given as rows x columns.
j.write(data, cbsize=(16, 32))
codestream = j.get_codestream()
# Code block size is reported as XY in the codestream.
self.assertEqual(tuple(codestream.segment[2].spcod[5:7]), (3, 2))
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_too_many_dimensions(self):
"""OpenJP2 only allows 2D or 3D images."""
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
data = np.zeros((128, 128, 2, 2), dtype=np.uint8)
j.write(data)
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_unrecognized_jp2_clrspace(self):
"""We only allow RGB and GRAYSCALE. Should error out with others"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
data = np.zeros((128, 128, 3), dtype=np.uint8)
j.write(data, colorspace='cmyk')
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_2d_rgb(self):
"""RGB must have at least 3 components."""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
data = np.zeros((128, 128, 2), dtype=np.uint8)
j.write(data, colorspace='rgb')
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_colorspace_with_j2k(self):
"""Specifying a colorspace with J2K does not make sense"""
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
data = np.zeros((128, 128, 3), dtype=np.uint8)
j.write(data, colorspace='rgb')
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_specify_rgb(self):
"""specify RGB explicitly"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
data = np.zeros((128, 128, 3), dtype=np.uint8)
j.write(data, colorspace='rgb')
self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_specify_gray(self):
"""test gray explicitly specified (that's GRAY, not GREY)"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
data = np.zeros((128, 128), dtype=np.uint8)
j.write(data, colorspace='gray')
self.assertEqual(j.box[2].box[1].colorspace,
glymur.core.GREYSCALE)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_specify_grey(self):
"""test grey explicitly specified"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
data = np.zeros((128, 128), dtype=np.uint8)
j.write(data, colorspace='grey')
self.assertEqual(j.box[2].box[1].colorspace,
glymur.core.GREYSCALE)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_grey_with_extra_component(self):
"""version 2.0 cannot write gray + extra"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
data = np.zeros((128, 128, 2), dtype=np.uint8)
j.write(data)
self.assertEqual(j.box[2].box[0].height, 128)
self.assertEqual(j.box[2].box[0].width, 128)
self.assertEqual(j.box[2].box[0].num_components, 2)
self.assertEqual(j.box[2].box[1].colorspace,
glymur.core.GREYSCALE)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_grey_with_two_extra_comps(self):
"""should be able to write gray + two extra components"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
data = np.zeros((128, 128, 3), dtype=np.uint8)
j.write(data, colorspace='gray')
self.assertEqual(j.box[2].box[0].height, 128)
self.assertEqual(j.box[2].box[0].width, 128)
self.assertEqual(j.box[2].box[0].num_components, 3)
self.assertEqual(j.box[2].box[1].colorspace,
glymur.core.GREYSCALE)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_rgb_with_extra_component(self):
"""v2.0+ should be able to write extra components"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
data = np.zeros((128, 128, 4), dtype=np.uint8)
j.write(data)
self.assertEqual(j.box[2].box[0].height, 128)
self.assertEqual(j.box[2].box[0].width, 128)
self.assertEqual(j.box[2].box[0].num_components, 4)
self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB)
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_extra_components_on_v2(self):
"""must error out in 1.x with extra components."""
# Extra components seems to require 2.0+. Verify that we error out.
with self.assertRaises(IOError):
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
data = np.zeros((128, 128, 4), dtype=np.uint8)
j.write(data)
@unittest.skip("Writing requires openjp2 library at the moment.")
def test_specify_ycc(self):
"""Should reject YCC"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
data = np.zeros((128, 128, 3), dtype=np.uint8)
j.write(data, colorspace='ycc')
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_uinf_ulst_url_boxes(self):
"""Verify that we can read UINF, ULST, and URL boxes"""
# Verify that we can read UINF, ULST, and URL boxes. I don't have
# easy access to such a file, and there's no such file in the
# openjpeg repository, so I'll fake one.
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
with open(self.jp2file, 'rb') as ifile:
# Everything up until the jp2c box.
write_buffer = ifile.read(77)
tfile.write(write_buffer)
# Write the UINF superbox
# Length = 50, id is uinf.
write_buffer = struct.pack('>I4s', int(50), b'uinf')
tfile.write(write_buffer)
# Write the ULST box.
# Length is 26, 1 UUID, hard code that UUID as zeros.
write_buffer = struct.pack('>I4sHIIII', int(26), b'ulst',
int(1), int(0), int(0), int(0),
int(0))
tfile.write(write_buffer)
# Write the URL box.
# Length is 16, version is one byte, flag is 3 bytes, url
# is the rest.
write_buffer = struct.pack('>I4sBBBB',
int(16), b'url ',
int(0), int(0), int(0), int(0))
tfile.write(write_buffer)
write_buffer = struct.pack('>ssss', b'a', b'b', b'c', b'd')
tfile.write(write_buffer)
# Get the rest of the input file.
write_buffer = ifile.read()
tfile.write(write_buffer)
tfile.flush()
jp2k = Jp2k(tfile.name)
self.assertEqual(jp2k.box[3].box_id, 'uinf')
self.assertEqual(jp2k.box[3].offset, 77)
self.assertEqual(jp2k.box[3].length, 50)
self.assertEqual(jp2k.box[3].box[0].box_id, 'ulst')
self.assertEqual(jp2k.box[3].box[0].offset, 85)
self.assertEqual(jp2k.box[3].box[0].length, 26)
ulst = []
ulst.append(uuid.UUID('00000000-0000-0000-0000-000000000000'))
self.assertEqual(jp2k.box[3].box[0].ulst, ulst)
self.assertEqual(jp2k.box[3].box[1].box_id, 'url ')
self.assertEqual(jp2k.box[3].box[1].offset, 111)
self.assertEqual(jp2k.box[3].box[1].length, 16)
self.assertEqual(jp2k.box[3].box[1].version, 0)
self.assertEqual(jp2k.box[3].box[1].flag, (0, 0, 0))
self.assertEqual(jp2k.box[3].box[1].url, 'abcd')
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_xml_with_trailing_nulls(self):
"""ElementTree doesn't like trailing null chars after valid XML text"""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
with open(self.jp2file, 'rb') as ifile:
# Everything up until the jp2c box.
write_buffer = ifile.read(77)
tfile.write(write_buffer)
# Write the xml box
# Length = 36, id is 'xml '.
write_buffer = struct.pack('>I4s', int(36), b'xml ')
tfile.write(write_buffer)
write_buffer = '<test>this is a test</test>' + chr(0)
write_buffer = write_buffer.encode()
tfile.write(write_buffer)
# Get the rest of the input file.
write_buffer = ifile.read()
tfile.write(write_buffer)
tfile.flush()
jp2k = Jp2k(tfile.name)
self.assertEqual(jp2k.box[3].box_id, 'xml ')
self.assertEqual(jp2k.box[3].offset, 77)
self.assertEqual(jp2k.box[3].length, 36)
self.assertEqual(ET.tostring(jp2k.box[3].xml.getroot()),
b'<test>this is a test</test>')
@unittest.skip("Writing requires openjp2 library at the moment.")
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_asoc_label_box(self):
"""Test asoc and label box"""
# Construct a fake file with an asoc and a label box, as
# OpenJPEG doesn't have such a file.
data = Jp2k(self.jp2file).read(rlevel=1)
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
j = Jp2k(tfile.name, 'wb')
j.write(data)
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile2:
# Offset of the codestream is where we start.
read_buffer = tfile.read(77)
tfile2.write(read_buffer)
# read the rest of the file, it's the codestream.
codestream = tfile.read()
# Write the asoc superbox.
# Length = 36, id is 'asoc'.
write_buffer = struct.pack('>I4s', int(56), b'asoc')
tfile2.write(write_buffer)
# Write the contained label box
write_buffer = struct.pack('>I4s', int(13), b'lbl ')
tfile2.write(write_buffer)
tfile2.write('label'.encode())
# Write the xml box
# Length = 36, id is 'xml '.
write_buffer = struct.pack('>I4s', int(35), b'xml ')
tfile2.write(write_buffer)
write_buffer = '<test>this is a test</test>'
write_buffer = write_buffer.encode()
tfile2.write(write_buffer)
# Now append the codestream.
tfile2.write(codestream)
tfile2.flush()
jasoc = Jp2k(tfile2.name)
self.assertEqual(jasoc.box[3].box_id, 'asoc')
self.assertEqual(jasoc.box[3].box[0].box_id, 'lbl ')
self.assertEqual(jasoc.box[3].box[0].label, 'label')
self.assertEqual(jasoc.box[3].box[1].box_id, 'xml ')
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
@unittest.skipIf(re.match('1\.[345]\.\d', OPENJPEG_VERSION) is not None,
"Segfault on official v1.x series.")
def test_openjpeg_library_message(self):
"""Verify the error message produced by the openjpeg library"""
# This will confirm that the error callback mechanism is working.
with open(self.jp2file, 'rb') as fptr:
data = fptr.read()
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
# Codestream starts at byte 3127. SIZ marker at 3137.
# COD marker at 3186. Subsampling at 3180.
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[3180:3182])
tfile.write(b'\x00')
tfile.write(data[3184:3186])
tfile.write(b'\x00')
tfile.write(data[3186:])
tfile.flush()
with warnings.catch_warnings():
warnings.simplefilter("ignore")
j = Jp2k(tfile.name)
regexp = re.compile(r'''OpenJPEG\slibrary\serror:\s+
Invalid\svalues\sfor\scomp\s=\s0\s+
:\sdx=1\sdy=0''', re.VERBOSE)
if sys.hexversion < 0x03020000:
with self.assertRaisesRegexp((IOError, OSError), regexp):
j.read(rlevel=1)
else:
with self.assertRaisesRegex((IOError, OSError), regexp):
j.read(rlevel=1)
def test_xmp_attribute(self):
"""Verify the XMP packet in the shipping example file can be read."""
j = Jp2k(self.jp2file)
xmp = j.box[4].data
ns0 = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}'
ns1 = '{http://ns.adobe.com/xap/1.0/}'
name = '{0}RDF/{0}Description'.format(ns0)
elt = xmp.find(name)
attr_value = elt.attrib['{0}CreatorTool'.format(ns1)]
self.assertEqual(attr_value, 'glymur')
@unittest.skipIf(os.name == "nt", "NamedTemporaryFile issue on windows")
def test_unrecognized_exif_tag(self):
"""An unrecognized exif tag should be handled gracefully."""
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
shutil.copyfile(self.jp2file, tfile.name)
# The Exif UUID starts at byte 77. There are 8 bytes for the L and
# T fields, then 16 bytes for the UUID identifier, then 6 exif
# header bytes, then 8 bytes for the TIFF header, then 2 bytes
# the the Image IFD number of tags, where we finally find the first
# tag, "Make" (271). We'll corrupt it by changing it into 171,
# which does not correspond to any known Exif Image tag.
with open(tfile.name, 'r+b') as fptr:
fptr.seek(117)
write_buffer = struct.pack('<H', int(171))
fptr.write(write_buffer)
# Verify that a warning is issued, but only on python3.
# On python2, just suppress the warning.
if sys.hexversion < 0x03030000:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
j = Jp2k(tfile.name)
else:
with self.assertWarns(UserWarning):
j = Jp2k(tfile.name)
exif = j.box[3].data
# Were the tag == 271, 'Make' would be in the keys instead.
self.assertTrue(171 in exif['Image'].keys())
self.assertFalse('Make' in exif['Image'].keys())
if __name__ == "__main__":
unittest.main()

View file

@ -17,7 +17,7 @@ suite.
# unittest fools pylint with "too many public methods"
# pylint: disable=R0904
# Some tests use numpy test infrastructure, which means the tests never
# Some tests use numpy test infrastructure, which means the tests never
# reference "self", so pylint claims it should be a function. No, no, no.
# pylint: disable=R0201

View file

@ -441,7 +441,8 @@ class TestSuiteWrite(unittest.TestCase):
self.assertEqual(len(codestream.segment[2].spcod), 9)
# 18 SOP segments.
nsops = [x.nsop for x in codestream.segment if x.marker_id == 'SOP']
nsops = [x.nsop for x in codestream.segment
if x.marker_id == 'SOP']
self.assertEqual(nsops, list(range(18)))
def test_NR_ENC_Bretagne2_ppm_7_encode(self):

View file

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
import sys
kwargs = {'name': 'Glymur',
'version': '0.3.1',
'version': '0.4.1',
'description': 'Tools for accessing JPEG2000 files',
'long_description': open('README.md').read(),
'author': 'John Evans',