diff options
Diffstat (limited to 'Lib/fontTools/ttLib/ttGlyphSet.py')
-rw-r--r-- | Lib/fontTools/ttLib/ttGlyphSet.py | 519 |
1 files changed, 308 insertions, 211 deletions
diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index be26215b..d4384c89 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -1,221 +1,318 @@ """GlyphSets returned by a TTFont.""" -from fontTools.misc.fixedTools import otRound +from abc import ABC, abstractmethod +from collections.abc import Mapping +from contextlib import contextmanager from copy import copy +from types import SimpleNamespace +from fontTools.misc.fixedTools import otRound +from fontTools.misc.loggingTools import deprecateFunction +from fontTools.misc.transform import Transform +from fontTools.pens.transformPen import TransformPen, TransformPointPen + + +class _TTGlyphSet(Mapping): + + """Generic dict-like GlyphSet class that pulls metrics from hmtx and + glyph shape from TrueType or CFF. + """ + + def __init__(self, font, location, glyphsMapping): + self.font = font + self.defaultLocationNormalized = ( + {axis.axisTag: 0 for axis in self.font["fvar"].axes} + if "fvar" in self.font + else {} + ) + self.location = location if location is not None else {} + self.rawLocation = {} # VarComponent-only location + self.originalLocation = location if location is not None else {} + self.depth = 0 + self.locationStack = [] + self.rawLocationStack = [] + self.glyphsMapping = glyphsMapping + self.hMetrics = font["hmtx"].metrics + self.vMetrics = getattr(font.get("vmtx"), "metrics", None) + self.hvarTable = None + if location: + from fontTools.varLib.varStore import VarStoreInstancer + + self.hvarTable = getattr(font.get("HVAR"), "table", None) + if self.hvarTable is not None: + self.hvarInstancer = VarStoreInstancer( + self.hvarTable.VarStore, font["fvar"].axes, location + ) + # TODO VVAR, VORG + + @contextmanager + def pushLocation(self, location, reset: bool): + self.locationStack.append(self.location) + self.rawLocationStack.append(self.rawLocation) + if reset: + self.location = self.originalLocation.copy() + self.rawLocation = self.defaultLocationNormalized.copy() + else: + self.location = self.location.copy() + self.rawLocation = {} + self.location.update(location) + self.rawLocation.update(location) + + try: + yield None + finally: + self.location = self.locationStack.pop() + self.rawLocation = self.rawLocationStack.pop() + + @contextmanager + def pushDepth(self): + try: + depth = self.depth + self.depth += 1 + yield depth + finally: + self.depth -= 1 + + def __contains__(self, glyphName): + return glyphName in self.glyphsMapping + + def __iter__(self): + return iter(self.glyphsMapping.keys()) + + def __len__(self): + return len(self.glyphsMapping) + + @deprecateFunction( + "use 'glyphName in glyphSet' instead", category=DeprecationWarning + ) + def has_key(self, glyphName): + return glyphName in self.glyphsMapping + + +class _TTGlyphSetGlyf(_TTGlyphSet): + def __init__(self, font, location): + self.glyfTable = font["glyf"] + super().__init__(font, location, self.glyfTable) + self.gvarTable = font.get("gvar") + + def __getitem__(self, glyphName): + return _TTGlyphGlyf(self, glyphName) + + +class _TTGlyphSetCFF(_TTGlyphSet): + def __init__(self, font, location): + tableTag = "CFF2" if "CFF2" in font else "CFF " + self.charStrings = list(font[tableTag].cff.values())[0].CharStrings + super().__init__(font, location, self.charStrings) + self.blender = None + if location: + from fontTools.varLib.varStore import VarStoreInstancer + + varStore = getattr(self.charStrings, "varStore", None) + if varStore is not None: + instancer = VarStoreInstancer( + varStore.otVarStore, font["fvar"].axes, location + ) + self.blender = instancer.interpolateFromDeltas + + def __getitem__(self, glyphName): + return _TTGlyphCFF(self, glyphName) + + +class _TTGlyph(ABC): + + """Glyph object that supports the Pen protocol, meaning that it has + .draw() and .drawPoints() methods that take a pen object as their only + argument. Additionally there are 'width' and 'lsb' attributes, read from + the 'hmtx' table. + + If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' + attributes. + """ + + def __init__(self, glyphSet, glyphName): + self.glyphSet = glyphSet + self.name = glyphName + self.width, self.lsb = glyphSet.hMetrics[glyphName] + if glyphSet.vMetrics is not None: + self.height, self.tsb = glyphSet.vMetrics[glyphName] + else: + self.height, self.tsb = None, None + if glyphSet.location and glyphSet.hvarTable is not None: + varidx = ( + glyphSet.font.getGlyphID(glyphName) + if glyphSet.hvarTable.AdvWidthMap is None + else glyphSet.hvarTable.AdvWidthMap.mapping[glyphName] + ) + self.width += glyphSet.hvarInstancer[varidx] + # TODO: VVAR/VORG + + @abstractmethod + def draw(self, pen): + """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details + how that works. + """ + raise NotImplementedError + + def drawPoints(self, pen): + """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details + how that works. + """ + from fontTools.pens.pointPen import SegmentToPointPen + + self.draw(SegmentToPointPen(pen)) -class _TTGlyphSet(object): - - """Generic dict-like GlyphSet class that pulls metrics from hmtx and - glyph shape from TrueType or CFF. - """ - - def __init__(self, ttFont, glyphs, glyphType): - """Construct a new glyphset. - - Args: - font (TTFont): The font object (used to get metrics). - glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects. - glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``. - """ - self._glyphs = glyphs - self._hmtx = ttFont['hmtx'] - self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None - self._glyphType = glyphType - - def keys(self): - return list(self._glyphs.keys()) - - def has_key(self, glyphName): - return glyphName in self._glyphs - - __contains__ = has_key - - def __getitem__(self, glyphName): - horizontalMetrics = self._hmtx[glyphName] - verticalMetrics = self._vmtx[glyphName] if self._vmtx else None - return self._glyphType( - self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics) - - def __len__(self): - return len(self._glyphs) - - def get(self, glyphName, default=None): - try: - return self[glyphName] - except KeyError: - return default - -class _TTGlyph(object): - - """Wrapper for a TrueType glyph that supports the Pen protocol, meaning - that it has .draw() and .drawPoints() methods that take a pen object as - their only argument. Additionally there are 'width' and 'lsb' attributes, - read from the 'hmtx' table. - - If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' - attributes. - """ - - def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None): - """Construct a new _TTGlyph. - - Args: - glyphset (_TTGlyphSet): A glyphset object used to resolve components. - glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object. - horizontalMetrics (int, int): The glyph's width and left sidebearing. - """ - self._glyphset = glyphset - self._glyph = glyph - self.width, self.lsb = horizontalMetrics - if verticalMetrics: - self.height, self.tsb = verticalMetrics - else: - self.height, self.tsb = None, None - - def draw(self, pen): - """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details - how that works. - """ - self._glyph.draw(pen) - - def drawPoints(self, pen): - from fontTools.pens.pointPen import SegmentToPointPen - self.draw(SegmentToPointPen(pen)) - -class _TTGlyphCFF(_TTGlyph): - pass class _TTGlyphGlyf(_TTGlyph): + def draw(self, pen): + """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details + how that works. + """ + glyph, offset = self._getGlyphAndOffset() + + with self.glyphSet.pushDepth() as depth: + if depth: + offset = 0 # Offset should only apply at top-level + + if glyph.isVarComposite(): + self._drawVarComposite(glyph, pen, False) + return + + glyph.draw(pen, self.glyphSet.glyfTable, offset) + + def drawPoints(self, pen): + """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details + how that works. + """ + glyph, offset = self._getGlyphAndOffset() + + with self.glyphSet.pushDepth() as depth: + if depth: + offset = 0 # Offset should only apply at top-level + + if glyph.isVarComposite(): + self._drawVarComposite(glyph, pen, True) + return + + glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) + + def _drawVarComposite(self, glyph, pen, isPointPen): + from fontTools.ttLib.tables._g_l_y_f import ( + VarComponentFlags, + VAR_COMPONENT_TRANSFORM_MAPPING, + ) + + for comp in glyph.components: + with self.glyphSet.pushLocation( + comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES + ): + try: + pen.addVarComponent( + comp.glyphName, comp.transform, self.glyphSet.rawLocation + ) + except AttributeError: + t = comp.transform.toTransform() + if isPointPen: + tPen = TransformPointPen(pen, t) + self.glyphSet[comp.glyphName].drawPoints(tPen) + else: + tPen = TransformPen(pen, t) + self.glyphSet[comp.glyphName].draw(tPen) + + def _getGlyphAndOffset(self): + if self.glyphSet.location and self.glyphSet.gvarTable is not None: + glyph = self._getGlyphInstance() + else: + glyph = self.glyphSet.glyfTable[self.name] + + offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 + return glyph, offset + + def _getGlyphInstance(self): + from fontTools.varLib.iup import iup_delta + from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates + from fontTools.varLib.models import supportScalar + + glyphSet = self.glyphSet + glyfTable = glyphSet.glyfTable + variations = glyphSet.gvarTable.variations[self.name] + hMetrics = glyphSet.hMetrics + vMetrics = glyphSet.vMetrics + coordinates, _ = glyfTable._getCoordinatesAndControls( + self.name, hMetrics, vMetrics + ) + origCoords, endPts = None, None + for var in variations: + scalar = supportScalar(glyphSet.location, var.axes) + if not scalar: + continue + delta = var.coordinates + if None in delta: + if origCoords is None: + origCoords, control = glyfTable._getCoordinatesAndControls( + self.name, 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 + + glyph = copy(glyfTable[self.name]) # Shallow copy + width, lsb, height, tsb = _setCoordinates(glyph, coordinates, glyfTable) + self.lsb = lsb + self.tsb = tsb + if glyphSet.hvarTable is None: + # no HVAR: let's set metrics from the phantom points + self.width = width + self.height = height + return glyph + - def draw(self, pen): - """Draw the glyph onto Pen. See fontTools.pens.basePen for details - how that works. - """ - glyfTable = self._glyphset._glyphs - glyph = self._glyph - offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 - glyph.draw(pen, glyfTable, offset) - - def drawPoints(self, pen): - """Draw the glyph onto PointPen. See fontTools.pens.pointPen - for details how that works. - """ - glyfTable = self._glyphset._glyphs - glyph = self._glyph - offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 - glyph.drawPoints(pen, glyfTable, offset) - - - -class _TTVarGlyphSet(_TTGlyphSet): - - def __init__(self, font, location, normalized=False): - self._ttFont = font - self._glyphs = font['glyf'] - - if not normalized: - from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap - - axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in font['fvar'].axes} - location = normalizeLocation(location, axes) - if 'avar' in font: - avar = font['avar'] - avarSegments = avar.segments - new_location = {} - for axis_tag, value in location.items(): - avarMapping = avarSegments.get(axis_tag, None) - if avarMapping is not None: - value = piecewiseLinearMap(value, avarMapping) - new_location[axis_tag] = value - location = new_location - del new_location - - self.location = location - - def __getitem__(self, glyphName): - if glyphName not in self._glyphs: - raise KeyError(glyphName) - return _TTVarGlyphGlyf(self._ttFont, glyphName, self.location) +class _TTGlyphCFF(_TTGlyph): + def draw(self, pen): + """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details + how that works. + """ + self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender) 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 == 0: - assert len(coord) == 0 - else: - assert len(coord) == len(glyph.coordinates) - glyph.coordinates = coord - - glyph.recalcBounds(glyfTable) - - horizontalAdvanceWidth = otRound(rightSideX - leftSideX) - verticalAdvanceWidth = otRound(topSideY - bottomSideY) - leftSideBearing = otRound(glyph.xMin - leftSideX) - topSideBearing = otRound(topSideY - glyph.yMax) - return ( - horizontalAdvanceWidth, - leftSideBearing, - verticalAdvanceWidth, - topSideBearing, - ) - - -class _TTVarGlyph(_TTGlyph): - def __init__(self, ttFont, glyphName, location): - self._ttFont = ttFont - self._glyphName = glyphName - self._location = location - # draw() fills these in - self.width = self.height = self.lsb = self.tsb = None - - -class _TTVarGlyphGlyf(_TTVarGlyph): - - 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'] - 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 - - glyph = copy(glyf[self._glyphName]) # Shallow copy - width, lsb, height, tsb = _setCoordinates(glyph, coordinates, glyf) - self.width = width - self.lsb = lsb - self.height = height - self.tsb = tsb - offset = lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 - glyph.draw(pen, glyf, offset) + # Handle phantom points for (left, right, top, bottom) positions. + assert len(coord) >= 4 + 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) + glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy + for p, comp in zip(coord, glyph.components): + if hasattr(comp, "x"): + comp.x, comp.y = p + elif glyph.isVarComposite(): + glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy + for comp in glyph.components: + coord = comp.setCoordinates(coord) + assert not coord + elif glyph.numberOfContours == 0: + assert len(coord) == 0 + else: + assert len(coord) == len(glyph.coordinates) + glyph.coordinates = coord + + glyph.recalcBounds(glyfTable) + + horizontalAdvanceWidth = otRound(rightSideX - leftSideX) + verticalAdvanceWidth = otRound(topSideY - bottomSideY) + leftSideBearing = otRound(glyph.xMin - leftSideX) + topSideBearing = otRound(topSideY - glyph.yMax) + return ( + horizontalAdvanceWidth, + leftSideBearing, + verticalAdvanceWidth, + topSideBearing, + ) |