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