Source code for pytoshop.path

# -*- coding: utf-8 -*-


"""
Handle Bézier paths.
"""


from __future__ import unicode_literals, absolute_import


import struct


import six


from . import docs
from . import enums
from . import util


from typing import Any, BinaryIO, Dict, List, Optional, TYPE_CHECKING, Union  # NOQA
if TYPE_CHECKING:
    from . import core  # NOQA


def _to_float(value):  # type: (Union[float, int]) -> float
    if isinstance(value, int):
        value = float(value)

    if not isinstance(value, float):
        raise ValueError("Must be a float")

    return value


def _read_point(x, size):
    # type: (Union[float, int], Union[float, int]) -> float
    return (float(x) / (1 << 24)) * float(size)


def _write_point(x, size):
    # type: (Union[float, int], Union[float, int]) -> float
    return int((x / float(size)) * (1 << 24))


class _PathRecordMeta(type):
    """
    A metaclass that builds a mapping of subclasses.
    """
    mapping = {}  # type: Dict[int, PathRecord]

    def __new__(cls, name, parents, dct):
        new_cls = type.__new__(cls, name, parents, dct)

        if '_type' in dct:
            if dct['_type'] in cls.mapping:
                raise ValueError("Duplicate type '{}'".format(dct['_type']))
            cls.mapping[dct['_type']] = new_cls

        return new_cls


[docs]@six.add_metaclass(_PathRecordMeta) class PathRecord(object): _type = -1 @property def type(self): # type: (...) -> int return self._type
[docs] def length(self, header): # type: (core.Header) -> int return 26
[docs] @classmethod @util.trace_read def read(cls, fd, header): # type: (BinaryIO, core.Header) -> PathRecord type = util.read_value(fd, 'H') new_cls = _PathRecordMeta.mapping[type] return new_cls.read_data(fd, header)
[docs] @classmethod def read_data(cls, fd, header): # type: (BinaryIO, core.Header) -> PathRecord raise NotImplementedError()
[docs] def write(self, fd, header): # type: (BinaryIO, core.Header) -> None util.write_value(fd, 'H', self.type) self.write_data(fd, header)
[docs] def write_data(self, fd, header): # type: (BinaryIO, core.Header) -> None raise NotImplementedError()
[docs]class PathFillRuleRecord(PathRecord): _type = enums.PathRecordType.path_fill_rule_record
[docs] @classmethod def read_data(cls, fd, header): # type: (BinaryIO, core.Header) -> PathRecord padding = fd.read(24) if padding != b'\0' * 24: # pragma: no cover raise ValueError( "Invalid padding in path fill rule record") return cls()
[docs] def write_data(self, fd, header): # type: (BinaryIO, core.Header) -> None fd.write(b'\0' * 24)
[docs]class InitialFillRuleRecord(PathRecord): def __init__(self, all_pixels=False # type: bool ): # type: (...) -> None self.all_pixels = all_pixels _type = enums.PathRecordType.initial_fill_rule_record @property def all_pixels(self): # type: (...) -> bool 'Fill starts with all pixels' return self._all_pixels @all_pixels.setter def all_pixels(self, value): # type: (Any) -> None self._all_pixels = bool(value)
[docs] @classmethod def read_data(cls, fd, header): # type: (BinaryIO, core.Header) -> PathRecord all_pixels = bool(util.read_value(fd, 'H')) padding = fd.read(22) if padding != b'\0' * 22: # pragma: no cover raise ValueError( "Invalid padding in initial fill rule record") util.log("all_pixels: {}", all_pixels) return cls(all_pixels=all_pixels)
[docs] def write_data(self, fd, header): # type: (BinaryIO, core.Header) -> None util.write_value(fd, 'H', self.all_pixels) fd.write(b'\0' * 22)
class _LengthRecord(PathRecord): def __init__(self, num_knots=0 # type: int ): # type: (...) -> None self.num_knots = num_knots @property def num_knots(self): # type: (...) -> int "Number of Bezier knots" return self._num_knots @num_knots.setter def num_knots(self, value): # type: (int) -> None if (not isinstance(value, int) or value < 0 or value > 65535): raise ValueError("num_knots must be a 16-bit integer") self._num_knots = value @classmethod def read_data(cls, fd, header): # type: (BinaryIO, core.Header) -> PathRecord num_knots = util.read_value(fd, 'H') fd.read(22) util.log('num_knots: {}', num_knots) return cls(num_knots=num_knots) def write_data(self, fd, header): # type: (BinaryIO, core.Header) -> None util.write_value(fd, 'H', self.num_knots) fd.write(b'\0' * 22)
[docs]class ClosedSubpathLengthRecord(_LengthRecord): _type = enums.PathRecordType.closed_subpath_length
[docs]class OpenSubpathLengthRecord(_LengthRecord): _type = enums.PathRecordType.open_subpath_length
class _PointRecord(PathRecord): def __init__(self, y0=0.0, # type: float x0=0.0, # type: float y1=None, # type: Optional[float] x1=None, # type: Optional[float] y2=None, # type: Optional[float] x2=None # type: Optional[float] ): # type: (...) -> None self.y0 = y0 self.x0 = x0 self.y1 = y1 self.x1 = x1 self.y2 = y2 self.x2 = x2 @property def y0(self): # type: (...) -> Union[int, float] 'y of control point preceding the knot, in pixels' return self._y0 @y0.setter def y0(self, value): # type: (Union[int, float]) -> None self._y0 = _to_float(value) @property def x0(self): # type: (...) -> Union[int, float] 'x of control point preceding the knot, in pixels' return self._x0 @x0.setter def x0(self, value): # type: (Union[int, float]) -> None self._x0 = _to_float(value) @property def y1(self): # type: (...) -> Optional[Union[int, float]] 'y of anchor point of the knot' return self._y1 @y1.setter def y1(self, value): # type: (Optional[Union[int, float]]) -> None if value is None: self._y1 = value else: self._y1 = _to_float(value) @property def x1(self): # type: (...) -> Optional[Union[int, float]] 'x of anchor point of the knot' return self._x1 @x1.setter def x1(self, value): # type: (Optional[Union[int, float]]) -> None if value is None: self._x1 = value else: self._x1 = _to_float(value) @property def y2(self): # type: (...) -> Optional[Union[int, float]] 'y of control point for the segment leaving the knot, in pixels' return self._y2 @y2.setter def y2(self, value): # type: (Optional[Union[int, float]]) -> None if value is None: self._y2 = value else: self._y2 = _to_float(value) @property def x2(self): # type: (...) -> Optional[Union[int, float]] 'x of control point for the segment leaving the knot, in pixels' return self._x2 @x2.setter def x2(self, value): # type: (Optional[Union[int, float]]) -> None if value is None: self._x2 = value else: self._x2 = _to_float(value) @classmethod def read_data(cls, fd, header): # type: (BinaryIO, core.Header) -> PathRecord data = fd.read(24) y0, x0, y1, x1, y2, x2 = struct.unpack(str('>iiiiii'), data) y0 = _read_point(y0, header.height) x0 = _read_point(x0, header.width) y1 = _read_point(y1, header.height) x1 = _read_point(x1, header.width) y2 = _read_point(y2, header.height) x2 = _read_point(x2, header.width) util.log( '({}, {}) ({}, {}), ({}, {})', y0, x0, y1, x1, y2, x2) return cls(y0=y0, x0=x0, y1=y1, x1=x1, y2=y2, x2=x2) def write_data(self, fd, header): # type: (BinaryIO, core.Header) -> None y0 = _write_point(self.y0, header.height) x0 = _write_point(self.x0, header.width) if self.y1 is None: y1 = y0 else: y1 = _write_point(self.y1, header.height) if self.x1 is None: x1 = x0 else: x1 = _write_point(self.x1, header.width) if self.y2 is None: y2 = y0 else: y2 = _write_point(self.y2, header.height) if self.x2 is None: x2 = x0 else: x2 = _write_point(self.x2, header.width) util.write_value(fd, 'iiiiii', y0, x0, y1, x1, y2, x2)
[docs]class ClosedSubpathBezierKnotLinked(_PointRecord): _type = enums.PathRecordType.closed_subpath_bezier_knot_linked
[docs]class ClosedSubpathBezierKnotUnlinked(_PointRecord): _type = enums.PathRecordType.closed_subpath_bezier_knot_unlinked
[docs]class OpenSubpathBezierKnotLinked(_PointRecord): _type = enums.PathRecordType.open_subpath_bezier_knot_linked
[docs]class OpenSubpathBezierKnotUnlinked(_PointRecord): _type = enums.PathRecordType.open_subpath_bezier_knot_unlinked
[docs]class ClipboardRecord(PathRecord): def __init__(self, top=0.0, # type: float left=0.0, # type: float bottom=0.0, # type: float right=0.0, # type: float resolution=0 # type: int ): # type: (...) -> None self.top = top self.left = left self.bottom = bottom self.right = right self.resolution = resolution _type = enums.PathRecordType.clipboard_record @property def top(self): # type: (...) -> Union[int, float] "top, in pixels" return self._top @top.setter def top(self, value): # type: (Union[int, float]) -> None self._top = _to_float(value) @property def left(self): # type: (...) -> Union[int, float] "left, in pixels" return self._left @left.setter def left(self, value): # type: (Union[int, float]) -> None self._left = _to_float(value) @property def bottom(self): # type: (...) -> Union[int, float] "bottom, in pixels" return self._bottom @bottom.setter def bottom(self, value): # type: (Union[int, float]) -> None self._bottom = _to_float(value) @property def right(self): # type: (...) -> Union[int, float] "right, in pixels" return self._right @right.setter def right(self, value): # type: (Union[int, float]) -> None self._right = _to_float(value) @property def resolution(self): # type: (...) -> int "resolution" return self._resolution @resolution.setter def resolution(self, value): # type: (int) -> None if not isinstance(value, int): raise TypeError("resolution must be an int") self._resolution = value
[docs] @classmethod def read_data(cls, fd, header): # type: (BinaryIO, core.Header) -> PathRecord data = fd.read(24) top, left, bottom, right, resolution, _ = struct.unpack( str('>iiiiii'), data) top = _read_point(top, header.height) left = _read_point(left, header.width) bottom = _read_point(bottom, header.height) right = _read_point(right, header.width) util.log( 'position: ({}, {}, {}, {}), resolution: {}', top, left, bottom, right, resolution) return cls(top=top, left=left, bottom=bottom, right=right, resolution=resolution)
[docs] def write_data(self, fd, header): # type: (BinaryIO, core.Header) -> None top = _write_point(self.top, header.height) left = _write_point(self.left, header.width) bottom = _write_point(self.bottom, header.height) right = _write_point(self.right, header.width) util.write_value( fd, 'iiiiii', top, left, bottom, right, self.resolution, 0 )
[docs]class PathResource(object): def __init__(self, path_records=[] # List[PathRecord] ): self.path_records = path_records @property def path_records(self): # type: (...) -> List[PathRecord] "List of `PathRecord` instances." return self._path_records @path_records.setter def path_records(self, value): # type: (List[PathRecord]) -> None util.assert_is_list_of(value, PathRecord) self._path_records = value
[docs] def length(self, header): # type: (core.Header) -> int return sum(x.length(header) for x in self.path_records)
length.__doc__ = docs.length # type: ignore
[docs] @classmethod def read(cls, fd, length, header): # type: (BinaryIO, int, core.Header) -> PathResource end = fd.tell() + length path_records = [] while fd.tell() + 26 <= end: path_records.append( PathRecord.read(fd, header)) if (path_records[0].type != enums.PathRecordType.path_fill_rule_record): raise ValueError( 'Path resource must start with path_fill_rule_record') fd.seek(end - fd.tell(), 1) return cls(path_records=path_records)
[docs] @classmethod def from_rect(cls, top, # type: Union[int, float] left, # type: Union[int, float] bottom, # type: Union[int, float] right, # type: Union[int, float] all_pixels=True # type: bool ): # type: (...) -> PathResource return cls(path_records=[ PathFillRuleRecord(), InitialFillRuleRecord(all_pixels=False), OpenSubpathLengthRecord(num_knots=5), OpenSubpathBezierKnotLinked(y0=top, x0=left), OpenSubpathBezierKnotLinked(y0=top, x0=right), OpenSubpathBezierKnotLinked(y0=bottom, x0=right), OpenSubpathBezierKnotLinked(y0=bottom, x0=left), OpenSubpathBezierKnotLinked(y0=top, x0=left), ] )
[docs] def write(self, fd, header): # type: (BinaryIO, core.Header) -> None for path_record in self.path_records: path_record.write(fd, header)