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