From 81d4bb43fd85cff67a9c464cf3f2052c6ddb23ee Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 11 Nov 2022 15:29:47 +0000 Subject: [PATCH] make PathPen curveTo/qCurveTo accept variable number of points When constructing a pathops.Path using the PathPen, we should handle variable number of point arguments to curveTo and qCurveTo, following fontTools' BasePen which is the blueprint of all segment-based pens. https://github.com/fonttools/fonttools/blob/35856412485bce4a1e6c08cceb52c7a87c75c4ee/Lib/fontTools/pens/basePen.py#L274-L329 Also see https://github.com/unified-font-object/ufo-spec/issues/211 and https://github.com/googlefonts/ufo2ft/issues/468#issuecomment-1311574944 --- src/python/pathops/__init__.py | 1 + src/python/pathops/_pathops.pxd | 2 +- src/python/pathops/_pathops.pyx | 39 +++++++++++++++++++------ tests/pathops_test.py | 51 +++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/src/python/pathops/__init__.py b/src/python/pathops/__init__.py index 5102972..5c1b2c0 100644 --- a/src/python/pathops/__init__.py +++ b/src/python/pathops/__init__.py @@ -14,6 +14,7 @@ PathOpsError, UnsupportedVerbError, OpenPathError, + NumberOfPointsError, bits2float, float2bits, decompose_quadratic_segment, diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index 412e82c..0b01fa9 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -247,7 +247,7 @@ cdef class PathPen: cpdef lineTo(self, pt) - cpdef curveTo(self, pt1, pt2, pt3) + # def curveTo(self, *points) # def qCurveTo(self, *points) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index e429b27..7f1517f 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -54,6 +54,10 @@ cdef class OpenPathError(PathOpsError): pass +cdef class NumberOfPointsError(PathOpsError): + pass + + # Helpers to convert to/from a float and its bit pattern cdef inline int32_t _float2bits(float x): @@ -891,16 +895,35 @@ cdef class PathPen: cpdef lineTo(self, pt): self.path.lineTo(pt[0], pt[1]) - cpdef curveTo(self, pt1, pt2, pt3): - # support BasePen "super-beziers"? Nah. - self.path.cubicTo( - pt1[0], pt1[1], - pt2[0], pt2[1], - pt3[0], pt3[1]) + def curveTo(self, *points): + num_offcurves = len(points) - 1 + if num_offcurves == 2: + pt1, pt2, pt3 = points + self.path.cubicTo( + pt1[0], pt1[1], + pt2[0], pt2[1], + pt3[0], pt3[1]) + elif num_offcurves == 1: + pt1, pt2 = points + self.path.quadTo(pt1[0], pt1[1], pt2[0], pt2[1]) + elif num_offcurves == 0: + pt = points[0] + self.path.lineTo(pt[0], pt[1]) + else: + # support BasePen "super-beziers"? Nah. + raise NumberOfPointsError( + "curveTo requires between 1 and 3 points; got %d" % len(points) + ) def qCurveTo(self, *points): - for pt1, pt2 in _decompose_quadratic_segment(points): - self._qCurveToOne(pt1, pt2) + num_offcurves = len(points) - 1 + if num_offcurves > 0: + for pt1, pt2 in _decompose_quadratic_segment(points): + self._qCurveToOne(pt1, pt2) + elif num_offcurves == 0: + self.lineTo(points[0]) + else: + raise NumberOfPointsError("qCurveTo requires at least 1 point; got 0") cdef _qCurveToOne(self, pt1, pt2): self.path.quadTo(pt1[0], pt1[1], pt2[0], pt2[1]) diff --git a/tests/pathops_test.py b/tests/pathops_test.py index 6a343cc..ab4ecdb 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -11,6 +11,7 @@ ArcSize, Direction, simplify, + NumberOfPointsError, ) import pytest @@ -106,6 +107,56 @@ def test_decompose_join_quadratic_segments(self): ('qCurveTo', ((1.0, 1.0), (2.0, 2.0), (3.0, 3.0))), ('closePath', ())] + def test_qCurveTo_varargs(self): + path = Path() + pen = path.getPen() + pen.moveTo((0, 0)) + pen.qCurveTo((1, 1)) + pen.closePath() + + items = list(path) + assert len(items) == 3 + # qcurve without offcurves is stored internally as a line + assert items[1][0] == PathVerb.LINE + assert items[1][1] == ((1.0, 1.0),) + + assert list(path.segments) == [ + ('moveTo', ((0.0, 0.0),)), + ('lineTo', ((1.0, 1.0),)), + ('closePath', ()), + ] + + with pytest.raises( + NumberOfPointsError, match="qCurveTo requires at least 1 point; got 0" + ): + pen.qCurveTo() + + def test_curveTo_varargs(self): + path = Path() + pen = path.getPen() + pen.moveTo((0, 0)) + pen.curveTo((1, 1), (2, 2), (3, 3)) # a cubic + pen.curveTo((4, 4), (5, 5)) # a quadratic + pen.curveTo((6, 6)) # a line + pen.closePath() + + assert list(path.segments) == [ + ('moveTo', ((0.0, 0.0),)), + ('curveTo', ((1.0, 1.0), (2.0, 2.0), (3.0, 3.0))), + ('qCurveTo', ((4.0, 4.0), (5.0, 5.0))), + ('lineTo', ((6.0, 6.0),)), + ('closePath', ()), + ] + + with pytest.raises( + NumberOfPointsError, match="curveTo requires between 1 and 3 points; got 0" + ): + pen.curveTo() + with pytest.raises( + NumberOfPointsError, match="curveTo requires between 1 and 3 points; got 4" + ): + pen.curveTo((0, 0), (1, 1), (2, 2), (3, 3)) + def test_last_implicit_lineTo(self): # https://github.com/fonttools/skia-pathops/issues/6 path = Path()