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 = '>'
|
endian = '>'
|
||||||
else:
|
else:
|
||||||
msg = "Bad byte order indication: {0}".format(read_buffer[6:8])
|
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])
|
_, offset = struct.unpack(endian + 'HI', read_buffer[8:14])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ codestreams.
|
||||||
# the base Segment class.
|
# the base Segment class.
|
||||||
# pylint: disable=R0903
|
# pylint: disable=R0903
|
||||||
|
|
||||||
import collections
|
|
||||||
import math
|
import math
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -28,33 +27,36 @@ import numpy as np
|
||||||
from .core import LRCP, RLCP, RPCL, PCRL, CPRL
|
from .core import LRCP, RLCP, RPCL, PCRL, CPRL
|
||||||
from .core import WAVELET_XFORM_9X7_IRREVERSIBLE
|
from .core import WAVELET_XFORM_9X7_IRREVERSIBLE
|
||||||
from .core import WAVELET_XFORM_5X3_REVERSIBLE
|
from .core import WAVELET_XFORM_5X3_REVERSIBLE
|
||||||
from .core import _CAPABILITIES_DISPLAY
|
from .core import _Keydefaultdict
|
||||||
from .lib import openjp2 as opj2
|
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)
|
_factory = lambda x: '{0} (invalid)'.format(x)
|
||||||
_PROGRESSION_ORDER_DISPLAY = _keydefaultdict(_factory,
|
_PROGRESSION_ORDER_DISPLAY = _Keydefaultdict(_factory,
|
||||||
{ LRCP: 'LRCP',
|
{ LRCP: 'LRCP',
|
||||||
RLCP: 'RLCP',
|
RLCP: 'RLCP',
|
||||||
RPCL: 'RPCL',
|
RPCL: 'RPCL',
|
||||||
PCRL: 'PCRL',
|
PCRL: 'PCRL',
|
||||||
CPRL: 'CPRL'})
|
CPRL: 'CPRL'})
|
||||||
|
|
||||||
_WAVELET_TRANSFORM_DISPLAY = {
|
_WAVELET_TRANSFORM_DISPLAY = _Keydefaultdict(_factory,
|
||||||
WAVELET_XFORM_9X7_IRREVERSIBLE: '9-7 irreversible',
|
{ WAVELET_XFORM_9X7_IRREVERSIBLE: '9-7 irreversible',
|
||||||
WAVELET_XFORM_5X3_REVERSIBLE: '5-3 reversible'}
|
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.
|
# Need a catch-all list of valid markers.
|
||||||
# See table A-1 in ISO/IEC FCD15444-1.
|
# See table A-1 in ISO/IEC FCD15444-1.
|
||||||
|
|
@ -390,6 +392,11 @@ class Codestream(object):
|
||||||
msg = "Invalid progression order in COD segment: {0}."
|
msg = "Invalid progression order in COD segment: {0}."
|
||||||
warnings.warn(msg.format(spcod[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
|
sop = (scod & 2) > 0
|
||||||
eph = (scod & 4) > 0
|
eph = (scod & 4) > 0
|
||||||
|
|
||||||
|
|
@ -667,6 +674,9 @@ class Codestream(object):
|
||||||
data = struct.unpack('>HIIIIIIIIH', xy_buffer)
|
data = struct.unpack('>HIIIIIIIIH', xy_buffer)
|
||||||
|
|
||||||
rsiz = data[0]
|
rsiz = data[0]
|
||||||
|
if rsiz not in _KNOWN_PROFILES:
|
||||||
|
warnings.warn("Invalid profile: (Rsiz={0}).".format(rsiz))
|
||||||
|
|
||||||
xysiz = (data[1], data[2])
|
xysiz = (data[1], data[2])
|
||||||
xyosiz = (data[3], data[4])
|
xyosiz = (data[3], data[4])
|
||||||
xytsiz = (data[5], data[6])
|
xytsiz = (data[5], data[6])
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,22 @@
|
||||||
"""Core definitions to be shared amongst the modules.
|
"""Core definitions to be shared amongst the modules.
|
||||||
"""
|
"""
|
||||||
|
import collections
|
||||||
import copy
|
import copy
|
||||||
import lxml.etree as ET
|
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
|
# Progression order
|
||||||
LRCP = 0
|
LRCP = 0
|
||||||
RLCP = 1
|
RLCP = 1
|
||||||
|
|
@ -57,24 +71,26 @@ YCC = 18
|
||||||
E_SRGB = 20
|
E_SRGB = 20
|
||||||
ROMM_RGB = 21
|
ROMM_RGB = 21
|
||||||
|
|
||||||
_COLORSPACE_MAP_DISPLAY = {
|
_factory = lambda x: '{0} (unrecognized)'.format(x)
|
||||||
CMYK: 'CMYK',
|
_COLORSPACE_MAP_DISPLAY = _Keydefaultdict(_factory,
|
||||||
|
{ CMYK: 'CMYK',
|
||||||
SRGB: 'sRGB',
|
SRGB: 'sRGB',
|
||||||
GREYSCALE: 'greyscale',
|
GREYSCALE: 'greyscale',
|
||||||
YCC: 'YCC',
|
YCC: 'YCC',
|
||||||
E_SRGB: 'e-sRGB',
|
E_SRGB: 'e-sRGB',
|
||||||
ROMM_RGB: 'ROMM-RGB'}
|
ROMM_RGB: 'ROMM-RGB'} )
|
||||||
|
|
||||||
# enumerated color channel types
|
# enumerated color channel types
|
||||||
COLOR = 0
|
COLOR = 0
|
||||||
OPACITY = 1
|
OPACITY = 1
|
||||||
PRE_MULTIPLIED_OPACITY = 2
|
PRE_MULTIPLIED_OPACITY = 2
|
||||||
_UNSPECIFIED = 65535
|
_UNSPECIFIED = 65535
|
||||||
_COLOR_TYPE_MAP_DISPLAY = {
|
_factory = lambda x: '{0} (invalid)'.format(x)
|
||||||
COLOR: 'color',
|
_COLOR_TYPE_MAP_DISPLAY = _Keydefaultdict(_factory,
|
||||||
|
{ COLOR: 'color',
|
||||||
OPACITY: 'opacity',
|
OPACITY: 'opacity',
|
||||||
PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity',
|
PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity',
|
||||||
_UNSPECIFIED: 'unspecified'}
|
_UNSPECIFIED: 'unspecified'})
|
||||||
|
|
||||||
# color channel definitions.
|
# color channel definitions.
|
||||||
RED = 1
|
RED = 1
|
||||||
|
|
@ -90,9 +106,3 @@ _COLORSPACE = {SRGB: {"R": 1, "G": 2, "B": 3},
|
||||||
E_SRGB: {"R": 1, "G": 2, "B": 3},
|
E_SRGB: {"R": 1, "G": 2, "B": 3},
|
||||||
ROMM_RGB: {"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():
|
def jpxfile():
|
||||||
"""Shortcut for specifying path to 12-v6.4.jpx.
|
"""Shortcut for specifying path to heliov.jpx.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
file : str
|
file : str
|
||||||
Platform-independent path to 12-v6.4.jpx
|
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
|
return filename
|
||||||
|
|
||||||
|
|
|
||||||
BIN
glymur/data/heliov.jpx
Normal file
BIN
glymur/data/heliov.jpx
Normal file
Binary file not shown.
524
glymur/jp2box.py
524
glymur/jp2box.py
File diff suppressed because it is too large
Load diff
188
glymur/jp2k.py
188
glymur/jp2k.py
|
|
@ -29,7 +29,7 @@ import numpy as np
|
||||||
|
|
||||||
from .codestream import Codestream
|
from .codestream import Codestream
|
||||||
from .core import SRGB, GREYSCALE
|
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 .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE
|
||||||
from .jp2box import Jp2kBox
|
from .jp2box import Jp2kBox
|
||||||
from .jp2box import JPEG2000SignatureBox, FileTypeBox, JP2HeaderBox
|
from .jp2box import JPEG2000SignatureBox, FileTypeBox, JP2HeaderBox
|
||||||
|
|
@ -606,7 +606,12 @@ class Jp2k(Jp2kBox):
|
||||||
self.parse()
|
self.parse()
|
||||||
|
|
||||||
def wrap(self, filename, boxes=None):
|
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
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
@ -616,6 +621,8 @@ class Jp2k(Jp2kBox):
|
||||||
JP2 box definitions to define the JP2 file format. If not
|
JP2 box definitions to define the JP2 file format. If not
|
||||||
provided, a default ""jacket" is assumed, consisting of JP2
|
provided, a default ""jacket" is assumed, consisting of JP2
|
||||||
signature, file type, JP2 header, and contiguous codestream boxes.
|
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
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|
@ -631,6 +638,74 @@ class Jp2k(Jp2kBox):
|
||||||
>>> jp2 = j2k.wrap(tfile.name)
|
>>> jp2 = j2k.wrap(tfile.name)
|
||||||
"""
|
"""
|
||||||
if boxes is None:
|
if boxes is None:
|
||||||
|
boxes = self._get_default_jp2_boxes()
|
||||||
|
|
||||||
|
_validate_jp2_box_sequence(boxes)
|
||||||
|
|
||||||
|
with open(filename, 'wb') as ofile:
|
||||||
|
for box in boxes:
|
||||||
|
if box.box_id != 'jp2c':
|
||||||
|
box.write(ofile)
|
||||||
|
else:
|
||||||
|
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.
|
# Try to create a reasonable default.
|
||||||
boxes = [JPEG2000SignatureBox(),
|
boxes = [JPEG2000SignatureBox(),
|
||||||
FileTypeBox(),
|
FileTypeBox(),
|
||||||
|
|
@ -640,45 +715,23 @@ class Jp2k(Jp2kBox):
|
||||||
height = codestream.segment[1].ysiz
|
height = codestream.segment[1].ysiz
|
||||||
width = codestream.segment[1].xsiz
|
width = codestream.segment[1].xsiz
|
||||||
num_components = len(codestream.segment[1].xrsiz)
|
num_components = len(codestream.segment[1].xrsiz)
|
||||||
boxes[2].box = [ImageHeaderBox(height=height,
|
if num_components < 3:
|
||||||
width=width,
|
colorspace = GREYSCALE
|
||||||
num_components=num_components),
|
|
||||||
ColourSpecificationBox(colorspace=SRGB)]
|
|
||||||
|
|
||||||
_validate_jp2_box_sequence(boxes)
|
|
||||||
|
|
||||||
with open(filename, 'wb') as ofile:
|
|
||||||
for box in boxes:
|
|
||||||
if box.box_id != 'jp2c':
|
|
||||||
box.write(ofile)
|
|
||||||
else:
|
else:
|
||||||
# The codestream gets written last.
|
|
||||||
if len(self.box) == 0:
|
if len(self.box) == 0:
|
||||||
# Am I a raw codestream? If so, then it is pretty
|
# Best guess is SRGB
|
||||||
# easy, just write the codestream box header plus all
|
colorspace = SRGB
|
||||||
# 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:
|
else:
|
||||||
# OK, I'm a jp2 file. Need to find out where the
|
# Take whatever the first jp2 header / color specification
|
||||||
# raw codestream actually starts.
|
# says.
|
||||||
jp2c = [box for box in self.box
|
jp2hs = [box for box in self.box if box.box_id == 'jp2h']
|
||||||
if box.box_id == 'jp2c']
|
colorspace = jp2hs[0].box[1].colorspace
|
||||||
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))
|
|
||||||
|
|
||||||
ofile.flush()
|
boxes[2].box = [ImageHeaderBox(height=height, width=width,
|
||||||
|
num_components=num_components),
|
||||||
|
ColourSpecificationBox(colorspace=colorspace)]
|
||||||
|
|
||||||
jp2 = Jp2k(filename)
|
return boxes
|
||||||
return jp2
|
|
||||||
|
|
||||||
def read(self, **kwargs):
|
def read(self, **kwargs):
|
||||||
"""Read a JPEG 2000 image.
|
"""Read a JPEG 2000 image.
|
||||||
|
|
@ -696,8 +749,9 @@ class Jp2k(Jp2kBox):
|
||||||
(first_row, first_col, last_row, last_col)
|
(first_row, first_col, last_row, last_col)
|
||||||
tile : int, optional
|
tile : int, optional
|
||||||
Number of tile to decode.
|
Number of tile to decode.
|
||||||
no_cxform : bool
|
ignore_pclr_cmap_cdef : bool
|
||||||
Whether or not to apply intended color transforms.
|
Whether or not to ignore the pclr, cmap, or cdef boxes during any
|
||||||
|
color transformation. Defaults to False.
|
||||||
verbose : bool, optional
|
verbose : bool, optional
|
||||||
Print informational messages produced by the OpenJPEG library.
|
Print informational messages produced by the OpenJPEG library.
|
||||||
|
|
||||||
|
|
@ -752,7 +806,8 @@ class Jp2k(Jp2kBox):
|
||||||
msg += "the read_bands method instead."
|
msg += "the read_bands method instead."
|
||||||
raise RuntimeError(msg)
|
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.
|
"""Read a JPEG 2000 image using libopenjpeg.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -760,8 +815,9 @@ class Jp2k(Jp2kBox):
|
||||||
rlevel : int, optional
|
rlevel : int, optional
|
||||||
Factor by which to rlevel output resolution. Use -1 to get the
|
Factor by which to rlevel output resolution. Use -1 to get the
|
||||||
lowest resolution thumbnail.
|
lowest resolution thumbnail.
|
||||||
no_cxform : bool
|
ignore_pclr_cmap_cdef : bool
|
||||||
Whether or not to apply intended color transforms.
|
Whether or not to ignore the pclr, cmap, or cdef boxes during any
|
||||||
|
color transformation. Defaults to False.
|
||||||
verbose : bool, optional
|
verbose : bool, optional
|
||||||
Print informational messages produced by the OpenJPEG library.
|
Print informational messages produced by the OpenJPEG library.
|
||||||
|
|
||||||
|
|
@ -797,7 +853,7 @@ class Jp2k(Jp2kBox):
|
||||||
dparameters = opj.DecompressionParametersType()
|
dparameters = opj.DecompressionParametersType()
|
||||||
opj.set_default_decoder_parameters(ctypes.byref(dparameters))
|
opj.set_default_decoder_parameters(ctypes.byref(dparameters))
|
||||||
|
|
||||||
if no_cxform is True:
|
if ignore_pclr_cmap_cdef is True:
|
||||||
# Return raw codestream components.
|
# Return raw codestream components.
|
||||||
dparameters.flags |= 1
|
dparameters.flags |= 1
|
||||||
|
|
||||||
|
|
@ -845,7 +901,7 @@ class Jp2k(Jp2kBox):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _read_openjp2(self, rlevel=0, layer=0, area=None, tile=None,
|
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.
|
"""Read a JPEG 2000 image using libopenjp2.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -875,7 +931,8 @@ class Jp2k(Jp2kBox):
|
||||||
"""
|
"""
|
||||||
self._subsampling_sanity_check()
|
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:
|
with ExitStack() as stack:
|
||||||
if hasattr(opj2.OPENJP2,
|
if hasattr(opj2.OPENJP2,
|
||||||
|
|
@ -919,7 +976,8 @@ class Jp2k(Jp2kBox):
|
||||||
|
|
||||||
return img_array
|
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.
|
"""Populate decompression structure with appropriate input parameters.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -933,8 +991,9 @@ class Jp2k(Jp2kBox):
|
||||||
(first_row, first_col, last_row, last_col)
|
(first_row, first_col, last_row, last_col)
|
||||||
tile : int
|
tile : int
|
||||||
Number of tile to decode.
|
Number of tile to decode.
|
||||||
no_cxform : bool
|
ignore_pclr_cmap_cdef : bool
|
||||||
Whether or not to apply intended color transforms.
|
Whether or not to ignore the pclr, cmap, or cdef boxes during any
|
||||||
|
color transformation. Defaults to False.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|
@ -972,14 +1031,14 @@ class Jp2k(Jp2kBox):
|
||||||
dparam.tile_index = tile
|
dparam.tile_index = tile
|
||||||
dparam.nb_tile_to_decode = 1
|
dparam.nb_tile_to_decode = 1
|
||||||
|
|
||||||
if no_cxform is True:
|
if ignore_pclr_cmap_cdef is True:
|
||||||
# Return raw codestream components.
|
# Return raw codestream components.
|
||||||
dparam.flags |= 1
|
dparam.flags |= 1
|
||||||
|
|
||||||
return dparam
|
return dparam
|
||||||
|
|
||||||
def read_bands(self, rlevel=0, layer=0, area=None, tile=None,
|
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.
|
"""Read a JPEG 2000 image.
|
||||||
|
|
||||||
The only time you should use this method is when the image has
|
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)
|
(first_row, first_col, last_row, last_col)
|
||||||
tile : int, optional
|
tile : int, optional
|
||||||
Number of tile to decode.
|
Number of tile to decode.
|
||||||
no_cxform : bool
|
ignore_pclr_cmap_cdef : bool
|
||||||
Whether or not to apply intended color transforms.
|
Whether or not to ignore the pclr, cmap, or cdef boxes during any
|
||||||
|
color transformation. Defaults to False.
|
||||||
verbose : bool, optional
|
verbose : bool, optional
|
||||||
Print informational messages produced by the OpenJPEG library.
|
Print informational messages produced by the OpenJPEG library.
|
||||||
|
|
||||||
|
|
@ -1028,7 +1088,7 @@ class Jp2k(Jp2kBox):
|
||||||
"of OpenJP2 installed before using "
|
"of OpenJP2 installed before using "
|
||||||
"this functionality.")
|
"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:
|
with ExitStack() as stack:
|
||||||
if hasattr(opj2.OPENJP2,
|
if hasattr(opj2.OPENJP2,
|
||||||
|
|
@ -1090,7 +1150,7 @@ class Jp2k(Jp2kBox):
|
||||||
>>> codestream = jp2.get_codestream()
|
>>> codestream = jp2.get_codestream()
|
||||||
>>> print(codestream.segment[1])
|
>>> print(codestream.segment[1])
|
||||||
SIZ marker segment @ (3233, 47)
|
SIZ marker segment @ (3233, 47)
|
||||||
Profile: 2
|
Profile: no profile
|
||||||
Reference Grid Height, Width: (1456 x 2592)
|
Reference Grid Height, Width: (1456 x 2592)
|
||||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||||
Reference Tile Height, Width: (1456 x 2592)
|
Reference Tile Height, Width: (1456 x 2592)
|
||||||
|
|
@ -1190,12 +1250,26 @@ def _validate_jp2_box_sequence(boxes):
|
||||||
if boxes[1].brand == 'jpx ':
|
if boxes[1].brand == 'jpx ':
|
||||||
_validate_jpx_box_sequence(boxes)
|
_validate_jpx_box_sequence(boxes)
|
||||||
else:
|
else:
|
||||||
|
# Validate the JP2 box IDs.
|
||||||
count = _collect_box_count(boxes)
|
count = _collect_box_count(boxes)
|
||||||
for id in count.keys():
|
for box_id in count.keys():
|
||||||
if id not in JP2_IDS:
|
if box_id not in JP2_IDS:
|
||||||
msg = "The presence of a '{0}' box requires that the file type "
|
msg = "The presence of a '{0}' box requires that the file type "
|
||||||
msg += "brand be set to 'jpx '."
|
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):
|
def _validate_jpx_box_sequence(boxes):
|
||||||
"""Run through series of tests for JPX box legality."""
|
"""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."""
|
"""Certain boxes can only reside in the JP2 header."""
|
||||||
box_ids = set([box.box_id for box in boxes])
|
box_ids = set([box.box_id for box in boxes])
|
||||||
intersection = box_ids.intersection(JP2H_CHILDREN)
|
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."
|
msg = "A '{0}' box can only be nested in a JP2 header box."
|
||||||
raise IOError(msg.format(list(intersection)[0]))
|
raise IOError(msg.format(list(intersection)[0]))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -412,7 +412,7 @@ Contiguous Codestream Box (jp2c) @ (3223, 1132296)
|
||||||
Main header:
|
Main header:
|
||||||
SOC marker segment @ (3231, 0)
|
SOC marker segment @ (3231, 0)
|
||||||
SIZ marker segment @ (3233, 47)
|
SIZ marker segment @ (3233, 47)
|
||||||
Profile: 2
|
Profile: no profile
|
||||||
Reference Grid Height, Width: (1456 x 2592)
|
Reference Grid Height, Width: (1456 x 2592)
|
||||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||||
Reference Tile Height, Width: (1456 x 2592)
|
Reference Tile Height, Width: (1456 x 2592)
|
||||||
|
|
@ -477,7 +477,7 @@ Contiguous Codestream Box (jp2c) @ (3223, 1132296)
|
||||||
Main header:
|
Main header:
|
||||||
SOC marker segment @ (3231, 0)
|
SOC marker segment @ (3231, 0)
|
||||||
SIZ marker segment @ (3233, 47)
|
SIZ marker segment @ (3233, 47)
|
||||||
Profile: 2
|
Profile: no profile
|
||||||
Reference Grid Height, Width: (1456 x 2592)
|
Reference Grid Height, Width: (1456 x 2592)
|
||||||
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
Vertical, Horizontal Reference Grid Offset: (0 x 0)
|
||||||
Reference Tile Height, Width: (1456 x 2592)
|
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)
|
issue_182_cmap = """Component Mapping Box (cmap) @ (130, 24)
|
||||||
Component 0 ==> palette column 0
|
Component 0 ==> palette column 0
|
||||||
Component 1 ==> palette column 0
|
Component 0 ==> palette column 1
|
||||||
Component 2 ==> 2"""
|
Component 0 ==> palette column 2
|
||||||
|
Component 0 ==> palette column 3"""
|
||||||
|
|
||||||
issue_183_colr = """Colour Specification Box (colr) @ (62, 12)
|
issue_183_colr = """Colour Specification Box (colr) @ (62, 12)
|
||||||
Method: restricted ICC profile
|
Method: restricted ICC profile
|
||||||
|
|
@ -610,3 +611,14 @@ issue_186_progression_order = """COD marker segment @ (174, 12)
|
||||||
Vertically stripe causal context: False
|
Vertically stripe causal context: False
|
||||||
Predictable termination: False
|
Predictable termination: False
|
||||||
Segmentation symbols: 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):
|
def tearDown(self):
|
||||||
pass
|
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):
|
def test_invalid_progression_order(self):
|
||||||
"""Should still be able to parse even if prog order is invalid."""
|
"""Should still be able to parse even if prog order is invalid."""
|
||||||
jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2')
|
jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2')
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import datetime
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
import warnings
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
@ -30,6 +31,9 @@ class TestICC(unittest.TestCase):
|
||||||
def test_file5(self):
|
def test_file5(self):
|
||||||
"""basic ICC profile"""
|
"""basic ICC profile"""
|
||||||
filename = opj_data_file('input/conformance/file5.jp2')
|
filename = opj_data_file('input/conformance/file5.jp2')
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# The file has a bad compatibility list entry. Not important here.
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
j = Jp2k(filename)
|
j = Jp2k(filename)
|
||||||
profile = j.box[2].box[1].icc_profile
|
profile = j.box[2].box[1].icc_profile
|
||||||
self.assertEqual(profile['Size'], 546)
|
self.assertEqual(profile['Size'], 546)
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,23 @@ class TestDataEntryURL(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.jp2file = glymur.data.nemo()
|
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):
|
def test_basic_url(self):
|
||||||
"""Just your most basic URL box."""
|
"""Just your most basic URL box."""
|
||||||
# Wrap our j2k file in a JP2 box along with an interior 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)):
|
with self.assertRaises((IOError, OSError)):
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
j2k.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||||
def test_bad_type(self):
|
def test_bad_type(self):
|
||||||
"""Channel types are limited to 0, 1, 2, 65535
|
"""Channel types are limited to 0, 1, 2, 65535
|
||||||
Should reject if not all of index, channel_type, association the
|
Should reject if not all of index, channel_type, association the
|
||||||
|
|
@ -347,17 +365,18 @@ class TestChannelDefinition(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
channel_type = (COLOR, COLOR, 3)
|
channel_type = (COLOR, COLOR, 3)
|
||||||
association = (RED, GREEN, BLUE)
|
association = (RED, GREEN, BLUE)
|
||||||
with self.assertRaises(IOError):
|
with self.assertWarns(UserWarning):
|
||||||
glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type,
|
glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type,
|
||||||
association=association)
|
association=association)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||||
def test_wrong_lengths(self):
|
def test_wrong_lengths(self):
|
||||||
"""Should reject if not all of index, channel_type, association the
|
"""Should reject if not all of index, channel_type, association the
|
||||||
same length.
|
same length.
|
||||||
"""
|
"""
|
||||||
channel_type = (COLOR, COLOR)
|
channel_type = (COLOR, COLOR)
|
||||||
association = (RED, GREEN, BLUE)
|
association = (RED, GREEN, BLUE)
|
||||||
with self.assertRaises(IOError):
|
with self.assertWarns(UserWarning):
|
||||||
glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type,
|
glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type,
|
||||||
association=association)
|
association=association)
|
||||||
|
|
||||||
|
|
@ -373,6 +392,8 @@ class TestFileTypeBox(unittest.TestCase):
|
||||||
|
|
||||||
def test_brand_unknown(self):
|
def test_brand_unknown(self):
|
||||||
"""A ftyp box brand must be 'jp2 ' or 'jpx '."""
|
"""A ftyp box brand must be 'jp2 ' or 'jpx '."""
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
ftyp = glymur.jp2box.FileTypeBox(brand='jp3')
|
ftyp = glymur.jp2box.FileTypeBox(brand='jp3')
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
with tempfile.TemporaryFile() as tfile:
|
with tempfile.TemporaryFile() as tfile:
|
||||||
|
|
@ -380,6 +401,9 @@ class TestFileTypeBox(unittest.TestCase):
|
||||||
|
|
||||||
def test_cl_entry_unknown(self):
|
def test_cl_entry_unknown(self):
|
||||||
"""A ftyp box cl list can only contain 'jp2 ', 'jpx ', or 'jpxb'."""
|
"""A ftyp box cl list can only contain 'jp2 ', 'jpx ', or 'jpxb'."""
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# Bad compatibility list item.
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
ftyp = glymur.jp2box.FileTypeBox(compatibility_list=['jp3'])
|
ftyp = glymur.jp2box.FileTypeBox(compatibility_list=['jp3'])
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
with tempfile.TemporaryFile() as tfile:
|
with tempfile.TemporaryFile() as tfile:
|
||||||
|
|
@ -429,6 +453,18 @@ class TestColourSpecificationBox(unittest.TestCase):
|
||||||
with self.assertRaises(IOError):
|
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.")
|
||||||
|
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):
|
def test_default_colr(self):
|
||||||
"""basic colr instantiation"""
|
"""basic colr instantiation"""
|
||||||
colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
|
colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
|
||||||
|
|
@ -438,27 +474,30 @@ class TestColourSpecificationBox(unittest.TestCase):
|
||||||
self.assertEqual(colr.colorspace, glymur.core.SRGB)
|
self.assertEqual(colr.colorspace, glymur.core.SRGB)
|
||||||
self.assertIsNone(colr.icc_profile)
|
self.assertIsNone(colr.icc_profile)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
|
||||||
def test_colr_with_cspace_and_icc(self):
|
def test_colr_with_cspace_and_icc(self):
|
||||||
"""Colour specification boxes can't have both."""
|
"""Colour specification boxes can't have both."""
|
||||||
with self.assertRaises((OSError, IOError)):
|
with self.assertWarns(UserWarning):
|
||||||
colorspace = glymur.core.SRGB
|
colorspace = glymur.core.SRGB
|
||||||
rawb = b'\x01\x02\x03\x04'
|
rawb = b'\x01\x02\x03\x04'
|
||||||
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||||
icc_profile=rawb)
|
icc_profile=rawb)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
|
||||||
def test_colr_with_bad_method(self):
|
def test_colr_with_bad_method(self):
|
||||||
"""colr must have a valid method field"""
|
"""colr must have a valid method field"""
|
||||||
colorspace = glymur.core.SRGB
|
colorspace = glymur.core.SRGB
|
||||||
method = -1
|
method = -1
|
||||||
with self.assertRaises(IOError):
|
with self.assertWarns(UserWarning):
|
||||||
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||||
method=method)
|
method=method)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03030000, "Requires 3.3+")
|
||||||
def test_colr_with_bad_approx(self):
|
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
|
colorspace = glymur.core.SRGB
|
||||||
approx = -1
|
approx = -1
|
||||||
with self.assertRaises(IOError):
|
with self.assertWarns(UserWarning):
|
||||||
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
|
||||||
approximation=approx)
|
approximation=approx)
|
||||||
|
|
||||||
|
|
@ -484,21 +523,23 @@ class TestPaletteBox(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||||
def test_mismatched_bitdepth_signed(self):
|
def test_mismatched_bitdepth_signed(self):
|
||||||
"""bitdepth and signed arguments must have equal length"""
|
"""bitdepth and signed arguments must have equal length"""
|
||||||
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
|
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
|
||||||
bps = (8, 8, 8)
|
bps = (8, 8, 8)
|
||||||
signed = (False, False)
|
signed = (False, False)
|
||||||
with self.assertRaises(IOError):
|
with self.assertWarns(UserWarning):
|
||||||
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
|
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
|
||||||
signed=signed)
|
signed=signed)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||||
def test_mismatched_signed_palette(self):
|
def test_mismatched_signed_palette(self):
|
||||||
"""bitdepth and signed arguments must have equal length"""
|
"""bitdepth and signed arguments must have equal length"""
|
||||||
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
|
palette = np.array([[255, 0, 255], [0, 255, 0]], dtype=np.uint8)
|
||||||
bps = (8, 8, 8, 8)
|
bps = (8, 8, 8, 8)
|
||||||
signed = (False, False, False, False)
|
signed = (False, False, False, False)
|
||||||
with self.assertRaises(IOError):
|
with self.assertWarns(UserWarning):
|
||||||
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
|
pclr = glymur.jp2box.PaletteBox(palette, bits_per_component=bps,
|
||||||
signed=signed)
|
signed=signed)
|
||||||
|
|
||||||
|
|
@ -666,8 +707,9 @@ class TestWrap(unittest.TestCase):
|
||||||
def test_jpx_to_jp2(self):
|
def test_jpx_to_jp2(self):
|
||||||
"""basic test for rewrapping a jpx file"""
|
"""basic test for rewrapping a jpx file"""
|
||||||
jpx = Jp2k(self.jpxfile)
|
jpx = Jp2k(self.jpxfile)
|
||||||
idx = [0, 1, 3, 6]
|
# Use only the signature, file type, header, and 1st codestream.
|
||||||
boxes = [jpx.box[idx] for idx in [0, 1, 3, 6]]
|
lst = [0, 1, 2, 5]
|
||||||
|
boxes = [jpx.box[idx] for idx in lst]
|
||||||
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
|
||||||
jp2 = jpx.wrap(tfile.name, boxes=boxes)
|
jp2 = jpx.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
|
|
@ -828,6 +870,67 @@ class TestWrap(unittest.TestCase):
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
j2k.wrap(tfile.name, boxes=boxes)
|
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):
|
class TestJp2Boxes(unittest.TestCase):
|
||||||
"""Tests for canonical JP2 boxes."""
|
"""Tests for canonical JP2 boxes."""
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,22 @@ import struct
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
import warnings
|
||||||
|
|
||||||
import lxml.etree as ET
|
import lxml.etree 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 DataEntryURLBox, FileTypeBox, JPEG2000SignatureBox
|
||||||
from glymur.jp2box import DataReferenceBox, FragmentListBox, FragmentTableBox
|
from glymur.jp2box import DataReferenceBox, FragmentListBox, FragmentTableBox
|
||||||
|
from glymur.jp2box import ColourSpecificationBox
|
||||||
|
|
||||||
@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):
|
||||||
"""Test suite for wrapping JPX files."""
|
"""Test suite for wrapping JPX files."""
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
|
@ -107,6 +111,68 @@ class TestJPXWrap(unittest.TestCase):
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
jp2.wrap(tfile.name, boxes=boxes)
|
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):
|
def test_ftbl(self):
|
||||||
"""Write a fragment table box."""
|
"""Write a fragment table box."""
|
||||||
# Add a negative test where offset < 0
|
# Add a negative test where offset < 0
|
||||||
|
|
@ -207,13 +273,14 @@ class TestJPXWrap(unittest.TestCase):
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
jp2.wrap(tfile.name, boxes=boxes)
|
jp2.wrap(tfile.name, boxes=boxes)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||||
def test_deurl_child_of_dtbl(self):
|
def test_deurl_child_of_dtbl(self):
|
||||||
"""Data reference boxes can only contain data entry url boxes."""
|
"""Data reference boxes can only contain data entry url boxes."""
|
||||||
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]]
|
||||||
|
|
||||||
ftyp = glymur.jp2box.FileTypeBox()
|
ftyp = glymur.jp2box.FileTypeBox()
|
||||||
with self.assertRaises(IOError):
|
with self.assertWarns(UserWarning):
|
||||||
dref = glymur.jp2box.DataReferenceBox([ftyp])
|
dref = glymur.jp2box.DataReferenceBox([ftyp])
|
||||||
|
|
||||||
# Try to get around it by appending the ftyp box after creation.
|
# Try to get around it by appending the ftyp box after creation.
|
||||||
|
|
@ -337,6 +404,8 @@ class TestJPX(unittest.TestCase):
|
||||||
offset = [89]
|
offset = [89]
|
||||||
length = [1132288]
|
length = [1132288]
|
||||||
reference = [0, 0]
|
reference = [0, 0]
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
with tempfile.TemporaryFile() as tfile:
|
with tempfile.TemporaryFile() as tfile:
|
||||||
|
|
@ -347,8 +416,10 @@ class TestJPX(unittest.TestCase):
|
||||||
offset = [0]
|
offset = [0]
|
||||||
length = [1132288]
|
length = [1132288]
|
||||||
reference = [0]
|
reference = [0]
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises((IOError, OSError)):
|
||||||
with tempfile.TemporaryFile() as tfile:
|
with tempfile.TemporaryFile() as tfile:
|
||||||
flst.write(tfile)
|
flst.write(tfile)
|
||||||
|
|
||||||
|
|
@ -357,6 +428,8 @@ class TestJPX(unittest.TestCase):
|
||||||
offset = [89]
|
offset = [89]
|
||||||
length = [0]
|
length = [0]
|
||||||
reference = [0]
|
reference = [0]
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
flst = glymur.jp2box.FragmentListBox(offset, length, reference)
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
with tempfile.TemporaryFile() as tfile:
|
with tempfile.TemporaryFile() as tfile:
|
||||||
|
|
@ -364,6 +437,8 @@ class TestJPX(unittest.TestCase):
|
||||||
|
|
||||||
def test_ftbl_boxes_empty(self):
|
def test_ftbl_boxes_empty(self):
|
||||||
"""A fragment table box must have at least one child box."""
|
"""A fragment table box must have at least one child box."""
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
ftbl = glymur.jp2box.FragmentTableBox()
|
ftbl = glymur.jp2box.FragmentTableBox()
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
with tempfile.TemporaryFile() as tfile:
|
with tempfile.TemporaryFile() as tfile:
|
||||||
|
|
@ -377,6 +452,7 @@ class TestJPX(unittest.TestCase):
|
||||||
with tempfile.TemporaryFile() as tfile:
|
with tempfile.TemporaryFile() as tfile:
|
||||||
ftbl.write(tfile)
|
ftbl.write(tfile)
|
||||||
|
|
||||||
|
@unittest.skip("No such jpx file anymore.")
|
||||||
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)
|
||||||
|
|
@ -386,7 +462,7 @@ 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.")
|
@unittest.skip("Requires unnecessarily complicated code")
|
||||||
def test_unknown_superbox(self):
|
def test_unknown_superbox(self):
|
||||||
"""Verify that we can handle an unknown superbox."""
|
"""Verify that we can handle an unknown superbox."""
|
||||||
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
|
with tempfile.NamedTemporaryFile(suffix='.jpx') as tfile:
|
||||||
|
|
@ -402,15 +478,9 @@ class TestJPX(unittest.TestCase):
|
||||||
|
|
||||||
with self.assertWarns(UserWarning):
|
with self.assertWarns(UserWarning):
|
||||||
jpx = Jp2k(tfile.name)
|
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')
|
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):
|
def test_data_reference_requires_dtbl(self):
|
||||||
"""The existance of a data reference box requires a ftbl box as well."""
|
"""The existance of a data reference box requires a ftbl box as well."""
|
||||||
flag = 0
|
flag = 0
|
||||||
|
|
@ -483,17 +553,17 @@ class TestJPX(unittest.TestCase):
|
||||||
self.assertEqual(jpx.box[-1].box[0].data_reference, (3,))
|
self.assertEqual(jpx.box[-1].box[0].data_reference, (3,))
|
||||||
|
|
||||||
def test_nlst(self):
|
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)
|
j = Jp2k(self.jpxfile)
|
||||||
self.assertEqual(j.box[16].box[1].box[0].box_id, 'nlst')
|
nlst = j.box[12].box[0].box[0]
|
||||||
self.assertEqual(type(j.box[16].box[1].box[0]),
|
self.assertEqual(nlst.box_id, 'nlst')
|
||||||
glymur.jp2box.NumberListBox)
|
self.assertEqual(type(nlst), glymur.jp2box.NumberListBox)
|
||||||
|
|
||||||
# Two associations.
|
# Two associations.
|
||||||
self.assertEqual(len(j.box[16].box[1].box[0].associations), 2)
|
self.assertEqual(len(nlst.associations), 2)
|
||||||
|
|
||||||
# Codestream 0
|
# 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
|
# 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):
|
def tearDown(self):
|
||||||
pass
|
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):
|
def test_repr(self):
|
||||||
"""Verify that results of __repr__ are eval-able."""
|
"""Verify that results of __repr__ are eval-able."""
|
||||||
j = Jp2k(self.j2kfile)
|
j = Jp2k(self.j2kfile)
|
||||||
|
|
@ -92,23 +110,6 @@ class TestJp2k(unittest.TestCase):
|
||||||
with self.assertRaises(IOError):
|
with self.assertRaises(IOError):
|
||||||
Jp2k(filename)
|
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):
|
def test_file_not_present(self):
|
||||||
"""Should error out if reading from a file that does not exist"""
|
"""Should error out if reading from a file that does not exist"""
|
||||||
# Verify that we error out appropriately if not given an existing file
|
# 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):
|
class TestJp2kOpjDataRoot(unittest.TestCase):
|
||||||
"""These tests should be run by just about all configuration."""
|
"""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):
|
def test_no_cxform_pclr_jp2(self):
|
||||||
"""Indices for pclr jpxfile if no color transform"""
|
"""Indices for pclr jpxfile if no color transform"""
|
||||||
filename = opj_data_file('input/conformance/file9.jp2')
|
filename = opj_data_file('input/conformance/file9.jp2')
|
||||||
j = Jp2k(filename)
|
j = Jp2k(filename)
|
||||||
rgb = j.read()
|
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(rgb.shape, (512, 768, 3))
|
||||||
self.assertEqual(idx.shape, (512, 768))
|
self.assertEqual(idx.shape, (512, 768))
|
||||||
|
|
||||||
|
|
@ -803,9 +840,12 @@ class TestJp2kOpjDataRoot(unittest.TestCase):
|
||||||
# This file has the components physically reversed. The cmap box
|
# This file has the components physically reversed. The cmap box
|
||||||
# tells the decoder how to order them, but this flag prevents that.
|
# tells the decoder how to order them, but this flag prevents that.
|
||||||
filename = opj_data_file('input/conformance/file2.jp2')
|
filename = opj_data_file('input/conformance/file2.jp2')
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# The file has a bad compatibility list entry. Not important here.
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
j = Jp2k(filename)
|
j = Jp2k(filename)
|
||||||
ycbcr = j.read()
|
ycbcr = j.read()
|
||||||
crcby = j.read(no_cxform=True)
|
crcby = j.read(ignore_pclr_cmap_cdef=True)
|
||||||
|
|
||||||
expected = np.zeros(ycbcr.shape, ycbcr.dtype)
|
expected = np.zeros(ycbcr.shape, ycbcr.dtype)
|
||||||
for k in range(crcby.shape[2]):
|
for k in range(crcby.shape[2]):
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,9 @@ class TestSuite(unittest.TestCase):
|
||||||
|
|
||||||
def test_ETS_JP2_file1(self):
|
def test_ETS_JP2_file1(self):
|
||||||
jfile = opj_data_file('input/conformance/file1.jp2')
|
jfile = opj_data_file('input/conformance/file1.jp2')
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# Bad compatibility list item.
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
jp2k = Jp2k(jfile)
|
jp2k = Jp2k(jfile)
|
||||||
jpdata = jp2k.read()
|
jpdata = jp2k.read()
|
||||||
self.assertEqual(jpdata.shape, (512, 768, 3))
|
self.assertEqual(jpdata.shape, (512, 768, 3))
|
||||||
|
|
@ -3114,6 +3117,9 @@ class TestSuiteDump(unittest.TestCase):
|
||||||
|
|
||||||
def test_NR_file1_dump(self):
|
def test_NR_file1_dump(self):
|
||||||
jfile = opj_data_file('input/conformance/file1.jp2')
|
jfile = opj_data_file('input/conformance/file1.jp2')
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# Bad compatibility list item.
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
jp2 = Jp2k(jfile)
|
jp2 = Jp2k(jfile)
|
||||||
|
|
||||||
ids = [box.box_id for box in jp2.box]
|
ids = [box.box_id for box in jp2.box]
|
||||||
|
|
@ -3441,7 +3447,7 @@ class TestSuiteDump(unittest.TestCase):
|
||||||
# Image header
|
# Image header
|
||||||
self.assertEqual(jp2.box[2].box[0].height, 400)
|
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].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].bits_per_component, 8)
|
||||||
self.assertEqual(jp2.box[2].box[0].signed, False)
|
self.assertEqual(jp2.box[2].box[0].signed, False)
|
||||||
self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet
|
self.assertEqual(jp2.box[2].box[0].compression, 7) # wavelet
|
||||||
|
|
@ -5476,7 +5482,7 @@ class TestSuiteDump(unittest.TestCase):
|
||||||
jp2 = Jp2k(jfile)
|
jp2 = Jp2k(jfile)
|
||||||
|
|
||||||
ids = [box.box_id for box in jp2.box]
|
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]
|
ids = [box.box_id for box in jp2.box[2].box]
|
||||||
self.assertEqual(ids, ['ihdr', 'colr'])
|
self.assertEqual(ids, ['ihdr', 'colr'])
|
||||||
|
|
@ -5830,9 +5836,9 @@ class TestSuiteDump(unittest.TestCase):
|
||||||
|
|
||||||
# Jp2 Header
|
# Jp2 Header
|
||||||
# Component mapping box
|
# Component mapping box
|
||||||
self.assertEqual(jp2.box[3].box[3].component_index, (0, 1, 2))
|
self.assertEqual(jp2.box[3].box[3].component_index, (0, 0, 0, 0))
|
||||||
self.assertEqual(jp2.box[3].box[3].mapping_type, (1, 1, 0))
|
self.assertEqual(jp2.box[3].box[3].mapping_type, (1, 1, 1, 1))
|
||||||
self.assertEqual(jp2.box[3].box[3].palette_index, (0, 0, 1))
|
self.assertEqual(jp2.box[3].box[3].palette_index, (0, 1, 2, 3))
|
||||||
|
|
||||||
c = jp2.box[4].main_header
|
c = jp2.box[4].main_header
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,14 @@ class TestPrinting(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
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.")
|
@unittest.skipIf(sys.hexversion < 0x03000000, "Needs unittest in 3.x.")
|
||||||
def test_unknown_superbox(self):
|
def test_unknown_superbox(self):
|
||||||
"""Verify that we can handle an unknown superbox."""
|
"""Verify that we can handle an unknown superbox."""
|
||||||
|
|
@ -60,6 +68,9 @@ class TestPrinting(unittest.TestCase):
|
||||||
# Add the header for an unknwon superbox.
|
# Add the header for an unknwon superbox.
|
||||||
write_buffer = struct.pack('>I4s', 20, 'grp '.encode())
|
write_buffer = struct.pack('>I4s', 20, 'grp '.encode())
|
||||||
tfile.write(write_buffer)
|
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)
|
write_buffer = struct.pack('>I4sI', 12, 'free'.encode(), 0)
|
||||||
tfile.write(write_buffer)
|
tfile.write(write_buffer)
|
||||||
tfile.flush()
|
tfile.flush()
|
||||||
|
|
@ -70,8 +81,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(jpx.box[-1])
|
print(jpx.box[-1])
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
lines = ['Unknown Box (grp ) @ (695609, 20)',
|
lines = ["Unknown Box (b'grp ') @ (1399071, 20)"]
|
||||||
' Free Box (free) @ (695617, 12)']
|
|
||||||
expected = '\n'.join(lines)
|
expected = '\n'.join(lines)
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
|
@ -341,7 +351,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
actual = fake_out.getvalue().strip()
|
actual = fake_out.getvalue().strip()
|
||||||
|
|
||||||
lines = ['SIZ marker segment @ (3233, 47)',
|
lines = ['SIZ marker segment @ (3233, 47)',
|
||||||
' Profile: 2',
|
' Profile: no profile',
|
||||||
' Reference Grid Height, Width: (1456 x 2592)',
|
' Reference Grid Height, Width: (1456 x 2592)',
|
||||||
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
||||||
' Reference Tile Height, Width: (1456 x 2592)',
|
' Reference Tile Height, Width: (1456 x 2592)',
|
||||||
|
|
@ -414,7 +424,7 @@ class TestPrinting(unittest.TestCase):
|
||||||
lst = ['Codestream:',
|
lst = ['Codestream:',
|
||||||
' SOC marker segment @ (3231, 0)',
|
' SOC marker segment @ (3231, 0)',
|
||||||
' SIZ marker segment @ (3233, 47)',
|
' SIZ marker segment @ (3233, 47)',
|
||||||
' Profile: 2',
|
' Profile: no profile',
|
||||||
' Reference Grid Height, Width: (1456 x 2592)',
|
' Reference Grid Height, Width: (1456 x 2592)',
|
||||||
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
' Vertical, Horizontal Reference Grid Offset: (0 x 0)',
|
||||||
' Reference Tile Height, Width: (1456 x 2592)',
|
' Reference Tile Height, Width: (1456 x 2592)',
|
||||||
|
|
@ -645,6 +655,43 @@ class TestPrintingOpjDataRoot(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
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):
|
def test_invalid_progression_order(self):
|
||||||
"""Should still be able to print even if prog order is invalid."""
|
"""Should still be able to print even if prog order is invalid."""
|
||||||
jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2')
|
jfile = opj_data_file('input/nonregression/2977.pdf.asan.67.2198.jp2')
|
||||||
|
|
@ -818,6 +865,9 @@ class TestPrintingOpjDataRoot(unittest.TestCase):
|
||||||
def test_channel_definition(self):
|
def test_channel_definition(self):
|
||||||
"""verify printing of cdef box"""
|
"""verify printing of cdef box"""
|
||||||
filename = opj_data_file('input/conformance/file2.jp2')
|
filename = opj_data_file('input/conformance/file2.jp2')
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# Bad compatibility list item.
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
j = glymur.Jp2k(filename)
|
j = glymur.Jp2k(filename)
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
with patch('sys.stdout', new=StringIO()) as fake_out:
|
||||||
print(j.box[2].box[2])
|
print(j.box[2].box[2])
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ License: MIT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import numpy as np
|
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
|
import lxml.etree
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from .lib import openjpeg as opj
|
from .lib import openjpeg as opj
|
||||||
from .lib import openjp2 as opj2
|
from .lib import openjp2 as opj2
|
||||||
|
|
||||||
|
|
@ -48,10 +50,12 @@ OPENJPEG {openjpeg}
|
||||||
Python {python}
|
Python {python}
|
||||||
sys.platform {platform}
|
sys.platform {platform}
|
||||||
sys.maxsize {maxsize}
|
sys.maxsize {maxsize}
|
||||||
|
lxml {elxml}
|
||||||
numpy {numpy}
|
numpy {numpy}
|
||||||
""".format(glymur=version,
|
""".format(glymur=version,
|
||||||
openjpeg=openjpeg_version,
|
openjpeg=openjpeg_version,
|
||||||
python=sys.version,
|
python=sys.version,
|
||||||
platform=sys.platform,
|
platform=sys.platform,
|
||||||
maxsize=sys.maxsize,
|
maxsize=sys.maxsize,
|
||||||
|
elxml=lxml.etree.__version__,
|
||||||
numpy=np.__version__)
|
numpy=np.__version__)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue