"""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 <?xml and go from there.
            decl_start = read_buffer.find(b'<?xml')
            if decl_start > -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('<BBHI', read_buffer[6:14])
        if data[0] == 73 and data[1] == 73:
            # little endian
            self.endian = '<'
        else:
            # big endian
            self.endian = '>'
        offset = data[3]

        # This is the 'Exif Image' portion.
        exif = _ExifImageIfd(self.endian, read_buffer[6:], offset)
        self.exif_image = exif.processed_ifd

        if 'ExifTag' in self.exif_image.keys():
            offset = self.exif_image['ExifTag']
            photo = _ExifPhotoIfd(self.endian, read_buffer[6:], offset)
            self.exif_photo = photo.processed_ifd

            if 'InteroperabilityTag' in self.exif_photo.keys():
                offset = self.exif_photo['InteroperabilityTag']
                interop = _ExifInteroperabilityIfd(self.endian,
                                                   read_buffer[6:],
                                                   offset)
                self.iop = interop.processed_ifd

        if 'GPSTag' in self.exif_image.keys():
            offset = self.exif_image['GPSTag']
            gps = _ExifGPSInfoIfd(self.endian, read_buffer[6:], offset)
            self.exif_gpsinfo = gps.processed_ifd


class _Ifd(object):
    """
    Attributes
    ----------
    read_buffer : bytes
        Raw byte stream consisting of the UUID data.
    datatype2fmt : dictionary
        Class attribute, maps the TIFF enumerated datatype to the python
        datatype and data width.
    endian : str
        Either '<' for big-endian, or '>' 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
