From c148dc76e6d6f1b5990fc50ceb3b6de192a1d47b Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 12 Aug 2022 13:36:34 -0600 Subject: [PATCH] [ttFont/ttGlyphSet] Add API for drawing variable fonts Fixes https://github.com/fonttools/fonttools/issues/1021 --- Lib/fontTools/ttLib/ttFont.py | 10 ++- Lib/fontTools/ttLib/ttGlyphSet.py | 103 ++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py index 7abb8f490..83ce3b5fb 100644 --- a/Lib/fontTools/ttLib/ttFont.py +++ b/Lib/fontTools/ttLib/ttFont.py @@ -7,6 +7,7 @@ from fontTools.ttLib import TTLibError from fontTools.ttLib.ttGlyphSet import ( _TTGlyphSet, _TTGlyph, _TTGlyphCFF, _TTGlyphGlyf, + _TTVarGlyphSet, ) from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter from io import BytesIO, StringIO @@ -677,7 +678,7 @@ class TTFont(object): else: raise KeyError(tag) - def getGlyphSet(self, preferCFF=True): + def getGlyphSet(self, preferCFF=True, location=None): """Return a generic GlyphSet, which is a dict-like object mapping glyph names to glyph objects. The returned glyph objects have a .draw() method that supports the Pen protocol, and will @@ -693,11 +694,16 @@ class TTFont(object): if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): table_tag = "CFF2" if "CFF2" in self else "CFF " + if location: + raise NotImplementedError glyphs = _TTGlyphSet(self, list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) if glyphs is None and "glyf" in self: - glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) + if location and 'gvar' in self: + glyphs = _TTVarGlyphSet(self, location) + else: + glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) if glyphs is None: raise TTLibError("Font contains no outlines") diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 48c2a6fdd..bad18bebb 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -102,3 +102,106 @@ class _TTGlyphGlyf(_TTGlyph): glyph.drawPoints(pen, glyfTable, offset) + +class _TTVarGlyphSet(object): + + def __init__(self, font, location): + from fontTools.varLib.models import normalizeLocation + self._ttFont = font + axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in font['fvar'].axes} + self.location = normalizeLocation(location, axes) + + def keys(self): + return list(self._ttFont['glyf'].keys()) + + def has_key(self, glyphName): + return glyphName in self._ttFont['glyf'] + __contains__ = has_key + + def __getitem__(self, glyphName): + return _TTVarGlyphGlyf(self._ttFont, glyphName, self.location) + + def get(self, glyphName, default=None): + try: + return self[glyphName] + except KeyError: + return default + +def setCoordinates(glyph, coord, glyfTable): + # Handle phantom points for (left, right, top, bottom) positions. + assert len(coord) >= 4 + if not hasattr(glyph, 'xMin'): + glyph.recalcBounds(glyfTable) + leftSideX = coord[-4][0] + rightSideX = coord[-3][0] + topSideY = coord[-2][1] + bottomSideY = coord[-1][1] + + for _ in range(4): + del coord[-1] + + if glyph.isComposite(): + assert len(coord) == len(glyph.components) + for p,comp in zip(coord, glyph.components): + if hasattr(comp, 'x'): + comp.x,comp.y = p + elif glyph.numberOfContours is 0: + assert len(coord) == 0 + else: + assert len(coord) == len(glyph.coordinates) + glyph.coordinates = coord + + glyph.recalcBounds(glyfTable) + + horizontalAdvanceWidth = rightSideX - leftSideX + leftSideBearing = glyph.xMin - leftSideX + return horizontalAdvanceWidth, leftSideBearing + + +class _TTVarGlyphGlyf(object): + + def __init__(self, ttFont, glyphName, location): + self._ttFont = ttFont + self._glyphName = glyphName + self._location = location + #self.width, self.lsb = ttFont['hmtx'][glyphName] + + @staticmethod + def _copyGlyph(glyph, glyfTable): + # would be nice if this could be avoided + from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph + from copy import deepcopy + #glyph = TTGlyph(glyph.compile(glyfTable)) + glyph.expand(glyfTable) + return deepcopy(glyph) + + def draw(self, pen): + from fontTools.varLib.iup import iup_delta + from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates + from fontTools.varLib.models import supportScalar + + glyf = self._ttFont['glyf'] + glyph = glyf[self._glyphName] + glyph = self._copyGlyph(glyph, glyf) + hMetrics = self._ttFont['hmtx'].metrics + vMetrics = getattr(self._ttFont.get('vmtx'), 'metrics', None) + + variations = self._ttFont['gvar'].variations[self._glyphName] + coordinates, _ = glyf._getCoordinatesAndControls(self._glyphName, hMetrics, vMetrics) + origCoords, endPts = None, None + for var in variations: + scalar = supportScalar(self._location, var.axes) + if not scalar: + continue + delta = var.coordinates + if None in delta: + if origCoords is None: + origCoords,control = glyf._getCoordinatesAndControls(self._glyphName, hMetrics, vMetrics) + endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) + delta = iup_delta(delta, origCoords, endPts) + coordinates += GlyphCoordinates(delta) * scalar + + horizontalAdvanceWidth, leftSideBearing = setCoordinates(glyph, coordinates, glyf) + self.width = horizontalAdvanceWidth + # XXX height! + glyph.draw(pen, self._ttFont['glyf']) # XXX offset based on lsb