If one is certain that the glyph data has compatible format and prefers not to wait for each glyph flag to checked...
416 lines
12 KiB
Python
416 lines
12 KiB
Python
import os
|
|
import pytest
|
|
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."
|
|
|
|
axes = [
|
|
("TEST", 0, 0, 100, "Test Axis"),
|
|
]
|
|
instances = [
|
|
dict(location=dict(TEST=0), stylename="TotallyNormal"),
|
|
dict(location=dict(TEST=100), stylename="TotallyTested"),
|
|
]
|
|
fb.setupFvar(axes, instances)
|
|
|
|
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, checkFormat=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"])
|