"""
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