123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993 |
- __all__ = ["FontBuilder"]
- """
- This module is *experimental*, meaning it still may evolve and change.
- The `FontBuilder` class is a convenient helper to construct working TTF or
- OTF fonts from scratch.
- Note that the various setup methods cannot be called in arbitrary order,
- due to various interdependencies between OpenType tables. Here is an order
- that works:
- fb = FontBuilder(...)
- fb.setupGlyphOrder(...)
- fb.setupCharacterMap(...)
- fb.setupGlyf(...) --or-- fb.setupCFF(...)
- fb.setupHorizontalMetrics(...)
- fb.setupHorizontalHeader()
- fb.setupNameTable(...)
- fb.setupOS2()
- fb.addOpenTypeFeatures(...)
- fb.setupPost()
- fb.save(...)
- Here is how to build a minimal TTF:
- ```python
- from fontTools.fontBuilder import FontBuilder
- from fontTools.pens.ttGlyphPen import TTGlyphPen
- def drawTestGlyph(pen):
- pen.moveTo((100, 100))
- pen.lineTo((100, 1000))
- pen.qCurveTo((200, 900), (400, 900), (500, 1000))
- pen.lineTo((500, 100))
- pen.closePath()
- fb = FontBuilder(1024, isTTF=True)
- fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"])
- fb.setupCharacterMap({32: "space", 65: "A", 97: "a"})
- advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0}
- familyName = "HelloTestFont"
- styleName = "TotallyNormal"
- version = "0.1"
- nameStrings = dict(
- familyName=dict(en=familyName, nl="HalloTestFont"),
- styleName=dict(en=styleName, nl="TotaalNormaal"),
- uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName,
- fullName=familyName + "-" + styleName,
- psName=familyName + "-" + styleName,
- version="Version " + version,
- )
- pen = TTGlyphPen(None)
- drawTestGlyph(pen)
- glyph = pen.glyph()
- glyphs = {".notdef": glyph, "space": glyph, "A": glyph, "a": glyph, ".null": glyph}
- fb.setupGlyf(glyphs)
- metrics = {}
- glyphTable = fb.font["glyf"]
- for gn, advanceWidth in advanceWidths.items():
- metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
- fb.setupHorizontalMetrics(metrics)
- fb.setupHorizontalHeader(ascent=824, descent=-200)
- fb.setupNameTable(nameStrings)
- fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200)
- fb.setupPost()
- fb.save("test.ttf")
- ```
- And here's how to build a minimal OTF:
- ```python
- from fontTools.fontBuilder import FontBuilder
- from fontTools.pens.t2CharStringPen import T2CharStringPen
- def drawTestGlyph(pen):
- pen.moveTo((100, 100))
- pen.lineTo((100, 1000))
- pen.curveTo((200, 900), (400, 900), (500, 1000))
- pen.lineTo((500, 100))
- pen.closePath()
- fb = FontBuilder(1024, isTTF=False)
- fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"])
- fb.setupCharacterMap({32: "space", 65: "A", 97: "a"})
- advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0}
- familyName = "HelloTestFont"
- styleName = "TotallyNormal"
- version = "0.1"
- nameStrings = dict(
- familyName=dict(en=familyName, nl="HalloTestFont"),
- styleName=dict(en=styleName, nl="TotaalNormaal"),
- uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName,
- fullName=familyName + "-" + styleName,
- psName=familyName + "-" + styleName,
- version="Version " + version,
- )
- pen = T2CharStringPen(600, None)
- drawTestGlyph(pen)
- charString = pen.getCharString()
- charStrings = {
- ".notdef": charString,
- "space": charString,
- "A": charString,
- "a": charString,
- ".null": charString,
- }
- fb.setupCFF(nameStrings["psName"], {"FullName": nameStrings["psName"]}, charStrings, {})
- lsb = {gn: cs.calcBounds(None)[0] for gn, cs in charStrings.items()}
- metrics = {}
- for gn, advanceWidth in advanceWidths.items():
- metrics[gn] = (advanceWidth, lsb[gn])
- fb.setupHorizontalMetrics(metrics)
- fb.setupHorizontalHeader(ascent=824, descent=200)
- fb.setupNameTable(nameStrings)
- fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200)
- fb.setupPost()
- fb.save("test.otf")
- ```
- """
- from .ttLib import TTFont, newTable
- from .ttLib.tables._c_m_a_p import cmap_classes
- from .ttLib.tables._g_l_y_f import flagCubic
- from .ttLib.tables.O_S_2f_2 import Panose
- from .misc.timeTools import timestampNow
- import struct
- from collections import OrderedDict
- _headDefaults = dict(
- tableVersion=1.0,
- fontRevision=1.0,
- checkSumAdjustment=0,
- magicNumber=0x5F0F3CF5,
- flags=0x0003,
- unitsPerEm=1000,
- created=0,
- modified=0,
- xMin=0,
- yMin=0,
- xMax=0,
- yMax=0,
- macStyle=0,
- lowestRecPPEM=3,
- fontDirectionHint=2,
- indexToLocFormat=0,
- glyphDataFormat=0,
- )
- _maxpDefaultsTTF = dict(
- tableVersion=0x00010000,
- numGlyphs=0,
- maxPoints=0,
- maxContours=0,
- maxCompositePoints=0,
- maxCompositeContours=0,
- maxZones=2,
- maxTwilightPoints=0,
- maxStorage=0,
- maxFunctionDefs=0,
- maxInstructionDefs=0,
- maxStackElements=0,
- maxSizeOfInstructions=0,
- maxComponentElements=0,
- maxComponentDepth=0,
- )
- _maxpDefaultsOTF = dict(
- tableVersion=0x00005000,
- numGlyphs=0,
- )
- _postDefaults = dict(
- formatType=3.0,
- italicAngle=0,
- underlinePosition=0,
- underlineThickness=0,
- isFixedPitch=0,
- minMemType42=0,
- maxMemType42=0,
- minMemType1=0,
- maxMemType1=0,
- )
- _hheaDefaults = dict(
- tableVersion=0x00010000,
- ascent=0,
- descent=0,
- lineGap=0,
- advanceWidthMax=0,
- minLeftSideBearing=0,
- minRightSideBearing=0,
- xMaxExtent=0,
- caretSlopeRise=1,
- caretSlopeRun=0,
- caretOffset=0,
- reserved0=0,
- reserved1=0,
- reserved2=0,
- reserved3=0,
- metricDataFormat=0,
- numberOfHMetrics=0,
- )
- _vheaDefaults = dict(
- tableVersion=0x00010000,
- ascent=0,
- descent=0,
- lineGap=0,
- advanceHeightMax=0,
- minTopSideBearing=0,
- minBottomSideBearing=0,
- yMaxExtent=0,
- caretSlopeRise=0,
- caretSlopeRun=0,
- reserved0=0,
- reserved1=0,
- reserved2=0,
- reserved3=0,
- reserved4=0,
- metricDataFormat=0,
- numberOfVMetrics=0,
- )
- _nameIDs = dict(
- copyright=0,
- familyName=1,
- styleName=2,
- uniqueFontIdentifier=3,
- fullName=4,
- version=5,
- psName=6,
- trademark=7,
- manufacturer=8,
- designer=9,
- description=10,
- vendorURL=11,
- designerURL=12,
- licenseDescription=13,
- licenseInfoURL=14,
- # reserved = 15,
- typographicFamily=16,
- typographicSubfamily=17,
- compatibleFullName=18,
- sampleText=19,
- postScriptCIDFindfontName=20,
- wwsFamilyName=21,
- wwsSubfamilyName=22,
- lightBackgroundPalette=23,
- darkBackgroundPalette=24,
- variationsPostScriptNamePrefix=25,
- )
- # to insert in setupNameTable doc string:
- # print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1])))
- _panoseDefaults = Panose()
- _OS2Defaults = dict(
- version=3,
- xAvgCharWidth=0,
- usWeightClass=400,
- usWidthClass=5,
- fsType=0x0004, # default: Preview & Print embedding
- ySubscriptXSize=0,
- ySubscriptYSize=0,
- ySubscriptXOffset=0,
- ySubscriptYOffset=0,
- ySuperscriptXSize=0,
- ySuperscriptYSize=0,
- ySuperscriptXOffset=0,
- ySuperscriptYOffset=0,
- yStrikeoutSize=0,
- yStrikeoutPosition=0,
- sFamilyClass=0,
- panose=_panoseDefaults,
- ulUnicodeRange1=0,
- ulUnicodeRange2=0,
- ulUnicodeRange3=0,
- ulUnicodeRange4=0,
- achVendID="????",
- fsSelection=0,
- usFirstCharIndex=0,
- usLastCharIndex=0,
- sTypoAscender=0,
- sTypoDescender=0,
- sTypoLineGap=0,
- usWinAscent=0,
- usWinDescent=0,
- ulCodePageRange1=0,
- ulCodePageRange2=0,
- sxHeight=0,
- sCapHeight=0,
- usDefaultChar=0, # .notdef
- usBreakChar=32, # space
- usMaxContext=0,
- usLowerOpticalPointSize=0,
- usUpperOpticalPointSize=0,
- )
- class FontBuilder(object):
- def __init__(self, unitsPerEm=None, font=None, isTTF=True, glyphDataFormat=0):
- """Initialize a FontBuilder instance.
- If the `font` argument is not given, a new `TTFont` will be
- constructed, and `unitsPerEm` must be given. If `isTTF` is True,
- the font will be a glyf-based TTF; if `isTTF` is False it will be
- a CFF-based OTF.
- The `glyphDataFormat` argument corresponds to the `head` table field
- that defines the format of the TrueType `glyf` table (default=0).
- TrueType glyphs historically can only contain quadratic splines and static
- components, but there's a proposal to add support for cubic Bezier curves as well
- as variable composites/components at
- https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
- You can experiment with the new features by setting `glyphDataFormat` to 1.
- A ValueError is raised if `glyphDataFormat` is left at 0 but glyphs are added
- that contain cubic splines or varcomposites. This is to prevent accidentally
- creating fonts that are incompatible with existing TrueType implementations.
- If `font` is given, it must be a `TTFont` instance and `unitsPerEm`
- must _not_ be given. The `isTTF` and `glyphDataFormat` arguments will be ignored.
- """
- if font is None:
- self.font = TTFont(recalcTimestamp=False)
- self.isTTF = isTTF
- now = timestampNow()
- assert unitsPerEm is not None
- self.setupHead(
- unitsPerEm=unitsPerEm,
- created=now,
- modified=now,
- glyphDataFormat=glyphDataFormat,
- )
- self.setupMaxp()
- else:
- assert unitsPerEm is None
- self.font = font
- self.isTTF = "glyf" in font
- def save(self, file):
- """Save the font. The 'file' argument can be either a pathname or a
- writable file object.
- """
- self.font.save(file)
- def _initTableWithValues(self, tableTag, defaults, values):
- table = self.font[tableTag] = newTable(tableTag)
- for k, v in defaults.items():
- setattr(table, k, v)
- for k, v in values.items():
- setattr(table, k, v)
- return table
- def _updateTableWithValues(self, tableTag, values):
- table = self.font[tableTag]
- for k, v in values.items():
- setattr(table, k, v)
- def setupHead(self, **values):
- """Create a new `head` table and initialize it with default values,
- which can be overridden by keyword arguments.
- """
- self._initTableWithValues("head", _headDefaults, values)
- def updateHead(self, **values):
- """Update the head table with the fields and values passed as
- keyword arguments.
- """
- self._updateTableWithValues("head", values)
- def setupGlyphOrder(self, glyphOrder):
- """Set the glyph order for the font."""
- self.font.setGlyphOrder(glyphOrder)
- def setupCharacterMap(self, cmapping, uvs=None, allowFallback=False):
- """Build the `cmap` table for the font. The `cmapping` argument should
- be a dict mapping unicode code points as integers to glyph names.
- The `uvs` argument, when passed, must be a list of tuples, describing
- Unicode Variation Sequences. These tuples have three elements:
- (unicodeValue, variationSelector, glyphName)
- `unicodeValue` and `variationSelector` are integer code points.
- `glyphName` may be None, to indicate this is the default variation.
- Text processors will then use the cmap to find the glyph name.
- Each Unicode Variation Sequence should be an officially supported
- sequence, but this is not policed.
- """
- subTables = []
- highestUnicode = max(cmapping) if cmapping else 0
- if highestUnicode > 0xFFFF:
- cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000)
- subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10)
- subTables.append(subTable_3_10)
- else:
- cmapping_3_1 = cmapping
- format = 4
- subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
- try:
- subTable_3_1.compile(self.font)
- except struct.error:
- # format 4 overflowed, fall back to format 12
- if not allowFallback:
- raise ValueError(
- "cmap format 4 subtable overflowed; sort glyph order by unicode to fix."
- )
- format = 12
- subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
- subTables.append(subTable_3_1)
- subTable_0_3 = buildCmapSubTable(cmapping_3_1, format, 0, 3)
- subTables.append(subTable_0_3)
- if uvs is not None:
- uvsDict = {}
- for unicodeValue, variationSelector, glyphName in uvs:
- if cmapping.get(unicodeValue) == glyphName:
- # this is a default variation
- glyphName = None
- if variationSelector not in uvsDict:
- uvsDict[variationSelector] = []
- uvsDict[variationSelector].append((unicodeValue, glyphName))
- uvsSubTable = buildCmapSubTable({}, 14, 0, 5)
- uvsSubTable.uvsDict = uvsDict
- subTables.append(uvsSubTable)
- self.font["cmap"] = newTable("cmap")
- self.font["cmap"].tableVersion = 0
- self.font["cmap"].tables = subTables
- def setupNameTable(self, nameStrings, windows=True, mac=True):
- """Create the `name` table for the font. The `nameStrings` argument must
- be a dict, mapping nameIDs or descriptive names for the nameIDs to name
- record values. A value is either a string, or a dict, mapping language codes
- to strings, to allow localized name table entries.
- By default, both Windows (platformID=3) and Macintosh (platformID=1) name
- records are added, unless any of `windows` or `mac` arguments is False.
- The following descriptive names are available for nameIDs:
- copyright (nameID 0)
- familyName (nameID 1)
- styleName (nameID 2)
- uniqueFontIdentifier (nameID 3)
- fullName (nameID 4)
- version (nameID 5)
- psName (nameID 6)
- trademark (nameID 7)
- manufacturer (nameID 8)
- designer (nameID 9)
- description (nameID 10)
- vendorURL (nameID 11)
- designerURL (nameID 12)
- licenseDescription (nameID 13)
- licenseInfoURL (nameID 14)
- typographicFamily (nameID 16)
- typographicSubfamily (nameID 17)
- compatibleFullName (nameID 18)
- sampleText (nameID 19)
- postScriptCIDFindfontName (nameID 20)
- wwsFamilyName (nameID 21)
- wwsSubfamilyName (nameID 22)
- lightBackgroundPalette (nameID 23)
- darkBackgroundPalette (nameID 24)
- variationsPostScriptNamePrefix (nameID 25)
- """
- nameTable = self.font["name"] = newTable("name")
- nameTable.names = []
- for nameName, nameValue in nameStrings.items():
- if isinstance(nameName, int):
- nameID = nameName
- else:
- nameID = _nameIDs[nameName]
- if isinstance(nameValue, str):
- nameValue = dict(en=nameValue)
- nameTable.addMultilingualName(
- nameValue, ttFont=self.font, nameID=nameID, windows=windows, mac=mac
- )
- def setupOS2(self, **values):
- """Create a new `OS/2` table and initialize it with default values,
- which can be overridden by keyword arguments.
- """
- self._initTableWithValues("OS/2", _OS2Defaults, values)
- if "xAvgCharWidth" not in values:
- assert (
- "hmtx" in self.font
- ), "the 'hmtx' table must be setup before the 'OS/2' table"
- self.font["OS/2"].recalcAvgCharWidth(self.font)
- if not (
- "ulUnicodeRange1" in values
- or "ulUnicodeRange2" in values
- or "ulUnicodeRange3" in values
- or "ulUnicodeRange3" in values
- ):
- assert (
- "cmap" in self.font
- ), "the 'cmap' table must be setup before the 'OS/2' table"
- self.font["OS/2"].recalcUnicodeRanges(self.font)
- def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
- from .cffLib import (
- CFFFontSet,
- TopDictIndex,
- TopDict,
- CharStrings,
- GlobalSubrsIndex,
- PrivateDict,
- )
- assert not self.isTTF
- self.font.sfntVersion = "OTTO"
- fontSet = CFFFontSet()
- fontSet.major = 1
- fontSet.minor = 0
- fontSet.otFont = self.font
- fontSet.fontNames = [psName]
- fontSet.topDictIndex = TopDictIndex()
- globalSubrs = GlobalSubrsIndex()
- fontSet.GlobalSubrs = globalSubrs
- private = PrivateDict()
- for key, value in privateDict.items():
- setattr(private, key, value)
- fdSelect = None
- fdArray = None
- topDict = TopDict()
- topDict.charset = self.font.getGlyphOrder()
- topDict.Private = private
- topDict.GlobalSubrs = fontSet.GlobalSubrs
- for key, value in fontInfo.items():
- setattr(topDict, key, value)
- if "FontMatrix" not in fontInfo:
- scale = 1 / self.font["head"].unitsPerEm
- topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
- charStrings = CharStrings(
- None, topDict.charset, globalSubrs, private, fdSelect, fdArray
- )
- for glyphName, charString in charStringsDict.items():
- charString.private = private
- charString.globalSubrs = globalSubrs
- charStrings[glyphName] = charString
- topDict.CharStrings = charStrings
- fontSet.topDictIndex.append(topDict)
- self.font["CFF "] = newTable("CFF ")
- self.font["CFF "].cff = fontSet
- def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None):
- from .cffLib import (
- CFFFontSet,
- TopDictIndex,
- TopDict,
- CharStrings,
- GlobalSubrsIndex,
- PrivateDict,
- FDArrayIndex,
- FontDict,
- )
- assert not self.isTTF
- self.font.sfntVersion = "OTTO"
- fontSet = CFFFontSet()
- fontSet.major = 2
- fontSet.minor = 0
- cff2GetGlyphOrder = self.font.getGlyphOrder
- fontSet.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None)
- globalSubrs = GlobalSubrsIndex()
- fontSet.GlobalSubrs = globalSubrs
- if fdArrayList is None:
- fdArrayList = [{}]
- fdSelect = None
- fdArray = FDArrayIndex()
- fdArray.strings = None
- fdArray.GlobalSubrs = globalSubrs
- for privateDict in fdArrayList:
- fontDict = FontDict()
- fontDict.setCFF2(True)
- private = PrivateDict()
- for key, value in privateDict.items():
- setattr(private, key, value)
- fontDict.Private = private
- fdArray.append(fontDict)
- topDict = TopDict()
- topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
- topDict.FDArray = fdArray
- scale = 1 / self.font["head"].unitsPerEm
- topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
- private = fdArray[0].Private
- charStrings = CharStrings(None, None, globalSubrs, private, fdSelect, fdArray)
- for glyphName, charString in charStringsDict.items():
- charString.private = private
- charString.globalSubrs = globalSubrs
- charStrings[glyphName] = charString
- topDict.CharStrings = charStrings
- fontSet.topDictIndex.append(topDict)
- self.font["CFF2"] = newTable("CFF2")
- self.font["CFF2"].cff = fontSet
- if regions:
- self.setupCFF2Regions(regions)
- def setupCFF2Regions(self, regions):
- from .varLib.builder import buildVarRegionList, buildVarData, buildVarStore
- from .cffLib import VarStoreData
- assert "fvar" in self.font, "fvar must to be set up first"
- assert "CFF2" in self.font, "CFF2 must to be set up first"
- axisTags = [a.axisTag for a in self.font["fvar"].axes]
- varRegionList = buildVarRegionList(regions, axisTags)
- varData = buildVarData(list(range(len(regions))), None, optimize=False)
- varStore = buildVarStore(varRegionList, [varData])
- vstore = VarStoreData(otVarStore=varStore)
- topDict = self.font["CFF2"].cff.topDictIndex[0]
- topDict.VarStore = vstore
- for fontDict in topDict.FDArray:
- fontDict.Private.vstore = vstore
- def setupGlyf(self, glyphs, calcGlyphBounds=True, validateGlyphFormat=True):
- """Create the `glyf` table from a dict, that maps glyph names
- to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example
- as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`.
- If `calcGlyphBounds` is True, the bounds of all glyphs will be
- calculated. Only pass False if your glyph objects already have
- their bounding box values set.
- If `validateGlyphFormat` is True, raise ValueError if any of the glyphs contains
- cubic curves or is a variable composite but head.glyphDataFormat=0.
- Set it to False to skip the check if you know in advance all the glyphs are
- compatible with the specified glyphDataFormat.
- """
- assert self.isTTF
- if validateGlyphFormat and self.font["head"].glyphDataFormat == 0:
- for name, g in glyphs.items():
- if g.isVarComposite():
- raise ValueError(
- f"Glyph {name!r} is a variable composite, but glyphDataFormat=0"
- )
- elif g.numberOfContours > 0 and any(f & flagCubic for f in g.flags):
- raise ValueError(
- f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; "
- "either convert to quadratics with cu2qu or set glyphDataFormat=1."
- )
- self.font["loca"] = newTable("loca")
- self.font["glyf"] = newTable("glyf")
- self.font["glyf"].glyphs = glyphs
- if hasattr(self.font, "glyphOrder"):
- self.font["glyf"].glyphOrder = self.font.glyphOrder
- if calcGlyphBounds:
- self.calcGlyphBounds()
- def setupFvar(self, axes, instances):
- """Adds an font variations table to the font.
- Args:
- axes (list): See below.
- instances (list): See below.
- ``axes`` should be a list of axes, with each axis either supplied as
- a py:class:`.designspaceLib.AxisDescriptor` object, or a tuple in the
- format ```tupletag, minValue, defaultValue, maxValue, name``.
- The ``name`` is either a string, or a dict, mapping language codes
- to strings, to allow localized name table entries.
- ```instances`` should be a list of instances, with each instance either
- supplied as a py:class:`.designspaceLib.InstanceDescriptor` object, or a
- dict with keys ``location`` (mapping of axis tags to float values),
- ``stylename`` and (optionally) ``postscriptfontname``.
- The ``stylename`` is either a string, or a dict, mapping language codes
- to strings, to allow localized name table entries.
- """
- addFvar(self.font, axes, instances)
- def setupAvar(self, axes, mappings=None):
- """Adds an axis variations table to the font.
- Args:
- axes (list): A list of py:class:`.designspaceLib.AxisDescriptor` objects.
- """
- from .varLib import _add_avar
- if "fvar" not in self.font:
- raise KeyError("'fvar' table is missing; can't add 'avar'.")
- axisTags = [axis.axisTag for axis in self.font["fvar"].axes]
- axes = OrderedDict(enumerate(axes)) # Only values are used
- _add_avar(self.font, axes, mappings, axisTags)
- def setupGvar(self, variations):
- gvar = self.font["gvar"] = newTable("gvar")
- gvar.version = 1
- gvar.reserved = 0
- gvar.variations = variations
- def calcGlyphBounds(self):
- """Calculate the bounding boxes of all glyphs in the `glyf` table.
- This is usually not called explicitly by client code.
- """
- glyphTable = self.font["glyf"]
- for glyph in glyphTable.glyphs.values():
- glyph.recalcBounds(glyphTable)
- def setupHorizontalMetrics(self, metrics):
- """Create a new `hmtx` table, for horizontal metrics.
- The `metrics` argument must be a dict, mapping glyph names to
- `(width, leftSidebearing)` tuples.
- """
- self.setupMetrics("hmtx", metrics)
- def setupVerticalMetrics(self, metrics):
- """Create a new `vmtx` table, for horizontal metrics.
- The `metrics` argument must be a dict, mapping glyph names to
- `(height, topSidebearing)` tuples.
- """
- self.setupMetrics("vmtx", metrics)
- def setupMetrics(self, tableTag, metrics):
- """See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
- assert tableTag in ("hmtx", "vmtx")
- mtxTable = self.font[tableTag] = newTable(tableTag)
- roundedMetrics = {}
- for gn in metrics:
- w, lsb = metrics[gn]
- roundedMetrics[gn] = int(round(w)), int(round(lsb))
- mtxTable.metrics = roundedMetrics
- def setupHorizontalHeader(self, **values):
- """Create a new `hhea` table initialize it with default values,
- which can be overridden by keyword arguments.
- """
- self._initTableWithValues("hhea", _hheaDefaults, values)
- def setupVerticalHeader(self, **values):
- """Create a new `vhea` table initialize it with default values,
- which can be overridden by keyword arguments.
- """
- self._initTableWithValues("vhea", _vheaDefaults, values)
- def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None):
- """Create a new `VORG` table. The `verticalOrigins` argument must be
- a dict, mapping glyph names to vertical origin values.
- The `defaultVerticalOrigin` argument should be the most common vertical
- origin value. If omitted, this value will be derived from the actual
- values in the `verticalOrigins` argument.
- """
- if defaultVerticalOrigin is None:
- # find the most frequent vorg value
- bag = {}
- for gn in verticalOrigins:
- vorg = verticalOrigins[gn]
- if vorg not in bag:
- bag[vorg] = 1
- else:
- bag[vorg] += 1
- defaultVerticalOrigin = sorted(
- bag, key=lambda vorg: bag[vorg], reverse=True
- )[0]
- self._initTableWithValues(
- "VORG",
- {},
- dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin),
- )
- vorgTable = self.font["VORG"]
- vorgTable.majorVersion = 1
- vorgTable.minorVersion = 0
- for gn in verticalOrigins:
- vorgTable[gn] = verticalOrigins[gn]
- def setupPost(self, keepGlyphNames=True, **values):
- """Create a new `post` table and initialize it with default values,
- which can be overridden by keyword arguments.
- """
- isCFF2 = "CFF2" in self.font
- postTable = self._initTableWithValues("post", _postDefaults, values)
- if (self.isTTF or isCFF2) and keepGlyphNames:
- postTable.formatType = 2.0
- postTable.extraNames = []
- postTable.mapping = {}
- else:
- postTable.formatType = 3.0
- def setupMaxp(self):
- """Create a new `maxp` table. This is called implicitly by FontBuilder
- itself and is usually not called by client code.
- """
- if self.isTTF:
- defaults = _maxpDefaultsTTF
- else:
- defaults = _maxpDefaultsOTF
- self._initTableWithValues("maxp", defaults, {})
- def setupDummyDSIG(self):
- """This adds an empty DSIG table to the font to make some MS applications
- happy. This does not properly sign the font.
- """
- values = dict(
- ulVersion=1,
- usFlag=0,
- usNumSigs=0,
- signatureRecords=[],
- )
- self._initTableWithValues("DSIG", {}, values)
- def addOpenTypeFeatures(self, features, filename=None, tables=None, debug=False):
- """Add OpenType features to the font from a string containing
- Feature File syntax.
- The `filename` argument is used in error messages and to determine
- where to look for "include" files.
- The optional `tables` argument can be a list of OTL tables tags to
- build, allowing the caller to only build selected OTL tables. See
- `fontTools.feaLib` for details.
- The optional `debug` argument controls whether to add source debugging
- information to the font in the `Debg` table.
- """
- from .feaLib.builder import addOpenTypeFeaturesFromString
- addOpenTypeFeaturesFromString(
- self.font, features, filename=filename, tables=tables, debug=debug
- )
- def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"):
- """Add conditional substitutions to a Variable Font.
- See `fontTools.varLib.featureVars.addFeatureVariations`.
- """
- from .varLib import featureVars
- if "fvar" not in self.font:
- raise KeyError("'fvar' table is missing; can't add FeatureVariations.")
- featureVars.addFeatureVariations(
- self.font, conditionalSubstitutions, featureTag=featureTag
- )
- def setupCOLR(
- self,
- colorLayers,
- version=None,
- varStore=None,
- varIndexMap=None,
- clipBoxes=None,
- allowLayerReuse=True,
- ):
- """Build new COLR table using color layers dictionary.
- Cf. `fontTools.colorLib.builder.buildCOLR`.
- """
- from fontTools.colorLib.builder import buildCOLR
- glyphMap = self.font.getReverseGlyphMap()
- self.font["COLR"] = buildCOLR(
- colorLayers,
- version=version,
- glyphMap=glyphMap,
- varStore=varStore,
- varIndexMap=varIndexMap,
- clipBoxes=clipBoxes,
- allowLayerReuse=allowLayerReuse,
- )
- def setupCPAL(
- self,
- palettes,
- paletteTypes=None,
- paletteLabels=None,
- paletteEntryLabels=None,
- ):
- """Build new CPAL table using list of palettes.
- Optionally build CPAL v1 table using paletteTypes, paletteLabels and
- paletteEntryLabels.
- Cf. `fontTools.colorLib.builder.buildCPAL`.
- """
- from fontTools.colorLib.builder import buildCPAL
- self.font["CPAL"] = buildCPAL(
- palettes,
- paletteTypes=paletteTypes,
- paletteLabels=paletteLabels,
- paletteEntryLabels=paletteEntryLabels,
- nameTable=self.font.get("name"),
- )
- def setupStat(self, axes, locations=None, elidedFallbackName=2):
- """Build a new 'STAT' table.
- See `fontTools.otlLib.builder.buildStatTable` for details about
- the arguments.
- """
- from .otlLib.builder import buildStatTable
- buildStatTable(self.font, axes, locations, elidedFallbackName)
- def buildCmapSubTable(cmapping, format, platformID, platEncID):
- subTable = cmap_classes[format](format)
- subTable.cmap = cmapping
- subTable.platformID = platformID
- subTable.platEncID = platEncID
- subTable.language = 0
- return subTable
- def addFvar(font, axes, instances):
- from .ttLib.tables._f_v_a_r import Axis, NamedInstance
- assert axes
- fvar = newTable("fvar")
- nameTable = font["name"]
- for axis_def in axes:
- axis = Axis()
- if isinstance(axis_def, tuple):
- (
- axis.axisTag,
- axis.minValue,
- axis.defaultValue,
- axis.maxValue,
- name,
- ) = axis_def
- else:
- (axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue, name) = (
- axis_def.tag,
- axis_def.minimum,
- axis_def.default,
- axis_def.maximum,
- axis_def.name,
- )
- if axis_def.hidden:
- axis.flags = 0x0001 # HIDDEN_AXIS
- if isinstance(name, str):
- name = dict(en=name)
- axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font)
- fvar.axes.append(axis)
- for instance in instances:
- if isinstance(instance, dict):
- coordinates = instance["location"]
- name = instance["stylename"]
- psname = instance.get("postscriptfontname")
- else:
- coordinates = instance.location
- name = instance.localisedStyleName or instance.styleName
- psname = instance.postScriptFontName
- if isinstance(name, str):
- name = dict(en=name)
- inst = NamedInstance()
- inst.subfamilyNameID = nameTable.addMultilingualName(name, ttFont=font)
- if psname is not None:
- inst.postscriptNameID = nameTable.addName(psname)
- inst.coordinates = coordinates
- fvar.instances.append(inst)
- font["fvar"] = fvar
|