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