Source code for psd_tools.composite.vector

"""Vector shapes and path operations for compositing."""

import logging

import numpy as np
from PIL import Image

from psd_tools.composite._compat import require_aggdraw

logger = logging.getLogger(__name__)


[docs] @require_aggdraw def draw_vector_mask(layer): """ Draw a vector mask. Requires aggdraw for bezier curve rasterization. """ return _draw_path(layer, brush={"color": 255})
[docs] @require_aggdraw def draw_stroke(layer): """ Draw a stroke. Requires aggdraw for bezier curve rasterization. """ desc = layer.stroke._data # _CAP = { # 'strokeStyleButtCap': 0, # 'strokeStyleSquareCap': 1, # 'strokeStyleRoundCap': 2, # } # _JOIN = { # 'strokeStyleMiterJoin': 0, # 'strokeStyleRoundJoin': 2, # 'strokeStyleBevelJoin': 3, # } width = float(desc.get("strokeStyleLineWidth", 1.0)) # linejoin = desc.get('strokeStyleLineJoinType', None) # linejoin = linejoin.enum if linejoin else 'strokeStyleMiterJoin' # linecap = desc.get('strokeStyleLineCapType', None) # linecap = linecap.enum if linecap else 'strokeStyleButtCap' # miterlimit = desc.get('strokeStyleMiterLimit', 100.0) / 100. # aggdraw >= 1.3.12 will support additional params. return _draw_path( layer, pen={ "color": 255, "width": width, # 'linejoin': _JOIN.get(linejoin, 0), # 'linecap': _CAP.get(linecap, 0), # 'miterlimit': miterlimit, }, )
def _draw_path(layer, brush=None, pen=None): height, width = layer._psd.height, layer._psd.width color = 0 if layer.vector_mask.initial_fill_rule and len(layer.vector_mask.paths) == 0: color = 1 mask = np.full((height, width, 1), color, dtype=np.float32) # Group merged path components. paths = [] for subpath in layer.vector_mask.paths: if subpath.operation == -1: paths[-1].append(subpath) else: paths.append([subpath]) # Apply shape operation. first = True for subpath_list in paths: plane = _draw_subpath(subpath_list, width, height, brush, pen) assert mask.shape == (height, width, 1) assert plane.shape == mask.shape op = subpath_list[0].operation if op == 0: # Exclude = Union - Intersect. mask = mask + plane - 2 * mask * plane elif op == 1: # Union (Combine). mask = mask + plane - mask * plane elif op == 2: # Subtract. if first and brush: mask = 1 - mask mask = np.maximum(0, mask - plane) elif op == 3: # Intersect. if first and brush: mask = 1 - mask mask = mask * plane first = False return np.minimum(1, np.maximum(0, mask)) def _draw_subpath(subpath_list, width, height, brush, pen): """ Rasterize Bezier curves using aggdraw. TODO: Replace aggdraw implementation with skimage.draw. Note: Callers must be decorated with @needs_aggdraw before calling. """ import aggdraw # type: ignore[import-not-found] mask = Image.new("L", (width, height), 0) draw = aggdraw.Draw(mask) pen = aggdraw.Pen(**pen) if pen else None brush = aggdraw.Brush(**brush) if brush else None for subpath in subpath_list: if len(subpath) <= 1: logger.warning("not enough knots: %d" % len(subpath)) continue path = " ".join(map(str, _generate_symbol(subpath, width, height))) symbol = aggdraw.Symbol(path) draw.symbol((0, 0), symbol, pen, brush) draw.flush() del draw return np.expand_dims(np.array(mask).astype(np.float32) / 255.0, 2) def _generate_symbol(path, width, height, command="C"): """Sequence generator for SVG path.""" if len(path) == 0: return # Initial point. yield "M" yield path[0].anchor[1] * width yield path[0].anchor[0] * height yield command # Closed path or open path points = ( zip(path, path[1:] + path[0:1]) if path.is_closed() else zip(path, path[1:]) ) # Rest of the points. for p1, p2 in points: yield p1.leaving[1] * width yield p1.leaving[0] * height yield p2.preceding[1] * width yield p2.preceding[0] * height yield p2.anchor[1] * width yield p2.anchor[0] * height if path.is_closed(): yield "Z"