• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1
2import os
3import pytest
4import re
5from fontTools.ttLib import TTFont
6from fontTools.pens.ttGlyphPen import TTGlyphPen
7from fontTools.pens.t2CharStringPen import T2CharStringPen
8from fontTools.fontBuilder import FontBuilder
9from fontTools.ttLib.tables.TupleVariation import TupleVariation
10from fontTools.misc.psCharStrings import T2CharString
11
12
13def getTestData(fileName, mode="r"):
14    path = os.path.join(os.path.dirname(__file__), "data", fileName)
15    with open(path, mode) as f:
16        return f.read()
17
18
19def strip_VariableItems(string):
20    # ttlib changes with the fontTools version
21    string = re.sub(' ttLibVersion=".*"', '', string)
22    # head table checksum and creation and mod date changes with each save.
23    string = re.sub('<checkSumAdjustment value="[^"]+"/>', '', string)
24    string = re.sub('<modified value="[^"]+"/>', '', string)
25    string = re.sub('<created value="[^"]+"/>', '', string)
26    return string
27
28
29def drawTestGlyph(pen):
30    pen.moveTo((100, 100))
31    pen.lineTo((100, 1000))
32    pen.qCurveTo((200, 900), (400, 900), (500, 1000))
33    pen.lineTo((500, 100))
34    pen.closePath()
35
36
37def _setupFontBuilder(isTTF, unitsPerEm=1024):
38    fb = FontBuilder(unitsPerEm, isTTF=isTTF)
39    fb.setupGlyphOrder([".notdef", ".null", "A", "a"])
40    fb.setupCharacterMap({65: "A", 97: "a"})
41
42    advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}
43
44    familyName = "HelloTestFont"
45    styleName = "TotallyNormal"
46    nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
47                       styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
48    nameStrings['psName'] = familyName + "-" + styleName
49
50    return fb, advanceWidths, nameStrings
51
52
53def _setupFontBuilderFvar(fb):
54    assert 'name' in fb.font, 'Must run setupNameTable() first.'
55
56    axes = [
57        ('TEST', 0, 0, 100, "Test Axis"),
58    ]
59    instances = [
60        dict(location=dict(TEST=0), stylename="TotallyNormal"),
61        dict(location=dict(TEST=100), stylename="TotallyTested"),
62    ]
63    fb.setupFvar(axes, instances)
64
65    return fb
66
67
68def _setupFontBuilderCFF2(fb):
69    assert 'fvar' in fb.font, 'Must run _setupFontBuilderFvar() first.'
70
71    pen = T2CharStringPen(None, None, CFF2=True)
72    drawTestGlyph(pen)
73    charString = pen.getCharString()
74
75    program = [
76        200, 200, -200, -200, 2, "blend", "rmoveto",
77        400, 400, 1, "blend", "hlineto",
78        400, 400, 1, "blend", "vlineto",
79        -400, -400, 1, "blend", "hlineto"
80    ]
81    charStringVariable = T2CharString(program=program)
82
83    charStrings = {".notdef": charString, "A": charString,
84                   "a": charStringVariable, ".null": charString}
85    fb.setupCFF2(charStrings, regions=[{"TEST": (0, 1, 1)}])
86
87    return fb
88
89
90def _verifyOutput(outPath, tables=None):
91    f = TTFont(outPath)
92    f.saveXML(outPath + ".ttx", tables=tables)
93    with open(outPath + ".ttx") as f:
94        testData = strip_VariableItems(f.read())
95    refData = strip_VariableItems(getTestData(os.path.basename(outPath) + ".ttx"))
96    assert refData == testData
97
98
99def test_build_ttf(tmpdir):
100    outPath = os.path.join(str(tmpdir), "test.ttf")
101
102    fb, advanceWidths, nameStrings = _setupFontBuilder(True)
103
104    pen = TTGlyphPen(None)
105    drawTestGlyph(pen)
106    glyph = pen.glyph()
107    glyphs = {".notdef": glyph, "A": glyph, "a": glyph, ".null": glyph}
108    fb.setupGlyf(glyphs)
109    metrics = {}
110    glyphTable = fb.font["glyf"]
111    for gn, advanceWidth in advanceWidths.items():
112        metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
113    fb.setupHorizontalMetrics(metrics)
114
115    fb.setupHorizontalHeader(ascent=824, descent=200)
116    fb.setupNameTable(nameStrings)
117    fb.setupOS2()
118    fb.addOpenTypeFeatures("feature salt { sub A by a; } salt;")
119    fb.setupPost()
120    fb.setupDummyDSIG()
121
122    fb.save(outPath)
123
124    _verifyOutput(outPath)
125
126
127def test_build_otf(tmpdir):
128    outPath = os.path.join(str(tmpdir), "test.otf")
129
130    fb, advanceWidths, nameStrings = _setupFontBuilder(False)
131
132    pen = T2CharStringPen(600, None)
133    drawTestGlyph(pen)
134    charString = pen.getCharString()
135    charStrings = {".notdef": charString, "A": charString, "a": charString, ".null": charString}
136    fb.setupCFF(nameStrings['psName'], {"FullName": nameStrings['psName']}, charStrings, {})
137
138    lsb = {gn: cs.calcBounds(None)[0] for gn, cs in charStrings.items()}
139    metrics = {}
140    for gn, advanceWidth in advanceWidths.items():
141        metrics[gn] = (advanceWidth, lsb[gn])
142    fb.setupHorizontalMetrics(metrics)
143
144    fb.setupHorizontalHeader(ascent=824, descent=200)
145    fb.setupNameTable(nameStrings)
146    fb.setupOS2()
147    fb.addOpenTypeFeatures("feature kern { pos A a -50; } kern;")
148    fb.setupPost()
149    fb.setupDummyDSIG()
150
151    fb.save(outPath)
152
153    _verifyOutput(outPath)
154
155
156def test_build_var(tmpdir):
157    outPath = os.path.join(str(tmpdir), "test_var.ttf")
158
159    fb, advanceWidths, nameStrings = _setupFontBuilder(True)
160
161    pen = TTGlyphPen(None)
162    pen.moveTo((100, 0))
163    pen.lineTo((100, 400))
164    pen.lineTo((500, 400))
165    pen.lineTo((500, 000))
166    pen.closePath()
167    glyph1 = pen.glyph()
168
169    pen = TTGlyphPen(None)
170    pen.moveTo((50, 0))
171    pen.lineTo((50, 200))
172    pen.lineTo((250, 200))
173    pen.lineTo((250, 0))
174    pen.closePath()
175    glyph2 = pen.glyph()
176
177    pen = TTGlyphPen(None)
178    emptyGlyph = pen.glyph()
179
180    glyphs = {".notdef": emptyGlyph, "A": glyph1, "a": glyph2, ".null": emptyGlyph}
181    fb.setupGlyf(glyphs)
182    metrics = {}
183    glyphTable = fb.font["glyf"]
184    for gn, advanceWidth in advanceWidths.items():
185        metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
186    fb.setupHorizontalMetrics(metrics)
187
188    fb.setupHorizontalHeader(ascent=824, descent=200)
189    fb.setupNameTable(nameStrings)
190
191    axes = [
192        ('LEFT', 0, 0, 100, "Left"),
193        ('RGHT', 0, 0, 100, "Right"),
194        ('UPPP', 0, 0, 100, "Up"),
195        ('DOWN', 0, 0, 100, "Down"),
196    ]
197    instances = [
198        dict(location=dict(LEFT=0, RGHT=0, UPPP=0, DOWN=0), stylename="TotallyNormal"),
199        dict(location=dict(LEFT=0, RGHT=100, UPPP=100, DOWN=0), stylename="Right Up"),
200    ]
201    fb.setupFvar(axes, instances)
202    variations = {}
203    # Four (x, y) pairs and four phantom points:
204    leftDeltas = [(-200, 0), (-200, 0), (0, 0), (0, 0), None, None, None, None]
205    rightDeltas = [(0, 0), (0, 0), (200, 0), (200, 0), None, None, None, None]
206    upDeltas = [(0, 0), (0, 200), (0, 200), (0, 0), None, None, None, None]
207    downDeltas = [(0, -200), (0, 0), (0, 0), (0, -200), None, None, None, None]
208    variations['a'] = [
209        TupleVariation(dict(RGHT=(0, 1, 1)), rightDeltas),
210        TupleVariation(dict(LEFT=(0, 1, 1)), leftDeltas),
211        TupleVariation(dict(UPPP=(0, 1, 1)), upDeltas),
212        TupleVariation(dict(DOWN=(0, 1, 1)), downDeltas),
213    ]
214    fb.setupGvar(variations)
215
216    fb.addFeatureVariations(
217        [
218            (
219                [
220                    {"LEFT": (0.8, 1), "DOWN": (0.8, 1)},
221                    {"RGHT": (0.8, 1), "UPPP": (0.8, 1)},
222                  ],
223                {"A": "a"}
224            )
225        ],
226        featureTag="rclt",
227    )
228
229    statAxes = []
230    for tag, minVal, defaultVal, maxVal, name in axes:
231        values = [dict(name="Neutral", value=defaultVal, flags=0x2),
232                  dict(name=name, value=maxVal)]
233        statAxes.append(dict(tag=tag, name=name, values=values))
234    fb.setupStat(statAxes)
235
236    fb.setupOS2()
237    fb.setupPost()
238    fb.setupDummyDSIG()
239
240    fb.save(outPath)
241
242    _verifyOutput(outPath)
243
244
245def test_build_cff2(tmpdir):
246    outPath = os.path.join(str(tmpdir), "test_var.otf")
247
248    fb, advanceWidths, nameStrings = _setupFontBuilder(False, 1000)
249    fb.setupNameTable(nameStrings)
250    fb = _setupFontBuilderFvar(fb)
251    fb = _setupFontBuilderCFF2(fb)
252
253    metrics = {gn: (advanceWidth, 0) for gn, advanceWidth in advanceWidths.items()}
254    fb.setupHorizontalMetrics(metrics)
255
256    fb.setupHorizontalHeader(ascent=824, descent=200)
257    fb.setupOS2(sTypoAscender=825, sTypoDescender=200, usWinAscent=824, usWinDescent=200)
258    fb.setupPost()
259
260    fb.save(outPath)
261
262    _verifyOutput(outPath)
263
264
265def test_build_cff_to_cff2(tmpdir):
266    fb, _, _ = _setupFontBuilder(False, 1000)
267
268    pen = T2CharStringPen(600, None)
269    drawTestGlyph(pen)
270    charString = pen.getCharString()
271    charStrings = {".notdef": charString, "A": charString, "a": charString, ".null": charString}
272    fb.setupCFF("TestFont", {}, charStrings, {})
273
274    from fontTools.varLib.cff import convertCFFtoCFF2
275    convertCFFtoCFF2(fb.font)
276
277
278def test_setupNameTable_no_mac():
279    fb, _, nameStrings = _setupFontBuilder(True)
280    fb.setupNameTable(nameStrings, mac=False)
281
282    assert all(n for n in fb.font["name"].names if n.platformID == 3)
283    assert not any(n for n in fb.font["name"].names if n.platformID == 1)
284
285
286def test_setupNameTable_no_windows():
287    fb, _, nameStrings = _setupFontBuilder(True)
288    fb.setupNameTable(nameStrings, windows=False)
289
290    assert all(n for n in fb.font["name"].names if n.platformID == 1)
291    assert not any(n for n in fb.font["name"].names if n.platformID == 3)
292
293
294@pytest.mark.parametrize('is_ttf, keep_glyph_names, make_cff2, post_format', [
295    (True, True, False, 2),    # TTF with post table format 2.0
296    (True, False, False, 3),   # TTF with post table format 3.0
297    (False, True, False, 3),   # CFF with post table format 3.0
298    (False, False, False, 3),  # CFF with post table format 3.0
299    (False, True, True, 2),    # CFF2 with post table format 2.0
300    (False, False, True, 3),   # CFF2 with post table format 3.0
301])
302def test_setupPost(is_ttf, keep_glyph_names, make_cff2, post_format):
303    fb, _, nameStrings = _setupFontBuilder(is_ttf)
304
305    if make_cff2:
306        fb.setupNameTable(nameStrings)
307        fb = _setupFontBuilderCFF2(_setupFontBuilderFvar(fb))
308
309    if keep_glyph_names:
310        fb.setupPost()
311    else:
312        fb.setupPost(keepGlyphNames=keep_glyph_names)
313
314    assert fb.isTTF is is_ttf
315    assert ('CFF2' in fb.font) is make_cff2
316    assert fb.font["post"].formatType == post_format
317
318
319def test_unicodeVariationSequences(tmpdir):
320    familyName = "UVSTestFont"
321    styleName = "Regular"
322    nameStrings = dict(familyName=familyName, styleName=styleName)
323    nameStrings['psName'] = familyName + "-" + styleName
324    glyphOrder = [".notdef", "space", "zero", "zero.slash"]
325    cmap = {ord(" "): "space", ord("0"): "zero"}
326    uvs = [
327        (0x0030, 0xFE00, "zero.slash"),
328        (0x0030, 0xFE01, None),  # not an official sequence, just testing
329    ]
330    metrics = {gn: (600, 0) for gn in glyphOrder}
331    pen = TTGlyphPen(None)
332    glyph = pen.glyph()  # empty placeholder
333    glyphs = {gn: glyph for gn in glyphOrder}
334
335    fb = FontBuilder(1024, isTTF=True)
336    fb.setupGlyphOrder(glyphOrder)
337    fb.setupCharacterMap(cmap, uvs)
338    fb.setupGlyf(glyphs)
339    fb.setupHorizontalMetrics(metrics)
340    fb.setupHorizontalHeader(ascent=824, descent=200)
341    fb.setupNameTable(nameStrings)
342    fb.setupOS2()
343    fb.setupPost()
344
345    outPath = os.path.join(str(tmpdir), "test_uvs.ttf")
346    fb.save(outPath)
347    _verifyOutput(outPath, tables=["cmap"])
348
349    uvs = [
350        (0x0030, 0xFE00, "zero.slash"),
351        (0x0030, 0xFE01, "zero"),  # should result in the exact same subtable data, due to cmap[0x0030] == "zero"
352    ]
353    fb.setupCharacterMap(cmap, uvs)
354    fb.save(outPath)
355    _verifyOutput(outPath, tables=["cmap"])
356