1from __future__ import print_function, absolute_import 2 3import os 4import textwrap 5from xml.etree import ElementTree 6from fontTools.ttLib import TTFont, newTable 7from fontTools.misc.psCharStrings import T2CharString 8from fontTools.ttLib.tables.otTables import ( 9 GSUB, 10 ScriptList, 11 ScriptRecord, 12 Script, 13 DefaultLangSys, 14 FeatureList, 15 FeatureRecord, 16 Feature, 17 LookupList, 18 Lookup, 19 AlternateSubst, 20 SingleSubst, 21) 22 23# paths 24directory = os.path.dirname(__file__) 25shellSourcePath = os.path.join(directory, "gsubtest-shell.ttx") 26shellTempPath = os.path.join(directory, "gsubtest-shell.otf") 27featureList = os.path.join(directory, "gsubtest-features.txt") 28javascriptData = os.path.join(directory, "gsubtest-features.js") 29outputPath = os.path.join(os.path.dirname(directory), "gsubtest-lookup%d") 30 31baseCodepoint = 0xE000 32 33# ------- 34# Features 35# ------- 36 37f = open(featureList, "rb") 38text = f.read() 39f.close() 40mapping = [] 41for line in text.splitlines(): 42 line = line.strip() 43 if not line: 44 continue 45 if line.startswith("#"): 46 continue 47 # parse 48 values = line.split("\t") 49 tag = values.pop(0) 50 mapping.append(tag) 51 52# -------- 53# Outlines 54# -------- 55 56 57def addGlyphToCFF( 58 glyphName=None, 59 program=None, 60 private=None, 61 globalSubrs=None, 62 charStringsIndex=None, 63 topDict=None, 64 charStrings=None, 65): 66 charString = T2CharString(program=program, private=private, globalSubrs=globalSubrs) 67 charStringsIndex.append(charString) 68 glyphID = len(topDict.charset) 69 charStrings.charStrings[glyphName] = glyphID 70 topDict.charset.append(glyphName) 71 72 73def makeLookup1(): 74 # make a variation of the shell TTX data 75 f = open(shellSourcePath) 76 ttxData = f.read() 77 f.close() 78 ttxData = ttxData.replace("__familyName__", "gsubtest-lookup1") 79 tempShellSourcePath = shellSourcePath + ".temp" 80 f = open(tempShellSourcePath, "wb") 81 f.write(ttxData) 82 f.close() 83 84 # compile the shell 85 shell = TTFont(sfntVersion="OTTO") 86 shell.importXML(tempShellSourcePath) 87 shell.save(shellTempPath) 88 os.remove(tempShellSourcePath) 89 90 # load the shell 91 shell = TTFont(shellTempPath) 92 93 # grab the PASS and FAIL data 94 hmtx = shell["hmtx"] 95 glyphSet = shell.getGlyphSet() 96 97 failGlyph = glyphSet["F"] 98 failGlyph.decompile() 99 failGlyphProgram = list(failGlyph.program) 100 failGlyphMetrics = hmtx["F"] 101 102 passGlyph = glyphSet["P"] 103 passGlyph.decompile() 104 passGlyphProgram = list(passGlyph.program) 105 passGlyphMetrics = hmtx["P"] 106 107 # grab some tables 108 hmtx = shell["hmtx"] 109 cmap = shell["cmap"] 110 111 # start the glyph order 112 existingGlyphs = [".notdef", "space", "F", "P"] 113 glyphOrder = list(existingGlyphs) 114 115 # start the CFF 116 cff = shell["CFF "].cff 117 globalSubrs = cff.GlobalSubrs 118 topDict = cff.topDictIndex[0] 119 topDict.charset = existingGlyphs 120 private = topDict.Private 121 charStrings = topDict.CharStrings 122 charStringsIndex = charStrings.charStringsIndex 123 124 features = sorted(mapping) 125 126 # build the outline, hmtx and cmap data 127 cp = baseCodepoint 128 for index, tag in enumerate(features): 129 130 # tag.pass 131 glyphName = "%s.pass" % tag 132 glyphOrder.append(glyphName) 133 addGlyphToCFF( 134 glyphName=glyphName, 135 program=passGlyphProgram, 136 private=private, 137 globalSubrs=globalSubrs, 138 charStringsIndex=charStringsIndex, 139 topDict=topDict, 140 charStrings=charStrings, 141 ) 142 hmtx[glyphName] = passGlyphMetrics 143 144 for table in cmap.tables: 145 if table.format == 4: 146 table.cmap[cp] = glyphName 147 else: 148 raise NotImplementedError( 149 "Unsupported cmap table format: %d" % table.format 150 ) 151 cp += 1 152 153 # tag.fail 154 glyphName = "%s.fail" % tag 155 glyphOrder.append(glyphName) 156 addGlyphToCFF( 157 glyphName=glyphName, 158 program=failGlyphProgram, 159 private=private, 160 globalSubrs=globalSubrs, 161 charStringsIndex=charStringsIndex, 162 topDict=topDict, 163 charStrings=charStrings, 164 ) 165 hmtx[glyphName] = failGlyphMetrics 166 167 for table in cmap.tables: 168 if table.format == 4: 169 table.cmap[cp] = glyphName 170 else: 171 raise NotImplementedError( 172 "Unsupported cmap table format: %d" % table.format 173 ) 174 175 # bump this up so that the sequence is the same as the lookup 3 font 176 cp += 3 177 178 # set the glyph order 179 shell.setGlyphOrder(glyphOrder) 180 181 # start the GSUB 182 shell["GSUB"] = newTable("GSUB") 183 gsub = shell["GSUB"].table = GSUB() 184 gsub.Version = 1.0 185 186 # make a list of all the features we will make 187 featureCount = len(features) 188 189 # set up the script list 190 scriptList = gsub.ScriptList = ScriptList() 191 scriptList.ScriptCount = 1 192 scriptList.ScriptRecord = [] 193 scriptRecord = ScriptRecord() 194 scriptList.ScriptRecord.append(scriptRecord) 195 scriptRecord.ScriptTag = "DFLT" 196 script = scriptRecord.Script = Script() 197 defaultLangSys = script.DefaultLangSys = DefaultLangSys() 198 defaultLangSys.FeatureCount = featureCount 199 defaultLangSys.FeatureIndex = range(defaultLangSys.FeatureCount) 200 defaultLangSys.ReqFeatureIndex = 65535 201 defaultLangSys.LookupOrder = None 202 script.LangSysCount = 0 203 script.LangSysRecord = [] 204 205 # set up the feature list 206 featureList = gsub.FeatureList = FeatureList() 207 featureList.FeatureCount = featureCount 208 featureList.FeatureRecord = [] 209 for index, tag in enumerate(features): 210 # feature record 211 featureRecord = FeatureRecord() 212 featureRecord.FeatureTag = tag 213 feature = featureRecord.Feature = Feature() 214 featureList.FeatureRecord.append(featureRecord) 215 # feature 216 feature.FeatureParams = None 217 feature.LookupCount = 1 218 feature.LookupListIndex = [index] 219 220 # write the lookups 221 lookupList = gsub.LookupList = LookupList() 222 lookupList.LookupCount = featureCount 223 lookupList.Lookup = [] 224 for tag in features: 225 # lookup 226 lookup = Lookup() 227 lookup.LookupType = 1 228 lookup.LookupFlag = 0 229 lookup.SubTableCount = 1 230 lookup.SubTable = [] 231 lookupList.Lookup.append(lookup) 232 # subtable 233 subtable = SingleSubst() 234 subtable.Format = 2 235 subtable.LookupType = 1 236 subtable.mapping = { 237 "%s.pass" % tag: "%s.fail" % tag, 238 "%s.fail" % tag: "%s.pass" % tag, 239 } 240 lookup.SubTable.append(subtable) 241 242 path = outputPath % 1 + ".otf" 243 if os.path.exists(path): 244 os.remove(path) 245 shell.save(path) 246 247 # get rid of the shell 248 if os.path.exists(shellTempPath): 249 os.remove(shellTempPath) 250 251 252def makeLookup3(): 253 # make a variation of the shell TTX data 254 f = open(shellSourcePath) 255 ttxData = f.read() 256 f.close() 257 ttxData = ttxData.replace("__familyName__", "gsubtest-lookup3") 258 tempShellSourcePath = shellSourcePath + ".temp" 259 f = open(tempShellSourcePath, "wb") 260 f.write(ttxData) 261 f.close() 262 263 # compile the shell 264 shell = TTFont(sfntVersion="OTTO") 265 shell.importXML(tempShellSourcePath) 266 shell.save(shellTempPath) 267 os.remove(tempShellSourcePath) 268 269 # load the shell 270 shell = TTFont(shellTempPath) 271 272 # grab the PASS and FAIL data 273 hmtx = shell["hmtx"] 274 glyphSet = shell.getGlyphSet() 275 276 failGlyph = glyphSet["F"] 277 failGlyph.decompile() 278 failGlyphProgram = list(failGlyph.program) 279 failGlyphMetrics = hmtx["F"] 280 281 passGlyph = glyphSet["P"] 282 passGlyph.decompile() 283 passGlyphProgram = list(passGlyph.program) 284 passGlyphMetrics = hmtx["P"] 285 286 # grab some tables 287 hmtx = shell["hmtx"] 288 cmap = shell["cmap"] 289 290 # start the glyph order 291 existingGlyphs = [".notdef", "space", "F", "P"] 292 glyphOrder = list(existingGlyphs) 293 294 # start the CFF 295 cff = shell["CFF "].cff 296 globalSubrs = cff.GlobalSubrs 297 topDict = cff.topDictIndex[0] 298 topDict.charset = existingGlyphs 299 private = topDict.Private 300 charStrings = topDict.CharStrings 301 charStringsIndex = charStrings.charStringsIndex 302 303 features = sorted(mapping) 304 305 # build the outline, hmtx and cmap data 306 cp = baseCodepoint 307 for index, tag in enumerate(features): 308 309 # tag.pass 310 glyphName = "%s.pass" % tag 311 glyphOrder.append(glyphName) 312 addGlyphToCFF( 313 glyphName=glyphName, 314 program=passGlyphProgram, 315 private=private, 316 globalSubrs=globalSubrs, 317 charStringsIndex=charStringsIndex, 318 topDict=topDict, 319 charStrings=charStrings, 320 ) 321 hmtx[glyphName] = passGlyphMetrics 322 323 # tag.fail 324 glyphName = "%s.fail" % tag 325 glyphOrder.append(glyphName) 326 addGlyphToCFF( 327 glyphName=glyphName, 328 program=failGlyphProgram, 329 private=private, 330 globalSubrs=globalSubrs, 331 charStringsIndex=charStringsIndex, 332 topDict=topDict, 333 charStrings=charStrings, 334 ) 335 hmtx[glyphName] = failGlyphMetrics 336 337 # tag.default 338 glyphName = "%s.default" % tag 339 glyphOrder.append(glyphName) 340 addGlyphToCFF( 341 glyphName=glyphName, 342 program=passGlyphProgram, 343 private=private, 344 globalSubrs=globalSubrs, 345 charStringsIndex=charStringsIndex, 346 topDict=topDict, 347 charStrings=charStrings, 348 ) 349 hmtx[glyphName] = passGlyphMetrics 350 351 for table in cmap.tables: 352 if table.format == 4: 353 table.cmap[cp] = glyphName 354 else: 355 raise NotImplementedError( 356 "Unsupported cmap table format: %d" % table.format 357 ) 358 cp += 1 359 360 # tag.alt1,2,3 361 for i in range(1, 4): 362 glyphName = "%s.alt%d" % (tag, i) 363 glyphOrder.append(glyphName) 364 addGlyphToCFF( 365 glyphName=glyphName, 366 program=failGlyphProgram, 367 private=private, 368 globalSubrs=globalSubrs, 369 charStringsIndex=charStringsIndex, 370 topDict=topDict, 371 charStrings=charStrings, 372 ) 373 hmtx[glyphName] = failGlyphMetrics 374 for table in cmap.tables: 375 if table.format == 4: 376 table.cmap[cp] = glyphName 377 else: 378 raise NotImplementedError( 379 "Unsupported cmap table format: %d" % table.format 380 ) 381 cp += 1 382 383 # set the glyph order 384 shell.setGlyphOrder(glyphOrder) 385 386 # start the GSUB 387 shell["GSUB"] = newTable("GSUB") 388 gsub = shell["GSUB"].table = GSUB() 389 gsub.Version = 1.0 390 391 # make a list of all the features we will make 392 featureCount = len(features) 393 394 # set up the script list 395 scriptList = gsub.ScriptList = ScriptList() 396 scriptList.ScriptCount = 1 397 scriptList.ScriptRecord = [] 398 scriptRecord = ScriptRecord() 399 scriptList.ScriptRecord.append(scriptRecord) 400 scriptRecord.ScriptTag = "DFLT" 401 script = scriptRecord.Script = Script() 402 defaultLangSys = script.DefaultLangSys = DefaultLangSys() 403 defaultLangSys.FeatureCount = featureCount 404 defaultLangSys.FeatureIndex = range(defaultLangSys.FeatureCount) 405 defaultLangSys.ReqFeatureIndex = 65535 406 defaultLangSys.LookupOrder = None 407 script.LangSysCount = 0 408 script.LangSysRecord = [] 409 410 # set up the feature list 411 featureList = gsub.FeatureList = FeatureList() 412 featureList.FeatureCount = featureCount 413 featureList.FeatureRecord = [] 414 for index, tag in enumerate(features): 415 # feature record 416 featureRecord = FeatureRecord() 417 featureRecord.FeatureTag = tag 418 feature = featureRecord.Feature = Feature() 419 featureList.FeatureRecord.append(featureRecord) 420 # feature 421 feature.FeatureParams = None 422 feature.LookupCount = 1 423 feature.LookupListIndex = [index] 424 425 # write the lookups 426 lookupList = gsub.LookupList = LookupList() 427 lookupList.LookupCount = featureCount 428 lookupList.Lookup = [] 429 for tag in features: 430 # lookup 431 lookup = Lookup() 432 lookup.LookupType = 3 433 lookup.LookupFlag = 0 434 lookup.SubTableCount = 1 435 lookup.SubTable = [] 436 lookupList.Lookup.append(lookup) 437 # subtable 438 subtable = AlternateSubst() 439 subtable.Format = 1 440 subtable.LookupType = 3 441 subtable.alternates = { 442 "%s.default" % tag: ["%s.fail" % tag, "%s.fail" % tag, "%s.fail" % tag], 443 "%s.alt1" % tag: ["%s.pass" % tag, "%s.fail" % tag, "%s.fail" % tag], 444 "%s.alt2" % tag: ["%s.fail" % tag, "%s.pass" % tag, "%s.fail" % tag], 445 "%s.alt3" % tag: ["%s.fail" % tag, "%s.fail" % tag, "%s.pass" % tag], 446 } 447 lookup.SubTable.append(subtable) 448 449 path = outputPath % 3 + ".otf" 450 if os.path.exists(path): 451 os.remove(path) 452 shell.save(path) 453 454 # get rid of the shell 455 if os.path.exists(shellTempPath): 456 os.remove(shellTempPath) 457 458 459def makeJavascriptData(): 460 features = sorted(mapping) 461 outStr = [] 462 463 outStr.append("") 464 outStr.append("/* This file is autogenerated by makegsubfonts.py */") 465 outStr.append("") 466 outStr.append("/* ") 467 outStr.append(" Features defined in gsubtest fonts with associated base") 468 outStr.append(" codepoints for each feature:") 469 outStr.append("") 470 outStr.append(" cp = codepoint for feature featX") 471 outStr.append("") 472 outStr.append(" cp default PASS") 473 outStr.append(" cp featX=1 FAIL") 474 outStr.append(" cp featX=2 FAIL") 475 outStr.append("") 476 outStr.append(" cp+1 default FAIL") 477 outStr.append(" cp+1 featX=1 PASS") 478 outStr.append(" cp+1 featX=2 FAIL") 479 outStr.append("") 480 outStr.append(" cp+2 default FAIL") 481 outStr.append(" cp+2 featX=1 FAIL") 482 outStr.append(" cp+2 featX=2 PASS") 483 outStr.append("") 484 outStr.append("*/") 485 outStr.append("") 486 outStr.append("var gFeatures = {") 487 cp = baseCodepoint 488 489 taglist = [] 490 for tag in features: 491 taglist.append('"%s": 0x%x' % (tag, cp)) 492 cp += 4 493 494 outStr.append( 495 textwrap.fill(", ".join(taglist), initial_indent=" ", subsequent_indent=" ") 496 ) 497 outStr.append("};") 498 outStr.append("") 499 500 if os.path.exists(javascriptData): 501 os.remove(javascriptData) 502 503 f = open(javascriptData, "wb") 504 f.write("\n".join(outStr)) 505 f.close() 506 507 508# build fonts 509 510print("Making lookup type 1 font...") 511makeLookup1() 512 513print("Making lookup type 3 font...") 514makeLookup3() 515 516# output javascript data 517 518print("Making javascript data file...") 519makeJavascriptData() 520