"""Classes for individual JPEG 2000 boxes. References ---------- .. [JP2K15444-1i] International Organization for Standardication. ISO/IEC 15444-1:2004 - Information technology -- JPEG 2000 image coding system: Core coding system .. [JP2K15444-2m] International Organization for Standardication. ISO/IEC 15444-2:2004 - Information technology -- JPEG 2000 image coding system: Extensions """ # pylint: disable=C0302,R0903,R0913 import copy import datetime import math import os import pprint import struct import sys import uuid import warnings import xml.etree.cElementTree as ET if sys.hexversion < 0x02070000: # pylint: disable=F0401,E0611 from ordereddict import OrderedDict from xml.etree.cElementTree import XMLParserError as ParseError else: from xml.etree.cElementTree import ParseError from collections import OrderedDict import numpy as np from .codestream import Codestream from .core import _COLORSPACE_MAP_DISPLAY from .core import _COLOR_TYPE_MAP_DISPLAY from .core import ENUMERATED_COLORSPACE, RESTRICTED_ICC_PROFILE from .core import ANY_ICC_PROFILE, VENDOR_COLOR_METHOD _METHOD_DISPLAY = { ENUMERATED_COLORSPACE: 'enumerated colorspace', RESTRICTED_ICC_PROFILE: 'restricted ICC profile', ANY_ICC_PROFILE: 'any ICC profile', VENDOR_COLOR_METHOD: 'vendor color method'} _APPROX_DISPLAY = {1: 'accurately represents correct colorspace definition', 2: 'approximates correct colorspace definition, ' + 'exceptional quality', 3: 'approximates correct colorspace definition, ' + 'reasonable quality', 4: 'approximates correct colorspace definition, ' + 'poor quality'} class Jp2kBox(object): """Superclass for JPEG 2000 boxes. Attributes ---------- box_id : str 4-character identifier for the box. length : int length of the box in bytes. offset : int offset of the box from the start of the file. longname : str more verbose description of the box. """ def __init__(self, box_id='', offset=0, length=0, longname=''): self.box_id = box_id self.length = length self.offset = offset self.longname = longname def __str__(self): msg = "{0} Box ({1})".format(self.longname, self.box_id) msg += " @ ({0}, {1})".format(self.offset, self.length) return msg def write(self, _): """Must be implemented in a subclass. """ msg = "Not supported for {0} box.".format(self.longname) raise NotImplementedError(msg) def parse_superbox(self, fptr): """Parse a superbox (box consisting of nothing but other boxes. Parameters ---------- fptr : file Open file object. Returns ------- List of top-level boxes in the JPEG 2000 file. """ superbox = [] start = fptr.tell() while True: # Are we at the end of the superbox? if start >= self.offset + self.length: break read_buffer = fptr.read(8) (box_length, box_id) = struct.unpack('>I4s', read_buffer) if sys.hexversion >= 0x03000000: box_id = box_id.decode('utf-8') if box_length == 0: # The length of the box is presumed to last until the end of # the file. Compute the effective length of the box. num_bytes = os.path.getsize(fptr.name) - fptr.tell() + 8 elif box_length == 1: # The length of the box is in the XL field, a 64-bit value. read_buffer = fptr.read(8) num_bytes, = struct.unpack('>Q', read_buffer) else: num_bytes = box_length # Call the proper parser for the given box with ID "T". try: box = _BOX_WITH_ID[box_id].parse(fptr, start, num_bytes) except KeyError: msg = 'Unrecognized box ({0}) encountered.'.format(box_id) warnings.warn(msg) box = Jp2kBox(box_id, offset=start, length=num_bytes, longname='Unknown box') superbox.append(box) # Position to the start of the next box. if num_bytes > self.length: # Length of the current box goes past the end of the # enclosing superbox. msg = '{0} box has incorrect box length ({1})' msg = msg.format(box_id, num_bytes) warnings.warn(msg) elif fptr.tell() > start + num_bytes: # The box must be invalid somehow, as the file pointer is # positioned past the end of the box. msg = '{0} box may be invalid, the file pointer is positioned ' msg += '{1} bytes past the end of the box.' msg = msg.format(box_id, fptr.tell() - (start + num_bytes)) warnings.warn(msg) fptr.seek(start + num_bytes) start += num_bytes return superbox class ColourSpecificationBox(Jp2kBox): """Container for JPEG 2000 color specification 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. method : int Method for defining the colorspace. precedence : int How this box ranks in priority compared to other color specification boxes. approximation : int Measure of colorspace accuracy. colorspace : int or None Enumerated colorspace, corresponds to one of 'sRGB', 'greyscale', or 'YCC'. If not None, then icc_profile must be None. icc_profile : dict ICC profile header according to ICC profile specification. If colorspace is not None, then icc_profile must be empty. """ def __init__(self, method=ENUMERATED_COLORSPACE, precedence=0, approximation=0, colorspace=None, icc_profile=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='colr', longname='Colour Specification') if colorspace is not None and icc_profile is not None: raise IOError("colorspace and icc_profile cannot both be set.") if method not in (1, 2, 3, 4): raise IOError("Invalid method.") if approximation not in (0, 1, 2, 3, 4): raise IOError("Invalid approximation.") self.method = method self.precedence = precedence self.approximation = approximation self.colorspace = colorspace self.icc_profile = icc_profile self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n Method: {0}'.format(_METHOD_DISPLAY[self.method]) msg += '\n Precedence: {0}'.format(self.precedence) if self.approximation is not 0: dispvalue = _APPROX_DISPLAY[self.approximation] msg += '\n Approximation: {0}'.format(dispvalue) if self.colorspace is not None: dispvalue = _COLORSPACE_MAP_DISPLAY[self.colorspace] msg += '\n Colorspace: {0}'.format(dispvalue) else: # 2.7 has trouble pretty-printing ordered dicts so we just have # to print as a regular dict in this case. if sys.hexversion < 0x03000000: icc_profile = dict(self.icc_profile) else: icc_profile = self.icc_profile dispvalue = pprint.pformat(icc_profile) lines = [' ' * 8 + y for y in dispvalue.split('\n')] msg += '\n ICC Profile:\n{0}'.format('\n'.join(lines)) return msg def write(self, fptr): """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) fptr.write(struct.pack('>I', length)) fptr.write('colr'.encode()) read_buffer = struct.pack('>BBBI', self.method, self.precedence, self.approximation, self.colorspace) fptr.write(read_buffer) @staticmethod def parse(fptr, offset, length): """Parse JPEG 2000 color specification box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- ColourSpecificationBox instance """ # Read the brand, minor version. read_buffer = fptr.read(3) (method, precedence, approximation) = struct.unpack('>BBB', read_buffer) if method == 1: # enumerated colour space read_buffer = fptr.read(4) colorspace, = struct.unpack('>I', read_buffer) icc_profile = None else: # ICC profile colorspace = None numbytes = offset + length - fptr.tell() if numbytes < 128: msg = "ICC profile header is corrupt, length is " msg += "only {0} instead of 128." warnings.warn(msg.format(numbytes), UserWarning) icc_profile = None else: profile = _ICCProfile(fptr.read(numbytes)) icc_profile = profile.header box = ColourSpecificationBox(method=method, precedence=precedence, approximation=approximation, colorspace=colorspace, icc_profile=icc_profile, length=length, offset=offset) return box class _ICCProfile(object): """ Container for ICC profile information. """ profile_class = {b'scnr': 'input device profile', b'mntr': 'display device profile', b'prtr': 'output device profile', b'link': 'devicelink profile', b'spac': 'colorspace conversion profile', b'abst': 'abstract profile', b'nmcl': 'name colour profile'} colour_space_dict = {b'XYZ ': 'XYZ', b'Lab ': 'Lab', b'Luv ': 'Luv', b'YCbr': 'YCbCr', b'Yxy ': 'Yxy', b'RGB ': 'RGB', b'GRAY': 'gray', b'HSV ': 'hsv', b'HLS ': 'hls', b'CMYK': 'CMYK', b'CMY ': 'cmy', b'2CLR': '2colour', b'3CLR': '3colour', b'4CLR': '4colour', b'5CLR': '5colour', b'6CLR': '6colour', b'7CLR': '7colour', b'8CLR': '8colour', b'9CLR': '9colour', b'ACLR': '10colour', b'BCLR': '11colour', b'CCLR': '12colour', b'DCLR': '13colour', b'ECLR': '14colour', b'FCLR': '15colour'} rendering_intent_dict = {0: 'perceptual', 1: 'media-relative colorimetric', 2: 'saturation', 3: 'ICC-absolute colorimetric'} def __init__(self, read_buffer): self._raw_buffer = read_buffer header = OrderedDict() data = struct.unpack('>IIBB', self._raw_buffer[0:10]) header['Size'] = data[0] header['Preferred CMM Type'] = data[1] major = data[2] minor = (data[3] & 0xf0) >> 4 bugfix = (data[3] & 0x0f) header['Version'] = '{0}.{1}.{2}'.format(major, minor, bugfix) header['Device Class'] = self.profile_class[self._raw_buffer[12:16]] header['Color Space'] = self.colour_space_dict[self._raw_buffer[16:20]] data = self.colour_space_dict[self._raw_buffer[20:24]] header['Connection Space'] = data data = struct.unpack('>HHHHHH', self._raw_buffer[24:36]) header['Datetime'] = datetime.datetime(data[0], data[1], data[2], data[3], data[4], data[5]) header['File Signature'] = read_buffer[36:40].decode('utf-8') if read_buffer[40:44] == b'\x00\x00\x00\x00': header['Platform'] = 'unrecognized' else: header['Platform'] = read_buffer[40:44].decode('utf-8') fval, = struct.unpack('>I', read_buffer[44:48]) flags = "{0}embedded, {1} be used independently" header['Flags'] = flags.format('' if fval & 0x01 else 'not ', 'cannot' if fval & 0x02 else 'can') header['Device Manufacturer'] = read_buffer[48:52].decode('utf-8') if read_buffer[52:56] == b'\x00\x00\x00\x00': device_model = '' else: device_model = read_buffer[52:56].decode('utf-8') header['Device Model'] = device_model val, = struct.unpack('>Q', read_buffer[56:64]) attr = "{0}, {1}, {2} media polarity, {3} media" attr = attr.format('transparency' if val & 0x01 else 'reflective', 'matte' if val & 0x02 else 'glossy', 'negative' if val & 0x04 else 'positive', 'black and white' if val & 0x08 else 'color') header['Device Attributes'] = attr rval, = struct.unpack('>I', read_buffer[64:68]) try: header['Rendering Intent'] = self.rendering_intent_dict[rval] except KeyError: header['Rendering Intent'] = 'unknown' data = struct.unpack('>iii', read_buffer[68:80]) header['Illuminant'] = np.array(data, dtype=np.float64) / 65536 if read_buffer[80:84] == b'\x00\x00\x00\x00': creator = 'unrecognized' else: creator = read_buffer[80:84].decode('utf-8') header['Creator'] = creator if header['Version'][0] == '4': header['Profile Id'] = read_buffer[84:100] # Final 27 bytes are reserved. self.header = header class ChannelDefinitionBox(Jp2kBox): """Container for component definition box information. Attributes ---------- box_id : str 4-character identifier for the box. length : numeric scalar 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. index : int number of the channel. Defaults to monotonically increasing sequence, i.e. [0, 1, 2, ...] channel_type : int type of the channel association : int index of the associated color """ def __init__(self, index=None, channel_type=None, association=None, **kwargs): Jp2kBox.__init__(self, box_id='cdef', longname='Channel Definition') # channel type and association must be specified. if channel_type is None or association is None: raise IOError("channel_type and association must be specified.") if index is None: index = list(range(len(channel_type))) 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.channel_type[j]] if self.association[j] == 0: assn = 'whole image' else: assn = str(self.association[j]) msg += '\n Channel {0} ({1}) ==> ({2})' msg = msg.format(self.index[j], color_type_string, assn) return msg def write(self, fptr): """Write a channel definition box to file. """ num_components = len(self.association) fptr.write(struct.pack('>I', 8 + 2 + num_components * 6)) fptr.write('cdef'.encode('utf-8')) fptr.write(struct.pack('>H', num_components)) for j in range(num_components): fptr.write(struct.pack('>' + 'H' * 3, self.index[j], self.channel_type[j], self.association[j])) @staticmethod def parse(fptr, offset, length): """Parse component definition box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- ComponentDefinitionBox instance """ # Read the number of components. read_buffer = fptr.read(2) num_components, = struct.unpack('>H', read_buffer) read_buffer = fptr.read(num_components * 6) data = struct.unpack('>' + 'HHH' * num_components, read_buffer) index = data[0:num_components * 6:3] channel_type = data[1:num_components * 6:3] association = data[2:num_components * 6:3] box = ChannelDefinitionBox(index=index, channel_type=channel_type, association=association, length=length, offset=offset) return box class CodestreamHeaderBox(Jp2kBox): """Container for codestream header 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. box : list List of boxes contained in this superbox. """ def __init__(self, length=0, offset=-1): Jp2kBox.__init__(self, box_id='jpch', longname='Codestream Header') self.length = length self.offset = offset self.box = [] def __str__(self): msg = Jp2kBox.__str__(self) for box in self.box: boxstr = str(box) # Add indentation. strs = [('\n ' + x) for x in boxstr.split('\n')] msg += ''.join(strs) return msg @staticmethod def parse(fptr, offset, length): """Parse codestream header box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- CodestreamHeaderBox instance """ box = CodestreamHeaderBox(length=length, offset=offset) # The codestream header box is a superbox, so go ahead and parse its # child boxes. box.box = box.parse_superbox(fptr) return box class CompositingLayerHeaderBox(Jp2kBox): """Container for compositing layer header 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. box : list List of boxes contained in this superbox. """ def __init__(self, length=0, offset=-1): Jp2kBox.__init__(self, box_id='jplh', longname='Compositing Layer Header') self.length = length self.offset = offset self.box = [] def __str__(self): msg = Jp2kBox.__str__(self) for box in self.box: boxstr = str(box) # Add indentation. strs = [('\n ' + x) for x in boxstr.split('\n')] msg += ''.join(strs) return msg @staticmethod def parse(fptr, offset, length): """Parse compositing layer header box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- CompositingLayerHeaderBox instance """ box = CompositingLayerHeaderBox(length=length, offset=offset) # This box is a superbox, so go ahead and parse its # child boxes. box.box = box.parse_superbox(fptr) return box class ComponentMappingBox(Jp2kBox): """Container for component mapping information. Attributes ---------- box_id : str 4-character identifier for the box. length : numeric scalar Length of the box in bytes. offset : int Offset of the box from the start of the file. longname : str Verbose description of the box. component_index : int Index of component in codestream that is mapped to this channel. mapping_type : int mapping type, either direct use (0) or palette (1) palette_index : int Index component from palette """ def __init__(self, component_index, mapping_type, palette_index, length=0, offset=-1): Jp2kBox.__init__(self, box_id='cmap', longname='Component Mapping') self.component_index = component_index self.mapping_type = mapping_type self.palette_index = palette_index self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) for k in range(len(self.component_index)): if self.mapping_type[k] == 1: msg += '\n Component {0} ==> palette column {1}' msg = msg.format(self.component_index[k], self.palette_index[k]) else: msg += '\n Component %d ==> %d' msg = msg.format(self.component_index[k], k) return msg @staticmethod def parse(fptr, offset, length): """Parse component mapping box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- ComponentMappingBox instance """ num_bytes = offset + length - fptr.tell() num_components = int(num_bytes/4) read_buffer = fptr.read(num_bytes) data = struct.unpack('>' + 'HBB' * num_components, read_buffer) component_index = data[0:num_bytes:num_components] mapping_type = data[1:num_bytes:num_components] palette_index = data[2:num_bytes:num_components] box = ComponentMappingBox(component_index, mapping_type, palette_index, length=length, offset=offset) return box class ContiguousCodestreamBox(Jp2kBox): """Container for JPEG2000 codestream 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. main_header : list List of segments in the codestream header. """ def __init__(self, main_header=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='jp2c', longname='Contiguous Codestream') self.main_header = main_header self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n Main header:' for segment in self.main_header.segment: segstr = str(segment) # Add indentation. strs = [('\n ' + x) for x in segstr.split('\n')] msg += ''.join(strs) return msg @staticmethod def parse(fptr, offset=0, length=0): """Parse a codestream box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- ContiguousCodestreamBox instance """ main_header = Codestream(fptr, length, header_only=True) box = ContiguousCodestreamBox(main_header, length=length, offset=offset) return box class FileTypeBox(Jp2kBox): """Container for JPEG 2000 file type 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. brand: str Specifies the governing recommendation or standard upon which this file is based. minor_version: int Minor version number identifying the JP2 specification used. compatibility_list: list List of file conformance profiles. """ def __init__(self, brand='jp2 ', minor_version=0, compatibility_list=None, length=0, offset=-1): Jp2kBox.__init__(self, box_id='ftyp', longname='File Type') self.brand = brand self.minor_version = minor_version if compatibility_list is None: # see W0102, pylint self.compatibility_list = ['jp2 '] else: self.compatibility_list = compatibility_list self.length = length self.offset = offset def __str__(self): lst = [Jp2kBox.__str__(self), ' Brand: {0}', ' Compatibility: {1}'] msg = '\n'.join(lst) msg = msg.format(self.brand, self.compatibility_list) return msg def write(self, fptr): """Write a File Type box to file. """ length = 16 + 4*len(self.compatibility_list) fptr.write(struct.pack('>I', length)) fptr.write('ftyp'.encode()) fptr.write(self.brand.encode()) fptr.write(struct.pack('>I', self.minor_version)) for item in self.compatibility_list: fptr.write(item.encode()) @staticmethod def parse(fptr, offset, length): """Parse JPEG 2000 file type box. Parameters ---------- f : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- FileTypeBox instance """ # Read the brand, minor version. read_buffer = fptr.read(8) (brand, minor_version) = struct.unpack('>4sI', read_buffer) if sys.hexversion >= 0x030000: brand = brand.decode('utf-8') # Read the compatibility list. Each entry has 4 bytes. current_pos = fptr.tell() num_bytes = (offset + length - current_pos) / 4 read_buffer = fptr.read(int(num_bytes) * 4) compatibility_list = [] for j in range(int(num_bytes)): entry, = struct.unpack('>4s', read_buffer[4*j:4*(j+1)]) if sys.hexversion >= 0x03000000: entry = entry.decode('utf-8') compatibility_list.append(entry) compatibility_list = compatibility_list box = FileTypeBox(brand=brand, minor_version=minor_version, compatibility_list=compatibility_list, length=length, offset=offset) return box class ImageHeaderBox(Jp2kBox): """Container for JPEG 2000 image header 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. height, width : int Height and width of image. num_components : int Number of image channels. bits_per_component : int Bits per component. signed : bool False if the image components are unsigned. compression : int The compression type, should be 7 if JP2. 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, height, width, num_components=1, signed=False, bits_per_component=8, compression=7, colorspace_unknown=False, ip_provided=False, length=0, offset=-1): """ Examples -------- >>> import glymur >>> box = glymur.jp2box.ImageHeaderBox(height=512, width=256) """ Jp2kBox.__init__(self, box_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 = ip_provided self.length = length self.offset = offset def __str__(self): 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, fptr): """Write an Image Header box to file. """ fptr.write(struct.pack('>I', 22)) fptr.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 read_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) fptr.write(read_buffer) @staticmethod def parse(fptr, offset, length): """Parse JPEG 2000 image header box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- ImageHeaderBox instance """ # Read the box information read_buffer = fptr.read(14) params = struct.unpack('>IIHBBBB', read_buffer) height = params[0] width = params[1] num_components = params[2] bits_per_component = (params[3] & 0x7f) + 1 signed = (params[3] & 0x80) > 1 compression = params[4] colorspace_unknown = True if params[5] else False ip_provided = True if params[6] else False box = ImageHeaderBox(height, width, num_components=num_components, bits_per_component=bits_per_component, signed=signed, compression=compression, colorspace_unknown=colorspace_unknown, ip_provided=ip_provided, length=length, offset=offset) return box class AssociationBox(Jp2kBox): """Container for Association 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. box : list List of boxes contained in this superbox. """ def __init__(self, length=0, offset=-1): Jp2kBox.__init__(self, box_id='asoc', longname='Association') self.length = length self.offset = offset self.box = [] def __str__(self): msg = Jp2kBox.__str__(self) for box in self.box: boxstr = str(box) # Add indentation. strs = [('\n ' + x) for x in boxstr.split('\n')] msg += ''.join(strs) return msg @staticmethod def parse(fptr, offset, length): """Parse association 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 """ box = AssociationBox(length=length, offset=offset) # The Association box is a superbox, so go ahead and parse its child # boxes. box.box = box.parse_superbox(fptr) return box class JP2HeaderBox(Jp2kBox): """Container for JP2 header 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. box : list List of boxes contained in this superbox. """ def __init__(self, length=0, offset=-1): Jp2kBox.__init__(self, box_id='jp2h', longname='JP2 Header') self.length = length self.offset = offset self.box = [] def __str__(self): msg = Jp2kBox.__str__(self) for box in self.box: boxstr = str(box) # Add indentation. strs = [('\n ' + x) for x in boxstr.split('\n')] msg += ''.join(strs) return msg def write(self, fptr): """Write a JP2 Header box to file. """ # Write the contained boxes, then come back and write the length. orig_pos = fptr.tell() fptr.write(struct.pack('>I', 0)) fptr.write('jp2h'.encode()) for box in self.box: box.write(fptr) end_pos = fptr.tell() fptr.seek(orig_pos) fptr.write(struct.pack('>I', end_pos - orig_pos)) fptr.seek(end_pos) @staticmethod def parse(fptr, offset, length): """Parse JPEG 2000 header box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- JP2HeaderBox instance """ box = JP2HeaderBox(length=length, offset=offset) # The JP2 header box is a superbox, so go ahead and parse its child # boxes. box.box = box.parse_superbox(fptr) return box class JPEG2000SignatureBox(Jp2kBox): """Container for JPEG 2000 signature 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. signature : byte Four-byte tuple identifying the file as JPEG 2000. """ def __init__(self, signature=(13, 10, 135, 10), length=0, offset=-1): Jp2kBox.__init__(self, box_id='jP ', longname='JPEG 2000 Signature') self.signature = signature self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n Signature: {0:02x}{1:02x}{2:02x}{3:02x}' msg = msg.format(self.signature[0], self.signature[1], self.signature[2], self.signature[3]) return msg def write(self, fptr): """Write a JPEG 2000 Signature box to file. """ fptr.write(struct.pack('>I', 12)) fptr.write(self.box_id.encode()) fptr.write(struct.pack('>BBBB', *self.signature)) @staticmethod def parse(fptr, offset, length): """Parse JPEG 2000 signature box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- JPEG2000SignatureBox instance """ read_buffer = fptr.read(4) signature = struct.unpack('>BBBB', read_buffer) box = JPEG2000SignatureBox(signature=signature, length=length, offset=offset) return box class PaletteBox(Jp2kBox): """Container for palette 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. palette : list Colormap represented as list of 1D arrays, one per color component. """ def __init__(self, palette, bits_per_component, signed, length=0, offset=-1): Jp2kBox.__init__(self, box_id='pclr', longname='Palette') self.palette = palette self.bits_per_component = bits_per_component self.signed = signed self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n Size: ({0} x {1})'.format(len(self.palette[0]), len(self.palette)) return msg @staticmethod def parse(fptr, offset, length): """Parse palette box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- PaletteBox instance """ # Get the size of the palette. read_buffer = fptr.read(3) (num_entries, num_columns) = struct.unpack('>HB', read_buffer) # Need to determine bps and signed or not read_buffer = fptr.read(num_columns) data = struct.unpack('>' + 'B' * num_columns, read_buffer) bps = [((x & 0x07f) + 1) for x in data] signed = [((x & 0x80) > 1) for x in data] # Each palette component is padded out to the next largest byte. # That means a list comprehension does this in one shot. row_nbytes = sum([int(math.ceil(x/8.0)) for x in bps]) # Form the format string so that we can intelligently unpack the # colormap. We have to do this because it is possible that the # colormap columns could have different datatypes. # # This means that we store the palette as a list of 1D arrays, # which reverses the usual indexing scheme. read_buffer = fptr.read(num_entries * row_nbytes) palette = _buffer2palette(read_buffer, num_entries, num_columns, bps) box = PaletteBox(palette, bps, signed, length=length, offset=offset) return box def _buffer2palette(read_buffer, num_rows, num_cols, bps): """Construct the palette from the buffer read from file. Parameters ---------- read_buffer : iterable Byte array of palette information read from file. num_rows, num_cols : int Size of palette. bps : iterable Bits per sample for each channel. Returns ------- palette : list of 1D arrays Each 1D array corresponds to a channel. """ row_nbytes = 0 palette = [] fmt = '>' for j in range(num_cols): if bps[j] <= 8: row_nbytes += 1 fmt += 'B' palette.append(np.zeros(num_rows, dtype=np.uint8)) elif bps[j] <= 16: row_nbytes += 2 fmt += 'H' palette.append(np.zeros(num_rows, dtype=np.uint16)) elif bps[j] <= 32: row_nbytes += 4 fmt += 'I' palette.append(np.zeros(num_rows, dtype=np.uint32)) else: msg = 'Unsupported palette bitdepth (%d).'.format(bps[j]) raise IOError(msg) for j in range(num_rows): row_buffer = read_buffer[(row_nbytes * j):(row_nbytes * (j + 1))] row = struct.unpack(fmt, row_buffer) for k in range(num_cols): palette[k][j] = row[k] return palette # Map rreq codes to display text. _READER_REQUIREMENTS_DISPLAY = { 0: 'File not completely understood', 1: 'Deprecated', 2: 'Contains multiple composition layers', 3: 'Deprecated', 4: 'JPEG 2000 Part 1 Profile 1 codestream', 5: 'Unrestricted JPEG 2000 Part 1 codestream, ITU-T Rec. T.800 ' + '| ISO/IEC 15444-1', 6: 'Unrestricted JPEG 2000 Part 2 codestream', 7: 'JPEG codestream as defined in ISO/IEC 10918-1', 8: 'Deprecated', 9: 'Non-premultiplied opacity channel', 10: 'Premultiplied opacity channel', 11: 'Chroma-key based opacity', 12: 'Deprecated', 13: 'Fragmented codestream where all fragments are in file and in order', 14: 'Fragmented codestream where all fragments are in file ' + 'but are out of order', 15: 'Fragmented codestream where not all fragments are within the file ' + 'but are all in locally accessible files', 16: 'Fragmented codestream where some fragments may be accessible ' + 'only through a URL specified network connection', 17: 'Compositing required to produce rendered result from multiple ' + 'compositing layers', 18: 'Deprecated', 19: 'Deprecated', 20: 'Deprecated', 21: 'At least one compositing layer consists of multiple codestreams', 22: 'Deprecated', 23: 'Colourspace transformations are required to combine compositing ' + 'layers; not all compositing layers are in the same colourspace', 24: 'Deprecated', 25: 'Deprecated', 26: 'First animation layer does not cover entire rendered result', 27: 'Deprecated', 28: 'Reuse of animation layers', 29: 'Deprecated', 30: 'Some animated frames are non-persistent', 31: 'Deprecated', 32: 'Rendered result involves scaling within a layer', 33: 'Rendered result involves scaling between layers', 34: 'ROI metadata', 35: 'IPR metadata', 36: 'Content metadata', 37: 'History metadata', 38: 'Creation metadata', 39: 'JPX digital signatures', 40: 'JPX checksums', 41: 'Desires Graphics Arts Reproduction specified', 42: 'Deprecated', 43: '(Deprecated) compositing layer uses restricted ICC profile', 44: 'Compositing layer uses Any ICC profile', 45: 'Deprecated', 46: 'Deprecated', 47: 'BiLevel 1 enumerated colourspace', 48: 'BiLevel 2 enumerated colourspace', 49: 'YCbCr 1 enumerated colourspace', 50: 'YCbCr 2 enumerated colourspace', 51: 'YCbCr 3 enumerated colourspace', 52: 'PhotoYCC enumerated colourspace', 53: 'YCCK enumerated colourspace', 54: 'CMY enumerated colourspace', 55: 'CMYK enumerated colorspace', 56: 'CIELab enumerated colourspace with default parameters', 57: 'CIELab enumerated colourspace with non-default parameters', 58: 'CIEJab enumerated colourspace with default parameters', 59: 'CIEJab enumerated colourspace with non-default parameters', 60: 'e-sRGB enumerated colorspace', 61: 'ROMM_RGB enumerated colorspace', 62: 'Non-square samples', 63: 'Deprecated', 64: 'Deprecated', 65: 'Deprecated', 66: 'Deprecated', 67: 'GIS metadata XML box', 68: 'JPSEC extensions in codestream as specified by ISO/IEC 15444-8', 69: 'JP3D extensions in codestream as specified by ISO/IEC 15444-10', 70: 'Deprecated', 71: 'e-sYCC enumerated colourspace', 72: 'JPEG 2000 Part 2 codestream as restricted by baseline conformance ' + 'requirements in M.9.2.3', 73: 'YPbPr(1125/60) enumerated colourspace', 74: 'YPbPr(1250/50) enumerated colourspace'} class ReaderRequirementsBox(Jp2kBox): """Container for reader requirements 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. fuam : int Fully Understand Aspects mask. dcm : int Decode completely mask. standard_flag : list Integers specifying standard features. standard_mask : list Specifies the compatibility mask for each corresponding standard flag. vendor_feature : list Each item is a UUID corresponding to a vendor defined feature. vendor_mask : list Specifies the compatibility mask for each corresponding vendor feature. """ def __init__(self, fuam, dcm, standard_flag, standard_mask, vendor_feature, vendor_mask, length=0, offset=-1): Jp2kBox.__init__(self, box_id='rreq', longname='Reader Requirements') self.fuam = fuam self.dcm = dcm self.standard_flag = standard_flag self.standard_mask = standard_mask self.vendor_feature = vendor_feature self.vendor_mask = vendor_mask self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n Standard Features:' for j in range(len(self.standard_flag)): sfl = self.standard_flag[j] rrdisp = _READER_REQUIREMENTS_DISPLAY[self.standard_flag[j]] msg += '\n Feature {0:03d}: {1}'.format(sfl, rrdisp) msg += '\n Vendor Features:' for j in range(len(self.vendor_feature)): msg += '\n UUID {0}'.format(self.vendor_feature[j]) return msg @staticmethod def parse(fptr, offset, length): """Parse reader requirements box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- ReaderRequirementsBox instance """ read_buffer = fptr.read(1) mask_length, = struct.unpack('>B', read_buffer) # Fully Understands Aspect Mask # Decodes Completely Mask read_buffer = fptr.read(2 * mask_length) fuam = dcm = standard_flag = standard_mask = [] vendor_feature = vendor_mask = [] # The mask length tells us the format string to use when unpacking # from the buffer read from file. try: mask_format = {1: 'B', 2: 'H', 4: 'I', 8: 'Q'}[mask_length] fuam, dcm = struct.unpack('>' + mask_format * 2, read_buffer) standard_flag, standard_mask = _parse_standard_flag(fptr, mask_length) vendor_feature, vendor_mask = _parse_vendor_features(fptr, mask_length) except KeyError: msg = 'The ReaderRequirements box (rreq) has a mask length of {0} ' msg += 'bytes, but only values of 1, 2, 4, or 8 are supported. ' msg += 'The box contents will not be interpreted.' warnings.warn(msg.format(mask_length), UserWarning) box = ReaderRequirementsBox(fuam, dcm, standard_flag, standard_mask, vendor_feature, vendor_mask, length=length, offset=offset) return box def _parse_standard_flag(fptr, mask_length): """Construct standard flag, standard mask data from the file. Specifically working on Reader Requirements box. Parameters ---------- fptr : file object File object for JP2K file. mask_length : int Length of standard mask flag """ # The mask length tells us the format string to use when unpacking # from the buffer read from file. mask_format = {1: 'B', 2: 'H', 4: 'I'}[mask_length] read_buffer = fptr.read(2) num_standard_flags, = struct.unpack('>H', read_buffer) # Read in standard flags and standard masks. Each standard flag should # be two bytes, but the standard mask flag is as long as specified by # the mask length. read_buffer = fptr.read(num_standard_flags * (2 + mask_length)) fmt = '>' + ('H' + mask_format) * num_standard_flags data = struct.unpack(fmt, read_buffer) standard_flag = data[0:num_standard_flags * 2:2] standard_mask = data[1:num_standard_flags * 2:2] return standard_flag, standard_mask def _parse_vendor_features(fptr, mask_length): """Construct vendor features, vendor mask data from the file. Specifically working on Reader Requirements box. Parameters ---------- fptr : file object File object for JP2K file. mask_length : int Length of vendor mask flag """ # The mask length tells us the format string to use when unpacking # from the buffer read from file. mask_format = {1: 'B', 2: 'H', 4: 'I'}[mask_length] read_buffer = fptr.read(2) num_vendor_features, = struct.unpack('>H', read_buffer) # Each vendor feature consists of a 16-byte UUID plus a mask whose # length is specified by, you guessed it, "mask_length". entry_length = 16 + mask_length read_buffer = fptr.read(num_vendor_features * entry_length) vendor_feature = [] vendor_mask = [] for j in range(num_vendor_features): ubuffer = read_buffer[j * entry_length:(j + 1) * entry_length] vendor_feature.append(uuid.UUID(bytes=ubuffer[0:16])) vmask = struct.unpack('>' + mask_format, ubuffer[16:]) vendor_mask.append(vmask) return vendor_feature, vendor_mask class ResolutionBox(Jp2kBox): """Container for Resolution superbox 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. box : list List of boxes contained in this superbox. """ def __init__(self, length=0, offset=-1): Jp2kBox.__init__(self, box_id='res ', longname='Resolution') self.length = length self.offset = offset self.box = [] def __str__(self): msg = Jp2kBox.__str__(self) for box in self.box: boxstr = str(box) # Add indentation. strs = [('\n ' + x) for x in boxstr.split('\n')] msg += ''.join(strs) return msg @staticmethod def parse(fptr, offset, length): """Parse Resolution box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- ResolutionBox instance """ box = ResolutionBox(length=length, offset=offset) # The JP2 header box is a superbox, so go ahead and parse its child # boxes. box.box = box.parse_superbox(fptr) return box class CaptureResolutionBox(Jp2kBox): """Container for Capture resolution 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. vertical_resolution, horizontal_resolution : float Vertical, horizontal resolution. """ def __init__(self, vertical_resolution, horizontal_resolution, length=0, offset=-1): Jp2kBox.__init__(self, box_id='resc', longname='Capture Resolution') self.vertical_resolution = vertical_resolution self.horizontal_resolution = horizontal_resolution self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n VCR: {0}'.format(self.vertical_resolution) msg += '\n HCR: {0}'.format(self.horizontal_resolution) return msg @staticmethod def parse(fptr, offset, length): """Parse CaptureResolutionBox. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- CaptureResolutionBox instance """ read_buffer = fptr.read(10) (rn1, rd1, rn2, rd2, re1, re2) = struct.unpack('>HHHHBB', read_buffer) vres = rn1 / rd1 * math.pow(10, re1) hres = rn2 / rd2 * math.pow(10, re2) box = CaptureResolutionBox(vres, hres, length=length, offset=offset) return box class DisplayResolutionBox(Jp2kBox): """Container for Display resolution 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. vertical_resolution, horizontal_resolution : float Vertical, horizontal resolution. """ def __init__(self, vertical_resolution, horizontal_resolution, length=0, offset=-1): Jp2kBox.__init__(self, box_id='resd', longname='Display Resolution') self.vertical_resolution = vertical_resolution self.horizontal_resolution = horizontal_resolution self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n VDR: {0}'.format(self.vertical_resolution) msg += '\n HDR: {0}'.format(self.horizontal_resolution) return msg @staticmethod def parse(fptr, offset, length): """Parse display resolution box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- DisplayResolutionBox instance """ read_buffer = fptr.read(10) (rn1, rd1, rn2, rd2, re1, re2) = struct.unpack('>HHHHBB', read_buffer) vres = rn1 / rd1 * math.pow(10, re1) hres = rn2 / rd2 * math.pow(10, re2) box = DisplayResolutionBox(vres, hres, length=length, offset=offset) return box class LabelBox(Jp2kBox): """Container for Label 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. label : str Label """ def __init__(self, label, length=0, offset=-1): Jp2kBox.__init__(self, box_id='lbl ', longname='Label') self.label = label self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n Label: {0}'.format(self.label) return msg @staticmethod def parse(fptr, offset, length): """Parse Label box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- LabelBox instance """ num_bytes = offset + length - fptr.tell() read_buffer = fptr.read(num_bytes) label = read_buffer.decode('utf-8') box = LabelBox(label, length=length, offset=offset) return box class XMLBox(Jp2kBox): """Container for XML 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. xml : ElementTree object XML section. """ def __init__(self, xml=None, filename=None, length=0, offset=-1): """ 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, box_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.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) xml = self.xml if self.xml is not None: msg += _pretty_print_xml(self.xml) else: msg += '\n {0}'.format(xml) return msg def write(self, fptr): """Write an XML box to file. """ try: read_buffer = ET.tostring(self.xml, encoding='utf-8') except (AttributeError, AssertionError): # AssertionError on 2.6 read_buffer = ET.tostring(self.xml.getroot(), encoding='utf-8') fptr.write(struct.pack('>I', len(read_buffer) + 8)) fptr.write(self.box_id.encode()) fptr.write(read_buffer) @staticmethod def parse(fptr, offset, length): """Parse XML box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- XMLBox instance """ num_bytes = offset + length - fptr.tell() read_buffer = fptr.read(num_bytes) try: text = read_buffer.decode('utf-8') except UnicodeDecodeError as ude: # Possibly bad string of bytes to begin with. # Try to search for -1: text = read_buffer[decl_start:].decode('utf-8') else: raise # Let the user know that the XML box was problematic. msg = 'A UnicodeDecodeError was encountered parsing an XML box at ' msg += 'byte position {0} ({1}), but the XML was still recovered.' msg = msg.format(offset, ude.reason) warnings.warn(msg, UserWarning) # Strip out any trailing nulls, as they can foul up XML parsing. text = text.rstrip(chr(0)) try: elt = ET.fromstring(text.encode('utf-8')) xml = ET.ElementTree(elt) except ParseError as parse_error: msg = 'A problem was encountered while parsing an XML box:' msg += '\n\n\t"{0}"\n\nNo XML was retrieved.' msg = msg.format(str(parse_error)) warnings.warn(msg, UserWarning) xml = None box = XMLBox(xml=xml, length=length, offset=offset) return box class UUIDListBox(Jp2kBox): """Container for JPEG 2000 UUID list box. 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. ulst : list List of UUIDs. """ def __init__(self, ulst, length=0, offset=-1): Jp2kBox.__init__(self, box_id='ulst', longname='UUID List') self.ulst = ulst self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) for j, uuid_item in enumerate(self.ulst): msg += '\n UUID[{0}]: {1}'.format(j, uuid_item) return(msg) @staticmethod def parse(fptr, offset, length): """Parse UUIDList box. Parameters ---------- f : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- UUIDListBox instance """ read_buffer = fptr.read(2) num_uuids, = struct.unpack('>H', read_buffer) ulst = [] for _ in range(num_uuids): read_buffer = fptr.read(16) ulst.append(uuid.UUID(bytes=read_buffer)) box = UUIDListBox(ulst, length=length, offset=offset) return(box) class UUIDInfoBox(Jp2kBox): """Container for JPEG 2000 UUID Info superbox. 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. box : list List of boxes contained in this superbox. """ def __init__(self, length=0, offset=-1): Jp2kBox.__init__(self, box_id='uinf', longname='UUIDInfo') self.length = length self.offset = offset self.box = [] def __str__(self): msg = Jp2kBox.__str__(self) for box in self.box: box_str = str(box) # Add indentation. lst = [('\n ' + x) for x in box_str.split('\n')] msg += ''.join(lst) return(msg) @staticmethod def parse(fptr, offset, length): """Parse UUIDInfo super box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- UUIDInfoBox instance """ box = UUIDInfoBox(length=length, offset=offset) # The UUIDInfo box is a superbox, so go ahead and parse its child # boxes. box.box = box.parse_superbox(fptr) return box class DataEntryURLBox(Jp2kBox): """Container for data entry URL 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. version : byte Must be 0 for JP2. flag : bytes Particular attributes of this box, consists of three bytes. URL : str Associated URL. """ def __init__(self, version, flag, url, length=0, offset=-1): Jp2kBox.__init__(self, box_id='url ', longname='Data Entry URL') self.version = version self.flag = flag self.url = url self.length = length self.offset = offset def __str__(self): msg = Jp2kBox.__str__(self) msg += '\n ' lines = ['Version: {0}', 'Flag: {1} {2} {3}', 'URL: "{4}"'] msg += '\n '.join(lines) msg = msg.format(self.version, self.flag[0], self.flag[1], self.flag[2], self.url) return msg @staticmethod def parse(fptr, offset, length): """Parse data entry URL box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- DataEntryURLbox instance """ read_buffer = fptr.read(4) data = struct.unpack('>BBBB', read_buffer) version = data[0] flag = data[1:4] numbytes = offset + length - fptr.tell() read_buffer = fptr.read(numbytes) url = read_buffer.decode('utf-8') box = DataEntryURLBox(version, flag, url, length=length, offset=offset) return box class UUIDBox(Jp2kBox): """Container for UUID 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. uuid : uuid.UUID 16-byte UUID data : bytes or dict or ElementTree.Element Vendor-specific data. Exif UUIDs are interpreted as dictionaries. XMP UUIDs are interpreted as standard XML. References ---------- .. [XMP] International Organization for Standardication. ISO/IEC 16684-1:2012 - Graphic technology -- Extensible metadata platform (XMP) specification -- Part 1: Data model, serialization and core properties """ def __init__(self, the_uuid, raw_data, length=0, offset=-1): """ Parameters ---------- the_uuid : uuid.UUID Identifies the type of UUID box. raw_data : byte array This is the "payload" of data for the specified UUID. length : int length of the box in bytes. offset : int offset of the box from the start of the file. """ Jp2kBox.__init__(self, box_id='uuid', longname='UUID') self.uuid = the_uuid if the_uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): # XMP data. Parse as XML. Seems to be a difference between # ElementTree in version 2.7 and 3.3. if sys.hexversion < 0x03000000: elt = ET.fromstring(raw_data) else: text = raw_data.decode('utf-8') elt = ET.fromstring(text) self.data = ET.ElementTree(elt) elif the_uuid.bytes == b'JpgTiffExif->JP2': exif_obj = Exif(raw_data) ifds = OrderedDict() ifds['Image'] = exif_obj.exif_image ifds['Photo'] = exif_obj.exif_photo ifds['GPSInfo'] = exif_obj.exif_gpsinfo ifds['Iop'] = exif_obj.exif_iop self.data = ifds else: self.data = raw_data self.length = length self.offset = offset def __str__(self): msg = '{0}\n' msg += ' UUID: {1}{2}\n' msg += ' UUID Data: {3}' if self.uuid == uuid.UUID('be7acfcb-97a9-42e8-9c71-999491e3afac'): uuid_type = ' (XMP)' uuid_data = _pretty_print_xml(self.data) elif self.uuid.bytes == b'JpgTiffExif->JP2': uuid_type = ' (Exif)' # 2.7 has trouble pretty-printing ordered dicts, so print them # as regular dicts. Not ideal, but at least it's good on 3.3+. if sys.hexversion < 0x03000000: data = dict(self.data) else: data = self.data uuid_data = '\n' + pprint.pformat(data) else: uuid_type = '' uuid_data = '{0} bytes'.format(len(self.data)) msg = msg.format(Jp2kBox.__str__(self), self.uuid, uuid_type, uuid_data) return msg @staticmethod def parse(fptr, offset, length): """Parse UUID box. Parameters ---------- fptr : file Open file object. offset : int Start position of box in bytes. length : int Length of the box in bytes. Returns ------- UUIDBox instance """ read_buffer = fptr.read(16) the_uuid = uuid.UUID(bytes=read_buffer) numbytes = offset + length - fptr.tell() read_buffer = fptr.read(numbytes) box = UUIDBox(the_uuid, read_buffer, length=length, offset=offset) return box class Exif(object): """ Attributes ---------- read_buffer : bytes Raw byte stream consisting of the UUID data. endian : str Either '<' for big-endian, or '>' for little-endian. """ def __init__(self, read_buffer): """Interpret raw buffer consisting of Exif IFD. """ self.exif_image = None self.exif_photo = None self.exif_gpsinfo = None self.exif_iop = None self.read_buffer = read_buffer # Ignore the first six bytes. # Next 8 should be (73, 73, 42, 8) data = struct.unpack('' for little-endian. num_tags : int Number of tags in the IFD. raw_ifd : dictionary Maps tag number to "mildly-interpreted" tag value. processed_ifd : dictionary Maps tag name to "mildly-interpreted" tag value. """ datatype2fmt = {1: ('B', 1), 2: ('B', 1), 3: ('H', 2), 4: ('I', 4), 5: ('II', 8), 7: ('B', 1), 9: ('i', 4), 10: ('ii', 8)} def __init__(self, endian, read_buffer, offset): self.endian = endian self.read_buffer = read_buffer self.processed_ifd = OrderedDict() self.num_tags, = struct.unpack(endian + 'H', read_buffer[offset:offset + 2]) fmt = self.endian + 'HHII' * self.num_tags ifd_buffer = read_buffer[offset + 2:offset + 2 + self.num_tags * 12] data = struct.unpack(fmt, ifd_buffer) self.raw_ifd = OrderedDict() for j, tag in enumerate(data[0::4]): # The offset to the tag offset/payload is the offset to the IFD # plus 2 bytes for the number of tags plus 12 bytes for each # tag entry plus 8 bytes to the offset/payload itself. toffp = read_buffer[offset + 10 + j * 12:offset + 10 + j * 12 + 4] tag_data = self.parse_tag(data[j * 4 + 1], data[j * 4 + 2], toffp) self.raw_ifd[tag] = tag_data def parse_tag(self, dtype, count, offset_buf): """Interpret an Exif image tag data payload. """ fmt = self.datatype2fmt[dtype][0] * count payload_size = self.datatype2fmt[dtype][1] * count if payload_size <= 4: # Interpret the payload from the 4 bytes in the tag entry. target_buffer = offset_buf[:payload_size] else: # Interpret the payload at the offset specified by the 4 bytes in # the tag entry. offset, = struct.unpack(self.endian + 'I', offset_buf) target_buffer = self.read_buffer[offset:offset + payload_size] if dtype == 2: # ASCII if sys.hexversion < 0x03000000: payload = target_buffer.rstrip('\x00') else: payload = target_buffer.decode('utf-8').rstrip('\x00') else: payload = struct.unpack(self.endian + fmt, target_buffer) if dtype == 5 or dtype == 10: # Rational or Signed Rational. Construct the list of values. rational_payload = [] for j in range(count): value = float(payload[j * 2]) / float(payload[j * 2 + 1]) rational_payload.append(value) payload = rational_payload if count == 1: # If just a single value, then return a scalar instead of a # tuple. payload = payload[0] return payload def post_process(self, tagnum2name): """Map the tag name instead of tag number to the tag value. """ for tag, value in self.raw_ifd.items(): try: tag_name = tagnum2name[tag] except KeyError: # Ok, we don't recognize this tag. Just use the numeric id. msg = 'Unrecognized Exif tag "{0}".'.format(tag) warnings.warn(msg, UserWarning) tag_name = tag self.processed_ifd[tag_name] = value class _ExifImageIfd(_Ifd): """ Attributes ---------- tagnum2name : dict Maps Exif image tag numbers to the tag names. ifd : dict Maps tag names to tag values. """ tagnum2name = {11: 'ProcessingSoftware', 254: 'NewSubfileType', 255: 'SubfileType', 256: 'ImageWidth', 257: 'ImageLength', 258: 'BitsPerSample', 259: 'Compression', 262: 'PhotometricInterpretation', 263: 'Threshholding', 264: 'CellWidth', 265: 'CellLength', 266: 'FillOrder', 269: 'DocumentName', 270: 'ImageDescription', 271: 'Make', 272: 'Model', 273: 'StripOffsets', 274: 'Orientation', 277: 'SamplesPerPixel', 278: 'RowsPerStrip', 279: 'StripByteCounts', 282: 'XResolution', 283: 'YResolution', 284: 'PlanarConfiguration', 290: 'GrayResponseUnit', 291: 'GrayResponseCurve', 292: 'T4Options', 293: 'T6Options', 296: 'ResolutionUnit', 301: 'TransferFunction', 305: 'Software', 306: 'DateTime', 315: 'Artist', 316: 'HostComputer', 317: 'Predictor', 318: 'WhitePoint', 319: 'PrimaryChromaticities', 320: 'ColorMap', 321: 'HalftoneHints', 322: 'TileWidth', 323: 'TileLength', 324: 'TileOffsets', 325: 'TileByteCounts', 330: 'SubIFDs', 332: 'InkSet', 333: 'InkNames', 334: 'NumberOfInks', 336: 'DotRange', 337: 'TargetPrinter', 338: 'ExtraSamples', 339: 'SampleFormat', 340: 'SMinSampleValue', 341: 'SMaxSampleValue', 342: 'TransferRange', 343: 'ClipPath', 344: 'XClipPathUnits', 345: 'YClipPathUnits', 346: 'Indexed', 347: 'JPEGTables', 351: 'OPIProxy', 512: 'JPEGProc', 513: 'JPEGInterchangeFormat', 514: 'JPEGInterchangeFormatLength', 515: 'JPEGRestartInterval', 517: 'JPEGLosslessPredictors', 518: 'JPEGPointTransforms', 519: 'JPEGQTables', 520: 'JPEGDCTables', 521: 'JPEGACTables', 529: 'YCbCrCoefficients', 530: 'YCbCrSubSampling', 531: 'YCbCrPositioning', 532: 'ReferenceBlackWhite', 700: 'XMLPacket', 18246: 'Rating', 18249: 'RatingPercent', 32781: 'ImageID', 33421: 'CFARepeatPatternDim', 33422: 'CFAPattern', 33423: 'BatteryLevel', 33432: 'Copyright', 33434: 'ExposureTime', 33437: 'FNumber', 33723: 'IPTCNAA', 34377: 'ImageResources', 34665: 'ExifTag', 34675: 'InterColorProfile', 34850: 'ExposureProgram', 34852: 'SpectralSensitivity', 34853: 'GPSTag', 34855: 'ISOSpeedRatings', 34856: 'OECF', 34857: 'Interlace', 34858: 'TimeZoneOffset', 34859: 'SelfTimerMode', 36867: 'DateTimeOriginal', 37122: 'CompressedBitsPerPixel', 37377: 'ShutterSpeedValue', 37378: 'ApertureValue', 37379: 'BrightnessValue', 37380: 'ExposureBiasValue', 37381: 'MaxApertureValue', 37382: 'SubjectDistance', 37383: 'MeteringMode', 37384: 'LightSource', 37385: 'Flash', 37386: 'FocalLength', 37387: 'FlashEnergy', 37388: 'SpatialFrequencyResponse', 37389: 'Noise', 37390: 'FocalPlaneXResolution', 37391: 'FocalPlaneYResolution', 37392: 'FocalPlaneResolutionUnit', 37393: 'ImageNumber', 37394: 'SecurityClassification', 37395: 'ImageHistory', 37396: 'SubjectLocation', 37397: 'ExposureIndex', 37398: 'TIFFEPStandardID', 37399: 'SensingMethod', 40091: 'XPTitle', 40092: 'XPComment', 40093: 'XPAuthor', 40094: 'XPKeywords', 40095: 'XPSubject', 50341: 'PrintImageMatching', 50706: 'DNGVersion', 50707: 'DNGBackwardVersion', 50708: 'UniqueCameraModel', 50709: 'LocalizedCameraModel', 50710: 'CFAPlaneColor', 50711: 'CFALayout', 50712: 'LinearizationTable', 50713: 'BlackLevelRepeatDim', 50714: 'BlackLevel', 50715: 'BlackLevelDeltaH', 50716: 'BlackLevelDeltaV', 50717: 'WhiteLevel', 50718: 'DefaultScale', 50719: 'DefaultCropOrigin', 50720: 'DefaultCropSize', 50721: 'ColorMatrix1', 50722: 'ColorMatrix2', 50723: 'CameraCalibration1', 50724: 'CameraCalibration2', 50725: 'ReductionMatrix1', 50726: 'ReductionMatrix2', 50727: 'AnalogBalance', 50728: 'AsShotNeutral', 50729: 'AsShotWhiteXY', 50730: 'BaselineExposure', 50731: 'BaselineNoise', 50732: 'BaselineSharpness', 50733: 'BayerGreenSplit', 50734: 'LinearResponseLimit', 50735: 'CameraSerialNumber', 50736: 'LensInfo', 50737: 'ChromaBlurRadius', 50738: 'AntiAliasStrength', 50739: 'ShadowScale', 50740: 'DNGPrivateData', 50741: 'MakerNoteSafety', 50778: 'CalibrationIlluminant1', 50779: 'CalibrationIlluminant2', 50780: 'BestQualityScale', 50781: 'RawDataUniqueID', 50827: 'OriginalRawFileName', 50828: 'OriginalRawFileData', 50829: 'ActiveArea', 50830: 'MaskedAreas', 50831: 'AsShotICCProfile', 50832: 'AsShotPreProfileMatrix', 50833: 'CurrentICCProfile', 50834: 'CurrentPreProfileMatrix', 50879: 'ColorimetricReference', 50931: 'CameraCalibrationSignature', 50932: 'ProfileCalibrationSignature', 50934: 'AsShotProfileName', 50935: 'NoiseReductionApplied', 50936: 'ProfileName', 50937: 'ProfileHueSatMapDims', 50938: 'ProfileHueSatMapData1', 50939: 'ProfileHueSatMapData2', 50940: 'ProfileToneCurve', 50941: 'ProfileEmbedPolicy', 50942: 'ProfileCopyright', 50964: 'ForwardMatrix1', 50965: 'ForwardMatrix2', 50966: 'PreviewApplicationName', 50967: 'PreviewApplicationVersion', 50968: 'PreviewSettingsName', 50969: 'PreviewSettingsDigest', 50970: 'PreviewColorSpace', 50971: 'PreviewDateTime', 50972: 'RawImageDigest', 50973: 'OriginalRawFileDigest', 50974: 'SubTileBlockSize', 50975: 'RowInterleaveFactor', 50981: 'ProfileLookTableDims', 50982: 'ProfileLookTableData', 51008: 'OpcodeList1', 51009: 'OpcodeList2', 51022: 'OpcodeList3', 51041: 'NoiseProfile'} def __init__(self, endian, read_buffer, offset): _Ifd.__init__(self, endian, read_buffer, offset) self.post_process(self.tagnum2name) class _ExifPhotoIfd(_Ifd): """Represents tags found in the Exif sub ifd. """ tagnum2name = {33434: 'ExposureTime', 33437: 'FNumber', 34850: 'ExposureProgram', 34852: 'SpectralSensitivity', 34855: 'ISOSpeedRatings', 34856: 'OECF', 34864: 'SensitivityType', 34865: 'StandardOutputSensitivity', 34866: 'RecommendedExposureIndex', 34867: 'ISOSpeed', 34868: 'ISOSpeedLatitudeyyy', 34869: 'ISOSpeedLatitudezzz', 36864: 'ExifVersion', 36867: 'DateTimeOriginal', 36868: 'DateTimeDigitized', 37121: 'ComponentsConfiguration', 37122: 'CompressedBitsPerPixel', 37377: 'ShutterSpeedValue', 37378: 'ApertureValue', 37379: 'BrightnessValue', 37380: 'ExposureBiasValue', 37381: 'MaxApertureValue', 37382: 'SubjectDistance', 37383: 'MeteringMode', 37384: 'LightSource', 37385: 'Flash', 37386: 'FocalLength', 37396: 'SubjectArea', 37500: 'MakerNote', 37510: 'UserComment', 37520: 'SubSecTime', 37521: 'SubSecTimeOriginal', 37522: 'SubSecTimeDigitized', 40960: 'FlashpixVersion', 40961: 'ColorSpace', 40962: 'PixelXDimension', 40963: 'PixelYDimension', 40964: 'RelatedSoundFile', 40965: 'InteroperabilityTag', 41483: 'FlashEnergy', 41484: 'SpatialFrequencyResponse', 41486: 'FocalPlaneXResolution', 41487: 'FocalPlaneYResolution', 41488: 'FocalPlaneResolutionUnit', 41492: 'SubjectLocation', 41493: 'ExposureIndex', 41495: 'SensingMethod', 41728: 'FileSource', 41729: 'SceneType', 41730: 'CFAPattern', 41985: 'CustomRendered', 41986: 'ExposureMode', 41987: 'WhiteBalance', 41988: 'DigitalZoomRatio', 41989: 'FocalLengthIn35mmFilm', 41990: 'SceneCaptureType', 41991: 'GainControl', 41992: 'Contrast', 41993: 'Saturation', 41994: 'Sharpness', 41995: 'DeviceSettingDescription', 41996: 'SubjectDistanceRange', 42016: 'ImageUniqueID', 42032: 'CameraOwnerName', 42033: 'BodySerialNumber', 42034: 'LensSpecification', 42035: 'LensMake', 42036: 'LensModel', 42037: 'LensSerialNumber'} def __init__(self, endian, read_buffer, offset): _Ifd.__init__(self, endian, read_buffer, offset) self.post_process(self.tagnum2name) class _ExifGPSInfoIfd(_Ifd): """Represents information found in the GPSInfo sub IFD. """ tagnum2name = {0: 'GPSVersionID', 1: 'GPSLatitudeRef', 2: 'GPSLatitude', 3: 'GPSLongitudeRef', 4: 'GPSLongitude', 5: 'GPSAltitudeRef', 6: 'GPSAltitude', 7: 'GPSTimeStamp', 8: 'GPSSatellites', 9: 'GPSStatus', 10: 'GPSMeasureMode', 11: 'GPSDOP', 12: 'GPSSpeedRef', 13: 'GPSSpeed', 14: 'GPSTrackRef', 15: 'GPSTrack', 16: 'GPSImgDirectionRef', 17: 'GPSImgDirection', 18: 'GPSMapDatum', 19: 'GPSDestLatitudeRef', 20: 'GPSDestLatitude', 21: 'GPSDestLongitudeRef', 22: 'GPSDestLongitude', 23: 'GPSDestBearingRef', 24: 'GPSDestBearing', 25: 'GPSDestDistanceRef', 26: 'GPSDestDistance', 27: 'GPSProcessingMethod', 28: 'GPSAreaInformation', 29: 'GPSDateStamp', 30: 'GPSDifferential'} def __init__(self, endian, read_buffer, offset): _Ifd.__init__(self, endian, read_buffer, offset) self.post_process(self.tagnum2name) class _ExifInteroperabilityIfd(_Ifd): """Represents tags found in the Interoperability sub IFD. """ tagnum2name = {1: 'InteroperabilityIndex', 2: 'InteroperabilityVersion', 4096: 'RelatedImageFileFormat', 4097: 'RelatedImageWidth', 4098: 'RelatedImageLength'} def __init__(self, endian, read_buffer, offset): _Ifd.__init__(self, endian, read_buffer, offset) self.post_process(self.tagnum2name) # Map each box ID to the corresponding class. _BOX_WITH_ID = { 'asoc': AssociationBox, 'cdef': ChannelDefinitionBox, 'cmap': ComponentMappingBox, 'colr': ColourSpecificationBox, 'ftyp': FileTypeBox, 'ihdr': ImageHeaderBox, 'jP ': JPEG2000SignatureBox, 'jpch': CodestreamHeaderBox, 'jplh': CompositingLayerHeaderBox, 'jp2c': ContiguousCodestreamBox, 'jp2h': JP2HeaderBox, 'lbl ': LabelBox, 'pclr': PaletteBox, 'res ': ResolutionBox, 'resc': CaptureResolutionBox, 'resd': DisplayResolutionBox, 'rreq': ReaderRequirementsBox, 'uinf': UUIDInfoBox, 'ulst': UUIDListBox, 'url ': DataEntryURLBox, 'uuid': UUIDBox, 'xml ': XMLBox} def _indent(elem, level=0): """Recipe for pretty printing XML. Please see http://effbot.org/zone/element-lib.htm#prettyprint """ i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: _indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def _pretty_print_xml(xml, level=0): """Pretty print XML data. """ xml = copy.deepcopy(xml) _indent(xml.getroot(), level=level) xmltext = ET.tostring(xml.getroot(), encoding='utf-8').decode('utf-8') # Indent it a bit. lst = [(' ' + x) for x in xmltext.split('\n')] try: xml = '\n'.join(lst) return '\n{0}'.format(xml) except UnicodeEncodeError: # This can happen on python 2.x if the character set contains certain # non-ascii characters. Just print out the corresponding xml char # entities instead. xml = u'\n'.join(lst) text = u'\n{0}'.format(xml) text = text.encode('ascii', 'xmlcharrefreplace') return text