Merge branch 'devel' into issue179

This commit is contained in:
John Evans 2014-03-13 07:02:31 -04:00
commit 0ad00be004
16 changed files with 129 additions and 248 deletions

View file

@ -9,6 +9,7 @@ ChangeLog
* Added lxml requirement.
* added set_printoptions, get_printoptions function
* dropped support for Python 2.6, added support for Python 3.4
* dropped support for OpenJPEG versions 1.3 and 1.4
* dropped windows support (it might work, it might not, I don't much care)
* added write support for JP2 UUID, dataEntryURL, palette, and component mapping boxes
* added read/write support for JPX free, number list, and data reference boxes

View file

@ -13,7 +13,7 @@ both read and write JPEG 2000 files, but you may wish to install version 2.0
or the 2.0+ version from OpenJPEG's development trunk for better performance.
If you do that, you should compile it as a shared library (named *openjp2*
instead of *openjpeg*) from the developmental source that you can retrieve
via subversion. As of this time of writing, svn revision r2651 works.
via subversion. As of this time of writing, svn revision r2691 works.
You should also download the test data for the purpose of configuring
and running OpenJPEG's test suite, check their instructions for all this.
You should set the **OPJ_DATA_ROOT** environment variable for the purpose

View file

@ -14,17 +14,7 @@ XMP UUIDs. There is some very limited support for reading JPX metadata.
Glymur 0.6 works on Python versions 2.7, 3.3 and 3.4. If you have Python 2.6,
you should use the 0.5 series of Glymur.
OpenJPEG Installation
=====================
Glymur will read JPEG 2000 images with versions 1.3, 1.4, 1.5, 2.0,
and the trunk/development version of OpenJPEG. Writing images is
only supported with the 1.5 or better, however, and the trunk/development
version of OpenJPEG is strongly recommended. For more information about
OpenJPEG, please consult http://www.openjpeg.org.
If you use MacPorts or if you have a sufficiently recent version of
Linux, your package manager should already provide you with a version of
OpenJPEG 1.X which glymur can already use.
For more information about OpenJPEG, please consult http://www.openjpeg.org.
Glymur Installation
===================

View file

@ -673,6 +673,18 @@ class Codestream(object):
msg = msg.format(j, subsampling[0], subsampling[1])
warnings.warn(msg)
try:
num_tiles_x = (xysiz[0] - xyosiz[0]) / (xytsiz[0] - xytosiz[0])
num_tiles_y = (xysiz[1] - xyosiz[1]) / (xytsiz[1] - xytosiz[1])
except ZeroDivisionError as err:
warnings.warn("Invalid tile dimensions.")
else:
numtiles = math.ceil(num_tiles_x) * math.ceil(num_tiles_y)
if numtiles > 65535:
msg = "Invalid number of tiles ({0}).".format(numtiles)
warnings.warn(msg)
kwargs = {'rsiz': rsiz,
'xysiz': xysiz,
'xyosiz': xyosiz,
@ -1514,14 +1526,6 @@ class SIZsegment(Segment):
lst.append(bitdepth - 1)
self.ssiz = tuple(lst)
num_tiles_x = (self.xsiz - self.xosiz) / (self.xtsiz - self.xtosiz)
num_tiles_y = (self.ysiz - self.yosiz) / (self.ytsiz - self.ytosiz)
numtiles = math.ceil(num_tiles_x) * math.ceil(num_tiles_y)
if numtiles > 65535:
msg = "Invalid number of tiles ({0}).".format(numtiles)
warnings.warn(msg)
def __repr__(self):
msg = "glymur.codestream.SIZsegment(rsiz={rsiz}, xysiz={xysiz}, "
msg += "xyosiz={xyosiz}, xytsiz={xytsiz}, xytosiz={xytosiz}, "

View file

@ -359,13 +359,16 @@ class ColourSpecificationBox(Jp2kBox):
else:
# 2.7 has trouble pretty-printing ordered dicts so we just have
# to print as a regular dict in this case.
if sys.hexversion < 0x03000000:
icc_profile = dict(self.icc_profile)
if self.icc_profile is None:
msg += '\n ICC Profile: None'
else:
icc_profile = self.icc_profile
dispvalue = pprint.pformat(icc_profile)
lines = [' ' * 8 + y for y in dispvalue.split('\n')]
msg += '\n ICC Profile:\n{0}'.format('\n'.join(lines))
if sys.hexversion < 0x03000000:
icc_profile = dict(self.icc_profile)
else:
icc_profile = self.icc_profile
dispvalue = pprint.pformat(icc_profile)
lines = [' ' * 8 + y for y in dispvalue.split('\n')]
msg += '\n ICC Profile:\n{0}'.format('\n'.join(lines))
return msg
@ -829,7 +832,7 @@ class ComponentMappingBox(Jp2kBox):
msg = msg.format(self.component_index[k],
self.palette_index[k])
else:
msg += '\n Component %d ==> %d'
msg += '\n Component {0} ==> {1}'
msg = msg.format(self.component_index[k], k)
return msg
@ -2628,19 +2631,22 @@ class XMLBox(Jp2kBox):
read_buffer = fptr.read(num_bytes)
try:
text = read_buffer.decode('utf-8')
except UnicodeDecodeError as ude:
except UnicodeDecodeError as err:
# Possibly bad string of bytes to begin with.
# Try to search for <?xml and go from there.
decl_start = read_buffer.find(b'<?xml')
if decl_start > -1:
text = read_buffer[decl_start:].decode('utf-8')
else:
raise
if decl_start <= -1:
msg = 'A problem was encountered while parsing an XML box:'
msg += '\n\n\t"{0}"\n\nNo XML was retrieved.'
warnings.warn(msg.format(str(err)))
return XMLBox(xml=None, length=length, offset=offset)
text = read_buffer[decl_start:].decode('utf-8')
# Let the user know that the XML box was problematic.
msg = 'A UnicodeDecodeError was encountered parsing an XML box at '
msg += 'byte position {0} ({1}), but the XML was still recovered.'
msg = msg.format(offset, ude.reason)
msg = msg.format(offset, err.reason)
warnings.warn(msg, UserWarning)
# Strip out any trailing nulls, as they can foul up XML parsing.

View file

@ -774,18 +774,18 @@ class Jp2k(Jp2kBox):
"""
self._subsampling_sanity_check()
# Must check the specified rlevel against the maximum.
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)
elif 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:
try:

View file

@ -395,7 +395,7 @@ class ImageCompType(ctypes.Structure):
# alpha channel
# TODO: exclude for 2.0, 1.5
("alpha", ctypes.POINTER(ctypes.c_uint16))]
("alpha", ctypes.c_uint16)]
class ImageType(ctypes.Structure):

View file

@ -577,3 +577,13 @@ file1_xml = """XML Box (xml ) @ (36, 439)
\t\t<IMAGE_SOURCE>Professional 120 Image</IMAGE_SOURCE>
\t</GENERAL_CREATION_INFO>
</IMAGE_CREATION>"""
issue_182_cmap = """Component Mapping Box (cmap) @ (130, 24)
Component 0 ==> palette column 0
Component 1 ==> palette column 0
Component 2 ==> 2"""
issue_183_colr = """Colour Specification Box (colr) @ (62, 12)
Method: restricted ICC profile
Precedence: 0
ICC Profile: None"""

View file

@ -13,17 +13,12 @@ import struct
import sys
import tempfile
import unittest
import warnings
from glymur import Jp2k
import glymur
try:
DATA_ROOT = os.environ['OPJ_DATA_ROOT']
except KeyError:
DATA_ROOT = None
except:
raise
from .fixtures import opj_data_file, OPJ_DATA_ROOT
class TestCodestream(unittest.TestCase):
"""Test suite for unusual codestream cases."""
@ -34,7 +29,21 @@ class TestCodestream(unittest.TestCase):
def tearDown(self):
pass
@unittest.skipIf(DATA_ROOT is None,
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_tile_height_is_zero(self):
"""Zero tile height should not cause an exception."""
filename = opj_data_file('input/nonregression/2539.pdf.SIGFPE.706.1712.jp2')
if sys.hexversion < 0x03000000:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
Jp2k(filename)
else:
with self.assertWarns(UserWarning):
Jp2k(filename)
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_reserved_marker_segment(self):
@ -45,7 +54,7 @@ class TestCodestream(unittest.TestCase):
#
# Let's inject a reserved marker segment into a file that
# we know something about to make sure we can still parse it.
filename = os.path.join(DATA_ROOT, 'input/conformance/p0_01.j2k')
filename = os.path.join(OPJ_DATA_ROOT, 'input/conformance/p0_01.j2k')
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
with open(filename, 'rb') as ifile:
# Everything up until the first QCD marker.
@ -67,7 +76,7 @@ class TestCodestream(unittest.TestCase):
self.assertEqual(codestream.segment[2].length, 3)
self.assertEqual(codestream.segment[2].data, b'\x00')
@unittest.skipIf(DATA_ROOT is None,
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
@unittest.skipIf(sys.hexversion < 0x03020000,
"Uses features introduced in 3.2.")
@ -77,7 +86,7 @@ class TestCodestream(unittest.TestCase):
# Let's inject a marker segment whose marker does not appear to
# be valid. We still parse the file, but warn about the offending
# marker.
filename = os.path.join(DATA_ROOT, 'input/conformance/p0_01.j2k')
filename = os.path.join(OPJ_DATA_ROOT, 'input/conformance/p0_01.j2k')
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
with open(filename, 'rb') as ifile:
# Everything up until the first QCD marker.
@ -100,11 +109,11 @@ class TestCodestream(unittest.TestCase):
self.assertEqual(codestream.segment[2].length, 3)
self.assertEqual(codestream.segment[2].data, b'\x00')
@unittest.skipIf(DATA_ROOT is None,
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_psot_is_zero(self):
"""Psot=0 in SOT is perfectly legal. Issue #78."""
filename = os.path.join(DATA_ROOT,
filename = os.path.join(OPJ_DATA_ROOT,
'input/nonregression/123.j2c')
j = Jp2k(filename)
codestream = j.get_codestream(header_only=False)
@ -125,11 +134,11 @@ class TestCodestream(unittest.TestCase):
self.assertEqual(codestream.segment[1].ssiz, (7, 7, 7))
@unittest.skipIf(DATA_ROOT is None,
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
def test_siz_segment_ssiz_signed(self):
"""ssiz attribute to be removed in future release"""
filename = os.path.join(DATA_ROOT, 'input/conformance/p0_03.j2k')
filename = os.path.join(OPJ_DATA_ROOT, 'input/conformance/p0_03.j2k')
j = Jp2k(filename)
codestream = j.get_codestream()

View file

@ -1,148 +0,0 @@
"""
These tests deal with JPX/JP2/J2K images in the format-corpus repository.
"""
# R0904: Not too many methods in unittest.
# pylint: disable=R0904
# E1101: assertWarns introduced in python 3.2
# pylint: disable=E1101
import os
from os.path import join
import re
import sys
import tempfile
import unittest
import glymur
from glymur import Jp2k
try:
FORMAT_CORPUS_DATA_ROOT = os.environ['FORMAT_CORPUS_DATA_ROOT']
except KeyError:
FORMAT_CORPUS_DATA_ROOT = None
try:
OPJ_DATA_ROOT = os.environ['OPJ_DATA_ROOT']
except KeyError:
OPJ_DATA_ROOT = None
@unittest.skipIf(sys.hexversion < 0x03020000,
"Requires features introduced in 3.2 (assertWarns)")
class TestSuiteConformance(unittest.TestCase):
"""Test suite for conformance."""
def setUp(self):
self.j2kfile = glymur.data.goodstuff()
def tearDown(self):
pass
@unittest.skipIf(re.match(r"""1\.[0123]""",
glymur.version.openjpeg_version) is not None,
"Needs 1.3+ to catch this.")
def test_truncated_eoc(self):
"""Has one byte shaved off of EOC marker."""
with open(self.j2kfile, 'rb') as ifile:
data = ifile.read()
with tempfile.NamedTemporaryFile(suffix='.j2k') as ofile:
ofile.write(data[:-1])
ofile.flush()
j2k = Jp2k(ofile.name)
with self.assertWarns(UserWarning):
codestream = j2k.get_codestream(header_only=False)
# The last segment is truncated, so there should not be an EOC
# marker.
self.assertNotEqual(codestream.segment[-1].marker_id, 'EOC')
# The codestream is not as long as claimed.
with self.assertRaises(OSError):
j2k.read(rlevel=-1)
@unittest.skipIf(FORMAT_CORPUS_DATA_ROOT is None,
"FORMAT_CORPUS_DATA_ROOT environment variable not set")
@unittest.skipIf(sys.hexversion < 0x03020000,
"Requires features introduced in 3.2 (assertWarns)")
class TestSuiteFormatCorpus(unittest.TestCase):
"""Test suite for files in format corpus repository."""
@unittest.skipIf(re.match(r"""1\.[01234]""",
glymur.version.openjpeg_version) is not None,
"Needs 1.4+ to catch this.")
def test_balloon_trunc2(self):
"""Shortened by 5000 bytes."""
jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT,
'jp2k-test/byteCorruption/balloon_trunc2.jp2')
j2k = Jp2k(jfile)
with self.assertWarns(UserWarning):
codestream = j2k.get_codestream(header_only=False)
# The last segment is truncated, so there should not be an EOC marker.
self.assertNotEqual(codestream.segment[-1].marker_id, 'EOC')
# The codestream is not as long as claimed.
with self.assertRaises(OSError):
j2k.read(rlevel=-1)
def test_balloon_trunc3(self):
"""Most of last tile is missing."""
jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT,
'jp2k-test/byteCorruption/balloon_trunc3.jp2')
j2k = Jp2k(jfile)
with self.assertWarns(UserWarning):
codestream = j2k.get_codestream(header_only=False)
# The last segment is truncated, so there should not be an EOC marker.
self.assertNotEqual(codestream.segment[-1].marker_id, 'EOC')
# Should error out, it does not.
#with self.assertRaises(OSError):
# j2k.read(rlevel=-1)
def test_jp2_brand_any_icc_profile(self):
"""If 'jp2 ', then the method cannot be any icc profile."""
jfile = os.path.join(FORMAT_CORPUS_DATA_ROOT,
'jp2k-test', 'icc',
'balloon_eciRGBv2_ps_adobeplugin.jpf')
with self.assertWarns(UserWarning):
Jp2k(jfile)
def test_jp2_brand_iccpr_mult_colr(self):
"""Has colr box, one that conforms, one that does not."""
# Wrong 'brand' field; contains two versions of ICC profile: one
# embedded using "Any ICC" method; other embedded using "Restricted
# ICC" method, with description ("Modified eciRGB v2") and profileClass
# ("Input Device") changed relative to original profile.
jfile = join(FORMAT_CORPUS_DATA_ROOT, 'jp2k-test', 'icc',
'balloon_eciRGBv2_ps_adobeplugin_jp2compatible.jpf')
with self.assertWarns(UserWarning):
Jp2k(jfile)
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")
@unittest.skipIf(sys.hexversion < 0x03020000,
"Requires features introduced in 3.2 (assertWarns)")
class TestSuiteOpj(unittest.TestCase):
"""Test suite for files in openjpeg repository."""
def setUp(self):
pass
def tearDown(self):
pass
def test_jp2_brand_any_icc_profile(self):
"""If 'jp2 ', then the method cannot be any icc profile."""
filename = os.path.join(OPJ_DATA_ROOT,
'input/nonregression/text_GBR.jp2')
with self.assertWarns(UserWarning):
Jp2k(filename)
if __name__ == "__main__":
unittest.main()

View file

@ -39,6 +39,7 @@ from glymur.jp2box import ColourSpecificationBox, ContiguousCodestreamBox
from glymur.jp2box import FileTypeBox, ImageHeaderBox, JP2HeaderBox
from glymur.jp2box import JPEG2000SignatureBox
from .fixtures import OPJ_DATA_ROOT, opj_data_file
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
class TestXML(unittest.TestCase):
@ -93,6 +94,22 @@ class TestXML(unittest.TestCase):
def tearDown(self):
os.unlink(self.xmlfile)
def test_invalid_utf8(self):
"""Bad byte sequence that cannot be parsed."""
filename = opj_data_file(os.path.join('input',
'nonregression',
'26ccf3651020967f7778238ef5af08af.SIGFPE.d25.527.jp2'))
if sys.hexversion < 0x03000000:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
jp2 = Jp2k(filename)
else:
with self.assertWarns(UserWarning):
jp2 = Jp2k(filename)
self.assertIsNone(jp2.box[3].box[1].box[1].xml)
def test_negative_file_and_xml(self):
"""The XML should come from only one source."""
xml_object = ET.parse(self.xmlfile)

View file

@ -403,15 +403,9 @@ class TestJp2k(unittest.TestCase):
# The file in question has multiple codestreams.
jpx = Jp2k(self.jpxfile)
data = jpx.read()
if re.match(r"""1\.[0123]""", glymur.version.openjpeg_version):
# openjpeg 1.3 doesn't apply the palette, so it's a 2D image here
self.assertEqual(data.shape, (1024, 1024))
else:
self.assertEqual(data.shape, (1024, 1024, 3))
self.assertEqual(data.shape, (1024, 1024, 3))
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
"Requires at least version 1.5")
class TestJp2k_write(unittest.TestCase):
"""Write tests, can be run by versions 1.5+"""

View file

@ -381,11 +381,7 @@ class TestSuite(unittest.TestCase):
jfile = opj_data_file('input/conformance/file9.jp2')
jp2k = Jp2k(jfile)
jpdata = jp2k.read()
if re.match(r"""1\.3""", glymur.version.openjpeg_version):
# Version 1.3 reads the indexed image as indices, not as RGB.
self.assertEqual(jpdata.shape, (512, 768))
else:
self.assertEqual(jpdata.shape, (512, 768, 3))
self.assertEqual(jpdata.shape, (512, 768, 3))
def test_NR_DEC_Bretagne2_j2k_1_decode(self):
jfile = opj_data_file('input/nonregression/Bretagne2.j2k')
@ -6307,11 +6303,16 @@ class TestSuiteDump(unittest.TestCase):
[8, 9, 9, 10, 9, 9, 10, 9, 9, 10, 9, 9, 10, 9, 9, 10])
def test_NR_text_GBR_dump(self):
# brand is 'jp2 ', but has any icc profile.
# Verify the warning on python3, but ignore it otherwise.
jfile = opj_data_file('input/nonregression/text_GBR.jp2')
with warnings.catch_warnings():
# brand is 'jp2 ', but has any icc profile.
warnings.simplefilter("ignore")
jp2 = Jp2k(jfile)
if sys.hexversion > 0x03030000:
with self.assertWarns(UserWarning):
jp2 = Jp2k(jfile)
else:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
jp2 = Jp2k(jfile)
ids = [box.box_id for box in jp2.box]
lst = ['jP ', 'ftyp', 'rreq', 'jp2h',

View file

@ -23,33 +23,6 @@ from glymur import Jp2k
import glymur
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
"Functionality not implemented for 1.3, 1.4")
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_OPJ_DATA_ROOT environment variable not set")
class TestSuiteNegative2pointzero(unittest.TestCase):
"""Feature set not supported for versions less than 2.0"""
def setUp(self):
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
def tearDown(self):
pass
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
def test_cinema_mode(self):
"""Cinema mode not supported for less than 2.0.1."""
infile = opj_data_file('input/nonregression/Bretagne1.ppm')
data = read_image(infile)
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
j = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
j.write(data, psnr=[30, 35, 40], cratios=[2, 3, 4])
@unittest.skipIf(re.match(r"""1\.[01234]""", glymur.version.openjpeg_version),
"Functionality not implemented for 1.3, 1.4")
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_OPJ_DATA_ROOT environment variable not set")
class TestSuiteNegative(unittest.TestCase):

View file

@ -249,9 +249,6 @@ class TestSuiteNegative2pointzero(unittest.TestCase):
@unittest.skipIf(os.name == "nt", "no write support on windows, period")
@unittest.skipIf(re.match(r"""1\.[01234]\.\d""",
glymur.version.openjpeg_version) is not None,
"Writing only supported with openjpeg version 1.5+.")
@unittest.skipIf(NO_READ_BACKEND, NO_READ_BACKEND_MSG)
@unittest.skipIf(OPJ_DATA_ROOT is None,
"OPJ_DATA_ROOT environment variable not set")

View file

@ -905,6 +905,33 @@ class TestPrinting(unittest.TestCase):
expected = '\n'.join(lines)
self.assertEqual(actual, expected)
def test_issue182(self):
"""Should not show the format string in output."""
# The cmap box is wildly broken, but printing was still wrong.
# Format strings like %d were showing up in the output.
filename = opj_data_file('input/nonregression/mem-b2ace68c-1381.jp2')
with warnings.catch_warnings():
# Ignore warning about bad pclr box.
warnings.simplefilter("ignore")
jp2 = Jp2k(filename)
with patch('sys.stdout', new=StringIO()) as fake_out:
print(jp2.box[3].box[3])
actual = fake_out.getvalue().strip()
self.assertEqual(actual, fixtures.issue_182_cmap)
def test_issue183(self):
filename = opj_data_file('input/nonregression/orb-blue10-lin-jp2.jp2')
with warnings.catch_warnings():
# Ignore warning about bad pclr box.
warnings.simplefilter("ignore")
jp2 = Jp2k(filename)
with patch('sys.stdout', new=StringIO()) as fake_out:
print(jp2.box[2].box[1])
actual = fake_out.getvalue().strip()
self.assertEqual(actual, fixtures.issue_183_colr)
@unittest.skipIf(sys.hexversion < 0x03000000,
"Ordered dicts not printing well in 2.7")
def test_exif_uuid(self):