Compare commits

..

7 commits

Author SHA1 Message Date
jevans
528fb3492d Added support for JP2 bits per component box
Closes-Issue: #321
2015-03-19 19:42:06 -04:00
jevans
d7a3c4df27 Refactor box validation when wrapping
These became functions only at the behest of pylint.  I don't really
agree with that justification anymore, and flake8 is perfectly happy
with them as methods, so back to methods they go.
2015-03-19 18:50:00 -04:00
jevans
858ecbbac8 Refactor image extraction routines
Able to combine nearly identical routines for extract imagery from
openjpeg data structures regardless of whether the image is ycbcr or
not.

Closes-Issue: #320
2015-03-18 20:30:50 -04:00
jevans
2d99154e58 Use textwrap for indenting
Use textwrap to replace hardcoding of spaces for indenting purposes.
Closes-Issue: #318
2015-03-12 07:39:56 -04:00
jevans
a3ba5a86ca Merge branch 'devel' of https://github.com/quintusdias/glymur into devel 2015-01-26 18:41:47 -05:00
John Evans
cbdd6200b6 reduce cyclomatic complexity where possible 2015-01-13 14:44:03 -05:00
jevans
2516a71bc8 merge branch 'master' into devel 2015-01-10 20:00:31 -05:00
5 changed files with 845 additions and 602 deletions

View file

@ -1,3 +1,5 @@
Mar 18, 2015 - Added support for JP2 bits per component box.
Jan 10, 2015 - v0.8.0 Reduced number of steps required for writing Jan 10, 2015 - v0.8.0 Reduced number of steps required for writing
images. Deprecated old read and write methods in favor of images. Deprecated old read and write methods in favor of
array-style slicing. Added ignore_pclr_cmap_cdef, verbose, array-style slicing. Added ignore_pclr_cmap_cdef, verbose,

View file

@ -356,35 +356,45 @@ class ColourSpecificationBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg += '\n Method: {0}'.format(_METHOD_DISPLAY[self.method]) lst = []
msg += '\n Precedence: {0}'.format(self.precedence) text = 'Method: {0}'.format(_METHOD_DISPLAY[self.method])
lst.append(text)
text = 'Precedence: {0}'.format(self.precedence)
lst.append(text)
if self.approximation is not 0: if self.approximation is not 0:
dispvalue = _APPROX_DISPLAY[self.approximation] dispvalue = _APPROX_DISPLAY[self.approximation]
msg += '\n Approximation: {0}'.format(dispvalue) text = 'Approximation: {0}'.format(dispvalue)
lst.append(text)
if self.colorspace is not None: if self.colorspace is not None:
dispvalue = _COLORSPACE_MAP_DISPLAY[self.colorspace] dispvalue = _COLORSPACE_MAP_DISPLAY[self.colorspace]
msg += '\n Colorspace: {0}'.format(dispvalue) text = 'Colorspace: {0}'.format(dispvalue)
else: else:
# 2.7 has trouble pretty-printing ordered dicts so we just have # 2.7 has trouble pretty-printing ordered dicts so we just have
# to print as a regular dict in this case. # to print as a regular dict in this case.
if self.icc_profile is None: if self.icc_profile is None:
msg += '\n ICC Profile: None' text = 'ICC Profile: None'
else: else:
if sys.hexversion < 0x03000000: if sys.hexversion < 0x03000000:
icc_profile = dict(self.icc_profile) icc_profile = dict(self.icc_profile)
else: else:
icc_profile = self.icc_profile icc_profile = self.icc_profile
dispvalue = pprint.pformat(icc_profile) text = pprint.pformat(icc_profile)
lines = [' ' * 8 + y for y in dispvalue.split('\n')] text = self._indent(text)
msg += '\n ICC Profile:\n{0}'.format('\n'.join(lines)) text = '\n'.join(['ICC Profile:', text])
return msg lst.append(text)
text = '\n'.join(lst)
text = '\n'.join([title, self._indent(text)])
return text
def write(self, fptr): def write(self, fptr):
"""Write an Colour Specification box to file. """Write an Colour Specification box to file.
@ -620,19 +630,26 @@ class ChannelDefinitionBox(Jp2kBox):
self._dispatch_validation_error(msg, writing=writing) self._dispatch_validation_error(msg, writing=writing)
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
lst = []
for j in range(len(self.association)): for j in range(len(self.association)):
color_type_string = _COLOR_TYPE_MAP_DISPLAY[self.channel_type[j]] color_type_string = _COLOR_TYPE_MAP_DISPLAY[self.channel_type[j]]
if self.association[j] == 0: if self.association[j] == 0:
assn = 'whole image' assn = 'whole image'
else: else:
assn = str(self.association[j]) assn = str(self.association[j])
msg += '\n Channel {0} ({1}) ==> ({2})' text = 'Channel {0} ({1}) ==> ({2})'.format(self.index[j],
msg = msg.format(self.index[j], color_type_string, assn) color_type_string,
return msg assn)
lst.append(text)
text = '\n'.join(lst)
text = self._indent(text)
text = '\n'.join([title, text])
return text
def __repr__(self): def __repr__(self):
msg = "glymur.jp2box.ChannelDefinitionBox(" msg = "glymur.jp2box.ChannelDefinitionBox("
@ -929,19 +946,26 @@ class ComponentMappingBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
lst = []
for k in range(len(self.component_index)): for k in range(len(self.component_index)):
if self.mapping_type[k] == 1: if self.mapping_type[k] == 1:
msg += '\n Component {0} ==> palette column {1}' text = 'Component {0} ==> palette column {1}'
msg = msg.format(self.component_index[k], text = text.format(self.component_index[k],
self.palette_index[k]) self.palette_index[k])
else: else:
msg += '\n Component {0} ==> {1}' text = 'Component {0} ==> {1}'
msg = msg.format(self.component_index[k], k) text = text.format(self.component_index[k], k)
return msg lst.append(text)
text = '\n'.join(lst)
text = self._indent(text)
text = '\n'.join([title, text])
return text
def write(self, fptr): def write(self, fptr):
"""Write a Component Mapping box to file. """Write a Component Mapping box to file.
@ -1041,16 +1065,20 @@ class ContiguousCodestreamBox(Jp2kBox):
return msg.format(repr(self.codestream)) return msg.format(repr(self.codestream))
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
if _printoptions['codestream'] is False: if _printoptions['codestream'] is False:
return msg return title
lst = []
for segment in self.codestream.segment: for segment in self.codestream.segment:
msg += '\n' + self._indent(str(segment), indent_level=4) lst.append(str(segment))
return msg text = '\n'.join(lst)
text = self._indent(text)
text = '\n'.join([title, text])
return text
@classmethod @classmethod
def parse(cls, fptr, offset=0, length=0): def parse(cls, fptr, offset=0, length=0):
@ -1149,13 +1177,18 @@ class DataReferenceBox(Jp2kBox):
fptr.seek(end_pos) fptr.seek(end_pos)
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
lst = []
for box in self.DR: for box in self.DR:
msg += '\n ' + str(box) lst.append(str(box))
return msg text = '\n'.join(lst)
text = self._indent(text)
text = '\n'.join([title, text])
return text
def __repr__(self): def __repr__(self):
msg = 'glymur.jp2box.DataReferenceBox()' msg = 'glymur.jp2box.DataReferenceBox()'
@ -1252,17 +1285,22 @@ class FileTypeBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
lst = [msg, lst = []
' Brand: {0}', text = 'Brand: {0}'.format(self.brand)
' Compatibility: {1}'] lst.append(text)
msg = '\n'.join(lst) text = 'Compatibility: {0}'.format(self.compatibility_list)
msg = msg.format(self.brand, self.compatibility_list) lst.append(text)
return msg text = '\n'.join(lst)
text = self._indent(text)
text = '\n'.join([title, text])
return text
def _validate(self, writing=False): def _validate(self, writing=False):
"""Validate the box before writing to file.""" """Validate the box before writing to file."""
@ -1384,19 +1422,24 @@ class FragmentListBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
lst = []
for j in range(len(self.fragment_offset)): for j in range(len(self.fragment_offset)):
msg += "\n Offset {0}: {1}" text = "Offset {0}: {1}".format(j, self.fragment_offset[j])
msg += "\n Fragment Length {2}: {3}" lst.append(text)
msg += "\n Data Reference {4}: {5}" text = "Fragment Length {0}: {1}".format(j,
msg = msg.format(j, self.fragment_offset[j], self.fragment_length[j])
j, self.fragment_length[j], lst.append(text)
j, self.data_reference[j]) text = "Data Reference {0}: {1}".format(j, self.data_reference[j])
lst.append(text)
return msg text = '\n'.join(lst)
text = self._indent(text)
text = '\n'.join([title, text])
return text
def write(self, fptr): def write(self, fptr):
"""Write a fragment list box to file. """Write a fragment list box to file.
@ -1546,11 +1589,7 @@ class FreeBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) return Jp2kBox.__str__(self)
if _printoptions['short'] is True:
return msg
return msg
@classmethod @classmethod
def parse(cls, fptr, offset, length): def parse(cls, fptr, offset, length):
@ -1642,23 +1681,34 @@ class ImageHeaderBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg = "{0}" lst = []
msg += '\n Size: [{1} {2} {3}]'
msg += '\n Bitdepth: {4}' text = 'Size: [{0} {1} {2}]'
msg += '\n Signed: {5}' text = text.format(self.height, self.width, self.num_components)
msg += '\n Compression: {6}' lst.append(text)
msg += '\n Colorspace Unknown: {7}'
msg = msg.format(Jp2kBox.__str__(self), text = 'Bitdepth: {0}'.format(self.bits_per_component)
self.height, self.width, self.num_components, lst.append(text)
self.bits_per_component,
self.signed, text = 'Signed: {0}'.format(self.signed)
'wavelet' if self.compression == 7 else 'unknown', lst.append(text)
self.colorspace_unknown)
return msg text = 'Compression: {0}'
text = text.format('wavelet' if self.compression == 7 else 'unknown')
lst.append(text)
text = 'Colorspace Unknown: {0}'.format(self.colorspace_unknown)
lst.append(text)
text = '\n'.join(lst)
text = self._indent(text)
text = '\n'.join([title, text])
return text
def write(self, fptr): def write(self, fptr):
"""Write an Image Header box to file. """Write an Image Header box to file.
@ -1780,6 +1830,79 @@ class AssociationBox(Jp2kBox):
self._write_superbox(fptr, b'asoc') self._write_superbox(fptr, b'asoc')
class BitsPerComponentBox(Jp2kBox):
"""Container for bits per component box information.
Attributes
----------
box_id : str
4-character identifier for the box.
length : int
length of the box in bytes.
offset : int
offset of the box from the start of the file.
longname : str
more verbose description of the box.
bpc : list
bits per component for each component
signed : list
True if signed, false if not, for each component
"""
box_id = 'bpcc'
longname = 'Bits Per Component'
def __init__(self, bpc, signed, length=0, offset=-1):
Jp2kBox.__init__(self)
self.length = length
self.offset = offset
self.bpc = bpc
self.signed = signed
def __repr__(self):
msg = "glymur.jp2box.BitsPerComponentBox(box={0})".format(self.box)
return msg
def __str__(self):
title = Jp2kBox.__str__(self)
if _printoptions['short'] is True:
return title
body = 'Bits per component: ['
body += ', '.join(str(x) for x in self.bpc)
body += ']'
body += '\n'
body += 'Signed: [' + ', '.join(str(x) for x in self.signed) + ']'
body = self._indent(body)
text = '\n'.join([title, body])
return text
@classmethod
def parse(cls, fptr, offset, length):
"""Parse bits per component box.
Parameters
----------
fptr : file
Open file object.
offset : int
Start position of box in bytes.
length : int
Length of the box in bytes.
Returns
-------
AssociationBox instance
"""
nbytes = length - 8
data = fptr.read(nbytes)
bpc = tuple(((x & 0x7f) + 1) for x in data)
signed = tuple(((x & 0x80) > 0) for x in data)
return cls(bpc, signed, length=length, offset=offset)
class JP2HeaderBox(Jp2kBox): class JP2HeaderBox(Jp2kBox):
"""Container for JP2 header box information. """Container for JP2 header box information.
@ -1873,14 +1996,16 @@ class JPEG2000SignatureBox(Jp2kBox):
return 'glymur.jp2box.JPEG2000SignatureBox()' return 'glymur.jp2box.JPEG2000SignatureBox()'
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg += '\n Signature: {0:02x}{1:02x}{2:02x}{3:02x}' body = 'Signature: {0:02x}{1:02x}{2:02x}{3:02x}'
msg = msg.format(self.signature[0], self.signature[1], body = body.format(self.signature[0], self.signature[1],
self.signature[2], self.signature[3]) self.signature[2], self.signature[3])
return msg body = self._indent(body)
text = '\n'.join([title, body])
return text
def write(self, fptr): def write(self, fptr):
"""Write a JPEG 2000 Signature box to file. """Write a JPEG 2000 Signature box to file.
@ -1962,12 +2087,15 @@ class PaletteBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg += '\n Size: ({0} x {1})'.format(*self.palette.shape) body = 'Size: ({0} x {1})'.format(*self.palette.shape)
return msg body = self._indent(body)
text = '\n'.join([title, body])
return text
def write(self, fptr): def write(self, fptr):
"""Write a Palette box to file. """Write a Palette box to file.
@ -2211,25 +2339,47 @@ class ReaderRequirementsBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg += '\n Fully Understands Aspect Mask: 0x{0:x}' lst = []
msg = msg.format(self.fuam)
msg += '\n Display Completely Mask: 0x{0:x}'.format(self.dcm)
msg += '\n Standard Features and Masks:' text = 'Fully Understands Aspect Mask: 0x{0:x}'.format(self.fuam)
lst.append(text)
text = 'Display Completely Mask: 0x{0:x}'.format(self.dcm)
lst.append(text)
text = 'Standard Features and Masks:'
lst.append(text)
lst2 = []
for j in range(len(self.standard_flag)): for j in range(len(self.standard_flag)):
args = (self.standard_flag[j], self.standard_mask[j], args = (self.standard_flag[j], self.standard_mask[j],
_READER_REQUIREMENTS_DISPLAY[self.standard_flag[j]]) _READER_REQUIREMENTS_DISPLAY[self.standard_flag[j]])
msg += '\n Feature {0:03d}: 0x{1:x} {2}'.format(*args) text = 'Feature {0:03d}: 0x{1:x} {2}'.format(*args)
lst2.append(text)
text = '\n'.join(lst2)
text = self._indent(text)
lst.append(text)
msg += '\n Vendor Features:' text = 'Vendor Features:'
lst.append(text)
lst2 = []
for j in range(len(self.vendor_feature)): for j in range(len(self.vendor_feature)):
msg += '\n UUID {0}'.format(self.vendor_feature[j]) text = 'UUID {0}'.format(self.vendor_feature[j])
lst2.append(text)
text = '\n'.join(lst2)
text = self._indent(text)
lst.append(text)
return msg text = '\n'.join(lst)
text = self._indent(text)
text = '\n'.join([title, text])
return text
@classmethod @classmethod
def parse(cls, fptr, offset, length): def parse(cls, fptr, offset, length):
@ -2500,13 +2650,21 @@ class CaptureResolutionBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg += '\n VCR: {0}'.format(self.vertical_resolution) lst = []
msg += '\n HCR: {0}'.format(self.horizontal_resolution) text = 'VCR: {0}'.format(self.vertical_resolution)
return msg lst.append(text)
text = 'HCR: {0}'.format(self.horizontal_resolution)
lst.append(text)
text = '\n'.join(lst)
body = self._indent(text)
text = '\n'.join([title, body])
return text
@classmethod @classmethod
def parse(cls, fptr, offset, length): def parse(cls, fptr, offset, length):
@ -2566,13 +2724,21 @@ class DisplayResolutionBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg += '\n VDR: {0}'.format(self.vertical_resolution) lst = []
msg += '\n HDR: {0}'.format(self.horizontal_resolution) text = 'VDR: {0}'.format(self.vertical_resolution)
return msg lst.append(text)
text = 'HDR: {0}'.format(self.horizontal_resolution)
lst.append(text)
text = '\n'.join(lst)
body = self._indent(text)
text = '\n'.join([title, body])
return text
@classmethod @classmethod
def parse(cls, fptr, offset, length): def parse(cls, fptr, offset, length):
@ -2626,12 +2792,15 @@ class LabelBox(Jp2kBox):
self.offset = offset self.offset = offset
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg += '\n Label: {0}'.format(self.label) text = 'Label: {0}'.format(self.label)
return msg body = self._indent(text)
text = '\n'.join([title, body])
return text
def __repr__(self): def __repr__(self):
msg = 'glymur.jp2box.LabelBox("{0}")'.format(self.label) msg = 'glymur.jp2box.LabelBox("{0}")'.format(self.label)
@ -2694,25 +2863,30 @@ class NumberListBox(Jp2kBox):
self.offset = offset self.offset = offset
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
lst = []
for j, association in enumerate(self.associations): for j, association in enumerate(self.associations):
msg += '\n Association[{0}]: '.format(j) text = 'Association[{0}]: '.format(j)
if association == 0: if association == 0:
msg += 'the rendered result' text += 'the rendered result'
elif (association >> 24) == 1: elif (association >> 24) == 1:
idx = association & 0x00FFFFFF idx = association & 0x00FFFFFF
msg += 'codestream {0}' text += 'codestream {0}'.format(idx)
msg = msg.format(idx)
elif (association >> 24) == 2: elif (association >> 24) == 2:
idx = association & 0x00FFFFFF idx = association & 0x00FFFFFF
msg += 'compositing layer {0}' text += 'compositing layer {0}'.format(idx)
msg = msg.format(idx)
else: else:
msg += 'unrecognized' text += 'unrecognized'
return msg lst.append(text)
body = '\n'.join(lst)
body = self._indent(body)
text = '\n'.join([title, body])
return text
def __repr__(self): def __repr__(self):
msg = 'glymur.jp2box.NumberListBox(associations={0})' msg = 'glymur.jp2box.NumberListBox(associations={0})'
@ -2797,21 +2971,22 @@ class XMLBox(Jp2kBox):
return "glymur.jp2box.XMLBox(xml={0})".format(self.xml) return "glymur.jp2box.XMLBox(xml={0})".format(self.xml)
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
if _printoptions['xml'] is False: if _printoptions['xml'] is False:
return msg return title
msg += '\n'
if self.xml is not None: if self.xml is not None:
xmlstring = ET.tostring(self.xml, body = ET.tostring(self.xml,
encoding='utf-8', encoding='utf-8',
pretty_print=True).decode('utf-8') pretty_print=True).decode('utf-8')
else: else:
xmlstring = 'None' body = 'None'
msg += self._indent(xmlstring) body = self._indent(body)
return msg
text = '\n'.join([title, body])
return text
def write(self, fptr): def write(self, fptr):
"""Write an XML box to file. """Write an XML box to file.
@ -2918,13 +3093,19 @@ class UUIDListBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
lst = []
for j, uuid_item in enumerate(self.ulst): for j, uuid_item in enumerate(self.ulst):
msg += '\n UUID[{0}]: {1}'.format(j, uuid_item) text = 'UUID[{0}]: {1}'.format(j, uuid_item)
return msg lst.append(text)
body = '\n'.join(lst)
body = self._indent(body)
text = '\n'.join([title, body])
return text
@classmethod @classmethod
def parse(cls, fptr, offset, length): def parse(cls, fptr, offset, length):
@ -3070,20 +3251,21 @@ class DataEntryURLBox(Jp2kBox):
return msg return msg
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg += '\n ' lst = ['Version: {0}',
lines = ['Version: {0}',
'Flag: {1} {2} {3}', 'Flag: {1} {2} {3}',
'URL: "{4}"'] 'URL: "{4}"']
msg += '\n '.join(lines) body = '\n'.join(lst)
msg = msg.format(self.version, body = body.format(self.version,
self.flag[0], self.flag[1], self.flag[2], self.flag[0], self.flag[1], self.flag[2],
self.url) self.url)
return msg body = self._indent(body)
text = '\n'.join([title, body])
return text
@classmethod @classmethod
def parse(cls, fptr, offset, length): def parse(cls, fptr, offset, length):
@ -3222,38 +3404,44 @@ class UUIDBox(Jp2kBox):
return msg.format(repr(self.uuid), len(self.raw_data)) return msg.format(repr(self.uuid), len(self.raw_data))
def __str__(self): def __str__(self):
msg = Jp2kBox.__str__(self) title = Jp2kBox.__str__(self)
if _printoptions['short'] is True: if _printoptions['short'] is True:
return msg return title
msg = '{0}\n UUID: {1}'.format(msg, self.uuid) text = 'UUID: {0}'.format(self.uuid)
if self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): if self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'):
msg += ' (XMP)' text += ' (XMP)'
elif self.uuid.bytes == b'JpgTiffExif->JP2': elif self.uuid.bytes == b'JpgTiffExif->JP2':
msg += ' (EXIF)' text += ' (EXIF)'
else: else:
msg += ' (unknown)' text += ' (unknown)'
lst = [text]
if (((_printoptions['xml'] is False) and if (((_printoptions['xml'] is False) and
(self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')))): (self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')))):
# If it's an XMP UUID, don't print the XML contents. # If it's an XMP UUID, don't print the XML contents.
return msg pass
if self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): elif self.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'):
line = '\n UUID Data:\n{0}' line = 'UUID Data:\n{0}'
xmlstring = ET.tostring(self.data, xmlstring = ET.tostring(self.data,
encoding='utf-8', encoding='utf-8',
pretty_print=True).decode('utf-8') pretty_print=True).decode('utf-8').rstrip()
# indent it a bit text = line.format(xmlstring)
xmlstring = self._indent(xmlstring.rstrip()) lst.append(text)
msg += line.format(xmlstring)
elif self.uuid.bytes == b'JpgTiffExif->JP2': elif self.uuid.bytes == b'JpgTiffExif->JP2':
msg += '\n UUID Data: {0}'.format(str(self.data)) text = 'UUID Data: {0}'.format(str(self.data))
lst.append(text)
else: else:
line = '\n UUID Data: {0} bytes' text = 'UUID Data: {0} bytes'.format(len(self.raw_data))
msg += line.format(len(self.raw_data)) lst.append(text)
return msg body = '\n'.join(lst)
body = self._indent(body)
text = '\n'.join([title, body])
return text
def write(self, fptr): def write(self, fptr):
"""Write a UUID box to file. """Write a UUID box to file.
@ -3289,6 +3477,7 @@ class UUIDBox(Jp2kBox):
# Map each box ID to the corresponding class. # Map each box ID to the corresponding class.
_BOX_WITH_ID = { _BOX_WITH_ID = {
b'asoc': AssociationBox, b'asoc': AssociationBox,
b'bpcc': BitsPerComponentBox,
b'cdef': ChannelDefinitionBox, b'cdef': ChannelDefinitionBox,
b'cgrp': ColourGroupBox, b'cgrp': ColourGroupBox,
b'cmap': ComponentMappingBox, b'cmap': ComponentMappingBox,

View file

@ -363,7 +363,11 @@ class Jp2k(Jp2kBox):
# 2.1 API # 2.1 API
self._cparams.rsiz = core.OPJ_PROFILE_CINEMA_4K self._cparams.rsiz = core.OPJ_PROFILE_CINEMA_4K
def _populate_cparams(self, img_array, **kwargs): def _populate_cparams(self, img_array, mct=None, cratios=None, psnr=None,
cinema2k=None, cinema4k=None, irreversible=None,
cbsize=None, eph=None, grid_offset=None, modesw=None,
numres=None, prog=None, psizes=None, sop=None,
subsam=None, tilesize=None, colorspace=None):
"""Directs processing of write method arguments. """Directs processing of write method arguments.
Parameters Parameters
@ -373,12 +377,14 @@ class Jp2k(Jp2kBox):
kwargs : dictionary kwargs : dictionary
non-image keyword inputs provided to write method non-image keyword inputs provided to write method
""" """
if ((('cinema2k' in kwargs or 'cinema4k' in kwargs) and other_args = (mct, cratios, psnr, irreversible, cbsize, eph,
(len(set(kwargs)) > 1))): grid_offset, modesw, numres, prog, psizes, sop, subsam)
if (((cinema2k is not None or cinema4k is not None) and
(not all([arg is None for arg in other_args])))):
msg = "Cannot specify cinema2k/cinema4k along with other options." msg = "Cannot specify cinema2k/cinema4k along with other options."
raise IOError(msg) raise IOError(msg)
if 'cratios' in kwargs and 'psnr' in kwargs: if cratios is not None and psnr is not None:
msg = "Cannot specify cratios and psnr together." msg = "Cannot specify cratios and psnr together."
raise IOError(msg) raise IOError(msg)
@ -402,90 +408,81 @@ class Jp2k(Jp2kBox):
cparams.tcp_numlayers = 1 cparams.tcp_numlayers = 1
cparams.cp_disto_alloc = 1 cparams.cp_disto_alloc = 1
if 'irreversible' in kwargs and kwargs['irreversible'] is True: cparams.irreversible = 1 if irreversible else 0
cparams.irreversible = 1
if 'cinema2k' in kwargs: if cinema2k is not None:
self._cparams = cparams self._cparams = cparams
self._set_cinema_params('cinema2k', kwargs['cinema2k']) self._set_cinema_params('cinema2k', cinema2k)
return return
if 'cinema4k' in kwargs: if cinema4k is not None:
self._cparams = cparams self._cparams = cparams
self._set_cinema_params('cinema4k', kwargs['cinema4k']) self._set_cinema_params('cinema4k', cinema4k)
return return
if 'cbsize' in kwargs: if cbsize is not None:
cparams.cblockw_init = kwargs['cbsize'][1] cparams.cblockw_init = cbsize[1]
cparams.cblockh_init = kwargs['cbsize'][0] cparams.cblockh_init = cbsize[0]
if 'cratios' in kwargs: if cratios is not None:
cparams.tcp_numlayers = len(kwargs['cratios']) cparams.tcp_numlayers = len(cratios)
for j, cratio in enumerate(kwargs['cratios']): for j, cratio in enumerate(cratios):
cparams.tcp_rates[j] = cratio cparams.tcp_rates[j] = cratio
cparams.cp_disto_alloc = 1 cparams.cp_disto_alloc = 1
if 'eph' in kwargs: cparams.csty |= 0x02 if sop else 0
cparams.csty |= 0x04 cparams.csty |= 0x04 if eph else 0
if 'grid_offset' in kwargs: if grid_offset is not None:
cparams.image_offset_x0 = kwargs['grid_offset'][1] cparams.image_offset_x0 = grid_offset[1]
cparams.image_offset_y0 = kwargs['grid_offset'][0] cparams.image_offset_y0 = grid_offset[0]
if 'modesw' in kwargs: if modesw is not None:
for shift in range(6): for shift in range(6):
power_of_two = 1 << shift power_of_two = 1 << shift
if kwargs['modesw'] & power_of_two: if modesw & power_of_two:
cparams.mode |= power_of_two cparams.mode |= power_of_two
if 'numres' in kwargs: if numres is not None:
cparams.numresolution = kwargs['numres'] cparams.numresolution = numres
if 'prog' in kwargs: if prog is not None:
prog = kwargs['prog'].upper() cparams.prog_order = core.PROGRESSION_ORDER[prog.upper()]
cparams.prog_order = core.PROGRESSION_ORDER[prog]
if 'psnr' in kwargs: if psnr is not None:
cparams.tcp_numlayers = len(kwargs['psnr']) cparams.tcp_numlayers = len(psnr)
for j, snr_layer in enumerate(kwargs['psnr']): for j, snr_layer in enumerate(psnr):
cparams.tcp_distoratio[j] = snr_layer cparams.tcp_distoratio[j] = snr_layer
cparams.cp_fixed_quality = 1 cparams.cp_fixed_quality = 1
if 'psizes' in kwargs: if psizes is not None:
for j, (prch, prcw) in enumerate(kwargs['psizes']): for j, (prch, prcw) in enumerate(psizes):
cparams.prcw_init[j] = prcw cparams.prcw_init[j] = prcw
cparams.prch_init[j] = prch cparams.prch_init[j] = prch
cparams.csty |= 0x01 cparams.csty |= 0x01
cparams.res_spec = len(kwargs['psizes']) cparams.res_spec = len(psizes)
if 'sop' in kwargs: if subsam is not None:
cparams.csty |= 0x02 cparams.subsampling_dy = subsam[0]
cparams.subsampling_dx = subsam[1]
if 'subsam' in kwargs: if tilesize is not None:
cparams.subsampling_dy = kwargs['subsam'][0] cparams.cp_tdx = tilesize[1]
cparams.subsampling_dx = kwargs['subsam'][1] cparams.cp_tdy = tilesize[0]
if 'tilesize' in kwargs:
cparams.cp_tdx = kwargs['tilesize'][1]
cparams.cp_tdy = kwargs['tilesize'][0]
cparams.tile_size_on = opj2.TRUE cparams.tile_size_on = opj2.TRUE
try: if mct is None:
mct = kwargs['mct'] # If the multi component transform was not specified, we infer
if mct and self._colorspace == opj2.CLRSPC_GRAY: # that it should be used if the color space is RGB.
cparams.tcp_mct = 1 if self._colorspace == opj2.CLRSPC_SRGB else 0
else:
if self._colorspace == opj2.CLRSPC_GRAY:
msg = "Cannot specify usage of the multi component transform " msg = "Cannot specify usage of the multi component transform "
msg += "if the colorspace is gray." msg += "if the colorspace is gray."
raise IOError(msg) raise IOError(msg)
cparams.tcp_mct = 1 if mct else 0 cparams.tcp_mct = 1 if mct else 0
except KeyError:
# If the multi component transform was not specified, we infer
# that it should be used if the color space is RGB.
if self._colorspace == opj2.CLRSPC_SRGB:
cparams.tcp_mct = 1
else:
cparams.tcp_mct = 0
self._validate_compression_params(img_array, cparams, **kwargs) self._validate_compression_params(img_array, cparams, colorspace)
self._cparams = cparams self._cparams = cparams
@ -576,29 +573,26 @@ class Jp2k(Jp2kBox):
self.parse() self.parse()
def _validate_compression_params(self, img_array, cparams, **kwargs): def _validate_j2k_colorspace(self, cparams, colorspace):
"""Check that the compression parameters are valid.
Parameters
----------
img_array : ndarray
Image data to be written to file.
cparams : CompressionParametersType(ctypes.Structure)
Corresponds to cparameters_t type in openjp2 headers.
""" """
# Cannot specify a colorspace with J2K. Cannot specify a colorspace with J2K.
if cparams.codec_fmt == opj2.CODEC_J2K and 'colorspace' in kwargs: """
if cparams.codec_fmt == opj2.CODEC_J2K and colorspace is not None:
msg = 'Do not specify a colorspace when writing a raw ' msg = 'Do not specify a colorspace when writing a raw '
msg += 'codestream.' msg += 'codestream.'
raise IOError(msg) raise IOError(msg)
# Code block size def _validate_codeblock_size(self, cparams):
code_block_specified = False """
Code block dimensions must satisfy certain restrictions.
They must both be a power of 2 and the total area defined by the width
and height cannot be either too great or too small for the codec.
"""
if cparams.cblockw_init != 0 and cparams.cblockh_init != 0: if cparams.cblockw_init != 0 and cparams.cblockh_init != 0:
# These fields ARE zero if uninitialized. # These fields ARE zero if uninitialized.
width = cparams.cblockw_init width = cparams.cblockw_init
height = cparams.cblockh_init height = cparams.cblockh_init
code_block_specified = True
if height * width > 4096 or height < 4 or width < 4: if height * width > 4096 or height < 4 or width < 4:
msg = "Code block area cannot exceed 4096. " msg = "Code block area cannot exceed 4096. "
msg += "Code block height and width must be larger than 4." msg += "Code block height and width must be larger than 4."
@ -609,7 +603,17 @@ class Jp2k(Jp2kBox):
msg += "must be powers of 2." msg += "must be powers of 2."
raise IOError(msg.format(height, width)) raise IOError(msg.format(height, width))
# Precinct size def _validate_precinct_size(self, cparams):
"""
Precinct dimensions must satisfy certain restrictions if specified.
They must both be a power of 2 and must both be at least twice the
size of their codeblock size counterparts.
"""
code_block_specified = False
if cparams.cblockw_init != 0 and cparams.cblockh_init != 0:
code_block_specified = True
if cparams.res_spec != 0: if cparams.res_spec != 0:
# precinct size was not specified if this field is zero. # precinct size was not specified if this field is zero.
for j in range(cparams.res_spec): for j in range(cparams.res_spec):
@ -627,11 +631,18 @@ class Jp2k(Jp2kBox):
msg += "must be powers of 2." msg += "must be powers of 2."
raise IOError(msg.format(prch, prcw)) raise IOError(msg.format(prch, prcw))
# What would the point of 1D images be? def _validate_image_rank(self, img_array):
"""
Images must be either 2D or 3D.
"""
if img_array.ndim == 1 or img_array.ndim > 3: if img_array.ndim == 1 or img_array.ndim > 3:
msg = "{0}D imagery is not allowed.".format(img_array.ndim) msg = "{0}D imagery is not allowed.".format(img_array.ndim)
raise IOError(msg) raise IOError(msg)
def _validate_v2_0_0_images(self, img_array):
"""
Version 2.0.0 is restricted to only the most common images.
"""
if re.match("2.0.0", version.openjpeg_version) is not None: if re.match("2.0.0", version.openjpeg_version) is not None:
if (((img_array.ndim != 2) and if (((img_array.ndim != 2) and
(img_array.shape[2] != 1 and img_array.shape[2] != 3))): (img_array.shape[2] != 1 and img_array.shape[2] != 3))):
@ -641,11 +652,32 @@ class Jp2k(Jp2kBox):
msg += "release." msg += "release."
raise IOError(msg) raise IOError(msg)
def _validate_image_datatype(self, img_array):
"""
Only uint8 and uint16 images are currently supported.
"""
if img_array.dtype != np.uint8 and img_array.dtype != np.uint16: if img_array.dtype != np.uint8 and img_array.dtype != np.uint16:
msg = "Only uint8 and uint16 datatypes are currently supported " msg = "Only uint8 and uint16 datatypes are currently supported "
msg += "when writing." msg += "when writing."
raise RuntimeError(msg) raise RuntimeError(msg)
def _validate_compression_params(self, img_array, cparams, colorspace):
"""Check that the compression parameters are valid.
Parameters
----------
img_array : ndarray
Image data to be written to file.
cparams : CompressionParametersType(ctypes.Structure)
Corresponds to cparameters_t type in openjp2 headers.
"""
self._validate_j2k_colorspace(cparams, colorspace)
self._validate_codeblock_size(cparams)
self._validate_precinct_size(cparams)
self._validate_image_rank(img_array)
self._validate_v2_0_0_images(img_array)
self._validate_image_datatype(img_array)
def _determine_colorspace(self, colorspace=None, **kwargs): def _determine_colorspace(self, colorspace=None, **kwargs):
"""Determine the colorspace from the supplied inputs. """Determine the colorspace from the supplied inputs.
@ -808,7 +840,7 @@ class Jp2k(Jp2kBox):
if boxes is None: if boxes is None:
boxes = self._get_default_jp2_boxes() boxes = self._get_default_jp2_boxes()
_validate_jp2_box_sequence(boxes) self._validate_jp2_box_sequence(boxes)
with open(filename, 'wb') as ofile: with open(filename, 'wb') as ofile:
for box in boxes: for box in boxes:
@ -916,6 +948,47 @@ class Jp2k(Jp2kBox):
msg = "Partial write operations are currently not allowed." msg = "Partial write operations are currently not allowed."
raise TypeError(msg) raise TypeError(msg)
def _remove_ellipsis(self, index, numrows, numcols, numbands):
"""
resolve the first ellipsis in the index so that it references the image
Parameters
----------
index : tuple
tuple of index arguments, presumably one of them is the Ellipsis
numrows, numcols, numbands : int
image dimensions
Returns
-------
newindex : tuple
Same as index, except that the first Ellipsis is replaced with
a proper slice whose start and stop members are not None
"""
# Remove the first ellipsis we find.
rows = slice(0, numrows)
cols = slice(0, numcols)
bands = slice(0, numbands)
if index[0] is Ellipsis:
if len(index) == 2:
# jp2k[..., other_slice]
newindex = (rows, cols, index[1])
else:
# jp2k[..., cols, bands]
newindex = (rows, index[1], index[2])
elif index[1] is Ellipsis:
if len(index) == 2:
# jp2k[rows, ...]
newindex = (index[0], cols, bands)
else:
# jp2k[rows, ..., bands]
newindex = (index[0], cols, index[2])
else:
# Assume that we don't have 4D imagery, of course.
newindex = (index[0], index[1], bands)
return newindex
def __getitem__(self, pargs): def __getitem__(self, pargs):
""" """
Slicing protocol. Slicing protocol.
@ -950,23 +1023,7 @@ class Jp2k(Jp2kBox):
pargs = (pargs, Ellipsis) pargs = (pargs, Ellipsis)
if isinstance(pargs, tuple) and any(x is Ellipsis for x in pargs): if isinstance(pargs, tuple) and any(x is Ellipsis for x in pargs):
# Remove the first ellipsis we find. newindex = self._remove_ellipsis(pargs, numrows, numcols, numbands)
rows = slice(0, numrows)
cols = slice(0, numcols)
bands = slice(0, numbands)
if pargs[0] is Ellipsis:
if len(pargs) == 2:
newindex = (rows, cols, pargs[1])
else:
newindex = (rows, pargs[1], pargs[2])
elif pargs[1] is Ellipsis:
if len(pargs) == 2:
newindex = (pargs[0], cols, bands)
else:
newindex = (pargs[0], cols, pargs[2])
else:
# Assume that we don't have 4D imagery, of course.
newindex = (pargs[0], pargs[1], bands)
# Run once again because it is possible that there's another # Run once again because it is possible that there's another
# Ellipsis object in the 2nd or 3rd position. # Ellipsis object in the 2nd or 3rd position.
@ -1110,7 +1167,7 @@ class Jp2k(Jp2kBox):
Returns Returns
------- -------
img_array : ndarray image : ndarray
The image data. The image data.
Raises Raises
@ -1146,21 +1203,21 @@ class Jp2k(Jp2kBox):
src = fptr.read() src = fptr.read()
cio = opj.cio_open(dinfo, src) cio = opj.cio_open(dinfo, src)
image = opj.decode(dinfo, cio) raw_image = opj.decode(dinfo, cio)
stack.callback(opj.image_destroy, image) stack.callback(opj.image_destroy, raw_image)
stack.callback(opj.destroy_decompress, dinfo) stack.callback(opj.destroy_decompress, dinfo)
stack.callback(opj.cio_close, cio) stack.callback(opj.cio_close, cio)
data = extract_image_cube(image) image = self._extract_image(raw_image)
except ValueError: except ValueError:
opj2.check_error(0) opj2.check_error(0)
if data.shape[2] == 1: if image.shape[2] == 1:
# The third dimension has just a single layer. Make the image # The third dimension has just a single layer. Make the image
# data 2D instead of 3D. # data 2D instead of 3D.
data.shape = data.shape[0:2] image.shape = image.shape[0:2]
if area is not None: if area is not None:
x0, y0, x1, y1 = area x0, y0, x1, y1 = area
@ -1172,9 +1229,9 @@ class Jp2k(Jp2kBox):
area = [int(round(float(x)/extent + 2 ** -20)) for x in area] area = [int(round(float(x)/extent + 2 ** -20)) for x in area]
rows = slice(area[0], area[2], None) rows = slice(area[0], area[2], None)
cols = slice(area[1], area[3], None) cols = slice(area[1], area[3], None)
data = data[rows, cols] image = image[rows, cols]
return data return image
def _read_openjp2(self, rlevel=0, layer=None, area=None, tile=None, def _read_openjp2(self, rlevel=0, layer=None, area=None, tile=None,
verbose=False): verbose=False):
@ -1197,7 +1254,7 @@ class Jp2k(Jp2kBox):
Returns Returns
------- -------
img_array : ndarray image : ndarray
The image data. The image data.
Raises Raises
@ -1234,26 +1291,26 @@ class Jp2k(Jp2kBox):
opj2.set_info_handler(codec, None) opj2.set_info_handler(codec, None)
opj2.setup_decoder(codec, self._dparams) opj2.setup_decoder(codec, self._dparams)
image = opj2.read_header(stream, codec) raw_image = opj2.read_header(stream, codec)
stack.callback(opj2.image_destroy, image) stack.callback(opj2.image_destroy, raw_image)
if self._dparams.nb_tile_to_decode: if self._dparams.nb_tile_to_decode:
opj2.get_decoded_tile(codec, stream, image, opj2.get_decoded_tile(codec, stream, raw_image,
self._dparams.tile_index) self._dparams.tile_index)
else: else:
opj2.set_decode_area(codec, image, opj2.set_decode_area(codec, raw_image,
self._dparams.DA_x0, self._dparams.DA_y0, self._dparams.DA_x0, self._dparams.DA_y0,
self._dparams.DA_x1, self._dparams.DA_y1) self._dparams.DA_x1, self._dparams.DA_y1)
opj2.decode(codec, stream, image) opj2.decode(codec, stream, raw_image)
opj2.end_decompress(codec, stream) opj2.end_decompress(codec, stream)
img_array = extract_image_cube(image) image = self._extract_image(raw_image)
if img_array.shape[2] == 1: if image.shape[2] == 1:
img_array.shape = img_array.shape[0:2] image.shape = image.shape[0:2]
return img_array return image
def _populate_dparams(self, rlevel, tile=None, area=None): def _populate_dparams(self, rlevel, tile=None, area=None):
"""Populate decompression structure with appropriate input parameters. """Populate decompression structure with appropriate input parameters.
@ -1279,12 +1336,11 @@ class Jp2k(Jp2kBox):
infile += b'0' * nelts infile += b'0' * nelts
dparam.infile = infile dparam.infile = infile
if self.ignore_pclr_cmap_cdef: # Return raw codestream components instead of "interpolating" the
# Return raw codestream components. # colormap?
dparam.flags |= 1 dparam.flags |= 1 if self.ignore_pclr_cmap_cdef else 0
dparam.decod_format = self._codec_format dparam.decod_format = self._codec_format
dparam.cp_layer = self._layer dparam.cp_layer = self._layer
# Must check the specified rlevel against the maximum. # Must check the specified rlevel against the maximum.
@ -1315,10 +1371,6 @@ class Jp2k(Jp2kBox):
dparam.tile_index = tile dparam.tile_index = tile
dparam.nb_tile_to_decode = 1 dparam.nb_tile_to_decode = 1
if self.ignore_pclr_cmap_cdef:
# Return raw codestream components.
dparam.flags |= 1
self._dparams = dparam self._dparams = dparam
def read_bands(self, rlevel=0, layer=None, area=None, tile=None, def read_bands(self, rlevel=0, layer=None, area=None, tile=None,
@ -1407,10 +1459,96 @@ class Jp2k(Jp2kBox):
opj2.decode(codec, stream, image) opj2.decode(codec, stream, image)
opj2.end_decompress(codec, stream) opj2.end_decompress(codec, stream)
lst = extract_image_bands(image) lst = self._extract_image(image)
return lst return lst
def _extract_image(self, raw_image):
"""
Extract unequally-sized image bands.
Parameters
----------
raw_image : reference to openjpeg ImageType instance
The image structure initialized with image characteristics.
Returns
-------
image : list or numpy array
If the JPEG 2000 image has unequally-sized images, they are
extracted into a list, otherwise a numpy array.
"""
ncomps = raw_image.contents.numcomps
# Make a pass thru the image, see if any of the band datatypes or
# dimensions differ.
dtypes, nrows, ncols = [], [], []
for k in range(raw_image.contents.numcomps):
component = raw_image.contents.comps[k]
dtypes.append(self._component2dtype(component))
nrows.append(component.h)
ncols.append(component.w)
is_cube = all(r == nrows[0] and c == ncols[0] and d == dtypes[0]
for r, c, d in zip(nrows, ncols, dtypes))
if is_cube:
image = np.zeros((nrows[0], ncols[0], ncomps), dtypes[0])
else:
image = []
for k in range(raw_image.contents.numcomps):
component = raw_image.contents.comps[k]
self._validate_nonzero_image_size(nrows[k], ncols[k], k)
addr = ctypes.addressof(component.data.contents)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
nelts = nrows[k] * ncols[k]
band = np.ctypeslib.as_array(
(ctypes.c_int32 * nelts).from_address(addr))
if is_cube:
image[:, :, k] = np.reshape(band.astype(dtypes[k]),
(nrows[k], ncols[k]))
else:
image.append(np.reshape(band.astype(dtypes[k]),
(nrows[k], ncols[k])))
return image
def _component2dtype(self, component):
"""Determin the appropriate numpy datatype for an OpenJPEG component.
Parameters
----------
component : ctypes pointer to ImageCompType (image_comp_t)
single image component structure.
Returns
-------
dtype : builtins.type
numpy datatype to be used to construct an image array.
"""
if component.sgnd:
if component.prec <= 8:
dtype = np.int8
elif component.prec <= 16:
dtype = np.int16
else:
msg = "Unhandled precision: {0} bits.".format(component.prec)
raise RuntimeError(msg)
else:
if component.prec <= 8:
dtype = np.uint8
elif component.prec <= 16:
dtype = np.uint16
else:
msg = "Unhandled precision: {0} bits.".format(component.prec)
raise RuntimeError(msg)
return dtype
def get_codestream(self, header_only=True): def get_codestream(self, header_only=True):
"""Returns a codestream object. """Returns a codestream object.
@ -1539,76 +1677,41 @@ class Jp2k(Jp2kBox):
self._comptparms = comptparms self._comptparms = comptparms
def _validate_nonzero_image_size(self, nrows, ncols, component_index):
def _component2dtype(component):
"""Take an OpenJPEG component structure and determine the numpy datatype.
Parameters
----------
component : ctypes pointer to ImageCompType (image_comp_t)
single image component structure.
Returns
-------
dtype : builtins.type
numpy datatype to be used to construct an image array.
"""
if component.sgnd:
if component.prec <= 8:
dtype = np.int8
elif component.prec <= 16:
dtype = np.int16
else:
raise RuntimeError("Unhandled precision, datatype")
else:
if component.prec <= 8:
dtype = np.uint8
elif component.prec <= 16:
dtype = np.uint16
else:
raise RuntimeError("Unhandled precision, datatype")
return dtype
def _validate_nonzero_image_size(nrows, ncols, component_index):
"""The image cannot have area of zero. """The image cannot have area of zero.
""" """
if nrows == 0 or ncols == 0: if nrows == 0 or ncols == 0:
# Letting this situation continue would segfault Python. # Letting this situation continue would segfault openjpeg.
msg = "Component {0} has dimensions {1} x {2}" msg = "Component {0} has dimensions {1} x {2}"
msg = msg.format(component_index, nrows, ncols) msg = msg.format(component_index, nrows, ncols)
raise IOError(msg) raise IOError(msg)
def _validate_jp2_box_sequence(self, boxes):
JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ',
'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
'uuid']
def _validate_jp2_box_sequence(boxes):
"""Run through series of tests for JP2 box legality. """Run through series of tests for JP2 box legality.
This is non-exhaustive. This is non-exhaustive.
""" """
_validate_signature_compatibility(boxes) JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h',
_validate_jp2h(boxes) 'jP ', 'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst',
_validate_jp2c(boxes) 'uinf', 'url ', 'uuid']
self._validate_signature_compatibility(boxes)
self._validate_jp2h(boxes)
self._validate_jp2c(boxes)
if boxes[1].brand == 'jpx ': if boxes[1].brand == 'jpx ':
_validate_jpx_box_sequence(boxes) self._validate_jpx_box_sequence(boxes)
else: else:
# Validate the JP2 box IDs. # Validate the JP2 box IDs.
count = _collect_box_count(boxes) count = self._collect_box_count(boxes)
for box_id in count.keys(): for box_id in count.keys():
if box_id not in JP2_IDS: if box_id not in JP2_IDS:
msg = "The presence of a '{0}' box requires that the file " msg = "The presence of a '{0}' box requires that the file "
msg += "type brand be set to 'jpx '." msg += "type brand be set to 'jpx '."
raise IOError(msg.format(box_id)) raise IOError(msg.format(box_id))
_validate_jp2_colr(boxes) self._validate_jp2_colr(boxes)
def _validate_jp2_colr(self, boxes):
def _validate_jp2_colr(boxes):
""" """
Validate JP2 requirements on colour specification boxes. Validate JP2 requirements on colour specification boxes.
""" """
@ -1616,20 +1719,19 @@ def _validate_jp2_colr(boxes):
jp2h = lst[0] jp2h = lst[0]
for colr in [box for box in jp2h.box if box.box_id == 'colr']: for colr in [box for box in jp2h.box if box.box_id == 'colr']:
if colr.approximation != 0: if colr.approximation != 0:
msg = "A JP2 colr box cannot have a non-zero approximation field." msg = "A JP2 colr box cannot have a non-zero approximation "
msg += "field."
raise IOError(msg) raise IOError(msg)
def _validate_jpx_box_sequence(self, 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."""
_validate_label(boxes) self._validate_label(boxes)
_validate_jpx_brand(boxes, boxes[1].brand) self._validate_jpx_brand(boxes, boxes[1].brand)
_validate_jpx_compatibility(boxes, boxes[1].compatibility_list) self._validate_jpx_compatibility(boxes, boxes[1].compatibility_list)
_validate_singletons(boxes) self._validate_singletons(boxes)
_validate_top_level(boxes) self._validate_top_level(boxes)
def _validate_signature_compatibility(self, boxes):
def _validate_signature_compatibility(boxes):
"""Validate the file signature and compatibility status.""" """Validate the file signature and compatibility status."""
# Check for a bad sequence of boxes. # Check for a bad sequence of boxes.
# 1st two boxes must be 'jP ' and 'ftyp' # 1st two boxes must be 'jP ' and 'ftyp'
@ -1643,8 +1745,7 @@ def _validate_signature_compatibility(boxes):
msg = "The ftyp box must contain 'jp2 ' in the compatibility list." msg = "The ftyp box must contain 'jp2 ' in the compatibility list."
raise IOError(msg) raise IOError(msg)
def _validate_jp2c(self, boxes):
def _validate_jp2c(boxes):
"""Validate the codestream box in relation to other boxes.""" """Validate the codestream box in relation to other boxes."""
# jp2c must be preceeded by jp2h # jp2c must be preceeded by jp2h
jp2h_lst = [idx for (idx, box) in enumerate(boxes) jp2h_lst = [idx for (idx, box) in enumerate(boxes)
@ -1662,10 +1763,9 @@ def _validate_jp2c(boxes):
msg = "The codestream box must be preceeded by a jp2 header box." msg = "The codestream box must be preceeded by a jp2 header box."
raise IOError(msg) raise IOError(msg)
def _validate_jp2h(self, boxes):
def _validate_jp2h(boxes):
"""Validate the JP2 Header box.""" """Validate the JP2 Header box."""
_check_jp2h_child_boxes(boxes, 'top-level') self._check_jp2h_child_boxes(boxes, 'top-level')
jp2h_lst = [box for box in boxes if box.box_id == 'jp2h'] jp2h_lst = [box for box in boxes if box.box_id == 'jp2h']
jp2h = jp2h_lst[0] jp2h = jp2h_lst[0]
@ -1689,12 +1789,12 @@ def _validate_jp2h(boxes):
raise IOError(msg) raise IOError(msg)
colr = jp2h.box[colr_lst[0]] colr = jp2h.box[colr_lst[0]]
_validate_channel_definition(jp2h, colr) self._validate_channel_definition(jp2h, colr)
def _validate_channel_definition(self, jp2h, colr):
def _validate_channel_definition(jp2h, colr):
"""Validate the channel definition box.""" """Validate the channel definition box."""
cdef_lst = [j for (j, box) in enumerate(jp2h.box) if box.box_id == 'cdef'] cdef_lst = [j for (j, box) in enumerate(jp2h.box)
if box.box_id == 'cdef']
if len(cdef_lst) > 1: if len(cdef_lst) > 1:
msg = "Only one channel definition box is allowed in the " msg = "Only one channel definition box is allowed in the "
msg += "JP2 header." msg += "JP2 header."
@ -1714,12 +1814,10 @@ def _validate_channel_definition(jp2h, colr):
msg += "channel definition box." msg += "channel definition box."
raise IOError(msg) raise IOError(msg)
def _check_jp2h_child_boxes(self, boxes, parent_box_name):
JP2H_CHILDREN = set(['bpcc', 'cdef', 'cmap', 'ihdr', 'pclr'])
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."""
JP2H_CHILDREN = set(['bpcc', 'cdef', 'cmap', 'ihdr', 'pclr'])
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 not in ['jp2h', 'jpch']: if len(intersection) > 0 and parent_box_name not in ['jp2h', 'jpch']:
@ -1729,27 +1827,24 @@ def _check_jp2h_child_boxes(boxes, parent_box_name):
# Recursively check any contained superboxes. # Recursively check any contained superboxes.
for box in boxes: for box in boxes:
if hasattr(box, 'box'): if hasattr(box, 'box'):
_check_jp2h_child_boxes(box.box, box.box_id) self._check_jp2h_child_boxes(box.box, box.box_id)
def _collect_box_count(self, boxes):
def _collect_box_count(boxes):
"""Count the occurences of each box type.""" """Count the occurences of each box type."""
count = Counter([box.box_id for box in boxes]) count = Counter([box.box_id for box in boxes])
# Add the counts in the superboxes. # Add the counts in the superboxes.
for box in boxes: for box in boxes:
if hasattr(box, 'box'): if hasattr(box, 'box'):
count.update(_collect_box_count(box.box)) count.update(self._collect_box_count(box.box))
return count return count
TOP_LEVEL_ONLY_BOXES = set(['dtbl']) def _check_superbox_for_top_levels(self, boxes):
def _check_superbox_for_top_levels(boxes):
"""Several boxes can only occur at the top level.""" """Several boxes can only occur at the top level."""
# We are only looking at the boxes contained in a superbox, so if any of # We are only looking at the boxes contained in a superbox, so if any
# the blacklisted boxes show up here, it's an error. # of the blacklisted boxes show up here, it's an error.
TOP_LEVEL_ONLY_BOXES = set(['dtbl'])
box_ids = set([box.box_id for box in boxes]) box_ids = set([box.box_id for box in boxes])
intersection = box_ids.intersection(TOP_LEVEL_ONLY_BOXES) intersection = box_ids.intersection(TOP_LEVEL_ONLY_BOXES)
if len(intersection) > 0: if len(intersection) > 0:
@ -1759,17 +1854,16 @@ def _check_superbox_for_top_levels(boxes):
# Recursively check any contained superboxes. # Recursively check any contained superboxes.
for box in boxes: for box in boxes:
if hasattr(box, 'box'): if hasattr(box, 'box'):
_check_superbox_for_top_levels(box.box) self._check_superbox_for_top_levels(box.box)
def _validate_top_level(self, boxes):
def _validate_top_level(boxes):
"""Several boxes can only occur at the top level.""" """Several boxes can only occur at the top level."""
# Add the counts in the superboxes. # Add the counts in the superboxes.
for box in boxes: for box in boxes:
if hasattr(box, 'box'): if hasattr(box, 'box'):
_check_superbox_for_top_levels(box.box) self._check_superbox_for_top_levels(box.box)
count = _collect_box_count(boxes) count = self._collect_box_count(boxes)
# Which boxes occur more than once? # Which boxes occur more than once?
multiples = [box_id for box_id, bcount in count.items() if bcount > 1] multiples = [box_id for box_id, bcount in count.items() if bcount > 1]
if 'dtbl' in multiples: if 'dtbl' in multiples:
@ -1777,26 +1871,23 @@ def _validate_top_level(boxes):
# If there is one data reference box, then there must also be one ftbl. # If there is one data reference box, then there must also be one ftbl.
if 'dtbl' in count and 'ftbl' not in count: if 'dtbl' in count and 'ftbl' not in count:
msg = 'The presence of a data reference box requires the presence of ' msg = 'The presence of a data reference box requires the presence '
msg += 'a fragment table box as well.' msg += 'of a fragment table box as well.'
raise IOError(msg) raise IOError(msg)
def _validate_singletons(self, boxes):
def _validate_singletons(boxes):
"""Several boxes can only occur once.""" """Several boxes can only occur once."""
count = _collect_box_count(boxes) count = self._collect_box_count(boxes)
# Which boxes occur more than once? # Which boxes occur more than once?
multiples = [box_id for box_id, bcount in count.items() if bcount > 1] multiples = [box_id for box_id, bcount in count.items() if bcount > 1]
if 'dtbl' in multiples: if 'dtbl' in multiples:
raise IOError('There can only be one dtbl box in a file.') raise IOError('There can only be one dtbl box in a file.')
JPX_IDS = ['asoc', 'nlst'] def _validate_jpx_brand(self, boxes, brand):
def _validate_jpx_brand(boxes, brand):
""" """
If there is a JPX box then the brand must be 'jpx '. If there is a JPX box then the brand must be 'jpx '.
""" """
JPX_IDS = ['asoc', 'nlst']
for box in boxes: for box in boxes:
if box.box_id in JPX_IDS: if box.box_id in JPX_IDS:
if brand != 'jpx ': if brand != 'jpx ':
@ -1805,13 +1896,14 @@ def _validate_jpx_brand(boxes, brand):
raise RuntimeError(msg) raise RuntimeError(msg)
if hasattr(box, 'box') != 0: if hasattr(box, 'box') != 0:
# Same set of checks on any child boxes. # Same set of checks on any child boxes.
_validate_jpx_brand(box.box, brand) self._validate_jpx_brand(box.box, brand)
def _validate_jpx_compatibility(self, boxes, compatibility_list):
def _validate_jpx_compatibility(boxes, compatibility_list):
""" """
If there is a JPX box then the compatibility list must also contain 'jpx '. If there is a JPX box then the compatibility list must also contain
'jpx '.
""" """
JPX_IDS = ['asoc', 'nlst']
jpx_cl = set(compatibility_list) jpx_cl = set(compatibility_list)
for box in boxes: for box in boxes:
if box.box_id in JPX_IDS: if box.box_id in JPX_IDS:
@ -1821,10 +1913,9 @@ def _validate_jpx_compatibility(boxes, compatibility_list):
raise RuntimeError(msg) raise RuntimeError(msg)
if hasattr(box, 'box') != 0: if hasattr(box, 'box') != 0:
# Same set of checks on any child boxes. # Same set of checks on any child boxes.
_validate_jpx_compatibility(box.box, compatibility_list) self._validate_jpx_compatibility(box.box, compatibility_list)
def _validate_label(self, boxes):
def _validate_label(boxes):
""" """
Label boxes can only be inside association, codestream headers, or Label boxes can only be inside association, codestream headers, or
compositing layer header boxes. compositing layer header boxes.
@ -1834,66 +1925,12 @@ def _validate_label(boxes):
if hasattr(box, 'box'): if hasattr(box, 'box'):
for boxi in box.box: for boxi in box.box:
if boxi.box_id == 'lbl ': if boxi.box_id == 'lbl ':
msg = "A label box cannot be nested inside a {0} box." msg = "A label box cannot be nested inside a "
msg += "{0} box."
msg = msg.format(box.box_id) msg = msg.format(box.box_id)
raise IOError(msg) raise IOError(msg)
# Same set of checks on any child boxes. # Same set of checks on any child boxes.
_validate_label(box.box) self._validate_label(box.box)
def extract_image_cube(image):
"""Extract 3D image from openjpeg data structure.
"""
ncomps = image.contents.numcomps
component = image.contents.comps[0]
dtype = _component2dtype(component)
nrows = component.h
ncols = component.w
data = np.zeros((nrows, ncols, ncomps), dtype)
for k in range(image.contents.numcomps):
component = image.contents.comps[k]
nrows = component.h
ncols = component.w
_validate_nonzero_image_size(nrows, ncols, k)
addr = ctypes.addressof(component.data.contents)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
nelts = nrows * ncols
band = np.ctypeslib.as_array(
(ctypes.c_int32 * nelts).from_address(addr))
data[:, :, k] = np.reshape(band.astype(dtype), (nrows, ncols))
return data
def extract_image_bands(image):
"""Extract unequally-sized image bands.
This routine need only be called when subsampling differs across image
components, such as is often the case with YCbCr imagery.
"""
data = []
for k in range(image.contents.numcomps):
component = image.contents.comps[k]
dtype = _component2dtype(component)
nrows = component.h
ncols = component.w
_validate_nonzero_image_size(nrows, ncols, k)
addr = ctypes.addressof(component.data.contents)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
band = np.ctypeslib.as_array(
(ctypes.c_int32 * nrows * ncols).from_address(addr))
data.append(np.reshape(band.astype(dtype), (nrows, ncols)))
return data
# Setup the default callback handlers. See the callback functions subsection # Setup the default callback handlers. See the callback functions subsection

View file

@ -1092,3 +1092,8 @@ goodstuff_with_full_header = r"""Codestream:
Step size: [(0, 8), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10)] Step size: [(0, 8), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10), (0, 9), (0, 9), (0, 10)]
SOD marker segment @ (164, 0) SOD marker segment @ (164, 0)
EOC marker segment @ (115218, 0)""" EOC marker segment @ (115218, 0)"""
bpcc = """Bits Per Component Box (bpcc) @ (62, 12)
Bits per component: [5, 5, 5, 1]
Signed: [False, False, False, False]"""

View file

@ -618,6 +618,17 @@ class TestPrintingOpjDataRoot(unittest.TestCase):
def tearDown(self): def tearDown(self):
pass pass
def test_bpcc(self):
"""BPCC boxes are rare :-)"""
self.maxDiff = None
filename = opj_data_file('input/nonregression/issue458.jp2')
jp2 = Jp2k(filename)
with patch('sys.stdout', new=StringIO()) as fake_out:
box = jp2.box[2].box[1]
print(box)
actual = fake_out.getvalue().strip()
self.assertEqual(actual, fixtures.bpcc)
def test_cinema_profile(self): def test_cinema_profile(self):
"""Should print Cinema 2K when the profile is 3.""" """Should print Cinema 2K when the profile is 3."""
filename = opj_data_file('input/nonregression/_00042.j2k') filename = opj_data_file('input/nonregression/_00042.j2k')
@ -1088,8 +1099,6 @@ class TestJp2dump(unittest.TestCase):
def test_jp2_codestream_2(self): def test_jp2_codestream_2(self):
"""Verify dumping with -c 2, print entire jp2 jacket, codestream.""" """Verify dumping with -c 2, print entire jp2 jacket, codestream."""
actual = self.run_jp2dump(['', '-c', '2', self.jp2file]) actual = self.run_jp2dump(['', '-c', '2', self.jp2file])
# shave off the non-main-header segments
expected = fixtures.nemo expected = fixtures.nemo
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
@ -1125,6 +1134,7 @@ class TestJp2dump(unittest.TestCase):
def test_suppress_xml(self): def test_suppress_xml(self):
"""Verify dumping with -x, suppress XML.""" """Verify dumping with -x, suppress XML."""
self.maxDiff = None
actual = self.run_jp2dump(['', '-x', self.jp2file]) actual = self.run_jp2dump(['', '-x', self.jp2file])
# shave off the XML and non-main-header segments # shave off the XML and non-main-header segments