Merge branch 'issue175' into devel

This commit is contained in:
jevans 2014-02-25 21:42:09 -05:00
commit 15358326da
10 changed files with 643 additions and 173 deletions

View file

@ -1,13 +1,14 @@
Feb 09, 2014 - Removed support for Python 2.6. Added write support for JP2 Feb 09, 2014 - Changed constructor for ChannelDefinition box. Removed support
UUID, DataEntryURL, Palette and Component Mapping boxes, JPX for Python 2.6. Added write support for JP2 UUID, DataEntryURL,
Association, NumberList and DataReference boxes. Added read Palette and Component Mapping boxes, JPX Association, NumberList
support for JPX free, number list, data reference, fragment and DataReference boxes. Added read support for JPX free,
table, and fragment list boxes. Improved JPX Reader Requirements box number list, data reference, fragment table, and fragment list
support. Added get_printoptions, set_printoptions functions. boxes. Improved JPX Reader Requirements box support. Added
Palette box now a 2D numpy array instead of a list of 1D arrays. get_printoptions, set_printoptions functions. Palette box now
JP2 super box constructors now take optional box list argument. a 2D numpy array instead of a list of 1D arrays. JP2 super box
Fixed bug where JPX files with more than one codestream but constructors now take optional box list argument. Fixed bug
advertising jp2 compatibility were not being read. 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 Jan 28, 2014 - v0.5.10 Fixed bad warning when reader requirements box mask
length is unsupported. length is unsupported.

View file

@ -11,6 +11,7 @@ ChangeLog
* added write support for JP2 UUID, dataEntryURL, palette, and component mapping boxes * 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/write support for JPX free, number list, and data reference boxes
* Added read support for JPX fragment list and fragment table 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 * 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) 0.5.0 (September 16, 2013)

View file

@ -359,7 +359,7 @@ channel, but we aren't doing that). ::
>>> from glymur.core import RED, GREEN, BLUE, WHOLE_IMAGE >>> from glymur.core import RED, GREEN, BLUE, WHOLE_IMAGE
>>> asoc = [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) >>> print(cdef)
Channel Definition Box (cdef) @ (0, 0) Channel Definition Box (cdef) @ (0, 0)
Channel 0 (color) ==> (1) Channel 0 (color) ==> (1)

View file

@ -146,7 +146,7 @@ class _Ifd(object):
tag_name = tagnum2name[tag] tag_name = tagnum2name[tag]
except KeyError: except KeyError:
# Ok, we don't recognize this tag. Just use the numeric id. # 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) warnings.warn(msg, UserWarning)
tag_name = tag tag_name = tag
self.processed_ifd[tag_name] = value self.processed_ifd[tag_name] = value

View file

@ -29,6 +29,7 @@ import numpy as np
from .codestream import Codestream from .codestream import Codestream
from .core import _COLORSPACE_MAP_DISPLAY from .core import _COLORSPACE_MAP_DISPLAY
from .core import _COLOR_TYPE_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 ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD
from .core import _pretty_print_xml from .core import _pretty_print_xml
@ -63,6 +64,8 @@ class Jp2kBox(object):
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.
box : list
List of JPEG 2000 boxes.
""" """
def __init__(self, box_id='', offset=0, length=0, longname=''): def __init__(self, box_id='', offset=0, length=0, longname=''):
@ -70,6 +73,7 @@ class Jp2kBox(object):
self.length = length self.length = length
self.offset = offset self.offset = offset
self.longname = longname self.longname = longname
self.box = []
def __repr__(self): def __repr__(self):
msg = "glymur.jp2box.Jp2kBox(box_id='{0}', offset={1}, length={2}, " 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) msg = "Not supported for {0} box.".format(self.longname)
raise NotImplementedError(msg) 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): def _write_superbox(self, fptr):
"""Write a superbox. """Write a superbox.
@ -108,6 +124,54 @@ class Jp2kBox(object):
fptr.write(struct.pack('>I', end_pos - orig_pos)) fptr.write(struct.pack('>I', end_pos - orig_pos))
fptr.seek(end_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): def parse_superbox(self, fptr):
"""Parse a superbox (box consisting of nothing but other boxes. """Parse a superbox (box consisting of nothing but other boxes.
@ -147,16 +211,10 @@ class Jp2kBox(object):
num_bytes, = struct.unpack('>Q', read_buffer) num_bytes, = struct.unpack('>Q', read_buffer)
else: else:
# The box_length value really is the length of the box!
num_bytes = box_length num_bytes = box_length
# Call the proper parser for the given box with ID "T". box = self._parse_this_box(fptr, box_id, start, num_bytes)
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')
superbox.append(box) superbox.append(box)
@ -213,13 +271,6 @@ class ColourSpecificationBox(Jp2kBox):
approximation=0, colorspace=None, icc_profile=None, approximation=0, colorspace=None, icc_profile=None,
length=0, offset=-1): length=0, offset=-1):
Jp2kBox.__init__(self, box_id='colr', longname='Colour Specification') 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.method = method
self.precedence = precedence self.precedence = precedence
self.approximation = approximation self.approximation = approximation
@ -227,6 +278,33 @@ class ColourSpecificationBox(Jp2kBox):
self.icc_profile = icc_profile self.icc_profile = icc_profile
self.length = length self.length = length
self.offset = offset 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): def __repr__(self):
msg = "glymur.jp2box.ColourSpecificationBox(" msg = "glymur.jp2box.ColourSpecificationBox("
@ -270,10 +348,7 @@ class ColourSpecificationBox(Jp2kBox):
def write(self, fptr): def write(self, fptr):
"""Write an Colour Specification box to file. """Write an Colour Specification box to file.
""" """
if self.colorspace is None: self._write_validate()
msg = "Writing Colour Specification boxes without enumerated "
msg += "colorspaces is not supported at this time."
raise NotImplementedError(msg)
length = 15 if self.icc_profile is None else 11 + len(self.icc_profile) length = 15 if self.icc_profile is None else 11 + len(self.icc_profile)
fptr.write(struct.pack('>I', length)) fptr.write(struct.pack('>I', length))
fptr.write('colr'.encode()) fptr.write('colr'.encode())
@ -469,23 +544,29 @@ class ChannelDefinitionBox(Jp2kBox):
association : list association : list
index of the associated color index of the associated color
""" """
def __init__(self, index=None, channel_type=None, association=None, def __init__(self, channel_type, association, index=None, **kwargs):
**kwargs):
Jp2kBox.__init__(self, box_id='cdef', longname='Channel Definition') 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: 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." msg = "Length of channel definition box inputs must be the same."
raise IOError(msg) raise IOError(msg)
# channel types must be one of 0, 1, 2, 65535 # 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 = "Channel types must be in the set of\n\n"
msg += " 0 - colour image data for associated color\n" msg += " 0 - colour image data for associated color\n"
msg += " 1 - opacity\n" msg += " 1 - opacity\n"
@ -493,10 +574,6 @@ class ChannelDefinitionBox(Jp2kBox):
msg += " 65535 - unspecified" msg += " 65535 - unspecified"
raise IOError(msg) raise IOError(msg)
self.index = tuple(index)
self.channel_type = tuple(channel_type)
self.association = tuple(association)
self.__dict__.update(**kwargs)
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) msg = Jp2kBox.__str__(self)
@ -522,6 +599,7 @@ class ChannelDefinitionBox(Jp2kBox):
def write(self, fptr): def write(self, fptr):
"""Write a channel definition box to file. """Write a channel definition box to file.
""" """
self._validate()
num_components = len(self.association) num_components = len(self.association)
fptr.write(struct.pack('>I', 8 + 2 + num_components * 6)) fptr.write(struct.pack('>I', 8 + 2 + num_components * 6))
fptr.write('cdef'.encode('utf-8')) fptr.write('cdef'.encode('utf-8'))
@ -559,9 +637,10 @@ class ChannelDefinitionBox(Jp2kBox):
channel_type = data[1:num_components * 6:3] channel_type = data[1:num_components * 6:3]
association = data[2:num_components * 6:3] association = data[2:num_components * 6:3]
box = ChannelDefinitionBox(index=index, channel_type=channel_type, box = ChannelDefinitionBox(index=tuple(index),
association=association, length=length, channel_type=tuple(channel_type),
offset=offset) association=tuple(association),
length=length, offset=offset)
return box return box
@ -592,16 +671,7 @@ class CodestreamHeaderBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) msg = self._str_superbox()
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)
return msg return msg
@staticmethod @staticmethod
@ -659,16 +729,7 @@ class CompositingLayerHeaderBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) msg = self._str_superbox()
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)
return msg return msg
@staticmethod @staticmethod
@ -738,7 +799,6 @@ class ComponentMappingBox(Jp2kBox):
if _printoptions['short'] == True: if _printoptions['short'] == True:
return msg return msg
for k in range(len(self.component_index)): for k in range(len(self.component_index)):
if self.mapping_type[k] == 1: if self.mapping_type[k] == 1:
msg += '\n Component {0} ==> palette column {1}' msg += '\n Component {0} ==> palette column {1}'
@ -831,10 +891,10 @@ class ContiguousCodestreamBox(Jp2kBox):
msg += '\n Main header:' msg += '\n Main header:'
for segment in self.main_header.segment: for segment in self.main_header.segment:
segstr = str(segment) segstr = str(segment)
# Add indentation. # Add indentation.
strs = [('\n ' + x) for x in segstr.split('\n')] strs = [('\n ' + x) for x in segstr.split('\n')]
msg += ''.join(strs) msg += ''.join(strs)
return msg return msg
@staticmethod @staticmethod
@ -876,15 +936,36 @@ class DataReferenceBox(Jp2kBox):
DR : list DR : list
Data Entry URL boxes. 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') 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.length = length
self.offset = offset 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): def write(self, fptr):
"""Write a Data Reference box to file. """Write a Data Reference box to file.
""" """
self._write_validate()
# Very similar to the say a superbox is written. # Very similar to the say a superbox is written.
orig_pos = fptr.tell() orig_pos = fptr.tell()
@ -980,7 +1061,6 @@ class FileTypeBox(Jp2kBox):
self.brand = brand self.brand = brand
self.minor_version = minor_version self.minor_version = minor_version
if compatibility_list is None: if compatibility_list is None:
# see W0102, pylint
self.compatibility_list = ['jp2 '] self.compatibility_list = ['jp2 ']
else: else:
self.compatibility_list = compatibility_list self.compatibility_list = compatibility_list
@ -1007,9 +1087,22 @@ class FileTypeBox(Jp2kBox):
return msg 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): def write(self, fptr):
"""Write a File Type box to file. """Write a File Type box to file.
""" """
self._validate()
length = 16 + 4*len(self.compatibility_list) length = 16 + 4*len(self.compatibility_list)
fptr.write(struct.pack('>I', length)) fptr.write(struct.pack('>I', length))
fptr.write('ftyp'.encode()) fptr.write('ftyp'.encode())
@ -1084,6 +1177,18 @@ class FragmentListBox(Jp2kBox):
self.length = length self.length = length
self.offset = offset 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): def __repr__(self):
msg = "glymur.jp2box.FragmentListBox()" msg = "glymur.jp2box.FragmentListBox()"
return msg return msg
@ -1103,6 +1208,22 @@ class FragmentListBox(Jp2kBox):
return msg 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 @staticmethod
def parse(fptr, offset, length): def parse(fptr, offset, length):
"""Parse JPX free box. """Parse JPX free box.
@ -1146,26 +1267,18 @@ class FragmentTableBox(Jp2kBox):
longname : str longname : str
more verbose description of the box. 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') Jp2kBox.__init__(self, box_id='ftbl', longname='Fragment Table')
self.length = length self.length = length
self.offset = offset self.offset = offset
self.box = box if box is not None else []
def __repr__(self): def __repr__(self):
msg = "glymur.jp2box.FragmentTableBox()" msg = "glymur.jp2box.FragmentTableBox()"
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) msg = self._str_superbox()
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)
return msg return msg
@staticmethod @staticmethod
@ -1183,7 +1296,7 @@ class FragmentTableBox(Jp2kBox):
Returns Returns
------- -------
FreeBox instance FragmentTableBox instance
""" """
box = FragmentTableBox(length=length, offset=offset) box = FragmentTableBox(length=length, offset=offset)
@ -1193,6 +1306,20 @@ class FragmentTableBox(Jp2kBox):
return box 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): class FreeBox(Jp2kBox):
@ -1415,16 +1542,7 @@ class AssociationBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) msg = self._str_superbox()
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)
return msg return msg
@staticmethod @staticmethod
@ -1485,13 +1603,7 @@ class JP2HeaderBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) msg = self._str_superbox()
for box in self.box:
boxstr = str(box)
# Add indentation.
strs = [('\n ' + x) for x in boxstr.split('\n')]
msg += ''.join(strs)
return msg return msg
def write(self, fptr): def write(self, fptr):
@ -1616,6 +1728,15 @@ class PaletteBox(Jp2kBox):
self.signed = signed self.signed = signed
self.length = length self.length = length
self.offset = offset 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): def __repr__(self):
msg = "glymur.jp2box.PaletteBox({0}, bits_per_component={1}, " msg = "glymur.jp2box.PaletteBox({0}, bits_per_component={1}, "
@ -1635,6 +1756,7 @@ class PaletteBox(Jp2kBox):
def write(self, fptr): def write(self, fptr):
"""Write a Palette box to file. """Write a Palette box to file.
""" """
self._validate()
bytes_per_row = sum(self.bits_per_component) / 8 bytes_per_row = sum(self.bits_per_component) / 8
bytes_per_palette = bytes_per_row * self.palette.shape[0] bytes_per_palette = bytes_per_row * self.palette.shape[0]
box_length = 8 + 3 + self.palette.shape[1] + bytes_per_palette 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, palette[j] = struct.unpack_from(fmt, read_buffer,
offset=j * row_nbytes) offset=j * row_nbytes)
box = PaletteBox(palette, bps, signed, length=length, offset=offset) return PaletteBox(palette, bps, signed, length=length, offset=offset)
return box
# Map rreq codes to display text. # Map rreq codes to display text.
@ -2086,13 +2207,7 @@ class ResolutionBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) msg = self._str_superbox()
for box in self.box:
boxstr = str(box)
# Add indentation.
strs = [('\n ' + x) for x in boxstr.split('\n')]
msg += ''.join(strs)
return msg return msg
@staticmethod @staticmethod
@ -2612,14 +2727,7 @@ class UUIDInfoBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) msg = self._str_superbox()
for box in self.box:
box_str = str(box)
# Add indentation.
lst = [('\n ' + x) for x in box_str.split('\n')]
msg += ''.join(lst)
return msg return msg
@staticmethod @staticmethod
@ -2680,13 +2788,18 @@ class DataEntryURLBox(Jp2kBox):
def write(self, fptr): def write(self, fptr):
"""Write a data entry url box to file. """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', write_buffer = struct.pack('>I4sBBBB',
length, self.box_id.encode(), length, self.box_id.encode(),
self.version, self.version,
self.flag[0], self.flag[1], self.flag[2]) self.flag[0], self.flag[1], self.flag[2])
fptr.write(write_buffer) fptr.write(write_buffer)
fptr.write(self.url.encode()) fptr.write(url.encode())
def __repr__(self): def __repr__(self):
@ -2739,6 +2852,37 @@ class DataEntryURLBox(Jp2kBox):
return box 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): class UUIDBox(Jp2kBox):
"""Container for UUID box information. """Container for UUID box information.

View file

@ -1110,6 +1110,10 @@ def _validate_nonzero_image_size(nrows, ncols, component_index):
raise IOError(msg) 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): def _validate_jp2_box_sequence(boxes):
"""Run through series of tests for JP2 box legality. """Run through series of tests for JP2 box legality.
@ -1118,14 +1122,24 @@ def _validate_jp2_box_sequence(boxes):
_validate_signature_compatibility(boxes) _validate_signature_compatibility(boxes)
_validate_jp2h(boxes) _validate_jp2h(boxes)
_validate_jp2c(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_label(boxes)
_validate_jpx_brand(boxes, boxes[1].brand) _validate_jpx_brand(boxes, boxes[1].brand)
_validate_jpx_compatibility(boxes, boxes[1].compatibility_list) _validate_jpx_compatibility(boxes, boxes[1].compatibility_list)
_validate_singletons(boxes) _validate_singletons(boxes)
_validate_top_level(boxes) _validate_top_level(boxes)
def _validate_signature_compatibility(boxes): def _validate_signature_compatibility(boxes):
"""Validate the file signature and compatibility status.""" """Validate the file signature and compatibility status."""
# Check for a bad sequence of boxes. # Check for a bad sequence of boxes.
@ -1268,6 +1282,12 @@ def _validate_top_level(boxes):
if 'dtbl' in multiples: if 'dtbl' in multiples:
raise IOError('There can only be one dtbl box in a file.') 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): def _validate_singletons(boxes):
"""Several boxes can only occur once.""" """Several boxes can only occur once."""
count = _collect_box_count(boxes) count = _collect_box_count(boxes)
@ -1321,23 +1341,6 @@ def _validate_label(boxes):
# Same set of checks on any child boxes. # Same set of checks on any child boxes.
_validate_label(box.box) _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): def extract_image_cube(image):
"""Extract 3D image from openjpeg data structure. """Extract 3D image from openjpeg data structure.
""" """

View file

@ -690,3 +690,11 @@ file7_rreq = r"""Reader Requirements Box (rreq) @ (44, 24)
Feature 060: 0x60 e-sRGB enumerated colorspace Feature 060: 0x60 e-sRGB enumerated colorspace
Feature 043: 0x40 Deprecated - compositing layer uses restricted ICC profile Feature 043: 0x40 Deprecated - compositing layer uses restricted ICC profile
Vendor Features:""" 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>"""

View file

@ -77,6 +77,28 @@ class TestDataEntryURL(unittest.TestCase):
self.assertEqual(jp22.box[4].flag, (0, 0, 0)) self.assertEqual(jp22.box[4].flag, (0, 0, 0))
self.assertEqual(jp22.box[4].url, url) 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 @unittest.skipIf(glymur.version.openjpeg_version_tuple[0] < 2 or
OPENJP2_IS_V2_OFFICIAL, OPENJP2_IS_V2_OFFICIAL,
@ -139,7 +161,7 @@ class TestChannelDefinition(unittest.TestCase):
def test_cdef_no_inputs(self): def test_cdef_no_inputs(self):
"""channel_type and association are required inputs.""" """channel_type and association are required inputs."""
with self.assertRaises(IOError): with self.assertRaises(TypeError):
glymur.jp2box.ChannelDefinitionBox() glymur.jp2box.ChannelDefinitionBox()
def test_rgb_with_index(self): def test_rgb_with_index(self):
@ -340,6 +362,29 @@ class TestChannelDefinition(unittest.TestCase):
association=association) 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): class TestColourSpecificationBox(unittest.TestCase):
"""Test suite for colr box instantiation.""" """Test suite for colr box instantiation."""
@ -371,7 +416,7 @@ class TestColourSpecificationBox(unittest.TestCase):
boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c] boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c]
boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)] boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(NotImplementedError): with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes) j2k.wrap(tfile.name, boxes=boxes)
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.") @unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
@ -417,6 +462,46 @@ class TestColourSpecificationBox(unittest.TestCase):
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace, glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
approximation=approx) 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): class TestAppend(unittest.TestCase):
"""Tests for append method.""" """Tests for append method."""

View file

@ -8,12 +8,12 @@ import struct
import sys import sys
import tempfile import tempfile
import unittest import unittest
import warnings
import xml.etree.cElementTree as ET import xml.etree.cElementTree as ET
import glymur import glymur
from glymur import Jp2k 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.") @unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
class TestJPXWrap(unittest.TestCase): class TestJPXWrap(unittest.TestCase):
@ -21,6 +21,7 @@ class TestJPXWrap(unittest.TestCase):
def setUp(self): def setUp(self):
self.jp2file = glymur.data.nemo() self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
raw_xml = b"""<?xml version="1.0"?> raw_xml = b"""<?xml version="1.0"?>
<data> <data>
@ -40,6 +41,100 @@ class TestJPXWrap(unittest.TestCase):
def tearDown(self): def tearDown(self):
os.unlink(self.xmlfile) 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): def test_jpxb_compatibility(self):
"""Wrap JP2 to JPX, state jpxb compatibility""" """Wrap JP2 to JPX, state jpxb compatibility"""
jp2 = Jp2k(self.jp2file) 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].box_id, 'lbl ')
self.assertEqual(jpx.box[-1].box[2].label, label) 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): def test_only_one_data_reference(self):
"""Data reference boxes cannot be inside a superbox .""" """Data reference boxes cannot be inside a superbox ."""
jp2 = Jp2k(self.jp2file) jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]] boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# Have to make the ftyp brand jpx.
boxes[1].brand = 'jpx '
flag = 0 flag = 0
version = (0, 0, 0) version = (0, 0, 0)
url = 'file:////usr/local/bin' url = 'file:////usr/local/bin'
@ -113,16 +244,16 @@ class TestJPXWrap(unittest.TestCase):
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile: with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError): with self.assertRaises(IOError):
jpx = jp2.wrap(tfile.name, boxes=boxes) jp2.wrap(tfile.name, boxes=boxes)
def test_lbl_at_top_level(self): def test_lbl_at_top_level(self):
"""Label boxes can only be inside a asoc box .""" """Label boxes can only be inside a asoc box ."""
jp2 = Jp2k(self.jp2file) jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]] boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
flag = 0 # Have to make the ftyp brand jpx.
version = (0, 0, 0) boxes[1].brand = 'jpx '
url = 'file:////usr/local/bin'
lblb = glymur.jp2box.LabelBox('hi there') lblb = glymur.jp2box.LabelBox('hi there')
# Put it inside the jp2 header box. # Put it inside the jp2 header box.
@ -130,13 +261,16 @@ class TestJPXWrap(unittest.TestCase):
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile: with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError): 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 .""" """Data reference boxes cannot be inside a superbox ."""
jp2 = Jp2k(self.jp2file) jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]] boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]]
# Have to make the ftyp brand jpx.
boxes[1].brand = 'jpx '
flag = 0 flag = 0
version = (0, 0, 0) version = (0, 0, 0)
url = 'file:////usr/local/bin' url = 'file:////usr/local/bin'
@ -148,13 +282,17 @@ class TestJPXWrap(unittest.TestCase):
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile: with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(IOError): 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): def test_jp2_to_jpx_sans_jp2_compatibility(self):
"""jp2 wrapped to jpx not including jp2 compatibility is wrong.""" """jp2 wrapped to jpx not including jp2 compatibility is wrong."""
jp2 = Jp2k(self.jp2file) jp2 = Jp2k(self.jp2file)
boxes = [jp2.box[idx] for idx in [0, 1, 2, 4]] 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 ') boxes[1].compatibility_list.append('jp2 ')
numbers = [0, 1] numbers = [0, 1]
nlst = glymur.jp2box.NumberListBox(numbers) nlst = glymur.jp2box.NumberListBox(numbers)
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>') 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 tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(RuntimeError): 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): def test_jp2_to_jpx_sans_jpx_brand(self):
"""Verify error when jp2 wrapped to jpx does not include jpx brand.""" """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 tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
with self.assertRaises(RuntimeError): 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.") @unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
@ -188,11 +326,57 @@ class TestJPX(unittest.TestCase):
"""Test suite for other JPX boxes.""" """Test suite for other JPX boxes."""
def setUp(self): def setUp(self):
self.jp2file = glymur.data.nemo()
self.jpxfile = glymur.data.jpxfile() self.jpxfile = glymur.data.jpxfile()
def tearDown(self): def tearDown(self):
pass 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): def test_jpx_rreq_mask_length_3(self):
"""There are some JPX files with rreq mask length of 3.""" """There are some JPX files with rreq mask length of 3."""
jpx = Jp2k(self.jpxfile) jpx = Jp2k(self.jpxfile)
@ -202,12 +386,50 @@ class TestJPX(unittest.TestCase):
self.assertEqual(jpx.box[2].standard_flag, self.assertEqual(jpx.box[2].standard_flag,
(5, 42, 45, 2, 18, 19, 1, 8, 12, 31, 20)) (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): def test_free_box(self):
"""Verify that we can handle a free box.""" """Verify that we can handle a free box."""
j = Jp2k(self.jpxfile) j = Jp2k(self.jpxfile)
self.assertEqual(j.box[16].box[0].box_id, 'free') self.assertEqual(j.box[16].box[0].box_id, 'free')
self.assertEqual(type(j.box[16].box[0]), glymur.jp2box.FreeBox) 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): def test_dtbl(self):
"""Verify that we can interpret Data Reference boxes.""" """Verify that we can interpret Data Reference boxes."""
# Copy the existing JPX file, add a data reference box onto the end. # Copy the existing JPX file, add a data reference box onto the end.

View file

@ -39,6 +39,7 @@ from .fixtures import text_gbr_27, text_gbr_33, text_gbr_34
class TestPrinting(unittest.TestCase): class TestPrinting(unittest.TestCase):
"""Tests for verifying how printing works.""" """Tests for verifying how printing works."""
def setUp(self): def setUp(self):
self.jpxfile = glymur.data.jpxfile()
self.jp2file = glymur.data.nemo() self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff() self.j2kfile = glymur.data.goodstuff()
@ -48,6 +49,31 @@ class TestPrinting(unittest.TestCase):
def tearDown(self): def tearDown(self):
pass 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): def test_printoptions_bad_argument(self):
"""Verify error when bad parameter to set_printoptions""" """Verify error when bad parameter to set_printoptions"""
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
@ -602,27 +628,7 @@ class TestPrinting(unittest.TestCase):
with patch('sys.stdout', new=StringIO()) as fake_out: with patch('sys.stdout', new=StringIO()) as fake_out:
print(j.box[2]) print(j.box[2])
actual = fake_out.getvalue().strip() actual = fake_out.getvalue().strip()
self.assertEqual(actual, fixtures.file1_xml)
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)
@unittest.skipIf(sys.hexversion < 0x03000000, @unittest.skipIf(sys.hexversion < 0x03000000,
"Only trusting python3 for printing non-ascii chars") "Only trusting python3 for printing non-ascii chars")