2015-10-17 00:47:12 -03:00
|
|
|
#!/usr/bin/python
|
|
|
|
|
2015-10-17 02:26:22 -03:00
|
|
|
# FontWorker-to-FontTools for OpenType Layout tables
|
2015-10-17 00:47:12 -03:00
|
|
|
|
2015-10-17 02:26:22 -03:00
|
|
|
from __future__ import print_function, division, absolute_import
|
|
|
|
from fontTools import ttLib
|
|
|
|
from fontTools.ttLib.tables import otTables as ot
|
|
|
|
import re
|
|
|
|
|
|
|
|
debug = print
|
|
|
|
|
|
|
|
def parseScriptList(lines):
|
2015-10-27 12:44:32 -07:00
|
|
|
lines.skipUntil('script table begin')
|
2015-10-17 02:26:22 -03:00
|
|
|
self = ot.ScriptList()
|
|
|
|
self.ScriptRecord = []
|
2015-10-27 12:44:32 -07:00
|
|
|
for line in lines.readUntil('script table end'):
|
2015-10-17 02:26:22 -03:00
|
|
|
scriptTag, langSysTag, defaultFeature, features = line
|
|
|
|
debug("Adding script", scriptTag, "language-system", langSysTag)
|
|
|
|
|
|
|
|
langSys = ot.LangSys()
|
|
|
|
langSys.LookupOrder = None
|
|
|
|
# TODO The following two lines should use lazy feature name-to-index mapping
|
|
|
|
langSys.ReqFeatureIndex = int(defaultFeature) if defaultFeature else 0xFFFF
|
|
|
|
langSys.FeatureIndex = [int(f) for f in features.split(',')]
|
|
|
|
langSys.FeatureCount = len(langSys.FeatureIndex)
|
|
|
|
|
|
|
|
script = [s for s in self.ScriptRecord if s.ScriptTag == scriptTag]
|
|
|
|
if script:
|
|
|
|
script = script[0].Script
|
|
|
|
else:
|
|
|
|
scriptRec = ot.ScriptRecord()
|
|
|
|
scriptRec.ScriptTag = scriptTag
|
|
|
|
scriptRec.Script = ot.Script()
|
|
|
|
self.ScriptRecord.append(scriptRec)
|
|
|
|
script = scriptRec.Script
|
|
|
|
script.DefaultLangSys = None
|
|
|
|
script.LangSysRecord = []
|
|
|
|
script.LangSysCount = 0
|
|
|
|
|
|
|
|
if langSysTag == 'default':
|
|
|
|
script.DefaultLangSys = langSys
|
|
|
|
else:
|
|
|
|
langSysRec = ot.LangSysRecord()
|
|
|
|
langSysRec.LangSysTag = langSysTag + ' '*(4 - len(langSysTag))
|
|
|
|
langSysRec.LangSys = langSys
|
|
|
|
script.LangSysRecord.append(langSysRec)
|
|
|
|
script.LangSysCount = len(script.LangSysRecord)
|
|
|
|
|
|
|
|
self.ScriptCount = len(self.ScriptRecord)
|
|
|
|
# TODO sort scripts and langSys's?
|
|
|
|
return self
|
|
|
|
|
|
|
|
def parseFeatureList(lines):
|
2015-10-27 12:44:32 -07:00
|
|
|
lines.skipUntil('feature table begin')
|
2015-10-17 02:26:22 -03:00
|
|
|
self = ot.FeatureList()
|
|
|
|
self.FeatureRecord = []
|
2015-10-27 12:44:32 -07:00
|
|
|
for line in lines.readUntil('feature table end'):
|
2015-10-17 02:26:22 -03:00
|
|
|
idx, featureTag, lookups = line
|
2015-10-17 02:55:00 -03:00
|
|
|
assert int(idx) == len(self.FeatureRecord), "%d %d" % (idx, len(self.FeatureRecord))
|
2015-10-17 02:26:22 -03:00
|
|
|
featureRec = ot.FeatureRecord()
|
|
|
|
featureRec.FeatureTag = featureTag
|
|
|
|
featureRec.Feature = ot.Feature()
|
|
|
|
self.FeatureRecord.append(featureRec)
|
|
|
|
feature = featureRec.Feature
|
|
|
|
feature.FeatureParams = None
|
|
|
|
# TODO The following line should use lazy lookup name-to-index mapping
|
|
|
|
feature.LookupListIndex = [int(l) for l in lookups.split(',')]
|
|
|
|
feature.LookupCount = len(feature.LookupListIndex)
|
|
|
|
|
|
|
|
self.FeatureCount = len(self.FeatureRecord)
|
|
|
|
return self
|
|
|
|
|
2015-10-27 14:16:00 -07:00
|
|
|
def parseLookupFlags(lines):
|
|
|
|
flags = 0
|
|
|
|
for line in lines:
|
|
|
|
flag = {
|
|
|
|
'RightToLeft': 0x0001,
|
|
|
|
'IgnoreBaseGlyphs': 0x0002,
|
|
|
|
'IgnoreLigatures': 0x0004,
|
|
|
|
'IgnoreMarks': 0x0008,
|
|
|
|
}.get(line[0])
|
|
|
|
if flag:
|
|
|
|
assert line[1] in ['yes', 'no'], line[1]
|
|
|
|
if line[1] == 'yes':
|
|
|
|
flags |= flag
|
|
|
|
continue
|
|
|
|
if line[0] == 'MarkAttachmentType':
|
|
|
|
flags |= int(line[1]) << 8
|
|
|
|
continue
|
|
|
|
lines.pack(line)
|
|
|
|
break
|
|
|
|
return flags
|
2015-10-17 03:30:31 -03:00
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parseSingleSubst(self, lines):
|
|
|
|
self.mapping = {}
|
|
|
|
for line in lines:
|
|
|
|
assert len(line) == 2, line
|
|
|
|
self.mapping[line[0]] = line[1]
|
|
|
|
|
|
|
|
def parseMultiple(self, lines):
|
2015-10-27 14:16:00 -07:00
|
|
|
self.mapping = {}
|
|
|
|
for line in lines:
|
|
|
|
self.mapping[line[0]] = line[1:]
|
2015-10-17 03:30:31 -03:00
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parseAlternate(self, lines):
|
2015-10-27 14:16:00 -07:00
|
|
|
self.alternates = {}
|
|
|
|
for line in lines:
|
|
|
|
self.alternates[line[0]] = line[1:]
|
2015-10-17 03:30:31 -03:00
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parseLigature(self, lines):
|
|
|
|
self.ligatures = {}
|
|
|
|
for line in lines:
|
|
|
|
assert len(line) >= 2, line
|
2015-10-27 16:20:41 -07:00
|
|
|
self.ligatures[tuple(line[1:])] = line[0]
|
|
|
|
# The following generates table using old, tedious, API
|
|
|
|
#ligGlyph, firstGlyph = line[:2]
|
|
|
|
#otherComponents = line[2:]
|
|
|
|
#ligature = ot.Ligature()
|
|
|
|
#ligature.Component = otherComponents
|
|
|
|
#ligature.CompCount = len(ligature.Component) + 1
|
|
|
|
#ligature.LigGlyph = ligGlyph
|
|
|
|
#self.ligatures.setdefault(firstGlyph, []).append(ligature)
|
2015-10-27 12:44:32 -07:00
|
|
|
|
|
|
|
def parseSinglePos(self, lines):
|
2015-10-17 03:30:31 -03:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parsePair(self, lines):
|
2015-10-17 03:30:31 -03:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parseCursive(self, lines):
|
2015-10-17 03:30:31 -03:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parseMarkToSomething(self, lines):
|
2015-10-17 03:30:31 -03:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parseMarkToLigature(self, lines):
|
2015-10-17 03:30:31 -03:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parseContext(self, lines):
|
2015-10-27 14:16:00 -07:00
|
|
|
typ = lines.peek()[0]
|
2015-10-27 12:44:32 -07:00
|
|
|
if typ == 'glyph':
|
2015-10-27 16:07:11 -07:00
|
|
|
self.Format = 1
|
2015-10-27 12:44:32 -07:00
|
|
|
return
|
2015-10-27 14:16:00 -07:00
|
|
|
elif typ == 'class definition begin':
|
2015-10-27 16:07:11 -07:00
|
|
|
self.Format = 2
|
2015-10-27 14:16:00 -07:00
|
|
|
return
|
|
|
|
print(typ)
|
2015-10-17 03:30:31 -03:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
def parseChained(self, lines):
|
2015-10-27 14:16:00 -07:00
|
|
|
typ = lines.peek()[0]
|
|
|
|
if typ == 'glyph':
|
2015-10-27 16:07:11 -07:00
|
|
|
self.Format = 1
|
2015-10-27 14:16:00 -07:00
|
|
|
return
|
|
|
|
elif typ == 'backtrackclass definition begin':
|
2015-10-27 16:07:11 -07:00
|
|
|
self.Format = 2
|
2015-10-27 14:16:00 -07:00
|
|
|
return
|
|
|
|
print(typ)
|
2015-10-17 03:30:31 -03:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-10-17 02:55:00 -03:00
|
|
|
def parseLookupList(lines, tableTag):
|
|
|
|
self = ot.LookupList()
|
|
|
|
self.Lookup = []
|
|
|
|
while True:
|
2015-10-27 12:44:32 -07:00
|
|
|
line = lines.skipUntil('lookup')
|
2015-10-17 02:55:00 -03:00
|
|
|
if line is None: break
|
2015-10-27 14:16:00 -07:00
|
|
|
lookupLines = lines.readUntil('lookup end')
|
2015-10-17 02:55:00 -03:00
|
|
|
_, idx, typ = line
|
|
|
|
assert int(idx) == len(self.Lookup), "%d %d" % (idx, len(self.Lookup))
|
|
|
|
|
|
|
|
lookup = ot.Lookup()
|
|
|
|
self.Lookup.append(lookup)
|
2015-10-27 16:07:11 -07:00
|
|
|
lookup.LookupFlag = parseLookupFlags(lookupLines)
|
2015-10-27 14:16:00 -07:00
|
|
|
lookup.LookupType, parseLookupSubTable = {
|
2015-10-17 02:55:00 -03:00
|
|
|
'GSUB': {
|
2015-10-27 12:44:32 -07:00
|
|
|
'single': (1, parseSingleSubst),
|
|
|
|
'multiple': (2, parseMultiple),
|
|
|
|
'alternate': (3, parseAlternate),
|
|
|
|
'ligature': (4, parseLigature),
|
|
|
|
'context': (5, parseContext),
|
|
|
|
'chained': (6, parseChained),
|
2015-10-17 02:55:00 -03:00
|
|
|
},
|
|
|
|
'GPOS': {
|
2015-10-27 12:44:32 -07:00
|
|
|
'single': (1, parseSinglePos),
|
|
|
|
'pair': (2, parsePair),
|
|
|
|
'kernset': (2, parsePair),
|
|
|
|
'cursive': (3, parseCursive),
|
|
|
|
'mark to base': (4, parseMarkToSomething),
|
|
|
|
'mark to ligature':(5, parseMarkToLigature),
|
|
|
|
'mark to mark': (6, parseMarkToSomething),
|
|
|
|
'context': (7, parseContext),
|
|
|
|
'chained': (8, parseChained),
|
2015-10-17 02:55:00 -03:00
|
|
|
},
|
|
|
|
}[tableTag][typ]
|
|
|
|
subtable = ot.lookupTypes[tableTag][lookup.LookupType]()
|
2015-10-17 03:30:31 -03:00
|
|
|
subtable.LookupType = lookup.LookupType
|
2015-10-27 12:44:32 -07:00
|
|
|
|
2015-10-27 14:16:00 -07:00
|
|
|
parseLookupSubTable(subtable, lookupLines)
|
2015-10-17 02:55:00 -03:00
|
|
|
|
|
|
|
lookup.SubTable = [subtable]
|
|
|
|
lookup.SubTableCount = len(lookup.SubTable)
|
|
|
|
|
|
|
|
self.LookupCount = len(self.Lookup)
|
|
|
|
return self
|
2015-10-17 02:26:22 -03:00
|
|
|
|
|
|
|
def parseGSUB(lines):
|
|
|
|
debug("Parsing GSUB")
|
|
|
|
self = ot.GSUB()
|
2015-10-27 16:07:11 -07:00
|
|
|
self.Version = 1.0
|
2015-10-17 02:26:22 -03:00
|
|
|
self.ScriptList = parseScriptList(lines)
|
|
|
|
self.FeatureList = parseFeatureList(lines)
|
2015-10-17 02:55:00 -03:00
|
|
|
self.LookupList = parseLookupList(lines, 'GSUB')
|
2015-10-17 02:26:22 -03:00
|
|
|
return self
|
|
|
|
|
|
|
|
def parseGPOS(lines):
|
|
|
|
debug("Parsing GPOS")
|
|
|
|
self = ot.GPOS()
|
2015-10-27 16:07:11 -07:00
|
|
|
self.Version = 1.0
|
2015-10-17 03:30:31 -03:00
|
|
|
# TODO parse EM?
|
2015-10-17 02:26:22 -03:00
|
|
|
self.ScriptList = parseScriptList(lines)
|
|
|
|
self.FeatureList = parseFeatureList(lines)
|
2015-10-17 02:55:00 -03:00
|
|
|
self.LookupList = parseLookupList(lines, 'GPOS')
|
2015-10-17 02:26:22 -03:00
|
|
|
return self
|
|
|
|
|
|
|
|
def parseGDEF(lines):
|
|
|
|
debug("Parsing GDEF TODO")
|
|
|
|
return None
|
|
|
|
|
2015-10-27 14:16:00 -07:00
|
|
|
class BufferedIter(object):
|
|
|
|
|
|
|
|
def __init__(self, it):
|
|
|
|
self.iter = it
|
|
|
|
self.buffer = []
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def next(self):
|
|
|
|
if self.buffer:
|
|
|
|
return self.buffer.pop(0)
|
|
|
|
else:
|
|
|
|
return self.iter.next()
|
|
|
|
|
|
|
|
def peek(self, n=0):
|
|
|
|
"""Return an item n entries ahead in the iteration."""
|
|
|
|
while n >= len(self.buffer):
|
|
|
|
try:
|
|
|
|
self.buffer.append(self.iter.next())
|
|
|
|
except StopIteration:
|
|
|
|
return None
|
|
|
|
return self.buffer[n]
|
|
|
|
|
|
|
|
def pack(self, item):
|
|
|
|
"""Push back item into the iterator."""
|
|
|
|
self.buffer.insert(0, item)
|
|
|
|
|
2015-10-27 12:44:32 -07:00
|
|
|
class Tokenizer(object):
|
|
|
|
|
|
|
|
def __init__(self, f):
|
|
|
|
# TODO BytesIO / StringIO as needed? also, figure out whether we work on bytes or unicode
|
|
|
|
|
|
|
|
lines = iter(f)
|
|
|
|
lines = ([s.strip() for s in line.split('\t')] for line in lines)
|
|
|
|
try:
|
|
|
|
self.filename = f.name
|
|
|
|
except:
|
|
|
|
self.filename = None
|
|
|
|
self._lines = lines
|
|
|
|
self._lineno = 0
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def _next(self):
|
|
|
|
self._lineno += 1
|
|
|
|
return next(self._lines)
|
|
|
|
|
|
|
|
def next(self):
|
|
|
|
while True:
|
|
|
|
line = self._next()
|
|
|
|
# Skip comments and empty lines
|
|
|
|
if line[0] not in ['', '%']:
|
|
|
|
return line
|
|
|
|
|
|
|
|
def skipUntil(self, what):
|
|
|
|
for line in self:
|
|
|
|
if line[0] == what:
|
|
|
|
return line
|
|
|
|
|
2015-10-27 14:16:00 -07:00
|
|
|
def _readUntil(self, what):
|
2015-10-27 12:44:32 -07:00
|
|
|
for line in self:
|
|
|
|
if line[0] == what:
|
|
|
|
raise StopIteration
|
|
|
|
yield line
|
2015-10-27 14:16:00 -07:00
|
|
|
def readUntil(self, what):
|
|
|
|
return BufferedIter(self._readUntil(what))
|
2015-10-27 12:44:32 -07:00
|
|
|
|
|
|
|
def compile(f):
|
|
|
|
lines = Tokenizer(f)
|
2015-10-17 02:26:22 -03:00
|
|
|
line = next(lines)
|
2015-10-17 02:55:00 -03:00
|
|
|
assert line[0][:9] == 'FontDame ', line
|
|
|
|
assert line[0][13:] == ' table', line
|
2015-10-17 02:26:22 -03:00
|
|
|
tableTag = line[0][9:13]
|
|
|
|
container = ttLib.getTableClass(tableTag)()
|
|
|
|
table = {'GSUB': parseGSUB,
|
|
|
|
'GPOS': parseGPOS,
|
|
|
|
'GDEF': parseGDEF,
|
|
|
|
}[tableTag](lines)
|
|
|
|
container.table = table
|
|
|
|
return container
|
|
|
|
|
2015-10-27 16:07:11 -07:00
|
|
|
|
|
|
|
class MockFont(object):
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self._glyphOrder = ['.notdef']
|
|
|
|
self._reverseGlyphOrder = {'.notdef': 0}
|
|
|
|
|
|
|
|
def getGlyphID(self, glyph):
|
|
|
|
gid = self._reverseGlyphOrder.get(glyph, None)
|
|
|
|
if gid is None:
|
|
|
|
gid = len(self._glyphOrder)
|
|
|
|
self._glyphOrder.append(glyph)
|
|
|
|
self._reverseGlyphOrder[glyph] = gid
|
|
|
|
return gid
|
|
|
|
|
|
|
|
def getGlyphName(self, gid):
|
|
|
|
return self._glyphOrder[gid]
|
|
|
|
|
2015-10-17 02:26:22 -03:00
|
|
|
if __name__ == '__main__':
|
|
|
|
import sys
|
2015-10-27 16:07:11 -07:00
|
|
|
font = MockFont()
|
2015-10-17 02:26:22 -03:00
|
|
|
for f in sys.argv[1:]:
|
|
|
|
debug("Processing", f)
|
2015-10-27 16:07:11 -07:00
|
|
|
table = compile(open(f, 'rt'))
|
|
|
|
blob = table.compile(font)
|
2015-10-17 00:47:12 -03:00
|
|
|
|