Merge branch 'issue41' into devel

This commit is contained in:
John Evans 2013-06-27 16:36:28 -04:00
commit b946d9b519
15 changed files with 1163 additions and 103 deletions

View file

@ -1,3 +1,7 @@
Jun 27, 2013 - v0.1.10 Can wrap codestreams in custom JP2 jackets. Exposing
parameter to specify multi component transform. Added a raw codestream
file.
Jun 16, 2013 - v0.1.9 Reading ICC profile headers as ordered dicts. Exif
dictionaries changed to ordered dicts. Honoring XDG_CONFIG_HOME
environment variable.

View file

@ -5,7 +5,7 @@ API
Jp2k
----
.. autoclass:: glymur.Jp2k
:members: read, write, read_bands, get_codestream
:members: read, write, wrap, read_bands, get_codestream
Individual Boxes
----------------
@ -24,9 +24,9 @@ ColourSpecificationBox
.. autoclass:: glymur.jp2box.ColourSpecificationBox
:members:
ComponentDefinitionBox
ChannelDefinitionBox
''''''''''''''''''''''
.. autoclass:: glymur.jp2box.ComponentDefinitionBox
.. autoclass:: glymur.jp2box.ChannelDefinitionBox
:members:
ComponentMappingBox

View file

@ -34,6 +34,91 @@ codestream box, only the main header is printed. It is possible to print
>>> print(j.get_codestream())
Add XML Metadata?
=================
An existing raw codestream (or JP2 file) can be wrapped (re-wrapped) in a
user-defined set of JP2 boxes. To get just a minimal JP2 jacket on the
codestream provided by `goodstuff.j2k`, you can use the **wrap** method with
no box argument: ::
>>> import glymur
>>> jfile = glymur.data.goodstuff()
>>> j2k = glymur.Jp2k(jfile)
>>> jp2 = j2k.wrap("newfile.jp2")
>>> print(jp2)
File: newfile.jp2
JPEG 2000 Signature Box (jP ) @ (0, 12)
Signature: 0d0a870a
File Type Box (ftyp) @ (12, 20)
Brand: jp2
Compatibility: ['jp2 ']
JP2 Header Box (jp2h) @ (32, 45)
Image Header Box (ihdr) @ (40, 22)
Size: [800 480 3]
Bitdepth: 8
Signed: False
Compression: wavelet
Colorspace Unknown: False
Colour Specification Box (colr) @ (62, 15)
Method: enumerated colorspace
Precedence: 0
Colorspace: sRGB
Contiguous Codestream Box (jp2c) @ (77, 115228)
Main header:
.
. (truncated)
.
The raw codestream was wrapped in a JP2 jacket with four boxes in the outer
layer (the signature, file type, JP2 header, and contiguous codestream), with
two additional boxes (image header and color specification) contained in the
JP2 header superbox.
XML boxes are not in the minimal set of box requirements for the JP2 format, so
in order to add an XML box into the mix, we'll need to specify all of the
boxes. If you already have a JP2 jacket in place, you can just reuse it,
though. Take the following example content in an XML file `favorites.xml` : ::
<?xml version="1.0"?>
<favorite_things>
<category>Light Ale</category>
</favorite_things>
and add it after the JP2 header box, but before the codestream box ::
>>> boxes = jp2.box # The box attribute is the list of JP2 boxes
>>> xmlbox = glymur.jp2box.XMLBox(file='favorites.xml')
>>> boxes.insert(3, xmlbox)
>>> jp2_xml = jp2.wrap("newfile_with_xml.jp2", boxes=boxes)
>>> print(jp2_xml)
File: newfile_with_xml.jp2
JPEG 2000 Signature Box (jP ) @ (0, 12)
Signature: 0d0a870a
File Type Box (ftyp) @ (12, 20)
Brand: jp2
Compatibility: ['jp2 ']
JP2 Header Box (jp2h) @ (32, 45)
Image Header Box (ihdr) @ (40, 22)
Size: [800 480 3]
Bitdepth: 8
Signed: False
Compression: wavelet
Colorspace Unknown: False
Colour Specification Box (colr) @ (62, 15)
Method: enumerated colorspace
Precedence: 0
Colorspace: sRGB
XML Box (xml ) @ (77, 76)
<favorite_things>
<category>Light Ale</category>
</favorite_things>
Contiguous Codestream Box (jp2c) @ (153, 115236)
Main header:
.
. (truncated)
.
Work with XMP UUIDs?
====================
The example JP2 file shipped with glymur has an XMP UUID. ::

View file

@ -175,7 +175,7 @@ class Codestream(object):
segment = self._parseGenericSegment(f, marker_id)
else:
msg = 'Invalid marker id encountered at byte {0:d}'
msg = 'Invalid marker id encountered at byte {0:d} '
msg += 'in codestream: "0x{1:x}"'
msg = msg.format(offset, marker_id)
raise IOError(msg)

View file

@ -74,6 +74,12 @@ _color_type_map_display = {
_PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity',
_UNSPECIFIED: 'unspecified'}
# color channel definitions.
RED = 1
GREEN = 2
BLUE = 3
GREY = 1
# enumerated color channel associations
_rgb_colorspace = {"R": 1, "G": 2, "B": 3}
_greyscale_colorspace = {"Y": 1}

View file

@ -1,8 +1,10 @@
"""Shipping JPEG 2000 files.
There is only one JP2 file at the moment, "nemo.jp2", converted from the
original JPEG photo of the aftermath of NEMO, the nor'easter that shutdown
Boston in February of 2013.
These include:
nemo.jp2: converted from the original JPEG photo of the aftermath of NEMO,
the nor'easter that shutdown Boston in February of 2013.
goodstuff.j2k: my favorite bevorage.
"""
import pkg_resources
@ -17,3 +19,15 @@ def nemo():
"""
file = pkg_resources.resource_filename(__name__, "nemo.jp2")
return file
def goodstuff():
"""Shortcut for specifying path to goodstuff.j2k.
Returns
-------
file : str
Platform-independent path to goodstuff.j2k.
"""
file = pkg_resources.resource_filename(__name__, "goodstuff.j2k")
return file

BIN
glymur/data/goodstuff.j2k Normal file

Binary file not shown.

View file

@ -31,6 +31,7 @@ from .core import _colorspace_map_display
from .core import _color_type_map_display
from .core import _method_display
from .core import _reader_requirements_display
from .core import *
class Jp2kBox(object):
@ -59,6 +60,12 @@ class Jp2kBox(object):
msg += " @ ({0}, {1})".format(self.offset, self.length)
return msg
def _write(self, f):
"""Must be implemented in a subclass.
"""
msg = "Not supported for {0} box.".format(self.longname)
raise NotImplementedError(msg)
def _parse_superbox(self, f):
"""Parse a superbox (box consisting of nothing but other boxes.
@ -159,9 +166,22 @@ class ColourSpecificationBox(Jp2kBox):
ICC profile header according to ICC profile specification. If
colorspace is not None, then icc_profile must be empty.
"""
def __init__(self, **kwargs):
Jp2kBox.__init__(self, id='', longname='Colour Specification')
def __init__(self, method=ENUMERATED_COLORSPACE, precedence=0,
approximation=0, colorspace=None, icc_profile=None, **kwargs):
Jp2kBox.__init__(self, id='colr', longname='Colour Specification')
if colorspace is not None and icc_profile is not None:
raise IOError("colorspace and icc_profile cannot both be set.")
if method not in (1, 2, 3, 4):
raise IOError("Invalid method.")
if approximation not in (0, 1, 2, 3, 4):
raise IOError("Invalid approximation.")
self.__dict__.update(**kwargs)
self.method = method
self.precedence = precedence
self.approximation = approximation
self.colorspace = colorspace
self.icc_profile = icc_profile
def __str__(self):
msg = Jp2kBox.__str__(self)
@ -188,6 +208,24 @@ class ColourSpecificationBox(Jp2kBox):
return msg
def _write(self, f):
"""Write an Colour Specification box to file.
"""
if self.colorspace is None:
msg = "Writing Colour Specification boxes without enumerated "
msg += "colorspaces is not supported at this time."
raise NotImplementedError(msg)
length = 15 if self.icc_profile is None else 11 + len(self.icc_profile)
f.write(struct.pack('>I', length))
f.write('colr'.encode())
buffer = struct.pack('>BBBI',
self.method,
self.precedence,
self.approximation,
self.colorspace)
f.write(buffer)
@staticmethod
def _parse(f, id, offset, length):
"""Parse JPEG 2000 color specification box.
@ -354,7 +392,7 @@ class _ICCProfile(object):
self.header = header
class ComponentDefinitionBox(Jp2kBox):
class ChannelDefinitionBox(Jp2kBox):
"""Container for component definition box information.
Attributes
@ -367,29 +405,58 @@ class ComponentDefinitionBox(Jp2kBox):
offset of the box from the start of the file.
longname : str
more verbose description of the box.
component_number : int
number of the component
component_type : int
type of the component
index : int
number of the channel
channel_type : int
type of the channel
association : int
number of the associated color
index of the associated color
"""
def __init__(self, **kwargs):
Jp2kBox.__init__(self, id='', longname='Component Definition')
def __init__(self, index, channel_type, association, **kwargs):
Jp2kBox.__init__(self, id='cdef', longname='Channel Definition')
if len(index) != len(channel_type) or len(index) != len(association):
msg = "Length of channel definition box inputs must be the same."
raise IOError(msg)
# channel types must be one of 0, 1, 2, 65535
if any(x not in [0, 1, 2, 65535] for x in channel_type):
msg = "Channel types must be in the set of\n\n"
msg += " 0 - colour image data for associated color\n"
msg += " 1 - opacity\n"
msg += " 2 - premultiplied opacity\n"
msg += " 65535 - unspecified"
raise IOError(msg)
self.index = index
self.channel_type = channel_type
self.association = association
self.__dict__.update(**kwargs)
def __str__(self):
msg = Jp2kBox.__str__(self)
for j in range(len(self.association)):
color_type_string = _color_type_map_display[self.component_type[j]]
color_type_string = _color_type_map_display[self.channel_type[j]]
if self.association[j] == 0:
assn = 'whole image'
else:
assn = str(self.association[j])
msg += '\n Component {0} ({1}) ==> ({2})'
msg = msg.format(self.component_number[j], color_type_string, assn)
msg += '\n Channel {0} ({1}) ==> ({2})'
msg = msg.format(self.index[j], color_type_string, assn)
return msg
def _write(self, f):
"""Write a channel definition box to file.
"""
N = len(self.association)
f.write(struct.pack('>I', 8 + 2 + N * 6))
f.write('cdef'.encode('utf-8'))
f.write(struct.pack('>H', N))
for j in range(N):
f.write(struct.pack('>' + 'H' * 3,
self.index[j],
self.channel_type[j],
self.association[j]))
@staticmethod
def _parse(f, id, offset, length):
"""Parse component definition box.
@ -418,17 +485,17 @@ class ComponentDefinitionBox(Jp2kBox):
buffer = f.read(2)
N, = struct.unpack('>H', buffer)
component_number = []
component_type = []
index = []
chan_type = []
association = []
buffer = f.read(N * 6)
data = struct.unpack('>' + 'HHH' * N, buffer)
kwargs['component_number'] = data[0:N * 6:3]
kwargs['component_type'] = data[1:N * 6:3]
kwargs['association'] = data[2:N * 6:3]
index = data[0:N * 6:3]
channel_type = data[1:N * 6:3]
association = data[2:N * 6:3]
box = ComponentDefinitionBox(**kwargs)
box = ChannelDefinitionBox(index, channel_type, association, **kwargs)
return box
@ -524,9 +591,10 @@ class ContiguousCodestreamBox(Jp2kBox):
List of segments in the codestream header.
"""
def __init__(self, **kwargs):
def __init__(self, main_header=[], **kwargs):
Jp2kBox.__init__(self, id='jp2c', longname='Contiguous Codestream')
self.__dict__.update(**kwargs)
self.main_header = main_header
def __str__(self):
msg = Jp2kBox.__str__(self)
@ -591,8 +659,14 @@ class FileTypeBox(Jp2kBox):
List of file conformance profiles.
"""
def __init__(self, **kwargs):
Jp2kBox.__init__(self, id='', longname='File Type')
Jp2kBox.__init__(self, id='ftyp', longname='File Type')
self.__dict__.update(**kwargs)
if 'brand' not in kwargs.keys():
self.brand = 'jp2 '
if 'minor_version' not in kwargs.keys():
self.minor_version = 0
if 'compatibility_list' not in kwargs.keys():
self.compatibility_list = ['jp2 ']
def __str__(self):
lst = [Jp2kBox.__str__(self),
@ -603,6 +677,18 @@ class FileTypeBox(Jp2kBox):
return msg
def _write(self, f):
"""Write a File Type box to file.
"""
length = 16 + 4*len(self.compatibility_list)
f.write(struct.pack('>I', length))
f.write('ftyp'.encode())
f.write(self.brand.encode())
f.write(struct.pack('>I', self.minor_version))
for item in self.compatibility_list:
f.write(item.encode())
@staticmethod
def _parse(f, id, offset, length):
"""Parse JPEG 2000 file type box.
@ -673,30 +759,68 @@ class ImageHeaderBox(Jp2kBox):
Bits per component.
signed : bool
False if the image components are unsigned.
compression : nt
compression : int
The compression type, should be 7 if JP2.
cspace_unknown : int
0 if the color space is known and correctly specified.
ip_provided : int
0 if the file does not contain intellectual propery rights information.
colorspace_unknown : bool
False if the color space is known and correctly specified.
ip_provided : bool
False if the file does not contain intellectual propery rights
information.
"""
def __init__(self, **kwargs):
Jp2kBox.__init__(self, id='', longname='Image Header')
def __init__(self, height, width, num_components=1, signed=False,
bits_per_component=8, compression=7, colorspace_unknown=False,
ip_provided=False, **kwargs):
"""
Examples
--------
>>> import glymur
>>> box = glymur.jp2box.ImageHeaderBox(height=512, width=256)
"""
Jp2kBox.__init__(self, id='ihdr', longname='Image Header')
self.height = height
self.width = width
self.num_components = num_components
self.signed = signed
self.bits_per_component = bits_per_component
self.compression = compression
self.colorspace_unknown = colorspace_unknown
self.ip_provided = False
self.__dict__.update(**kwargs)
def __str__(self):
lst = [Jp2kBox.__str__(self)]
lst.append('Size: [{0} {1} {2}]'.format(self.height, self.width,
self.num_components))
lst.append('Bitdepth: {0}'.format(self.bits_per_component))
lst.append('Signed: {0}'.format(self.signed))
if self.compression == 7:
lst.append('Compression: wavelet')
if self.cspace_unknown:
lst.append('Colorspace Unknown: True')
else:
lst.append('Colorspace Unknown: False')
return '\n '.join(lst)
msg = Jp2kBox.__str__(self)
msg = "{0}"
msg += '\n Size: [{1} {2} {3}]'
msg += '\n Bitdepth: {4}'
msg += '\n Signed: {5}'
msg += '\n Compression: {6}'
msg += '\n Colorspace Unknown: {7}'
msg = msg.format(Jp2kBox.__str__(self),
self.height, self.width, self.num_components,
self.bits_per_component,
self.signed,
'wavelet' if self.compression == 7 else 'unknown',
self.colorspace_unknown)
return msg
def _write(self, f):
"""Write an Image Header box to file.
"""
f.write(struct.pack('>I', 22))
f.write('ihdr'.encode())
# signedness and bps are stored together in a single byte
bit_depth_signedness = 0x80 if self.signed else 0x00
bit_depth_signedness |= self.bits_per_component - 1
buffer = struct.pack('>IIHBBBB',
self.height,
self.width,
self.num_components,
bit_depth_signedness,
self.compression,
1 if self.colorspace_unknown else 0,
1 if self.ip_provided else 0)
f.write(buffer)
@staticmethod
def _parse(f, id, offset, length):
@ -724,16 +848,16 @@ class ImageHeaderBox(Jp2kBox):
# Read the box information
buffer = f.read(14)
params = struct.unpack('>IIHBBBB', buffer)
kwargs['height'] = params[0]
kwargs['width'] = params[1]
height = params[0]
width = params[1]
kwargs['num_components'] = params[2]
kwargs['bits_per_component'] = (params[3] & 0x7f) + 1
kwargs['signed'] = (params[3] & 0x80) > 1
kwargs['compression'] = params[4]
kwargs['cspace_unknown'] = params[5]
kwargs['ip_provided'] = params[6]
kwargs['colorspace_unknown'] = True if params[5] else False
kwargs['ip_provided'] = True if params[6] else False
box = ImageHeaderBox(**kwargs)
box = ImageHeaderBox(height, width, **kwargs)
return box
@ -818,7 +942,7 @@ class JP2HeaderBox(Jp2kBox):
List of boxes contained in this superbox.
"""
def __init__(self, **kwargs):
Jp2kBox.__init__(self, id='', longname='JP2 Header')
Jp2kBox.__init__(self, id='jp2h', longname='JP2 Header')
self.__dict__.update(**kwargs)
def __str__(self):
@ -831,6 +955,21 @@ class JP2HeaderBox(Jp2kBox):
msg += ''.join(strs)
return msg
def _write(self, f):
"""Write a JP2 Header box to file.
"""
# Write the contained boxes, then come back and write the length.
orig_pos = f.tell()
f.write(struct.pack('>I', 0))
f.write('jp2h'.encode())
for box in self.box:
box._write(f)
end_pos = f.tell()
f.seek(orig_pos)
f.write(struct.pack('>I', end_pos - orig_pos))
f.seek(end_pos)
@staticmethod
def _parse(f, id, offset, length):
"""Parse JPEG 2000 header box.
@ -882,8 +1021,10 @@ class JPEG2000SignatureBox(Jp2kBox):
Four-byte tuple identifying the file as JPEG 2000.
"""
def __init__(self, **kwargs):
Jp2kBox.__init__(self, id='', longname='JPEG 2000 Signature')
Jp2kBox.__init__(self, id='jP ', longname='JPEG 2000 Signature')
self.__dict__.update(**kwargs)
if 'signature' not in kwargs.keys():
self.signature = (13, 10, 135, 10)
def __str__(self):
msg = Jp2kBox.__str__(self)
@ -891,6 +1032,13 @@ class JPEG2000SignatureBox(Jp2kBox):
msg = msg.format(*self.signature)
return msg
def _write(self, f):
"""Write a JPEG 2000 Signature box to file.
"""
f.write(struct.pack('>I', 12))
f.write(self.id.encode())
f.write(struct.pack('>BBBB', *self.signature))
@staticmethod
def _parse(f, id, offset, length):
"""Parse JPEG 2000 signature box.
@ -1412,19 +1560,50 @@ class XMLBox(Jp2kBox):
xml : ElementTree.Element
XML section.
"""
def __init__(self, **kwargs):
Jp2kBox.__init__(self, id='', longname='XML')
def __init__(self, xml=None, filename=None, **kwargs):
"""
Parameters
----------
xml : ElementTree
An ElementTree object already existing in python.
filename : str
File from which to read XML. If filename is not None, then the xml
keyword argument must be None.
"""
Jp2kBox.__init__(self, id='xml ', longname='XML')
if filename is not None and xml is not None:
msg = "Only one of either filename or xml should be provided."
raise IOError(msg)
if filename is not None:
self.xml = ET.parse(filename)
else:
self.xml = xml
self.__dict__.update(**kwargs)
def __str__(self):
msg = Jp2kBox.__str__(self)
xml = self.xml
if self.xml is not None:
msg += _pretty_print_xml(self.xml)
try:
msg += _pretty_print_xml(self.xml)
except TypeError:
msg += _pretty_print_xml(self.xml.getroot())
else:
msg += '\n {0}'.format(xml)
return msg
def _write(self, f):
"""Write an XML box to file.
"""
try:
buffer = ET.tostring(self.xml, encoding='utf-8')
except AttributeError:
buffer = ET.tostring(self.xml.getroot(), encoding='utf-8')
f.write(struct.pack('>I', len(buffer) + 8))
f.write(self.id.encode())
f.write(buffer)
@staticmethod
def _parse(f, id, offset, length):
"""Parse XML box.
@ -2273,7 +2452,7 @@ class _ExifInteroperabilityIfd(_Ifd):
# Map each box ID to the corresponding class.
_box_with_id = {
'asoc': AssociationBox,
'cdef': ComponentDefinitionBox,
'cdef': ChannelDefinitionBox,
'cmap': ComponentMappingBox,
'colr': ColourSpecificationBox,
'jP ': JPEG2000SignatureBox,

View file

@ -16,8 +16,8 @@ import warnings
import numpy as np
from .codestream import Codestream
from .core import progression_order
from .jp2box import Jp2kBox
from .core import *
from .jp2box import *
from .lib import openjp2 as opj2
_cspace_map = {'rgb': opj2._CLRSPC_SRGB,
@ -141,7 +141,7 @@ class Jp2k(Jp2kBox):
def write(self, img_array, cratios=None, eph=False, psnr=None, numres=None,
cbsize=None, psizes=None, grid_offset=None, sop=False,
subsam=None, tilesize=None, prog=None, modesw=None,
colorspace=None, verbose=False):
colorspace=None, verbose=False, mct=None):
"""Write image data to a JP2/JPX/J2k file. Intended usage of the
various parameters follows that of OpenJPEG's opj_compress utility.
@ -167,6 +167,9 @@ class Jp2k(Jp2kBox):
If true, write SOP marker after each header packet.
grid_offset : tuple, optional
Offset (DY, DX) of the origin of the image in the reference grid.
mct : bool, optional
Specifies usage of the multi component transform. If not
specified, defaults to True if the colorspace is RGB.
modesw : int, optional
Mode switch.
1 = BYPASS(LAZY)
@ -336,6 +339,18 @@ class Jp2k(Jp2kBox):
else:
colorspace = _cspace_map[colorspace]
if mct is None:
if colorspace == opj2._CLRSPC_SRGB:
cparams.tcp_mct = 1
else:
cparams.tcp_mct = 0
else:
if mct and colorspace == opj2._CLRSPC_GRAY:
msg = "Cannot specify usage of the multi component transform "
msg += "if the colorspace is gray."
raise IOError(msg)
cparams.tcp_mct = 1 if mct else 0
if img_array.dtype == np.uint8:
comp_prec = 8
elif img_array.dtype == np.uint16:
@ -372,12 +387,6 @@ class Jp2k(Jp2kBox):
src = layer.ctypes.data
ctypes.memmove(dest, src, layer.nbytes)
# set multi-component transform?
if image.contents.numcomps == 3:
cparams.tcp_mct = 1
else:
cparams.tcp_mct = 0
codec = opj2._create_compress(codec_fmt)
if verbose:
@ -398,6 +407,143 @@ class Jp2k(Jp2kBox):
self._parse()
def wrap(self, filename, boxes=None):
"""Write the codestream back out to file, wrapped in new JP2 jacket.
Parameters
----------
filename : str
JP2 file to be created from a raw codestream.
boxes : list
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.
Returns
-------
jp2 : Jp2k object
Newly wrapped Jp2k object.
Examples
--------
>>> import glymur, tempfile
>>> jfile = glymur.data.goodstuff()
>>> j2k = glymur.Jp2k(jfile)
>>> tfile = tempfile.NamedTemporaryFile(suffix='jp2')
>>> jp2 = j2k.wrap(tfile.name)
"""
if boxes is None:
# Try to create a reasonable default.
boxes = [JPEG2000SignatureBox(),
FileTypeBox(),
JP2HeaderBox(),
ContiguousCodestreamBox()]
c = self.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
boxes[2].box = [ImageHeaderBox(height=height,
width=width,
num_components=num_components),
ColourSpecificationBox(colorspace=SRGB)]
# Check for a bad sequence of boxes.
# 1st two boxes must be 'jP ' and 'ftyp'
if boxes[0].id != 'jP ' or boxes[1].id != 'ftyp':
msg = "The first box must be the signature box and the second "
msg += "must be the file type box."
raise IOError(msg)
# jp2c must be preceeded by jp2h
jp2h_lst = [idx for (idx, box) in enumerate(boxes) if box.id == 'jp2h']
jp2h_idx = jp2h_lst[0]
jp2c_lst = [idx for (idx, box) in enumerate(boxes) if box.id == 'jp2c']
if len(jp2c_lst) == 0:
msg = "A codestream box must be defined in the outermost "
msg += "list of boxes."
raise IOError(msg)
jp2c_idx = jp2c_lst[0]
if jp2h_idx >= jp2c_idx:
msg = "The codestream box must be preceeded by a jp2 header box."
raise IOError(msg)
# 1st jp2 header box must be ihdr
jp2h = boxes[jp2h_idx]
if jp2h.box[0].id != 'ihdr':
msg = "The first box in the jp2 header box must be the image "
msg += "header box."
raise IOError(msg)
# colr must be present in jp2 header box.
jp2hb = jp2h.box
colr_lst = [j for (j, box) in enumerate(jp2h.box) if box.id == 'colr']
if len(colr_lst) == 0:
msg = "The jp2 header box must contain a color definition box."
raise IOError(msg)
colr = jp2h.box[colr_lst[0]]
# Any cdef box must be in the jp2 header following the image header.
cdef_lst = [j for (j, box) in enumerate(boxes) if box.id == 'cdef']
if len(cdef_lst) != 0:
msg = "Any channel defintion box must be in the JP2 header "
msg += "following the image header."
raise IOError(msg)
cdef_lst = [j for (j, box) in enumerate(jp2h.box) if box.id == 'cdef']
if len(cdef_lst) > 1:
msg = "Only one channel definition box is allowed in the "
msg += "JP2 header."
raise IOError(msg)
elif len(cdef_lst) == 1:
cdef = jp2h.box[cdef_lst[0]]
assn = cdef.association
typ = cdef.channel_type
index = cdef.index
if colr.colorspace == SRGB:
if any([chan + 1 not in assn or typ[chan] != 0
for chan in [0, 1, 2]]):
msg = "All color channels must be defined in the "
msg += "channel definition box."
raise IOError(msg)
elif colr.colorspace == GREYSCALE:
if 0 not in typ:
msg = "All color channels must be defined in the "
msg += "channel definition box."
raise IOError(msg)
with open(filename, 'wb') as ofile:
for box in boxes:
if 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.id == 'jp2c']
jp2c = jp2c[0]
ofile.write(struct.pack('>I', jp2c.length + 8))
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()
jp2 = Jp2k(filename)
return jp2
def read(self, reduce=0, layer=0, area=None, tile=None, verbose=False):
"""Read a JPEG 2000 image.

View file

@ -6,3 +6,4 @@ from .test_printing import TestPrinting as printing
from .test_opj_suite import TestSuite as suite
from .test_opj_suite_write import TestSuiteWrite as suitew
from .test_opj_suite_neg import TestSuiteNegative as suiteneg
from .test_jp2box import TestJp2Boxes as box

604
glymur/test/test_jp2box.py Normal file
View file

@ -0,0 +1,604 @@
import doctest
import tempfile
import xml.etree.cElementTree as ET
import unittest
import numpy as np
import pkg_resources
import glymur
from glymur import Jp2k
from glymur.jp2box import *
# Doc tests should be run as well.
def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite('glymur.jp2box'))
return tests
class TestChannelDefinition(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Need a one_plane plane image for greyscale testing."""
j2k = Jp2k(glymur.data.goodstuff())
data = j2k.read()
# Write the first component back out to file.
with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile:
grey_j2k = Jp2k(tfile.name, 'wb')
grey_j2k.write(data[:, :, 0])
cls.one_plane = tfile.name
# Write the first two components back out to file.
with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile:
grey_j2k = Jp2k(tfile.name, 'wb')
grey_j2k.write(data[:, :, 0:1])
cls.two_planes = tfile.name
# Write four components back out to file.
with tempfile.NamedTemporaryFile(suffix=".j2k", delete=False) as tfile:
rgba_jp2 = Jp2k(tfile.name, 'wb')
shape = (data.shape[0], data.shape[1], 1)
alpha = np.zeros((shape), dtype=data.dtype)
data4 = np.concatenate((data, alpha), axis=2)
rgba_jp2.write(data4)
cls.four_planes = tfile.name
@classmethod
def tearDownClass(cls):
os.unlink(cls.one_plane)
os.unlink(cls.two_planes)
os.unlink(cls.four_planes)
def setUp(self):
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
j2k = Jp2k(self.j2kfile)
c = j2k.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
self.jP = JPEG2000SignatureBox()
self.ftyp = FileTypeBox()
self.jp2h = JP2HeaderBox()
self.jp2c = ContiguousCodestreamBox()
self.ihdr = ImageHeaderBox(height=height, width=width,
num_components=num_components)
self.colr_rgb = ColourSpecificationBox(colorspace=glymur.core.SRGB)
self.colr_gr = ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)
def tearDown(self):
pass
def test_rgb(self):
"""Just regular RGB."""
j2k = Jp2k(self.j2kfile)
cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
channel_type=[0, 0, 0],
association=[1, 2, 3])
boxes = [self.ihdr, self.colr_rgb, cdef]
self.jp2h.box = boxes
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
j2k.wrap(tfile.name, boxes=boxes)
jp2 = Jp2k(tfile.name)
jp2h = jp2.box[2]
boxes = [box.id for box in jp2h.box]
self.assertEqual(boxes, ['ihdr', 'colr', 'cdef'])
self.assertEqual(jp2h.box[2].index, (0, 1, 2))
self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0))
self.assertEqual(jp2h.box[2].association, (1, 2, 3))
def test_rgba(self):
"""Just regular RGBA."""
j2k = Jp2k(self.four_planes)
cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2, 3],
channel_type=[0, 0, 0, 1],
association=[1, 2, 3, 0])
boxes = [self.ihdr, self.colr_rgb, cdef]
self.jp2h.box = boxes
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
j2k.wrap(tfile.name, boxes=boxes)
jp2 = Jp2k(tfile.name)
jp2h = jp2.box[2]
boxes = [box.id for box in jp2h.box]
self.assertEqual(boxes, ['ihdr', 'colr', 'cdef'])
self.assertEqual(jp2h.box[2].index, (0, 1, 2, 3))
self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0, 1))
self.assertEqual(jp2h.box[2].association, (1, 2, 3, 0))
def test_bad_rgba(self):
"""R, G, and B must be specified."""
j2k = Jp2k(self.four_planes)
cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2, 3],
channel_type=[0, 0, 1, 1],
association=[1, 2, 3, 0])
boxes = [self.ihdr, self.colr_rgb, cdef]
self.jp2h.box = boxes
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError) as ce:
j2k.wrap(tfile.name, boxes=boxes)
def test_grey(self):
"""Just regular greyscale."""
j2k = Jp2k(self.one_plane)
cdef = glymur.jp2box.ChannelDefinitionBox(index=[0],
channel_type=[0],
association=[1])
boxes = [self.ihdr, self.colr_gr, cdef]
self.jp2h.box = boxes
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
j2k.wrap(tfile.name, boxes=boxes)
jp2 = Jp2k(tfile.name)
jp2h = jp2.box[2]
boxes = [box.id for box in jp2h.box]
self.assertEqual(boxes, ['ihdr', 'colr', 'cdef'])
self.assertEqual(jp2h.box[2].index, (0,))
self.assertEqual(jp2h.box[2].channel_type, (0,))
self.assertEqual(jp2h.box[2].association, (1,))
def test_grey_alpha(self):
"""Just regular greyscale plus alpha."""
j2k = Jp2k(self.two_planes)
cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1],
channel_type=[0, 1],
association=[1, 0])
boxes = [self.ihdr, self.colr_gr, cdef]
self.jp2h.box = boxes
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
j2k.wrap(tfile.name, boxes=boxes)
jp2 = Jp2k(tfile.name)
jp2h = jp2.box[2]
boxes = [box.id for box in jp2h.box]
self.assertEqual(boxes, ['ihdr', 'colr', 'cdef'])
self.assertEqual(jp2h.box[2].index, (0, 1))
self.assertEqual(jp2h.box[2].channel_type, (0, 1))
self.assertEqual(jp2h.box[2].association, (1, 0))
def test_bad_grey_alpha(self):
"""A greyscale image with alpha layer must specify Y"""
j2k = Jp2k(self.two_planes)
# This cdef box
cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1],
channel_type=[1, 1],
association=[0, 1])
boxes = [self.ihdr, self.colr_gr, cdef]
self.jp2h.box = boxes
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises((OSError, IOError)) as ce:
j2k.wrap(tfile.name, boxes=boxes)
def test_only_one_cdef_in_jp2_header(self):
"""There can only be one channel definition box in the jp2 header."""
j2k = Jp2k(self.j2kfile)
cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
channel_type=[0, 0, 0],
association=[1, 2, 3])
boxes = [self.ihdr, cdef, self.colr_rgb, cdef]
self.jp2h.box = boxes
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
def test_not_in_jp2_header(self):
j2k = Jp2k(self.j2kfile)
boxes = [self.ihdr, self.colr_rgb]
self.jp2h.box = boxes
cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
channel_type=[0, 0, 0],
association=[1, 2, 3])
boxes = [self.jP, self.ftyp, self.jp2h, cdef, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
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
# same length.
with self.assertRaises(IOError):
box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
channel_type=[0, 0, 3],
association=[1, 2, 3])
def test_wrong_lengths(self):
# Should reject if not all of index, channel_type, association the
# same length.
with self.assertRaises(IOError):
box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2],
channel_type=[0, 0],
association=[1, 2, 3])
class TestXML(unittest.TestCase):
def setUp(self):
self.jp2file = glymur.data.nemo()
self.j2kfile = glymur.data.goodstuff()
raw_xml = b"""<?xml version="1.0"?>
<data>
<country name="Liechtenstein">
<rank>1</rank>
<year>2008</year>
<gdppc>141100</gdppc>
<neighbor name="Austria" direction="E"/>
<neighbor name="Switzerland" direction="W"/>
</country>
<country name="Singapore">
<rank>4</rank>
<year>2011</year>
<gdppc>59900</gdppc>
<neighbor name="Malaysia" direction="N"/>
</country>
<country name="Panama">
<rank>68</rank>
<year>2011</year>
<gdppc>13600</gdppc>
<neighbor name="Costa Rica" direction="W"/>
<neighbor name="Colombia" direction="E"/>
</country>
</data>"""
with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tfile:
tfile.write(raw_xml)
tfile.flush()
self.xmlfile = tfile.name
j2k = Jp2k(self.j2kfile)
c = j2k.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
self.jP = JPEG2000SignatureBox()
self.ftyp = FileTypeBox()
self.jp2h = JP2HeaderBox()
self.jp2c = ContiguousCodestreamBox()
self.ihdr = ImageHeaderBox(height=height, width=width,
num_components=num_components)
self.colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
def tearDown(self):
os.unlink(self.xmlfile)
pass
def test_negative_both_file_and_xml_provided(self):
"""The XML should come from only one source."""
j2k = Jp2k(self.j2kfile)
xml_object = ET.parse(self.xmlfile)
with self.assertRaises((IOError, OSError)) as ce:
xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile, xml=xml_object)
def test_basic_xml(self):
# Should be able to write an XMLBox.
j2k = Jp2k(self.j2kfile)
self.jp2h.box = [self.ihdr, self.colr]
the_xml = ET.fromstring('<?xml version="1.0"?><data>0</data>')
xmlb = glymur.jp2box.XMLBox(xml=the_xml)
self.assertEqual(ET.tostring(xmlb.xml),
b'<data>0</data>')
boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
j2k.wrap(tfile.name, boxes=boxes)
jp2 = Jp2k(tfile.name)
self.assertEqual(jp2.box[3].id, 'xml ')
self.assertEqual(ET.tostring(jp2.box[3].xml),
b'<data>0</data>')
def test_xml_from_file(self):
j2k = Jp2k(self.j2kfile)
self.jp2h.box = [self.ihdr, self.colr]
xmlb = glymur.jp2box.XMLBox(filename=self.xmlfile)
boxes = [self.jP, self.ftyp, self.jp2h, xmlb, self.jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
j2k.wrap(tfile.name, boxes=boxes)
jp2 = Jp2k(tfile.name)
output_boxes = [box.id for box in jp2.box]
self.assertEqual(output_boxes, ['jP ', 'ftyp', 'jp2h', 'xml ',
'jp2c'])
elts = jp2.box[3].xml.findall('country')
self.assertEqual(len(elts), 3)
neighbor = elts[1].find('neighbor')
self.assertEqual(neighbor.attrib['name'], 'Malaysia')
self.assertEqual(neighbor.attrib['direction'], 'N')
class TestColourSpecificationBox(unittest.TestCase):
def setUp(self):
self.j2kfile = glymur.data.goodstuff()
j2k = Jp2k(self.j2kfile)
c = j2k.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
self.jP = JPEG2000SignatureBox()
self.ftyp = FileTypeBox()
self.jp2h = JP2HeaderBox()
self.jp2c = ContiguousCodestreamBox()
self.ihdr = ImageHeaderBox(height=height, width=width,
num_components=num_components)
def tearDown(self):
pass
def test_color_specification_box_with_out_enumerated_colorspace(self):
j2k = Jp2k(self.j2kfile)
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
boxes[2].box = [self.ihdr, ColourSpecificationBox(colorspace=None)]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(NotImplementedError):
j2k.wrap(tfile.name, boxes=boxes)
def test_missing_colr_box(self):
j2k = Jp2k(self.j2kfile)
boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c]
boxes[2].box = [self.ihdr]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
def test_default_ColourSpecificationBox(self):
b = glymur.jp2box.ColourSpecificationBox(colorspace=glymur.core.SRGB)
self.assertEqual(b.method, glymur.core.ENUMERATED_COLORSPACE)
self.assertEqual(b.precedence, 0)
self.assertEqual(b.approximation, 0)
self.assertEqual(b.colorspace, glymur.core.SRGB)
self.assertIsNone(b.icc_profile)
def test_ColourSpecificationBox_with_colorspace_and_icc(self):
# Colour specification boxes can't have both.
with self.assertRaises((OSError, IOError)):
colorspace = glymur.core.SRGB
icc_profile = b'\x01\x02\x03\x04'
b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
icc_profile=icc_profile)
def test_ColourSpecificationBox_with_bad_method(self):
colorspace = glymur.core.SRGB
method = -1
with self.assertRaises(IOError):
b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
method=method)
def test_ColourSpecificationBox_with_bad_approximation(self):
colorspace = glymur.core.SRGB
approx = -1
with self.assertRaises(IOError):
b = glymur.jp2box.ColourSpecificationBox(colorspace=colorspace,
approximation=approx)
@unittest.skipIf(glymur.lib.openjp2._OPENJP2 is None,
"Missing openjp2 library.")
class TestJp2Boxes(unittest.TestCase):
def setUp(self):
self.j2kfile = glymur.data.goodstuff()
def tearDown(self):
pass
def test_default_JPEG2000SignatureBox(self):
# Should be able to instantiate a JPEG2000SignatureBox
b = glymur.jp2box.JPEG2000SignatureBox()
self.assertEqual(b.signature, (13, 10, 135, 10))
def test_default_FileTypeBox(self):
# Should be able to instantiate a FileTypeBox
b = glymur.jp2box.FileTypeBox()
self.assertEqual(b.brand, 'jp2 ')
self.assertEqual(b.minor_version, 0)
self.assertEqual(b.compatibility_list, ['jp2 '])
def test_default_ImageHeaderBox(self):
# Should be able to instantiate an image header box.
b = glymur.jp2box.ImageHeaderBox(height=512, width=256,
num_components=3)
self.assertEqual(b.height, 512)
self.assertEqual(b.width, 256)
self.assertEqual(b.num_components, 3)
self.assertEqual(b.bits_per_component, 8)
self.assertFalse(b.signed)
self.assertFalse(b.colorspace_unknown)
def test_default_JP2HeaderBox(self):
b1 = JP2HeaderBox()
b1.box = [ImageHeaderBox(height=512, width=256),
ColourSpecificationBox(colorspace=glymur.core.GREYSCALE)]
def test_default_ContiguousCodestreamBox(self):
b = ContiguousCodestreamBox()
self.assertEqual(b.id, 'jp2c')
self.assertEqual(b.main_header, [])
def verify_wrapped_raw(self, jp2file):
# Shared method by at least two tests.
jp2 = Jp2k(jp2file)
self.assertEqual(len(jp2.box), 4)
self.assertEqual(jp2.box[0].id, 'jP ')
self.assertEqual(jp2.box[0].offset, 0)
self.assertEqual(jp2.box[0].length, 12)
self.assertEqual(jp2.box[0].longname, 'JPEG 2000 Signature')
self.assertEqual(jp2.box[1].id, 'ftyp')
self.assertEqual(jp2.box[1].offset, 12)
self.assertEqual(jp2.box[1].length, 20)
self.assertEqual(jp2.box[1].longname, 'File Type')
self.assertEqual(jp2.box[2].id, 'jp2h')
self.assertEqual(jp2.box[2].offset, 32)
self.assertEqual(jp2.box[2].length, 45)
self.assertEqual(jp2.box[2].longname, 'JP2 Header')
self.assertEqual(jp2.box[3].id, 'jp2c')
self.assertEqual(jp2.box[3].offset, 77)
self.assertEqual(jp2.box[3].length, 115228)
# jp2h super box
self.assertEqual(len(jp2.box[2].box), 2)
self.assertEqual(jp2.box[2].box[0].id, 'ihdr')
self.assertEqual(jp2.box[2].box[0].offset, 40)
self.assertEqual(jp2.box[2].box[0].length, 22)
self.assertEqual(jp2.box[2].box[0].longname, 'Image Header')
self.assertEqual(jp2.box[2].box[0].height, 800)
self.assertEqual(jp2.box[2].box[0].width, 480)
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)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
self.assertEqual(jp2.box[2].box[1].id, 'colr')
self.assertEqual(jp2.box[2].box[1].offset, 62)
self.assertEqual(jp2.box[2].box[1].length, 15)
self.assertEqual(jp2.box[2].box[1].longname, 'Colour Specification')
self.assertEqual(jp2.box[2].box[1].precedence, 0)
self.assertEqual(jp2.box[2].box[1].approximation, 0)
self.assertEqual(jp2.box[2].box[1].colorspace, glymur.core.SRGB)
self.assertIsNone(jp2.box[2].box[1].icc_profile)
def test_wrap(self):
j2k = Jp2k(self.j2kfile)
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
j2k.wrap(tfile.name)
self.verify_wrapped_raw(tfile.name)
def test_wrap_jp2(self):
j2k = Jp2k(self.j2kfile)
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
jp2 = j2k.wrap(tfile.name)
boxes = [box.id for box in jp2.box]
self.assertEqual(boxes, ['jP ', 'ftyp', 'jp2h', 'jp2c'])
def test_default_layout_but_with_specified_boxes(self):
j2k = Jp2k(self.j2kfile)
boxes = [JPEG2000SignatureBox(),
FileTypeBox(),
JP2HeaderBox(),
ContiguousCodestreamBox()]
c = j2k.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
boxes[2].box = [ImageHeaderBox(height=height,
width=width,
num_components=num_components),
ColourSpecificationBox(colorspace=glymur.core.SRGB)]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
j2k.wrap(tfile.name, boxes=boxes)
self.verify_wrapped_raw(tfile.name)
def test_image_header_box_not_first_in_jp2_header(self):
# The specification says that ihdr must be the first box in jp2h.
j2k = Jp2k(self.j2kfile)
boxes = [JPEG2000SignatureBox(),
FileTypeBox(),
JP2HeaderBox(),
ContiguousCodestreamBox()]
c = j2k.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
boxes[2].box = [ColourSpecificationBox(colorspace=glymur.core.SRGB),
ImageHeaderBox(height=height,
width=width,
num_components=num_components)]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
def test_first_2_boxes_not_jP_and_ftyp(self):
j2k = Jp2k(self.j2kfile)
c = j2k.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
jP = JPEG2000SignatureBox()
ftyp = FileTypeBox()
jp2h = JP2HeaderBox()
jp2c = ContiguousCodestreamBox()
colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
ihdr = ImageHeaderBox(height=height, width=width,
num_components=num_components)
jp2h.box = [ihdr, colr]
boxes = [ftyp, jP, jp2h, jp2c]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
def test_jp2h_not_preceeding_jp2c(self):
j2k = Jp2k(self.j2kfile)
c = j2k.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
jP = JPEG2000SignatureBox()
ftyp = FileTypeBox()
jp2h = JP2HeaderBox()
jp2c = ContiguousCodestreamBox()
colr = ColourSpecificationBox(colorspace=glymur.core.SRGB)
ihdr = ImageHeaderBox(height=height, width=width,
num_components=num_components)
jp2h.box = [ihdr, colr]
boxes = [jP, ftyp, jp2c, jp2h]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
def test_missing_codestream(self):
j2k = Jp2k(self.j2kfile)
c = j2k.get_codestream()
height = c.segment[1].Ysiz
width = c.segment[1].Xsiz
num_components = len(c.segment[1].XRsiz)
jP = JPEG2000SignatureBox()
ftyp = FileTypeBox()
jp2h = JP2HeaderBox()
ihdr = ImageHeaderBox(height=height, width=width,
num_components=num_components)
jp2h.box = [ihdr]
boxes = [jP, ftyp, jp2h]
with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile:
with self.assertRaises(IOError):
j2k.wrap(tfile.name, boxes=boxes)
if __name__ == "__main__":
unittest.main()

View file

@ -15,7 +15,6 @@ if sys.hexversion <= 0x03030000:
else:
from unittest.mock import patch
import warnings
from xml.etree import cElementTree as ET
import numpy as np
import pkg_resources
@ -77,6 +76,8 @@ class TestJp2k(unittest.TestCase):
def setUp(self):
self.jp2file = pkg_resources.resource_filename(glymur.__name__,
"data/nemo.jp2")
self.j2kfile = pkg_resources.resource_filename(glymur.__name__,
"data/goodstuff.j2k")
def tearDown(self):
pass
@ -153,6 +154,27 @@ class TestJp2k(unittest.TestCase):
subsetdata = j.read(area=(0, 0, 512, 512))
np.testing.assert_array_equal(tiledata, subsetdata)
def test_write_srgb_without_mct(self):
j2k = Jp2k(self.j2kfile)
expdata = j2k.read()
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
ofile = Jp2k(tfile.name, 'wb')
ofile.write(expdata, mct=False)
actdata = ofile.read()
np.testing.assert_array_equal(actdata, expdata)
c = ofile.get_codestream()
self.assertEqual(c.segment[2].SPcod[3], 0) # no mct
def test_write_grayscale_with_mct(self):
# MCT usage makes no sense for grayscale images.
j2k = Jp2k(self.j2kfile)
expdata = j2k.read()
with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile:
ofile = Jp2k(tfile.name, 'wb')
with self.assertRaises(IOError):
ofile.write(expdata[:, :, 0], mct=True)
def test_write_cprl(self):
# Issue 17
j = Jp2k(self.jp2file)
@ -213,7 +235,7 @@ class TestJp2k(unittest.TestCase):
self.assertEqual(jp2k.box[2].box[0].bits_per_component, 8)
self.assertEqual(jp2k.box[2].box[0].signed, False)
self.assertEqual(jp2k.box[2].box[0].compression, 7)
self.assertEqual(jp2k.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2k.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2k.box[2].box[0].ip_provided, False)
self.assertEqual(jp2k.box[2].box[1].id, 'colr')

View file

@ -4116,7 +4116,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 8)
self.assertEqual(jp2.box[3].box[0].signed, False)
self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
self.assertEqual(jp2.box[3].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@ -4158,7 +4158,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -4170,8 +4170,8 @@ class TestSuiteDump(unittest.TestCase):
# Jp2 Header
# Channel Definition
self.assertEqual(jp2.box[2].box[2].component_number, (0, 1, 2))
self.assertEqual(jp2.box[2].box[2].component_type, (0, 0, 0)) # color
self.assertEqual(jp2.box[2].box[2].index, (0, 1, 2))
self.assertEqual(jp2.box[2].box[2].channel_type, (0, 0, 0)) # color
self.assertEqual(jp2.box[2].box[2].association, (3, 2, 1)) # reverse
def test_NR_file3_dump(self):
@ -4204,7 +4204,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -4250,7 +4250,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -4292,7 +4292,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].num_components, 3)
self.assertEqual(jp2.box[3].box[0].signed, False)
self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
self.assertEqual(jp2.box[3].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@ -4338,7 +4338,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[2].box[0].bits_per_component, 12)
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].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -4387,7 +4387,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 16)
self.assertEqual(jp2.box[3].box[0].signed, False)
self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
self.assertEqual(jp2.box[3].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@ -4436,7 +4436,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -4488,7 +4488,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Palette box.
@ -5985,7 +5985,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -6118,7 +6118,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -6246,7 +6246,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -6386,7 +6386,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 8)
self.assertEqual(jp2.box[3].box[0].signed, False)
self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
self.assertEqual(jp2.box[3].box[0].cspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@ -6497,7 +6497,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, True)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -6597,7 +6597,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 8)
self.assertEqual(jp2.box[3].box[0].signed, False)
self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
self.assertEqual(jp2.box[3].box[0].cspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@ -6694,7 +6694,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, True)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -6706,8 +6706,8 @@ class TestSuiteDump(unittest.TestCase):
# Jp2 Header
# Channel Definition
self.assertEqual(jp2.box[2].box[2].component_number, (0, 1))
self.assertEqual(jp2.box[2].box[2].component_type, (0, 1)) # opacity
self.assertEqual(jp2.box[2].box[2].index, (0, 1))
self.assertEqual(jp2.box[2].box[2].channel_type, (0, 1)) # opacity
self.assertEqual(jp2.box[2].box[2].association, (0, 0)) # both main
c = jp2.box[3].main_header
@ -6813,7 +6813,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 1)
self.assertEqual(jp2.box[3].box[0].signed, False)
self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
self.assertEqual(jp2.box[3].box[0].cspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@ -6927,7 +6927,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 4)
self.assertEqual(jp2.box[3].box[0].signed, False)
self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
self.assertEqual(jp2.box[3].box[0].cspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header
@ -7035,7 +7035,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -7142,7 +7142,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -7241,7 +7241,7 @@ class TestSuiteDump(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header
@ -7343,7 +7343,7 @@ class TestSuiteDump(unittest.TestCase):
self.assertEqual(jp2.box[3].box[0].bits_per_component, 8)
self.assertEqual(jp2.box[3].box[0].signed, False)
self.assertEqual(jp2.box[3].box[0].compression, 7) # wavelet
self.assertEqual(jp2.box[3].box[0].cspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].colorspace_unknown, True)
self.assertEqual(jp2.box[3].box[0].ip_provided, False)
# Jp2 Header

View file

@ -686,7 +686,7 @@ class TestSuiteWrite(unittest.TestCase):
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
self.assertEqual(jp2.box[2].box[0].cspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].colorspace_unknown, False)
self.assertEqual(jp2.box[2].box[0].ip_provided, False)
# Jp2 Header

View file

@ -30,8 +30,7 @@ class TestPrintingNeedsLib(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Setup a plain JP2 file without the two UUID boxes.
jp2file = pkg_resources.resource_filename(glymur.__name__,
"data/nemo.jp2")
jp2file = glymur.data.nemo()
with tempfile.NamedTemporaryFile(suffix='.jp2', delete=False) as tfile:
cls._plain_nemo_file = tfile.name
ijfile = Jp2k(jp2file)
@ -676,15 +675,15 @@ class TestPrinting(unittest.TestCase):
@unittest.skipIf(data_root is None,
"OPJ_DATA_ROOT environment variable not set")
def test_component_definition(self):
def test_channel_definition(self):
filename = os.path.join(data_root, 'input/conformance/file2.jp2')
j = glymur.Jp2k(filename)
print(j.box[2].box[2])
actual = sys.stdout.getvalue().strip()
lines = ['Component Definition Box (cdef) @ (81, 28)',
' Component 0 (color) ==> (3)',
' Component 1 (color) ==> (2)',
' Component 2 (color) ==> (1)']
lines = ['Channel Definition Box (cdef) @ (81, 28)',
' Channel 0 (color) ==> (3)',
' Channel 1 (color) ==> (2)',
' Channel 2 (color) ==> (1)']
expected = '\n'.join(lines)
self.assertEqual(actual, expected)