makegsubfonts.py 14 KB

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