Merge branch 'devel' into issue210
This commit is contained in:
commit
9b533b72e3
17 changed files with 912 additions and 423 deletions
|
|
@ -43,7 +43,7 @@ def tiff_header(read_buffer):
|
|||
endian = '>'
|
||||
else:
|
||||
msg = "Bad byte order indication: {0}".format(read_buffer[6:8])
|
||||
raise RuntimeError(msg)
|
||||
raise IOError(msg)
|
||||
|
||||
_, offset = struct.unpack(endian + 'HI', read_buffer[8:14])
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ codestreams.
|
|||
# the base Segment class.
|
||||
# pylint: disable=R0903
|
||||
|
||||
import collections
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
|
|
@ -28,33 +27,36 @@ import numpy as np
|
|||
from .core import LRCP, RLCP, RPCL, PCRL, CPRL
|
||||
from .core import WAVELET_XFORM_9X7_IRREVERSIBLE
|
||||
from .core import WAVELET_XFORM_5X3_REVERSIBLE
|
||||
from .core import _CAPABILITIES_DISPLAY
|
||||
from .core import _Keydefaultdict
|
||||
from .lib import openjp2 as opj2
|
||||
|
||||
class _keydefaultdict(collections.defaultdict):
|
||||
"""Unlisted keys help form their own error message.
|
||||
|
||||
Normally defaultdict uses a factory function with no input arguments, but
|
||||
that's not quite the behavior we want.
|
||||
"""
|
||||
def __missing__(self, key):
|
||||
if self.default_factory is None:
|
||||
raise KeyError(key)
|
||||
else:
|
||||
ret = self[key] = self.default_factory(key)
|
||||
return ret
|
||||
|
||||
_factory = lambda x: '{0} (invalid)'.format(x)
|
||||
_PROGRESSION_ORDER_DISPLAY = _keydefaultdict(_factory,
|
||||
_PROGRESSION_ORDER_DISPLAY = _Keydefaultdict(_factory,
|
||||
{ LRCP: 'LRCP',
|
||||
RLCP: 'RLCP',
|
||||
RPCL: 'RPCL',
|
||||
PCRL: 'PCRL',
|
||||
CPRL: 'CPRL'})
|
||||
|
||||
_WAVELET_TRANSFORM_DISPLAY = {
|
||||
WAVELET_XFORM_9X7_IRREVERSIBLE: '9-7 irreversible',
|
||||
WAVELET_XFORM_5X3_REVERSIBLE: '5-3 reversible'}
|
||||
_WAVELET_TRANSFORM_DISPLAY = _Keydefaultdict(_factory,
|
||||
{ WAVELET_XFORM_9X7_IRREVERSIBLE: '9-7 irreversible',
|
||||
WAVELET_XFORM_5X3_REVERSIBLE: '5-3 reversible'})
|
||||
|
||||
_NO_PROFILE = 0
|
||||
_PROFILE_0 = 1
|
||||
_PROFILE_1 = 2
|
||||
_PROFILE_3 = 3
|
||||
_PROFILE_4 = 4
|
||||
|
||||
_KNOWN_PROFILES = [_NO_PROFILE, _PROFILE_0, _PROFILE_1, _PROFILE_3, _PROFILE_4]
|
||||
|
||||
# How to display the codestream profile.
|
||||
_CAPABILITIES_DISPLAY = _Keydefaultdict(_factory,
|
||||
{ _NO_PROFILE: 'no profile',
|
||||
_PROFILE_0: '0',
|
||||
_PROFILE_1: '1',
|
||||
_PROFILE_3: 'Cinema 2K',
|
||||
_PROFILE_4: 'Cinema 4K'} )
|
||||
|
||||
# Need a catch-all list of valid markers.
|
||||
# See table A-1 in ISO/IEC FCD15444-1.
|
||||
|
|
@ -390,6 +392,11 @@ class Codestream(object):
|
|||
msg = "Invalid progression order in COD segment: {0}."
|
||||
warnings.warn(msg.format(spcod[0]))
|
||||
|
||||
if spcod[8] not in [WAVELET_XFORM_9X7_IRREVERSIBLE,
|
||||
WAVELET_XFORM_5X3_REVERSIBLE]:
|
||||
msg = "Invalid wavelet transform in COD segment: {0}."
|
||||
warnings.warn(msg.format(spcod[8]))
|
||||
|
||||
sop = (scod & 2) > 0
|
||||
eph = (scod & 4) > 0
|
||||
|
||||
|
|
@ -667,6 +674,9 @@ class Codestream(object):
|
|||
data = struct.unpack('>HIIIIIIIIH', xy_buffer)
|
||||
|
||||
rsiz = data[0]
|
||||
if rsiz not in _KNOWN_PROFILES:
|
||||
warnings.warn("Invalid profile: (Rsiz={0}).".format(rsiz))
|
||||
|
||||
xysiz = (data[1], data[2])
|
||||
xyosiz = (data[3], data[4])
|
||||
xytsiz = (data[5], data[6])
|
||||
|
|
|
|||
|
|
@ -1,8 +1,22 @@
|
|||
"""Core definitions to be shared amongst the modules.
|
||||
"""
|
||||
import collections
|
||||
import copy
|
||||
import lxml.etree as ET
|
||||
|
||||
class _Keydefaultdict(collections.defaultdict):
|
||||
"""Unlisted keys help form their own error message.
|
||||
|
||||
Normally defaultdict uses a factory function with no input arguments, but
|
||||
that's not quite the behavior we want.
|
||||
"""
|
||||
def __missing__(self, key):
|
||||
if self.default_factory is None:
|
||||
raise KeyError(key)
|
||||
else:
|
||||
ret = self[key] = self.default_factory(key)
|
||||
return ret
|
||||
|
||||
# Progression order
|
||||
LRCP = 0
|
||||
RLCP = 1
|
||||
|
|
@ -57,24 +71,26 @@ YCC = 18
|
|||
E_SRGB = 20
|
||||
ROMM_RGB = 21
|
||||
|
||||
_COLORSPACE_MAP_DISPLAY = {
|
||||
CMYK: 'CMYK',
|
||||
SRGB: 'sRGB',
|
||||
GREYSCALE: 'greyscale',
|
||||
YCC: 'YCC',
|
||||
E_SRGB: 'e-sRGB',
|
||||
ROMM_RGB: 'ROMM-RGB'}
|
||||
_factory = lambda x: '{0} (unrecognized)'.format(x)
|
||||
_COLORSPACE_MAP_DISPLAY = _Keydefaultdict(_factory,
|
||||
{ CMYK: 'CMYK',
|
||||
SRGB: 'sRGB',
|
||||
GREYSCALE: 'greyscale',
|
||||
YCC: 'YCC',
|
||||
E_SRGB: 'e-sRGB',
|
||||
ROMM_RGB: 'ROMM-RGB'} )
|
||||
|
||||
# enumerated color channel types
|
||||
COLOR = 0
|
||||
OPACITY = 1
|
||||
PRE_MULTIPLIED_OPACITY = 2
|
||||
_UNSPECIFIED = 65535
|
||||
_COLOR_TYPE_MAP_DISPLAY = {
|
||||
COLOR: 'color',
|
||||
OPACITY: 'opacity',
|
||||
PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity',
|
||||
_UNSPECIFIED: 'unspecified'}
|
||||
_factory = lambda x: '{0} (invalid)'.format(x)
|
||||
_COLOR_TYPE_MAP_DISPLAY = _Keydefaultdict(_factory,
|
||||
{ COLOR: 'color',
|
||||
OPACITY: 'opacity',
|
||||
PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity',
|
||||
_UNSPECIFIED: 'unspecified'})
|
||||
|
||||
# color channel definitions.
|
||||
RED = 1
|
||||
|
|
@ -90,9 +106,3 @@ _COLORSPACE = {SRGB: {"R": 1, "G": 2, "B": 3},
|
|||
E_SRGB: {"R": 1, "G": 2, "B": 3},
|
||||
ROMM_RGB: {"R": 1, "G": 2, "B": 3}}
|
||||
|
||||
# How to display the codestream profile.
|
||||
_CAPABILITIES_DISPLAY = {
|
||||
0: '2',
|
||||
1: '0',
|
||||
2: '1',
|
||||
3: '3'}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -34,13 +34,13 @@ def goodstuff():
|
|||
|
||||
|
||||
def jpxfile():
|
||||
"""Shortcut for specifying path to 12-v6.4.jpx.
|
||||
"""Shortcut for specifying path to heliov.jpx.
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : str
|
||||
Platform-independent path to 12-v6.4.jpx
|
||||
"""
|
||||
filename = pkg_resources.resource_filename(__name__, "12-v6.4.jpx")
|
||||
filename = pkg_resources.resource_filename(__name__, "heliov.jpx")
|
||||
return filename
|
||||
|
||||
|
|
|
|||
BIN
glymur/data/heliov.jpx
Normal file
BIN
glymur/data/heliov.jpx
Normal file
Binary file not shown.
582
glymur/jp2box.py
582
glymur/jp2box.py
File diff suppressed because it is too large
Load diff
200
glymur/jp2k.py
200
glymur/jp2k.py
|
|
@ -29,7 +29,7 @@ import numpy as np
|
|||
|
||||
from .codestream import Codestream
|
||||
from .core import SRGB, GREYSCALE
|
||||
from .core import PROGRESSION_ORDER, RSIZ, CINEMA_MODE
|
||||
from .core import PROGRESSION_ORDER, CINEMA_MODE
|
||||
from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
|
||||
from .jp2box import Jp2kBox
|
||||
from .jp2box import JPEG2000SignatureBox, FileTypeBox, JP2HeaderBox
|
||||
|
|
@ -442,7 +442,7 @@ class Jp2k(Jp2kBox):
|
|||
If glymur is unable to load the openjp2 library.
|
||||
"""
|
||||
if opj2.OPENJP2 is not None:
|
||||
self._write_openjp2(img_array, verbose=verbose, **kwargs)
|
||||
self._write_openjp2(img_array, verbose=verbose, **kwargs)
|
||||
elif opj.OPENJPEG is not None:
|
||||
self._write_openjpeg(img_array, verbose=verbose, **kwargs)
|
||||
else:
|
||||
|
|
@ -606,7 +606,12 @@ class Jp2k(Jp2kBox):
|
|||
self.parse()
|
||||
|
||||
def wrap(self, filename, boxes=None):
|
||||
"""Write the codestream back out to file, wrapped in new JP2 jacket.
|
||||
"""Create a new JP2/JPX file wrapped in a new set of JP2 boxes.
|
||||
|
||||
This method is primarily aimed at wrapping a raw codestream in a set of
|
||||
of JP2 boxes (turning it into a JP2 file instead of just a raw
|
||||
codestream), or rewrapping a codestream in a JP2 file in a new "jacket"
|
||||
of JP2 boxes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -616,6 +621,8 @@ class Jp2k(Jp2kBox):
|
|||
JP2 box definitions to define the JP2 file format. If not
|
||||
provided, a default ""jacket" is assumed, consisting of JP2
|
||||
signature, file type, JP2 header, and contiguous codestream boxes.
|
||||
A JPX file rewrapped without the boxes argument results in a JP2
|
||||
file encompassing the first codestream.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -631,19 +638,7 @@ class Jp2k(Jp2kBox):
|
|||
>>> jp2 = j2k.wrap(tfile.name)
|
||||
"""
|
||||
if boxes is None:
|
||||
# Try to create a reasonable default.
|
||||
boxes = [JPEG2000SignatureBox(),
|
||||
FileTypeBox(),
|
||||
JP2HeaderBox(),
|
||||
ContiguousCodestreamBox()]
|
||||
codestream = self.get_codestream()
|
||||
height = codestream.segment[1].ysiz
|
||||
width = codestream.segment[1].xsiz
|
||||
num_components = len(codestream.segment[1].xrsiz)
|
||||
boxes[2].box = [ImageHeaderBox(height=height,
|
||||
width=width,
|
||||
num_components=num_components),
|
||||
ColourSpecificationBox(colorspace=SRGB)]
|
||||
boxes = self._get_default_jp2_boxes()
|
||||
|
||||
_validate_jp2_box_sequence(boxes)
|
||||
|
||||
|
|
@ -652,34 +647,92 @@ class Jp2k(Jp2kBox):
|
|||
if box.box_id != 'jp2c':
|
||||
box.write(ofile)
|
||||
else:
|
||||
# The codestream gets written last.
|
||||
if len(self.box) == 0:
|
||||
# Am I a raw codestream? If so, then it is pretty
|
||||
# easy, just write the codestream box header plus all
|
||||
# of myself out to file.
|
||||
ofile.write(struct.pack('>I', self.length + 8))
|
||||
ofile.write('jp2c'.encode())
|
||||
with open(self.filename, 'rb') as ifile:
|
||||
ofile.write(ifile.read())
|
||||
else:
|
||||
# OK, I'm a jp2 file. Need to find out where the
|
||||
# raw codestream actually starts.
|
||||
jp2c = [box for box in self.box
|
||||
if box.box_id == 'jp2c']
|
||||
jp2c = jp2c[0]
|
||||
ofile.write(struct.pack('>I', jp2c.length))
|
||||
ofile.write('jp2c'.encode())
|
||||
with open(self.filename, 'rb') as ifile:
|
||||
# Seek 8 bytes past the L, T fields to get to the
|
||||
# raw codestream.
|
||||
ifile.seek(jp2c.offset + 8)
|
||||
ofile.write(ifile.read(jp2c.length - 8))
|
||||
|
||||
self._write_wrapped_codestream(ofile, box)
|
||||
ofile.flush()
|
||||
|
||||
jp2 = Jp2k(filename)
|
||||
return jp2
|
||||
|
||||
def _write_wrapped_codestream(self, ofile, box):
|
||||
"""Write wrapped codestream."""
|
||||
# Codestreams require a bit more care.
|
||||
# Am I a raw codestream?
|
||||
if len(self.box) == 0:
|
||||
# Yes, just write the codestream box header plus all
|
||||
# of myself out to file.
|
||||
ofile.write(struct.pack('>I', self.length + 8))
|
||||
ofile.write(b'jp2c')
|
||||
with open(self.filename, 'rb') as ifile:
|
||||
ofile.write(ifile.read())
|
||||
return
|
||||
|
||||
# OK, I'm a jp2/jpx file. Need to find out where the raw codestream
|
||||
# actually starts.
|
||||
offset = box.offset
|
||||
if offset == -1:
|
||||
if self.box[1].brand == 'jpx ':
|
||||
msg = "The codestream box must have its offset and "
|
||||
msg += "length attributes fully specified if the file "
|
||||
msg += "type brand is JPX."
|
||||
raise IOError(msg)
|
||||
|
||||
# Find the first codestream in the file.
|
||||
jp2c = [box for box in self.box if box.box_id == 'jp2c']
|
||||
offset = jp2c[0].offset
|
||||
|
||||
# Ready to write the codestream.
|
||||
with open(self.filename, 'rb') as ifile:
|
||||
ifile.seek(offset)
|
||||
|
||||
# Verify that the specified codestream is right.
|
||||
read_buffer = ifile.read(8)
|
||||
L, T = struct.unpack_from('>I4s', read_buffer, 0)
|
||||
if T != b'jp2c':
|
||||
msg = "Unable to locate the specified codestream."
|
||||
raise IOError(msg)
|
||||
if L == 0:
|
||||
# The length of the box is presumed to last until the end of
|
||||
# the file. Compute the effective length of the box.
|
||||
L = os.path.getsize(ifile.name) - ifile.tell() + 8
|
||||
|
||||
elif L == 1:
|
||||
# The length of the box is in the XL field, a 64-bit value.
|
||||
read_buffer = ifile.read(8)
|
||||
L, = struct.unpack('>Q', read_buffer)
|
||||
|
||||
ifile.seek(offset)
|
||||
read_buffer = ifile.read(L)
|
||||
ofile.write(read_buffer)
|
||||
|
||||
def _get_default_jp2_boxes(self):
|
||||
"""Create a default set of JP2 boxes."""
|
||||
# Try to create a reasonable default.
|
||||
boxes = [JPEG2000SignatureBox(),
|
||||
FileTypeBox(),
|
||||
JP2HeaderBox(),
|
||||
ContiguousCodestreamBox()]
|
||||
codestream = self.get_codestream()
|
||||
height = codestream.segment[1].ysiz
|
||||
width = codestream.segment[1].xsiz
|
||||
num_components = len(codestream.segment[1].xrsiz)
|
||||
if num_components < 3:
|
||||
colorspace = GREYSCALE
|
||||
else:
|
||||
if len(self.box) == 0:
|
||||
# Best guess is SRGB
|
||||
colorspace = SRGB
|
||||
else:
|
||||
# Take whatever the first jp2 header / color specification
|
||||
# says.
|
||||
jp2hs = [box for box in self.box if box.box_id == 'jp2h']
|
||||
colorspace = jp2hs[0].box[1].colorspace
|
||||
|
||||
boxes[2].box = [ImageHeaderBox(height=height, width=width,
|
||||
num_components=num_components),
|
||||
ColourSpecificationBox(colorspace=colorspace)]
|
||||
|
||||
return boxes
|
||||
|
||||
def read(self, **kwargs):
|
||||
"""Read a JPEG 2000 image.
|
||||
|
||||
|
|
@ -696,8 +749,9 @@ class Jp2k(Jp2kBox):
|
|||
(first_row, first_col, last_row, last_col)
|
||||
tile : int, optional
|
||||
Number of tile to decode.
|
||||
no_cxform : bool
|
||||
Whether or not to apply intended color transforms.
|
||||
ignore_pclr_cmap_cdef : bool
|
||||
Whether or not to ignore the pclr, cmap, or cdef boxes during any
|
||||
color transformation. Defaults to False.
|
||||
verbose : bool, optional
|
||||
Print informational messages produced by the OpenJPEG library.
|
||||
|
||||
|
|
@ -752,7 +806,8 @@ class Jp2k(Jp2kBox):
|
|||
msg += "the read_bands method instead."
|
||||
raise RuntimeError(msg)
|
||||
|
||||
def _read_openjpeg(self, rlevel=0, no_cxform=False, verbose=False):
|
||||
def _read_openjpeg(self, rlevel=0, ignore_pclr_cmap_cdef=False,
|
||||
verbose=False):
|
||||
"""Read a JPEG 2000 image using libopenjpeg.
|
||||
|
||||
Parameters
|
||||
|
|
@ -760,8 +815,9 @@ class Jp2k(Jp2kBox):
|
|||
rlevel : int, optional
|
||||
Factor by which to rlevel output resolution. Use -1 to get the
|
||||
lowest resolution thumbnail.
|
||||
no_cxform : bool
|
||||
Whether or not to apply intended color transforms.
|
||||
ignore_pclr_cmap_cdef : bool
|
||||
Whether or not to ignore the pclr, cmap, or cdef boxes during any
|
||||
color transformation. Defaults to False.
|
||||
verbose : bool, optional
|
||||
Print informational messages produced by the OpenJPEG library.
|
||||
|
||||
|
|
@ -786,9 +842,9 @@ class Jp2k(Jp2kBox):
|
|||
# -1 is shorthand for the largest rlevel
|
||||
rlevel = max_rlevel
|
||||
elif rlevel < -1 or rlevel > max_rlevel:
|
||||
msg = "rlevel must be in the range [-1, {0}] for this image."
|
||||
msg = msg.format(max_rlevel)
|
||||
raise IOError(msg)
|
||||
msg = "rlevel must be in the range [-1, {0}] for this image."
|
||||
msg = msg.format(max_rlevel)
|
||||
raise IOError(msg)
|
||||
|
||||
with ExitStack() as stack:
|
||||
try:
|
||||
|
|
@ -797,7 +853,7 @@ class Jp2k(Jp2kBox):
|
|||
dparameters = opj.DecompressionParametersType()
|
||||
opj.set_default_decoder_parameters(ctypes.byref(dparameters))
|
||||
|
||||
if no_cxform is True:
|
||||
if ignore_pclr_cmap_cdef is True:
|
||||
# Return raw codestream components.
|
||||
dparameters.flags |= 1
|
||||
|
||||
|
|
@ -845,7 +901,7 @@ class Jp2k(Jp2kBox):
|
|||
return data
|
||||
|
||||
def _read_openjp2(self, rlevel=0, layer=0, area=None, tile=None,
|
||||
verbose=False, no_cxform=False):
|
||||
verbose=False, ignore_pclr_cmap_cdef=False):
|
||||
"""Read a JPEG 2000 image using libopenjp2.
|
||||
|
||||
Parameters
|
||||
|
|
@ -875,7 +931,8 @@ class Jp2k(Jp2kBox):
|
|||
"""
|
||||
self._subsampling_sanity_check()
|
||||
|
||||
dparam = self._populate_dparam(layer, rlevel, area, tile, no_cxform)
|
||||
dparam = self._populate_dparam(layer, rlevel, area, tile,
|
||||
ignore_pclr_cmap_cdef)
|
||||
|
||||
with ExitStack() as stack:
|
||||
if hasattr(opj2.OPENJP2,
|
||||
|
|
@ -919,7 +976,8 @@ class Jp2k(Jp2kBox):
|
|||
|
||||
return img_array
|
||||
|
||||
def _populate_dparam(self, layer, rlevel, area, tile, no_cxform):
|
||||
def _populate_dparam(self, layer, rlevel, area, tile,
|
||||
ignore_pclr_cmap_cdef):
|
||||
"""Populate decompression structure with appropriate input parameters.
|
||||
|
||||
Parameters
|
||||
|
|
@ -933,8 +991,9 @@ class Jp2k(Jp2kBox):
|
|||
(first_row, first_col, last_row, last_col)
|
||||
tile : int
|
||||
Number of tile to decode.
|
||||
no_cxform : bool
|
||||
Whether or not to apply intended color transforms.
|
||||
ignore_pclr_cmap_cdef : bool
|
||||
Whether or not to ignore the pclr, cmap, or cdef boxes during any
|
||||
color transformation. Defaults to False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -972,14 +1031,14 @@ class Jp2k(Jp2kBox):
|
|||
dparam.tile_index = tile
|
||||
dparam.nb_tile_to_decode = 1
|
||||
|
||||
if no_cxform is True:
|
||||
if ignore_pclr_cmap_cdef is True:
|
||||
# Return raw codestream components.
|
||||
dparam.flags |= 1
|
||||
|
||||
return dparam
|
||||
|
||||
def read_bands(self, rlevel=0, layer=0, area=None, tile=None,
|
||||
verbose=False, no_cxform=False):
|
||||
verbose=False, ignore_pclr_cmap_cdef=False):
|
||||
"""Read a JPEG 2000 image.
|
||||
|
||||
The only time you should use this method is when the image has
|
||||
|
|
@ -997,8 +1056,9 @@ class Jp2k(Jp2kBox):
|
|||
(first_row, first_col, last_row, last_col)
|
||||
tile : int, optional
|
||||
Number of tile to decode.
|
||||
no_cxform : bool
|
||||
Whether or not to apply intended color transforms.
|
||||
ignore_pclr_cmap_cdef : bool
|
||||
Whether or not to ignore the pclr, cmap, or cdef boxes during any
|
||||
color transformation. Defaults to False.
|
||||
verbose : bool, optional
|
||||
Print informational messages produced by the OpenJPEG library.
|
||||
|
||||
|
|
@ -1028,7 +1088,7 @@ class Jp2k(Jp2kBox):
|
|||
"of OpenJP2 installed before using "
|
||||
"this functionality.")
|
||||
|
||||
dparam = self._populate_dparam(layer, rlevel, area, tile, no_cxform)
|
||||
dparam = self._populate_dparam(layer, rlevel, area, tile, ignore_pclr_cmap_cdef)
|
||||
|
||||
with ExitStack() as stack:
|
||||
if hasattr(opj2.OPENJP2,
|
||||
|
|
@ -1090,7 +1150,7 @@ class Jp2k(Jp2kBox):
|
|||
>>> codestream = jp2.get_codestream()
|
||||
>>> print(codestream.segment[1])
|
||||
SIZ marker segment @ (3233, 47)
|
||||
Profile: 2
|
||||
Profile: no profile
|
||||
Reference Grid Height, Width: (1456 x 2592)
|
||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||
Reference Tile Height, Width: (1456 x 2592)
|
||||
|
|
@ -1190,12 +1250,26 @@ def _validate_jp2_box_sequence(boxes):
|
|||
if boxes[1].brand == 'jpx ':
|
||||
_validate_jpx_box_sequence(boxes)
|
||||
else:
|
||||
# Validate the JP2 box IDs.
|
||||
count = _collect_box_count(boxes)
|
||||
for id in count.keys():
|
||||
if id not in JP2_IDS:
|
||||
for box_id in count.keys():
|
||||
if box_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))
|
||||
raise IOError(msg.format(box_id))
|
||||
|
||||
_validate_jp2_colr(boxes)
|
||||
|
||||
def _validate_jp2_colr(boxes):
|
||||
"""
|
||||
Validate JP2 requirements on colour specification boxes.
|
||||
"""
|
||||
lst = [box for box in boxes if box.box_id == 'jp2h']
|
||||
jp2h = lst[0]
|
||||
for colr in [box for box in jp2h.box if box.box_id == 'colr']:
|
||||
if colr.approximation != 0:
|
||||
msg = "A JP2 colr box cannot have a non-zero approximation field."
|
||||
raise IOError(msg)
|
||||
|
||||
def _validate_jpx_box_sequence(boxes):
|
||||
"""Run through series of tests for JPX box legality."""
|
||||
|
|
@ -1296,7 +1370,7 @@ def _check_jp2h_child_boxes(boxes, parent_box_name):
|
|||
"""Certain boxes can only reside in the JP2 header."""
|
||||
box_ids = set([box.box_id for box in boxes])
|
||||
intersection = box_ids.intersection(JP2H_CHILDREN)
|
||||
if len(intersection) > 0 and parent_box_name != 'jp2h':
|
||||
if len(intersection) > 0 and parent_box_name not in ['jp2h', 'jpch']:
|
||||
msg = "A '{0}' box can only be nested in a JP2 header box."
|
||||
raise IOError(msg.format(list(intersection)[0]))
|
||||
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ Contiguous Codestream Box (jp2c) @ (3223, 1132296)
|
|||
Main header:
|
||||
SOC marker segment @ (3231, 0)
|
||||
SIZ marker segment @ (3233, 47)
|
||||
Profile: 2
|
||||
Profile: no profile
|
||||
Reference Grid Height, Width: (1456 x 2592)
|
||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||
Reference Tile Height, Width: (1456 x 2592)
|
||||
|
|
@ -477,7 +477,7 @@ Contiguous Codestream Box (jp2c) @ (3223, 1132296)
|
|||
Main header:
|
||||
SOC marker segment @ (3231, 0)
|
||||
SIZ marker segment @ (3233, 47)
|
||||
Profile: 2
|
||||
Profile: no profile
|
||||
Reference Grid Height, Width: (1456 x 2592)
|
||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||
Reference Tile Height, Width: (1456 x 2592)
|
||||
|
|
@ -580,8 +580,9 @@ file1_xml = """XML Box (xml ) @ (36, 439)
|
|||
|
||||
issue_182_cmap = """Component Mapping Box (cmap) @ (130, 24)
|
||||
Component 0 ==> palette column 0
|
||||
Component 1 ==> palette column 0
|
||||
Component 2 ==> 2"""
|
||||
Component 0 ==> palette column 1
|
||||
Component 0 ==> palette column 2
|
||||
Component 0 ==> palette column 3"""
|
||||
|
||||
issue_183_colr = """Colour Specification Box (colr) @ (62, 12)
|
||||
Method: restricted ICC profile
|
||||
|
|
@ -610,3 +611,14 @@ issue_186_progression_order = """COD marker segment @ (174, 12)
|
|||
Vertically stripe causal context: False
|
||||
Predictable termination: False
|
||||
Segmentation symbols: False"""
|
||||
|
||||
# Cinema 2K profile
|
||||
cinema2k_profile = """SIZ marker segment @ (2, 47)
|
||||
Profile: Cinema 2K
|
||||
Reference Grid Height, Width: (1080 x 1920)
|
||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||
Reference Tile Height, Width: (1080 x 1920)
|
||||
Vertical, Horizontal Reference Tile Offset: (0 x 0)
|
||||
Bitdepth: (12, 12, 12)
|
||||
Signed: (False, False, False)
|
||||
Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1))"""
|
||||
|
|
|
|||
|
|
@ -51,6 +51,28 @@ class TestCodestreamOpjData(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_bad_rsiz(self):
|
||||
"""Should warn if RSIZ is bad. Issue196"""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_1002767.jp2')
|
||||
if sys.hexversion < 0x03000000:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
j = Jp2k(filename)
|
||||
else:
|
||||
with self.assertWarns(UserWarning):
|
||||
j = Jp2k(filename)
|
||||
|
||||
def test_bad_wavelet_transform(self):
|
||||
"""Should warn if wavelet transform is bad. Issue195"""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_10025.jp2')
|
||||
if sys.hexversion < 0x03000000:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
j = Jp2k(filename)
|
||||
else:
|
||||
with self.assertWarns(UserWarning):
|
||||
j = Jp2k(filename)
|
||||
|
||||
def test_invalid_progression_order(self):
|
||||
"""Should still be able to parse even if prog order is invalid."""
|
||||
jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2')
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import datetime
|
|||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -30,7 +31,10 @@ class TestICC(unittest.TestCase):
|
|||
def test_file5(self):
|
||||
"""basic ICC profile"""
|
||||
filename = opj_data_file('input/conformance/file5.jp2')
|
||||
j = Jp2k(filename)
|
||||
with warnings.catch_warnings():
|
||||
# The file has a bad compatibility list entry. Not important here.
|
||||
warnings.simplefilter("ignore")
|
||||
j = Jp2k(filename)
|
||||
profile = j.box[2].box[1].icc_profile
|
||||
self.assertEqual(profile['Size'], 546)
|
||||
self.assertEqual(profile['Preferred CMM Type'], 0)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,23 @@ class TestDataEntryURL(unittest.TestCase):
|
|||
def setUp(self):
|
||||
self.jp2file = glymur.data.nemo()
|
||||
|
||||
def test_wrap_greyscale(self):
|
||||
"""A single component should be wrapped as GREYSCALE."""
|
||||
j = Jp2k(self.jp2file)
|
||||
data = j.read()
|
||||
red = data[:, :, 0]
|
||||
|
||||
# Write it back out as a raw codestream.
|
||||
with tempfile.NamedTemporaryFile(suffix=".j2k") as tfile1:
|
||||
j2k = glymur.Jp2k(tfile1.name, 'wb')
|
||||
j2k.write(data[:, :, 0])
|
||||
|
||||
# Ok, now rewrap it as JP2. The colorspace should be GREYSCALE.
|
||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile2:
|
||||
jp2 = j2k.wrap(tfile2.name)
|
||||
self.assertEqual(jp2.box[2].box[1].colorspace,
|
||||
glymur.core.GREYSCALE)
|
||||
|
||||
def test_basic_url(self):
|
||||
"""Just your most basic URL box."""
|
||||
# Wrap our j2k file in a JP2 box along with an interior url box.
|
||||
|
|
@ -340,6 +357,7 @@ class TestChannelDefinition(unittest.TestCase):
|
|||
with self.assertRaises((IOError, OSError)):
|
||||
j2k.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||
def test_bad_type(self):
|
||||
"""Channel types are limited to 0, 1, 2, 65535
|
||||
Should reject if not all of index, channel_type, association the
|
||||
|
|
@ -347,17 +365,18 @@ class TestChannelDefinition(unittest.TestCase):
|
|||
"""
|
||||
channel_type = (COLOR, COLOR, 3)
|
||||
association = (RED, GREEN, BLUE)
|
||||
with self.assertRaises(IOError):
|
||||
with self.assertWarns(UserWarning):
|
||||
glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type,
|
||||
association=association)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||
def test_wrong_lengths(self):
|
||||
"""Should reject if not all of index, channel_type, association the
|
||||
same length.
|
||||
"""
|
||||
channel_type = (COLOR, COLOR)
|
||||
association = (RED, GREEN, BLUE)
|
||||
with self.assertRaises(IOError):
|
||||
with self.assertWarns(UserWarning):
|
||||
glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type,
|
||||
association=association)
|
||||
|
||||
|
|
@ -373,14 +392,19 @@ class TestFileTypeBox(unittest.TestCase):
|
|||
|
||||
def test_brand_unknown(self):
|
||||
"""A ftyp box brand must be 'jp2 ' or 'jpx '."""
|
||||
ftyp = glymur.jp2box.FileTypeBox(brand='jp3')
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
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 warnings.catch_warnings():
|
||||
# Bad compatibility list item.
|
||||
warnings.simplefilter("ignore")
|
||||
ftyp = glymur.jp2box.FileTypeBox(compatibility_list=['jp3'])
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
ftyp.write(tfile)
|
||||
|
|
@ -429,6 +453,18 @@ class TestColourSpecificationBox(unittest.TestCase):
|
|||
with self.assertRaises(IOError):
|
||||
j2k.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||
def test_bad_approx_jp2_field(self):
|
||||
"""JP2 has requirements for approx field"""
|
||||
j2k = Jp2k(self.j2kfile)
|
||||
boxes = [self.jp2b, self.ftyp, self.jp2h, self.jp2c]
|
||||
colr = ColourSpecificationBox(colorspace=glymur.core.SRGB,
|
||||
approximation=1)
|
||||
boxes[2].box = [self.ihdr, colr]
|
||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
j2k.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_default_colr(self):
|
||||
"""basic colr instantiation"""
|
||||
colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
|
||||
|
|
@ -438,27 +474,30 @@ class TestColourSpecificationBox(unittest.TestCase):
|
|||
self.assertEqual(colr.colorspace, glymur.core.SRGB)
|
||||
self.assertIsNone(colr.icc_profile)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
|
||||
def test_colr_with_cspace_and_icc(self):
|
||||
"""Colour specification boxes can't have both."""
|
||||
with self.assertRaises((OSError, IOError)):
|
||||
with self.assertWarns(UserWarning):
|
||||
colorspace = glymur.core.SRGB
|
||||
rawb = b'\x01\x02\x03\x04'
|
||||
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||
icc_profile=rawb)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
|
||||
def test_colr_with_bad_method(self):
|
||||
"""colr must have a valid method field"""
|
||||
colorspace = glymur.core.SRGB
|
||||
method = -1
|
||||
with self.assertRaises(IOError):
|
||||
with self.assertWarns(UserWarning):
|
||||
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||
method=method)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
|
||||
def test_colr_with_bad_approx(self):
|
||||
"""colr must have a valid approximation field"""
|
||||
"""colr should have a valid approximation field"""
|
||||
colorspace = glymur.core.SRGB
|
||||
approx = -1
|
||||
with self.assertRaises(IOError):
|
||||
with self.assertWarns(UserWarning):
|
||||
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||
approximation=approx)
|
||||
|
||||
|
|
@ -484,21 +523,23 @@ class TestPaletteBox(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||
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):
|
||||
with self.assertWarns(UserWarning):
|
||||
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
|
||||
signed=signed)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||
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):
|
||||
with self.assertWarns(UserWarning):
|
||||
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
|
||||
signed=signed)
|
||||
|
||||
|
|
@ -666,8 +707,9 @@ class TestWrap(unittest.TestCase):
|
|||
def test_jpx_to_jp2(self):
|
||||
"""basic test for rewrapping a jpx file"""
|
||||
jpx = Jp2k(self.jpxfile)
|
||||
idx = [0, 1, 3, 6]
|
||||
boxes = [jpx.box[idx] for idx in [0, 1, 3, 6]]
|
||||
# Use only the signature, file type, header, and 1st codestream.
|
||||
lst = [0, 1, 2, 5]
|
||||
boxes = [jpx.box[idx] for idx in lst]
|
||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||
jp2 = jpx.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
|
|
@ -828,6 +870,67 @@ class TestWrap(unittest.TestCase):
|
|||
with self.assertRaises(IOError):
|
||||
j2k.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_wrap_jpx_to_jp2_with_unadorned_jpch(self):
|
||||
"""A JPX file rewrapped with plain jpch is not allowed."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
|
||||
jpx = Jp2k(self.jpxfile)
|
||||
boxes = [jpx.box[0], jpx.box[1], jpx.box[2],
|
||||
glymur.jp2box.ContiguousCodestreamBox()]
|
||||
with self.assertRaises(IOError):
|
||||
jpx.wrap(tfile1.name, boxes=boxes)
|
||||
|
||||
def test_wrap_jpx_to_jp2_with_incorrect_jp2c_offset(self):
|
||||
"""Reject A JPX file rewrapped with bad jp2c offset."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
|
||||
jpx = Jp2k(self.jpxfile)
|
||||
jpch = jpx.box[5]
|
||||
|
||||
# The offset should be 902.
|
||||
jpch.offset = 901
|
||||
jpch.length = 313274
|
||||
boxes = [jpx.box[0], jpx.box[1], jpx.box[2], jpch]
|
||||
with self.assertRaises(IOError):
|
||||
jpx.wrap(tfile1.name, boxes=boxes)
|
||||
|
||||
def test_wrap_jpx_to_jp2_with_correctly_specified_jp2c(self):
|
||||
"""Accept A JPX file rewrapped with good jp2c."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
|
||||
jpx = Jp2k(self.jpxfile)
|
||||
jpch = jpx.box[5]
|
||||
|
||||
# This time get it right.
|
||||
jpch.offset = 903
|
||||
jpch.length = 313274
|
||||
boxes = [jpx.box[0], jpx.box[1], jpx.box[2], jpch]
|
||||
jp2 = jpx.wrap(tfile1.name, boxes=boxes)
|
||||
|
||||
act_ids = [box.box_id for box in jp2.box]
|
||||
exp_ids = ['jP ', 'ftyp', 'jp2h', 'jp2c']
|
||||
self.assertEqual(act_ids, exp_ids)
|
||||
|
||||
act_offsets = [box.offset for box in jp2.box]
|
||||
exp_offsets = [0, 12, 40, 887]
|
||||
self.assertEqual(act_offsets, exp_offsets)
|
||||
|
||||
act_lengths = [box.length for box in jp2.box]
|
||||
exp_lengths = [12, 28, 847, 313274]
|
||||
self.assertEqual(act_lengths, exp_lengths)
|
||||
|
||||
def test_full_blown_jpx(self):
|
||||
"""Rewrap a jpx file."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile1:
|
||||
jpx = Jp2k(self.jpxfile)
|
||||
idx = list(range(5)) + list(range(9, 12)) + list(range(6, 9)) + [12]
|
||||
boxes = [jpx.box[j] for j in idx]
|
||||
jpx2 = jpx.wrap(tfile1.name, boxes=boxes)
|
||||
exp_ids = [box.box_id for box in boxes]
|
||||
lengths = [box.length for box in jpx.box]
|
||||
exp_lengths = [lengths[j] for j in idx]
|
||||
act_ids = [box.box_id for box in jpx2.box]
|
||||
act_lengths = [box.length for box in jpx2.box]
|
||||
self.assertEqual(exp_ids, act_ids)
|
||||
self.assertEqual(exp_lengths, act_lengths)
|
||||
|
||||
|
||||
class TestJp2Boxes(unittest.TestCase):
|
||||
"""Tests for canonical JP2 boxes."""
|
||||
|
|
|
|||
|
|
@ -8,18 +8,22 @@ import struct
|
|||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
import lxml.etree as ET
|
||||
|
||||
import glymur
|
||||
from glymur import Jp2k
|
||||
from glymur.jp2box import DataEntryURLBox, FileTypeBox, JPEG2000SignatureBox
|
||||
from glymur.jp2box import DataReferenceBox, FragmentListBox, FragmentTableBox
|
||||
from glymur.jp2box import ColourSpecificationBox
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "Temporary file issue on window.")
|
||||
class TestJPXWrap(unittest.TestCase):
|
||||
"""Test suite for wrapping JPX files."""
|
||||
|
||||
def setUp(self):
|
||||
self.jpxfile = glymur.data.jpxfile()
|
||||
self.jp2file = glymur.data.nemo()
|
||||
self.j2kfile = glymur.data.goodstuff()
|
||||
|
||||
|
|
@ -107,6 +111,68 @@ class TestJPXWrap(unittest.TestCase):
|
|||
with self.assertRaises(IOError):
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_jpch_jplh(self):
|
||||
"""Write a codestream header, compositing layer header box."""
|
||||
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']
|
||||
|
||||
jpch = glymur.jp2box.CodestreamHeaderBox()
|
||||
boxes.append(jpch)
|
||||
jplh = glymur.jp2box.CompositingLayerHeaderBox()
|
||||
boxes.append(jplh)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
self.assertEqual(jpx.box[-2].box_id, 'jpch')
|
||||
self.assertEqual(jpx.box[-1].box_id, 'jplh')
|
||||
|
||||
def test_cgrp(self):
|
||||
"""Write a color group box."""
|
||||
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']
|
||||
|
||||
colr_rgb = ColourSpecificationBox(colorspace=glymur.core.SRGB)
|
||||
colr_gr = ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)
|
||||
box = [colr_rgb, colr_gr]
|
||||
|
||||
cgrp = glymur.jp2box.ColourGroupBox(box=box)
|
||||
boxes.append(cgrp)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
self.assertEqual(jpx.box[-1].box_id, 'cgrp')
|
||||
self.assertEqual(jpx.box[-1].box[0].box_id, 'colr')
|
||||
self.assertEqual(jpx.box[-1].box[1].box_id, 'colr')
|
||||
|
||||
def test_cgrp_neg(self):
|
||||
"""Can't write a cgrp with anything but colr sub boxes"""
|
||||
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']
|
||||
|
||||
lblb = glymur.jp2box.LabelBox("Just a test")
|
||||
box = [lblb]
|
||||
|
||||
cgrp = glymur.jp2box.ColourGroupBox(box=box)
|
||||
boxes.append(cgrp)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpx") as tfile:
|
||||
with self.assertRaises(IOError):
|
||||
jpx = jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
def test_ftbl(self):
|
||||
"""Write a fragment table box."""
|
||||
# Add a negative test where offset < 0
|
||||
|
|
@ -207,13 +273,14 @@ class TestJPXWrap(unittest.TestCase):
|
|||
with self.assertRaises(IOError):
|
||||
jp2.wrap(tfile.name, boxes=boxes)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||
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):
|
||||
with self.assertWarns(UserWarning):
|
||||
dref = glymur.jp2box.DataReferenceBox([ftyp])
|
||||
|
||||
# Try to get around it by appending the ftyp box after creation.
|
||||
|
|
@ -337,7 +404,9 @@ class TestJPX(unittest.TestCase):
|
|||
offset = [89]
|
||||
length = [1132288]
|
||||
reference = [0, 0]
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
flst.write(tfile)
|
||||
|
|
@ -347,8 +416,10 @@ class TestJPX(unittest.TestCase):
|
|||
offset = [0]
|
||||
length = [1132288]
|
||||
reference = [0]
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
with self.assertRaises(IOError):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
with self.assertRaises((IOError, OSError)):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
flst.write(tfile)
|
||||
|
||||
|
|
@ -357,14 +428,18 @@ class TestJPX(unittest.TestCase):
|
|||
offset = [89]
|
||||
length = [0]
|
||||
reference = [0]
|
||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
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 warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
ftbl = glymur.jp2box.FragmentTableBox()
|
||||
with self.assertRaises(IOError):
|
||||
with tempfile.TemporaryFile() as tfile:
|
||||
ftbl.write(tfile)
|
||||
|
|
@ -377,6 +452,7 @@ class TestJPX(unittest.TestCase):
|
|||
with tempfile.TemporaryFile() as tfile:
|
||||
ftbl.write(tfile)
|
||||
|
||||
@unittest.skip("No such jpx file anymore.")
|
||||
def test_jpx_rreq_mask_length_3(self):
|
||||
"""There are some JPX files with rreq mask length of 3."""
|
||||
jpx = Jp2k(self.jpxfile)
|
||||
|
|
@ -386,7 +462,7 @@ 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.")
|
||||
@unittest.skip("Requires unnecessarily complicated code")
|
||||
def test_unknown_superbox(self):
|
||||
"""Verify that we can handle an unknown superbox."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
|
||||
|
|
@ -402,15 +478,9 @@ class TestJPX(unittest.TestCase):
|
|||
|
||||
with self.assertWarns(UserWarning):
|
||||
jpx = Jp2k(tfile.name)
|
||||
self.assertEqual(jpx.box[-1].box_id, 'grp ')
|
||||
self.assertEqual(jpx.box[-1].box_id, b'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
|
||||
|
|
@ -483,17 +553,17 @@ class TestJPX(unittest.TestCase):
|
|||
self.assertEqual(jpx.box[-1].box[0].data_reference, (3,))
|
||||
|
||||
def test_nlst(self):
|
||||
"""Verify that we can handle a free box."""
|
||||
"""Verify that we can handle a number list box."""
|
||||
j = Jp2k(self.jpxfile)
|
||||
self.assertEqual(j.box[16].box[1].box[0].box_id, 'nlst')
|
||||
self.assertEqual(type(j.box[16].box[1].box[0]),
|
||||
glymur.jp2box.NumberListBox)
|
||||
nlst = j.box[12].box[0].box[0]
|
||||
self.assertEqual(nlst.box_id, 'nlst')
|
||||
self.assertEqual(type(nlst), glymur.jp2box.NumberListBox)
|
||||
|
||||
# Two associations.
|
||||
self.assertEqual(len(j.box[16].box[1].box[0].associations), 2)
|
||||
self.assertEqual(len(nlst.associations), 2)
|
||||
|
||||
# Codestream 0
|
||||
self.assertEqual(j.box[16].box[1].box[0].associations[0], 1 << 24)
|
||||
self.assertEqual(nlst.associations[0], 1 << 24)
|
||||
|
||||
# Compositing Layer 0
|
||||
self.assertEqual(j.box[16].box[1].box[0].associations[1], 2 << 24)
|
||||
self.assertEqual(nlst.associations[1], 2 << 24)
|
||||
|
|
|
|||
|
|
@ -63,6 +63,24 @@ class TestJp2k(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_no_cxform_pclr_jpx(self):
|
||||
"""Indices for pclr jpxfile if no color transform"""
|
||||
j = Jp2k(self.jpxfile)
|
||||
rgb = j.read()
|
||||
idx = j.read(ignore_pclr_cmap_cdef=True)
|
||||
nr, nc = 1024, 1024
|
||||
self.assertEqual(rgb.shape, (nr, nc, 3))
|
||||
self.assertEqual(idx.shape, (nr, nc))
|
||||
|
||||
# Should be able to manually reconstruct the RGB image from the palette
|
||||
# and indices.
|
||||
palette = j.box[2].box[2].palette
|
||||
rgb_from_idx = np.zeros(rgb.shape, dtype=np.uint8)
|
||||
for r in np.arange(nr):
|
||||
for c in np.arange(nc):
|
||||
rgb_from_idx[r, c] = palette[idx[r, c]]
|
||||
np.testing.assert_array_equal(rgb, rgb_from_idx)
|
||||
|
||||
def test_repr(self):
|
||||
"""Verify that results of __repr__ are eval-able."""
|
||||
j = Jp2k(self.j2kfile)
|
||||
|
|
@ -92,23 +110,6 @@ class TestJp2k(unittest.TestCase):
|
|||
with self.assertRaises(IOError):
|
||||
Jp2k(filename)
|
||||
|
||||
def test_no_cxform_pclr_jpx(self):
|
||||
"""Indices for pclr jpxfile if no color transform"""
|
||||
j = Jp2k(self.jpxfile)
|
||||
rgb = j.read()
|
||||
idx = j.read(no_cxform=True)
|
||||
self.assertEqual(rgb.shape, (1024, 1024, 3))
|
||||
self.assertEqual(idx.shape, (1024, 1024))
|
||||
|
||||
# Should be able to manually reconstruct the RGB image from the palette
|
||||
# and indices.
|
||||
palette = j.box[3].box[2].palette
|
||||
rgb_from_idx = np.zeros(rgb.shape, dtype=np.uint8)
|
||||
for r in np.arange(1024):
|
||||
for c in np.arange(1024):
|
||||
rgb_from_idx[r, c] = palette[idx[r, c]]
|
||||
np.testing.assert_array_equal(rgb, rgb_from_idx)
|
||||
|
||||
def test_file_not_present(self):
|
||||
"""Should error out if reading from a file that does not exist"""
|
||||
# Verify that we error out appropriately if not given an existing file
|
||||
|
|
@ -758,12 +759,48 @@ class TestJp2k_2_1(unittest.TestCase):
|
|||
class TestJp2kOpjDataRoot(unittest.TestCase):
|
||||
"""These tests should be run by just about all configuration."""
|
||||
|
||||
def test_undecodeable_box_id(self):
|
||||
"""Should warn in case of undecodeable box ID but not error out."""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_1013627.jp2')
|
||||
if sys.hexversion < 0x03000000:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
jp2 = Jp2k(filename)
|
||||
else:
|
||||
with self.assertWarns(UserWarning):
|
||||
jp2 = Jp2k(filename)
|
||||
|
||||
# Now make sure we got all of the boxes. Ignore the last, which was
|
||||
# bad.
|
||||
box_ids = [box.box_id for box in jp2.box[:-1]]
|
||||
self.assertEqual(box_ids, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
|
||||
|
||||
def test_invalid_approximation(self):
|
||||
"""Should warn in case of bad ftyp brand."""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_1000290.jp2')
|
||||
with self.assertWarns(UserWarning):
|
||||
jp2 = Jp2k(filename)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Test requires Python 3.3+")
|
||||
def test_invalid_approximation(self):
|
||||
"""Should warn in case of invalid approximation."""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_1015644.jp2')
|
||||
with self.assertWarns(UserWarning):
|
||||
jp2 = Jp2k(filename)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Test requires Python 3.3+")
|
||||
def test_invalid_colorspace(self):
|
||||
"""Should warn in case of invalid colorspace."""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_1103421.jp2')
|
||||
with self.assertWarns(UserWarning):
|
||||
jp2 = Jp2k(filename)
|
||||
|
||||
def test_no_cxform_pclr_jp2(self):
|
||||
"""Indices for pclr jpxfile if no color transform"""
|
||||
filename = opj_data_file('input/conformance/file9.jp2')
|
||||
j = Jp2k(filename)
|
||||
rgb = j.read()
|
||||
idx = j.read(no_cxform=True)
|
||||
idx = j.read(ignore_pclr_cmap_cdef=True)
|
||||
self.assertEqual(rgb.shape, (512, 768, 3))
|
||||
self.assertEqual(idx.shape, (512, 768))
|
||||
|
||||
|
|
@ -803,9 +840,12 @@ class TestJp2kOpjDataRoot(unittest.TestCase):
|
|||
# This file has the components physically reversed. The cmap box
|
||||
# tells the decoder how to order them, but this flag prevents that.
|
||||
filename = opj_data_file('input/conformance/file2.jp2')
|
||||
j = Jp2k(filename)
|
||||
with warnings.catch_warnings():
|
||||
# The file has a bad compatibility list entry. Not important here.
|
||||
warnings.simplefilter("ignore")
|
||||
j = Jp2k(filename)
|
||||
ycbcr = j.read()
|
||||
crcby = j.read(no_cxform=True)
|
||||
crcby = j.read(ignore_pclr_cmap_cdef=True)
|
||||
|
||||
expected = np.zeros(ycbcr.shape, ycbcr.dtype)
|
||||
for k in range(crcby.shape[2]):
|
||||
|
|
|
|||
|
|
@ -327,7 +327,10 @@ class TestSuite(unittest.TestCase):
|
|||
|
||||
def test_ETS_JP2_file1(self):
|
||||
jfile = opj_data_file('input/conformance/file1.jp2')
|
||||
jp2k = Jp2k(jfile)
|
||||
with warnings.catch_warnings():
|
||||
# Bad compatibility list item.
|
||||
warnings.simplefilter("ignore")
|
||||
jp2k = Jp2k(jfile)
|
||||
jpdata = jp2k.read()
|
||||
self.assertEqual(jpdata.shape, (512, 768, 3))
|
||||
|
||||
|
|
@ -3114,7 +3117,10 @@ class TestSuiteDump(unittest.TestCase):
|
|||
|
||||
def test_NR_file1_dump(self):
|
||||
jfile = opj_data_file('input/conformance/file1.jp2')
|
||||
jp2 = Jp2k(jfile)
|
||||
with warnings.catch_warnings():
|
||||
# Bad compatibility list item.
|
||||
warnings.simplefilter("ignore")
|
||||
jp2 = Jp2k(jfile)
|
||||
|
||||
ids = [box.box_id for box in jp2.box]
|
||||
self.assertEqual(ids, ['jP ', 'ftyp', 'xml ', 'jp2h', 'xml ',
|
||||
|
|
@ -3441,7 +3447,7 @@ class TestSuiteDump(unittest.TestCase):
|
|||
# Image header
|
||||
self.assertEqual(jp2.box[2].box[0].height, 400)
|
||||
self.assertEqual(jp2.box[2].box[0].width, 700)
|
||||
self.assertEqual(jp2.box[2].box[0].num_components, 1)
|
||||
self.assertEqual(jp2.box[2].box[0].num_components, 3)
|
||||
self.assertEqual(jp2.box[2].box[0].bits_per_component, 8)
|
||||
self.assertEqual(jp2.box[2].box[0].signed, False)
|
||||
self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet
|
||||
|
|
@ -5476,7 +5482,7 @@ class TestSuiteDump(unittest.TestCase):
|
|||
jp2 = Jp2k(jfile)
|
||||
|
||||
ids = [box.box_id for box in jp2.box]
|
||||
self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', 'XML ', 'jp2c'])
|
||||
self.assertEqual(ids, ['jP ', 'ftyp', 'jp2h', b'XML ', 'jp2c'])
|
||||
|
||||
ids = [box.box_id for box in jp2.box[2].box]
|
||||
self.assertEqual(ids, ['ihdr', 'colr'])
|
||||
|
|
@ -5830,9 +5836,9 @@ class TestSuiteDump(unittest.TestCase):
|
|||
|
||||
# Jp2 Header
|
||||
# Component mapping box
|
||||
self.assertEqual(jp2.box[3].box[3].component_index, (0, 1, 2))
|
||||
self.assertEqual(jp2.box[3].box[3].mapping_type, (1, 1, 0))
|
||||
self.assertEqual(jp2.box[3].box[3].palette_index, (0, 0, 1))
|
||||
self.assertEqual(jp2.box[3].box[3].component_index, (0, 0, 0, 0))
|
||||
self.assertEqual(jp2.box[3].box[3].mapping_type, (1, 1, 1, 1))
|
||||
self.assertEqual(jp2.box[3].box[3].palette_index, (0, 1, 2, 3))
|
||||
|
||||
c = jp2.box[4].main_header
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ class TestPrinting(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_version_info(self):
|
||||
"""Should be able to print(glymur.version.info)"""
|
||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||
print(glymur.version.info)
|
||||
actual = fake_out.getvalue().strip()
|
||||
|
||||
self.assertTrue(True)
|
||||
|
||||
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||
def test_unknown_superbox(self):
|
||||
"""Verify that we can handle an unknown superbox."""
|
||||
|
|
@ -60,6 +68,9 @@ class TestPrinting(unittest.TestCase):
|
|||
# Add the header for an unknwon superbox.
|
||||
write_buffer = struct.pack('>I4s', 20, 'grp '.encode())
|
||||
tfile.write(write_buffer)
|
||||
|
||||
# Add a free box inside of it. We won't be able to identify it,
|
||||
# but it's there.
|
||||
write_buffer = struct.pack('>I4sI', 12, 'free'.encode(), 0)
|
||||
tfile.write(write_buffer)
|
||||
tfile.flush()
|
||||
|
|
@ -70,8 +81,7 @@ class TestPrinting(unittest.TestCase):
|
|||
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)']
|
||||
lines = ["Unknown Box (b'grp ') @ (1399071, 20)"]
|
||||
expected = '\n'.join(lines)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
|
|
@ -341,7 +351,7 @@ class TestPrinting(unittest.TestCase):
|
|||
actual = fake_out.getvalue().strip()
|
||||
|
||||
lines = ['SIZ marker segment @ (3233, 47)',
|
||||
' Profile: 2',
|
||||
' Profile: no profile',
|
||||
' Reference Grid Height, Width: (1456 x 2592)',
|
||||
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
||||
' Reference Tile Height, Width: (1456 x 2592)',
|
||||
|
|
@ -414,7 +424,7 @@ class TestPrinting(unittest.TestCase):
|
|||
lst = ['Codestream:',
|
||||
' SOC marker segment @ (3231, 0)',
|
||||
' SIZ marker segment @ (3233, 47)',
|
||||
' Profile: 2',
|
||||
' Profile: no profile',
|
||||
' Reference Grid Height, Width: (1456 x 2592)',
|
||||
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
||||
' Reference Tile Height, Width: (1456 x 2592)',
|
||||
|
|
@ -645,6 +655,43 @@ class TestPrintingOpjDataRoot(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_cinema_profile(self):
|
||||
"""Should print Cinema 2K when the profile is 3."""
|
||||
filename = opj_data_file('input/nonregression/_00042.j2k')
|
||||
j2k = Jp2k(filename)
|
||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||
c = j2k.get_codestream()
|
||||
print(c.segment[1])
|
||||
actual = fake_out.getvalue().strip()
|
||||
self.assertEqual(actual, fixtures.cinema2k_profile)
|
||||
|
||||
def test_invalid_colorspace(self):
|
||||
"""An invalid colorspace shouldn't cause an error."""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_1103421.jp2')
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
jp2 = Jp2k(filename)
|
||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||
print(jp2)
|
||||
|
||||
def test_bad_rsiz(self):
|
||||
"""Should still be able to print if rsiz is bad, issue196"""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_1002767.jp2')
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
j = Jp2k(filename)
|
||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||
print(j)
|
||||
|
||||
def test_bad_wavelet_transform(self):
|
||||
"""Should still be able to print if wavelet xform is bad, issue195"""
|
||||
filename = opj_data_file('input/nonregression/edf_c2_10025.jp2')
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
j = Jp2k(filename)
|
||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||
print(j)
|
||||
|
||||
def test_invalid_progression_order(self):
|
||||
"""Should still be able to print even if prog order is invalid."""
|
||||
jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2')
|
||||
|
|
@ -818,7 +865,10 @@ class TestPrintingOpjDataRoot(unittest.TestCase):
|
|||
def test_channel_definition(self):
|
||||
"""verify printing of cdef box"""
|
||||
filename = opj_data_file('input/conformance/file2.jp2')
|
||||
j = glymur.Jp2k(filename)
|
||||
with warnings.catch_warnings():
|
||||
# Bad compatibility list item.
|
||||
warnings.simplefilter("ignore")
|
||||
j = glymur.Jp2k(filename)
|
||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||
print(j.box[2].box[2])
|
||||
actual = fake_out.getvalue().strip()
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ License: MIT
|
|||
"""
|
||||
|
||||
import sys
|
||||
import numpy as np
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
import lxml.etree
|
||||
import numpy as np
|
||||
|
||||
from .lib import openjpeg as opj
|
||||
from .lib import openjp2 as opj2
|
||||
|
||||
|
|
@ -48,10 +50,12 @@ OPENJPEG {openjpeg}
|
|||
Python {python}
|
||||
sys.platform {platform}
|
||||
sys.maxsize {maxsize}
|
||||
lxml {elxml}
|
||||
numpy {numpy}
|
||||
""".format(glymur=version,
|
||||
openjpeg=openjpeg_version,
|
||||
python=sys.version,
|
||||
platform=sys.platform,
|
||||
maxsize=sys.maxsize,
|
||||
elxml=lxml.etree.__version__,
|
||||
numpy=np.__version__)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue