• 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        self._initTableWithValues("OS/2", _OS2Defaults, values)
490        if "xAvgCharWidth" not in values:
491            assert (
492                "hmtx" in self.font
493            ), "the 'hmtx' table must be setup before the 'OS/2' table"
494            self.font["OS/2"].recalcAvgCharWidth(self.font)
495        if not (
496            "ulUnicodeRange1" in values
497            or "ulUnicodeRange2" in values
498            or "ulUnicodeRange3" in values
499            or "ulUnicodeRange3" in values
500        ):
501            assert (
502                "cmap" in self.font
503            ), "the 'cmap' table must be setup before the 'OS/2' table"
504            self.font["OS/2"].recalcUnicodeRanges(self.font)
505
506    def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
507        from .cffLib import (
508            CFFFontSet,
509            TopDictIndex,
510            TopDict,
511            CharStrings,
512            GlobalSubrsIndex,
513            PrivateDict,
514        )
515
516        assert not self.isTTF
517        self.font.sfntVersion = "OTTO"
518        fontSet = CFFFontSet()
519        fontSet.major = 1
520        fontSet.minor = 0
521        fontSet.otFont = self.font
522        fontSet.fontNames = [psName]
523        fontSet.topDictIndex = TopDictIndex()
524
525        globalSubrs = GlobalSubrsIndex()
526        fontSet.GlobalSubrs = globalSubrs
527        private = PrivateDict()
528        for key, value in privateDict.items():
529            setattr(private, key, value)
530        fdSelect = None
531        fdArray = None
532
533        topDict = TopDict()
534        topDict.charset = self.font.getGlyphOrder()
535        topDict.Private = private
536        topDict.GlobalSubrs = fontSet.GlobalSubrs
537        for key, value in fontInfo.items():
538            setattr(topDict, key, value)
539        if "FontMatrix" not in fontInfo:
540            scale = 1 / self.font["head"].unitsPerEm
541            topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
542
543        charStrings = CharStrings(
544            None, topDict.charset, globalSubrs, private, fdSelect, fdArray
545        )
546        for glyphName, charString in charStringsDict.items():
547            charString.private = private
548            charString.globalSubrs = globalSubrs
549            charStrings[glyphName] = charString
550        topDict.CharStrings = charStrings
551
552        fontSet.topDictIndex.append(topDict)
553
554        self.font["CFF "] = newTable("CFF ")
555        self.font["CFF "].cff = fontSet
556
557    def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None):
558        from .cffLib import (
559            CFFFontSet,
560            TopDictIndex,
561            TopDict,
562            CharStrings,
563            GlobalSubrsIndex,
564            PrivateDict,
565            FDArrayIndex,
566            FontDict,
567        )
568
569        assert not self.isTTF
570        self.font.sfntVersion = "OTTO"
571        fontSet = CFFFontSet()
572        fontSet.major = 2
573        fontSet.minor = 0
574
575        cff2GetGlyphOrder = self.font.getGlyphOrder
576        fontSet.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None)
577
578        globalSubrs = GlobalSubrsIndex()
579        fontSet.GlobalSubrs = globalSubrs
580
581        if fdArrayList is None:
582            fdArrayList = [{}]
583        fdSelect = None
584        fdArray = FDArrayIndex()
585        fdArray.strings = None
586        fdArray.GlobalSubrs = globalSubrs
587        for privateDict in fdArrayList:
588            fontDict = FontDict()
589            fontDict.setCFF2(True)
590            private = PrivateDict()
591            for key, value in privateDict.items():
592                setattr(private, key, value)
593            fontDict.Private = private
594            fdArray.append(fontDict)
595
596        topDict = TopDict()
597        topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
598        topDict.FDArray = fdArray
599        scale = 1 / self.font["head"].unitsPerEm
600        topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
601
602        private = fdArray[0].Private
603        charStrings = CharStrings(None, None, globalSubrs, private, fdSelect, fdArray)
604        for glyphName, charString in charStringsDict.items():
605            charString.private = private
606            charString.globalSubrs = globalSubrs
607            charStrings[glyphName] = charString
608        topDict.CharStrings = charStrings
609
610        fontSet.topDictIndex.append(topDict)
611
612        self.font["CFF2"] = newTable("CFF2")
613        self.font["CFF2"].cff = fontSet
614
615        if regions:
616            self.setupCFF2Regions(regions)
617
618    def setupCFF2Regions(self, regions):
619        from .varLib.builder import buildVarRegionList, buildVarData, buildVarStore
620        from .cffLib import VarStoreData
621
622        assert "fvar" in self.font, "fvar must to be set up first"
623        assert "CFF2" in self.font, "CFF2 must to be set up first"
624        axisTags = [a.axisTag for a in self.font["fvar"].axes]
625        varRegionList = buildVarRegionList(regions, axisTags)
626        varData = buildVarData(list(range(len(regions))), None, optimize=False)
627        varStore = buildVarStore(varRegionList, [varData])
628        vstore = VarStoreData(otVarStore=varStore)
629        topDict = self.font["CFF2"].cff.topDictIndex[0]
630        topDict.VarStore = vstore
631        for fontDict in topDict.FDArray:
632            fontDict.Private.vstore = vstore
633
634    def setupGlyf(self, glyphs, calcGlyphBounds=True):
635        """Create the `glyf` table from a dict, that maps glyph names
636        to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example
637        as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`.
638
639        If `calcGlyphBounds` is True, the bounds of all glyphs will be
640        calculated. Only pass False if your glyph objects already have
641        their bounding box values set.
642        """
643        assert self.isTTF
644        self.font["loca"] = newTable("loca")
645        self.font["glyf"] = newTable("glyf")
646        self.font["glyf"].glyphs = glyphs
647        if hasattr(self.font, "glyphOrder"):
648            self.font["glyf"].glyphOrder = self.font.glyphOrder
649        if calcGlyphBounds:
650            self.calcGlyphBounds()
651
652    def setupFvar(self, axes, instances):
653        """Adds an font variations table to the font.
654
655        Args:
656            axes (list): See below.
657            instances (list): See below.
658
659        ``axes`` should be a list of axes, with each axis either supplied as
660        a py:class:`.designspaceLib.AxisDescriptor` object, or a tuple in the
661        format ```tupletag, minValue, defaultValue, maxValue, name``.
662        The ``name`` is either a string, or a dict, mapping language codes
663        to strings, to allow localized name table entries.
664
665        ```instances`` should be a list of instances, with each instance either
666        supplied as a py:class:`.designspaceLib.InstanceDescriptor` object, or a
667        dict with keys ``location`` (mapping of axis tags to float values),
668        ``stylename`` and (optionally) ``postscriptfontname``.
669        The ``stylename`` is either a string, or a dict, mapping language codes
670        to strings, to allow localized name table entries.
671        """
672
673        addFvar(self.font, axes, instances)
674
675    def setupAvar(self, axes):
676        """Adds an axis variations table to the font.
677
678        Args:
679            axes (list): A list of py:class:`.designspaceLib.AxisDescriptor` objects.
680        """
681        from .varLib import _add_avar
682
683        _add_avar(self.font, OrderedDict(enumerate(axes)))  # Only values are used
684
685    def setupGvar(self, variations):
686        gvar = self.font["gvar"] = newTable("gvar")
687        gvar.version = 1
688        gvar.reserved = 0
689        gvar.variations = variations
690
691    def calcGlyphBounds(self):
692        """Calculate the bounding boxes of all glyphs in the `glyf` table.
693        This is usually not called explicitly by client code.
694        """
695        glyphTable = self.font["glyf"]
696        for glyph in glyphTable.glyphs.values():
697            glyph.recalcBounds(glyphTable)
698
699    def setupHorizontalMetrics(self, metrics):
700        """Create a new `hmtx` table, for horizontal metrics.
701
702        The `metrics` argument must be a dict, mapping glyph names to
703        `(width, leftSidebearing)` tuples.
704        """
705        self.setupMetrics("hmtx", metrics)
706
707    def setupVerticalMetrics(self, metrics):
708        """Create a new `vmtx` table, for horizontal metrics.
709
710        The `metrics` argument must be a dict, mapping glyph names to
711        `(height, topSidebearing)` tuples.
712        """
713        self.setupMetrics("vmtx", metrics)
714
715    def setupMetrics(self, tableTag, metrics):
716        """See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
717        assert tableTag in ("hmtx", "vmtx")
718        mtxTable = self.font[tableTag] = newTable(tableTag)
719        roundedMetrics = {}
720        for gn in metrics:
721            w, lsb = metrics[gn]
722            roundedMetrics[gn] = int(round(w)), int(round(lsb))
723        mtxTable.metrics = roundedMetrics
724
725    def setupHorizontalHeader(self, **values):
726        """Create a new `hhea` table initialize it with default values,
727        which can be overridden by keyword arguments.
728        """
729        self._initTableWithValues("hhea", _hheaDefaults, values)
730
731    def setupVerticalHeader(self, **values):
732        """Create a new `vhea` table initialize it with default values,
733        which can be overridden by keyword arguments.
734        """
735        self._initTableWithValues("vhea", _vheaDefaults, values)
736
737    def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None):
738        """Create a new `VORG` table. The `verticalOrigins` argument must be
739        a dict, mapping glyph names to vertical origin values.
740
741        The `defaultVerticalOrigin` argument should be the most common vertical
742        origin value. If omitted, this value will be derived from the actual
743        values in the `verticalOrigins` argument.
744        """
745        if defaultVerticalOrigin is None:
746            # find the most frequent vorg value
747            bag = {}
748            for gn in verticalOrigins:
749                vorg = verticalOrigins[gn]
750                if vorg not in bag:
751                    bag[vorg] = 1
752                else:
753                    bag[vorg] += 1
754            defaultVerticalOrigin = sorted(
755                bag, key=lambda vorg: bag[vorg], reverse=True
756            )[0]
757        self._initTableWithValues(
758            "VORG",
759            {},
760            dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin),
761        )
762        vorgTable = self.font["VORG"]
763        vorgTable.majorVersion = 1
764        vorgTable.minorVersion = 0
765        for gn in verticalOrigins:
766            vorgTable[gn] = verticalOrigins[gn]
767
768    def setupPost(self, keepGlyphNames=True, **values):
769        """Create a new `post` table and initialize it with default values,
770        which can be overridden by keyword arguments.
771        """
772        isCFF2 = "CFF2" in self.font
773        postTable = self._initTableWithValues("post", _postDefaults, values)
774        if (self.isTTF or isCFF2) and keepGlyphNames:
775            postTable.formatType = 2.0
776            postTable.extraNames = []
777            postTable.mapping = {}
778        else:
779            postTable.formatType = 3.0
780
781    def setupMaxp(self):
782        """Create a new `maxp` table. This is called implicitly by FontBuilder
783        itself and is usually not called by client code.
784        """
785        if self.isTTF:
786            defaults = _maxpDefaultsTTF
787        else:
788            defaults = _maxpDefaultsOTF
789        self._initTableWithValues("maxp", defaults, {})
790
791    def setupDummyDSIG(self):
792        """This adds an empty DSIG table to the font to make some MS applications
793        happy. This does not properly sign the font.
794        """
795        values = dict(
796            ulVersion=1,
797            usFlag=0,
798            usNumSigs=0,
799            signatureRecords=[],
800        )
801        self._initTableWithValues("DSIG", {}, values)
802
803    def addOpenTypeFeatures(self, features, filename=None, tables=None):
804        """Add OpenType features to the font from a string containing
805        Feature File syntax.
806
807        The `filename` argument is used in error messages and to determine
808        where to look for "include" files.
809
810        The optional `tables` argument can be a list of OTL tables tags to
811        build, allowing the caller to only build selected OTL tables. See
812        `fontTools.feaLib` for details.
813        """
814        from .feaLib.builder import addOpenTypeFeaturesFromString
815
816        addOpenTypeFeaturesFromString(
817            self.font, features, filename=filename, tables=tables
818        )
819
820    def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"):
821        """Add conditional substitutions to a Variable Font.
822
823        See `fontTools.varLib.featureVars.addFeatureVariations`.
824        """
825        from .varLib import featureVars
826
827        if "fvar" not in self.font:
828            raise KeyError("'fvar' table is missing; can't add FeatureVariations.")
829
830        featureVars.addFeatureVariations(
831            self.font, conditionalSubstitutions, featureTag=featureTag
832        )
833
834    def setupCOLR(
835        self,
836        colorLayers,
837        version=None,
838        varStore=None,
839        varIndexMap=None,
840        clipBoxes=None,
841        allowLayerReuse=True,
842    ):
843        """Build new COLR table using color layers dictionary.
844
845        Cf. `fontTools.colorLib.builder.buildCOLR`.
846        """
847        from fontTools.colorLib.builder import buildCOLR
848
849        glyphMap = self.font.getReverseGlyphMap()
850        self.font["COLR"] = buildCOLR(
851            colorLayers,
852            version=version,
853            glyphMap=glyphMap,
854            varStore=varStore,
855            varIndexMap=varIndexMap,
856            clipBoxes=clipBoxes,
857            allowLayerReuse=allowLayerReuse,
858        )
859
860    def setupCPAL(
861        self,
862        palettes,
863        paletteTypes=None,
864        paletteLabels=None,
865        paletteEntryLabels=None,
866    ):
867        """Build new CPAL table using list of palettes.
868
869        Optionally build CPAL v1 table using paletteTypes, paletteLabels and
870        paletteEntryLabels.
871
872        Cf. `fontTools.colorLib.builder.buildCPAL`.
873        """
874        from fontTools.colorLib.builder import buildCPAL
875
876        self.font["CPAL"] = buildCPAL(
877            palettes,
878            paletteTypes=paletteTypes,
879            paletteLabels=paletteLabels,
880            paletteEntryLabels=paletteEntryLabels,
881            nameTable=self.font.get("name"),
882        )
883
884    def setupStat(self, axes, locations=None, elidedFallbackName=2):
885        """Build a new 'STAT' table.
886
887        See `fontTools.otlLib.builder.buildStatTable` for details about
888        the arguments.
889        """
890        from .otlLib.builder import buildStatTable
891
892        buildStatTable(self.font, axes, locations, elidedFallbackName)
893
894
895def buildCmapSubTable(cmapping, format, platformID, platEncID):
896    subTable = cmap_classes[format](format)
897    subTable.cmap = cmapping
898    subTable.platformID = platformID
899    subTable.platEncID = platEncID
900    subTable.language = 0
901    return subTable
902
903
904def addFvar(font, axes, instances):
905    from .ttLib.tables._f_v_a_r import Axis, NamedInstance
906
907    assert axes
908
909    fvar = newTable("fvar")
910    nameTable = font["name"]
911
912    for axis_def in axes:
913        axis = Axis()
914
915        if isinstance(axis_def, tuple):
916            (
917                axis.axisTag,
918                axis.minValue,
919                axis.defaultValue,
920                axis.maxValue,
921                name,
922            ) = axis_def
923        else:
924            (axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue, name) = (
925                axis_def.tag,
926                axis_def.minimum,
927                axis_def.default,
928                axis_def.maximum,
929                axis_def.name,
930            )
931
932        if isinstance(name, str):
933            name = dict(en=name)
934
935        axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font)
936        fvar.axes.append(axis)
937
938    for instance in instances:
939        if isinstance(instance, dict):
940            coordinates = instance["location"]
941            name = instance["stylename"]
942            psname = instance.get("postscriptfontname")
943        else:
944            coordinates = instance.location
945            name = instance.localisedStyleName or instance.styleName
946            psname = instance.postScriptFontName
947
948        if isinstance(name, str):
949            name = dict(en=name)
950
951        inst = NamedInstance()
952        inst.subfamilyNameID = nameTable.addMultilingualName(name, ttFont=font)
953        if psname is not None:
954            inst.postscriptNameID = nameTable.addName(psname)
955        inst.coordinates = coordinates
956        fvar.instances.append(inst)
957
958    font["fvar"] = fvar
959