# Source code for psd_tools.psd.layer_and_mask

    """
    Layer and mask data structure.
    """

    from __future__ import annotations

    import io
    import logging

    import attr

    from psd_tools.compression import compress, decompress
    from psd_tools.constants import (
        BlendMode,
        ChannelID,
        Clipping,
        Compression,
        GlobalLayerMaskKind,
        Tag,
    )
    from psd_tools.psd.base import BaseElement, ListElement
    from psd_tools.psd.tagged_blocks import TaggedBlocks, register
    from psd_tools.utils import (
        is_readable,
        read_fmt,
        read_length_block,
        read_pascal_string,
        write_bytes,
        write_fmt,
        write_length_block,
        write_padding,
        write_pascal_string,
    )
    from psd_tools.validators import in_, range_

    logger = logging.getLogger(__name__)

    @attr.s(repr=False, slots=True)
    class LayerAndMaskInformation(BaseElement):
        """
        Layer and mask information section.

        .. py:attribute:: layer_info

            See :py:class:`.LayerInfo`.

        .. py:attribute:: global_layer_mask_info

            See :py:class:`.GlobalLayerMaskInfo`.

        .. py:attribute:: tagged_blocks

            See :py:class:`.TaggedBlocks`.
        """

        layer_info = attr.ib(default=None)
        global_layer_mask_info = attr.ib(default=None)
        tagged_blocks = attr.ib(default=None)

        @classmethod
        def read(cls, fp, encoding="macroman", version=1):
            start_pos = fp.tell()
            length = read_fmt(("I", "Q")[version - 1], fp)[0]
            end_pos = fp.tell() + length
            logger.debug(
                "reading layer and mask info, len=%d, offset=%d" % (length, start_pos)
            )
            if length == 0:
                self = cls()
            else:
                self = cls._read_body(fp, end_pos, encoding, version)
            if fp.tell() > end_pos:
                logger.warning(
                    "LayerAndMaskInformation is broken: current fp=%d, expected=%d"
                    % (fp.tell(), end_pos)
                )
            fp.seek(end_pos, 0)
            return self

        @classmethod
        def _read_body(cls, fp, end_pos, encoding, version):
            layer_info = LayerInfo.read(fp, encoding, version)

            global_layer_mask_info = None
            if is_readable(fp, 17) and fp.tell() < end_pos:
                global_layer_mask_info = GlobalLayerMaskInfo.read(fp)

            tagged_blocks = None
            if is_readable(fp):
                # For some reason, global tagged blocks aligns 4 byte
                tagged_blocks = TaggedBlocks.read(
                    fp, version=version, padding=4, end_pos=end_pos
                )

            return cls(layer_info, global_layer_mask_info, tagged_blocks)

        def write(self, fp, encoding="macroman", version=1, padding=4):
            def writer(f):
                written = self._write_body(f, encoding, version, padding)
                logger.debug("writing layer and mask info, len=%d" % (written))
                return written

            fmt = ("I", "Q")[version - 1]
            return write_length_block(fp, writer, fmt=fmt)

        def _write_body(self, fp, encoding, version, padding):
            written = 0
            if self.layer_info:
                written += self.layer_info.write(fp, encoding, version, padding)
            if self.global_layer_mask_info:
                written += self.global_layer_mask_info.write(fp)
            if self.tagged_blocks:
                written += self.tagged_blocks.write(fp, version=version, padding=4)
            return written

    @attr.s(repr=False, slots=True)
    class LayerInfo(BaseElement):
        """
        High-level organization of the layer information.

        .. py:attribute:: layer_count

            Layer count. If it is a negative number, its absolute value is the
            number of layers and the first alpha channel contains the transparency
            data for the merged result.

        .. py:attribute:: layer_records

            Information about each layer. See :py:class:`.LayerRecords`.

        .. py:attribute:: channel_image_data

            Channel image data. See :py:class:`.ChannelImageData`.
        """

        layer_count = attr.ib(default=0, type=int)
        layer_records = attr.ib(default=None)
        channel_image_data = attr.ib(default=None)

        @classmethod
        def read(cls, fp, encoding="macroman", version=1):
            length = read_fmt(("I", "Q")[version - 1], fp)[0]
            logger.debug("reading layer info, len=%d" % length)
            end_pos = fp.tell() + length
            if length == 0:
                self = LayerInfo()
            else:
                self = cls._read_body(fp, encoding, version)
            assert fp.tell() <= end_pos
            fp.seek(end_pos, 0)
            return self

        @classmethod
        def _read_body(cls, fp, encoding, version):
            start_pos = fp.tell()
            layer_count = read_fmt("h", fp)[0]
            layer_records = LayerRecords.read(fp, layer_count, encoding, version)
            logger.debug("  read layer records, len=%d" % (fp.tell() - start_pos))
            channel_image_data = ChannelImageData.read(fp, layer_records)
            return cls(layer_count, layer_records, channel_image_data)

        def write(self, fp, encoding="macroman", version=1, padding=4):
            def writer(f):
                written = self._write_body(f, encoding, version, padding)
                logger.debug("writing layer info, len=%d" % (written))
                return written

            fmt = ("I", "Q")[version - 1]
            if self.layer_count == 0:
                return write_fmt(fp, fmt, 0)
            return write_length_block(fp, writer, fmt=fmt)

        def _write_body(self, fp, encoding, version, padding):
            start_pos = fp.tell()
            written = write_fmt(fp, "h", self.layer_count)
            if self.layer_records:
                self._update_channel_length()
                written += self.layer_records.write(fp, encoding, version)
            logger.debug("  wrote layer records, len=%d" % (fp.tell() - start_pos))
            if self.channel_image_data:
                written += self.channel_image_data.write(fp)
            # Seems the padding size here is different between Photoshop and GIMP.
            written += write_padding(fp, written, padding)
            return written

        def _update_channel_length(self):
            if not self.layer_records or not self.channel_image_data:
                return

            for layer, lengths in zip(self.layer_records, self.channel_image_data._lengths):
                for channel_info, length in zip(layer.channel_info, lengths):
                    channel_info.length = length

    @register(Tag.LAYER_16)
    @register(Tag.LAYER_32)
    @attr.s(repr=False)
    class LayerInfoBlock(LayerInfo):
        """ """

        @classmethod
        def read(cls, fp, encoding="macroman", version=1, **kwargs):
            return cls._read_body(fp, encoding, version)

        def write(self, fp, encoding="macroman", version=1, padding=4, **kwargs):
            return self._write_body(fp, encoding, version, padding)

    @attr.s(repr=False, slots=True)
    class ChannelInfo(BaseElement):
        """
        Channel information.

        .. py:attribute:: id

            Channel ID: 0 = red, 1 = green, etc.; -1 = transparency mask; -2 =
            user supplied layer mask, -3 real user supplied layer mask (when both
            a user mask and a vector mask are present). See
            :py:class:`~psd_tools.constants.ChannelID`.

        .. py:attribute:: length

            Length of the corresponding channel data.
        """

        id = attr.ib(
            default=ChannelID.CHANNEL_0, converter=ChannelID, validator=in_(ChannelID)
        )
        length = attr.ib(default=0, type=int)

        @classmethod
        def read(cls, fp, version=1):
            return cls(*read_fmt(("hI", "hQ")[version - 1], fp))

        def write(self, fp, version=1):
            return write_fmt(fp, ("hI", "hQ")[version - 1], *attr.astuple(self))

    @attr.s(repr=False, slots=True)
    class LayerFlags(BaseElement):
        """
        Layer flags.

        Note there are undocumented flags. Maybe photoshop version.

        .. py:attribute:: transparency_protected
        .. py:attribute:: visible
        .. py:attribute:: pixel_data_irrelevant
        """

        transparency_protected = attr.ib(default=False, type=bool)
        visible = attr.ib(default=True, type=bool)
        obsolete = attr.ib(default=False, type=bool, repr=False)
        photoshop_v5_later = attr.ib(default=True, type=bool, repr=False)
        pixel_data_irrelevant = attr.ib(default=False, type=bool)
        undocumented_1 = attr.ib(default=False, type=bool, repr=False)
        undocumented_2 = attr.ib(default=False, type=bool, repr=False)
        undocumented_3 = attr.ib(default=False, type=bool, repr=False)

        @classmethod
        def read(cls, fp):
            flags = read_fmt("B", fp)[0]
            return cls(
                bool(flags & 1),
                not bool(flags & 2),  # why "not"?
                bool(flags & 4),
                bool(flags & 8),
                bool(flags & 16),
                bool(flags & 32),
                bool(flags & 64),
                bool(flags & 128),
            )

        def write(self, fp):
            flags = (
                (self.transparency_protected * 1)
                | ((not self.visible) * 2)
                | (self.obsolete * 4)
                | (self.photoshop_v5_later * 8)
                | (self.pixel_data_irrelevant * 16)
                | (self.undocumented_1 * 32)
                | (self.undocumented_2 * 64)
                | (self.undocumented_3 * 128)
            )
            return write_fmt(fp, "B", flags)

    @attr.s(repr=False, slots=True)
    class LayerBlendingRanges(BaseElement):
        """
        Layer blending ranges.

        All ranges contain 2 black values followed by 2 white values.

        .. py:attribute:: composite_ranges

            List of composite gray blend source and destination ranges.

        .. py:attribute:: channel_ranges

            List of channel source and destination ranges.
        """

        composite_ranges = attr.ib(
            factory=lambda: [(0, 65535), (0, 65535)],
        )
        channel_ranges = attr.ib(
            factory=lambda: [
                [(0, 65535), (0, 65535)],
                [(0, 65535), (0, 65535)],
                [(0, 65535), (0, 65535)],
                [(0, 65535), (0, 65535)],
            ],
        )

        @classmethod
        def read(cls, fp):
            data = read_length_block(fp)
            if len(data) == 0:
                return cls(None, None)

            with io.BytesIO(data) as f:
                return cls._read_body(f)

        @classmethod
        def _read_body(cls, fp):
            def read_channel_range(f):
                values = read_fmt("4H", f)
                return [values[0:2], values[2:4]]

            composite_ranges = read_channel_range(fp)
            channel_ranges = []
            while is_readable(fp, 8):
                channel_ranges.append(read_channel_range(fp))
            return cls(composite_ranges, channel_ranges)

        def write(self, fp):
            return write_length_block(fp, lambda f: self._write_body(f))

        def _write_body(self, fp):
            written = 0
            if self.composite_ranges is not None:
                for x in self.composite_ranges:
                    written += write_fmt(fp, "2H", *x)
            if self.channel_ranges is not None:
                for channel in self.channel_ranges:
                    for x in channel:
                        written += write_fmt(fp, "2H", *x)
            return written

    class LayerRecords(ListElement):
        """
        List of layer records. See :py:class:`.LayerRecord`.
        """

        @classmethod
        def read(cls, fp, layer_count, encoding="macroman", version=1):
            items = []
            for _ in range(abs(layer_count)):
                items.append(LayerRecord.read(fp, encoding, version))
            return cls(items)

    @attr.s(repr=False, slots=True)
    class LayerRecord(BaseElement):
        """
        Layer record.

        .. py:attribute:: top

            Top position.

        .. py:attribute:: left

            Left position.

        .. py:attribute:: bottom

            Bottom position.

        .. py:attribute:: right

            Right position.

        .. py:attribute:: channel_info

            List of :py:class:`.ChannelInfo`.

        .. py:attribute:: signature

            Blend mode signature ``b'8BIM'``.

        .. py:attribute:: blend_mode

            Blend mode key. See :py:class:`~psd_tools.constants.BlendMode`.

        .. py:attribute:: opacity

            Opacity, 0 = transparent, 255 = opaque.

        .. py:attribute:: clipping

            Clipping, 0 = base, 1 = non-base. See
            :py:class:`~psd_tools.constants.Clipping`.

        .. py:attribute:: flags

            See :py:class:`.LayerFlags`.

        .. py:attribute:: mask_data

            :py:class:`.MaskData` or None.

        .. py:attribute:: blending_ranges

            See :py:class:`~psd_tools.constants.LayerBlendingRanges`.

        .. py:attribute:: name

            Layer name.

        .. py:attribute:: tagged_blocks

            See :py:class:`.TaggedBlocks`.
        """

        top = attr.ib(default=0, type=int)
        left = attr.ib(default=0, type=int)
        bottom = attr.ib(default=0, type=int)
        right = attr.ib(default=0, type=int)
        channel_info: list[ChannelInfo] = attr.ib(factory=list)
        signature = attr.ib(
            default=b"8BIM", repr=False, type=bytes, validator=in_((b"8BIM",))
        )
        blend_mode = attr.ib(
            default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode)
        )
        opacity = attr.ib(default=255, type=int, validator=range_(0, 255))
        clipping = attr.ib(
            default=Clipping.BASE, converter=Clipping, validator=in_(Clipping)
        )
        flags = attr.ib(factory=LayerFlags)
        mask_data = attr.ib(default=None)
        blending_ranges = attr.ib(factory=LayerBlendingRanges)
        name = attr.ib(default="", type=str)
        tagged_blocks: TaggedBlocks = attr.ib(factory=TaggedBlocks)

        @classmethod
        def read(cls, fp, encoding="macroman", version=1):
            start_pos = fp.tell()
            top, left, bottom, right, num_channels = read_fmt("4iH", fp)
            channel_info = [ChannelInfo.read(fp, version) for i in range(num_channels)]
            signature, blend_mode, opacity, clipping = read_fmt("4s4sBB", fp)
            flags = LayerFlags.read(fp)

            data = read_length_block(fp, fmt="xI")
            logger.debug("  read layer record, len=%d" % (fp.tell() - start_pos))
            with io.BytesIO(data) as f:
                self = cls(
                    top,
                    left,
                    bottom,
                    right,
                    channel_info,
                    signature,
                    blend_mode,
                    opacity,
                    clipping,
                    flags,
                    *cls._read_extra(f, encoding, version),
                )

            # with io.BytesIO() as f:
            #     self._write_extra(f, encoding, version)
            #     assert data == f.getvalue()

            return self

        @classmethod
        def _read_extra(cls, fp, encoding, version):
            mask_data = MaskData.read(fp)
            blending_ranges = LayerBlendingRanges.read(fp)
            name = read_pascal_string(fp, encoding, padding=4)
            tagged_blocks = TaggedBlocks.read(fp, version=version, padding=1)
            return mask_data, blending_ranges, name, tagged_blocks

        def write(self, fp, encoding="macroman", version=1):
            start_pos = fp.tell()
            written = write_fmt(
                fp,
                "4iH",
                self.top,
                self.left,
                self.bottom,
                self.right,
                len(self.channel_info),
            )
            written += sum(c.write(fp, version) for c in self.channel_info)
            written += write_fmt(
                fp,
                "4s4sBB",
                self.signature,
                self.blend_mode.value,
                self.opacity,
                self.clipping.value,
            )
            written += self.flags.write(fp)

            def writer(f):
                written = self._write_extra(f, encoding, version)
                logger.debug("  wrote layer record, len=%d" % (fp.tell() - start_pos))
                return written

            written += write_length_block(fp, writer, fmt="xI")
            return written

        def _write_extra(self, fp, encoding, version):
            written = 0
            if self.mask_data:
                written += self.mask_data.write(fp)
            else:
                written += write_fmt(fp, "I", 0)

            written += self.blending_ranges.write(fp)
            written += write_pascal_string(fp, self.name, encoding, padding=4)
            written += self.tagged_blocks.write(fp, version, padding=1)
            written += write_padding(fp, written, 2)
            return written

        @property
        def width(self):
            """Width of the layer."""
            return max(self.right - self.left, 0)

        @property
        def height(self):
            """Height of the layer."""
            return max(self.bottom - self.top, 0)

        @property
        def channel_sizes(self):
            """List of channel sizes: [(width, height)]."""
            sizes = []
            for channel in self.channel_info:
                if channel.id == ChannelID.USER_LAYER_MASK:
                    sizes.append((self.mask_data.width, self.mask_data.height))
                elif channel.id == ChannelID.REAL_USER_LAYER_MASK:
                    sizes.append((self.mask_data.real_width, self.mask_data.real_height))
                else:
                    sizes.append((self.width, self.height))
            return sizes

    @attr.s(repr=False, slots=True)
    class MaskFlags(BaseElement):
        """
        Mask flags.

        .. py:attribute:: pos_relative_to_layer

            Position relative to layer.

        .. py:attribute:: mask_disabled

            Layer mask disabled.

        .. py:attribute:: invert_mask

            Invert layer mask when blending (Obsolete).

        .. py:attribute:: user_mask_from_render

            The user mask actually came from rendering other data.

        .. py:attribute:: parameters_applied

            The user and/or vector masks have parameters applied to them.
        """

        pos_relative_to_layer = attr.ib(default=False, type=bool)
        mask_disabled = attr.ib(default=False, type=bool)
        invert_mask = attr.ib(default=False, type=bool)
        user_mask_from_render = attr.ib(default=False, type=bool)
        parameters_applied = attr.ib(default=False, type=bool)
        undocumented_1 = attr.ib(default=False, type=bool, repr=False)
        undocumented_2 = attr.ib(default=False, type=bool, repr=False)
        undocumented_3 = attr.ib(default=False, type=bool, repr=False)

        @classmethod
        def read(cls, fp):
            flags = read_fmt("B", fp)[0]
            return cls(
                bool(flags & 1),
                bool(flags & 2),
                bool(flags & 4),
                bool(flags & 8),
                bool(flags & 16),
                bool(flags & 32),
                bool(flags & 64),
                bool(flags & 128),
            )

        def write(self, fp):
            flags = (
                (self.pos_relative_to_layer * 1)
                | (self.mask_disabled * 2)
                | (self.invert_mask * 4)
                | (self.user_mask_from_render * 8)
                | (self.parameters_applied * 16)
                | (self.undocumented_1 * 32)
                | (self.undocumented_2 * 64)
                | (self.undocumented_3 * 128)
            )
            return write_fmt(fp, "B", flags)

    @attr.s(repr=False, slots=True)
    class MaskData(BaseElement):
        """
        Mask data.

        Real user mask is a final composite mask of vector and pixel masks.

        .. py:attribute:: top

            Top position.

        .. py:attribute:: left

            Left position.

        .. py:attribute:: bottom

            Bottom position.

        .. py:attribute:: right

            Right position.

        .. py:attribute:: background_color

            Default color. 0 or 255.

        .. py:attribute:: flags

            See :py:class:`.MaskFlags`.

        .. py:attribute:: parameters

            :py:class:`.MaskParameters` or None.

        .. py:attribute:: real_flags

            Real user mask flags. See :py:class:`.MaskFlags`.

        .. py:attribute:: real_background_color

            Real user mask background. 0 or 255.

        .. py:attribute:: real_top

            Top position of real user mask.

        .. py:attribute:: real_left

            Left position of real user mask.

        .. py:attribute:: real_bottom

            Bottom position of real user mask.

        .. py:attribute:: real_right

            Right position of real user mask.
        """

        top = attr.ib(default=0, type=int)
        left = attr.ib(default=0, type=int)
        bottom = attr.ib(default=0, type=int)
        right = attr.ib(default=0, type=int)
        background_color = attr.ib(default=0, type=int)
        flags = attr.ib(factory=MaskFlags)
        parameters = attr.ib(default=None)
        real_flags = attr.ib(default=None)
        real_background_color = attr.ib(default=None)
        real_top = attr.ib(default=None)
        real_left = attr.ib(default=None)
        real_bottom = attr.ib(default=None)
        real_right = attr.ib(default=None)

        @classmethod
        def read(cls, fp):
            data = read_length_block(fp)
            if len(data) == 0:
                return None

            with io.BytesIO(data) as f:
                return cls._read_body(f, len(data))

        @classmethod
        def _read_body(cls, fp, length):
            top, left, bottom, right, background_color = read_fmt("4iB", fp)
            flags = MaskFlags.read(fp)

            # Order is based on tests. The specification is messed up here...

            # if length == 20:
            #     read_fmt('2x', fp)
            #     return cls(top, left, bottom, right, background_color, flags)

            real_flags, real_background_color = None, None
            real_top, real_left, real_bottom, real_right = None, None, None, None
            if length >= 36:
                real_flags = MaskFlags.read(fp)
                real_background_color = read_fmt("B", fp)[0]
                real_top, real_left, real_bottom, real_right = read_fmt("4i", fp)

            parameters = None
            if flags.parameters_applied:
                parameters = MaskParameters.read(fp)

            # logger.debug('    skipping %d' % (len(fp.read())))
            return cls(
                top,
                left,
                bottom,
                right,
                background_color,
                flags,
                parameters,
                real_flags,
                real_background_color,
                real_top,
                real_left,
                real_bottom,
                real_right,
            )

        def write(self, fp):
            return write_length_block(fp, lambda f: self._write_body(f))

        def _write_body(self, fp):
            written = write_fmt(
                fp,
                "4iB",
                self.top,
                self.left,
                self.bottom,
                self.right,
                self.background_color,
            )
            written += self.flags.write(fp)

            # if self.real_flags is None and self.parameters is None:
            #     written += write_fmt(fp, '2x')
            #     assert written == 20

            if self.real_flags:
                written += self.real_flags.write(fp)
                written += write_fmt(
                    fp,
                    "B4i",
                    self.real_background_color,
                    self.real_top,
                    self.real_left,
                    self.real_bottom,
                    self.real_right,
                )

            if self.flags.parameters_applied and self.parameters:
                written += self.parameters.write(fp)

            written += write_padding(fp, written, 4)
            return written

        @property
        def width(self):
            """Width of the mask."""
            return max(self.right - self.left, 0)

        @property
        def height(self):
            """Height of the mask."""
            return max(self.bottom - self.top, 0)

        @property
        def real_width(self):
            """Width of real user mask."""
            return max(self.real_right - self.real_left, 0)

        @property
        def real_height(self):
            """Height of real user mask."""
            return max(self.real_bottom - self.real_top, 0)

    @attr.s(repr=False, slots=True)
    class MaskParameters(BaseElement):
        """
        Mask parameters.

        .. py:attribute:: user_mask_density
        .. py:attribute:: user_mask_feather
        .. py:attribute:: vector_mask_density
        .. py:attribute:: vector_mask_feather
        """

        user_mask_density = attr.ib(default=None)
        user_mask_feather = attr.ib(default=None)
        vector_mask_density = attr.ib(default=None)
        vector_mask_feather = attr.ib(default=None)

        @classmethod
        def read(cls, fp):
            parameters = read_fmt("B", fp)[0]
            return cls(
                read_fmt("B", fp)[0] if bool(parameters & 1) else None,
                read_fmt("d", fp)[0] if bool(parameters & 2) else None,
                read_fmt("B", fp)[0] if bool(parameters & 4) else None,
                read_fmt("d", fp)[0] if bool(parameters & 8) else None,
            )

        def write(self, fp):
            written = 0
            written += write_fmt(
                fp,
                "B",
                (
                    (1 if self.user_mask_density is not None else 0)
                    | (2 if self.user_mask_feather is not None else 0)
                    | (4 if self.vector_mask_density is not None else 0)
                    | (8 if self.vector_mask_feather is not None else 0)
                ),
            )
            if self.user_mask_density is not None:
                written += write_fmt(fp, "B", self.user_mask_density)
            if self.user_mask_feather is not None:
                written += write_fmt(fp, "d", self.user_mask_feather)
            if self.vector_mask_density is not None:
                written += write_fmt(fp, "B", self.vector_mask_density)
            if self.vector_mask_feather is not None:
                written += write_fmt(fp, "d", self.vector_mask_feather)
            return written

    class ChannelImageData(ListElement):
        """
        List of channel data list.

        This size of this list corresponds to the size of
        :py:class:`LayerRecords`. Each item corresponds to the channels of each
        layer.

        See :py:class:`.ChannelDataList`.
        """

        @classmethod
        def read(cls, fp, layer_records=None):
            start_pos = fp.tell()
            items = []
            for layer in layer_records:
                items.append(ChannelDataList.read(fp, layer.channel_info))
            logger.debug("  read channel image data, len=%d" % (fp.tell() - start_pos))
            return cls(items)

        def write(self, fp, **kwargs):
            start_pos = fp.tell()
            written = sum(item.write(fp) for item in self)
            logger.debug("  wrote channel image data, len=%d" % (fp.tell() - start_pos))
            return written

        @property
        def _lengths(self):
            """List of layer channel lengths."""
            return [item._lengths for item in self]

    class ChannelDataList(ListElement):
        """
        List of channel image data, corresponding to each color or alpha.

        See :py:class:`.ChannelData`.
        """

        @classmethod
        def read(cls, fp, channel_info, **kwargs):
            items = []
            for c in channel_info:
                items.append(ChannelData.read(fp, c.length - 2, **kwargs))
            return cls(items)

        @property
        def _lengths(self):
            """List of channel lengths."""
            return [item._length for item in self]

    @attr.s(repr=False, slots=True)
    class ChannelData(BaseElement):
        """
        Channel data.

        .. py:attribute:: compression

            Compression type. See :py:class:`~psd_tools.constants.Compression`.

        .. py:attribute:: data

            Data.
        """

        compression = attr.ib(
            default=Compression.RAW, converter=Compression, validator=in_(Compression)
        )
        data = attr.ib(default=b"", type=bytes)

        @classmethod
        def read(cls, fp, length=0, **kwargs):
            compression = Compression(read_fmt("H", fp)[0])
            data = fp.read(length)
            return cls(compression, data)

        def write(self, fp, **kwargs):
            written = write_fmt(fp, "H", self.compression.value)
            written += write_bytes(fp, self.data)
            # written += write_padding(fp, written, 2)  # Seems no padding here.
            return written

        def get_data(self, width, height, depth, version=1):
            """Get decompressed channel data.

            :param width: width.
            :param height: height.
            :param depth: bit depth of the pixel.
            :param version: psd file version.
            :rtype: bytes
            """
            return decompress(self.data, self.compression, width, height, depth, version)

        def set_data(self, data, width, height, depth, version=1):
            """Set raw channel data and compress to store.

            :param data: raw data bytes to write.
            :param compression: compression type,
                see :py:class:`~psd_tools.constants.Compression`.
            :param width: width.
            :param height: height.
            :param depth: bit depth of the pixel.
            :param version: psd file version.
            """
            self.data = compress(data, self.compression, width, height, depth, version)
            return len(self.data)

        @property
        def _length(self):
            """Length of channel data block."""
            return 2 + len(self.data)

    @attr.s(repr=False, slots=True)
    class GlobalLayerMaskInfo(BaseElement):
        """
        Global mask information.

        .. py:attribute:: overlay_color

            Overlay color space (undocumented) and color components.

        .. py:attribute:: opacity

            Opacity. 0 = transparent, 100 = opaque.

        .. py:attribute:: kind

            Kind.
            0 = Color selected--i.e. inverted;
            1 = Color protected;
            128 = use value stored per layer. This value is preferred. The others
            are for backward compatibility with beta versions.
        """

        overlay_color = attr.ib(default=None)
        opacity = attr.ib(default=0, type=int)
        kind = attr.ib(
            default=GlobalLayerMaskKind.PER_LAYER,
            converter=GlobalLayerMaskKind,
            validator=in_(GlobalLayerMaskKind),
        )

        @classmethod
        def read(cls, fp):
            pos = fp.tell()
            data = read_length_block(fp)  # fmt?
            logger.debug("reading global layer mask info, len=%d" % (len(data)))
            if len(data) == 0:
                return cls(overlay_color=None)
            elif len(data) < 13:
                logger.warning(
                    "global layer mask info is broken, expected 13 bytes but found"
                    " only %d" % (len(data))
                )
                fp.seek(pos)
                return cls(overlay_color=None)

            with io.BytesIO(data) as f:
                return cls._read_body(f)

        @classmethod
        def _read_body(cls, fp):
            overlay_color = list(read_fmt("5H", fp))
            opacity, kind = read_fmt("HB", fp)
            return cls(overlay_color, opacity, kind)

        def write(self, fp):
            return write_length_block(fp, lambda f: self._write_body(f))

        def _write_body(self, fp):
            written = 0
            if self.overlay_color is not None:
                written = write_fmt(fp, "5H", *self.overlay_color)
                written += write_fmt(fp, "HB", self.opacity, self.kind.value)
                written += write_padding(fp, written, 4)
            logger.debug("writing global layer mask info, len=%d" % (written))
            return written