aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/ttLib/ttGlyphSet.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/ttLib/ttGlyphSet.py')
-rw-r--r--Lib/fontTools/ttLib/ttGlyphSet.py519
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,
+ )