import os import textwrap from xml.etree import ElementTree from fontTools.ttLib import TTFont, newTable from fontTools.misc.psCharStrings import T2CharString from fontTools.ttLib.tables.otTables import ( GSUB, ScriptList, ScriptRecord, Script, DefaultLangSys, FeatureList, FeatureRecord, Feature, LookupList, Lookup, AlternateSubst, SingleSubst, ) # paths directory = os.path.dirname(__file__) shellSourcePath = os.path.join(directory, "gsubtest-shell.ttx") shellTempPath = os.path.join(directory, "gsubtest-shell.otf") featureList = os.path.join(directory, "gsubtest-features.txt") javascriptData = os.path.join(directory, "gsubtest-features.js") outputPath = os.path.join(os.path.dirname(directory), "gsubtest-lookup%d") baseCodepoint = 0xE000 # ------- # Features # ------- f = open(featureList, "rb") text = f.read() f.close() mapping = [] for line in text.splitlines(): line = line.strip() if not line: continue if line.startswith("#"): continue # parse values = line.split("\t") tag = values.pop(0) mapping.append(tag) # -------- # Outlines # -------- def addGlyphToCFF( glyphName=None, program=None, private=None, globalSubrs=None, charStringsIndex=None, topDict=None, charStrings=None, ): charString = T2CharString(program=program, private=private, globalSubrs=globalSubrs) charStringsIndex.append(charString) glyphID = len(topDict.charset) charStrings.charStrings[glyphName] = glyphID topDict.charset.append(glyphName) def makeLookup1(): # make a variation of the shell TTX data f = open(shellSourcePath) ttxData = f.read() f.close() ttxData = ttxData.replace("__familyName__", "gsubtest-lookup1") tempShellSourcePath = shellSourcePath + ".temp" f = open(tempShellSourcePath, "wb") f.write(ttxData) f.close() # compile the shell shell = TTFont(sfntVersion="OTTO") shell.importXML(tempShellSourcePath) shell.save(shellTempPath) os.remove(tempShellSourcePath) # load the shell shell = TTFont(shellTempPath) # grab the PASS and FAIL data hmtx = shell["hmtx"] glyphSet = shell.getGlyphSet() failGlyph = glyphSet["F"] failGlyph.decompile() failGlyphProgram = list(failGlyph.program) failGlyphMetrics = hmtx["F"] passGlyph = glyphSet["P"] passGlyph.decompile() passGlyphProgram = list(passGlyph.program) passGlyphMetrics = hmtx["P"] # grab some tables hmtx = shell["hmtx"] cmap = shell["cmap"] # start the glyph order existingGlyphs = [".notdef", "space", "F", "P"] glyphOrder = list(existingGlyphs) # start the CFF cff = shell["CFF "].cff globalSubrs = cff.GlobalSubrs topDict = cff.topDictIndex[0] topDict.charset = existingGlyphs private = topDict.Private charStrings = topDict.CharStrings charStringsIndex = charStrings.charStringsIndex features = sorted(mapping) # build the outline, hmtx and cmap data cp = baseCodepoint for index, tag in enumerate(features): # tag.pass glyphName = "%s.pass" % tag glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=passGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings, ) hmtx[glyphName] = passGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError( "Unsupported cmap table format: %d" % table.format ) cp += 1 # tag.fail glyphName = "%s.fail" % tag glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=failGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings, ) hmtx[glyphName] = failGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError( "Unsupported cmap table format: %d" % table.format ) # bump this up so that the sequence is the same as the lookup 3 font cp += 3 # set the glyph order shell.setGlyphOrder(glyphOrder) # start the GSUB shell["GSUB"] = newTable("GSUB") gsub = shell["GSUB"].table = GSUB() gsub.Version = 1.0 # make a list of all the features we will make featureCount = len(features) # set up the script list scriptList = gsub.ScriptList = ScriptList() scriptList.ScriptCount = 1 scriptList.ScriptRecord = [] scriptRecord = ScriptRecord() scriptList.ScriptRecord.append(scriptRecord) scriptRecord.ScriptTag = "DFLT" script = scriptRecord.Script = Script() defaultLangSys = script.DefaultLangSys = DefaultLangSys() defaultLangSys.FeatureCount = featureCount defaultLangSys.FeatureIndex = range(defaultLangSys.FeatureCount) defaultLangSys.ReqFeatureIndex = 65535 defaultLangSys.LookupOrder = None script.LangSysCount = 0 script.LangSysRecord = [] # set up the feature list featureList = gsub.FeatureList = FeatureList() featureList.FeatureCount = featureCount featureList.FeatureRecord = [] for index, tag in enumerate(features): # feature record featureRecord = FeatureRecord() featureRecord.FeatureTag = tag feature = featureRecord.Feature = Feature() featureList.FeatureRecord.append(featureRecord) # feature feature.FeatureParams = None feature.LookupCount = 1 feature.LookupListIndex = [index] # write the lookups lookupList = gsub.LookupList = LookupList() lookupList.LookupCount = featureCount lookupList.Lookup = [] for tag in features: # lookup lookup = Lookup() lookup.LookupType = 1 lookup.LookupFlag = 0 lookup.SubTableCount = 1 lookup.SubTable = [] lookupList.Lookup.append(lookup) # subtable subtable = SingleSubst() subtable.Format = 2 subtable.LookupType = 1 subtable.mapping = { "%s.pass" % tag: "%s.fail" % tag, "%s.fail" % tag: "%s.pass" % tag, } lookup.SubTable.append(subtable) path = outputPath % 1 + ".otf" if os.path.exists(path): os.remove(path) shell.save(path) # get rid of the shell if os.path.exists(shellTempPath): os.remove(shellTempPath) def makeLookup3(): # make a variation of the shell TTX data f = open(shellSourcePath) ttxData = f.read() f.close() ttxData = ttxData.replace("__familyName__", "gsubtest-lookup3") tempShellSourcePath = shellSourcePath + ".temp" f = open(tempShellSourcePath, "wb") f.write(ttxData) f.close() # compile the shell shell = TTFont(sfntVersion="OTTO") shell.importXML(tempShellSourcePath) shell.save(shellTempPath) os.remove(tempShellSourcePath) # load the shell shell = TTFont(shellTempPath) # grab the PASS and FAIL data hmtx = shell["hmtx"] glyphSet = shell.getGlyphSet() failGlyph = glyphSet["F"] failGlyph.decompile() failGlyphProgram = list(failGlyph.program) failGlyphMetrics = hmtx["F"] passGlyph = glyphSet["P"] passGlyph.decompile() passGlyphProgram = list(passGlyph.program) passGlyphMetrics = hmtx["P"] # grab some tables hmtx = shell["hmtx"] cmap = shell["cmap"] # start the glyph order existingGlyphs = [".notdef", "space", "F", "P"] glyphOrder = list(existingGlyphs) # start the CFF cff = shell["CFF "].cff globalSubrs = cff.GlobalSubrs topDict = cff.topDictIndex[0] topDict.charset = existingGlyphs private = topDict.Private charStrings = topDict.CharStrings charStringsIndex = charStrings.charStringsIndex features = sorted(mapping) # build the outline, hmtx and cmap data cp = baseCodepoint for index, tag in enumerate(features): # tag.pass glyphName = "%s.pass" % tag glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=passGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings, ) hmtx[glyphName] = passGlyphMetrics # tag.fail glyphName = "%s.fail" % tag glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=failGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings, ) hmtx[glyphName] = failGlyphMetrics # tag.default glyphName = "%s.default" % tag glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=passGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings, ) hmtx[glyphName] = passGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError( "Unsupported cmap table format: %d" % table.format ) cp += 1 # tag.alt1,2,3 for i in range(1, 4): glyphName = "%s.alt%d" % (tag, i) glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=failGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings, ) hmtx[glyphName] = failGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError( "Unsupported cmap table format: %d" % table.format ) cp += 1 # set the glyph order shell.setGlyphOrder(glyphOrder) # start the GSUB shell["GSUB"] = newTable("GSUB") gsub = shell["GSUB"].table = GSUB() gsub.Version = 1.0 # make a list of all the features we will make featureCount = len(features) # set up the script list scriptList = gsub.ScriptList = ScriptList() scriptList.ScriptCount = 1 scriptList.ScriptRecord = [] scriptRecord = ScriptRecord() scriptList.ScriptRecord.append(scriptRecord) scriptRecord.ScriptTag = "DFLT" script = scriptRecord.Script = Script() defaultLangSys = script.DefaultLangSys = DefaultLangSys() defaultLangSys.FeatureCount = featureCount defaultLangSys.FeatureIndex = range(defaultLangSys.FeatureCount) defaultLangSys.ReqFeatureIndex = 65535 defaultLangSys.LookupOrder = None script.LangSysCount = 0 script.LangSysRecord = [] # set up the feature list featureList = gsub.FeatureList = FeatureList() featureList.FeatureCount = featureCount featureList.FeatureRecord = [] for index, tag in enumerate(features): # feature record featureRecord = FeatureRecord() featureRecord.FeatureTag = tag feature = featureRecord.Feature = Feature() featureList.FeatureRecord.append(featureRecord) # feature feature.FeatureParams = None feature.LookupCount = 1 feature.LookupListIndex = [index] # write the lookups lookupList = gsub.LookupList = LookupList() lookupList.LookupCount = featureCount lookupList.Lookup = [] for tag in features: # lookup lookup = Lookup() lookup.LookupType = 3 lookup.LookupFlag = 0 lookup.SubTableCount = 1 lookup.SubTable = [] lookupList.Lookup.append(lookup) # subtable subtable = AlternateSubst() subtable.Format = 1 subtable.LookupType = 3 subtable.alternates = { "%s.default" % tag: ["%s.fail" % tag, "%s.fail" % tag, "%s.fail" % tag], "%s.alt1" % tag: ["%s.pass" % tag, "%s.fail" % tag, "%s.fail" % tag], "%s.alt2" % tag: ["%s.fail" % tag, "%s.pass" % tag, "%s.fail" % tag], "%s.alt3" % tag: ["%s.fail" % tag, "%s.fail" % tag, "%s.pass" % tag], } lookup.SubTable.append(subtable) path = outputPath % 3 + ".otf" if os.path.exists(path): os.remove(path) shell.save(path) # get rid of the shell if os.path.exists(shellTempPath): os.remove(shellTempPath) def makeJavascriptData(): features = sorted(mapping) outStr = [] outStr.append("") outStr.append("/* This file is autogenerated by makegsubfonts.py */") outStr.append("") outStr.append("/* ") outStr.append(" Features defined in gsubtest fonts with associated base") outStr.append(" codepoints for each feature:") outStr.append("") outStr.append(" cp = codepoint for feature featX") outStr.append("") outStr.append(" cp default PASS") outStr.append(" cp featX=1 FAIL") outStr.append(" cp featX=2 FAIL") outStr.append("") outStr.append(" cp+1 default FAIL") outStr.append(" cp+1 featX=1 PASS") outStr.append(" cp+1 featX=2 FAIL") outStr.append("") outStr.append(" cp+2 default FAIL") outStr.append(" cp+2 featX=1 FAIL") outStr.append(" cp+2 featX=2 PASS") outStr.append("") outStr.append("*/") outStr.append("") outStr.append("var gFeatures = {") cp = baseCodepoint taglist = [] for tag in features: taglist.append('"%s": 0x%x' % (tag, cp)) cp += 4 outStr.append( textwrap.fill(", ".join(taglist), initial_indent=" ", subsequent_indent=" ") ) outStr.append("};") outStr.append("") if os.path.exists(javascriptData): os.remove(javascriptData) f = open(javascriptData, "wb") f.write("\n".join(outStr)) f.close() # build fonts print("Making lookup type 1 font...") makeLookup1() print("Making lookup type 3 font...") makeLookup3() # output javascript data print("Making javascript data file...") makeJavascriptData()