xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/ttGlyphSet.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""GlyphSets returned by a TTFont."""
2
3from abc import ABC, abstractmethod
4from collections.abc import Mapping
5from contextlib import contextmanager
6from copy import copy
7from types import SimpleNamespace
8from fontTools.misc.fixedTools import otRound
9from fontTools.misc.loggingTools import deprecateFunction
10from fontTools.misc.transform import Transform
11from fontTools.pens.transformPen import TransformPen, TransformPointPen
12from fontTools.pens.recordingPen import (
13    DecomposingRecordingPen,
14    lerpRecordings,
15    replayRecording,
16)
17
18
19class _TTGlyphSet(Mapping):
20    """Generic dict-like GlyphSet class that pulls metrics from hmtx and
21    glyph shape from TrueType or CFF.
22    """
23
24    def __init__(self, font, location, glyphsMapping, *, recalcBounds=True):
25        self.recalcBounds = recalcBounds
26        self.font = font
27        self.defaultLocationNormalized = (
28            {axis.axisTag: 0 for axis in self.font["fvar"].axes}
29            if "fvar" in self.font
30            else {}
31        )
32        self.location = location if location is not None else {}
33        self.rawLocation = {}  # VarComponent-only location
34        self.originalLocation = location if location is not None else {}
35        self.depth = 0
36        self.locationStack = []
37        self.rawLocationStack = []
38        self.glyphsMapping = glyphsMapping
39        self.hMetrics = font["hmtx"].metrics
40        self.vMetrics = getattr(font.get("vmtx"), "metrics", None)
41        self.hvarTable = None
42        if location:
43            from fontTools.varLib.varStore import VarStoreInstancer
44
45            self.hvarTable = getattr(font.get("HVAR"), "table", None)
46            if self.hvarTable is not None:
47                self.hvarInstancer = VarStoreInstancer(
48                    self.hvarTable.VarStore, font["fvar"].axes, location
49                )
50            # TODO VVAR, VORG
51
52    @contextmanager
53    def pushLocation(self, location, reset: bool):
54        self.locationStack.append(self.location)
55        self.rawLocationStack.append(self.rawLocation)
56        if reset:
57            self.location = self.originalLocation.copy()
58            self.rawLocation = self.defaultLocationNormalized.copy()
59        else:
60            self.location = self.location.copy()
61            self.rawLocation = {}
62        self.location.update(location)
63        self.rawLocation.update(location)
64
65        try:
66            yield None
67        finally:
68            self.location = self.locationStack.pop()
69            self.rawLocation = self.rawLocationStack.pop()
70
71    @contextmanager
72    def pushDepth(self):
73        try:
74            depth = self.depth
75            self.depth += 1
76            yield depth
77        finally:
78            self.depth -= 1
79
80    def __contains__(self, glyphName):
81        return glyphName in self.glyphsMapping
82
83    def __iter__(self):
84        return iter(self.glyphsMapping.keys())
85
86    def __len__(self):
87        return len(self.glyphsMapping)
88
89    @deprecateFunction(
90        "use 'glyphName in glyphSet' instead", category=DeprecationWarning
91    )
92    def has_key(self, glyphName):
93        return glyphName in self.glyphsMapping
94
95
96class _TTGlyphSetGlyf(_TTGlyphSet):
97    def __init__(self, font, location, recalcBounds=True):
98        self.glyfTable = font["glyf"]
99        super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds)
100        self.gvarTable = font.get("gvar")
101
102    def __getitem__(self, glyphName):
103        return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds)
104
105
106class _TTGlyphSetCFF(_TTGlyphSet):
107    def __init__(self, font, location):
108        tableTag = "CFF2" if "CFF2" in font else "CFF "
109        self.charStrings = list(font[tableTag].cff.values())[0].CharStrings
110        super().__init__(font, location, self.charStrings)
111        self.blender = None
112        if location:
113            from fontTools.varLib.varStore import VarStoreInstancer
114
115            varStore = getattr(self.charStrings, "varStore", None)
116            if varStore is not None:
117                instancer = VarStoreInstancer(
118                    varStore.otVarStore, font["fvar"].axes, location
119                )
120                self.blender = instancer.interpolateFromDeltas
121
122    def __getitem__(self, glyphName):
123        return _TTGlyphCFF(self, glyphName)
124
125
126class _TTGlyph(ABC):
127    """Glyph object that supports the Pen protocol, meaning that it has
128    .draw() and .drawPoints() methods that take a pen object as their only
129    argument. Additionally there are 'width' and 'lsb' attributes, read from
130    the 'hmtx' table.
131
132    If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
133    attributes.
134    """
135
136    def __init__(self, glyphSet, glyphName, *, recalcBounds=True):
137        self.glyphSet = glyphSet
138        self.name = glyphName
139        self.recalcBounds = recalcBounds
140        self.width, self.lsb = glyphSet.hMetrics[glyphName]
141        if glyphSet.vMetrics is not None:
142            self.height, self.tsb = glyphSet.vMetrics[glyphName]
143        else:
144            self.height, self.tsb = None, None
145        if glyphSet.location and glyphSet.hvarTable is not None:
146            varidx = (
147                glyphSet.font.getGlyphID(glyphName)
148                if glyphSet.hvarTable.AdvWidthMap is None
149                else glyphSet.hvarTable.AdvWidthMap.mapping[glyphName]
150            )
151            self.width += glyphSet.hvarInstancer[varidx]
152        # TODO: VVAR/VORG
153
154    @abstractmethod
155    def draw(self, pen):
156        """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
157        how that works.
158        """
159        raise NotImplementedError
160
161    def drawPoints(self, pen):
162        """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details
163        how that works.
164        """
165        from fontTools.pens.pointPen import SegmentToPointPen
166
167        self.draw(SegmentToPointPen(pen))
168
169
170class _TTGlyphGlyf(_TTGlyph):
171    def draw(self, pen):
172        """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
173        how that works.
174        """
175        glyph, offset = self._getGlyphAndOffset()
176
177        with self.glyphSet.pushDepth() as depth:
178            if depth:
179                offset = 0  # Offset should only apply at top-level
180
181            if glyph.isVarComposite():
182                self._drawVarComposite(glyph, pen, False)
183                return
184
185            glyph.draw(pen, self.glyphSet.glyfTable, offset)
186
187    def drawPoints(self, pen):
188        """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details
189        how that works.
190        """
191        glyph, offset = self._getGlyphAndOffset()
192
193        with self.glyphSet.pushDepth() as depth:
194            if depth:
195                offset = 0  # Offset should only apply at top-level
196
197            if glyph.isVarComposite():
198                self._drawVarComposite(glyph, pen, True)
199                return
200
201            glyph.drawPoints(pen, self.glyphSet.glyfTable, offset)
202
203    def _drawVarComposite(self, glyph, pen, isPointPen):
204        from fontTools.ttLib.tables._g_l_y_f import (
205            VarComponentFlags,
206            VAR_COMPONENT_TRANSFORM_MAPPING,
207        )
208
209        for comp in glyph.components:
210            with self.glyphSet.pushLocation(
211                comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
212            ):
213                try:
214                    pen.addVarComponent(
215                        comp.glyphName, comp.transform, self.glyphSet.rawLocation
216                    )
217                except AttributeError:
218                    t = comp.transform.toTransform()
219                    if isPointPen:
220                        tPen = TransformPointPen(pen, t)
221                        self.glyphSet[comp.glyphName].drawPoints(tPen)
222                    else:
223                        tPen = TransformPen(pen, t)
224                        self.glyphSet[comp.glyphName].draw(tPen)
225
226    def _getGlyphAndOffset(self):
227        if self.glyphSet.location and self.glyphSet.gvarTable is not None:
228            glyph = self._getGlyphInstance()
229        else:
230            glyph = self.glyphSet.glyfTable[self.name]
231
232        offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
233        return glyph, offset
234
235    def _getGlyphInstance(self):
236        from fontTools.varLib.iup import iup_delta
237        from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
238        from fontTools.varLib.models import supportScalar
239
240        glyphSet = self.glyphSet
241        glyfTable = glyphSet.glyfTable
242        variations = glyphSet.gvarTable.variations[self.name]
243        hMetrics = glyphSet.hMetrics
244        vMetrics = glyphSet.vMetrics
245        coordinates, _ = glyfTable._getCoordinatesAndControls(
246            self.name, hMetrics, vMetrics
247        )
248        origCoords, endPts = None, None
249        for var in variations:
250            scalar = supportScalar(glyphSet.location, var.axes)
251            if not scalar:
252                continue
253            delta = var.coordinates
254            if None in delta:
255                if origCoords is None:
256                    origCoords, control = glyfTable._getCoordinatesAndControls(
257                        self.name, hMetrics, vMetrics
258                    )
259                    endPts = (
260                        control[1] if control[0] >= 1 else list(range(len(control[1])))
261                    )
262                delta = iup_delta(delta, origCoords, endPts)
263            coordinates += GlyphCoordinates(delta) * scalar
264
265        glyph = copy(glyfTable[self.name])  # Shallow copy
266        width, lsb, height, tsb = _setCoordinates(
267            glyph, coordinates, glyfTable, recalcBounds=self.recalcBounds
268        )
269        self.lsb = lsb
270        self.tsb = tsb
271        if glyphSet.hvarTable is None:
272            # no HVAR: let's set metrics from the phantom points
273            self.width = width
274            self.height = height
275        return glyph
276
277
278class _TTGlyphCFF(_TTGlyph):
279    def draw(self, pen):
280        """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
281        how that works.
282        """
283        self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender)
284
285
286def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True):
287    # Handle phantom points for (left, right, top, bottom) positions.
288    assert len(coord) >= 4
289    leftSideX = coord[-4][0]
290    rightSideX = coord[-3][0]
291    topSideY = coord[-2][1]
292    bottomSideY = coord[-1][1]
293
294    for _ in range(4):
295        del coord[-1]
296
297    if glyph.isComposite():
298        assert len(coord) == len(glyph.components)
299        glyph.components = [copy(comp) for comp in glyph.components]  # Shallow copy
300        for p, comp in zip(coord, glyph.components):
301            if hasattr(comp, "x"):
302                comp.x, comp.y = p
303    elif glyph.isVarComposite():
304        glyph.components = [copy(comp) for comp in glyph.components]  # Shallow copy
305        for comp in glyph.components:
306            coord = comp.setCoordinates(coord)
307        assert not coord
308    elif glyph.numberOfContours == 0:
309        assert len(coord) == 0
310    else:
311        assert len(coord) == len(glyph.coordinates)
312        glyph.coordinates = coord
313
314    if recalcBounds:
315        glyph.recalcBounds(glyfTable)
316
317    horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
318    verticalAdvanceWidth = otRound(topSideY - bottomSideY)
319    leftSideBearing = otRound(glyph.xMin - leftSideX)
320    topSideBearing = otRound(topSideY - glyph.yMax)
321    return (
322        horizontalAdvanceWidth,
323        leftSideBearing,
324        verticalAdvanceWidth,
325        topSideBearing,
326    )
327
328
329class LerpGlyphSet(Mapping):
330    """A glyphset that interpolates between two other glyphsets.
331
332    Factor is typically between 0 and 1. 0 means the first glyphset,
333    1 means the second glyphset, and 0.5 means the average of the
334    two glyphsets. Other values are possible, and can be useful to
335    extrapolate. Defaults to 0.5.
336    """
337
338    def __init__(self, glyphset1, glyphset2, factor=0.5):
339        self.glyphset1 = glyphset1
340        self.glyphset2 = glyphset2
341        self.factor = factor
342
343    def __getitem__(self, glyphname):
344        if glyphname in self.glyphset1 and glyphname in self.glyphset2:
345            return LerpGlyph(glyphname, self)
346        raise KeyError(glyphname)
347
348    def __contains__(self, glyphname):
349        return glyphname in self.glyphset1 and glyphname in self.glyphset2
350
351    def __iter__(self):
352        set1 = set(self.glyphset1)
353        set2 = set(self.glyphset2)
354        return iter(set1.intersection(set2))
355
356    def __len__(self):
357        set1 = set(self.glyphset1)
358        set2 = set(self.glyphset2)
359        return len(set1.intersection(set2))
360
361
362class LerpGlyph:
363    def __init__(self, glyphname, glyphset):
364        self.glyphset = glyphset
365        self.glyphname = glyphname
366
367    def draw(self, pen):
368        recording1 = DecomposingRecordingPen(self.glyphset.glyphset1)
369        self.glyphset.glyphset1[self.glyphname].draw(recording1)
370        recording2 = DecomposingRecordingPen(self.glyphset.glyphset2)
371        self.glyphset.glyphset2[self.glyphname].draw(recording2)
372
373        factor = self.glyphset.factor
374
375        replayRecording(lerpRecordings(recording1.value, recording2.value, factor), pen)
376