示例#1
0
class FileHeader(BaseElement):
    """
    Header section of the PSD file.

    Example::

        from psd_tools.psd.header import FileHeader
        from psd_tools.constants import ColorMode

        header = FileHeader(channels=2, height=359, width=400, depth=8,
                            color_mode=ColorMode.GRAYSCALE)

    .. py:attribute:: signature

        Signature: always equal to ``b'8BPS'``.

    .. py:attribute:: version

        Version number. PSD is 1, and PSB is 2.

    .. py:attribute:: channels

        The number of channels in the image, including any alpha channels.

    .. py:attribute:: height

        The height of the image in pixels.

    .. py:attribute:: width

        The width of the image in pixels.

    .. py:attribute:: depth

        The number of bits per channel.

    .. py:attribute:: color_mode

        The color mode of the file. See
        :py:class:`~psd_tools.constants.ColorMode`
    """
    _FORMAT = '4sH6xHIIHH'

    signature = attr.ib(default=b'8BPS', type=bytes, repr=False)
    version = attr.ib(default=1, type=int, validator=in_((1, 2)))
    channels = attr.ib(default=4, type=int, validator=range_(1, 57))
    height = attr.ib(default=64, type=int, validator=range_(1, 300001))
    width = attr.ib(default=64, type=int, validator=range_(1, 300001))
    depth = attr.ib(default=8, type=int, validator=in_((1, 8, 16, 32)))
    color_mode = attr.ib(default=ColorMode.RGB,
                         converter=ColorMode,
                         validator=in_(ColorMode))

    @signature.validator
    def _validate_signature(self, attribute, value):
        if value != b'8BPS':
            raise ValueError('This is not a PSD or PSB file')

    @classmethod
    def read(cls, fp):
        """Read the element from a file-like object.

        :param fp: file-like object
        :rtype: FileHeader
        """
        return cls(*read_fmt(cls._FORMAT, fp))

    def write(self, fp):
        """Write the element to a file-like object.

        :param fp: file-like object
        """
        return write_fmt(fp, self._FORMAT, *attr.astuple(self))
示例#2
0
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 = 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 = attr.ib(factory=TaggedBlocks)

    @classmethod
    def read(cls, fp, encoding='macroman', version=1):
        """Read the element from a file-like object.

        :param fp: file-like object
        :param encoding: encoding of the string
        :param version: psd file version
        :rtype: :py:class:`.LayerRecord`
        """
        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):
        """Write the element to a file-like object.

        :param fp: file-like object
        :param encoding: encoding of the string
        :param version: psd file version
        """
        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
示例#3
0
class LinkedLayer(BaseElement):
    """
    LinkedLayer structure.

    .. py:attribute:: kind
    .. py:attribute:: version
    .. py:attribute:: uuid
    .. py:attribute:: filename
    .. py:attribute:: filetype
    .. py:attribute:: creator
    .. py:attribute:: filesize
    .. py:attribute:: open_file
    .. py:attribute:: linked_file
    .. py:attribute:: timestamp
    .. py:attribute:: data
    .. py:attribute:: child_id
    .. py:attribute:: mod_time
    .. py:attribute:: lock_state
    """
    kind = attr.ib(default=LinkedLayerType.ALIAS,
                   validator=in_(LinkedLayerType))
    version = attr.ib(default=1, validator=range_(1, 7))
    uuid = attr.ib(default='', type=str)
    filename = attr.ib(default='', type=str)
    filetype = attr.ib(default=b'\x00\x00\x00\x00', type=bytes)
    creator = attr.ib(default=b'\x00\x00\x00\x00', type=bytes)
    filesize = attr.ib(default=None)
    open_file = attr.ib(default=None)
    linked_file = attr.ib(default=None)
    timestamp = attr.ib(default=None)
    data = attr.ib(default=None)
    child_id = attr.ib(default=None)
    mod_time = attr.ib(default=None)
    lock_state = attr.ib(default=None)

    @classmethod
    def read(cls, fp, **kwargs):
        kind = LinkedLayerType(read_fmt('4s', fp)[0])
        version = read_fmt('I', fp)[0]
        assert 1 <= version and version <= 7, 'Invalid version %d' % (version)
        uuid = read_pascal_string(fp, 'macroman', padding=1)
        filename = read_unicode_string(fp)
        filetype, creator, datasize, open_file = read_fmt('4s4sQB', fp)
        if open_file:
            open_file = DescriptorBlock.read(fp, padding=1)
        else:
            open_file = None

        linked_file = None
        timestamp = None
        data = None
        filesize = None
        child_id = None
        mod_time = None
        lock_state = None

        if kind == LinkedLayerType.EXTERNAL:
            linked_file = DescriptorBlock.read(fp, padding=1)
            if version > 3:
                timestamp = read_fmt('I4Bd', fp)
            filesize = read_fmt('Q', fp)[0]  # External file size.
            if version > 2:
                data = fp.read(datasize)
        elif kind == LinkedLayerType.ALIAS:
            read_fmt('8x', fp)
        if kind == LinkedLayerType.DATA:
            data = fp.read(datasize)
            assert len(data) == datasize, '(%d vs %d)' % (len(data), datasize)

        # The followings are not well documented...
        if version >= 5:
            child_id = read_unicode_string(fp)
        if version >= 6:
            mod_time = read_fmt('d', fp)[0]
        if version >= 7:
            lock_state = read_fmt('B', fp)[0]
        if kind == LinkedLayerType.EXTERNAL and version == 2:
            data = fp.read(datasize)

        return cls(kind, version, uuid, filename, filetype, creator, filesize,
                   open_file, linked_file, timestamp, data, child_id, mod_time,
                   lock_state)

    def write(self, fp, padding=1, **kwargs):
        written = write_fmt(fp, '4sI', self.kind.value, self.version)
        written += write_pascal_string(fp, self.uuid, 'macroman', padding=1)
        written += write_unicode_string(fp, self.filename)
        written += write_fmt(fp, '4s4sQB', self.filetype, self.creator,
                             len(self.data) if self.data is not None else 0,
                             self.open_file is not None)
        if self.open_file is not None:
            written += self.open_file.write(fp, padding=1)

        if self.kind == LinkedLayerType.EXTERNAL:
            written += self.linked_file.write(fp, padding=1)
            if self.version > 3:
                written += write_fmt(fp, 'I4Bd', *self.timestamp)
            written += write_fmt(fp, 'Q', self.filesize)
            if self.version > 2:
                written += write_bytes(fp, self.data)
        elif self.kind == LinkedLayerType.ALIAS:
            written += write_fmt(fp, '8x')
        if self.kind == LinkedLayerType.DATA:
            written += write_bytes(fp, self.data)

        if self.child_id is not None:
            written += write_unicode_string(fp, self.child_id)
        if self.mod_time is not None:
            written += write_fmt(fp, 'd', self.mod_time)
        if self.lock_state is not None:
            written += write_fmt(fp, 'B', self.lock_state)

        if self.kind == LinkedLayerType.EXTERNAL and self.version == 2:
            written += write_bytes(fp, self.data)

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