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