Spaces:
Running
Running
__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.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 | |