994 lines
33 KiB
Python
994 lines
33 KiB
Python
__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
|