Spaces:
Paused
Paused
| """GlyphSets returned by a TTFont.""" | |
| from abc import ABC, abstractmethod | |
| from collections.abc import Mapping | |
| from contextlib import contextmanager | |
| from copy import copy, deepcopy | |
| from types import SimpleNamespace | |
| from fontTools.misc.vector import Vector | |
| from fontTools.misc.fixedTools import otRound, fixedToFloat as fi2fl | |
| from fontTools.misc.loggingTools import deprecateFunction | |
| from fontTools.misc.transform import Transform, DecomposedTransform | |
| from fontTools.pens.transformPen import TransformPen, TransformPointPen | |
| from fontTools.pens.recordingPen import ( | |
| DecomposingRecordingPen, | |
| lerpRecordings, | |
| replayRecording, | |
| ) | |
| 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, *, recalcBounds=True): | |
| self.recalcBounds = recalcBounds | |
| 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 | |
| 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() | |
| 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) | |
| def has_key(self, glyphName): | |
| return glyphName in self.glyphsMapping | |
| class _TTGlyphSetGlyf(_TTGlyphSet): | |
| def __init__(self, font, location, recalcBounds=True): | |
| self.glyfTable = font["glyf"] | |
| super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds) | |
| self.gvarTable = font.get("gvar") | |
| def __getitem__(self, glyphName): | |
| return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) | |
| class _TTGlyphSetGlyf(_TTGlyphSet): | |
| def __init__(self, font, location, recalcBounds=True): | |
| self.glyfTable = font["glyf"] | |
| super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds) | |
| self.gvarTable = font.get("gvar") | |
| def __getitem__(self, glyphName): | |
| return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) | |
| 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.setLocation(location) | |
| def __getitem__(self, glyphName): | |
| return _TTGlyphCFF(self, glyphName) | |
| def setLocation(self, location): | |
| self.blender = None | |
| if location: | |
| # TODO Optimize by using instancer.setLocation() | |
| from fontTools.varLib.varStore import VarStoreInstancer | |
| varStore = getattr(self.charStrings, "varStore", None) | |
| if varStore is not None: | |
| instancer = VarStoreInstancer( | |
| varStore.otVarStore, self.font["fvar"].axes, location | |
| ) | |
| self.blender = instancer.interpolateFromDeltas | |
| else: | |
| self.blender = None | |
| def pushLocation(self, location, reset: bool): | |
| self.setLocation(location) | |
| with _TTGlyphSet.pushLocation(self, location, reset) as value: | |
| try: | |
| yield value | |
| finally: | |
| self.setLocation(self.location) | |
| class _TTGlyphSetVARC(_TTGlyphSet): | |
| def __init__(self, font, location, glyphSet): | |
| self.glyphSet = glyphSet | |
| super().__init__(font, location, glyphSet) | |
| self.varcTable = font["VARC"].table | |
| def __getitem__(self, glyphName): | |
| varc = self.varcTable | |
| if glyphName not in varc.Coverage.glyphs: | |
| return self.glyphSet[glyphName] | |
| return _TTGlyphVARC(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, *, recalcBounds=True): | |
| self.glyphSet = glyphSet | |
| self.name = glyphName | |
| self.recalcBounds = recalcBounds | |
| 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 | |
| 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 _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 | |
| 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 | |
| glyph.drawPoints(pen, self.glyphSet.glyfTable, offset) | |
| 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, recalcBounds=self.recalcBounds | |
| ) | |
| 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 | |
| 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 _evaluateCondition(condition, fvarAxes, location, instancer): | |
| if condition.Format == 1: | |
| # ConditionAxisRange | |
| axisIndex = condition.AxisIndex | |
| axisTag = fvarAxes[axisIndex].axisTag | |
| axisValue = location.get(axisTag, 0) | |
| minValue = condition.FilterRangeMinValue | |
| maxValue = condition.FilterRangeMaxValue | |
| return minValue <= axisValue <= maxValue | |
| elif condition.Format == 2: | |
| # ConditionValue | |
| value = condition.DefaultValue | |
| value += instancer[condition.VarIdx][0] | |
| return value > 0 | |
| elif condition.Format == 3: | |
| # ConditionAnd | |
| for subcondition in condition.ConditionTable: | |
| if not _evaluateCondition(subcondition, fvarAxes, location, instancer): | |
| return False | |
| return True | |
| elif condition.Format == 4: | |
| # ConditionOr | |
| for subcondition in condition.ConditionTable: | |
| if _evaluateCondition(subcondition, fvarAxes, location, instancer): | |
| return True | |
| return False | |
| elif condition.Format == 5: | |
| # ConditionNegate | |
| return not _evaluateCondition( | |
| condition.conditionTable, fvarAxes, location, instancer | |
| ) | |
| else: | |
| return False # Unkonwn condition format | |
| class _TTGlyphVARC(_TTGlyph): | |
| def _draw(self, pen, isPointPen): | |
| """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details | |
| how that works. | |
| """ | |
| from fontTools.ttLib.tables.otTables import ( | |
| VarComponentFlags, | |
| NO_VARIATION_INDEX, | |
| ) | |
| glyphSet = self.glyphSet | |
| varc = glyphSet.varcTable | |
| idx = varc.Coverage.glyphs.index(self.name) | |
| glyph = varc.VarCompositeGlyphs.VarCompositeGlyph[idx] | |
| from fontTools.varLib.multiVarStore import MultiVarStoreInstancer | |
| from fontTools.varLib.varStore import VarStoreInstancer | |
| fvarAxes = glyphSet.font["fvar"].axes | |
| instancer = MultiVarStoreInstancer( | |
| varc.MultiVarStore, fvarAxes, self.glyphSet.location | |
| ) | |
| for comp in glyph.components: | |
| if comp.flags & VarComponentFlags.HAVE_CONDITION: | |
| condition = varc.ConditionList.ConditionTable[comp.conditionIndex] | |
| if not _evaluateCondition( | |
| condition, fvarAxes, self.glyphSet.location, instancer | |
| ): | |
| continue | |
| location = {} | |
| if comp.axisIndicesIndex is not None: | |
| axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex] | |
| axisValues = Vector(comp.axisValues) | |
| if comp.axisValuesVarIndex != NO_VARIATION_INDEX: | |
| axisValues += fi2fl(instancer[comp.axisValuesVarIndex], 14) | |
| assert len(axisIndices) == len(axisValues), ( | |
| len(axisIndices), | |
| len(axisValues), | |
| ) | |
| location = { | |
| fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues) | |
| } | |
| if comp.transformVarIndex != NO_VARIATION_INDEX: | |
| deltas = instancer[comp.transformVarIndex] | |
| comp = deepcopy(comp) | |
| comp.applyTransformDeltas(deltas) | |
| transform = comp.transform | |
| reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES | |
| with self.glyphSet.glyphSet.pushLocation(location, reset): | |
| with self.glyphSet.pushLocation(location, reset): | |
| shouldDecompose = self.name == comp.glyphName | |
| if not shouldDecompose: | |
| try: | |
| pen.addVarComponent( | |
| comp.glyphName, transform, self.glyphSet.rawLocation | |
| ) | |
| except AttributeError: | |
| shouldDecompose = True | |
| if shouldDecompose: | |
| t = transform.toTransform() | |
| compGlyphSet = ( | |
| self.glyphSet | |
| if comp.glyphName != self.name | |
| else glyphSet.glyphSet | |
| ) | |
| g = compGlyphSet[comp.glyphName] | |
| if isPointPen: | |
| tPen = TransformPointPen(pen, t) | |
| g.drawPoints(tPen) | |
| else: | |
| tPen = TransformPen(pen, t) | |
| g.draw(tPen) | |
| def draw(self, pen): | |
| self._draw(pen, False) | |
| def drawPoints(self, pen): | |
| self._draw(pen, True) | |
| def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True): | |
| # 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.numberOfContours == 0: | |
| assert len(coord) == 0 | |
| else: | |
| assert len(coord) == len(glyph.coordinates) | |
| glyph.coordinates = coord | |
| if recalcBounds: | |
| 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 LerpGlyphSet(Mapping): | |
| """A glyphset that interpolates between two other glyphsets. | |
| Factor is typically between 0 and 1. 0 means the first glyphset, | |
| 1 means the second glyphset, and 0.5 means the average of the | |
| two glyphsets. Other values are possible, and can be useful to | |
| extrapolate. Defaults to 0.5. | |
| """ | |
| def __init__(self, glyphset1, glyphset2, factor=0.5): | |
| self.glyphset1 = glyphset1 | |
| self.glyphset2 = glyphset2 | |
| self.factor = factor | |
| def __getitem__(self, glyphname): | |
| if glyphname in self.glyphset1 and glyphname in self.glyphset2: | |
| return LerpGlyph(glyphname, self) | |
| raise KeyError(glyphname) | |
| def __contains__(self, glyphname): | |
| return glyphname in self.glyphset1 and glyphname in self.glyphset2 | |
| def __iter__(self): | |
| set1 = set(self.glyphset1) | |
| set2 = set(self.glyphset2) | |
| return iter(set1.intersection(set2)) | |
| def __len__(self): | |
| set1 = set(self.glyphset1) | |
| set2 = set(self.glyphset2) | |
| return len(set1.intersection(set2)) | |
| class LerpGlyph: | |
| def __init__(self, glyphname, glyphset): | |
| self.glyphset = glyphset | |
| self.glyphname = glyphname | |
| def draw(self, pen): | |
| recording1 = DecomposingRecordingPen(self.glyphset.glyphset1) | |
| self.glyphset.glyphset1[self.glyphname].draw(recording1) | |
| recording2 = DecomposingRecordingPen(self.glyphset.glyphset2) | |
| self.glyphset.glyphset2[self.glyphname].draw(recording2) | |
| factor = self.glyphset.factor | |
| replayRecording(lerpRecordings(recording1.value, recording2.value, factor), pen) | |