tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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()