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