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