2018-10-31 20:54:34 +01:00
|
|
|
from __future__ import print_function, division, absolute_import
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
2018-11-01 10:28:05 +01:00
|
|
|
__all__ = ["FontBuilder"]
|
|
|
|
|
2018-11-01 10:19:51 +01:00
|
|
|
"""
|
2018-11-01 12:32:17 +01:00
|
|
|
This module is *experimental*, meaning it still may evolve and change.
|
|
|
|
|
2018-11-01 12:37:37 +01:00
|
|
|
The `FontBuilder` class is a convenient helper to construct working TTF or
|
|
|
|
OTF fonts from scratch.
|
2018-11-01 10:19:51 +01:00
|
|
|
|
2018-11-01 12:37:37 +01:00
|
|
|
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(...)
|
2018-11-01 20:30:21 +01:00
|
|
|
fb.setupHorizontalMetrics(...)
|
2018-11-01 12:37:37 +01:00
|
|
|
fb.setupHorizontalHeader()
|
|
|
|
fb.setupNameTable(...)
|
|
|
|
fb.setupOS2()
|
|
|
|
fb.setupPost()
|
|
|
|
fb.save(...)
|
2018-11-01 12:21:38 +01:00
|
|
|
|
2018-11-01 10:19:51 +01:00
|
|
|
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", "A", "a"])
|
|
|
|
fb.setupCharacterMap({65: "A", 97: "a"})
|
|
|
|
|
|
|
|
advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}
|
|
|
|
|
|
|
|
familyName = "HelloTestFont"
|
|
|
|
styleName = "TotallyNormal"
|
|
|
|
nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
|
|
|
|
styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
|
|
|
|
nameStrings['psName'] = familyName + "-" + styleName
|
|
|
|
|
|
|
|
pen = TTGlyphPen(None)
|
|
|
|
drawTestGlyph(pen)
|
|
|
|
glyph = pen.glyph()
|
|
|
|
glyphs = {".notdef": 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)
|
2018-11-01 20:30:21 +01:00
|
|
|
fb.setupHorizontalMetrics(metrics)
|
2018-11-01 10:19:51 +01:00
|
|
|
|
|
|
|
fb.setupHorizontalHeader()
|
|
|
|
fb.setupNameTable(nameStrings)
|
|
|
|
fb.setupOS2()
|
|
|
|
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", "A", "a"])
|
|
|
|
fb.setupCharacterMap({65: "A", 97: "a"})
|
|
|
|
|
|
|
|
advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}
|
|
|
|
|
|
|
|
familyName = "HelloTestFont"
|
|
|
|
styleName = "TotallyNormal"
|
|
|
|
nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
|
|
|
|
styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
|
|
|
|
nameStrings['psName'] = familyName + "-" + styleName
|
|
|
|
|
|
|
|
pen = T2CharStringPen(600, None)
|
|
|
|
drawTestGlyph(pen)
|
|
|
|
charString = pen.getCharString()
|
|
|
|
charStrings = {".notdef": charString, "A": charString, "a": charString, ".null": charString}
|
|
|
|
fb.setupCFF(nameStrings['psName'], {"FullName": nameStrings['psName']}, charStrings, {})
|
|
|
|
|
|
|
|
metrics = {}
|
|
|
|
for gn, advanceWidth in advanceWidths.items():
|
|
|
|
metrics[gn] = (advanceWidth, 100) # XXX lsb from glyph
|
2018-11-01 20:30:21 +01:00
|
|
|
fb.setupHorizontalMetrics(metrics)
|
2018-11-01 10:19:51 +01:00
|
|
|
|
|
|
|
fb.setupHorizontalHeader()
|
|
|
|
fb.setupNameTable(nameStrings)
|
|
|
|
fb.setupOS2()
|
|
|
|
fb.setupPost()
|
|
|
|
|
|
|
|
fb.save("test.otf")
|
|
|
|
```
|
|
|
|
"""
|
|
|
|
|
2018-11-01 09:44:56 +01:00
|
|
|
from .misc.py23 import *
|
2018-10-31 20:54:34 +01:00
|
|
|
from .ttLib import TTFont, newTable
|
|
|
|
from .ttLib.tables._c_m_a_p import cmap_classes
|
2018-11-01 09:36:37 +01:00
|
|
|
from .ttLib.tables._n_a_m_e import NameRecord, makeName
|
2018-10-31 20:54:34 +01:00
|
|
|
from .misc.timeTools import timestampNow
|
|
|
|
import struct
|
|
|
|
|
|
|
|
|
|
|
|
_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(
|
2018-11-01 20:30:21 +01:00
|
|
|
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,
|
2018-10-31 20:54:34 +01:00
|
|
|
)
|
|
|
|
|
2018-11-01 20:30:21 +01:00
|
|
|
# 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])))
|
|
|
|
|
2018-10-31 20:54:34 +01:00
|
|
|
_panoseDefaults = dict(
|
|
|
|
bFamilyType = 0,
|
|
|
|
bSerifStyle = 0,
|
|
|
|
bWeight = 0,
|
|
|
|
bProportion = 0,
|
|
|
|
bContrast = 0,
|
|
|
|
bStrokeVariation = 0,
|
|
|
|
bArmStyle = 0,
|
|
|
|
bLetterForm = 0,
|
|
|
|
bMidline = 0,
|
|
|
|
bXHeight = 0,
|
|
|
|
)
|
|
|
|
|
|
|
|
_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 = 2, # just kerning
|
|
|
|
usLowerOpticalPointSize = 0,
|
|
|
|
usUpperOpticalPointSize = 0,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class FontBuilder(object):
|
|
|
|
|
|
|
|
def __init__(self, unitsPerEm=None, font=None, isTTF=True):
|
2018-11-01 10:17:07 +01:00
|
|
|
"""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.
|
|
|
|
|
|
|
|
If `font` is given, it must be a `TTFont` instance and `unitsPerEm`
|
|
|
|
must _not_ be given. The `isTTF` argument will be ignored.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
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)
|
|
|
|
self.setupMaxp()
|
|
|
|
else:
|
|
|
|
assert unitsPerEm is None
|
|
|
|
self.font = font
|
|
|
|
self.isTTF = "glyf" in font
|
|
|
|
|
2018-11-01 20:30:21 +01:00
|
|
|
def save(self, file):
|
|
|
|
"""Save the font. The 'file' argument can be either a pathname or a
|
|
|
|
writable file object.
|
|
|
|
"""
|
|
|
|
self.font.save(file)
|
2018-10-31 20:54:34 +01:00
|
|
|
|
|
|
|
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):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Create a new `head` table and initialize it with default values,
|
|
|
|
which can be overridden by keyword arguments.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
self._initTableWithValues("head", _headDefaults, values)
|
|
|
|
|
|
|
|
def updateHead(self, **values):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Update the head table with the fields and values passed as
|
|
|
|
keyword arguments.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
self._updateTableWithValues("head", values)
|
|
|
|
|
|
|
|
def setupGlyphOrder(self, glyphOrder):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Set the glyph order for the font."""
|
2018-10-31 20:54:34 +01:00
|
|
|
self.font.setGlyphOrder(glyphOrder)
|
|
|
|
|
|
|
|
def setupCharacterMap(self, cmapping, allowFallback=False):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Build the `cmap` table for the font. The `cmapping` argument should
|
|
|
|
be a dict mapping unicode code points as integers to glyph names.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
subTables = []
|
|
|
|
highestUnicode = max(cmapping)
|
|
|
|
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)
|
|
|
|
|
|
|
|
self.font["cmap"] = newTable("cmap")
|
|
|
|
self.font["cmap"].tableVersion = 0
|
|
|
|
self.font["cmap"].tables = subTables
|
|
|
|
|
|
|
|
def setupNameTable(self, nameStrings):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""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.
|
|
|
|
|
|
|
|
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)
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
nameTable = self.font["name"] = newTable("name")
|
|
|
|
nameTable.names = []
|
|
|
|
|
|
|
|
for nameName, nameValue in nameStrings.items():
|
|
|
|
if isinstance(nameName, int):
|
|
|
|
nameID = nameName
|
|
|
|
else:
|
|
|
|
nameID = _nameIDs[nameName]
|
2018-11-01 09:52:06 +01:00
|
|
|
if isinstance(nameValue, basestring):
|
|
|
|
nameValue = dict(en=nameValue)
|
|
|
|
nameTable.addMultilingualName(nameValue, ttFont=self.font, nameID=nameID)
|
2018-10-31 20:54:34 +01:00
|
|
|
|
|
|
|
def setupOS2(self, **values):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Create a new `OS/2` table and initialize it with default values,
|
|
|
|
which can be overridden by keyword arguments.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
if "xAvgCharWidth" not in values:
|
|
|
|
gs = self.font.getGlyphSet()
|
|
|
|
widths = [gs[glyphName].width for glyphName in gs.keys() if gs[glyphName].width > 0]
|
|
|
|
values["xAvgCharWidth"] = int(round(sum(widths) / float(len(widths))))
|
|
|
|
self._initTableWithValues("OS/2", _OS2Defaults, values)
|
|
|
|
if not ("ulUnicodeRange1" in values or "ulUnicodeRange2" in values or
|
|
|
|
"ulUnicodeRange3" in values or "ulUnicodeRange3" in values):
|
2018-11-01 12:13:35 +01:00
|
|
|
assert "cmap" in self.font, "the 'cmap' table must be setup before the 'OS/2' table"
|
2018-10-31 20:54:34 +01:00
|
|
|
self.font["OS/2"].recalcUnicodeRanges(self.font)
|
|
|
|
|
|
|
|
def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
|
|
|
|
assert not self.isTTF
|
|
|
|
from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \
|
|
|
|
GlobalSubrsIndex, PrivateDict
|
|
|
|
self.font.sfntVersion = "OTTO"
|
|
|
|
fontSet = CFFFontSet()
|
|
|
|
fontSet.major = 1
|
|
|
|
fontSet.minor = 0
|
|
|
|
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
|
|
|
|
for key, value in fontInfo.items():
|
|
|
|
setattr(topDict, key, value)
|
|
|
|
|
|
|
|
charStrings = CharStrings(None, topDict.charset, globalSubrs, private, fdSelect, fdArray)
|
|
|
|
for glypnName, charString in charStringsDict.items():
|
|
|
|
charString.private = private
|
|
|
|
charString.globalSubrs = globalSubrs
|
|
|
|
charStrings[glypnName] = charString
|
|
|
|
topDict.CharStrings = charStrings
|
|
|
|
|
|
|
|
fontSet.topDictIndex.append(topDict)
|
|
|
|
|
|
|
|
self.font["CFF "] = newTable("CFF ")
|
|
|
|
self.font["CFF "].cff = fontSet
|
|
|
|
|
2018-11-01 10:17:07 +01:00
|
|
|
def setupGlyf(self, glyphs, calcGlyphBounds=True):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
assert self.isTTF
|
|
|
|
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
|
2018-11-01 10:17:07 +01:00
|
|
|
if calcGlyphBounds:
|
|
|
|
self.calcGlyphBounds()
|
2018-10-31 20:54:34 +01:00
|
|
|
|
|
|
|
def setupFvar(self, axes, instances):
|
|
|
|
addFvar(self.font, axes, instances)
|
|
|
|
|
|
|
|
def setupGvar(self, variations):
|
|
|
|
gvar = self.font["gvar"] = newTable('gvar')
|
|
|
|
gvar.version = 1
|
|
|
|
gvar.reserved = 0
|
|
|
|
gvar.variations = variations
|
|
|
|
|
|
|
|
def calcGlyphBounds(self):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Calculate the bounding boxes of all glyphs in the `glyf` table.
|
|
|
|
This is usually not called explicitly by client code.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
glyphTable = self.font["glyf"]
|
|
|
|
for glyph in glyphTable.glyphs.values():
|
|
|
|
glyph.recalcBounds(glyphTable)
|
|
|
|
|
2018-11-01 20:30:21 +01:00
|
|
|
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)
|
|
|
|
|
2018-10-31 20:54:34 +01:00
|
|
|
def setupMetrics(self, tableTag, metrics):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
|
2018-10-31 20:54:34 +01:00
|
|
|
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):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Create a new `hhea` table initialize it with default values,
|
|
|
|
which can be overridden by keyword arguments.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
self._initTableWithValues("hhea", _hheaDefaults, values)
|
|
|
|
|
|
|
|
def setupVerticalHeader(self, **values):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Create a new `vhea` table initialize it with default values,
|
|
|
|
which can be overridden by keyword arguments.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
self._initTableWithValues("vhea", _vheaDefaults, values)
|
|
|
|
|
|
|
|
def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
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):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Create a new `post` table and initialize it with default values,
|
|
|
|
which can be overridden by keyword arguments.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
postTable = self._initTableWithValues("post", _postDefaults, values)
|
|
|
|
if self.isTTF and keepGlyphNames:
|
|
|
|
postTable.formatType = 2.0
|
|
|
|
postTable.extraNames = []
|
|
|
|
postTable.mapping = {}
|
|
|
|
else:
|
|
|
|
postTable.formatType = 3.0
|
|
|
|
|
|
|
|
def setupMaxp(self):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""Create a new `maxp` table. This is called implicitly by FontBuilder
|
|
|
|
itself and is usually not called by client code.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
if self.isTTF:
|
|
|
|
defaults = _maxpDefaultsTTF
|
|
|
|
else:
|
|
|
|
defaults = _maxpDefaultsOTF
|
|
|
|
self._initTableWithValues("maxp", defaults, {})
|
|
|
|
|
2018-11-01 13:19:27 +01:00
|
|
|
def setupDummyDSIG(self):
|
|
|
|
"""This adds a dummy DSIG table to the font to make some MS applications
|
|
|
|
happy. This does not properly sign the font.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
from .ttLib.tables.D_S_I_G_ import SignatureRecord
|
|
|
|
|
|
|
|
sig = SignatureRecord()
|
|
|
|
sig.ulLength = 20
|
|
|
|
sig.cbSignature = 12
|
|
|
|
sig.usReserved2 = 0
|
|
|
|
sig.usReserved1 = 0
|
2018-11-01 12:13:35 +01:00
|
|
|
sig.pkcs7 = b'\xd3M4\xd3M5\xd3M4\xd3M4'
|
2018-10-31 20:54:34 +01:00
|
|
|
sig.ulFormat = 1
|
|
|
|
sig.ulOffset = 20
|
|
|
|
|
|
|
|
values = dict(
|
|
|
|
ulVersion = 1,
|
|
|
|
usFlag = 1,
|
|
|
|
usNumSigs = 1,
|
|
|
|
signatureRecords = [sig],
|
|
|
|
)
|
|
|
|
self._initTableWithValues("DSIG", {}, values)
|
|
|
|
|
2018-10-31 21:01:00 +01:00
|
|
|
def addOpenTypeFeatures(self, features, filename=None, tables=None):
|
2018-11-01 20:30:21 +01:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-10-31 20:54:34 +01:00
|
|
|
from .feaLib.builder import addOpenTypeFeaturesFromString
|
2018-10-31 21:01:00 +01:00
|
|
|
addOpenTypeFeaturesFromString(self.font, features, filename=filename, tables=tables)
|
2018-10-31 20:54:34 +01:00
|
|
|
|
|
|
|
|
|
|
|
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 .misc.py23 import Tag, tounicode
|
|
|
|
from .ttLib.tables._f_v_a_r import Axis, NamedInstance
|
|
|
|
|
|
|
|
assert axes
|
|
|
|
|
|
|
|
fvar = newTable('fvar')
|
|
|
|
nameTable = font['name']
|
|
|
|
|
|
|
|
for tag, minValue, defaultValue, maxValue, name in axes:
|
|
|
|
axis = Axis()
|
|
|
|
axis.axisTag = Tag(tag)
|
|
|
|
axis.minValue, axis.defaultValue, axis.maxValue = minValue, defaultValue, maxValue
|
|
|
|
axis.axisNameID = nameTable.addName(tounicode(name))
|
|
|
|
fvar.axes.append(axis)
|
|
|
|
|
|
|
|
for instance in instances:
|
|
|
|
coordinates = instance['location']
|
|
|
|
name = tounicode(instance['stylename'])
|
|
|
|
psname = instance.get('postscriptfontname')
|
|
|
|
|
|
|
|
inst = NamedInstance()
|
|
|
|
inst.subfamilyNameID = nameTable.addName(name)
|
|
|
|
if psname is not None:
|
|
|
|
psname = tounicode(psname)
|
|
|
|
inst.postscriptNameID = nameTable.addName(psname)
|
|
|
|
inst.coordinates = coordinates
|
|
|
|
fvar.instances.append(inst)
|
|
|
|
|
|
|
|
font['fvar'] = fvar
|