@@ -41,6 +41,23 @@ from cpython.mem cimport PyMem_Malloc, PyMem_Free, PyMem_Realloc
4141from libc.string cimport memset
4242cimport cython
4343import itertools
44+ import sys
45+
46+
47+ if sys.version_info[:2 ] < (3 , 10 ):
48+ from itertools import tee
49+
50+ def pairwise (iterable ):
51+ """ Return successive overlapping pairs taken from the input iterable.
52+
53+ Backported from Python 3.10.
54+ https://docs.python.org/3/library/itertools.html#itertools.pairwise
55+ """
56+ a, b = tee(iterable)
57+ next(b, None )
58+ return zip (a, b)
59+ else :
60+ from itertools import pairwise
4461
4562
4663cdef class PathOpsError(Exception ):
@@ -138,6 +155,17 @@ def _format_hex_coords(floats):
138155 ) + " \n "
139156
140157
158+ def triplewise (iterable ):
159+ """ Return overlapping triplets from an iterable
160+
161+ E.g. triplewise('ABCDEFG') --> ABC BCD CDE DEF EFG
162+
163+ From: https://docs.python.org/3/library/itertools.html#itertools-recipes
164+ """
165+ for (a, _), (b, c) in pairwise(pairwise(iterable)):
166+ yield a, b, c
167+
168+
141169cdef class Path:
142170
143171 def __init__ (self , other = None , fillType = None ):
@@ -243,9 +271,8 @@ cdef class Path:
243271 cpdef draw(self , pen):
244272 cdef str method
245273 cdef tuple pts
246- cdef SegmentPenIterator iterator = SegmentPenIterator(self )
247274
248- for method, pts in iterator :
275+ for method, pts in self .segments :
249276 getattr (pen, method)(* pts)
250277
251278 def dump (self , cpp = False , as_hex = False ):
@@ -617,7 +644,37 @@ cdef class Path:
617644
618645 @property
619646 def segments (self ):
620- return SegmentPenIterator(self )
647+ # We need to check for TrueType special quadratic closed spline made of
648+ # off-curve points only so that we can make the move point implied.
649+ # It's easier to do this in here than inside the SegmentPenIterator, as that
650+ # yields each segment one by one, whereas we want to sometimes *not* yield a
651+ # moveTo in very specific circumstances (i.e. the whole contour is a single
652+ # closed quadratic spline where all the on-curve points are midway between
653+ # consecutive off-curve points) based on previous and next segments.
654+ cdef SkPoint p1, p2, p3
655+ result = []
656+ it = itertools.chain([None ], SegmentPenIterator(self ), [None ])
657+ for previous, current, next_ in triplewise(it):
658+ if (
659+ previous is not None
660+ and previous[0 ] == " moveTo"
661+ and current[0 ] == " qCurveTo"
662+ and next_ is not None
663+ and next_[0 ] == " closePath"
664+ and previous[1 ][0 ] == current[1 ][- 1 ]
665+ ):
666+ qpoints = current[1 ]
667+ last_off, move_pt, first_off = qpoints[- 2 ], qpoints[- 1 ], qpoints[0 ]
668+ p1 = SkPoint.Make(last_off[0 ], last_off[1 ])
669+ p2 = SkPoint.Make(move_pt[0 ], move_pt[1 ])
670+ p3 = SkPoint.Make(first_off[0 ], first_off[1 ])
671+ if is_middle_point(p1, p2, p3):
672+ # drop the moveTo and make the last on-curve None
673+ del result[- 1 ]
674+ result.append((current[0 ], current[1 ][:- 1 ] + (None ,)))
675+ continue
676+ result.append(current)
677+ yield from result
621678
622679 cpdef Path transform(
623680 self ,
@@ -919,8 +976,24 @@ cdef class PathPen:
919976 def qCurveTo (self , *points ):
920977 num_offcurves = len (points) - 1
921978 if num_offcurves > 0 :
979+ oncurveless_contour = points[- 1 ] is None
980+ if oncurveless_contour:
981+ # Special case for TrueType closed contours without on-curve points.
982+ # FontTools pens supports this by allowing the last point of qCurveTo
983+ # to be None, which is translated as an implied on-curve point between
984+ # the last and the first off-curve points:
985+ # https://github.com/fonttools/fonttools/blob/02a0636/Lib/fontTools/pens/basePen.py#L332-L344
986+ # https://github.com/fonttools/skia-pathops/issues/45
987+ x, y = points[- 2 ]
988+ nx, ny = points[0 ]
989+ implied_pt = (0.5 * (x + nx), 0.5 * (y + ny))
990+ self .moveTo(implied_pt)
991+ points = points[:- 1 ] + (implied_pt,)
922992 for pt1, pt2 in _decompose_quadratic_segment(points):
923993 self ._qCurveToOne(pt1, pt2)
994+ if oncurveless_contour:
995+ # oncurve-less contour is closed by definition
996+ self .closePath()
924997 elif num_offcurves == 0 :
925998 self .lineTo(points[0 ])
926999 else :
0 commit comments