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