Source code for psd_tools.api.shape

"""
Shape module.

In PSD/PSB, shapes are all represented as :py:class:`VectorMask` in each
layer, and optionally there might be :py:class:`Origination` object to control
live shape properties and :py:class:`Stroke` to specify how outline is
stylized.
"""

from __future__ import absolute_import
import logging

from psd_tools.psd.vector import Subpath, InitialFillRule, ClipboardRecord
from psd_tools.terminology import Event

logger = logging.getLogger(__name__)


[docs]class VectorMask(object): """ Vector mask data. Vector mask is a resolution-independent mask that consists of one or more Path objects. In Photoshop, all the path objects are represented as Bezier curves. Check :py:attr:`~psd_tools.api.shape.VectorMask.paths` property for how to deal with path objects. """ def __init__(self, data): self._data = data self._build() def _build(self): self._paths = [] self._clipboard_record = None self._initial_fill_rule = None for x in self._data.path: if isinstance(x, InitialFillRule): self._initial_fill_rule = x elif isinstance(x, ClipboardRecord): self._clipboard_record = x elif isinstance(x, Subpath): self._paths.append(x) @property def inverted(self): """Invert the mask.""" return self._data.invert @property def not_linked(self): """If the knots are not linked.""" return self._data.not_link @property def disabled(self): """If the mask is disabled.""" return self._data.disable @property def paths(self): """ List of :py:class:`~psd_tools.psd.vector.Subpath`. Subpath is a list-like structure that contains one or more :py:class:`~psd_tools.psd.vector.Knot` items. Knot contains relative coordinates of control points for a Bezier curve. :py:attr:`~psd_tools.psd.vector.Subpath.index` indicates which origination item the subpath belongs, and :py:class:`~psd_tools.psd.vector.Subpath.operation` indicates how to combine multiple shape paths. In PSD, path fill rule is even-odd. Example:: for subpath in layer.vector_mask.paths: anchors = [( int(knot.anchor[1] * psd.width), int(knot.anchor[0] * psd.height), ) for knot in subpath] :return: List of Subpath. """ return self._paths @property def initial_fill_rule(self): """ Initial fill rule. When 0, fill inside of the path. When 1, fill outside of the shape. :return: `int` """ return self._initial_fill_rule.value @initial_fill_rule.setter def initial_fill_rule(self, value): assert value in (0, 1) self._initial_fill_rule.value = value @property def clipboard_record(self): """ Clipboard record containing bounding box information. Depending on the Photoshop version, this field can be `None`. """ return self._clipboard_record @property def bbox(self): """ Bounding box tuple (left, top, right, bottom) in relative coordinates, where top-left corner is (0., 0.) and bottom-right corner is (1., 1.). :return: `tuple` """ from itertools import chain knots = [(knot.anchor[1], knot.anchor[0]) for knot in chain.from_iterable(self.paths)] if len(knots) == 0: return (0., 0., 1., 1.) x, y = zip(*knots) return (min(x), min(y), max(x), max(y)) def __repr__(self): bbox = self.bbox return '%s(bbox=(%g, %g, %g, %g) paths=%d%s)' % ( self.__class__.__name__, bbox[0], bbox[1], bbox[2], bbox[3], len(self.paths), ' disabled' if self.disabled else '', )
[docs]class Stroke(object): """ Stroke contains decorative information for strokes. This is a thin wrapper around :py:class:`~psd_tools.psd.descriptor.Descriptor` structure. Check `_data` attribute to get the raw data. """ STROKE_STYLE_LINE_CAP_TYPES = { b'strokeStyleButtCap': 'butt', b'strokeStyleRoundCap': 'round', b'strokeStyleSquareCap': 'square', } STROKE_STYLE_LINE_JOIN_TYPES = { b'strokeStyleMiterJoin': 'miter', b'strokeStyleRoundJoin': 'round', b'strokeStyleBevelJoin': 'bevel', } STROKE_STYLE_LINE_ALIGNMENTS = { b'strokeStyleAlignInside': 'inner', b'strokeStyleAlignOutside': 'outer', b'strokeStyleAlignCenter': 'center', } def __init__(self, data): self._data = data if self._data.classID not in (b'strokeStyle', Event.Stroke): logger.warning("Unknown class ID found: {}".format(self._data.classID)) @property def enabled(self): """If the stroke is enabled.""" return bool(self._data.get(b'strokeEnabled')) @property def fill_enabled(self): """If the stroke fill is enabled.""" return bool(self._data.get(b'fillEnabled')) @property def line_width(self): """Stroke width in float.""" return self._data.get(b'strokeStyleLineWidth') @property def line_dash_set(self): """ Line dash set in list of :py:class:`~psd_tools.decoder.actions.UnitFloat`. :return: list """ return self._data.get(b'strokeStyleLineDashSet') @property def line_dash_offset(self): """ Line dash offset in float. :return: float """ return self._data.get(b'strokeStyleLineDashOffset') @property def miter_limit(self): """Miter limit in float.""" return self._data.get(b'strokeStyleMiterLimit') @property def line_cap_type(self): """Cap type, one of `butt`, `round`, `square`.""" key = self._data.get(b'strokeStyleLineCapType').enum return self.STROKE_STYLE_LINE_CAP_TYPES.get(key, str(key)) @property def line_join_type(self): """Join type, one of `miter`, `round`, `bevel`.""" key = self._data.get(b'strokeStyleLineJoinType').enum return self.STROKE_STYLE_LINE_JOIN_TYPES.get(key, str(key)) @property def line_alignment(self): """Alignment, one of `inner`, `outer`, `center`.""" key = self._data.get(b'strokeStyleLineAlignment').enum return self.STROKE_STYLE_LINE_ALIGNMENTS.get(key, str(key)) @property def scale_lock(self): return self._data.get(b'strokeStyleScaleLock') @property def stroke_adjust(self): """Stroke adjust""" return self._data.get(b'strokeStyleStrokeAdjust') @property def blend_mode(self): """Blend mode.""" return self._data.get(b'strokeStyleBlendMode').enum @property def opacity(self): """Opacity value.""" return self._data.get(b'strokeStyleOpacity') @property def content(self): """ Fill effect. """ return self._data.get(b'strokeStyleContent') def __repr__(self): return '%s(width=%g)' % (self.__class__.__name__, self.line_width)
class Origination(object): """ Vector origination. Vector origination keeps live shape properties for some of the primitive shapes. """ @classmethod def create(kls, data): if data.get(b'keyShapeInvalidated'): return Invalidated(data) origin_type = data.get(b'keyOriginType') types = {1: Rectangle, 2: RoundedRectangle, 4: Line, 5: Ellipse} return types.get(origin_type, kls)(data) def __init__(self, data): self._data = data @property def origin_type(self): """ Type of the vector shape. * 1: :py:class:`~psd_tools.api.shape.Rectangle` * 2: :py:class:`~psd_tools.api.shape.RoundedRectangle` * 4: :py:class:`~psd_tools.api.shape.Line` * 5: :py:class:`~psd_tools.api.shape.Ellipse` :return: `int` """ return int(self._data.get(b'keyOriginType')) @property def resolution(self): """Resolution. :return: `float` """ return float(self._data.get(b'keyOriginResolution')) @property def bbox(self): """ Bounding box of the live shape. :return: :py:class:`~psd_tools.psd.descriptor.Descriptor` """ bbox = self._data.get(b'keyOriginShapeBBox') if bbox: return ( bbox.get(b'Left').value, bbox.get(b'Top ').value, bbox.get(b'Rght').value, bbox.get(b'Btom').value, ) return (0, 0, 0, 0) @property def index(self): """ Origination item index. :return: `int` """ return self._data.get(b'keyOriginIndex') @property def invalidated(self): """ :return: `bool` """ return False def __repr__(self): bbox = self.bbox return '%s(bbox=(%g, %g, %g, %g))' % ( self.__class__.__name__, bbox[0], bbox[1], bbox[2], bbox[3] )
[docs]class Invalidated(Origination): """ Invalidated live shape. This equals to a primitive shape that does not provide Live shape properties. Use :py:class:`~psd_tools.api.shape.VectorMask` to access shape information instead of this origination object. """ @property def invalidated(self): return True def __repr__(self): return '%s()' % (self.__class__.__name__)
[docs]class Rectangle(Origination): """Rectangle live shape.""" pass
[docs]class Ellipse(Origination): """Ellipse live shape.""" pass
[docs]class RoundedRectangle(Origination): """Rounded rectangle live shape.""" @property def radii(self): """ Corner radii of rounded rectangles. The order is top-left, top-right, bottom-left, bottom-right. :return: :py:class:`~psd_tools.psd.descriptor.Descriptor` """ return self._data.get(b'keyOriginRRectRadii')
[docs]class Line(Origination): """Line live shape.""" @property def line_end(self): """ Line end. :return: :py:class:`~psd_tools.psd.descriptor.Descriptor` """ return self._data.get(b'keyOriginLineEnd') @property def line_start(self): """ Line start. :return: :py:class:`~psd_tools.psd.descriptor.Descriptor` """ return self._data.get(b'keyOriginLineStart') @property def line_weight(self): """ Line weight :return: `float` """ return self._data.get(b'keyOriginLineWeight') @property def arrow_start(self): """Line arrow start. :return: `bool` """ return bool(self._data.get(b'keyOriginLineArrowSt')) @property def arrow_end(self): """ Line arrow end. :return: `bool` """ return bool(self._data.get(b'keyOriginLineArrowEnd')) @property def arrow_width(self): """Line arrow width. :return: `float` """ return self._data.get(b'keyOriginLineArrWdth') @property def arrow_length(self): """Line arrow length. :return: `float` """ return self._data.get(b'keyOriginLineArrLngth') @property def arrow_conc(self): """ :return: `int` """ return self._data.get(b'keyOriginLineArrConc')