[glyf] Support cubic curves

https://github.com/harfbuzz/boring-expansion-spec/issues/41
This commit is contained in:
Behdad Esfahbod 2023-02-20 11:05:35 -07:00
parent 085b489012
commit 82e0536beb
3 changed files with 65 additions and 19 deletions

View File

@ -7,7 +7,7 @@ from fontTools.misc.roundTools import otRound
from fontTools.pens.basePen import LoggingPen, PenError from fontTools.pens.basePen import LoggingPen, PenError
from fontTools.pens.transformPen import TransformPen, TransformPointPen from fontTools.pens.transformPen import TransformPen, TransformPointPen
from fontTools.ttLib.tables import ttProgram 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 Glyph
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
@ -220,9 +220,9 @@ class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
super().__init__(glyphSet, handleOverflowingTransforms) super().__init__(glyphSet, handleOverflowingTransforms)
self.outputImpliedClosingLine = outputImpliedClosingLine 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.points.append(pt)
self.types.append(onCurve) self.types.append(tp)
def _popPoint(self) -> None: def _popPoint(self) -> None:
self.points.pop() self.points.pop()
@ -234,15 +234,21 @@ class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
) )
def lineTo(self, pt: Tuple[float, float]) -> None: def lineTo(self, pt: Tuple[float, float]) -> None:
self._addPoint(pt, 1) self._addPoint(pt, flagOnCurve)
def moveTo(self, pt: Tuple[float, float]) -> None: def moveTo(self, pt: Tuple[float, float]) -> None:
if not self._isClosed(): if not self._isClosed():
raise PenError('"move"-type point must begin a new contour.') raise PenError('"move"-type point must begin a new contour.')
self._addPoint(pt, 1) self._addPoint(pt, flagOnCurve)
def curveTo(self, *points) -> None: 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: def qCurveTo(self, *points) -> None:
assert len(points) >= 1 assert len(points) >= 1
@ -313,9 +319,23 @@ class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
raise PenError("Contour is already closed.") raise PenError("Contour is already closed.")
if self._currentContourStartIndex == len(self.points): if self._currentContourStartIndex == len(self.points):
raise PenError("Tried to end an empty contour.") 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.endPts.append(len(self.points) - 1)
self._currentContourStartIndex = None 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( def addPoint(
self, self,
pt: Tuple[float, float], pt: Tuple[float, float],
@ -331,11 +351,13 @@ class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
if self._isClosed(): if self._isClosed():
raise PenError("Can't add a point to a closed contour.") raise PenError("Can't add a point to a closed contour.")
if segmentType is None: if segmentType is None:
self.types.append(0) # offcurve self.types.append(0)
elif segmentType in ("qcurve", "line", "move"): elif segmentType in ("line", "move"):
self.types.append(1) # oncurve self.types.append(flagOnCurve)
elif segmentType == "qcurve":
self.types.append(flagOnCurve)
elif segmentType == "curve": elif segmentType == "curve":
raise NotImplementedError("cubic curves are not supported") self.types.append("curve")
else: else:
raise AssertionError(segmentType) raise AssertionError(segmentType)

View File

@ -569,10 +569,10 @@ flagRepeat = 0x08
flagXsame = 0x10 flagXsame = 0x10
flagYsame = 0x20 flagYsame = 0x20
flagOverlapSimple = 0x40 flagOverlapSimple = 0x40
flagReserved = 0x80 flagCubic = 0x80
# These flags are kept for XML output after decompiling the coordinates # These flags are kept for XML output after decompiling the coordinates
keepFlags = flagOnCurve + flagOverlapSimple keepFlags = flagOnCurve + flagOverlapSimple + flagCubic
_flagSignBytes = { _flagSignBytes = {
0: 2, 0: 2,
@ -764,6 +764,8 @@ class Glyph(object):
if self.flags[j] & flagOverlapSimple: if self.flags[j] & flagOverlapSimple:
# Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours
attrs.append(("overlap", 1)) attrs.append(("overlap", 1))
if self.flags[j] & flagCubic:
attrs.append(("cubic", 1))
writer.simpletag("pt", attrs) writer.simpletag("pt", attrs)
writer.newline() writer.newline()
last = self.endPtsOfContours[i] + 1 last = self.endPtsOfContours[i] + 1
@ -797,6 +799,8 @@ class Glyph(object):
flag = bool(safeEval(attrs["on"])) flag = bool(safeEval(attrs["on"]))
if "overlap" in attrs and bool(safeEval(attrs["overlap"])): if "overlap" in attrs and bool(safeEval(attrs["overlap"])):
flag |= flagOverlapSimple flag |= flagOverlapSimple
if "cubic" in attrs and bool(safeEval(attrs["cubic"])):
flag |= flagCubic
flags.append(flag) flags.append(flag)
if not hasattr(self, "coordinates"): if not hasattr(self, "coordinates"):
self.coordinates = coordinates self.coordinates = coordinates
@ -1416,20 +1420,27 @@ class Glyph(object):
end = end + 1 end = end + 1
contour = coordinates[start:end] contour = coordinates[start:end]
cFlags = [flagOnCurve & f for f in flags[start:end]] cFlags = [flagOnCurve & f for f in flags[start:end]]
cuFlags = [flagCubic & f for f in flags[start:end]]
start = end start = end
if 1 not in cFlags: if 1 not in cFlags:
# There is not a single on-curve point on the curve, # There is not a single on-curve point on the curve,
# use pen.qCurveTo's special case by specifying None # use pen.qCurveTo's special case by specifying None
# as the on-curve point. # as the on-curve point.
contour.append(None) 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: 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 # to *end* in an on-curve point, which we'll use for
# the moveTo. # the moveTo.
firstOnCurve = cFlags.index(1) + 1 firstOnCurve = cFlags.index(1) + 1
contour = contour[firstOnCurve:] + contour[:firstOnCurve] contour = contour[firstOnCurve:] + contour[:firstOnCurve]
cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve]
cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve]
pen.moveTo(contour[-1]) pen.moveTo(contour[-1])
while contour: while contour:
nextOnCurve = cFlags.index(1) + 1 nextOnCurve = cFlags.index(1) + 1
@ -1439,9 +1450,22 @@ class Glyph(object):
if len(contour) > 1: if len(contour) > 1:
pen.lineTo(contour[0]) pen.lineTo(contour[0])
else: 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:] contour = contour[nextOnCurve:]
cFlags = cFlags[nextOnCurve:] cFlags = cFlags[nextOnCurve:]
cuFlags = cuFlags[nextOnCurve:]
pen.closePath() pen.closePath()
def drawPoints(self, pen, glyfTable, offset=0): def drawPoints(self, pen, glyfTable, offset=0):
@ -1474,7 +1498,7 @@ class Glyph(object):
segmentType = "line" segmentType = "line"
else: else:
pen.addPoint(pt) pen.addPoint(pt)
segmentType = "qcurve" segmentType = "curve" if cFlags[i] & flagCubic else "qcurve"
pen.endPath() pen.endPath()
def __eq__(self, other): def __eq__(self, other):

View File

@ -332,11 +332,11 @@ class TTGlyphPointPenTest(TTGlyphPenTestBase):
assert glyph.numberOfContours == 1 assert glyph.numberOfContours == 1
assert glyph.endPtsOfContours == [3] assert glyph.endPtsOfContours == [3]
def test_addPoint_errorOnCurve(self): def test_addPoint_noErrorOnCurve(self):
pen = TTGlyphPointPen(None) pen = TTGlyphPointPen(None)
pen.beginPath() pen.beginPath()
with pytest.raises(NotImplementedError): pen.addPoint((0, 0), "curve")
pen.addPoint((0, 0), "curve") pen.endPath()
def test_beginPath_beginPathOnOpenPath(self): def test_beginPath_beginPathOnOpenPath(self):
pen = TTGlyphPointPen(None) pen = TTGlyphPointPen(None)