Merge branch 'issue175' into devel
This commit is contained in:
commit
15358326da
10 changed files with 643 additions and 173 deletions
21
CHANGES.txt
21
CHANGES.txt
|
|
@ -1,13 +1,14 @@
|
|||
Feb 09, 2014 - Removed support for Python 2.6. Added write support for JP2
|
||||
UUID, DataEntryURL, Palette and Component Mapping boxes, JPX
|
||||
Association, NumberList and DataReference boxes. Added read
|
||||
support for JPX free, number list, data reference, fragment
|
||||
table, and fragment list boxes. Improved JPX Reader Requirements box
|
||||
support. Added get_printoptions, set_printoptions functions.
|
||||
Palette box now a 2D numpy array instead of a list of 1D arrays.
|
||||
JP2 super box constructors now take optional box list argument.
|
||||
Fixed bug where JPX files with more than one codestream but
|
||||
advertising jp2 compatibility were not being read.
|
||||
Feb 09, 2014 - Changed constructor for ChannelDefinition box. Removed support
|
||||
for Python 2.6. Added write support for JP2 UUID, DataEntryURL,
|
||||
Palette and Component Mapping boxes, JPX Association, NumberList
|
||||
and DataReference boxes. Added read support for JPX free,
|
||||
number list, data reference, fragment table, and fragment list
|
||||
boxes. Improved JPX Reader Requirements box support. Added
|
||||
get_printoptions, set_printoptions functions. Palette box now
|
||||
a 2D numpy array instead of a list of 1D arrays. JP2 super box
|
||||
constructors now take optional box list argument. Fixed bug
|
||||
where JPX files with more than one codestream but advertising
|
||||
jp2 compatibility were not being read.
|
||||
|
||||
Jan 28, 2014 - v0.5.10 Fixed bad warning when reader requirements box mask
|
||||
length is unsupported.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ ChangeLog
|
|||
* 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
|
||||
* Added read support for JPX fragment list and fragment table boxes
|
||||
* incompatible change to channel definition box constructor, channel_type and association are no longer keyword arguments
|
||||
* incompatible change to palette box constructor, it now takes a 2D numpy array instead of a list of 1D arrays
|
||||
|
||||
0.5.0 (September 16, 2013)
|
||||
|
|
|
|||
|
|
@ -359,7 +359,7 @@ channel, but we aren't doing that). ::
|
|||
|
||||
>>> from glymur.core import RED, GREEN, BLUE, WHOLE_IMAGE
|
||||
>>> asoc = [RED, GREEN, BLUE, WHOLE_IMAGE]
|
||||
>>> cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=ctype, association=asoc)
|
||||
>>> cdef = glymur.jp2box.ChannelDefinitionBox(ctype, asoc)
|
||||
>>> print(cdef)
|
||||
Channel Definition Box (cdef) @ (0, 0)
|
||||
Channel 0 (color) ==> (1)
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ class _Ifd(object):
|
|||
tag_name = tagnum2name[tag]
|
||||
except KeyError:
|
||||
# Ok, we don't recognize this tag. Just use the numeric id.
|
||||
msg = 'Unrecognized Exif tag "{0}".'.format(tag)
|
||||
msg = 'Unrecognized Exif tag: {0}'.format(tag)
|
||||
warnings.warn(msg, UserWarning)
|
||||
tag_name = tag
|
||||
self.processed_ifd[tag_name] = value
|
||||
|
|
|
|||
360
glymur/jp2box.py
360
glymur/jp2box.py
|
|
@ -29,6 +29,7 @@ import numpy as np
|
|||
from .codestream import Codestream
|
||||
from .core import _COLORSPACE_MAP_DISPLAY
|
||||
from .core import _COLOR_TYPE_MAP_DISPLAY
|
||||
from .core import SRGB, GREYSCALE, YCC
|
||||
from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
|
||||
from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD
|
||||
from .core import _pretty_print_xml
|
||||
|
|
@ -63,6 +64,8 @@ class Jp2kBox(object):
|
|||
offset of the box from the start of the file.
|
||||
longname : str
|
||||
more verbose description of the box.
|
||||
box : list
|
||||
List of JPEG 2000 boxes.
|
||||
"""
|
||||
|
||||
def __init__(self, box_id='', offset=0, length=0, longname=''):
|
||||
|
|
@ -70,6 +73,7 @@ class Jp2kBox(object):
|
|||
self.length = length
|
||||
self.offset = offset
|
||||
self.longname = longname
|
||||
self.box = []
|
||||
|
||||
def __repr__(self):
|
||||
msg = "glymur.jp2box.Jp2kBox(box_id='{0}', offset={1}, length={2}, "
|
||||
|
|
@ -88,6 +92,18 @@ class Jp2kBox(object):
|
|||
msg = "Not supported for {0} box.".format(self.longname)
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
def _str_superbox(self):
|
||||
"""__str__ method for all superboxes."""
|
||||
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_superbox(self, fptr):
|
||||
"""Write a superbox.
|
||||
|
||||
|
|
@ -108,6 +124,54 @@ class Jp2kBox(object):
|
|||
fptr.write(struct.pack('>I', end_pos - orig_pos))
|
||||
fptr.seek(end_pos)
|
||||
|
||||
def _parse_this_box(self, fptr, box_id, start, num_bytes):
|
||||
"""Parse the current box.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fptr : file
|
||||
Open file object.
|
||||
box_id : str
|
||||
4-letter identifier for the current box.
|
||||
start, num_bytes: int
|
||||
Byte offset and length of the current box.
|
||||
|
||||
Returns
|
||||
-------
|
||||
box : Jp2kBox
|
||||
object corresponding to the current box
|
||||
"""
|
||||
try:
|
||||
box = _BOX_WITH_ID[box_id].parse(fptr, start, num_bytes)
|
||||
except KeyError:
|
||||
msg = 'Unrecognized box ({0}) encountered.'.format(box_id)
|
||||
warnings.warn(msg)
|
||||
box = UnknownBox(box_id, offset=start, length=num_bytes,
|
||||
longname='Unknown')
|
||||
|
||||
cpos = fptr.tell()
|
||||
if not ((cpos == start + 8) or (cpos == start + 16)):
|
||||
# If the file pointer has advanced, then the KeyError
|
||||
# ocurred during the parsing of the box.
|
||||
pass
|
||||
else:
|
||||
# Could it be a superbox with recognizable child boxes?
|
||||
# Peek ahead to see.
|
||||
pos = fptr.tell()
|
||||
read_buffer = fptr.read(8)
|
||||
_, sub_id = struct.unpack('>I4s', read_buffer)
|
||||
sub_id = sub_id.decode('utf-8')
|
||||
|
||||
# Regardless of whether or not we recognize the box, rewind back
|
||||
# to properly advance to the next box.
|
||||
fptr.seek(pos)
|
||||
|
||||
# Now process any child boxes if we actually did recognize it.
|
||||
if sub_id in _BOX_WITH_ID.keys():
|
||||
box.box = box.parse_superbox(fptr)
|
||||
|
||||
return box
|
||||
|
||||
def parse_superbox(self, fptr):
|
||||
"""Parse a superbox (box consisting of nothing but other boxes.
|
||||
|
||||
|
|
@ -147,16 +211,10 @@ class Jp2kBox(object):
|
|||
num_bytes, = struct.unpack('>Q', read_buffer)
|
||||
|
||||
else:
|
||||
# The box_length value really is the length of the box!
|
||||
num_bytes = box_length
|
||||
|
||||
# Call the proper parser for the given box with ID "T".
|
||||
try:
|
||||
box = _BOX_WITH_ID[box_id].parse(fptr, start, num_bytes)
|
||||
except KeyError:
|
||||
msg = 'Unrecognized box ({0}) encountered.'.format(box_id)
|
||||
warnings.warn(msg)
|
||||
box = Jp2kBox(box_id, offset=start, length=num_bytes,
|
||||
longname='Unknown box')
|
||||
box = self._parse_this_box(fptr, box_id, start, num_bytes)
|
||||
|
||||
superbox.append(box)
|
||||
|
||||
|
|
@ -213,13 +271,6 @@ class ColourSpecificationBox(Jp2kBox):
|
|||
approximation=0, colorspace=None, icc_profile=None,
|
||||
length=0, offset=-1):
|
||||
Jp2kBox.__init__(self, box_id='colr', longname='Colour Specification')
|
||||
|
||||
if colorspace is not None and icc_profile is not None:
|
||||
raise IOError("colorspace and icc_profile cannot both be set.")
|
||||
if method not in (1, 2, 3, 4):
|
||||
raise IOError("Invalid method.")
|
||||
if approximation not in (0, 1, 2, 3, 4):
|
||||
raise IOError("Invalid approximation.")
|
||||
self.method = method
|
||||
self.precedence = precedence
|
||||
self.approximation = approximation
|
||||
|
|
@ -227,6 +278,33 @@ class ColourSpecificationBox(Jp2kBox):
|
|||
self.icc_profile = icc_profile
|
||||
self.length = length
|
||||
self.offset = offset
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
"""Verify that the box obeys the specifications."""
|
||||
if self.colorspace is not None and self.icc_profile is not None:
|
||||
raise IOError("colorspace and icc_profile cannot both be set.")
|
||||
if self.method not in (1, 2, 3, 4):
|
||||
raise IOError("Invalid method.")
|
||||
if self.approximation not in (0, 1, 2, 3, 4):
|
||||
raise IOError("Invalid approximation.")
|
||||
|
||||
def _write_validate(self):
|
||||
"""In addition to constructor validation steps, run validation steps
|
||||
for writing."""
|
||||
if self.colorspace is None:
|
||||
msg = "Writing Colour Specification boxes without enumerated "
|
||||
msg += "colorspaces is not supported at this time."
|
||||
raise IOError(msg)
|
||||
|
||||
if self.icc_profile is None:
|
||||
if self.colorspace not in [SRGB, GREYSCALE, YCC]:
|
||||
msg = "Colorspace should correspond to one of SRGB, GREYSCALE, "
|
||||
msg += "or YCC."
|
||||
raise IOError(msg)
|
||||
|
||||
self._validate()
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
msg = "glymur.jp2box.ColourSpecificationBox("
|
||||
|
|
@ -270,10 +348,7 @@ class ColourSpecificationBox(Jp2kBox):
|
|||
def write(self, fptr):
|
||||
"""Write an Colour Specification box to file.
|
||||
"""
|
||||
if self.colorspace is None:
|
||||
msg = "Writing Colour Specification boxes without enumerated "
|
||||
msg += "colorspaces is not supported at this time."
|
||||
raise NotImplementedError(msg)
|
||||
self._write_validate()
|
||||
length = 15 if self.icc_profile is None else 11 + len(self.icc_profile)
|
||||
fptr.write(struct.pack('>I', length))
|
||||
fptr.write('colr'.encode())
|
||||
|
|
@ -469,23 +544,29 @@ class ChannelDefinitionBox(Jp2kBox):
|
|||
association : list
|
||||
index of the associated color
|
||||
"""
|
||||
def __init__(self, index=None, channel_type=None, association=None,
|
||||
**kwargs):
|
||||
def __init__(self, channel_type, association, index=None, **kwargs):
|
||||
Jp2kBox.__init__(self, box_id='cdef', longname='Channel Definition')
|
||||
|
||||
# channel type and association must be specified.
|
||||
if channel_type is None or association is None:
|
||||
raise IOError("channel_type and association must be specified.")
|
||||
|
||||
if index is None:
|
||||
index = list(range(len(channel_type)))
|
||||
self.index = tuple(range(len(channel_type)))
|
||||
else:
|
||||
self.index = tuple(index)
|
||||
|
||||
if len(index) != len(channel_type) or len(index) != len(association):
|
||||
self.channel_type = tuple(channel_type)
|
||||
self.association = tuple(association)
|
||||
self.__dict__.update(**kwargs)
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
"""Verify that the box obeys the specifications."""
|
||||
# channel type and association must be specified.
|
||||
if not ((len(self.index) == len(self.channel_type)) and
|
||||
(len(self.channel_type) == len(self.association))):
|
||||
msg = "Length of channel definition box inputs must be the same."
|
||||
raise IOError(msg)
|
||||
|
||||
# channel types must be one of 0, 1, 2, 65535
|
||||
if any(x not in [0, 1, 2, 65535] for x in channel_type):
|
||||
if any(x not in [0, 1, 2, 65535] for x in self.channel_type):
|
||||
msg = "Channel types must be in the set of\n\n"
|
||||
msg += " 0 - colour image data for associated color\n"
|
||||
msg += " 1 - opacity\n"
|
||||
|
|
@ -493,10 +574,6 @@ class ChannelDefinitionBox(Jp2kBox):
|
|||
msg += " 65535 - unspecified"
|
||||
raise IOError(msg)
|
||||
|
||||
self.index = tuple(index)
|
||||
self.channel_type = tuple(channel_type)
|
||||
self.association = tuple(association)
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
def __str__(self):
|
||||
msg = Jp2kBox.__str__(self)
|
||||
|
|
@ -522,6 +599,7 @@ class ChannelDefinitionBox(Jp2kBox):
|
|||
def write(self, fptr):
|
||||
"""Write a channel definition box to file.
|
||||
"""
|
||||
self._validate()
|
||||
num_components = len(self.association)
|
||||
fptr.write(struct.pack('>I', 8 + 2 + num_components * 6))
|
||||
fptr.write('cdef'.encode('utf-8'))
|
||||
|
|
@ -559,9 +637,10 @@ class ChannelDefinitionBox(Jp2kBox):
|
|||
channel_type = data[1:num_components * 6:3]
|
||||
association = data[2:num_components * 6:3]
|
||||
|
||||
box = ChannelDefinitionBox(index=index, channel_type=channel_type,
|
||||
association=association, length=length,
|
||||
offset=offset)
|
||||
box = ChannelDefinitionBox(index=tuple(index),
|
||||
channel_type=tuple(channel_type),
|
||||
association=tuple(association),
|
||||
length=length, offset=offset)
|
||||
return box
|
||||
|
||||
|
||||
|
|
@ -592,16 +671,7 @@ class CodestreamHeaderBox(Jp2kBox):
|
|||
return msg
|
||||
|
||||
def __str__(self):
|
||||
msg = Jp2kBox.__str__(self)
|
||||
if _printoptions['short'] == True:
|
||||
return msg
|
||||
|
||||
for box in self.box:
|
||||
boxstr = str(box)
|
||||
|
||||
# Add indentation.
|
||||
strs = [('\n ' + x) for x in boxstr.split('\n')]
|
||||
msg += ''.join(strs)
|
||||
msg = self._str_superbox()
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -659,16 +729,7 @@ class CompositingLayerHeaderBox(Jp2kBox):
|
|||
return msg
|
||||
|
||||
def __str__(self):
|
||||
msg = Jp2kBox.__str__(self)
|
||||
if _printoptions['short'] == True:
|
||||
return msg
|
||||
|
||||
for box in self.box:
|
||||
boxstr = str(box)
|
||||
|
||||
# Add indentation.
|
||||
strs = [('\n ' + x) for x in boxstr.split('\n')]
|
||||
msg += ''.join(strs)
|
||||
msg = self._str_superbox()
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -738,7 +799,6 @@ class ComponentMappingBox(Jp2kBox):
|
|||
if _printoptions['short'] == True:
|
||||
return msg
|
||||
|
||||
|
||||
for k in range(len(self.component_index)):
|
||||
if self.mapping_type[k] == 1:
|
||||
msg += '\n Component {0} ==> palette column {1}'
|
||||
|
|
@ -831,10 +891,10 @@ class ContiguousCodestreamBox(Jp2kBox):
|
|||
msg += '\n Main header:'
|
||||
for segment in self.main_header.segment:
|
||||
segstr = str(segment)
|
||||
|
||||
# Add indentation.
|
||||
strs = [('\n ' + x) for x in segstr.split('\n')]
|
||||
msg += ''.join(strs)
|
||||
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -876,15 +936,36 @@ class DataReferenceBox(Jp2kBox):
|
|||
DR : list
|
||||
Data Entry URL boxes.
|
||||
"""
|
||||
def __init__(self, data_entry_url_boxes, length=0, offset=-1):
|
||||
def __init__(self, data_entry_url_boxes=None, length=0, offset=-1):
|
||||
Jp2kBox.__init__(self, box_id='dtbl', longname='Data Reference')
|
||||
self.DR = data_entry_url_boxes
|
||||
if data_entry_url_boxes is None:
|
||||
self.DR = []
|
||||
else:
|
||||
self.DR = data_entry_url_boxes
|
||||
self.length = length
|
||||
self.offset = offset
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
"""Verify that the box obeys the specifications."""
|
||||
for box in self.DR:
|
||||
if box.box_id != 'url ':
|
||||
msg = 'All child boxes of a data reference box must be data '
|
||||
msg += 'entry URL boxes.'
|
||||
raise IOError(msg)
|
||||
|
||||
def _write_validate(self):
|
||||
"""Verify that the box obeys the specifications for writing.
|
||||
"""
|
||||
if len(self.DR) == 0:
|
||||
msg = "A data reference box cannot be empty when written to a file."
|
||||
raise IOError(msg)
|
||||
self._validate()
|
||||
|
||||
def write(self, fptr):
|
||||
"""Write a Data Reference box to file.
|
||||
"""
|
||||
self._write_validate()
|
||||
|
||||
# Very similar to the say a superbox is written.
|
||||
orig_pos = fptr.tell()
|
||||
|
|
@ -980,7 +1061,6 @@ class FileTypeBox(Jp2kBox):
|
|||
self.brand = brand
|
||||
self.minor_version = minor_version
|
||||
if compatibility_list is None:
|
||||
# see W0102, pylint
|
||||
self.compatibility_list = ['jp2 ']
|
||||
else:
|
||||
self.compatibility_list = compatibility_list
|
||||
|
|
@ -1007,9 +1087,22 @@ class FileTypeBox(Jp2kBox):
|
|||
|
||||
return msg
|
||||
|
||||
def _validate(self):
|
||||
"""Validate the box before writing to file."""
|
||||
if self.brand not in ['jp2 ', 'jpx ']:
|
||||
msg = "The file type brand must be either 'jp2 ' or 'jpx '."
|
||||
raise IOError(msg)
|
||||
valid_cls = ['jp2 ', 'jpx ', 'jpxb']
|
||||
for item in self.compatibility_list:
|
||||
if item not in valid_cls:
|
||||
msg = "The file type compatibility list item '{0}' is not "
|
||||
msg += "valid: valid entries are {1}"
|
||||
raise IOError(msg.format(item, valid_cls))
|
||||
|
||||
def write(self, fptr):
|
||||
"""Write a File Type box to file.
|
||||
"""
|
||||
self._validate()
|
||||
length = 16 + 4*len(self.compatibility_list)
|
||||
fptr.write(struct.pack('>I', length))
|
||||
fptr.write('ftyp'.encode())
|
||||
|
|
@ -1084,6 +1177,18 @@ class FragmentListBox(Jp2kBox):
|
|||
self.length = length
|
||||
self.offset = offset
|
||||
|
||||
def _validate(self):
|
||||
"""Validate internal correctness."""
|
||||
if (((len(self.fragment_offset) != len(self.fragment_length)) or
|
||||
(len(self.fragment_length) != len(self.data_reference)))):
|
||||
msg = "The lengths of the fragment offsets, fragment lengths, and "
|
||||
msg += "data reference items must be the same."
|
||||
raise IOError(msg)
|
||||
if any([x <= 0 for x in self.fragment_offset]):
|
||||
raise IOError("Fragment offsets must all be positive.")
|
||||
if any([x <= 0 for x in self.fragment_length]):
|
||||
raise IOError("Fragment lengths must all be positive.")
|
||||
|
||||
def __repr__(self):
|
||||
msg = "glymur.jp2box.FragmentListBox()"
|
||||
return msg
|
||||
|
|
@ -1103,6 +1208,22 @@ class FragmentListBox(Jp2kBox):
|
|||
|
||||
return msg
|
||||
|
||||
def write(self, fptr):
|
||||
"""Write a fragment list box to file.
|
||||
"""
|
||||
self._validate()
|
||||
num_items = len(self.fragment_offset)
|
||||
length = 8 + 2 + num_items * 14
|
||||
fptr.write(struct.pack('>I', length))
|
||||
fptr.write(self.box_id.encode())
|
||||
fptr.write(struct.pack('>H', num_items))
|
||||
for j in range(num_items):
|
||||
write_buffer = struct.pack('>QIH',
|
||||
self.fragment_offset[j],
|
||||
self.fragment_length[j],
|
||||
self.data_reference[j])
|
||||
fptr.write(write_buffer)
|
||||
|
||||
@staticmethod
|
||||
def parse(fptr, offset, length):
|
||||
"""Parse JPX free box.
|
||||
|
|
@ -1146,26 +1267,18 @@ class FragmentTableBox(Jp2kBox):
|
|||
longname : str
|
||||
more verbose description of the box.
|
||||
"""
|
||||
def __init__(self, length=0, offset=-1):
|
||||
def __init__(self, box=None, length=0, offset=-1):
|
||||
Jp2kBox.__init__(self, box_id='ftbl', longname='Fragment Table')
|
||||
self.length = length
|
||||
self.offset = offset
|
||||
self.box = box if box is not None else []
|
||||
|
||||
def __repr__(self):
|
||||
msg = "glymur.jp2box.FragmentTableBox()"
|
||||
return msg
|
||||
|
||||
def __str__(self):
|
||||
msg = Jp2kBox.__str__(self)
|
||||
if _printoptions['short'] == True:
|
||||
return msg
|
||||
|
||||
for box in self.box:
|
||||
boxstr = str(box)
|
||||
|
||||
# Add indentation.
|
||||
strs = [('\n ' + x) for x in boxstr.split('\n')]
|
||||
msg += ''.join(strs)
|
||||
msg = self._str_superbox()
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -1183,7 +1296,7 @@ class FragmentTableBox(Jp2kBox):
|
|||
|
||||
Returns
|
||||
-------
|
||||
FreeBox instance
|
||||
FragmentTableBox instance
|
||||
"""
|
||||
box = FragmentTableBox(length=length, offset=offset)
|
||||
|
||||
|
|
@ -1193,6 +1306,20 @@ class FragmentTableBox(Jp2kBox):
|
|||
|
||||
return box
|
||||
|
||||
def _validate(self):
|
||||
"""Self-validate the box before writing."""
|
||||
box_ids = [box.box_id for box in self.box]
|
||||
if len(box_ids) != 1 or box_ids[0] != 'flst':
|
||||
msg = "Fragment table boxes must have a single fragment list "
|
||||
msg += "box as a child box."
|
||||
raise IOError(msg)
|
||||
|
||||
def write(self, fptr):
|
||||
"""Write a fragment table box to file.
|
||||
"""
|
||||
self._validate()
|
||||
self._write_superbox(fptr)
|
||||
|
||||
|
||||
|
||||
class FreeBox(Jp2kBox):
|
||||
|
|
@ -1415,16 +1542,7 @@ class AssociationBox(Jp2kBox):
|
|||
return msg
|
||||
|
||||
def __str__(self):
|
||||
msg = Jp2kBox.__str__(self)
|
||||
if _printoptions['short'] == True:
|
||||
return msg
|
||||
|
||||
for box in self.box:
|
||||
boxstr = str(box)
|
||||
|
||||
# Add indentation.
|
||||
strs = [('\n ' + x) for x in boxstr.split('\n')]
|
||||
msg += ''.join(strs)
|
||||
msg = self._str_superbox()
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -1485,13 +1603,7 @@ class JP2HeaderBox(Jp2kBox):
|
|||
return msg
|
||||
|
||||
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)
|
||||
msg = self._str_superbox()
|
||||
return msg
|
||||
|
||||
def write(self, fptr):
|
||||
|
|
@ -1616,6 +1728,15 @@ class PaletteBox(Jp2kBox):
|
|||
self.signed = signed
|
||||
self.length = length
|
||||
self.offset = offset
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
"""Verify that the box obeys the specifications."""
|
||||
if ((len(self.bits_per_component) != len(self.signed)) or
|
||||
(len(self.signed) != self.palette.shape[1])):
|
||||
msg = "The length of the 'bits_per_component' and the 'signed' "
|
||||
msg += "members must equal the number of columns of the palette."
|
||||
raise IOError(msg)
|
||||
|
||||
def __repr__(self):
|
||||
msg = "glymur.jp2box.PaletteBox({0}, bits_per_component={1}, "
|
||||
|
|
@ -1635,6 +1756,7 @@ class PaletteBox(Jp2kBox):
|
|||
def write(self, fptr):
|
||||
"""Write a Palette box to file.
|
||||
"""
|
||||
self._validate()
|
||||
bytes_per_row = sum(self.bits_per_component) / 8
|
||||
bytes_per_palette = bytes_per_row * self.palette.shape[0]
|
||||
box_length = 8 + 3 + self.palette.shape[1] + bytes_per_palette
|
||||
|
|
@ -1714,8 +1836,7 @@ class PaletteBox(Jp2kBox):
|
|||
palette[j] = struct.unpack_from(fmt, read_buffer,
|
||||
offset=j * row_nbytes)
|
||||
|
||||
box = PaletteBox(palette, bps, signed, length=length, offset=offset)
|
||||
return box
|
||||
return PaletteBox(palette, bps, signed, length=length, offset=offset)
|
||||
|
||||
|
||||
# Map rreq codes to display text.
|
||||
|
|
@ -2086,13 +2207,7 @@ class ResolutionBox(Jp2kBox):
|
|||
return msg
|
||||
|
||||
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)
|
||||
msg = self._str_superbox()
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -2612,14 +2727,7 @@ class UUIDInfoBox(Jp2kBox):
|
|||
return msg
|
||||
|
||||
def __str__(self):
|
||||
msg = Jp2kBox.__str__(self)
|
||||
for box in self.box:
|
||||
box_str = str(box)
|
||||
|
||||
# Add indentation.
|
||||
lst = [('\n ' + x) for x in box_str.split('\n')]
|
||||
msg += ''.join(lst)
|
||||
|
||||
msg = self._str_superbox()
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -2680,13 +2788,18 @@ class DataEntryURLBox(Jp2kBox):
|
|||
def write(self, fptr):
|
||||
"""Write a data entry url box to file.
|
||||
"""
|
||||
length = 8 + 1 + 3 + len(self.url.encode())
|
||||
# Make sure it is written out as null-terminated.
|
||||
url = self.url
|
||||
if self.url[-1] != chr(0):
|
||||
url = url + chr(0)
|
||||
|
||||
length = 8 + 1 + 3 + len(url.encode())
|
||||
write_buffer = struct.pack('>I4sBBBB',
|
||||
length, self.box_id.encode(),
|
||||
self.version,
|
||||
self.flag[0], self.flag[1], self.flag[2])
|
||||
fptr.write(write_buffer)
|
||||
fptr.write(self.url.encode())
|
||||
fptr.write(url.encode())
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
|
|
@ -2739,6 +2852,37 @@ class DataEntryURLBox(Jp2kBox):
|
|||
return box
|
||||
|
||||
|
||||
class UnknownBox(Jp2kBox):
|
||||
"""Container for unrecognized boxes.
|
||||
|
||||
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.
|
||||
"""
|
||||
def __init__(self, box_id, length=0, offset=-1, longname=''):
|
||||
Jp2kBox.__init__(self, box_id=box_id, longname=longname)
|
||||
self.length = length
|
||||
self.offset = offset
|
||||
|
||||
def __repr__(self):
|
||||
msg = "glymur.jp2box.UnknownBox({0})".format(self.box_id)
|
||||
return msg
|
||||
|
||||
def __str__(self):
|
||||
if len(self.box) > 0:
|
||||
msg = self._str_superbox()
|
||||
else:
|
||||
msg = Jp2kBox.__str__(self)
|
||||
return msg
|
||||
|
||||
|
||||
class UUIDBox(Jp2kBox):
|
||||
"""Container for UUID box information.
|
||||
|
||||
|
|
|
|||
|
|
@ -1110,6 +1110,10 @@ def _validate_nonzero_image_size(nrows, ncols, component_index):
|
|||
raise IOError(msg)
|
||||
|
||||
|
||||
JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ',
|
||||
'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
|
||||
'uuid']
|
||||
|
||||
def _validate_jp2_box_sequence(boxes):
|
||||
"""Run through series of tests for JP2 box legality.
|
||||
|
||||
|
|
@ -1118,14 +1122,24 @@ def _validate_jp2_box_sequence(boxes):
|
|||
_validate_signature_compatibility(boxes)
|
||||
_validate_jp2h(boxes)
|
||||
_validate_jp2c(boxes)
|
||||
_validate_association(boxes)
|
||||
if boxes[1].brand == 'jpx ':
|
||||
_validate_jpx_box_sequence(boxes)
|
||||
else:
|
||||
count = _collect_box_count(boxes)
|
||||
for id in count.keys():
|
||||
if id not in JP2_IDS:
|
||||
msg = "The presence of a '{0}' box requires that the file type "
|
||||
msg += "brand be set to 'jpx '."
|
||||
raise IOError(msg.format(id))
|
||||
|
||||
def _validate_jpx_box_sequence(boxes):
|
||||
"""Run through series of tests for JPX box legality."""
|
||||
_validate_label(boxes)
|
||||
_validate_jpx_brand(boxes, boxes[1].brand)
|
||||
_validate_jpx_compatibility(boxes, boxes[1].compatibility_list)
|
||||
_validate_singletons(boxes)
|
||||
_validate_top_level(boxes)
|
||||
|
||||
|
||||
def _validate_signature_compatibility(boxes):
|
||||
"""Validate the file signature and compatibility status."""
|
||||
# Check for a bad sequence of boxes.
|
||||
|
|
@ -1268,6 +1282,12 @@ def _validate_top_level(boxes):
|
|||
if 'dtbl' in multiples:
|
||||
raise IOError('There can only be one dtbl box in a file.')
|
||||
|
||||
# If there is one data reference box, then there must also be one ftbl.
|
||||
if 'dtbl' in count and 'ftbl' not in count:
|
||||
msg = 'The presence of a data reference box requires the presence of '
|
||||
msg += 'a fragment table box as well.'
|
||||
raise IOError(msg)
|
||||
|
||||
def _validate_singletons(boxes):
|
||||
"""Several boxes can only occur once."""
|
||||
count = _collect_box_count(boxes)
|
||||
|
|
@ -1321,23 +1341,6 @@ def _validate_label(boxes):
|
|||
# Same set of checks on any child boxes.
|
||||
_validate_label(box.box)
|
||||
|
||||
def _validate_association(boxes):
|
||||
"""
|
||||
Association boxes can only contain number list boxes and xml boxes, as far
|
||||
as we know.
|
||||
"""
|
||||
for box in boxes:
|
||||
if box.box_id == 'asoc':
|
||||
if box.box[0].box_id != 'nlst' or box.box[1].box_id != 'xml ':
|
||||
msg = "An Association box can only contain a NumberList box "
|
||||
msg += "followed by an XML box."
|
||||
raise RuntimeError(msg)
|
||||
if hasattr(box, 'box') != 0:
|
||||
# Same set of checks on any child boxes.
|
||||
_validate_association(box.box)
|
||||
|
||||
|
||||
|
||||
def extract_image_cube(image):
|
||||
"""Extract 3D image from openjpeg data structure.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -690,3 +690,11 @@ file7_rreq = r"""Reader Requirements Box (rreq) @ (44, 24)
|
|||
Feature 060: 0x60 e-sRGB enumerated colorspace
|
||||
Feature 043: 0x40 Deprecated - compositing layer uses restricted ICC profile
|
||||
Vendor Features:"""
|
||||
|
||||
file1_xml = r"""XML Box (xml ) @ (36, 439)
|
||||
<ns0:IMAGE_CREATION xmlns:ns0="http://www.jpeg.org/jpx/1.0/xml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jpeg.org/jpx/1.0/xml http://www.jpeg.org/metadata/15444-2.xsd">
|
||||
<ns0:GENERAL_CREATION_INFO>
|
||||
<ns0:CREATION_TIME>2001-11-01T13:45:00.000-06:00</ns0:CREATION_TIME>
|
||||
<ns0:IMAGE_SOURCE>Professional 120 Image</ns0:IMAGE_SOURCE>
|
||||
</ns0:GENERAL_CREATION_INFO>
|
||||
</ns0:IMAGE_CREATION>"""
|
||||
|
|
|
|||
|
|
@ -77,6 +77,28 @@ class TestDataEntryURL(unittest.TestCase):
|
|||
self.assertEqual(jp22.box[4].flag, (0, 0, 0))
|
||||
self.assertEqual(jp22.box[4].url, url)
|
||||
|
||||
def test_null_termination(self):
|
||||
"""I.9.3.2 specifies that the location field must be null terminated."""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
|
||||
url = 'http://glymur.readthedocs.org'
|
||||
deurl = glymur.jp2box.DataEntryURLBox(0, (0, 0, 0), url)
|
||||
boxes = [box for box in jp2.box if box.box_id != 'uuid']
|
||||
boxes.append(deurl)
|
||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||
jp22 = jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
self.assertEqual(jp22.box[-1].length, 42)
|
||||
|
||||
# Go to the last box. Seek past the L, T, version, and flag fields.
|
||||
with open(tfile.name, 'rb') as fptr:
|
||||
fptr.seek(jp22.box[-1].offset + 4 + 4 + 1 + 3)
|
||||
|
||||
nbytes = jp22.box[-1].offset + jp22.box[-1].length - fptr.tell()
|
||||
read_buffer = fptr.read(nbytes)
|
||||
read_url = read_buffer.decode('utf-8')
|
||||
self.assertEqual(url + chr(0), read_url)
|
||||
|
||||
|
||||
@unittest.skipIf(glymur.version.openjpeg_version_tuple[0] < 2 or
|
||||
OPENJP2_IS_V2_OFFICIAL,
|
||||
|
|
@ -139,7 +161,7 @@ class TestChannelDefinition(unittest.TestCase):
|
|||
|
||||
def test_cdef_no_inputs(self):
|
||||
"""channel_type and association are required inputs."""
|
||||
with self.assertRaises(IOError):
|
||||
with self.assertRaises(TypeError):
|
||||
glymur.jp2box.ChannelDefinitionBox()
|
||||
|
||||
def test_rgb_with_index(self):
|
||||
|
|
@ -340,6 +362,29 @@ class TestChannelDefinition(unittest.TestCase):
|
|||
association=association)
|
||||
|
||||
|
||||
class TestFileTypeBox(unittest.TestCase):
|
||||
"""Test suite for ftyp box issues."""
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_brand_unknown(self):
|
||||
"""A ftyp box brand must be 'jp2 ' or 'jpx '."""
|
||||
ftyp = glymur.jp2box.FileTypeBox(brand='jp3')
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
ftyp.write(tfile)
|
||||
|
||||
def test_cl_entry_unknown(self):
|
||||
"""A ftyp box cl list can only contain 'jp2 ', 'jpx ', or 'jpxb'."""
|
||||
ftyp = glymur.jp2box.FileTypeBox(compatibility_list=['jp3'])
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
ftyp.write(tfile)
|
||||
|
||||
class TestColourSpecificationBox(unittest.TestCase):
|
||||
"""Test suite for colr box instantiation."""
|
||||
|
||||
|
|
@ -371,7 +416,7 @@ class TestColourSpecificationBox(unittest.TestCase):
|
|||
boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c]
|
||||
boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)]
|
||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||
with self.assertRaises(NotImplementedError):
|
||||
with self.assertRaises(IOError):
|
||||
j2k.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||
|
|
@ -417,6 +462,46 @@ class TestColourSpecificationBox(unittest.TestCase):
|
|||
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||
approximation=approx)
|
||||
|
||||
def test_colr_with_bad_color(self):
|
||||
"""colr must have a valid color, strange as though that may sound."""
|
||||
colorspace = -1
|
||||
approx = 0
|
||||
colr = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||
approximation=approx)
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
colr.write(tfile)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt",
|
||||
"Problems using NamedTemporaryFile on windows.")
|
||||
class TestPaletteBox(unittest.TestCase):
|
||||
"""Test suite for pclr box instantiation."""
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_mismatched_bitdepth_signed(self):
|
||||
"""bitdepth and signed arguments must have equal length"""
|
||||
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
|
||||
bps = (8, 8, 8)
|
||||
signed = (False, False)
|
||||
with self.assertRaises(IOError):
|
||||
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
|
||||
signed=signed)
|
||||
|
||||
def test_mismatched_signed_palette(self):
|
||||
"""bitdepth and signed arguments must have equal length"""
|
||||
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
|
||||
bps = (8, 8, 8, 8)
|
||||
signed = (False, False, False, False)
|
||||
with self.assertRaises(IOError):
|
||||
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
|
||||
signed=signed)
|
||||
|
||||
|
||||
class TestAppend(unittest.TestCase):
|
||||
"""Tests for append method."""
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ import struct
|
|||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import warnings
|
||||
import xml.etree.cElementTree as ET
|
||||
|
||||
import glymur
|
||||
from glymur import Jp2k
|
||||
|
||||
from glymur.jp2box import DataEntryURLBox, FileTypeBox, JPEG2000SignatureBox
|
||||
from glymur.jp2box import DataReferenceBox, FragmentListBox, FragmentTableBox
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||
class TestJPXWrap(unittest.TestCase):
|
||||
|
|
@ -21,6 +21,7 @@ class TestJPXWrap(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.jp2file = glymur.data.nemo()
|
||||
self.j2kfile = glymur.data.goodstuff()
|
||||
|
||||
raw_xml = b"""<?xml version="1.0"?>
|
||||
<data>
|
||||
|
|
@ -40,6 +41,100 @@ class TestJPXWrap(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
os.unlink(self.xmlfile)
|
||||
|
||||
def test_jpx_ftbl_no_codestream(self):
|
||||
"""Can have a jpx with no codestream."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
|
||||
with open(self.jp2file, 'rb') as fptr:
|
||||
tfile1.write(fptr.read())
|
||||
tfile1.flush()
|
||||
jp2_1 = Jp2k(tfile1.name)
|
||||
|
||||
jp2c = [box for box in jp2_1.box if box.box_id == 'jp2c'][0]
|
||||
|
||||
# coff and clen will be the offset and length input arguments
|
||||
# to the fragment list box. dr_idx is the data reference index.
|
||||
coff = []
|
||||
clen = []
|
||||
dr_idx = []
|
||||
|
||||
coff.append(jp2c.offset + 8)
|
||||
clen.append(jp2c.length - (coff[0] - jp2c.offset))
|
||||
dr_idx.append(1)
|
||||
|
||||
# Make the url box for this codestream.
|
||||
url1 = DataEntryURLBox(0, [0, 0, 0], 'file://' + tfile1.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile2:
|
||||
|
||||
j2k = Jp2k(self.j2kfile)
|
||||
jp2_2 = j2k.wrap(tfile2.name)
|
||||
|
||||
jp2c = [box for box in jp2_2.box if box.box_id == 'jp2c'][0]
|
||||
coff.append(jp2c.offset + 8)
|
||||
clen.append(jp2c.length - (coff[0] - jp2c.offset))
|
||||
dr_idx.append(2)
|
||||
|
||||
# Make the url box for this codestream.
|
||||
url2 = DataEntryURLBox(0, [0, 0, 0], 'file://' + tfile2.name)
|
||||
|
||||
boxes = [JPEG2000SignatureBox(),
|
||||
FileTypeBox(brand='jpx ',
|
||||
compatibility_list=['jpx ',
|
||||
'jp2 ', 'jpxb']),
|
||||
jp2_1.box[2]]
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpx') as tjpx:
|
||||
for box in boxes:
|
||||
box.write(tjpx)
|
||||
|
||||
flst = FragmentListBox(coff, clen, dr_idx)
|
||||
ftbl = FragmentTableBox([flst])
|
||||
ftbl.write(tjpx)
|
||||
|
||||
boxes = [url1, url2]
|
||||
dtbl = DataReferenceBox(data_entry_url_boxes=boxes)
|
||||
dtbl.write(tjpx)
|
||||
tjpx.flush()
|
||||
|
||||
def test_jp2_with_jpx_box(self):
|
||||
"""If the brand is jp2, then no jpx boxes are allowed."""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
|
||||
boxes = jp2.box
|
||||
|
||||
boxes.append(glymur.jp2box.AssociationBox())
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_ftbl(self):
|
||||
"""Write a fragment table box."""
|
||||
# Add a negative test where offset < 0
|
||||
# Add a negative test where length < 0
|
||||
# Add a negative test where ref > 0 but no data reference box.
|
||||
# Add a negative test where more than one flst
|
||||
# Add negative test where ftbl contained in a superbox.
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
|
||||
|
||||
# The ftyp box must be modified to jpx.
|
||||
boxes[1].brand = 'jpx '
|
||||
boxes[1].compatibility_list = ['jp2 ', 'jpxb']
|
||||
|
||||
offset = [89]
|
||||
length = [1132288]
|
||||
reference = [0]
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
ftbl = glymur.jp2box.FragmentTableBox(box=[flst])
|
||||
boxes.append(ftbl)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
self.assertEqual(jpx.box[1].compatibility_list, ['jp2 ', 'jpxb'])
|
||||
self.assertEqual(jpx.box[-1].box_id, 'ftbl')
|
||||
self.assertEqual(jpx.box[-1].box[0].box_id, 'flst')
|
||||
|
||||
def test_jpxb_compatibility(self):
|
||||
"""Wrap JP2 to JPX, state jpxb compatibility"""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
|
|
@ -98,11 +193,47 @@ class TestJPXWrap(unittest.TestCase):
|
|||
self.assertEqual(jpx.box[-1].box[2].box_id, 'lbl ')
|
||||
self.assertEqual(jpx.box[-1].box[2].label, label)
|
||||
|
||||
def test_empty_data_reference(self):
|
||||
"""Empty data reference boxes can be created, but not written."""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
|
||||
|
||||
boxes[1].brand = 'jpx '
|
||||
|
||||
dref = glymur.jp2box.DataReferenceBox()
|
||||
boxes.append(dref)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_deurl_child_of_dtbl(self):
|
||||
"""Data reference boxes can only contain data entry url boxes."""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
|
||||
|
||||
ftyp = glymur.jp2box.FileTypeBox()
|
||||
with self.assertRaises(IOError):
|
||||
dref = glymur.jp2box.DataReferenceBox([ftyp])
|
||||
|
||||
# Try to get around it by appending the ftyp box after creation.
|
||||
dref = glymur.jp2box.DataReferenceBox()
|
||||
dref.DR.append(ftyp)
|
||||
|
||||
boxes.append(dref)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_only_one_data_reference(self):
|
||||
"""Data reference boxes cannot be inside a superbox ."""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
|
||||
|
||||
# Have to make the ftyp brand jpx.
|
||||
boxes[1].brand = 'jpx '
|
||||
|
||||
flag = 0
|
||||
version = (0, 0, 0)
|
||||
url = 'file:////usr/local/bin'
|
||||
|
|
@ -113,16 +244,16 @@ class TestJPXWrap(unittest.TestCase):
|
|||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_lbl_at_top_level(self):
|
||||
"""Label boxes can only be inside a asoc box ."""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
|
||||
|
||||
flag = 0
|
||||
version = (0, 0, 0)
|
||||
url = 'file:////usr/local/bin'
|
||||
# Have to make the ftyp brand jpx.
|
||||
boxes[1].brand = 'jpx '
|
||||
|
||||
lblb = glymur.jp2box.LabelBox('hi there')
|
||||
|
||||
# Put it inside the jp2 header box.
|
||||
|
|
@ -130,13 +261,16 @@ class TestJPXWrap(unittest.TestCase):
|
|||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_data_reference_not_at_top_level(self):
|
||||
def test_data_reference_in_subbox(self):
|
||||
"""Data reference boxes cannot be inside a superbox ."""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
|
||||
|
||||
# Have to make the ftyp brand jpx.
|
||||
boxes[1].brand = 'jpx '
|
||||
|
||||
flag = 0
|
||||
version = (0, 0, 0)
|
||||
url = 'file:////usr/local/bin'
|
||||
|
|
@ -148,13 +282,17 @@ class TestJPXWrap(unittest.TestCase):
|
|||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_jp2_to_jpx_sans_jp2_compatibility(self):
|
||||
"""jp2 wrapped to jpx not including jp2 compatibility is wrong."""
|
||||
jp2 = Jp2k(self.jp2file)
|
||||
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
|
||||
|
||||
# Have to make the ftyp brand jpx.
|
||||
boxes[1].brand = 'jpx '
|
||||
boxes[1].compatibility_list.append('jp2 ')
|
||||
|
||||
numbers = [0, 1]
|
||||
nlst = glymur.jp2box.NumberListBox(numbers)
|
||||
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
|
||||
|
|
@ -164,7 +302,7 @@ class TestJPXWrap(unittest.TestCase):
|
|||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(RuntimeError):
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_jp2_to_jpx_sans_jpx_brand(self):
|
||||
"""Verify error when jp2 wrapped to jpx does not include jpx brand."""
|
||||
|
|
@ -180,7 +318,7 @@ class TestJPXWrap(unittest.TestCase):
|
|||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(RuntimeError):
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||
|
|
@ -188,11 +326,57 @@ class TestJPX(unittest.TestCase):
|
|||
"""Test suite for other JPX boxes."""
|
||||
|
||||
def setUp(self):
|
||||
self.jp2file = glymur.data.nemo()
|
||||
self.jpxfile = glymur.data.jpxfile()
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_flst_lens_not_the_same(self):
|
||||
"""A fragment list box items must be the same length."""
|
||||
offset = [89]
|
||||
length = [1132288]
|
||||
reference = [0, 0]
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
flst.write(tfile)
|
||||
|
||||
def test_flst_offsets_not_positive(self):
|
||||
"""A fragment list box offsets must be positive."""
|
||||
offset = [0]
|
||||
length = [1132288]
|
||||
reference = [0]
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
flst.write(tfile)
|
||||
|
||||
def test_flst_lengths_not_positive(self):
|
||||
"""A fragment list box lengths must be positive."""
|
||||
offset = [89]
|
||||
length = [0]
|
||||
reference = [0]
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
flst.write(tfile)
|
||||
|
||||
def test_ftbl_boxes_empty(self):
|
||||
"""A fragment table box must have at least one child box."""
|
||||
ftbl = glymur.jp2box.FragmentTableBox()
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
ftbl.write(tfile)
|
||||
|
||||
def test_ftbl_child_not_flst(self):
|
||||
"""A fragment table box can only contain a fragment list."""
|
||||
free = glymur.jp2box.FreeBox()
|
||||
ftbl = glymur.jp2box.FragmentTableBox(box=[free])
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
ftbl.write(tfile)
|
||||
|
||||
def test_jpx_rreq_mask_length_3(self):
|
||||
"""There are some JPX files with rreq mask length of 3."""
|
||||
jpx = Jp2k(self.jpxfile)
|
||||
|
|
@ -202,12 +386,50 @@ class TestJPX(unittest.TestCase):
|
|||
self.assertEqual(jpx.box[2].standard_flag,
|
||||
(5, 42, 45, 2, 18, 19, 1, 8, 12, 31, 20))
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||
def test_unknown_superbox(self):
|
||||
"""Verify that we can handle an unknown superbox."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
|
||||
with open(self.jpxfile, 'rb') as ifile:
|
||||
tfile.write(ifile.read())
|
||||
|
||||
# Add the header for an unknwon superbox.
|
||||
write_buffer = struct.pack('>I4s', 20, 'grp '.encode())
|
||||
tfile.write(write_buffer)
|
||||
write_buffer = struct.pack('>I4sI', 12, 'free'.encode(), 0)
|
||||
tfile.write(write_buffer)
|
||||
tfile.flush()
|
||||
|
||||
with self.assertWarns(UserWarning):
|
||||
jpx = Jp2k(tfile.name)
|
||||
self.assertEqual(jpx.box[-1].box_id, 'grp ')
|
||||
self.assertEqual(jpx.box[-1].box[0].box_id, 'free')
|
||||
|
||||
def test_free_box(self):
|
||||
"""Verify that we can handle a free box."""
|
||||
j = Jp2k(self.jpxfile)
|
||||
self.assertEqual(j.box[16].box[0].box_id, 'free')
|
||||
self.assertEqual(type(j.box[16].box[0]), glymur.jp2box.FreeBox)
|
||||
|
||||
def test_data_reference_requires_dtbl(self):
|
||||
"""The existance of a data reference box requires a ftbl box as well."""
|
||||
flag = 0
|
||||
version = (0, 0, 0)
|
||||
url1 = 'file:////usr/local/bin'
|
||||
url2 = 'http://glymur.readthedocs.org'
|
||||
jpx1 = glymur.Jp2k(self.jp2file)
|
||||
boxes = jpx1.box
|
||||
boxes[1].brand = 'jpx '
|
||||
|
||||
deurl1 = glymur.jp2box.DataEntryURLBox(flag, version, url1)
|
||||
deurl2 = glymur.jp2box.DataEntryURLBox(flag, version, url2)
|
||||
dref = glymur.jp2box.DataReferenceBox([deurl1, deurl2])
|
||||
boxes.append(dref)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
jpx1.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_dtbl(self):
|
||||
"""Verify that we can interpret Data Reference boxes."""
|
||||
# Copy the existing JPX file, add a data reference box onto the end.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from .fixtures import text_gbr_27, text_gbr_33, text_gbr_34
|
|||
class TestPrinting(unittest.TestCase):
|
||||
"""Tests for verifying how printing works."""
|
||||
def setUp(self):
|
||||
self.jpxfile = glymur.data.jpxfile()
|
||||
self.jp2file = glymur.data.nemo()
|
||||
self.j2kfile = glymur.data.goodstuff()
|
||||
|
||||
|
|
@ -48,6 +49,31 @@ class TestPrinting(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||
def test_unknown_superbox(self):
|
||||
"""Verify that we can handle an unknown superbox."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
|
||||
with open(self.jpxfile, 'rb') as ifile:
|
||||
tfile.write(ifile.read())
|
||||
|
||||
# Add the header for an unknwon superbox.
|
||||
write_buffer = struct.pack('>I4s', 20, 'grp '.encode())
|
||||
tfile.write(write_buffer)
|
||||
write_buffer = struct.pack('>I4sI', 12, 'free'.encode(), 0)
|
||||
tfile.write(write_buffer)
|
||||
tfile.flush()
|
||||
|
||||
with self.assertWarns(UserWarning):
|
||||
jpx = Jp2k(tfile.name)
|
||||
glymur.set_printoptions(short=True)
|
||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||
print(jpx.box[-1])
|
||||
actual = fake_out.getvalue().strip()
|
||||
lines = ['Unknown Box (grp ) @ (695609, 20)',
|
||||
' Free Box (free) @ (695617, 12)']
|
||||
expected = '\n'.join(lines)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_printoptions_bad_argument(self):
|
||||
"""Verify error when bad parameter to set_printoptions"""
|
||||
with self.assertRaises(TypeError):
|
||||
|
|
@ -602,27 +628,7 @@ class TestPrinting(unittest.TestCase):
|
|||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||
print(j.box[2])
|
||||
actual = fake_out.getvalue().strip()
|
||||
|
||||
lines = ['XML Box (xml ) @ (36, 439)',
|
||||
' <ns0:IMAGE_CREATION '
|
||||
+ 'xmlns:ns0="http://www.jpeg.org/jpx/1.0/xml" '
|
||||
+ 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
||||
+ 'xsi:schemaLocation="http://www.jpeg.org/jpx/1.0/xml '
|
||||
+ 'http://www.jpeg.org/metadata/15444-2.xsd">',
|
||||
|
||||
' <ns0:GENERAL_CREATION_INFO>',
|
||||
' <ns0:CREATION_TIME>'
|
||||
+ '2001-11-01T13:45:00.000-06:00'
|
||||
+ '</ns0:CREATION_TIME>',
|
||||
|
||||
' <ns0:IMAGE_SOURCE>'
|
||||
+ 'Professional 120 Image'
|
||||
+ '</ns0:IMAGE_SOURCE>',
|
||||
|
||||
' </ns0:GENERAL_CREATION_INFO>',
|
||||
' </ns0:IMAGE_CREATION>']
|
||||
expected = '\n'.join(lines)
|
||||
self.assertEqual(actual, expected)
|
||||
self.assertEqual(actual, fixtures.file1_xml)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000,
|
||||
"Only trusting python3 for printing non-ascii chars")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue