[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.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)

View File

@ -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)
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
@ -1438,10 +1449,23 @@ class Glyph(object):
# pen.closePath()
if len(contour) > 1:
pen.lineTo(contour[0])
else:
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):

View File

@ -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.endPath()
def test_beginPath_beginPathOnOpenPath(self):
pen = TTGlyphPointPen(None)