Esempio n. 1
0
def draw(layer, instance, pen):
    pen = PointToSegmentPen(pen)

    for path in layer.paths:
        nodes = list(path.nodes)

        pen.beginPath()
        if nodes:
            if not path.closed:
                node = nodes.pop(0)
                assert node.type == "line", "Open path starts with off-curve points"
                pen.addPoint(tuple(node.position), segmentType="move")
            else:
                # In Glyphs.app, the starting node of a closed contour is always
                # stored at the end of the nodes list.
                nodes.insert(0, nodes.pop())
            for node in nodes:
                node_type = node.type
                if node_type not in ["line", "curve", "qcurve"]:
                    node_type = None
                pen.addPoint(tuple(node.position), segmentType=node_type, smooth=node.smooth)
        pen.endPath();

    for component in layer.components:
        componentLayer = getLayer(component.component, instance)
        transform = component.transform.value
        componentPen = pen.pen
        if transform != DEFAULT_TRANSFORM:
            componentPen = TransformPen(pen.pen, transform)
            xx, xy, yx, yy = transform[:4]
            if xx * yy - xy * yx < 0:
                componentPen = ReverseContourPen(componentPen)
        draw(componentLayer, instance, componentPen)

    return pen.pen
Esempio n. 2
0
 def test_open(self):
     pen = _TestSegmentPen()
     ppen = PointToSegmentPen(pen)
     ppen.beginPath()
     ppen.addPoint((10, 10), "move")
     ppen.addPoint((10, 20), "line")
     ppen.endPath()
     self.assertEqual("10 10 moveto 10 20 lineto endpath", repr(pen))
Esempio n. 3
0
 def test_quad_onlyOffCurvePoints(self):
     pen = _TestSegmentPen()
     ppen = PointToSegmentPen(pen)
     ppen.beginPath()
     ppen.addPoint((10, 10))
     ppen.addPoint((10, 40))
     ppen.addPoint((40, 40))
     ppen.endPath()
     self.assertEqual("10 10 10 40 40 40 None qcurveto closepath", repr(pen))
Esempio n. 4
0
 def test_quad(self):
     pen = _TestSegmentPen()
     ppen = PointToSegmentPen(pen)
     ppen.beginPath(identifier='foo')
     ppen.addPoint((10, 10), "line")
     ppen.addPoint((10, 40))
     ppen.addPoint((40, 40))
     ppen.addPoint((10, 40), "qcurve")
     ppen.endPath()
     self.assertEqual("10 10 moveto 10 40 40 40 10 40 qcurveto closepath", repr(pen))
Esempio n. 5
0
 def test_cubic(self):
     pen = _TestSegmentPen()
     ppen = PointToSegmentPen(pen)
     ppen.beginPath()
     ppen.addPoint((10, 10), "line")
     ppen.addPoint((10, 20))
     ppen.addPoint((20, 20))
     ppen.addPoint((20, 40), "curve")
     ppen.endPath()
     self.assertEqual("10 10 moveto 10 20 20 20 20 40 curveto closepath", repr(pen))
Esempio n. 6
0
 def test_roundTrip1(self):
     tpen = _TestPointPen()
     ppen = PointToSegmentPen(SegmentToPointPen(tpen))
     ppen.beginPath()
     ppen.addPoint((10, 10), "line")
     ppen.addPoint((10, 20))
     ppen.addPoint((20, 20))
     ppen.addPoint((20, 40), "curve")
     ppen.endPath()
     self.assertEqual("beginPath() addPoint((10, 10), segmentType='line') addPoint((10, 20)) "
                      "addPoint((20, 20)) addPoint((20, 40), segmentType='curve') endPath()",
                      repr(tpen))
Esempio n. 7
0
 def test_closed_outputImpliedClosingLine(self):
     tpen = _TestSegmentPen()
     ppen = PointToSegmentPen(tpen, outputImpliedClosingLine=True)
     ppen.beginPath()
     ppen.addPoint((10, 10), "line")
     ppen.addPoint((10, 20), "line")
     ppen.addPoint((20, 20), "line")
     ppen.endPath()
     self.assertEqual(
         "10 10 moveto "
         "10 20 lineto "
         "20 20 lineto "
         "10 10 lineto "  # explicit closing line
         "closepath",
         repr(tpen))
Esempio n. 8
0
 def test_roundTrip2(self):
     tpen = _TestPointPen()
     ppen = PointToSegmentPen(SegmentToPointPen(tpen))
     ppen.beginPath()
     ppen.addPoint((0, 651), segmentType="line")
     ppen.addPoint((0, 101), segmentType="line")
     ppen.addPoint((0, 101), segmentType="line")
     ppen.addPoint((0, 651), segmentType="line")
     ppen.endPath()
     self.assertEqual(
         "beginPath() "
         "addPoint((0, 651), segmentType='line') "
         "addPoint((0, 101), segmentType='line') "
         "addPoint((0, 101), segmentType='line') "
         "addPoint((0, 651), segmentType='line') "
         "endPath()", repr(tpen))
Esempio n. 9
0
 def draw(self, pen):
     ppen = PointToSegmentPen(pen)
     startIndex = 0
     points = self.getPoints()
     for endIndex in self.contours:
         lastTag = self.tags[endIndex]
         endIndex += 1
         contourTags = self.tags[startIndex:endIndex]
         contourPoints = points[startIndex:endIndex]
         ppen.beginPath()
         for tag, (x, y) in zip(contourTags, contourPoints):
             if tag == FT_CURVE_TAG_ON:
                 segmentType = segmentTypes[lastTag]
             else:
                 segmentType = None
             ppen.addPoint((x, y), segmentType=segmentType)
             lastTag = tag
         ppen.endPath()
         startIndex = endIndex
Esempio n. 10
0
 def test_closed_line_overlapping_start_end_points(self):
     # Test case from https://github.com/googlefonts/fontmake/issues/572.
     tpen = _TestSegmentPen()
     ppen = PointToSegmentPen(tpen, outputImpliedClosingLine=False)
     # The last oncurve point on this closed contour is a "line" segment and has
     # same coordinates as the starting point.
     ppen.beginPath()
     ppen.addPoint((0, 651), segmentType="line")
     ppen.addPoint((0, 101), segmentType="line")
     ppen.addPoint((0, 101), segmentType="line")
     ppen.addPoint((0, 651), segmentType="line")
     ppen.endPath()
     # Check that we always output an explicit 'lineTo' segment at the end,
     # regardless of the value of 'outputImpliedClosingLine', to disambiguate
     # the duplicate point from the implied closing line.
     self.assertEqual(
         "0 651 moveto "
         "0 101 lineto "
         "0 101 lineto "
         "0 651 lineto "
         "0 651 lineto "
         "closepath", repr(tpen))
Esempio n. 11
0
class BezierPath(BasePen):
    def __init__(self, path=None, glyphSet=None):
        super().__init__(glyphSet)
        if path is None:
            path = skia.Path()
        self.path = path

    def _moveTo(self, pt):
        self.path.moveTo(*pt)

    def _lineTo(self, pt):
        self.path.lineTo(*pt)

    def _curveToOne(self, pt1, pt2, pt3):
        x1, y1 = pt1
        x2, y2 = pt2
        x3, y3 = pt3
        self.path.cubicTo(x1, y1, x2, y2, x3, y3)

    def _qCurveToOne(self, pt1, pt2):
        x1, y1 = pt1
        x2, y2 = pt2
        self.path.quadTo(x1, y1, x2, y2)

    def _closePath(self):
        self.path.close()

    def beginPath(self, identifier=None):
        self._pointToSegmentPen = PointToSegmentPen(self)
        self._pointToSegmentPen.beginPath()

    def addPoint(self,
                 point,
                 segmentType=None,
                 smooth=False,
                 name=None,
                 identifier=None,
                 **kwargs):
        if not hasattr(self, "_pointToSegmentPen"):
            raise AttributeError(
                "path.beginPath() must be called before the path can be used as a point pen"
            )
        self._pointToSegmentPen.addPoint(point,
                                         segmentType=segmentType,
                                         smooth=smooth,
                                         name=name,
                                         identifier=identifier,
                                         **kwargs)

    def endPath(self):
        if hasattr(self, "_pointToSegmentPen"):
            # We are drawing as a point pen
            pointToSegmentPen = self._pointToSegmentPen
            del self._pointToSegmentPen
            pointToSegmentPen.endPath()

    def arc(self, center, radius, startAngle, endAngle, clockwise):
        cx, cy = center
        diameter = radius * 2
        rect = (cx - radius, cy - radius, diameter, diameter)
        sweepAngle = (endAngle - startAngle) % 360
        if clockwise:
            sweepAngle -= 360
        self.path.arcTo(rect, startAngle, sweepAngle, False)

    def arcTo(self, point1, point2, radius):
        self.path.arcTo(point1, point2, radius)

    def rect(self, x, y, w, h):
        self.path.addRect((x, y, w, h))

    def oval(self, x, y, w, h):
        self.path.addOval((x, y, w, h))

    def line(self, pt1, pt2):
        points = [(x, y) for x, y in [pt1, pt2]]
        self.path.addPoly(points, False)

    def polygon(self, firstPoint, *points, close=True):
        points = [(x, y) for x, y in (firstPoint, ) + points]
        self.path.addPoly(points, close)

    def pointInside(self, point):
        x, y = point
        return self.path.contains(x, y)

    def bounds(self):
        if self.path.countVerbs() == 0:
            return None
        return tuple(self.path.computeTightBounds())

    def controlPointBounds(self):
        if self.path.countVerbs() == 0:
            return None
        return tuple(self.path.getBounds())

    def reverse(self):
        path = skia.Path()
        path.reverseAddPath(self.path)
        self.path = path

    def appendPath(self, other):
        self.path.addPath(other.path)

    def copy(self):
        path = skia.Path(self.path)
        return BezierPath(path=path)

    def translate(self, x, y):
        self.path.offset(x, y)

    def scale(self, x, y=None, center=(0, 0)):
        if y is None:
            y = x
        self.transform((x, 0, 0, y, 0, 0), center=center)

    def rotate(self, angle, center=(0, 0)):
        t = Transform()
        t = t.rotate(math.radians(angle))
        self.transform(t, center=center)

    def skew(self, x, y=0, center=(0, 0)):
        t = Transform()
        t = t.skew(math.radians(x), math.radians(y))
        self.transform(t, center=center)

    def transform(self, transform, center=(0, 0)):
        cx, cy = center
        t = Transform()
        t = t.translate(cx, cy)
        t = t.transform(transform)
        t = t.translate(-cx, -cy)
        matrix = skia.Matrix()
        matrix.setAffine(t)
        self.path.transform(matrix)

    def drawToPen(self, pen):
        it = skia.Path.Iter(self.path, False)
        needEndPath = False
        for verb, points in it:
            penVerb, startIndex, numPoints = _pathVerbsToPenMethod.get(
                verb, (None, None, None))
            if penVerb is None:
                continue
            assert len(points) == numPoints, (verb, numPoints, len(points))
            if penVerb == "conicTo":
                # We should only call _convertConicToCubicDirty()
                # if it.conicWeight() == sqrt(2)/2, but skia-python doesn't
                # give the correct value.
                # https://github.com/kyamagu/skia-python/issues/116
                # if abs(it.conicWeight() - 0.707...) > 1e-10:
                #     logging.warning("unsupported conic form (weight != sqrt(2)/2): conic to cubic conversion will be bad")
                # TODO: we should fall back to skia.Path.ConvertConicToQuads(),
                # but that call is currently also not working.
                pen.curveTo(*_convertConicToCubicDirty(*points))
            elif penVerb == "closePath":
                needEndPath = False
                pen.closePath()
            else:
                if penVerb == "moveTo":
                    if needEndPath:
                        pen.endPath()
                    needEndPath = True
                pointArgs = ((x, y) for x, y in points[startIndex:])
                getattr(pen, penVerb)(*pointArgs)
        if needEndPath:
            pen.endPath()

    def drawToPointPen(self, pen):
        self.drawToPen(SegmentToPointPen(pen))

    def text(self, txt, offset=None, font=None, fontSize=10, align=None):
        if not txt:
            return
        textStyle = TextStyle(font=font, fontSize=fontSize)
        glyphsInfo = textStyle.shape(txt)
        textStyle.alignGlyphPositions(glyphsInfo, align)
        gids = sorted(set(glyphsInfo.gids))
        paths = [textStyle.skFont.getPath(gid) for gid in gids]
        for path in paths:
            path.transform(FLIP_MATRIX)
        paths = dict(zip(gids, paths))
        x, y = (0, 0) if offset is None else offset
        for gid, pos in zip(glyphsInfo.gids, glyphsInfo.positions):
            path = paths[gid]
            self.path.addPath(path, pos[0] + x, pos[1] + y)

    def _doPathOp(self, other, operator):
        from pathops import Path, op
        path1 = Path()
        path2 = Path()
        self.drawToPen(path1.getPen())
        other.drawToPen(path2.getPen())
        result = op(
            path1,
            path2,
            operator,
            fix_winding=True,
            keep_starting_points=True,
        )
        resultPath = BezierPath()
        result.draw(resultPath)
        return resultPath

    def union(self, other):
        from pathops import PathOp
        return self._doPathOp(other, PathOp.UNION)

    def intersection(self, other):
        from pathops import PathOp
        return self._doPathOp(other, PathOp.INTERSECTION)

    def difference(self, other):
        from pathops import PathOp
        return self._doPathOp(other, PathOp.DIFFERENCE)

    def xor(self, other):
        from pathops import PathOp
        return self._doPathOp(other, PathOp.XOR)

    def removeOverlap(self):
        from pathops import Path
        path = Path()
        self.drawToPen(path.getPen())
        path.simplify(
            fix_winding=True,
            keep_starting_points=False,
        )
        resultPath = BezierPath()
        path.draw(resultPath)
        self.path = resultPath.path

    __mod__ = difference

    def __imod__(self, other):
        result = self.difference(other)
        self.path = result.path
        return self

    __or__ = union

    def __ior__(self, other):
        result = self.union(other)
        self.path = result.path
        return self

    __and__ = intersection

    def __iand__(self, other):
        result = self.intersection(other)
        self.path = result.path
        return self

    __xor__ = xor

    def __ixor__(self, other):
        result = self.xor(other)
        self.path = result.path
        return self
Esempio n. 12
0
class BaseBezierPath(BasePen):
    """Base class with same interface as DrawBot Bézier path."""

    contourClass = BaseBezierContour

    def __init__(self, path=None, glyphSet=None):
        """

        >>> path = BaseBezierPath()
        >>> path
        <BaseBezierPath>
        """
        self._contours = []
        #super().__init__(glyphSet)
        BasePen.__init__(self, glyphSet)

    def __repr__(self):
        return "<BaseBezierPath>"

    def _points(self, onCurve=True, offCurve=True):
        """Internal points representation, corresponding to the DrawBot Bézier
        path."""
        points = []

        if not onCurve and not offCurve:
            return points

        for contour in self._contours:
            for segment in contour:
                pts = segment.points

                if not onCurve:
                    pts = pts[:-1]
                elif not offCurve:
                    pts = pts[-1:]
                points.extend([(p.x, p.y) for p in pts])

        return tuple(points)

    def _get_points(self):
        return self._points()

    points = property(_get_points, doc="Return a list of all points.")

    def _get_onCurvePoints(self):
        return self._points(offCurve=False)

    onCurvePoints = property(_get_onCurvePoints, doc="Return a list of all on curve points.")

    def _get_offCurvePoints(self):
        return self._points(onCurve=False)

    offCurvePoints = property(_get_offCurvePoints, doc="Return a list of all off curve points.")

    def _get_contours(self):
        """Internal contour representation, corresponding to the DrawBot Bézier path."""
        contours = []
        for contour in self._contours:
            for segment in contour:
                if segment.instruction == MOVETO:
                    contours.append(BaseBezierContour())
                if segment.instruction == CLOSEPATH:
                    contours[-1].open = False
                if segment.points:
                    contours[-1].append([(p.x, p.y) for p in segment.points])

        if len(contours) >= 2 and len(contours[-1]) == 1 and contours[-1][0] == contours[-2][0]:
            contours.pop()

        return tuple(contours)

    contours = property(_get_contours, doc="Return a list of contours with all point coordinates sorted in segments. A contour object has an `open` attribute.")

    def __len__(self):
        return len(self.contours)

    def __getitem__(self, index):
        return self.contours[index]

    def __iter__(self):
        contours = self.contours
        count = len(contours)
        index = 0
        while index < count:
            contour = contours[index]
            yield contour
            index += 1

    def addSegment(self, instruction, points):
        """Adds a new segment to the current contour."""
        segment = BaseBezierSegment(instruction, points)
        contour = self.getContour()
        contour.append(segment)

    def getContour(self):
        """Gets the current contour if it exists, else make one."""
        if len(self._contours) == 0:
            contour = self.contourClass()
            self._contours.append(contour)
        else:
            contour = self._contours[-1]

        return contour

    def getPoint(self, p, onCurve=True):
        x, y = p
        point = BaseBezierPoint(x, y, onCurve=onCurve)
        return point

    # FontTools PointToSegmentPen routines.

    def beginPath(self, identifier=None):
        """Begin using the path as a point pen and start a new subpath."""
        self._pointToSegmentPen = PointToSegmentPen(self)
        self._pointToSegmentPen.beginPath()

    def addPoint(self, point, segmentType=None, smooth=False, name=None,
            identifier=None, **kwargs):
        """Use the path as a point pen and add a point to the current subpath.
        `beginPath` must have been called prior to adding points with
        `addPoint` calls."""
        if not hasattr(self, "_pointToSegmentPen"):
            msg = "path.beginPath() must be called before the path can be used as a point pen."
            raise PageBotError(msg)
        self._pointToSegmentPen.addPoint(
            point,
            segmentType=segmentType,
            smooth=smooth,
            name=name,
            identifier=identifier,
            **kwargs
        )

    def endPath(self):
        """Ends the current subpath. Calling this method has two distinct
        meanings depending on the context:

        When the Bézier path is used as a segment pen (using `moveTo`,
        `lineTo`, etc.), the current subpath will be finished as an open
        contour.

        When the Bézier path is used as a point pen (using `beginPath`,
        `addPoint` and `endPath`), the path will process all the points added
        with `addPoint`, finishing the current subpath."""
        if hasattr(self, "_pointToSegmentPen"):
            pointToSegmentPen = self._pointToSegmentPen
            del self._pointToSegmentPen
            pointToSegmentPen.endPath()
        else:
            msg = "path.beginPath() must be called before the path can be used as a point pen."
            raise PageBotError(msg)

    def draw(self, pen):
        """Draws the contours with **pen**."""
        pointPen = PointToSegmentPen(pen)
        self.drawToPointPen(pointPen)

    def bounds(self):
        """Returns the bounding box of the path."""
        pen = BoundsPen(self)
        self.draw(pen)
        return pen.bounds

    def drawToPointPen(self, pointPen):
        """Draws the Bézier path into a point pen."""
        contours = self.contours

        for contour in contours:
            contour.drawToPointPen(pointPen)

    def drawToPen(self, pen):
        """Draws the Bézier path into a pen."""
        contours = self.contours

        for contour in contours:
            contour.drawToPen(pen)

    # Drawing.

    def addComponent(self, glyphName, transformation):
        """
        Adds a sub glyph. The 'transformation' argument must be a 6-tuple
        containing an affine transformation, or a Transform object from the
        fontTools.misc.transform module. More precisely: it should be a
        sequence containing 6 numbers.

        A `glyphSet` is required during initialization of the BezierPath
        object.
        """
        raise NotImplementedError

    # Shapes.

    def rect(self, x, y, w, h):
        """Adds a rectangle at position `x`, `y` with a size of `w`, `h`."""
        x1 = x + w
        y1 = y + h
        p0 = (x, y)
        p1 = (x1, y)
        p2 = (x1, y1)
        p3 = (x, y1)
        self.moveTo(p0)
        self.lineTo(p1)
        self.lineTo(p2)
        self.lineTo(p3)
        self.closePath()

    def oval(self, x, y, w, h):
        """Adds an oval at position `x`, `y` with a size of `w`, `h`"""
        # Control point offsets.
        kappa = .5522848
        offsetX = (w / 2) * kappa
        offsetY = (h / 2) * kappa

        # Middle and other extreme points.
        x0 = x + (w / 2)
        y0 = y + (h / 2)
        x1 = x + w
        y1 = y + h

        self.moveTo((x0, y0))

        cp1 = (x, y0 - offsetY)
        cp2 = (x0 - offsetX, y)
        p = (x1, y0)
        self.curveTo(cp1, cp2, p)

        cp1 = (x0 + offsetX, y)
        cp2 = (x1, y0 - offsetY)
        p = (x1, y0)
        self.curveTo(cp1, cp2, p)

        cp1 = (x1, y0 + offsetY)
        cp2 = (x0 + offsetX, y1)
        p = (x0, y1)
        self.curveTo(cp1, cp2, p)

        cp1 = (x0 - offsetX, y1)
        cp2 = (x, y0 + offsetY)
        p = (x, y0)
        self.curveTo(cp1, cp2, p)

    def line(self, point1, point2):
        """Adds a line between two given points."""
        self.moveTo(point1)
        self.lineTo(point2)

    def polygon(self, *points, **kwargs):
        """Draws a polygon with `n` points. Optionally a `close` argument can
        be provided to open or close the path. By default a `polygon` is a
        closed path."""
        self.moveTo(points[0])

        for point in points[1:]:
            self.lineTo(point)

        # TODO: optionally close.

    def arc(self, center, radius, startAngle, endAngle, clockwise):
        """Arc with `center` and a given `radius`, from `startAngle` to
        `endAngle`, going clockwise if `clockwise` is True and counter
        clockwise if `clockwise` is False."""
        raise NotImplementedError

    def arcTo(self, point1, point2, radius):
        """Arc defined by a circle inscribed inside the angle specified by
        three points: the current point, `point1`, and `point2`. The arc is
        drawn between the two points of the circle that are tangent to the two
        legs of the angle."""
        raise NotImplementedError

    # Text.

    def text(self, txt, offset=None, font=_FALLBACKFONT, fontSize=10,
            align=None):
        """Draws a `txt` with a `font` and `fontSize` at an `offset` in the
        Bézier path. If a font path is given the font will be installed and
        used directly.

        * Optionally an alignment can be set. Possible `align` values are:
          `"left"`, `"center"` and `"right"`.
        * The default alignment is `left`.
        * Optionally `txt` can be a `FormattedString`.

        """
        raise NotImplementedError

    def textBox(self, txt, box, font=None, fontSize=10, align=None,
            hyphenation=None):
        """
        Draws a `txt` with a `font` and `fontSize` in a `box` in the Bézier path.
        If a font path is given the font will be installed and used directly.

        Optionally an alignment can be set.

        * Possible `align` values are: `"left"`, `"center"` and `"right"`.
        * The default alignment is `left`.
        * Optionally `hyphenation` can be provided.
        * Optionally `txt` can be a `FormattedString`.
        * Optionally `box` can be a `BezierPath`.

        """
        raise NotImplementedError

    # Path operations.
    # These are specific for a DrawBot path, dropping from interface.

    #def getNSBezierPath(self):
    def getBezierPath(self):
        """Returns the equivalent of an NSBezierPath."""

    #def setNSBezierPath(self, path):
    def setBezierPath(self, path):
        """Sets the equivalent of an NSBezierPath."""

    def traceImage(self, path, threshold=.2, blur=None, invert=False, turd=2,
            tolerance=0.2, offset=None):
        """Converts a given image to a vector outline. Optionally some tracing
        options can be provided:

        * `threshold`: the threshold used to bitmap an image
        * `blur`: the image can be blurred
        * `invert`: invert to the image
        * `turd`: the size of small turd that can be ignored
        * `tolerance`: the precision tolerance of the vector outline
        * `offset`: add the traced vector outline with an offset to the BezierPath
        """
        # TODO: use potrace, see drawBot.context.tools.TraceImage.

    def pointInside(self, xy):
        """Checks if a point `x`, `y` is inside a path."""
        raise NotImplementedError

    def controlPointBounds(self):
        """Returns the bounding box of the path including the offcurve
        points."""
        raise NotImplementedError

    def optimizePath(self):
        raise NotImplementedError

    def copy(self):
        """Copy the Bézier path."""
        raise NotImplementedError

    def reverse(self):
        """Reverse the path direction."""
        raise NotImplementedError

    def appendPath(self, otherPath):
        """Append a path."""
        raise NotImplementedError

    # transformations
    # NOTE: currently handled within context.

    def translate(self, x=0, y=0):
        """Translates the path with a given offset."""
        self.transform((1, 0, 0, 1, x, y))

    def rotate(self, angle, center=(0, 0)):
        """Rotates the path around the `center` point (which is the origin by
        default) with a given angle in degrees."""
        angle = math.radians(angle)
        c = math.cos(angle)
        s = math.sin(angle)
        self.transform((c, s, -s, c, 0, 0), center)

    def scale(self, x=1, y=None, center=(0, 0)):
        """Scales the path with a given `x` (horizontal scale) and `y`
        (vertical scale).

        If only 1 argument is provided a proportional scale is applied.

        The center of scaling can optionally be set via the `center` keyword
        argument. By default this is the origin."""
        if y is None:
            y = x
        self.transform((x, 0, 0, y, 0, 0), center)

    def skew(self, angle1, angle2=0, center=(0, 0)):
        """Skews the path with given `angle1` and `angle2`. If only one argument
        is provided a proportional skew is applied. The center of skewing can
        optionally be set via the `center` keyword argument. By default this is
        the origin."""
        angle1 = math.radians(angle1)
        angle2 = math.radians(angle2)
        self.transform((1, math.tan(angle2), math.tan(angle1), 1, 0, 0), center)

    def transform(self, transformMatrix, center=(0, 0)):
        """Transforms a path with a transform matrix (xy, xx, yy, yx, x, y)."""

    # Boolean operations.

    def _contoursForBooleanOperations(self):
        # contours are temporary objects
        # redirect drawToPointPen to drawPoints
        contours = self.contours
        for contour in contours:
            contour.drawPoints = contour.drawToPointPen
            if contour.open:
                raise PageBotError("open contours are not supported during boolean operations")
        return contours


    def union(self, other):
        """Returns the union between two Bézier paths."""
        assert isinstance(other, self.__class__)
        contours = self._contoursForBooleanOperations() + other._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.union(contours, result)
        return result

    def removeOverlap(self):
        """Remove all overlaps in a Bézier path."""
        contours = self._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.union(contours, result)
        # TODO:
        #self.setNSBezierPath(result.getNSBezierPath())
        #self.setBezierPath(result.getBezierPath())
        return self

    def difference(self, other):
        """Returns the difference between two Bézier paths.
        """
        subjectContours = self._contoursForBooleanOperations()
        clipContours = other._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.difference(subjectContours, clipContours, result)
        return result

    def intersection(self, other):
        """Returns the intersection between two Bézier paths."""
        assert isinstance(other, self.__class__)
        subjectContours = self._contoursForBooleanOperations()
        clipContours = other._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.intersection(subjectContours, clipContours, result)
        return result

    def xor(self, other):
        """Returns the xor between two Bézier paths."""
        assert isinstance(other, self.__class__)
        subjectContours = self._contoursForBooleanOperations()
        clipContours = other._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.xor(subjectContours, clipContours, result)
        return result

    def intersectionPoints(self, other=None):
        """
        Returns a list of intersection points as `x`, `y` tuples. Optionaly
        provides another path object to find intersection points.
        """
        contours = self._contoursForBooleanOperations()
        if other is not None:
            assert isinstance(other, self.__class__)
            contours += other._contoursForBooleanOperations()
        return booleanOperations.getIntersections(contours)

    def expandStroke(self, width, lineCap="round", lineJoin="round",
            miterLimit=10):
        """Returns a new Bézier path with an expanded stroke around the original path,
        with a given `width`. Note: the new path will not contain the original path.

        The following optional arguments are available with respect to line caps and joins:
        * `lineCap`: Possible values are `"butt"`, `"square"` or `"round"`
        * `lineJoin`: Possible values are `"bevel"`, `"miter"` or `"round"`
        * `miterLimit`: The miter limit to use for `"miter"` lineJoin option
        """
        # TODO: find cross-platform alternative to Quartz.CGPathCreateCopyByStrokingPath.

    #

    def __add__(self, otherPath):
        #new = self.copy()
        #new.appendPath(otherPath)
        #return new
        pass

    def __iadd__(self, other):
        self.appendPath(other)
        return self

    def __mod__(self, other):
        return self.difference(other)

    __rmod__ = __mod__

    def __imod__(self, other):
        #result = self.difference(other)
        #self.setBezierPath(result.getBezierPath())
        #return self
        pass

    def __or__(self, other):
        return self.union(other)

    __ror__ = __or__

    def __ior__(self, other):
        #result = self.union(other)
        #self.setBezierPath(result.getBezierPath())
        #return self
        pass

    def __and__(self, other):
        return self.intersection(other)

    __rand__ = __and__

    def __iand__(self, other):
        #result = self.intersection(other)
        #self.setBezierPath(result.getBezierPath())
        #return self
        pass

    def __xor__(self, other):
        return self.xor(other)

    __rxor__ = __xor__

    def __ixor__(self, other):
        #result = self.xor(other)
        #self.setBezierPath(result.getBezierPath())
        #return self
        pass
Esempio n. 13
0
class BezierPath(BasePen):
    """A Bézier path object, if you want to draw the same over and over
    again."""

    contourClass = BezierContour

    _textAlignMap = dict(
        center=AppKit.NSCenterTextAlignment,
        left=AppKit.NSLeftTextAlignment,
        right=AppKit.NSRightTextAlignment,
        justified=AppKit.NSJustifiedTextAlignment,
    )

    _instructionSegmentTypeMap = {
        AppKit.NSMoveToBezierPathElement: "move",
        AppKit.NSLineToBezierPathElement: "line",
        AppKit.NSCurveToBezierPathElement: "curve"
    }

    def __init__(self, path=None, glyphSet=None):
        if path is None:
            self._path = AppKit.NSBezierPath.alloc().init()
        else:
            self._path = path
        BasePen.__init__(self, glyphSet)

    def __repr__(self):
        return "<BezierPath>"

    # pen support.

    def _moveTo(self, pt):
        self._path.moveToPoint_(pt)

    def _lineTo(self, pt):
        self._path.lineToPoint_(pt)

    def _curveToOne(self, pt1, pt2, pt3):
        """Curve to a point `x3`, `y3`. With given Bézier handles `x1`, `y1`
        and `x2`, `y2`."""
        self._path.curveToPoint_controlPoint1_controlPoint2_(pt3, pt1, pt2)

    # TODO: QCurve?

    def closePath(self):
        """Close the path."""
        self._path.closePath()

    def beginPath(self, identifier=None):
        """Begin using the path as a so called point pen and start a new
        subpath."""
        from fontTools.pens.pointPen import PointToSegmentPen
        self._pointToSegmentPen = PointToSegmentPen(self)
        self._pointToSegmentPen.beginPath()

    def addPoint(self,
                 point,
                 segmentType=None,
                 smooth=False,
                 name=None,
                 identifier=None,
                 **kwargs):
        """Use the path as a point pen and add a point to the current subpath.
        `beginPath` must have been called prior to adding points with
        `addPoint` calls."""
        if not hasattr(self, "_pointToSegmentPen"):
            raise PageBotError(
                "path.beginPath() must be called before the path can be used as a point pen"
            )
        self._pointToSegmentPen.addPoint(point,
                                         segmentType=segmentType,
                                         smooth=smooth,
                                         name=name,
                                         identifier=identifier,
                                         **kwargs)

    def endPath(self):
        """End the current subpath. Calling this method has two distinct
        meanings depending on the context:

        When the Bézier path is used as a segment pen (using `moveTo`,
        `lineTo`, etc.), the current subpath will be finished as an open
        contour.

        When the Bézier path is used as a point pen (using `beginPath`,
        `addPoint` and `endPath`), the path will process all the points added
        with `addPoint`, finishing the current subpath."""
        if hasattr(self, "_pointToSegmentPen"):
            # its been uses in a point pen world
            pointToSegmentPen = self._pointToSegmentPen
            del self._pointToSegmentPen
            pointToSegmentPen.endPath()
        else:
            # with NSBezierPath, nothing special needs to be done for an open subpath.
            pass

    def drawToPen(self, pen):
        """
        Draw the Bézier path into a pen
        """
        contours = self.contours
        for contour in contours:
            contour.drawToPen(pen)

    def drawToPointPen(self, pointPen):
        """Draw the Bézier path into a point pen."""
        contours = self.contours
        for contour in contours:
            contour.drawToPointPen(pointPen)

    def arc(self, center, radius, startAngle, endAngle, clockwise):
        """Arc with `center` and a given `radius`, from `startAngle` to
        `endAngle`, going clockwise if `clockwise` is True and counter
        clockwise if `clockwise` is False."""
        self._path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_clockwise_(
            center, radius, startAngle, endAngle, clockwise)

    def arcTo(self, point1, point2, radius):
        """Arc defined by a circle inscribed inside the angle specified by
        three points: the current point, `point1`, and `point2`. The arc is
        drawn between the two points of the circle that are tangent to the two
        legs of the angle. """
        self._path.appendBezierPathWithArcFromPoint_toPoint_radius_(
            point1, point2, radius)

    def rect(self, x, y, w, h):
        """Add a rectangle at possition `x`, `y` with a size of `w`, `h`."""
        self._path.appendBezierPathWithRect_(((x, y), (w, h)))

    def oval(self, x, y, w, h):
        """Add a oval at possition `x`, `y` with a size of `w`, `h`."""
        self._path.appendBezierPathWithOvalInRect_(((x, y), (w, h)))
        self.closePath()

    def text(self,
             txt,
             offset=None,
             font=_FALLBACKFONT,
             fontSize=10,
             align=None):
        """Draws a `txt` with a `font` and `fontSize` at an `offset` in the
        Bézier path. If a font path is given the font will be installed and
        used directly.

        - Optionally an alignment can be set.
        - Possible `align` values are: `"left"`, `"center"` and `"right"`.
        - The default alignment is `left`.
        - Optionally `txt` can be a `FormattedString`.
        """
        context = BaseContext()

        if align and align not in self._textAlignMap.keys():
            raise PageBotError("align must be %s" %
                               (", ".join(self._textAlignMap.keys())))

        context.font(font, fontSize)
        attributedString = context.attributedString(txt, align)
        w, h = attributedString.size()
        w *= 2

        if offset:
            x, y = offset
        else:
            x = y = 0
        if align == "right":
            x -= w
        elif align == "center":
            x -= w * .5

        setter = CoreText.CTFramesetterCreateWithAttributedString(
            attributedString)
        path = Quartz.CGPathCreateMutable()
        Quartz.CGPathAddRect(path, None, Quartz.CGRectMake(x, y, w, h * 2))
        frame = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None)
        ctLines = CoreText.CTFrameGetLines(frame)
        origins = CoreText.CTFrameGetLineOrigins(frame, (0, len(ctLines)),
                                                 None)

        if origins:
            y -= origins[0][1]

        self.textBox(txt,
                     box=(x, y, w, h * 2),
                     font=font,
                     fontSize=fontSize,
                     align=align)

    def textBox(self,
                txt,
                box,
                font=_FALLBACKFONT,
                fontSize=10,
                align=None,
                hyphenation=None):
        """Draws a `txt` with a `font` and `fontSize` in a `box` in the Bézier
        path. If a font path is given the font will be installed and used
        directly.

        - Optionally an alignment can be set.
        - Possible `align` values are: `"left"`, `"center"` and `"right"`.
        - The default alignment is `left`.
        - Optionally `hyphenation` can be provided.
        - Optionally `txt` can be a `FormattedString`.
        - Optionally `box` can be a `BezierPath`.
        """
        if align and align not in self._textAlignMap.keys():
            raise PageBotError("align must be %s" %
                               (", ".join(self._textAlignMap.keys())))

        context = BaseContext()
        context.font(font, fontSize)
        context.hyphenation(hyphenation)
        path, (x, y) = context._getPathForFrameSetter(box)
        attributedString = context.attributedString(txt, align)

        setter = CoreText.CTFramesetterCreateWithAttributedString(
            attributedString)
        frame = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None)
        ctLines = CoreText.CTFrameGetLines(frame)
        origins = CoreText.CTFrameGetLineOrigins(frame, (0, len(ctLines)),
                                                 None)

        for i, (originX, originY) in enumerate(origins):
            ctLine = ctLines[i]
            ctRuns = CoreText.CTLineGetGlyphRuns(ctLine)
            for ctRun in ctRuns:
                attributes = CoreText.CTRunGetAttributes(ctRun)
                font = attributes.get(AppKit.NSFontAttributeName)
                baselineShift = attributes.get(
                    AppKit.NSBaselineOffsetAttributeName, 0)
                glyphCount = CoreText.CTRunGetGlyphCount(ctRun)
                for i in range(glyphCount):
                    glyph = CoreText.CTRunGetGlyphs(ctRun, (i, 1), None)[0]
                    ax, ay = CoreText.CTRunGetPositions(ctRun, (i, 1), None)[0]
                    if glyph:
                        self._path.moveToPoint_(
                            (x + originX + ax,
                             y + originY + ay + baselineShift))
                        self._path.appendBezierPathWithGlyph_inFont_(
                            glyph, font)
        self.optimizePath()
        return context.clippedText(txt, box, align)

    def traceImage(self,
                   path,
                   threshold=.2,
                   blur=None,
                   invert=False,
                   turd=2,
                   tolerance=0.2,
                   offset=None):
        """Converts a given image to a vector outline. Some tracing options can
        be provide:

        * `threshold`: the threshold used to bitmap an image
        * `blur`: the image can be blurred
        * `invert`: invert to the image
        * `turd`: the size of small turd that can be ignored
        * `tolerance`: the precision tolerance of the vector outline
        * `offset`: add the traced vector outline with an offset to the BezierPath
        """
        from .tools import traceImage
        traceImage.TraceImage(path, self, threshold, blur, invert, turd,
                              tolerance, offset)

    def getNSBezierPath(self):
        """
        Return the nsBezierPath.
        """
        return self._path

    def _getCGPath(self):
        path = Quartz.CGPathCreateMutable()
        count = self._path.elementCount()
        for i in range(count):
            instruction, points = self._path.elementAtIndex_associatedPoints_(
                i)
            if instruction == AppKit.NSMoveToBezierPathElement:
                Quartz.CGPathMoveToPoint(path, None, points[0].x, points[0].y)
            elif instruction == AppKit.NSLineToBezierPathElement:
                Quartz.CGPathAddLineToPoint(path, None, points[0].x,
                                            points[0].y)
            elif instruction == AppKit.NSCurveToBezierPathElement:
                Quartz.CGPathAddCurveToPoint(path, None, points[0].x,
                                             points[0].y, points[1].x,
                                             points[1].y, points[2].x,
                                             points[2].y)
            elif instruction == AppKit.NSClosePathBezierPathElement:
                Quartz.CGPathCloseSubpath(path)

        # hacking to get a proper close path at the end of the path
        x, y, _, _ = self.bounds()
        Quartz.CGPathMoveToPoint(path, None, x, y)
        Quartz.CGPathAddLineToPoint(path, None, x, y)
        Quartz.CGPathAddLineToPoint(path, None, x, y)
        Quartz.CGPathAddLineToPoint(path, None, x, y)
        Quartz.CGPathCloseSubpath(path)
        return path

    def setNSBezierPath(self, path):
        """Sets a nsBezierPath."""
        self._path = path

    def pointInside(self, xy):
        """Checks if a point `x`, `y` is inside a path."""
        x, y = xy
        return self._path.containsPoint_((x, y))

    def bounds(self):
        """Returns the bounding box of the path."""
        if self._path.isEmpty():
            return None
        (x, y), (w, h) = self._path.bounds()
        return x, y, x + w, y + h

    def controlPointBounds(self):
        """Returns the bounding box of the path including the offcurve
        points."""
        (x, y), (w, h) = self._path.controlPointBounds()
        return x, y, x + w, y + h

    def optimizePath(self):
        count = self._path.elementCount()
        if self._path.elementAtIndex_(count -
                                      1) == AppKit.NSMoveToBezierPathElement:
            optimizedPath = AppKit.NSBezierPath.alloc().init()
            for i in range(count - 1):
                instruction, points = self._path.elementAtIndex_associatedPoints_(
                    i)
                if instruction == AppKit.NSMoveToBezierPathElement:
                    optimizedPath.moveToPoint_(*points)
                elif instruction == AppKit.NSLineToBezierPathElement:
                    optimizedPath.lineToPoint_(*points)
                elif instruction == AppKit.NSCurveToBezierPathElement:
                    p1, p2, p3 = points
                    optimizedPath.curveToPoint_controlPoint1_controlPoint2_(
                        p3, p1, p2)
                elif instruction == AppKit.NSClosePathBezierPathElement:
                    optimizedPath.closePath()
            self._path = optimizedPath

    def copy(self):
        """Copies the Bézier path."""
        new = self.__class__()
        new._path = self._path.copy()
        return new

    def reverse(self):
        """Reverses the path direction."""
        self._path = self._path.bezierPathByReversingPath()

    def appendPath(self, otherPath):
        """Appends a path."""
        self._path.appendBezierPath_(otherPath.getNSBezierPath())

    def __add__(self, otherPath):
        new = self.copy()
        new.appendPath(otherPath)
        return new

    def __iadd__(self, other):
        self.appendPath(other)
        return self

    # transformations

    def translate(self, x=0, y=0):
        """Translates the path with a given offset."""
        self.transform((1, 0, 0, 1, x, y))

    def rotate(self, angle, center=(0, 0)):
        """Rotates the path around the `center` point (which is the origin by
        default) with a given angle in degrees."""
        angle = math.radians(angle)
        c = math.cos(angle)
        s = math.sin(angle)
        self.transform((c, s, -s, c, 0, 0), center)

    def scale(self, x=1, y=None, center=(0, 0)):
        """Scales the path with a given `x` (horizontal scale) and `y` (vertical
        scale). If only 1 argument is provided a proportional scale is
        applied. The center of scaling can optionally be set via the `center`
        keyword argument. By default this is the origin."""
        if y is None:
            y = x
        self.transform((x, 0, 0, y, 0, 0), center)

    def skew(self, angle1, angle2=0, center=(0, 0)):
        """Skews the path with given `angle1` and `angle2`. If only one
        argument is provided a proportional skew is applied. The center of
        skewing can optionally be set via the `center` keyword argument. By
        default this is the origin."""
        angle1 = math.radians(angle1)
        angle2 = math.radians(angle2)
        self.transform((1, math.tan(angle2), math.tan(angle1), 1, 0, 0),
                       center)

    def transform(self, transformMatrix, center=(0, 0)):
        """Transforms a path with a transform matrix (xy, xx, yy, yx, x, y)."""
        if center != (0, 0):
            transformMatrix = transformationAtCenter(transformMatrix, center)

        aT = AppKit.NSAffineTransform.alloc().init()
        aT.setTransformStruct_(transformMatrix[:])
        self._path.transformUsingAffineTransform_(aT)

    # boolean operations

    def _contoursForBooleanOperations(self):
        # contours are very temporaly objects
        # redirect drawToPointPen to drawPoints
        contours = self.contours
        for contour in contours:
            contour.drawPoints = contour.drawToPointPen
            if contour.open:
                raise PageBotError(
                    "open contours are not supported during boolean operations"
                )
        return contours

    def union(self, other):
        """Returns the union between two Bézier paths."""
        assert isinstance(other, self.__class__)
        contours = self._contoursForBooleanOperations(
        ) + other._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.union(contours, result)
        return result

    def removeOverlap(self):
        """Removes all overlaps in a Bézier path."""
        contours = self._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.union(contours, result)
        self.setNSBezierPath(result.getNSBezierPath())
        return self

    def difference(self, other):
        """Returns the difference between two Bézier paths."""
        assert isinstance(other, self.__class__)
        subjectContours = self._contoursForBooleanOperations()
        clipContours = other._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.difference(subjectContours, clipContours, result)
        return result

    def intersection(self, other):
        """Returns the intersection between two Bézier paths."""
        assert isinstance(other, self.__class__)
        subjectContours = self._contoursForBooleanOperations()
        clipContours = other._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.intersection(subjectContours, clipContours, result)
        return result

    def xor(self, other):
        """Returns the xor between two Bézier paths."""
        assert isinstance(other, self.__class__)
        subjectContours = self._contoursForBooleanOperations()
        clipContours = other._contoursForBooleanOperations()
        result = self.__class__()
        booleanOperations.xor(subjectContours, clipContours, result)
        return result

    def intersectionPoints(self, other=None):
        """Returns a list of intersection points as `x`, `y` tuples. Optionaly
        provides an other path object to find intersection points."""
        contours = self._contoursForBooleanOperations()
        if other is not None:
            assert isinstance(other, self.__class__)
            contours += other._contoursForBooleanOperations()
        return booleanOperations.getIntersections(contours)

    def __mod__(self, other):
        return self.difference(other)

    __rmod__ = __mod__

    def __imod__(self, other):
        result = self.difference(other)
        self.setNSBezierPath(result.getNSBezierPath())
        return self

    def __or__(self, other):
        return self.union(other)

    __ror__ = __or__

    def __ior__(self, other):
        result = self.union(other)
        self.setNSBezierPath(result.getNSBezierPath())
        return self

    def __and__(self, other):
        return self.intersection(other)

    __rand__ = __and__

    def __iand__(self, other):
        result = self.intersection(other)
        self.setNSBezierPath(result.getNSBezierPath())
        return self

    def __xor__(self, other):
        return self.xor(other)

    __rxor__ = __xor__

    def __ixor__(self, other):
        result = self.xor(other)
        self.setNSBezierPath(result.getNSBezierPath())
        return self

    def _points(self, onCurve=True, offCurve=True):
        points = []
        if not onCurve and not offCurve:
            return points
        for index in range(self._path.elementCount()):
            instruction, pts = self._path.elementAtIndex_associatedPoints_(
                index)
            if not onCurve:
                pts = pts[:-1]
            elif not offCurve:
                pts = pts[-1:]
            points.extend([(p.x, p.y) for p in pts])
        return points

    def _get_points(self):
        return self._points()

    points = property(_get_points, doc="Return a list of all points.")

    def _get_onCurvePoints(self):
        return self._points(offCurve=False)

    onCurvePoints = property(_get_onCurvePoints,
                             doc="Return a list of all on curve points.")

    def _get_offCurvePoints(self):
        return self._points(onCurve=False)

    offCurvePoints = property(_get_offCurvePoints,
                              doc="Return a list of all off curve points.")

    def _get_contours(self):
        contours = []
        for index in range(self._path.elementCount()):
            instruction, pts = self._path.elementAtIndex_associatedPoints_(
                index)
            if instruction == AppKit.NSMoveToBezierPathElement:
                contours.append(self.contourClass())
            if instruction == AppKit.NSClosePathBezierPathElement:
                contours[-1].open = False
            if pts:
                contours[-1].append([(p.x, p.y) for p in pts])
        if len(contours) >= 2 and len(
                contours[-1]) == 1 and contours[-1][0] == contours[-2][0]:
            contours.pop()
        return contours

    contours = property(
        _get_contours,
        doc=
        "Return a list of contours with all point coordinates sorted in segments. A contour object has an `open` attribute."
    )

    def __len__(self):
        return len(self.contours)

    def __getitem__(self, index):
        return self.contours[index]

    def __iter__(self):
        contours = self.contours
        count = len(contours)
        index = 0
        while index < count:
            contour = contours[index]
            yield contour
            index += 1