xref: /aosp_15_r20/external/fonttools/Lib/fontTools/fontBuilder.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1__all__ = ["FontBuilder"]
2
3"""
4This module is *experimental*, meaning it still may evolve and change.
5
6The `FontBuilder` class is a convenient helper to construct working TTF or
7OTF fonts from scratch.
8
9Note that the various setup methods cannot be called in arbitrary order,
10due to various interdependencies between OpenType tables. Here is an order
11that works:
12
13    fb = FontBuilder(...)
14    fb.setupGlyphOrder(...)
15    fb.setupCharacterMap(...)
16    fb.setupGlyf(...) --or-- fb.setupCFF(...)
17    fb.setupHorizontalMetrics(...)
18    fb.setupHorizontalHeader()
19    fb.setupNameTable(...)
20    fb.setupOS2()
21    fb.addOpenTypeFeatures(...)
22    fb.setupPost()
23    fb.save(...)
24
25Here is how to build a minimal TTF:
26
27```python
28from fontTools.fontBuilder import FontBuilder
29from fontTools.pens.ttGlyphPen import TTGlyphPen
30
31
32def drawTestGlyph(pen):
33    pen.moveTo((100, 100))
34    pen.lineTo((100, 1000))
35    pen.qCurveTo((200, 900), (400, 900), (500, 1000))
36    pen.lineTo((500, 100))
37    pen.closePath()
38
39
40fb = FontBuilder(1024, isTTF=True)
41fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"])
42fb.setupCharacterMap({32: "space", 65: "A", 97: "a"})
43advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0}
44
45familyName = "HelloTestFont"
46styleName = "TotallyNormal"
47version = "0.1"
48
49nameStrings = dict(
50    familyName=dict(en=familyName, nl="HalloTestFont"),
51    styleName=dict(en=styleName, nl="TotaalNormaal"),
52    uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName,
53    fullName=familyName + "-" + styleName,
54    psName=familyName + "-" + styleName,
55    version="Version " + version,
56)
57
58pen = TTGlyphPen(None)
59drawTestGlyph(pen)
60glyph = pen.glyph()
61glyphs = {".notdef": glyph, "space": glyph, "A": glyph, "a": glyph, ".null": glyph}
62fb.setupGlyf(glyphs)
63metrics = {}
64glyphTable = fb.font["glyf"]
65for gn, advanceWidth in advanceWidths.items():
66    metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
67fb.setupHorizontalMetrics(metrics)
68fb.setupHorizontalHeader(ascent=824, descent=-200)
69fb.setupNameTable(nameStrings)
70fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200)
71fb.setupPost()
72fb.save("test.ttf")
73```
74
75And here's how to build a minimal OTF:
76
77```python
78from fontTools.fontBuilder import FontBuilder
79from fontTools.pens.t2CharStringPen import T2CharStringPen
80
81
82def drawTestGlyph(pen):
83    pen.moveTo((100, 100))
84    pen.lineTo((100, 1000))
85    pen.curveTo((200, 900), (400, 900), (500, 1000))
86    pen.lineTo((500, 100))
87    pen.closePath()
88
89
90fb = FontBuilder(1024, isTTF=False)
91fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"])
92fb.setupCharacterMap({32: "space", 65: "A", 97: "a"})
93advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0}
94
95familyName = "HelloTestFont"
96styleName = "TotallyNormal"
97version = "0.1"
98
99nameStrings = dict(
100    familyName=dict(en=familyName, nl="HalloTestFont"),
101    styleName=dict(en=styleName, nl="TotaalNormaal"),
102    uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName,
103    fullName=familyName + "-" + styleName,
104    psName=familyName + "-" + styleName,
105    version="Version " + version,
106)
107
108pen = T2CharStringPen(600, None)
109drawTestGlyph(pen)
110charString = pen.getCharString()
111charStrings = {
112    ".notdef": charString,
113    "space": charString,
114    "A": charString,
115    "a": charString,
116    ".null": charString,
117}
118fb.setupCFF(nameStrings["psName"], {"FullName": nameStrings["psName"]}, charStrings, {})
119lsb = {gn: cs.calcBounds(None)[0] for gn, cs in charStrings.items()}
120metrics = {}
121for gn, advanceWidth in advanceWidths.items():
122    metrics[gn] = (advanceWidth, lsb[gn])
123fb.setupHorizontalMetrics(metrics)
124fb.setupHorizontalHeader(ascent=824, descent=200)
125fb.setupNameTable(nameStrings)
126fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200)
127fb.setupPost()
128fb.save("test.otf")
129```
130"""
131
132from .ttLib import TTFont, newTable
133from .ttLib.tables._c_m_a_p import cmap_classes
134from .ttLib.tables._g_l_y_f import flagCubic
135from .ttLib.tables.O_S_2f_2 import Panose
136from .misc.timeTools import timestampNow
137import struct
138from collections import OrderedDict
139
140
141_headDefaults = dict(
142    tableVersion=1.0,
143    fontRevision=1.0,
144    checkSumAdjustment=0,
145    magicNumber=0x5F0F3CF5,
146    flags=0x0003,
147    unitsPerEm=1000,
148    created=0,
149    modified=0,
150    xMin=0,
151    yMin=0,
152    xMax=0,
153    yMax=0,
154    macStyle=0,
155    lowestRecPPEM=3,
156    fontDirectionHint=2,
157    indexToLocFormat=0,
158    glyphDataFormat=0,
159)
160
161_maxpDefaultsTTF = dict(
162    tableVersion=0x00010000,
163    numGlyphs=0,
164    maxPoints=0,
165    maxContours=0,
166    maxCompositePoints=0,
167    maxCompositeContours=0,
168    maxZones=2,
169    maxTwilightPoints=0,
170    maxStorage=0,
171    maxFunctionDefs=0,
172    maxInstructionDefs=0,
173    maxStackElements=0,
174    maxSizeOfInstructions=0,
175    maxComponentElements=0,
176    maxComponentDepth=0,
177)
178_maxpDefaultsOTF = dict(
179    tableVersion=0x00005000,
180    numGlyphs=0,
181)
182
183_postDefaults = dict(
184    formatType=3.0,
185    italicAngle=0,
186    underlinePosition=0,
187    underlineThickness=0,
188    isFixedPitch=0,
189    minMemType42=0,
190    maxMemType42=0,
191    minMemType1=0,
192    maxMemType1=0,
193)
194
195_hheaDefaults = dict(
196    tableVersion=0x00010000,
197    ascent=0,
198    descent=0,
199    lineGap=0,
200    advanceWidthMax=0,
201    minLeftSideBearing=0,
202    minRightSideBearing=0,
203    xMaxExtent=0,
204    caretSlopeRise=1,
205    caretSlopeRun=0,
206    caretOffset=0,
207    reserved0=0,
208    reserved1=0,
209    reserved2=0,
210    reserved3=0,
211    metricDataFormat=0,
212    numberOfHMetrics=0,
213)
214
215_vheaDefaults = dict(
216    tableVersion=0x00010000,
217    ascent=0,
218    descent=0,
219    lineGap=0,
220    advanceHeightMax=0,
221    minTopSideBearing=0,
222    minBottomSideBearing=0,
223    yMaxExtent=0,
224    caretSlopeRise=0,
225    caretSlopeRun=0,
226    reserved0=0,
227    reserved1=0,
228    reserved2=0,
229    reserved3=0,
230    reserved4=0,
231    metricDataFormat=0,
232    numberOfVMetrics=0,
233)
234
235_nameIDs = dict(
236    copyright=0,
237    familyName=1,
238    styleName=2,
239    uniqueFontIdentifier=3,
240    fullName=4,
241    version=5,
242    psName=6,
243    trademark=7,
244    manufacturer=8,
245    designer=9,
246    description=10,
247    vendorURL=11,
248    designerURL=12,
249    licenseDescription=13,
250    licenseInfoURL=14,
251    # reserved = 15,
252    typographicFamily=16,
253    typographicSubfamily=17,
254    compatibleFullName=18,
255    sampleText=19,
256    postScriptCIDFindfontName=20,
257    wwsFamilyName=21,
258    wwsSubfamilyName=22,
259    lightBackgroundPalette=23,
260    darkBackgroundPalette=24,
261    variationsPostScriptNamePrefix=25,
262)
263
264# to insert in setupNameTable doc string:
265# print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1])))
266
267_panoseDefaults = Panose()
268
269_OS2Defaults = dict(
270    version=3,
271    xAvgCharWidth=0,
272    usWeightClass=400,
273    usWidthClass=5,
274    fsType=0x0004,  # default: Preview & Print embedding
275    ySubscriptXSize=0,
276    ySubscriptYSize=0,
277    ySubscriptXOffset=0,
278    ySubscriptYOffset=0,
279    ySuperscriptXSize=0,
280    ySuperscriptYSize=0,
281    ySuperscriptXOffset=0,
282    ySuperscriptYOffset=0,
283    yStrikeoutSize=0,
284    yStrikeoutPosition=0,
285    sFamilyClass=0,
286    panose=_panoseDefaults,
287    ulUnicodeRange1=0,
288    ulUnicodeRange2=0,
289    ulUnicodeRange3=0,
290    ulUnicodeRange4=0,
291    achVendID="????",
292    fsSelection=0,
293    usFirstCharIndex=0,
294    usLastCharIndex=0,
295    sTypoAscender=0,
296    sTypoDescender=0,
297    sTypoLineGap=0,
298    usWinAscent=0,
299    usWinDescent=0,
300    ulCodePageRange1=0,
301    ulCodePageRange2=0,
302    sxHeight=0,
303    sCapHeight=0,
304    usDefaultChar=0,  # .notdef
305    usBreakChar=32,  # space
306    usMaxContext=0,
307    usLowerOpticalPointSize=0,
308    usUpperOpticalPointSize=0,
309)
310
311
312class FontBuilder(object):
313    def __init__(self, unitsPerEm=None, font=None, isTTF=True, glyphDataFormat=0):
314        """Initialize a FontBuilder instance.
315
316        If the `font` argument is not given, a new `TTFont` will be
317        constructed, and `unitsPerEm` must be given. If `isTTF` is True,
318        the font will be a glyf-based TTF; if `isTTF` is False it will be
319        a CFF-based OTF.
320
321        The `glyphDataFormat` argument corresponds to the `head` table field
322        that defines the format of the TrueType `glyf` table (default=0).
323        TrueType glyphs historically can only contain quadratic splines and static
324        components, but there's a proposal to add support for cubic Bezier curves as well
325        as variable composites/components at
326        https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
327        You can experiment with the new features by setting `glyphDataFormat` to 1.
328        A ValueError is raised if `glyphDataFormat` is left at 0 but glyphs are added
329        that contain cubic splines or varcomposites. This is to prevent accidentally
330        creating fonts that are incompatible with existing TrueType implementations.
331
332        If `font` is given, it must be a `TTFont` instance and `unitsPerEm`
333        must _not_ be given. The `isTTF` and `glyphDataFormat` arguments will be ignored.
334        """
335        if font is None:
336            self.font = TTFont(recalcTimestamp=False)
337            self.isTTF = isTTF
338            now = timestampNow()
339            assert unitsPerEm is not None
340            self.setupHead(
341                unitsPerEm=unitsPerEm,
342                created=now,
343                modified=now,
344                glyphDataFormat=glyphDataFormat,
345            )
346            self.setupMaxp()
347        else:
348            assert unitsPerEm is None
349            self.font = font
350            self.isTTF = "glyf" in font
351
352    def save(self, file):
353        """Save the font. The 'file' argument can be either a pathname or a
354        writable file object.
355        """
356        self.font.save(file)
357
358    def _initTableWithValues(self, tableTag, defaults, values):
359        table = self.font[tableTag] = newTable(tableTag)
360        for k, v in defaults.items():
361            setattr(table, k, v)
362        for k, v in values.items():
363            setattr(table, k, v)
364        return table
365
366    def _updateTableWithValues(self, tableTag, values):
367        table = self.font[tableTag]
368        for k, v in values.items():
369            setattr(table, k, v)
370
371    def setupHead(self, **values):
372        """Create a new `head` table and initialize it with default values,
373        which can be overridden by keyword arguments.
374        """
375        self._initTableWithValues("head", _headDefaults, values)
376
377    def updateHead(self, **values):
378        """Update the head table with the fields and values passed as
379        keyword arguments.
380        """
381        self._updateTableWithValues("head", values)
382
383    def setupGlyphOrder(self, glyphOrder):
384        """Set the glyph order for the font."""
385        self.font.setGlyphOrder(glyphOrder)
386
387    def setupCharacterMap(self, cmapping, uvs=None, allowFallback=False):
388        """Build the `cmap` table for the font. The `cmapping` argument should
389        be a dict mapping unicode code points as integers to glyph names.
390
391        The `uvs` argument, when passed, must be a list of tuples, describing
392        Unicode Variation Sequences. These tuples have three elements:
393            (unicodeValue, variationSelector, glyphName)
394        `unicodeValue` and `variationSelector` are integer code points.
395        `glyphName` may be None, to indicate this is the default variation.
396        Text processors will then use the cmap to find the glyph name.
397        Each Unicode Variation Sequence should be an officially supported
398        sequence, but this is not policed.
399        """
400        subTables = []
401        highestUnicode = max(cmapping) if cmapping else 0
402        if highestUnicode > 0xFFFF:
403            cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000)
404            subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10)
405            subTables.append(subTable_3_10)
406        else:
407            cmapping_3_1 = cmapping
408        format = 4
409        subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
410        try:
411            subTable_3_1.compile(self.font)
412        except struct.error:
413            # format 4 overflowed, fall back to format 12
414            if not allowFallback:
415                raise ValueError(
416                    "cmap format 4 subtable overflowed; sort glyph order by unicode to fix."
417                )
418            format = 12
419            subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
420        subTables.append(subTable_3_1)
421        subTable_0_3 = buildCmapSubTable(cmapping_3_1, format, 0, 3)
422        subTables.append(subTable_0_3)
423
424        if uvs is not None:
425            uvsDict = {}
426            for unicodeValue, variationSelector, glyphName in uvs:
427                if cmapping.get(unicodeValue) == glyphName:
428                    # this is a default variation
429                    glyphName = None
430                if variationSelector not in uvsDict:
431                    uvsDict[variationSelector] = []
432                uvsDict[variationSelector].append((unicodeValue, glyphName))
433            uvsSubTable = buildCmapSubTable({}, 14, 0, 5)
434            uvsSubTable.uvsDict = uvsDict
435            subTables.append(uvsSubTable)
436
437        self.font["cmap"] = newTable("cmap")
438        self.font["cmap"].tableVersion = 0
439        self.font["cmap"].tables = subTables
440
441    def setupNameTable(self, nameStrings, windows=True, mac=True):
442        """Create the `name` table for the font. The `nameStrings` argument must
443        be a dict, mapping nameIDs or descriptive names for the nameIDs to name
444        record values. A value is either a string, or a dict, mapping language codes
445        to strings, to allow localized name table entries.
446
447        By default, both Windows (platformID=3) and Macintosh (platformID=1) name
448        records are added, unless any of `windows` or `mac` arguments is False.
449
450        The following descriptive names are available for nameIDs:
451
452            copyright (nameID 0)
453            familyName (nameID 1)
454            styleName (nameID 2)
455            uniqueFontIdentifier (nameID 3)
456            fullName (nameID 4)
457            version (nameID 5)
458            psName (nameID 6)
459            trademark (nameID 7)
460            manufacturer (nameID 8)
461            designer (nameID 9)
462            description (nameID 10)
463            vendorURL (nameID 11)
464            designerURL (nameID 12)
465            licenseDescription (nameID 13)
466            licenseInfoURL (nameID 14)
467            typographicFamily (nameID 16)
468            typographicSubfamily (nameID 17)
469            compatibleFullName (nameID 18)
470            sampleText (nameID 19)
471            postScriptCIDFindfontName (nameID 20)
472            wwsFamilyName (nameID 21)
473            wwsSubfamilyName (nameID 22)
474            lightBackgroundPalette (nameID 23)
475            darkBackgroundPalette (nameID 24)
476            variationsPostScriptNamePrefix (nameID 25)
477        """
478        nameTable = self.font["name"] = newTable("name")
479        nameTable.names = []
480
481        for nameName, nameValue in nameStrings.items():
482            if isinstance(nameName, int):
483                nameID = nameName
484            else:
485                nameID = _nameIDs[nameName]
486            if isinstance(nameValue, str):
487                nameValue = dict(en=nameValue)
488            nameTable.addMultilingualName(
489                nameValue, ttFont=self.font, nameID=nameID, windows=windows, mac=mac
490            )
491
492    def setupOS2(self, **values):
493        """Create a new `OS/2` table and initialize it with default values,
494        which can be overridden by keyword arguments.
495        """
496        self._initTableWithValues("OS/2", _OS2Defaults, values)
497        if "xAvgCharWidth" not in values:
498            assert (
499                "hmtx" in self.font
500            ), "the 'hmtx' table must be setup before the 'OS/2' table"
501            self.font["OS/2"].recalcAvgCharWidth(self.font)
502        if not (
503            "ulUnicodeRange1" in values
504            or "ulUnicodeRange2" in values
505            or "ulUnicodeRange3" in values
506            or "ulUnicodeRange3" in values
507        ):
508            assert (
509                "cmap" in self.font
510            ), "the 'cmap' table must be setup before the 'OS/2' table"
511            self.font["OS/2"].recalcUnicodeRanges(self.font)
512
513    def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
514        from .cffLib import (
515            CFFFontSet,
516            TopDictIndex,
517            TopDict,
518            CharStrings,
519            GlobalSubrsIndex,
520            PrivateDict,
521        )
522
523        assert not self.isTTF
524        self.font.sfntVersion = "OTTO"
525        fontSet = CFFFontSet()
526        fontSet.major = 1
527        fontSet.minor = 0
528        fontSet.otFont = self.font
529        fontSet.fontNames = [psName]
530        fontSet.topDictIndex = TopDictIndex()
531
532        globalSubrs = GlobalSubrsIndex()
533        fontSet.GlobalSubrs = globalSubrs
534        private = PrivateDict()
535        for key, value in privateDict.items():
536            setattr(private, key, value)
537        fdSelect = None
538        fdArray = None
539
540        topDict = TopDict()
541        topDict.charset = self.font.getGlyphOrder()
542        topDict.Private = private
543        topDict.GlobalSubrs = fontSet.GlobalSubrs
544        for key, value in fontInfo.items():
545            setattr(topDict, key, value)
546        if "FontMatrix" not in fontInfo:
547            scale = 1 / self.font["head"].unitsPerEm
548            topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
549
550        charStrings = CharStrings(
551            None, topDict.charset, globalSubrs, private, fdSelect, fdArray
552        )
553        for glyphName, charString in charStringsDict.items():
554            charString.private = private
555            charString.globalSubrs = globalSubrs
556            charStrings[glyphName] = charString
557        topDict.CharStrings = charStrings
558
559        fontSet.topDictIndex.append(topDict)
560
561        self.font["CFF "] = newTable("CFF ")
562        self.font["CFF "].cff = fontSet
563
564    def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None):
565        from .cffLib import (
566            CFFFontSet,
567            TopDictIndex,
568            TopDict,
569            CharStrings,
570            GlobalSubrsIndex,
571            PrivateDict,
572            FDArrayIndex,
573            FontDict,
574        )
575
576        assert not self.isTTF
577        self.font.sfntVersion = "OTTO"
578        fontSet = CFFFontSet()
579        fontSet.major = 2
580        fontSet.minor = 0
581
582        cff2GetGlyphOrder = self.font.getGlyphOrder
583        fontSet.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None)
584
585        globalSubrs = GlobalSubrsIndex()
586        fontSet.GlobalSubrs = globalSubrs
587
588        if fdArrayList is None:
589            fdArrayList = [{}]
590        fdSelect = None
591        fdArray = FDArrayIndex()
592        fdArray.strings = None
593        fdArray.GlobalSubrs = globalSubrs
594        for privateDict in fdArrayList:
595            fontDict = FontDict()
596            fontDict.setCFF2(True)
597            private = PrivateDict()
598            for key, value in privateDict.items():
599                setattr(private, key, value)
600            fontDict.Private = private
601            fdArray.append(fontDict)
602
603        topDict = TopDict()
604        topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
605        topDict.FDArray = fdArray
606        scale = 1 / self.font["head"].unitsPerEm
607        topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
608
609        private = fdArray[0].Private
610        charStrings = CharStrings(None, None, globalSubrs, private, fdSelect, fdArray)
611        for glyphName, charString in charStringsDict.items():
612            charString.private = private
613            charString.globalSubrs = globalSubrs
614            charStrings[glyphName] = charString
615        topDict.CharStrings = charStrings
616
617        fontSet.topDictIndex.append(topDict)
618
619        self.font["CFF2"] = newTable("CFF2")
620        self.font["CFF2"].cff = fontSet
621
622        if regions:
623            self.setupCFF2Regions(regions)
624
625    def setupCFF2Regions(self, regions):
626        from .varLib.builder import buildVarRegionList, buildVarData, buildVarStore
627        from .cffLib import VarStoreData
628
629        assert "fvar" in self.font, "fvar must to be set up first"
630        assert "CFF2" in self.font, "CFF2 must to be set up first"
631        axisTags = [a.axisTag for a in self.font["fvar"].axes]
632        varRegionList = buildVarRegionList(regions, axisTags)
633        varData = buildVarData(list(range(len(regions))), None, optimize=False)
634        varStore = buildVarStore(varRegionList, [varData])
635        vstore = VarStoreData(otVarStore=varStore)
636        topDict = self.font["CFF2"].cff.topDictIndex[0]
637        topDict.VarStore = vstore
638        for fontDict in topDict.FDArray:
639            fontDict.Private.vstore = vstore
640
641    def setupGlyf(self, glyphs, calcGlyphBounds=True, validateGlyphFormat=True):
642        """Create the `glyf` table from a dict, that maps glyph names
643        to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example
644        as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`.
645
646        If `calcGlyphBounds` is True, the bounds of all glyphs will be
647        calculated. Only pass False if your glyph objects already have
648        their bounding box values set.
649
650        If `validateGlyphFormat` is True, raise ValueError if any of the glyphs contains
651        cubic curves or is a variable composite but head.glyphDataFormat=0.
652        Set it to False to skip the check if you know in advance all the glyphs are
653        compatible with the specified glyphDataFormat.
654        """
655        assert self.isTTF
656
657        if validateGlyphFormat and self.font["head"].glyphDataFormat == 0:
658            for name, g in glyphs.items():
659                if g.isVarComposite():
660                    raise ValueError(
661                        f"Glyph {name!r} is a variable composite, but glyphDataFormat=0"
662                    )
663                elif g.numberOfContours > 0 and any(f & flagCubic for f in g.flags):
664                    raise ValueError(
665                        f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; "
666                        "either convert to quadratics with cu2qu or set glyphDataFormat=1."
667                    )
668
669        self.font["loca"] = newTable("loca")
670        self.font["glyf"] = newTable("glyf")
671        self.font["glyf"].glyphs = glyphs
672        if hasattr(self.font, "glyphOrder"):
673            self.font["glyf"].glyphOrder = self.font.glyphOrder
674        if calcGlyphBounds:
675            self.calcGlyphBounds()
676
677    def setupFvar(self, axes, instances):
678        """Adds an font variations table to the font.
679
680        Args:
681            axes (list): See below.
682            instances (list): See below.
683
684        ``axes`` should be a list of axes, with each axis either supplied as
685        a py:class:`.designspaceLib.AxisDescriptor` object, or a tuple in the
686        format ```tupletag, minValue, defaultValue, maxValue, name``.
687        The ``name`` is either a string, or a dict, mapping language codes
688        to strings, to allow localized name table entries.
689
690        ```instances`` should be a list of instances, with each instance either
691        supplied as a py:class:`.designspaceLib.InstanceDescriptor` object, or a
692        dict with keys ``location`` (mapping of axis tags to float values),
693        ``stylename`` and (optionally) ``postscriptfontname``.
694        The ``stylename`` is either a string, or a dict, mapping language codes
695        to strings, to allow localized name table entries.
696        """
697
698        addFvar(self.font, axes, instances)
699
700    def setupAvar(self, axes, mappings=None):
701        """Adds an axis variations table to the font.
702
703        Args:
704            axes (list): A list of py:class:`.designspaceLib.AxisDescriptor` objects.
705        """
706        from .varLib import _add_avar
707
708        if "fvar" not in self.font:
709            raise KeyError("'fvar' table is missing; can't add 'avar'.")
710
711        axisTags = [axis.axisTag for axis in self.font["fvar"].axes]
712        axes = OrderedDict(enumerate(axes))  # Only values are used
713        _add_avar(self.font, axes, mappings, axisTags)
714
715    def setupGvar(self, variations):
716        gvar = self.font["gvar"] = newTable("gvar")
717        gvar.version = 1
718        gvar.reserved = 0
719        gvar.variations = variations
720
721    def calcGlyphBounds(self):
722        """Calculate the bounding boxes of all glyphs in the `glyf` table.
723        This is usually not called explicitly by client code.
724        """
725        glyphTable = self.font["glyf"]
726        for glyph in glyphTable.glyphs.values():
727            glyph.recalcBounds(glyphTable)
728
729    def setupHorizontalMetrics(self, metrics):
730        """Create a new `hmtx` table, for horizontal metrics.
731
732        The `metrics` argument must be a dict, mapping glyph names to
733        `(width, leftSidebearing)` tuples.
734        """
735        self.setupMetrics("hmtx", metrics)
736
737    def setupVerticalMetrics(self, metrics):
738        """Create a new `vmtx` table, for horizontal metrics.
739
740        The `metrics` argument must be a dict, mapping glyph names to
741        `(height, topSidebearing)` tuples.
742        """
743        self.setupMetrics("vmtx", metrics)
744
745    def setupMetrics(self, tableTag, metrics):
746        """See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
747        assert tableTag in ("hmtx", "vmtx")
748        mtxTable = self.font[tableTag] = newTable(tableTag)
749        roundedMetrics = {}
750        for gn in metrics:
751            w, lsb = metrics[gn]
752            roundedMetrics[gn] = int(round(w)), int(round(lsb))
753        mtxTable.metrics = roundedMetrics
754
755    def setupHorizontalHeader(self, **values):
756        """Create a new `hhea` table initialize it with default values,
757        which can be overridden by keyword arguments.
758        """
759        self._initTableWithValues("hhea", _hheaDefaults, values)
760
761    def setupVerticalHeader(self, **values):
762        """Create a new `vhea` table initialize it with default values,
763        which can be overridden by keyword arguments.
764        """
765        self._initTableWithValues("vhea", _vheaDefaults, values)
766
767    def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None):
768        """Create a new `VORG` table. The `verticalOrigins` argument must be
769        a dict, mapping glyph names to vertical origin values.
770
771        The `defaultVerticalOrigin` argument should be the most common vertical
772        origin value. If omitted, this value will be derived from the actual
773        values in the `verticalOrigins` argument.
774        """
775        if defaultVerticalOrigin is None:
776            # find the most frequent vorg value
777            bag = {}
778            for gn in verticalOrigins:
779                vorg = verticalOrigins[gn]
780                if vorg not in bag:
781                    bag[vorg] = 1
782                else:
783                    bag[vorg] += 1
784            defaultVerticalOrigin = sorted(
785                bag, key=lambda vorg: bag[vorg], reverse=True
786            )[0]
787        self._initTableWithValues(
788            "VORG",
789            {},
790            dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin),
791        )
792        vorgTable = self.font["VORG"]
793        vorgTable.majorVersion = 1
794        vorgTable.minorVersion = 0
795        for gn in verticalOrigins:
796            vorgTable[gn] = verticalOrigins[gn]
797
798    def setupPost(self, keepGlyphNames=True, **values):
799        """Create a new `post` table and initialize it with default values,
800        which can be overridden by keyword arguments.
801        """
802        isCFF2 = "CFF2" in self.font
803        postTable = self._initTableWithValues("post", _postDefaults, values)
804        if (self.isTTF or isCFF2) and keepGlyphNames:
805            postTable.formatType = 2.0
806            postTable.extraNames = []
807            postTable.mapping = {}
808        else:
809            postTable.formatType = 3.0
810
811    def setupMaxp(self):
812        """Create a new `maxp` table. This is called implicitly by FontBuilder
813        itself and is usually not called by client code.
814        """
815        if self.isTTF:
816            defaults = _maxpDefaultsTTF
817        else:
818            defaults = _maxpDefaultsOTF
819        self._initTableWithValues("maxp", defaults, {})
820
821    def setupDummyDSIG(self):
822        """This adds an empty DSIG table to the font to make some MS applications
823        happy. This does not properly sign the font.
824        """
825        values = dict(
826            ulVersion=1,
827            usFlag=0,
828            usNumSigs=0,
829            signatureRecords=[],
830        )
831        self._initTableWithValues("DSIG", {}, values)
832
833    def addOpenTypeFeatures(self, features, filename=None, tables=None, debug=False):
834        """Add OpenType features to the font from a string containing
835        Feature File syntax.
836
837        The `filename` argument is used in error messages and to determine
838        where to look for "include" files.
839
840        The optional `tables` argument can be a list of OTL tables tags to
841        build, allowing the caller to only build selected OTL tables. See
842        `fontTools.feaLib` for details.
843
844        The optional `debug` argument controls whether to add source debugging
845        information to the font in the `Debg` table.
846        """
847        from .feaLib.builder import addOpenTypeFeaturesFromString
848
849        addOpenTypeFeaturesFromString(
850            self.font, features, filename=filename, tables=tables, debug=debug
851        )
852
853    def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"):
854        """Add conditional substitutions to a Variable Font.
855
856        See `fontTools.varLib.featureVars.addFeatureVariations`.
857        """
858        from .varLib import featureVars
859
860        if "fvar" not in self.font:
861            raise KeyError("'fvar' table is missing; can't add FeatureVariations.")
862
863        featureVars.addFeatureVariations(
864            self.font, conditionalSubstitutions, featureTag=featureTag
865        )
866
867    def setupCOLR(
868        self,
869        colorLayers,
870        version=None,
871        varStore=None,
872        varIndexMap=None,
873        clipBoxes=None,
874        allowLayerReuse=True,
875    ):
876        """Build new COLR table using color layers dictionary.
877
878        Cf. `fontTools.colorLib.builder.buildCOLR`.
879        """
880        from fontTools.colorLib.builder import buildCOLR
881
882        glyphMap = self.font.getReverseGlyphMap()
883        self.font["COLR"] = buildCOLR(
884            colorLayers,
885            version=version,
886            glyphMap=glyphMap,
887            varStore=varStore,
888            varIndexMap=varIndexMap,
889            clipBoxes=clipBoxes,
890            allowLayerReuse=allowLayerReuse,
891        )
892
893    def setupCPAL(
894        self,
895        palettes,
896        paletteTypes=None,
897        paletteLabels=None,
898        paletteEntryLabels=None,
899    ):
900        """Build new CPAL table using list of palettes.
901
902        Optionally build CPAL v1 table using paletteTypes, paletteLabels and
903        paletteEntryLabels.
904
905        Cf. `fontTools.colorLib.builder.buildCPAL`.
906        """
907        from fontTools.colorLib.builder import buildCPAL
908
909        self.font["CPAL"] = buildCPAL(
910            palettes,
911            paletteTypes=paletteTypes,
912            paletteLabels=paletteLabels,
913            paletteEntryLabels=paletteEntryLabels,
914            nameTable=self.font.get("name"),
915        )
916
917    def setupStat(self, axes, locations=None, elidedFallbackName=2):
918        """Build a new 'STAT' table.
919
920        See `fontTools.otlLib.builder.buildStatTable` for details about
921        the arguments.
922        """
923        from .otlLib.builder import buildStatTable
924
925        buildStatTable(self.font, axes, locations, elidedFallbackName)
926
927
928def buildCmapSubTable(cmapping, format, platformID, platEncID):
929    subTable = cmap_classes[format](format)
930    subTable.cmap = cmapping
931    subTable.platformID = platformID
932    subTable.platEncID = platEncID
933    subTable.language = 0
934    return subTable
935
936
937def addFvar(font, axes, instances):
938    from .ttLib.tables._f_v_a_r import Axis, NamedInstance
939
940    assert axes
941
942    fvar = newTable("fvar")
943    nameTable = font["name"]
944
945    for axis_def in axes:
946        axis = Axis()
947
948        if isinstance(axis_def, tuple):
949            (
950                axis.axisTag,
951                axis.minValue,
952                axis.defaultValue,
953                axis.maxValue,
954                name,
955            ) = axis_def
956        else:
957            (axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue, name) = (
958                axis_def.tag,
959                axis_def.minimum,
960                axis_def.default,
961                axis_def.maximum,
962                axis_def.name,
963            )
964            if axis_def.hidden:
965                axis.flags = 0x0001  # HIDDEN_AXIS
966
967        if isinstance(name, str):
968            name = dict(en=name)
969
970        axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font)
971        fvar.axes.append(axis)
972
973    for instance in instances:
974        if isinstance(instance, dict):
975            coordinates = instance["location"]
976            name = instance["stylename"]
977            psname = instance.get("postscriptfontname")
978        else:
979            coordinates = instance.location
980            name = instance.localisedStyleName or instance.styleName
981            psname = instance.postScriptFontName
982
983        if isinstance(name, str):
984            name = dict(en=name)
985
986        inst = NamedInstance()
987        inst.subfamilyNameID = nameTable.addMultilingualName(name, ttFont=font)
988        if psname is not None:
989            inst.postscriptNameID = nameTable.addName(psname)
990        inst.coordinates = coordinates
991        fvar.instances.append(inst)
992
993    font["fvar"] = fvar
994