123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- """GlyphSets returned by a TTFont."""
- 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, *, 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
- @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, 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.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, *, 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
- @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 _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, 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 _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.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
- 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,
- )
|