Merge branch 'issue86' into devel

This commit is contained in:
John Evans 2013-07-24 19:03:17 -04:00
commit ce46cfe710
19 changed files with 513 additions and 164 deletions

View file

@ -1,3 +1,10 @@
Jul 23, 2013 - v0.2.5 Fixed inconsistency in XML handling, now all instances
are always ElementTree objects (issue82).
Jul 21, 2013 - v0.2.4 Fixed markdown bug for Fedora 17 information, fixed
out-of-date windows information (issue79). Fixed incorrect
interpretation of Psot parameter (issue78).
Jul 18, 2013 - v0.2.3 Support for Python 2.6, OpenJPEG 1.4. Incompatible Jul 18, 2013 - v0.2.3 Support for Python 2.6, OpenJPEG 1.4. Incompatible
change to ChannelDefinitionBox constructor. Added RGBA example. change to ChannelDefinitionBox constructor. Added RGBA example.

View file

@ -1,9 +1,9 @@
glymur: a Python interface for JPEG 2000 glymur: a Python interface for JPEG 2000
========================================= =========================================
**glymur** contains a Python interface to the OpenJPEG library **glymur** contains a Python interface to the OpenJPEG library which
which allows linux and mac users to read and write JPEG 2000 files. allows one to read and write JPEG 2000 files. **glymur** works on
**glymur** works on Python 2.6, 2.7 and 3.3. Python 3.3 is strongly Python 2.6, 2.7 and 3.3. Python 3.3 is strongly
recommended. recommended.
Please read the docs, https://glymur.readthedocs.org/en/latest/ Please read the docs, https://glymur.readthedocs.org/en/latest/

View file

@ -78,7 +78,7 @@ copyright = u'2013, John Evans'
# The short X.Y version. # The short X.Y version.
version = '0.1' version = '0.1'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '0.2.3' release = '0.2.5'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

View file

@ -115,7 +115,7 @@ following RPMs installed.
* numpy * numpy
* matplotlib (optional) * matplotlib (optional)
In addition, you must install contextlib2 and Pillow via pip. In addition, you must install contextlib2 and Pillow via pip. ::
$ yum install python-devel # pip needs this in order to compile Pillow $ yum install python-devel # pip needs this in order to compile Pillow
$ pip-python install Pillow --user $ pip-python install Pillow --user
@ -137,8 +137,17 @@ platforms.
Testing Testing
''''''' '''''''
If you wish to run the tests (strongly recommended :-), you can either run them There are two environment variables you may wish to set before running the
from within python as follows ... :: tests.
* **OPJ_DATA_ROOT** - points to directory for OpenJPEG test data
* **FORMAT_CORPUS_ROOT** - points to directory for format-corpus repository (see https://github.com/openplanets/format-corpus)
Setting these two environment variables is not required, as any tests using
either of them will be skipped.
In order to run the tests, you can either run them from within
python as follows ... ::
>>> import glymur >>> import glymur
>>> glymur.runtests() >>> glymur.runtests()

View file

@ -185,9 +185,8 @@ Work with XMP UUIDs?
==================== ====================
The example JP2 file shipped with glymur has an XMP UUID. :: The example JP2 file shipped with glymur has an XMP UUID. ::
>>> from glymur import Jp2k >>> import glymur
>>> file = glymur.data.nemo() >>> j = glymur.Jp2k(glymur.data.nemo())
>>> j = Jp2k(file)
>>> print(j.box[4]) >>> print(j.box[4])
UUID Box (uuid) @ (715, 2412) UUID Box (uuid) @ (715, 2412)
UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP) UUID: be7acfcb-97a9-42e8-9c71-999491e3afac (XMP)
@ -198,7 +197,7 @@ The example JP2 file shipped with glymur has an XMP UUID. ::
</rdf:RDF> </rdf:RDF>
</ns0:xmpmeta> </ns0:xmpmeta>
Since the UUID data in this case is returned as an ElementTree Element, one can Since the UUID data in this case is returned as an ElementTree instance, one can
use ElementTree to access the data. For example, to extract the use ElementTree to access the data. For example, to extract the
**CreatorTool** attribute value, the following would work:: **CreatorTool** attribute value, the following would work::

View file

@ -6,7 +6,6 @@ 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 * continue to monitor upstream changes in the openjp2 library
* investigate using CFFI or cython instead of ctypes to wrap openjp2 * investigate using CFFI or cython instead of ctypes to wrap openjp2
* investigate swapping out ElementTree for LXML
* eventually expose the openjp2 API * eventually expose the openjp2 API
* investigate JPIP * investigate JPIP

View file

@ -6,7 +6,6 @@ from .jp2k import Jp2k
from .jp2dump import jp2dump from .jp2dump import jp2dump
from . import data from . import data
from . import test
def runtests(): def runtests():

View file

@ -5,7 +5,6 @@ codestreams.
""" """
# pylint: disable=C0302,R0902,R0903,R0913 # pylint: disable=C0302,R0902,R0903,R0913
from itertools import takewhile
import math import math
import struct import struct
import sys import sys
@ -43,16 +42,6 @@ for _marker in range(0xff90, 0xff94):
_VALID_MARKERS.append(_marker) _VALID_MARKERS.append(_marker)
class InconsistentStartOfTileError(IOError):
"""To be raised if bad SOT segment encountered.
SOT segment offsets are recorded as encountered. The offsets should all be
different.
"""
def __init__(self, msg):
IOError.__init__(self, msg)
class Codestream(object): class Codestream(object):
"""Container for codestream information. """Container for codestream information.
@ -113,7 +102,19 @@ class Codestream(object):
while True: while True:
read_buffer = fptr.read(2) read_buffer = fptr.read(2)
marker_id, = struct.unpack('>H', read_buffer) try:
marker_id, = struct.unpack('>H', read_buffer)
except struct.error:
# Treat this as a warning.
msg = "Marker had length {0} instead of expected length of 2 "
msg += "bytes. Codestream parsing terminated."
warnings.warn(msg.format(len(read_buffer)))
break
if marker_id == 0xff90 and header_only:
# Start-of-tile (SOT) means that we are out of the main header
# and there is no need to go further.
break
try: try:
segment = self._process_marker_segment(fptr, marker_id) segment = self._process_marker_segment(fptr, marker_id)
@ -139,12 +140,6 @@ class Codestream(object):
fptr.seek(self._tile_offset[-1] + self._tile_length[-1]) fptr.seek(self._tile_offset[-1] + self._tile_length[-1])
if header_only:
# start-of-tile (SOT) means we are out of the main header.
# No need to go any further.
gen = takewhile(lambda s: s.marker_id != 'SOT', self.segment)
self.segment = list(gen)
def _process_marker_segment(self, fptr, marker_id): def _process_marker_segment(self, fptr, marker_id):
"""Process and return a segment from the codestream. """Process and return a segment from the codestream.
@ -210,19 +205,12 @@ class Codestream(object):
# we encounter start-of-data marker segments. # we encounter start-of-data marker segments.
segment = _parse_sot_segment(fptr) segment = _parse_sot_segment(fptr)
if segment.offset not in self._tile_offset: self._tile_offset.append(segment.offset)
self._tile_offset.append(segment.offset) if segment.psot == 0:
if segment.psot == 0: tile_part_length = self.offset + self.length - segment.offset - 2
tile_part_length = self.offset + self.length - segment.offset - 2
else:
tile_part_length = segment.psot
self._tile_length.append(tile_part_length)
else: else:
msg = "Inconsistent start-of-tile (SOT) marker segment " tile_part_length = segment.psot
msg += "encountered in tile with index {0}. " self._tile_length.append(tile_part_length)
msg += "Codestream parsing terminated."
msg = msg.format(segment.isot)
raise InconsistentStartOfTileError(msg)
elif marker_id == 0xff93: elif marker_id == 0xff93:
# start of data. Need to seek past the current tile part. # start of data. Need to seek past the current tile part.

View file

@ -520,6 +520,195 @@ class ChannelDefinitionBox(Jp2kBox):
return box return box
class CodestreamHeaderBox(Jp2kBox):
"""Container for codestream header box information.
Attributes
----------
box_id : str
4-character identifier for the box.
length : int
length of the box in bytes.
offset : int
offset of the box from the start of the file.
longname : str
more verbose description of the box.
box : list
List of boxes contained in this superbox.
"""
def __init__(self, length=0, offset=-1):
Jp2kBox.__init__(self, box_id='jpch', longname='Codestream Header')
self.length = length
self.offset = offset
self.box = []
def __str__(self):
msg = Jp2kBox.__str__(self)
for box in self.box:
boxstr = str(box)
# Add indentation.
strs = [('\n ' + x) for x in boxstr.split('\n')]
msg += ''.join(strs)
return msg
@staticmethod
def parse(fptr, offset, length):
"""Parse codestream header box.
Parameters
----------
fptr : file
Open file object.
offset : int
Start position of box in bytes.
length : int
Length of the box in bytes.
Returns
-------
AssociationBox instance
"""
box = CodestreamHeaderBox(length=length, offset=offset)
# The codestream header box is a superbox, so go ahead and parse its
# child boxes.
box.box = box.parse_superbox(fptr)
return box
class CompositingLayerHeaderBox(Jp2kBox):
"""Container for compositing layer header box information.
Attributes
----------
box_id : str
4-character identifier for the box.
length : int
length of the box in bytes.
offset : int
offset of the box from the start of the file.
longname : str
more verbose description of the box.
box : list
List of boxes contained in this superbox.
"""
def __init__(self, length=0, offset=-1):
Jp2kBox.__init__(self, box_id='jplh',
longname='Compositing Layer Header')
self.length = length
self.offset = offset
self.box = []
def __str__(self):
msg = Jp2kBox.__str__(self)
for box in self.box:
boxstr = str(box)
# Add indentation.
strs = [('\n ' + x) for x in boxstr.split('\n')]
msg += ''.join(strs)
return msg
@staticmethod
def parse(fptr, offset, length):
"""Parse compositing layer header box.
Parameters
----------
fptr : file
Open file object.
offset : int
Start position of box in bytes.
length : int
Length of the box in bytes.
Returns
-------
AssociationBox instance
"""
box = CompositingLayerHeaderBox(length=length, offset=offset)
# This box is a superbox, so go ahead and parse its # child boxes.
box.box = box.parse_superbox(fptr)
return box
class JP2HeaderBox(Jp2kBox):
"""Container for JP2 header box information.
Attributes
----------
box_id : str
4-character identifier for the box.
length : int
length of the box in bytes.
offset : int
offset of the box from the start of the file.
longname : str
more verbose description of the box.
box : list
List of boxes contained in this superbox.
"""
def __init__(self, length=0, offset=-1):
Jp2kBox.__init__(self, box_id='jp2h', longname='JP2 Header')
self.length = length
self.offset = offset
self.box = []
def __str__(self):
msg = Jp2kBox.__str__(self)
for box in self.box:
boxstr = str(box)
# Add indentation.
strs = [('\n ' + x) for x in boxstr.split('\n')]
msg += ''.join(strs)
return msg
def write(self, fptr):
"""Write a JP2 Header box to file.
"""
# Write the contained boxes, then come back and write the length.
orig_pos = fptr.tell()
fptr.write(struct.pack('>I', 0))
fptr.write('jp2h'.encode())
for box in self.box:
box.write(fptr)
end_pos = fptr.tell()
fptr.seek(orig_pos)
fptr.write(struct.pack('>I', end_pos - orig_pos))
fptr.seek(end_pos)
@staticmethod
def parse(fptr, offset, length):
"""Parse JPEG 2000 header box.
Parameters
----------
fptr : file
Open file object.
offset : int
Start position of box in bytes.
length : int
Length of the box in bytes.
Returns
-------
JP2HeaderBox instance
"""
box = JP2HeaderBox(length=length, offset=offset)
# The JP2 header box is a superbox, so go ahead and parse its child
# boxes.
box.box = box.parse_superbox(fptr)
return box
class ComponentMappingBox(Jp2kBox): class ComponentMappingBox(Jp2kBox):
"""Container for channel identification information. """Container for channel identification information.
@ -1608,7 +1797,7 @@ class XMLBox(Jp2kBox):
offset of the box from the start of the file. offset of the box from the start of the file.
longname : str longname : str
more verbose description of the box. more verbose description of the box.
xml : ElementTree.Element xml : ElementTree object
XML section. XML section.
""" """
def __init__(self, xml=None, filename=None, length=0, offset=-1): def __init__(self, xml=None, filename=None, length=0, offset=-1):
@ -1636,10 +1825,7 @@ class XMLBox(Jp2kBox):
msg = Jp2kBox.__str__(self) msg = Jp2kBox.__str__(self)
xml = self.xml xml = self.xml
if self.xml is not None: if self.xml is not None:
try: msg += _pretty_print_xml(self.xml)
msg += _pretty_print_xml(self.xml)
except TypeError:
msg += _pretty_print_xml(self.xml.getroot())
else: else:
msg += '\n {0}'.format(xml) msg += '\n {0}'.format(xml)
return msg return msg
@ -1682,7 +1868,8 @@ class XMLBox(Jp2kBox):
text = text.rstrip('\0') text = text.rstrip('\0')
try: try:
xml = ET.fromstring(text) elt = ET.fromstring(text)
xml = ET.ElementTree(elt)
except ParseError as parse_error: except ParseError as parse_error:
msg = 'A problem was encountered while parsing an XML box: "{0}"' msg = 'A problem was encountered while parsing an XML box: "{0}"'
msg = msg.format(str(parse_error)) msg = msg.format(str(parse_error))
@ -1926,13 +2113,11 @@ class UUIDBox(Jp2kBox):
# 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. # ElementTree in version 2.7 and 3.3.
if sys.hexversion < 0x03000000: if sys.hexversion < 0x03000000:
#parser = ET.XMLParser(encoding='utf-8') elt = ET.fromstring(raw_data)
#import pdb; pdb.set_trace()
#self.data = ET.fromstringlist(raw_data, parser=parser)
self.data = ET.fromstring(raw_data)
else: else:
text = raw_data.decode('utf-8') text = raw_data.decode('utf-8')
self.data = ET.fromstring(text) elt = ET.fromstring(text)
self.data = ET.ElementTree(elt)
elif the_uuid.bytes == b'JpgTiffExif->JP2': elif the_uuid.bytes == b'JpgTiffExif->JP2':
exif_obj = Exif(raw_data) exif_obj = Exif(raw_data)
ifds = OrderedDict() ifds = OrderedDict()
@ -2513,11 +2698,13 @@ _BOX_WITH_ID = {
'cdef': ChannelDefinitionBox, 'cdef': ChannelDefinitionBox,
'cmap': ComponentMappingBox, 'cmap': ComponentMappingBox,
'colr': ColourSpecificationBox, 'colr': ColourSpecificationBox,
'jP ': JPEG2000SignatureBox,
'ftyp': FileTypeBox, 'ftyp': FileTypeBox,
'ihdr': ImageHeaderBox, 'ihdr': ImageHeaderBox,
'jp2h': JP2HeaderBox, 'jP ': JPEG2000SignatureBox,
'jpch': CodestreamHeaderBox,
'jplh': CompositingLayerHeaderBox,
'jp2c': ContiguousCodestreamBox, 'jp2c': ContiguousCodestreamBox,
'jp2h': JP2HeaderBox,
'lbl ': LabelBox, 'lbl ': LabelBox,
'pclr': PaletteBox, 'pclr': PaletteBox,
'res ': ResolutionBox, 'res ': ResolutionBox,
@ -2555,8 +2742,8 @@ def _pretty_print_xml(xml, level=0):
"""Pretty print XML data. """Pretty print XML data.
""" """
xml = copy.deepcopy(xml) xml = copy.deepcopy(xml)
_indent(xml, level=level) _indent(xml.getroot(), level=level)
xmltext = ET.tostring(xml).decode('utf-8') xmltext = ET.tostring(xml.getroot()).decode('utf-8')
# Indent it a bit. # Indent it a bit.
lst = [(' ' + x) for x in xmltext.split('\n')] lst = [(' ' + x) for x in xmltext.split('\n')]

View file

@ -22,6 +22,7 @@ from .codestream import Codestream
from .core import SRGB from .core import SRGB
from .core import GREYSCALE from .core import GREYSCALE
from .core import PROGRESSION_ORDER from .core import PROGRESSION_ORDER
from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
from .jp2box import Jp2kBox from .jp2box import Jp2kBox
from .jp2box import JPEG2000SignatureBox from .jp2box import JPEG2000SignatureBox
from .jp2box import FileTypeBox from .jp2box import FileTypeBox
@ -159,6 +160,24 @@ class Jp2k(Jp2kBox):
# boxes) here. # boxes) here.
fptr.seek(0) fptr.seek(0)
self.box = self.parse_superbox(fptr) self.box = self.parse_superbox(fptr)
self._validate()
def _validate(self):
"""Validate the JPEG 2000 outermost superbox.
"""
# A jp2-branded file cannot contain an "any ICC profile
ftyp = self.box[1]
if ftyp.brand == 'jp2 ':
jp2h = [box for box in self.box if box.box_id == 'jp2h'][0]
colrs = [box for box in jp2h.box if box.box_id == 'colr']
for colr in colrs:
if colr.method not in (ENUMERATED_COLORSPACE,
RESTRICTED_ICC_PROFILE):
msg = "Color Specification box method must specify either "
msg += "an enumerated colorspace or a restricted ICC "
msg += "profile if the file type box brand is 'jp2 '."
warnings.warn(msg)
# pylint: disable-msg=W0221 # pylint: disable-msg=W0221
def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None, def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None,

View file

@ -35,9 +35,6 @@ def glymurrc_fname():
fname = os.path.join(confdir, 'glymurrc') fname = os.path.join(confdir, 'glymurrc')
if os.path.exists(fname): if os.path.exists(fname):
return fname return fname
else:
msg = "Configuration directory '{0}' does not exist.".format(fname)
warnings.warn(msg)
# didn't find a configuration file. # didn't find a configuration file.
return None return None

View file

@ -1,11 +0,0 @@
from .test_callbacks import TestCallbacks as callbacks
from .test_codestream import TestCodestream as codestream
from .test_config import TestSuite as config
from .test_jp2k import TestJp2k as jp2k
from .test_icc import TestICC as icc
from .test_printing import TestPrinting as printing
from .test_opj_suite import TestSuite as suite
from .test_opj_suite import TestSuiteDump as suitedump
from .test_opj_suite_write import TestSuiteWrite as suitew
from .test_opj_suite_neg import TestSuiteNegative as suiteneg
from .test_jp2box import TestJp2Boxes as box

View file

@ -81,40 +81,5 @@ class TestSuite(unittest.TestCase):
with self.assertWarns(UserWarning) as cw: with self.assertWarns(UserWarning) as cw:
imp.reload(glymur.lib.openjp2) imp.reload(glymur.lib.openjp2)
def test_missing_config_file_via_environ(self):
# Verify that we error out properly if the configuration file
# specified via environment variable is not found.
with tempfile.TemporaryDirectory() as tdir:
with patch.dict('os.environ', {'XDG_CONFIG_HOME': tdir}):
# Misconfigured new configuration file should
# be rejected.
with self.assertWarns(UserWarning) as cw:
imp.reload(glymur.lib.openjp2)
def test_home_dir_missing_config_dir(self):
# Verify no exception is raised if $HOME is missing .config directory.
with tempfile.TemporaryDirectory() as tdir:
with patch.dict('os.environ', {'HOME': tdir}):
# Misconfigured new configuration file should
# be rejected.
with self.assertWarns(UserWarning) as cw:
imp.reload(glymur.lib.openjp2)
def test_home_dir_missing_glymur_rc_dir(self):
# Should warn but not error if $HOME/.config but no glymurrc dir.
with tempfile.TemporaryDirectory() as tdir:
# We need the subdirectory to be specifically named as ".config"
# in order for this test to work. A specifically-named temporary
# directory does not seem to be possible, so try to symlink it.
# Supposedly the symlink gets cleaned up with tdir gets cleaned up.
with tempfile.TemporaryDirectory(suffix=".config", dir=tdir) \
as tdir_config:
os.symlink(tdir_config, os.path.join(tdir, '.config'))
with patch.dict('os.environ', {'HOME': tdir}):
# Misconfigured new configuration file should
# be rejected.
with self.assertWarns(UserWarning) as cw:
imp.reload(glymur.lib.openjp2)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -0,0 +1,129 @@
"""
These tests deal with JPX/JP2/J2K images in the format-corpus repository.
"""
#pylint: disable-all
import os
import sys
if sys.hexversion < 0x02070000:
import unittest2 as unittest
else:
import unittest
import warnings
from glymur import Jp2k
import glymur
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(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):
def setUp(self):
pass
def tearDown(self):
pass
def test_balloon_trunc1(self):
# Has one byte shaved off of EOC marker.
jfile = os.path.join(format_corpus_data_root,
'jp2k-test/byteCorruption/balloon_trunc1.jp2')
j2k = Jp2k(jfile)
with self.assertWarns(UserWarning):
c = j2k.get_codestream(header_only=False)
# The last segment is truncated, so there should not be an EOC marker.
self.assertNotEqual(c.segment[-1].marker_id, 'EOC')
# The codestream is not as long as claimed.
with self.assertRaises(OSError):
j2k.read(rlevel=-1)
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):
c = j2k.get_codestream(header_only=False)
# The last segment is truncated, so there should not be an EOC marker.
self.assertNotEqual(c.segment[-1].marker_id, 'EOC')
# 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):
c = j2k.get_codestream(header_only=False)
# The last segment is truncated, so there should not be an EOC marker.
self.assertNotEqual(c.segment[-1].marker_id, 'EOC')
# Should error out, it does not.
#with self.assertRaises(OSError):
# j2k.read(rlevel=-1)
def test_jp2_brand_vs_any_icc_profile(self):
# If 'jp2 ', then the method cannot be any icc profile.
jfile = os.path.join(format_corpus_data_root,
'jp2k-test', 'icc',
'balloon_eciRGBv2_ps_adobeplugin.jpf')
with self.assertWarns(UserWarning):
j2k = Jp2k(jfile)
def test_jp2_brand_vs_any_icc_profile_multiple_colr(self):
# Has colr box, one that conforms, one that does not.
# Wrong 'brand' field; contains two versions of ICC profile: one
# embedded using "Any ICC" method; other embedded using "Restricted
# ICC" method, with description ("Modified eciRGB v2") and profileClass
# ("Input Device") changed relative to original profile.
lst = [format_corpus_data_root, 'jp2k-test', 'icc',
'balloon_eciRGBv2_ps_adobeplugin_jp2compatible.jpf']
jfile = os.path.join(*lst)
with self.assertWarns(UserWarning):
j2k = 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):
def setUp(self):
pass
def tearDown(self):
pass
def test_jp2_brand_vs_any_icc_profile(self):
# If 'jp2 ', then the method cannot be any icc profile.
filename = os.path.join(opj_data_root,
'input/nonregression/text_GBR.jp2')
with self.assertWarns(UserWarning):
j2k = Jp2k(filename)
if __name__ == "__main__":
unittest.main()

View file

@ -19,6 +19,11 @@ from glymur.jp2box import *
from glymur.core import COLOR, OPACITY from glymur.core import COLOR, OPACITY
from glymur.core import RED, GREEN, BLUE, GREY, WHOLE_IMAGE from glymur.core import RED, GREEN, BLUE, GREY, WHOLE_IMAGE
try:
format_corpus_data_root = os.environ['FORMAT_CORPUS_DATA_ROOT']
except KeyError:
format_corpus_data_root = None
# Doc tests should be run as well. # Doc tests should be run as well.
def load_tests(loader, tests, ignore): def load_tests(loader, tests, ignore):
@ -365,7 +370,7 @@ class TestXML(unittest.TestCase):
j2k.wrap(tfile.name, boxes=boxes) j2k.wrap(tfile.name, boxes=boxes)
jp2 = Jp2k(tfile.name) jp2 = Jp2k(tfile.name)
self.assertEqual(jp2.box[3].box_id, 'xml ') self.assertEqual(jp2.box[3].box_id, 'xml ')
self.assertEqual(ET.tostring(jp2.box[3].xml), self.assertEqual(ET.tostring(jp2.box[3].xml.getroot()),
b'<data>0</data>') b'<data>0</data>')
@unittest.skipIf(os.name == "nt", @unittest.skipIf(os.name == "nt",
@ -467,7 +472,7 @@ class TestColourSpecificationBox(unittest.TestCase):
@unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None, @unittest.skipIf(glymur.lib.openjp2.OPENJP2 is None,
"Missing openjp2 library.") "Missing openjp2 library.")
class TestJp2Boxes(unittest.TestCase): class TestWrap(unittest.TestCase):
def setUp(self): def setUp(self):
self.j2kfile = glymur.data.goodstuff() self.j2kfile = glymur.data.goodstuff()
@ -476,39 +481,6 @@ class TestJp2Boxes(unittest.TestCase):
def tearDown(self): def tearDown(self):
pass pass
def test_default_JPEG2000SignatureBox(self):
# Should be able to instantiate a JPEG2000SignatureBox
b = glymur.jp2box.JPEG2000SignatureBox()
self.assertEqual(b.signature, (13, 10, 135, 10))
def test_default_FileTypeBox(self):
# Should be able to instantiate a FileTypeBox
b = glymur.jp2box.FileTypeBox()
self.assertEqual(b.brand, 'jp2 ')
self.assertEqual(b.minor_version, 0)
self.assertEqual(b.compatibility_list, ['jp2 '])
def test_default_ImageHeaderBox(self):
# Should be able to instantiate an image header box.
b = glymur.jp2box.ImageHeaderBox(height=512, width=256,
num_components=3)
self.assertEqual(b.height, 512)
self.assertEqual(b.width, 256)
self.assertEqual(b.num_components, 3)
self.assertEqual(b.bits_per_component, 8)
self.assertFalse(b.signed)
self.assertFalse(b.colorspace_unknown)
def test_default_JP2HeaderBox(self):
b1 = JP2HeaderBox()
b1.box = [ImageHeaderBox(height=512, width=256),
ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
def test_default_ContiguousCodestreamBox(self):
b = ContiguousCodestreamBox()
self.assertEqual(b.box_id, 'jp2c')
self.assertIsNone(b.main_header)
def verify_wrapped_raw(self, jp2file): def verify_wrapped_raw(self, jp2file):
# Shared method by at least two tests. # Shared method by at least two tests.
jp2 = Jp2k(jp2file) jp2 = Jp2k(jp2file)
@ -673,5 +645,75 @@ class TestJp2Boxes(unittest.TestCase):
with self.assertRaises(IOError): with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes) j2k.wrap(tfile.name, boxes=boxes)
class TestJp2Boxes(unittest.TestCase):
def test_default_JPEG2000SignatureBox(self):
# Should be able to instantiate a JPEG2000SignatureBox
b = glymur.jp2box.JPEG2000SignatureBox()
self.assertEqual(b.signature, (13, 10, 135, 10))
def test_default_FileTypeBox(self):
# Should be able to instantiate a FileTypeBox
b = glymur.jp2box.FileTypeBox()
self.assertEqual(b.brand, 'jp2 ')
self.assertEqual(b.minor_version, 0)
self.assertEqual(b.compatibility_list, ['jp2 '])
def test_default_ImageHeaderBox(self):
# Should be able to instantiate an image header box.
b = glymur.jp2box.ImageHeaderBox(height=512, width=256,
num_components=3)
self.assertEqual(b.height, 512)
self.assertEqual(b.width, 256)
self.assertEqual(b.num_components, 3)
self.assertEqual(b.bits_per_component, 8)
self.assertFalse(b.signed)
self.assertFalse(b.colorspace_unknown)
def test_default_JP2HeaderBox(self):
b1 = JP2HeaderBox()
b1.box = [ImageHeaderBox(height=512, width=256),
ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
def test_default_ContiguousCodestreamBox(self):
b = ContiguousCodestreamBox()
self.assertEqual(b.box_id, 'jp2c')
self.assertIsNone(b.main_header)
class TestJpxBoxes(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
@unittest.skipIf(format_corpus_data_root is None,
"FORMAT_CORPUS_DATA_ROOT environment variable not set")
def test_codestream_header(self):
# Should recognize codestream header box.
jfile = os.path.join(format_corpus_data_root,
'jp2k-formats/balloon.jpf')
jpx = Jp2k(jfile)
# This superbox just happens to be empty.
self.assertEqual(jpx.box[4].box_id, 'jpch')
self.assertEqual(len(jpx.box[4].box), 0)
@unittest.skipIf(format_corpus_data_root is None,
"FORMAT_CORPUS_DATA_ROOT environment variable not set")
def test_compositing_layer_header(self):
# Should recognize compositing layer header box.
jfile = os.path.join(format_corpus_data_root,
'jp2k-formats/balloon.jpf')
jpx = Jp2k(jfile)
# This superbox just happens to be empty.
self.assertEqual(jpx.box[5].box_id, 'jplh')
self.assertEqual(len(jpx.box[5].box), 0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -990,7 +990,11 @@ class TestSuite(unittest.TestCase):
def test_NR_DEC_text_GBR_jp2_29_decode(self): def test_NR_DEC_text_GBR_jp2_29_decode(self):
jfile = os.path.join(data_root, jfile = os.path.join(data_root,
'input/nonregression/text_GBR.jp2') 'input/nonregression/text_GBR.jp2')
data = Jp2k(jfile).read() with warnings.catch_warnings():
# brand is 'jp2 ', but has any icc profile.
warnings.simplefilter("ignore")
jp2 = Jp2k(jfile)
data = jp2.read()
self.assertTrue(True) self.assertTrue(True)
def test_NR_DEC_pacs_ge_j2k_30_decode(self): def test_NR_DEC_pacs_ge_j2k_30_decode(self):
@ -4021,7 +4025,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[1].compatibility_list[1], 'jp2 ') self.assertEqual(jp2.box[1].compatibility_list[1], 'jp2 ')
# XML box # XML box
tags = [x.tag for x in jp2.box[2].xml] tags = [x.tag for x in jp2.box[2].xml.getroot()]
self.assertEqual(tags, self.assertEqual(tags,
['{http://www.jpeg.org/jpx/1.0/xml}' ['{http://www.jpeg.org/jpx/1.0/xml}'
+ 'GENERAL_CREATION_INFO']) + 'GENERAL_CREATION_INFO'])
@ -4046,7 +4050,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[1].colorspace, glymur.core.SRGB) self.assertEqual(jp2.box[3].box[1].colorspace, glymur.core.SRGB)
# XML box # XML box
tags = [x.tag for x in jp2.box[4].xml] tags = [x.tag for x in jp2.box[4].xml.getroot()]
self.assertEqual(tags, ['{http://www.jpeg.org/jpx/1.0/xml}CAPTION', self.assertEqual(tags, ['{http://www.jpeg.org/jpx/1.0/xml}CAPTION',
'{http://www.jpeg.org/jpx/1.0/xml}LOCATION', '{http://www.jpeg.org/jpx/1.0/xml}LOCATION',
'{http://www.jpeg.org/jpx/1.0/xml}EVENT']) '{http://www.jpeg.org/jpx/1.0/xml}EVENT'])
@ -4376,13 +4380,13 @@ class TestSuiteDump(unittest.TestCase):
self.assertIsNone(jp2.box[2].box[1].colorspace) self.assertIsNone(jp2.box[2].box[1].colorspace)
# XML box # XML box
tags = [x.tag for x in jp2.box[3].xml] tags = [x.tag for x in jp2.box[3].xml.getroot()]
self.assertEqual(tags, self.assertEqual(tags,
['{http://www.jpeg.org/jpx/1.0/xml}' ['{http://www.jpeg.org/jpx/1.0/xml}'
+ 'GENERAL_CREATION_INFO']) + 'GENERAL_CREATION_INFO'])
# XML box # XML box
tags = [x.tag for x in jp2.box[5].xml] tags = [x.tag for x in jp2.box[5].xml.getroot()]
self.assertEqual(tags, self.assertEqual(tags,
['{http://www.jpeg.org/jpx/1.0/xml}CAPTION', ['{http://www.jpeg.org/jpx/1.0/xml}CAPTION',
'{http://www.jpeg.org/jpx/1.0/xml}LOCATION', '{http://www.jpeg.org/jpx/1.0/xml}LOCATION',
@ -6954,8 +6958,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(c.segment[3]._exponent, [4] + [5, 5, 6] * 5) self.assertEqual(c.segment[3]._exponent, [4] + [5, 5, 6] * 5)
def test_NR_merged_dump(self): def test_NR_merged_dump(self):
jfile = os.path.join(data_root, jfile = os.path.join(data_root, 'input/nonregression/merged.jp2')
'input/nonregression/merged.jp2')
jp2 = Jp2k(jfile) jp2 = Jp2k(jfile)
ids = [box.box_id for box in jp2.box] ids = [box.box_id for box in jp2.box]
@ -7261,7 +7264,10 @@ class TestSuiteDump(unittest.TestCase):
def test_NR_text_GBR_dump(self): def test_NR_text_GBR_dump(self):
jfile = os.path.join(data_root, jfile = os.path.join(data_root,
'input/nonregression/text_GBR.jp2') 'input/nonregression/text_GBR.jp2')
jp2 = Jp2k(jfile) with warnings.catch_warnings():
# brand is 'jp2 ', but has any icc profile.
warnings.simplefilter("ignore")
jp2 = Jp2k(jfile)
ids = [box.box_id for box in jp2.box] ids = [box.box_id for box in jp2.box]
lst = ['jP ', 'ftyp', 'rreq', 'jp2h', lst = ['jP ', 'ftyp', 'rreq', 'jp2h',
@ -7813,6 +7819,8 @@ class TestSuite15(unittest.TestCase):
data = jp2.read() data = jp2.read()
self.assertTrue(True) self.assertTrue(True)
@unittest.skipIf(int(glymur.lib.openjpeg.version().split('.')[1]) < 5,
"Segfaults openjpeg 1.4 and earlier.")
def test_NR_DEC_broken2_jp2_5_decode(self): def test_NR_DEC_broken2_jp2_5_decode(self):
# Null pointer access # Null pointer access
jfile = os.path.join(data_root, 'input/nonregression/broken2.jp2') jfile = os.path.join(data_root, 'input/nonregression/broken2.jp2')
@ -7834,6 +7842,8 @@ class TestSuite15(unittest.TestCase):
with self.assertRaises(ValueError) as ce: with self.assertRaises(ValueError) as ce:
d = j.read() d = j.read()
@unittest.skipIf(int(glymur.lib.openjpeg.version().split('.')[1]) < 5,
"Segfaults openjpeg 1.4 and earlier.")
def test_NR_DEC_broken4_jp2_7_decode(self): def test_NR_DEC_broken4_jp2_7_decode(self):
# Null pointer access # Null pointer access
jfile = os.path.join(data_root, 'input/nonregression/broken4.jp2') jfile = os.path.join(data_root, 'input/nonregression/broken4.jp2')

View file

@ -4,6 +4,7 @@ import pkg_resources
import struct import struct
import sys import sys
import tempfile import tempfile
import warnings
if sys.hexversion < 0x02070000: if sys.hexversion < 0x02070000:
import unittest2 as unittest import unittest2 as unittest
@ -277,9 +278,12 @@ class TestPrinting(unittest.TestCase):
"OPJ_DATA_ROOT environment variable not set") "OPJ_DATA_ROOT environment variable not set")
def test_icc_profile(self): def test_icc_profile(self):
filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2')
j = glymur.Jp2k(filename) with warnings.catch_warnings():
# brand is 'jp2 ', but has any icc profile.
warnings.simplefilter("ignore")
jp2 = Jp2k(filename)
with patch('sys.stdout', new=StringIO()) as fake_out: with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[3].box[1]) print(jp2.box[3].box[1])
actual = fake_out.getvalue().strip() actual = fake_out.getvalue().strip()
lin27 = ["Colour Specification Box (colr) @ (179, 1339)", lin27 = ["Colour Specification Box (colr) @ (179, 1339)",
" Method: any ICC profile", " Method: any ICC profile",
@ -902,10 +906,13 @@ class TestPrinting(unittest.TestCase):
# ICC profiles may be used in JP2, but the approximation field should # ICC profiles may be used in JP2, but the approximation field should
# be zero unless we have jpx. This file does both. # be zero unless we have jpx. This file does both.
filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2')
j = glymur.Jp2k(filename) with warnings.catch_warnings():
# brand is 'jp2 ', but has any icc profile.
warnings.simplefilter("ignore")
jp2 = Jp2k(filename)
with patch('sys.stdout', new=StringIO()) as fake_out: with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[3].box[1]) print(jp2.box[3].box[1])
actual = fake_out.getvalue().strip() actual = fake_out.getvalue().strip()
lines = ["Colour Specification Box (colr) @ (179, 1339)", lines = ["Colour Specification Box (colr) @ (179, 1339)",
" Method: any ICC profile", " Method: any ICC profile",
@ -942,10 +949,13 @@ class TestPrinting(unittest.TestCase):
def test_uuid(self): def test_uuid(self):
# UUID box # UUID box
filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2') filename = os.path.join(data_root, 'input/nonregression/text_GBR.jp2')
j = glymur.Jp2k(filename) with warnings.catch_warnings():
# brand is 'jp2 ', but has any icc profile.
warnings.simplefilter("ignore")
jp2 = Jp2k(filename)
with patch('sys.stdout', new=StringIO()) as fake_out: with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[4]) print(jp2.box[4])
actual = fake_out.getvalue().strip() actual = fake_out.getvalue().strip()
lines = ['UUID Box (uuid) @ (1544, 25)', lines = ['UUID Box (uuid) @ (1544, 25)',
' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71', ' UUID: 3a0d0218-0ae9-4115-b376-4bca41ce0e71',

View file

@ -6,30 +6,30 @@
| | | | | pass. | | | | | | pass. |
+-----------+--------+--------+--------+--------------------------------------+ +-----------+--------+--------+--------+--------------------------------------+
| Mac | X | | | MacPorts with both OpenJPEG 1.5.1 | | Mac | X | | | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG svn. 353 of 449 tests | | 10.6.8 | | | | and OpenJPEG svn. 352 of 450 tests |
| | | | | should pass. | | | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+ +-----------+--------+--------+--------+--------------------------------------+
| Mac | | X | | MacPorts with both OpenJPEG 1.5.1 | | Mac | | X | | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG svn. 376 of 454 tests | | 10.6.8 | | | | and OpenJPEG svn. 377 of 455 tests |
| | | | | should pass. | | | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+ +-----------+--------+--------+--------+--------------------------------------+
| Mac | | | X | MacPorts with both OpenJPEG 1.5.1 | | Mac | | | X | MacPorts with both OpenJPEG 1.5.1 |
| 10.6.8 | | | | and OpenJPEG svn. 401 of 454 | | 10.6.8 | | | | and OpenJPEG svn. 402 of 455 |
| | | | | tests should pass. | | | | | | tests should pass. |
+-----------+--------+--------+-----------------------------------------------+ +-----------+--------+--------+-----------------------------------------------+
| Fedora 19 | | | X | Ships with 1.5.1, openjp2 built too. | | Fedora 19 | | | X | Ships with 1.5.1, openjp2 built too. |
| | | | | 401 of 454 tests should pass. | | | | | | 402 of 455 tests should pass. |
+-----------+--------+--------+--------+--------------------------------------+ +-----------+--------+--------+--------+--------------------------------------+
| Fedora 18 | | | X | Ships with 1.5.1. 169 of 449 tests | | Fedora 18 | | | X | Ships with 1.5.1. 169 of 449 tests |
| | | | | should pass. | | | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+ +-----------+--------+--------+--------+--------------------------------------+
| Fedora 17 | | X | | Ships with 1.4.0. 169 of 449 tests | | Fedora 17 | | X | | Ships with 1.4.0. 166 of 450 tests |
| | | | | should pass. | | | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+ +-----------+--------+--------+--------+--------------------------------------+
| CentOS | X | | | Ships with 1.3.0. 167 of 449 tests | | CentOS | X | | | Ships with 1.3.0. 164 of 450 tests |
| 6.3 | | | | should pass. | | 6.3 | | | | should pass. |
+-----------+--------+--------+--------+--------------------------------------+ +-----------+--------+--------+--------+--------------------------------------+
| Raspberry | | X | | Ships with 1.3.0. 169 of 449 tests | | Raspberry | | X | | Ships with 1.3.0. 166 of 450 tests |
| Pi | | | | should pass. | | Pi | | | | should pass. |
| Debian 7 | | | | | | Debian 7 | | | | |
+-----------+--------+--------+--------+--------------------------------------+ +-----------+--------+--------+--------+--------------------------------------+

View file

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