From 82e0536beb7fab32bb122405167a6e67a8ef3faf Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 20 Feb 2023 11:05:35 -0700 Subject: [PATCH] [glyf] Support cubic curves https://github.com/harfbuzz/boring-expansion-spec/issues/41 --- Lib/fontTools/pens/ttGlyphPen.py | 42 ++++++++++++++++++++------ Lib/fontTools/ttLib/tables/_g_l_y_f.py | 36 ++++++++++++++++++---- Tests/pens/ttGlyphPen_test.py | 6 ++-- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py index 8f8e7d748..48f595849 100644 --- a/Lib/fontTools/pens/ttGlyphPen.py +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -7,7 +7,7 @@ from fontTools.misc.roundTools import otRound from fontTools.pens.basePen import LoggingPen, PenError from fontTools.pens.transformPen import TransformPen, TransformPointPen from fontTools.ttLib.tables import ttProgram -from fontTools.ttLib.tables._g_l_y_f import flagOnCurve +from fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic from fontTools.ttLib.tables._g_l_y_f import Glyph from fontTools.ttLib.tables._g_l_y_f import GlyphComponent from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates @@ -220,9 +220,9 @@ class TTGlyphPen(_TTGlyphBasePen, LoggingPen): super().__init__(glyphSet, handleOverflowingTransforms) self.outputImpliedClosingLine = outputImpliedClosingLine - def _addPoint(self, pt: Tuple[float, float], onCurve: int) -> None: + def _addPoint(self, pt: Tuple[float, float], tp: int) -> None: self.points.append(pt) - self.types.append(onCurve) + self.types.append(tp) def _popPoint(self) -> None: self.points.pop() @@ -234,15 +234,21 @@ class TTGlyphPen(_TTGlyphBasePen, LoggingPen): ) def lineTo(self, pt: Tuple[float, float]) -> None: - self._addPoint(pt, 1) + self._addPoint(pt, flagOnCurve) def moveTo(self, pt: Tuple[float, float]) -> None: if not self._isClosed(): raise PenError('"move"-type point must begin a new contour.') - self._addPoint(pt, 1) + self._addPoint(pt, flagOnCurve) def curveTo(self, *points) -> None: - raise NotImplementedError + assert len(points) % 2 == 1 + for pt in points[:-1]: + self._addPoint(pt, flagCubic) + + # last point is None if there are no on-curve points + if points[-1] is not None: + self._addPoint(points[-1], 1) def qCurveTo(self, *points) -> None: assert len(points) >= 1 @@ -313,9 +319,23 @@ class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen): raise PenError("Contour is already closed.") if self._currentContourStartIndex == len(self.points): raise PenError("Tried to end an empty contour.") + + contourStart = self.endPts[-1] + 1 if self.endPts else 0 self.endPts.append(len(self.points) - 1) self._currentContourStartIndex = None + # Resolve types for any cubic segments + flags = self.types + for i in range(contourStart, len(flags)): + if flags[i] == "curve": + j = i - 1 + if j < contourStart: + j = len(flags) - 1 + while flags[j] == 0: + flags[j] = flagCubic + j -= 1 + flags[i] = flagOnCurve + def addPoint( self, pt: Tuple[float, float], @@ -331,11 +351,13 @@ class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen): if self._isClosed(): raise PenError("Can't add a point to a closed contour.") if segmentType is None: - self.types.append(0) # offcurve - elif segmentType in ("qcurve", "line", "move"): - self.types.append(1) # oncurve + self.types.append(0) + elif segmentType in ("line", "move"): + self.types.append(flagOnCurve) + elif segmentType == "qcurve": + self.types.append(flagOnCurve) elif segmentType == "curve": - raise NotImplementedError("cubic curves are not supported") + self.types.append("curve") else: raise AssertionError(segmentType) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 18a230c0d..d2d6c1456 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -569,10 +569,10 @@ flagRepeat = 0x08 flagXsame = 0x10 flagYsame = 0x20 flagOverlapSimple = 0x40 -flagReserved = 0x80 +flagCubic = 0x80 # These flags are kept for XML output after decompiling the coordinates -keepFlags = flagOnCurve + flagOverlapSimple +keepFlags = flagOnCurve + flagOverlapSimple + flagCubic _flagSignBytes = { 0: 2, @@ -764,6 +764,8 @@ class Glyph(object): if self.flags[j] & flagOverlapSimple: # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours attrs.append(("overlap", 1)) + if self.flags[j] & flagCubic: + attrs.append(("cubic", 1)) writer.simpletag("pt", attrs) writer.newline() last = self.endPtsOfContours[i] + 1 @@ -797,6 +799,8 @@ class Glyph(object): flag = bool(safeEval(attrs["on"])) if "overlap" in attrs and bool(safeEval(attrs["overlap"])): flag |= flagOverlapSimple + if "cubic" in attrs and bool(safeEval(attrs["cubic"])): + flag |= flagCubic flags.append(flag) if not hasattr(self, "coordinates"): self.coordinates = coordinates @@ -1416,20 +1420,27 @@ class Glyph(object): end = end + 1 contour = coordinates[start:end] cFlags = [flagOnCurve & f for f in flags[start:end]] + cuFlags = [flagCubic & f for f in flags[start:end]] start = end if 1 not in cFlags: # There is not a single on-curve point on the curve, # use pen.qCurveTo's special case by specifying None # as the on-curve point. contour.append(None) - pen.qCurveTo(*contour) + assert all(cuFlags) or not any(cuFlags) + cubic = all(cuFlags) + if cubic: + pen.curveTo(*contour) + else: + pen.qCurveTo(*contour) else: - # Shuffle the points so that contour the is guaranteed + # Shuffle the points so that the contour is guaranteed # to *end* in an on-curve point, which we'll use for # the moveTo. firstOnCurve = cFlags.index(1) + 1 contour = contour[firstOnCurve:] + contour[:firstOnCurve] cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] + cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve] pen.moveTo(contour[-1]) while contour: nextOnCurve = cFlags.index(1) + 1 @@ -1439,9 +1450,22 @@ class Glyph(object): if len(contour) > 1: pen.lineTo(contour[0]) else: - pen.qCurveTo(*contour[:nextOnCurve]) + cubicFlags = [f for f in cuFlags[: nextOnCurve - 1]] + assert all(cubicFlags) or not any(cubicFlags) + cubic = any(cubicFlags) + if cubic: + assert all( + cubicFlags + ), "Mixed segments not currently supported" + assert ( + len(cubicFlags) == 2 + ), "Cubic multi-segments not currently supported" + pen.curveTo(*contour[:nextOnCurve]) + else: + pen.qCurveTo(*contour[:nextOnCurve]) contour = contour[nextOnCurve:] cFlags = cFlags[nextOnCurve:] + cuFlags = cuFlags[nextOnCurve:] pen.closePath() def drawPoints(self, pen, glyfTable, offset=0): @@ -1474,7 +1498,7 @@ class Glyph(object): segmentType = "line" else: pen.addPoint(pt) - segmentType = "qcurve" + segmentType = "curve" if cFlags[i] & flagCubic else "qcurve" pen.endPath() def __eq__(self, other): diff --git a/Tests/pens/ttGlyphPen_test.py b/Tests/pens/ttGlyphPen_test.py index de06931a6..c3c2603da 100644 --- a/Tests/pens/ttGlyphPen_test.py +++ b/Tests/pens/ttGlyphPen_test.py @@ -332,11 +332,11 @@ class TTGlyphPointPenTest(TTGlyphPenTestBase): assert glyph.numberOfContours == 1 assert glyph.endPtsOfContours == [3] - def test_addPoint_errorOnCurve(self): + def test_addPoint_noErrorOnCurve(self): pen = TTGlyphPointPen(None) pen.beginPath() - with pytest.raises(NotImplementedError): - pen.addPoint((0, 0), "curve") + pen.addPoint((0, 0), "curve") + pen.endPath() def test_beginPath_beginPathOnOpenPath(self): pen = TTGlyphPointPen(None)