libraries
This commit is contained in:
989
.venv/lib/python3.12/site-packages/fontTools/fontBuilder.py
Normal file
989
.venv/lib/python3.12/site-packages/fontTools/fontBuilder.py
Normal file
@@ -0,0 +1,989 @@
|
||||
__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
|
||||
Reference in New Issue
Block a user