"""
Vector mask, path, and stroke structure.
"""
import logging
from typing import Any, BinaryIO, Sequence, TypeVar
from typing_extensions import Self
from attrs import define, field, astuple
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.psd.bin_utils import (
is_readable,
read_fmt,
write_fmt,
write_padding,
)
from psd_tools.registry import new_registry
logger = logging.getLogger(__name__)
TYPES, register = new_registry(attribute="selector") # Path item types.
T_Path = TypeVar("T_Path", bound="Path")
T_Subpath = TypeVar("T_Subpath", bound="Subpath")
T_Knot = TypeVar("T_Knot", bound="Knot")
T_PathFillRule = TypeVar("T_PathFillRule", bound="PathFillRule")
T_ClipboardRecord = TypeVar("T_ClipboardRecord", bound="ClipboardRecord")
T_InitialFillRule = TypeVar("T_InitialFillRule", bound="InitialFillRule")
T_VectorMaskSetting = TypeVar("T_VectorMaskSetting", bound="VectorMaskSetting")
T_VectorStrokeContentSetting = TypeVar(
"T_VectorStrokeContentSetting", bound="VectorStrokeContentSetting"
)
def decode_fixed_point(numbers: Sequence[int]) -> tuple[float, ...]:
return tuple(float(x) / 0x01000000 for x in numbers)
def encode_fixed_point(numbers: Sequence[float]) -> tuple[int, ...]:
return tuple(int(x * 0x01000000) for x in numbers)
[docs]
@define(repr=False)
class Path(ListElement):
"""
List-like Path structure. Elements are either PathFillRule,
InitialFillRule, ClipboardRecord, ClosedPath, or OpenPath.
"""
@classmethod
def read(cls: type[T_Path], fp: BinaryIO, **kwargs: Any) -> T_Path:
items = []
while is_readable(fp, 26):
selector = PathResourceID(read_fmt("H", fp)[0])
kls = TYPES.get(selector)
assert kls is not None
items.append(kls.read(fp))
return cls(items)
def write(self, fp: BinaryIO, padding: int = 4, **kwargs: Any) -> int:
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]
@define(repr=False)
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: int = 1 # Type of shape operation.
_unknown1: int = 1
_unknown2: int = 0
index: int = 0 # type: ignore[assignment] # Origination index.
_unknown3: bytes = field(default=b"\x00" * 10, repr=False)
@classmethod
def read(cls: type[T_Subpath], fp: BinaryIO, **kwargs: Any) -> T_Subpath:
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)
assert kls is not None
items.append(kls.read(fp))
return cls(
items=items,
operation=operation,
index=index,
unknown1=_unknown1,
unknown2=_unknown2,
unknown3=_unknown3,
)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
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) -> bool:
"""
Returns whether if the path is closed or not.
:return: `bool`.
"""
raise NotImplementedError
def __repr__(self) -> str:
"""Return a string representation of the subpath."""
return "%s(index=%d, operation=%d)" % (
self.__class__.__name__,
self.index,
self.operation,
)
def _repr_pretty_(self, p: Any, cycle: bool) -> None:
if cycle:
p.text(f"({self.__class__.__name__} ...)")
return
p.text(self.__repr__())
super()._repr_pretty_(p, cycle)
[docs]
@define(repr=False)
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: tuple = (0.0,)
anchor: tuple = (0.0,)
leaving: tuple = (0.0,)
@classmethod
def read(cls: type[T_Knot], fp: BinaryIO, **kwargs: Any) -> T_Knot:
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: BinaryIO, **kwargs: Any) -> int:
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) -> bool:
return True
@register(PathResourceID.OPEN_LENGTH)
class OpenPath(Subpath):
def is_closed(self) -> bool:
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)
@define(repr=False)
class PathFillRule(BaseElement):
"""
Path fill rule record, empty.
"""
@classmethod
def read(cls: type[T_PathFillRule], fp: BinaryIO, **kwargs: Any) -> T_PathFillRule:
read_fmt("24x", fp)
return cls()
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "24x")
[docs]
@register(PathResourceID.CLIPBOARD)
@define(repr=False)
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: int = 0
left: int = 0
bottom: int = 0
right: int = 0
resolution: int = 0
@classmethod
def read(
cls: type[T_ClipboardRecord], fp: BinaryIO, **kwargs: Any
) -> T_ClipboardRecord:
return cls(*decode_fixed_point(read_fmt("5i4x", fp))) # type: ignore[arg-type]
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "5i4x", *encode_fixed_point(astuple(self)))
[docs]
@register(PathResourceID.INITIAL_FILL)
@define(repr=False)
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: int = field(default=0, converter=int)
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> Self:
return cls(*read_fmt("H22x", fp))
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
return write_fmt(fp, "H22x", *astuple(self))
[docs]
@define(repr=False)
class VectorMaskSetting(BaseElement):
"""
VectorMaskSetting structure.
.. py:attribute:: version
.. py:attribute:: path
List of :py:class:`~psd_tools.psd.vector.Subpath` objects.
"""
version: int = 3
flags: int = 0
path: Path | None = None
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> Self:
version, flags = read_fmt("2I", fp)
assert version == 3, "Unknown vector mask version %d" % version
path = Path.read(fp)
return cls(version=version, flags=flags, path=path)
def write(self, fp: BinaryIO, **kwargs: Any) -> int:
written = write_fmt(fp, "2I", self.version, self.flags)
if self.path:
written += self.path.write(fp)
return written
@property
def invert(self) -> bool:
"""Flag to indicate that the vector mask is inverted."""
return bool(self.flags & 1)
@property
def not_link(self) -> bool:
"""Flag to indicate that the vector mask is not linked."""
return bool(self.flags & 2)
@property
def disable(self) -> bool:
"""Flag to indicate that the vector mask is disabled."""
return bool(self.flags & 4)
[docs]
@define(repr=False)
class VectorStrokeContentSetting(Descriptor):
"""
Dict-like Descriptor-based structure. See
:py:class:`~psd_tools.psd.descriptor.Descriptor`.
.. py:attribute:: key
.. py:attribute:: version
"""
key: bytes = b"\x00\x00\x00\x00"
version: int = 1
@classmethod
def read(cls, fp: BinaryIO, **kwargs: Any) -> Self:
key, version = read_fmt("4sI", fp)
return cls(key=key, version=version, **cls._read_body(fp))
def write(self, fp: BinaryIO, padding: int = 4, **kwargs: Any) -> int:
written = write_fmt(fp, "4sI", self.key, self.version)
written += self._write_body(fp)
written += write_padding(fp, written, padding)
return written