Source code for psd_tools.psd.vector

"""
Vector mask, path, and stroke structure.
"""

from __future__ import absolute_import, unicode_literals

import logging

import attr

from psd_tools.constants import PathResourceID
from psd_tools.psd.base import BaseElement, ListElement, ValueElement
from psd_tools.psd.descriptor import Descriptor
from psd_tools.utils import (
    is_readable,
    new_registry,
    read_fmt,
    write_fmt,
    write_padding,
)

logger = logging.getLogger(__name__)

TYPES, register = new_registry(attribute="selector")  # Path item types.


def decode_fixed_point(numbers):
    return tuple(float(x) / 0x01000000 for x in numbers)


def encode_fixed_point(numbers):
    return tuple(int(x * 0x01000000) for x in numbers)


[docs] @attr.s(repr=False, slots=True) class Path(ListElement): """ List-like Path structure. Elements are either PathFillRule, InitialFillRule, ClipboardRecord, ClosedPath, or OpenPath. """ @classmethod def read(cls, fp): items = [] while is_readable(fp, 26): selector = PathResourceID(read_fmt("H", fp)[0]) kls = TYPES.get(selector) items.append(kls.read(fp)) return cls(items) def write(self, fp, padding=4): written = 0 for item in self: written += write_fmt(fp, "H", item.selector.value) written += item.write(fp) written += write_padding(fp, written, padding) return written
[docs] @attr.s(repr=False, slots=True) class Subpath(ListElement): """ Subpath element. This is a list of Knot objects. .. note:: There are undocumented data associated with this structure. .. py:attribute:: operation `int` value indicating how multiple subpath should be combined: 1: Or (union), 2: Not-Or, 3: And (intersect), 0: Xor (exclude) The first path element is applied to the background surface. Intersection does not have strokes. .. py:attribute:: index `int` index that specifies corresponding origination object. """ # Undocumented data that seem to contain path operation info. operation = attr.ib(default=1, type=int) # Type of shape operation. _unknown1 = attr.ib(default=1, type=int) _unknown2 = attr.ib(default=0, type=int) index = attr.ib(default=0, type=int) # Origination index. _unknown3 = attr.ib(default=b"\x00" * 10, type=bytes, repr=False) @classmethod def read(cls, fp): items = [] length, operation, _unknown1, _unknown2, index, _unknown3 = read_fmt( "HhH2I10s", fp ) for _ in range(length): selector = PathResourceID(read_fmt("H", fp)[0]) kls = TYPES.get(selector) items.append(kls.read(fp)) return cls( items=items, operation=operation, index=index, unknown1=_unknown1, unknown2=_unknown2, unknown3=_unknown3, ) def write(self, fp): written = write_fmt( fp, "HhH2I10s", len(self), self.operation, self._unknown1, self._unknown2, self.index, self._unknown3, ) for item in self: written += write_fmt(fp, "H", item.selector.value) written += item.write(fp) return written
[docs] def is_closed(self): """ Returns whether if the path is closed or not. :return: `bool`. """ raise NotImplementedError
# TODO: Make subpath repr better. # def __repr__(self):
[docs] @attr.s(repr=False, slots=True) class Knot(BaseElement): """ Knot element consisting of 3 control points for Bezier curves. .. py:attribute:: preceding (y, x) tuple of preceding control point in relative coordinates. .. py:attribute:: anchor (y, x) tuple of anchor point in relative coordinates. .. py:attribute:: leaving (y, x) tuple of leaving control point in relative coordinates. """ preceding = attr.ib(default=(0.0, 0.0), type=tuple) anchor = attr.ib(default=(0.0, 0.0), type=tuple) leaving = attr.ib(default=(0.0, 0.0), type=tuple) @classmethod def read(cls, fp): preceding = decode_fixed_point(read_fmt("2i", fp)) anchor = decode_fixed_point(read_fmt("2i", fp)) leaving = decode_fixed_point(read_fmt("2i", fp)) return cls(preceding, anchor, leaving) def write(self, fp): values = self.preceding + self.anchor + self.leaving return write_fmt(fp, "6i", *encode_fixed_point(values))
@register(PathResourceID.CLOSED_LENGTH) class ClosedPath(Subpath): def is_closed(self): return True @register(PathResourceID.OPEN_LENGTH) class OpenPath(Subpath): def is_closed(self): return False @register(PathResourceID.CLOSED_KNOT_LINKED) class ClosedKnotLinked(Knot): pass @register(PathResourceID.CLOSED_KNOT_UNLINKED) class ClosedKnotUnlinked(Knot): pass @register(PathResourceID.OPEN_KNOT_LINKED) class OpenKnotLinked(Knot): pass @register(PathResourceID.OPEN_KNOT_UNLINKED) class OpenKnotUnlinked(Knot): pass
[docs] @register(PathResourceID.PATH_FILL) @attr.s(repr=False, slots=True) class PathFillRule(BaseElement): """ Path fill rule record, empty. """ @classmethod def read(cls, fp): read_fmt("24x", fp) return cls() def write(self, fp): return write_fmt(fp, "24x")
[docs] @register(PathResourceID.CLIPBOARD) @attr.s(repr=False, slots=True) class ClipboardRecord(BaseElement): """ Clipboard record. .. py:attribute:: top Top position in `int` .. py:attribute:: left Left position in `int` .. py:attribute:: bottom Bottom position in `int` .. py:attribute:: right Right position in `int` .. py:attribute:: resolution Resolution in `int` """ 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) resolution = attr.ib(default=0, type=int) @classmethod def read(cls, fp): return cls(*decode_fixed_point(read_fmt("5i4x", fp))) def write(self, fp): return write_fmt(fp, "5i4x", *encode_fixed_point(attr.astuple(self)))
[docs] @register(PathResourceID.INITIAL_FILL) @attr.s(repr=False, slots=True) class InitialFillRule(ValueElement): """ Initial fill rule record. .. py:attribute:: rule A value of 1 means that the fill starts with all pixels. The value will be either 0 or 1. """ value = attr.ib(default=0, converter=int, type=int) @classmethod def read(cls, fp): return cls(*read_fmt("H22x", fp)) def write(self, fp): return write_fmt(fp, "H22x", *attr.astuple(self))
[docs] @attr.s(repr=False, slots=True) class VectorMaskSetting(BaseElement): """ VectorMaskSetting structure. .. py:attribute:: version .. py:attribute:: path List of :py:class:`~psd_tools.psd.vector.Subpath` objects. """ version = attr.ib(default=3, type=int) flags = attr.ib(default=0, type=int) path = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): version, flags = read_fmt("2I", fp) assert version == 3, "Unknown vector mask version %d" % version path = Path.read(fp) return cls(version, flags, path) def write(self, fp, **kwargs): written = write_fmt(fp, "2I", self.version, self.flags) written += self.path.write(fp) return written @property def invert(self): """Flag to indicate that the vector mask is inverted.""" return bool(self.flags & 1) @property def not_link(self): """Flag to indicate that the vector mask is not linked.""" return bool(self.flags & 2) @property def disable(self): """Flag to indicate that the vector mask is disabled.""" return bool(self.flags & 4)
[docs] @attr.s(repr=False, slots=True) class VectorStrokeContentSetting(Descriptor): """ Dict-like Descriptor-based structure. See :py:class:`~psd_tools.psd.descriptor.Descriptor`. .. py:attribute:: key .. py:attribute:: version """ key = attr.ib(default=b"\x00\x00\x00\x00", type=bytes) version = attr.ib(default=1, type=int) @classmethod def read(cls, fp, **kwargs): key, version = read_fmt("4sI", fp) return cls(key=key, version=version, **cls._read_body(fp)) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, "4sI", self.key, self.version) written += self._write_body(fp) written += write_padding(fp, written, padding) return written