468 lines
13 KiB
Python
468 lines
13 KiB
Python
import os
|
|
import pytest
|
|
from fontTools.designspaceLib import AxisDescriptor
|
|
from fontTools.ttLib import TTFont
|
|
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
|
from fontTools.pens.t2CharStringPen import T2CharStringPen
|
|
from fontTools.fontBuilder import FontBuilder
|
|
from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
|
from fontTools.misc.psCharStrings import T2CharString
|
|
from fontTools.misc.testTools import stripVariableItemsFromTTX
|
|
|
|
|
|
def getTestData(fileName, mode="r"):
|
|
path = os.path.join(os.path.dirname(__file__), "data", fileName)
|
|
with open(path, mode) as f:
|
|
return f.read()
|
|
|
|
|
|
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()
|
|
|
|
|
|
def _setupFontBuilder(isTTF, unitsPerEm=1024):
|
|
fb = FontBuilder(unitsPerEm, isTTF=isTTF)
|
|
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
|
|
|
|
return fb, advanceWidths, nameStrings
|
|
|
|
|
|
def _setupFontBuilderFvar(fb):
|
|
assert "name" in fb.font, "Must run setupNameTable() first."
|
|
|
|
testAxis = AxisDescriptor()
|
|
testAxis.name = "Test Axis"
|
|
testAxis.tag = "TEST"
|
|
testAxis.minimum = 0
|
|
testAxis.default = 0
|
|
testAxis.maximum = 100
|
|
testAxis.map = [(0, 0), (40, 60), (100, 100)]
|
|
axes = [testAxis]
|
|
instances = [
|
|
dict(location=dict(TEST=0), stylename="TotallyNormal"),
|
|
dict(location=dict(TEST=100), stylename="TotallyTested"),
|
|
]
|
|
fb.setupFvar(axes, instances)
|
|
fb.setupAvar(axes)
|
|
|
|
return fb
|
|
|
|
|
|
def _setupFontBuilderCFF2(fb):
|
|
assert "fvar" in fb.font, "Must run _setupFontBuilderFvar() first."
|
|
|
|
pen = T2CharStringPen(None, None, CFF2=True)
|
|
drawTestGlyph(pen)
|
|
charString = pen.getCharString()
|
|
|
|
program = [
|
|
200,
|
|
200,
|
|
-200,
|
|
-200,
|
|
2,
|
|
"blend",
|
|
"rmoveto",
|
|
400,
|
|
400,
|
|
1,
|
|
"blend",
|
|
"hlineto",
|
|
400,
|
|
400,
|
|
1,
|
|
"blend",
|
|
"vlineto",
|
|
-400,
|
|
-400,
|
|
1,
|
|
"blend",
|
|
"hlineto",
|
|
]
|
|
charStringVariable = T2CharString(program=program)
|
|
|
|
charStrings = {
|
|
".notdef": charString,
|
|
"A": charString,
|
|
"a": charStringVariable,
|
|
".null": charString,
|
|
}
|
|
fb.setupCFF2(charStrings, regions=[{"TEST": (0, 1, 1)}])
|
|
|
|
return fb
|
|
|
|
|
|
def _verifyOutput(outPath, tables=None):
|
|
f = TTFont(outPath)
|
|
f.saveXML(outPath + ".ttx", tables=tables)
|
|
with open(outPath + ".ttx") as f:
|
|
testData = stripVariableItemsFromTTX(f.read())
|
|
refData = stripVariableItemsFromTTX(getTestData(os.path.basename(outPath) + ".ttx"))
|
|
assert refData == testData
|
|
|
|
|
|
def test_build_ttf(tmpdir):
|
|
outPath = os.path.join(str(tmpdir), "test.ttf")
|
|
|
|
fb, advanceWidths, nameStrings = _setupFontBuilder(True)
|
|
|
|
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)
|
|
fb.setupHorizontalMetrics(metrics)
|
|
|
|
fb.setupHorizontalHeader(ascent=824, descent=200)
|
|
fb.setupNameTable(nameStrings)
|
|
fb.setupOS2()
|
|
fb.addOpenTypeFeatures("feature salt { sub A by a; } salt;")
|
|
fb.setupPost()
|
|
fb.setupDummyDSIG()
|
|
|
|
fb.save(outPath)
|
|
|
|
_verifyOutput(outPath)
|
|
|
|
|
|
def test_build_cubic_ttf(tmp_path):
|
|
pen = TTGlyphPen(None)
|
|
pen.moveTo((100, 100))
|
|
pen.curveTo((200, 200), (300, 300), (400, 400))
|
|
pen.closePath()
|
|
glyph = pen.glyph()
|
|
glyphs = {"A": glyph}
|
|
|
|
# cubic outlines are not allowed in glyf table format 0
|
|
fb = FontBuilder(1000, isTTF=True, glyphDataFormat=0)
|
|
with pytest.raises(
|
|
ValueError, match="Glyph 'A' has cubic Bezier outlines, but glyphDataFormat=0"
|
|
):
|
|
fb.setupGlyf(glyphs)
|
|
# can skip check if feeling adventurous
|
|
fb.setupGlyf(glyphs, validateGlyphFormat=False)
|
|
|
|
# cubics are (will be) allowed in glyf table format 1
|
|
fb = FontBuilder(1000, isTTF=True, glyphDataFormat=1)
|
|
fb.setupGlyf(glyphs)
|
|
assert "A" in fb.font["glyf"].glyphs
|
|
|
|
|
|
def test_build_otf(tmpdir):
|
|
outPath = os.path.join(str(tmpdir), "test.otf")
|
|
|
|
fb, advanceWidths, nameStrings = _setupFontBuilder(False)
|
|
|
|
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, {}
|
|
)
|
|
|
|
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()
|
|
fb.addOpenTypeFeatures("feature kern { pos A a -50; } kern;")
|
|
fb.setupPost()
|
|
fb.setupDummyDSIG()
|
|
|
|
fb.save(outPath)
|
|
|
|
_verifyOutput(outPath)
|
|
|
|
|
|
def test_build_var(tmpdir):
|
|
outPath = os.path.join(str(tmpdir), "test_var.ttf")
|
|
|
|
fb, advanceWidths, nameStrings = _setupFontBuilder(True)
|
|
|
|
pen = TTGlyphPen(None)
|
|
pen.moveTo((100, 0))
|
|
pen.lineTo((100, 400))
|
|
pen.lineTo((500, 400))
|
|
pen.lineTo((500, 000))
|
|
pen.closePath()
|
|
glyph1 = pen.glyph()
|
|
|
|
pen = TTGlyphPen(None)
|
|
pen.moveTo((50, 0))
|
|
pen.lineTo((50, 200))
|
|
pen.lineTo((250, 200))
|
|
pen.lineTo((250, 0))
|
|
pen.closePath()
|
|
glyph2 = pen.glyph()
|
|
|
|
pen = TTGlyphPen(None)
|
|
emptyGlyph = pen.glyph()
|
|
|
|
glyphs = {".notdef": emptyGlyph, "A": glyph1, "a": glyph2, ".null": emptyGlyph}
|
|
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)
|
|
|
|
axes = [
|
|
("LEFT", 0, 0, 100, "Left"),
|
|
("RGHT", 0, 0, 100, "Right"),
|
|
("UPPP", 0, 0, 100, "Up"),
|
|
("DOWN", 0, 0, 100, "Down"),
|
|
]
|
|
instances = [
|
|
dict(location=dict(LEFT=0, RGHT=0, UPPP=0, DOWN=0), stylename="TotallyNormal"),
|
|
dict(location=dict(LEFT=0, RGHT=100, UPPP=100, DOWN=0), stylename="Right Up"),
|
|
]
|
|
fb.setupFvar(axes, instances)
|
|
variations = {}
|
|
# Four (x, y) pairs and four phantom points:
|
|
leftDeltas = [(-200, 0), (-200, 0), (0, 0), (0, 0), None, None, None, None]
|
|
rightDeltas = [(0, 0), (0, 0), (200, 0), (200, 0), None, None, None, None]
|
|
upDeltas = [(0, 0), (0, 200), (0, 200), (0, 0), None, None, None, None]
|
|
downDeltas = [(0, -200), (0, 0), (0, 0), (0, -200), None, None, None, None]
|
|
variations["a"] = [
|
|
TupleVariation(dict(RGHT=(0, 1, 1)), rightDeltas),
|
|
TupleVariation(dict(LEFT=(0, 1, 1)), leftDeltas),
|
|
TupleVariation(dict(UPPP=(0, 1, 1)), upDeltas),
|
|
TupleVariation(dict(DOWN=(0, 1, 1)), downDeltas),
|
|
]
|
|
fb.setupGvar(variations)
|
|
|
|
fb.addFeatureVariations(
|
|
[
|
|
(
|
|
[
|
|
{"LEFT": (0.8, 1), "DOWN": (0.8, 1)},
|
|
{"RGHT": (0.8, 1), "UPPP": (0.8, 1)},
|
|
],
|
|
{"A": "a"},
|
|
)
|
|
],
|
|
featureTag="rclt",
|
|
)
|
|
|
|
statAxes = []
|
|
for tag, minVal, defaultVal, maxVal, name in axes:
|
|
values = [
|
|
dict(name="Neutral", value=defaultVal, flags=0x2),
|
|
dict(name=name, value=maxVal),
|
|
]
|
|
statAxes.append(dict(tag=tag, name=name, values=values))
|
|
fb.setupStat(statAxes)
|
|
|
|
fb.setupOS2()
|
|
fb.setupPost()
|
|
fb.setupDummyDSIG()
|
|
|
|
fb.save(outPath)
|
|
|
|
_verifyOutput(outPath)
|
|
|
|
|
|
def test_build_cff2(tmpdir):
|
|
outPath = os.path.join(str(tmpdir), "test_var.otf")
|
|
|
|
fb, advanceWidths, nameStrings = _setupFontBuilder(False, 1000)
|
|
fb.setupNameTable(nameStrings)
|
|
fb = _setupFontBuilderFvar(fb)
|
|
fb = _setupFontBuilderCFF2(fb)
|
|
|
|
metrics = {gn: (advanceWidth, 0) for gn, advanceWidth in advanceWidths.items()}
|
|
fb.setupHorizontalMetrics(metrics)
|
|
|
|
fb.setupHorizontalHeader(ascent=824, descent=200)
|
|
fb.setupOS2(
|
|
sTypoAscender=825, sTypoDescender=200, usWinAscent=824, usWinDescent=200
|
|
)
|
|
fb.setupPost()
|
|
|
|
fb.save(outPath)
|
|
|
|
_verifyOutput(outPath)
|
|
|
|
|
|
def test_build_cff_to_cff2(tmpdir):
|
|
fb, _, _ = _setupFontBuilder(False, 1000)
|
|
|
|
pen = T2CharStringPen(600, None)
|
|
drawTestGlyph(pen)
|
|
charString = pen.getCharString()
|
|
charStrings = {
|
|
".notdef": charString,
|
|
"A": charString,
|
|
"a": charString,
|
|
".null": charString,
|
|
}
|
|
fb.setupCFF("TestFont", {}, charStrings, {})
|
|
|
|
from fontTools.varLib.cff import convertCFFtoCFF2
|
|
|
|
convertCFFtoCFF2(fb.font)
|
|
|
|
|
|
def test_setupNameTable_no_mac():
|
|
fb, _, nameStrings = _setupFontBuilder(True)
|
|
fb.setupNameTable(nameStrings, mac=False)
|
|
|
|
assert all(n for n in fb.font["name"].names if n.platformID == 3)
|
|
assert not any(n for n in fb.font["name"].names if n.platformID == 1)
|
|
|
|
|
|
def test_setupNameTable_no_windows():
|
|
fb, _, nameStrings = _setupFontBuilder(True)
|
|
fb.setupNameTable(nameStrings, windows=False)
|
|
|
|
assert all(n for n in fb.font["name"].names if n.platformID == 1)
|
|
assert not any(n for n in fb.font["name"].names if n.platformID == 3)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"is_ttf, keep_glyph_names, make_cff2, post_format",
|
|
[
|
|
(True, True, False, 2), # TTF with post table format 2.0
|
|
(True, False, False, 3), # TTF with post table format 3.0
|
|
(False, True, False, 3), # CFF with post table format 3.0
|
|
(False, False, False, 3), # CFF with post table format 3.0
|
|
(False, True, True, 2), # CFF2 with post table format 2.0
|
|
(False, False, True, 3), # CFF2 with post table format 3.0
|
|
],
|
|
)
|
|
def test_setupPost(is_ttf, keep_glyph_names, make_cff2, post_format):
|
|
fb, _, nameStrings = _setupFontBuilder(is_ttf)
|
|
|
|
if make_cff2:
|
|
fb.setupNameTable(nameStrings)
|
|
fb = _setupFontBuilderCFF2(_setupFontBuilderFvar(fb))
|
|
|
|
if keep_glyph_names:
|
|
fb.setupPost()
|
|
else:
|
|
fb.setupPost(keepGlyphNames=keep_glyph_names)
|
|
|
|
assert fb.isTTF is is_ttf
|
|
assert ("CFF2" in fb.font) is make_cff2
|
|
assert fb.font["post"].formatType == post_format
|
|
|
|
|
|
def test_unicodeVariationSequences(tmpdir):
|
|
familyName = "UVSTestFont"
|
|
styleName = "Regular"
|
|
nameStrings = dict(familyName=familyName, styleName=styleName)
|
|
nameStrings["psName"] = familyName + "-" + styleName
|
|
glyphOrder = [".notdef", "space", "zero", "zero.slash"]
|
|
cmap = {ord(" "): "space", ord("0"): "zero"}
|
|
uvs = [
|
|
(0x0030, 0xFE00, "zero.slash"),
|
|
(0x0030, 0xFE01, None), # not an official sequence, just testing
|
|
]
|
|
metrics = {gn: (600, 0) for gn in glyphOrder}
|
|
pen = TTGlyphPen(None)
|
|
glyph = pen.glyph() # empty placeholder
|
|
glyphs = {gn: glyph for gn in glyphOrder}
|
|
|
|
fb = FontBuilder(1024, isTTF=True)
|
|
fb.setupGlyphOrder(glyphOrder)
|
|
fb.setupCharacterMap(cmap, uvs)
|
|
fb.setupGlyf(glyphs)
|
|
fb.setupHorizontalMetrics(metrics)
|
|
fb.setupHorizontalHeader(ascent=824, descent=200)
|
|
fb.setupNameTable(nameStrings)
|
|
fb.setupOS2()
|
|
fb.setupPost()
|
|
|
|
outPath = os.path.join(str(tmpdir), "test_uvs.ttf")
|
|
fb.save(outPath)
|
|
_verifyOutput(outPath, tables=["cmap"])
|
|
|
|
uvs = [
|
|
(0x0030, 0xFE00, "zero.slash"),
|
|
(
|
|
0x0030,
|
|
0xFE01,
|
|
"zero",
|
|
), # should result in the exact same subtable data, due to cmap[0x0030] == "zero"
|
|
]
|
|
fb.setupCharacterMap(cmap, uvs)
|
|
fb.save(outPath)
|
|
_verifyOutput(outPath, tables=["cmap"])
|
|
|
|
|
|
def test_setupPanose():
|
|
from fontTools.ttLib.tables.O_S_2f_2 import Panose
|
|
|
|
fb, advanceWidths, nameStrings = _setupFontBuilder(True)
|
|
|
|
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)
|
|
fb.setupHorizontalMetrics(metrics)
|
|
|
|
fb.setupHorizontalHeader(ascent=824, descent=200)
|
|
fb.setupNameTable(nameStrings)
|
|
fb.setupOS2()
|
|
fb.setupPost()
|
|
|
|
panoseValues = { # sample value of Times New Roman from https://www.w3.org/Printing/stevahn.html
|
|
"bFamilyType": 2,
|
|
"bSerifStyle": 2,
|
|
"bWeight": 6,
|
|
"bProportion": 3,
|
|
"bContrast": 5,
|
|
"bStrokeVariation": 4,
|
|
"bArmStyle": 5,
|
|
"bLetterForm": 2,
|
|
"bMidline": 3,
|
|
"bXHeight": 4,
|
|
}
|
|
panoseObj = Panose(**panoseValues)
|
|
|
|
for name in panoseValues:
|
|
assert getattr(fb.font["OS/2"].panose, name) == 0
|
|
|
|
fb.setupOS2(panose=panoseObj)
|
|
fb.setupPost()
|
|
|
|
for name, value in panoseValues.items():
|
|
assert getattr(fb.font["OS/2"].panose, name) == value
|