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