fonttools/fd2ft.py

582 lines
16 KiB
Python
Raw Normal View History

2015-10-17 00:47:12 -03:00
#!/usr/bin/python
2015-11-24 15:01:11 -06:00
# FontDame-to-FontTools for OpenType Layout tables
#
# Source language spec is available at:
# https://rawgit.com/Monotype/OpenType_Table_Source/master/otl_source.html
# https://github.com/Monotype/OpenType_Table_Source/
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
2015-12-08 16:21:59 +01:00
def parseGlyph(s):
return s
def parseGlyphs(l):
return [parseGlyph(g) for g in l]
2015-10-17 02:26:22 -03:00
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
2015-12-07 21:54:53 +01:00
langSys.FeatureIndex = intSplitComma(features)
2015-10-17 02:26:22 -03:00
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
2015-12-07 21:54:53 +01:00
feature.LookupListIndex = intSplitComma(lookups)
2015-10-17 02:26:22 -03:00
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-12-07 21:54:53 +01:00
def parseClassDef(lines, klass=ot.ClassDef):
line = next(lines)
assert line[0].endswith('class definition begin'), line
self = klass()
classDefs = self.classDefs = {}
for line in lines.readUntil('class definition end'):
2015-12-08 16:21:59 +01:00
classDefs[parseGlyph(line[0])] = int(line[1])
2015-12-07 21:54:53 +01:00
return self
def parseSingleSubst(self, lines, font):
2015-10-27 12:44:32 -07:00
self.mapping = {}
for line in lines:
assert len(line) == 2, line
2015-12-08 16:21:59 +01:00
line = parseGlyphs(line)
2015-10-27 12:44:32 -07:00
self.mapping[line[0]] = line[1]
2015-12-07 21:54:53 +01:00
def parseMultiple(self, lines, font):
2015-10-27 14:16:00 -07:00
self.mapping = {}
for line in lines:
2015-12-08 16:21:59 +01:00
line = parseGlyphs(line)
2015-10-27 14:16:00 -07:00
self.mapping[line[0]] = line[1:]
2015-10-17 03:30:31 -03:00
2015-12-07 21:54:53 +01:00
def parseAlternate(self, lines, font):
2015-10-27 14:16:00 -07:00
self.alternates = {}
for line in lines:
2015-12-08 16:21:59 +01:00
line = parseGlyphs(line)
2015-10-27 14:16:00 -07:00
self.alternates[line[0]] = line[1:]
2015-10-17 03:30:31 -03:00
2015-12-07 21:54:53 +01:00
def parseLigature(self, lines, font):
2015-10-27 12:44:32 -07:00
self.ligatures = {}
for line in lines:
assert len(line) >= 2, line
2015-12-08 16:21:59 +01:00
line = parseGlyphs(line)
2015-12-07 21:54:53 +01:00
# The following single line can replace the rest of this function with fontTools >= 3.1
#self.ligatures[tuple(line[1:])] = line[0]
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)
def parseSinglePos(self, lines, font):
2015-10-17 03:30:31 -03:00
raise NotImplementedError
2015-12-07 21:54:53 +01:00
def parsePair(self, lines, font):
2015-10-17 03:30:31 -03:00
raise NotImplementedError
2015-12-07 21:54:53 +01:00
def parseCursive(self, lines, font):
2015-10-17 03:30:31 -03:00
raise NotImplementedError
2015-12-07 21:54:53 +01:00
def parseMarkToSomething(self, lines, font):
2015-10-17 03:30:31 -03:00
raise NotImplementedError
2015-12-07 21:54:53 +01:00
def parseMarkToLigature(self, lines, font):
2015-10-17 03:30:31 -03:00
raise NotImplementedError
2015-12-07 21:54:53 +01:00
def stripSplitComma(line):
return [s.strip() for s in line.split(',')]
def intSplitComma(line):
return [int(i) for i in line.split(',')]
# Copied from fontTools.subset
class ContextHelper(object):
def __init__(self, klassName, Format):
if klassName.endswith('Subst'):
Typ = 'Sub'
Type = 'Subst'
else:
Typ = 'Pos'
Type = 'Pos'
if klassName.startswith('Chain'):
Chain = 'Chain'
else:
Chain = ''
ChainTyp = Chain+Typ
self.Typ = Typ
self.Type = Type
self.Chain = Chain
self.ChainTyp = ChainTyp
self.LookupRecord = Type+'LookupRecord'
if Format == 1:
Coverage = lambda r: r.Coverage
ChainCoverage = lambda r: r.Coverage
ContextData = lambda r:(None,)
ChainContextData = lambda r:(None, None, None)
RuleData = lambda r:(r.Input,)
ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
SetRuleData = None
ChainSetRuleData = None
elif Format == 2:
Coverage = lambda r: r.Coverage
ChainCoverage = lambda r: r.Coverage
ContextData = lambda r:(r.ClassDef,)
ChainContextData = lambda r:(r.BacktrackClassDef,
r.InputClassDef,
r.LookAheadClassDef)
RuleData = lambda r:(r.Class,)
ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
def SetRuleData(r, d):(r.Class,) = d
def ChainSetRuleData(r, d):(r.Backtrack, r.Input, r.LookAhead) = d
elif Format == 3:
Coverage = lambda r: r.Coverage[0]
ChainCoverage = lambda r: r.InputCoverage[0]
ContextData = None
ChainContextData = None
RuleData = lambda r: r.Coverage
ChainRuleData = lambda r:(r.BacktrackCoverage +
r.InputCoverage +
r.LookAheadCoverage)
SetRuleData = None
ChainSetRuleData = None
else:
assert 0, "unknown format: %s" % Format
if Chain:
self.Coverage = ChainCoverage
self.ContextData = ChainContextData
self.RuleData = ChainRuleData
self.SetRuleData = ChainSetRuleData
else:
self.Coverage = Coverage
self.ContextData = ContextData
self.RuleData = RuleData
self.SetRuleData = SetRuleData
if Format == 1:
self.Rule = ChainTyp+'Rule'
self.RuleCount = ChainTyp+'RuleCount'
self.RuleSet = ChainTyp+'RuleSet'
self.RuleSetCount = ChainTyp+'RuleSetCount'
self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
elif Format == 2:
self.Rule = ChainTyp+'ClassRule'
self.RuleCount = ChainTyp+'ClassRuleCount'
self.RuleSet = ChainTyp+'ClassSet'
self.RuleSetCount = ChainTyp+'ClassSetCount'
self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c
else (set(glyphs) if r == 0 else set()))
self.ClassDef = 'InputClassDef' if Chain else 'ClassDef'
self.ClassDefIndex = 1 if Chain else 0
self.Input = 'Input' if Chain else 'Class'
def parseLookupRecords(items, klassName):
klass = getattr(ot, klassName)
lst = []
for item in items:
rec = klass()
item = intSplitComma(item)
assert len(item) == 2, item
assert item[0] > 0, item[0]
rec.SequenceIndex = item[0] - 1
2015-12-08 11:30:32 +01:00
# TODO The following line should use lazy lookup name-to-index mapping
2015-12-07 21:54:53 +01:00
rec.LookupListIndex = item[1]
lst.append(rec)
return lst
2015-12-08 16:09:29 +01:00
def makeCoverage(glyphs, fonts, klass=ot.Coverage):
coverage = klass()
2015-12-07 21:54:53 +01:00
coverage.glyphs = sorted(set(glyphs), key=font.getGlyphID)
return coverage
2015-12-08 16:09:29 +01:00
def parseCoverage(lines, font, klass=ot.Coverage):
line = next(lines)
assert line[0].endswith('coverage definition begin'), line
glyphs = []
for line in lines.readUntil('coverage definition end'):
2015-12-08 16:21:59 +01:00
glyphs.append(parseGlyph(line[0]))
2015-12-08 16:09:29 +01:00
return makeCoverage(glyphs, font, klass)
2015-12-07 21:54:53 +01:00
def bucketizeRules(self, c, rules, bucketKeys):
buckets = {}
for seq,recs in rules:
buckets.setdefault(seq[0], []).append((seq[1:], recs))
rulesets = []
for firstGlyph in bucketKeys:
if firstGlyph not in buckets:
rulesets.append(None)
continue
thisRules = []
for seq,recs in buckets[firstGlyph]:
rule = getattr(ot, c.Rule)()
rule.GlyphCount = 1 + len(seq)
rule.Input = seq
setattr(rule, c.Type+'Count', len(recs))
setattr(rule, c.LookupRecord, recs)
thisRules.append(rule)
ruleset = getattr(ot, c.RuleSet)()
setattr(ruleset, c.Rule, thisRules)
setattr(ruleset, c.RuleCount, len(thisRules))
rulesets.append(ruleset)
setattr(self, c.RuleSet, rulesets)
setattr(self, c.RuleSetCount, len(rulesets))
def parseContext(self, lines, font, Type):
2015-12-08 16:09:29 +01:00
typ = lines.peek()[0].split()[0]
2015-10-27 12:44:32 -07:00
if typ == 'glyph':
2015-10-27 16:07:11 -07:00
self.Format = 1
2015-12-07 21:54:53 +01:00
c = ContextHelper('Context'+Type, self.Format)
rules = []
for line in lines:
recs = parseLookupRecords(line[2:], c.LookupRecord)
2015-12-08 16:21:59 +01:00
seq = parseGlyphs(stripSplitComma(line[1]))
2015-12-07 21:54:53 +01:00
rules.append((seq, recs))
self.Coverage = makeCoverage((seq[0] for seq,recs in rules), font)
bucketizeRules(self, c, rules, self.Coverage.glyphs)
2015-12-08 16:09:29 +01:00
elif typ == 'class':
2015-10-27 16:07:11 -07:00
self.Format = 2
2015-12-07 21:54:53 +01:00
c = ContextHelper('Context'+Type, self.Format)
self.ClassDef = parseClassDef(lines)
rules = []
for line in lines:
recs = parseLookupRecords(line[2:], c.LookupRecord)
seq = intSplitComma(line[1])
rules.append((seq, recs))
self.Coverage = makeCoverage(self.ClassDef.classDefs.keys(), font)
maxClass = max(seq[0] for seq,recs in rules)
bucketizeRules(self, c, rules, range(maxClass + 1))
2015-12-08 16:09:29 +01:00
elif typ == 'coverage':
self.Format = 3
c = ContextHelper('Context'+Type, self.Format)
self.Coverage = []
while lines.peek()[0].endswith("coverage definition begin"):
self.Coverage.append(parseCoverage(lines, font))
self.GlyphCount = len(self.Coverage)
lines = list(lines)
assert len(lines) == 1
line = lines[0]
assert line[0] == 'coverage'
recs = parseLookupRecords(line[1:], c.LookupRecord)
setattr(self, c.Type+'Count', len(recs))
setattr(self, c.LookupRecord, recs)
else:
assert 0
2015-10-17 03:30:31 -03:00
2015-12-07 21:54:53 +01:00
def parseContextSubst(self, lines, font):
return parseContext(self, lines, font, "Subst")
def parseContextPos(self, lines, font):
return parseContext(self, lines, font, "Pos")
def parseChained(self, lines, font, Type):
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-12-07 21:54:53 +01:00
for line in lines:
print (line)
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-12-07 21:54:53 +01:00
def parseChainedSubst(self, lines, font):
return parseChained(self, lines, font, "Subst")
def parseChainedPos(self, lines):
return parseChained(self, lines, font, "Pos")
2015-12-08 14:37:33 +01:00
def parseLookup(lines, tableTag, font):
line = lines.skipUntil('lookup')
if line is None: return None, None
lookupLines = lines.readUntil('lookup end')
_, name, typ = line
lookup = ot.Lookup()
lookup.LookupFlag = parseLookupFlags(lookupLines)
lookup.LookupType, parseLookupSubTable = {
'GSUB': {
'single': (1, parseSingleSubst),
'multiple': (2, parseMultiple),
'alternate': (3, parseAlternate),
'ligature': (4, parseLigature),
'context': (5, parseContextSubst),
'chained': (6, parseChainedSubst),
},
'GPOS': {
'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, parseContextPos),
'chained': (8, parseChainedPos),
},
}[tableTag][typ]
subtable = ot.lookupTypes[tableTag][lookup.LookupType]()
subtable.LookupType = lookup.LookupType
parseLookupSubTable(subtable, lookupLines, font)
lookup.SubTable = [subtable]
lookup.SubTableCount = len(lookup.SubTable)
return lookup, name
2015-12-07 21:54:53 +01:00
def parseLookupList(lines, tableTag, font):
2015-10-17 02:55:00 -03:00
self = ot.LookupList()
self.Lookup = []
while True:
2015-12-08 14:37:33 +01:00
lookup, name = parseLookup(lines, tableTag, font)
if lookup is None: break
assert int(name) == len(self.Lookup), "%d %d" % (name, len(self.Lookup))
2015-10-17 02:55:00 -03:00
self.Lookup.append(lookup)
self.LookupCount = len(self.Lookup)
return self
2015-10-17 02:26:22 -03:00
2015-12-07 21:54:53 +01:00
def parseGSUB(lines, font):
2015-10-17 02:26:22 -03:00
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-12-07 21:54:53 +01:00
self.LookupList = parseLookupList(lines, 'GSUB', font)
2015-10-17 02:26:22 -03:00
return self
2015-12-07 21:54:53 +01:00
def parseGPOS(lines, font):
2015-10-17 02:26:22 -03:00
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-12-07 21:54:53 +01:00
self.LookupList = parseLookupList(lines, 'GPOS', font)
2015-10-17 02:26:22 -03:00
return self
2015-12-07 21:54:53 +01:00
def parseGDEF(lines, font):
2015-10-17 02:26:22 -03:00
debug("Parsing GDEF TODO")
return None
2015-12-07 21:54:53 +01:00
class ReadUntilMixin(object):
def _readUntil(self, what):
for line in self:
if line[0] == what:
raise StopIteration
yield line
def readUntil(self, what):
return BufferedIter(self._readUntil(what))
class BufferedIter(ReadUntilMixin):
2015-10-27 14:16:00 -07:00
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-12-07 21:54:53 +01:00
class Tokenizer(ReadUntilMixin):
2015-10-27 12:44:32 -07:00
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
2015-12-08 16:55:41 +01:00
if line[0] and line[0][0] != '%':
2015-10-27 12:44:32 -07:00
return line
def skipUntil(self, what):
for line in self:
if line[0] == what:
return line
2015-12-08 14:56:23 +01:00
def parseTable(lines, font):
debug("Parsing table")
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,
2015-12-07 21:54:53 +01:00
}[tableTag](lines, font)
2015-10-17 02:26:22 -03:00
container.table = table
return container
2015-12-08 14:56:23 +01:00
def build(f, font):
lines = Tokenizer(f)
return parseTable(lines, font)
2015-10-27 16:07:11 -07:00
class MockFont(object):
def __init__(self):
self._glyphOrder = ['.notdef']
self._reverseGlyphOrder = {'.notdef': 0}
2015-12-07 12:12:11 +01:00
self.lazy = False
2015-10-27 16:07:11 -07:00
2015-12-07 12:12:11 +01:00
def getGlyphID(self, glyph, requireReal=None):
2015-10-27 16:07:11 -07:00
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-12-07 12:12:11 +01:00
def getGlyphOrder(self):
return self._glyphOrder
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-12-07 21:54:53 +01:00
table = build(open(f, 'rt'), font)
2015-10-27 16:07:11 -07:00
blob = table.compile(font)
2015-12-07 12:12:11 +01:00
decompiled = table.__class__()
decompiled.decompile(blob, font)
2015-12-08 14:37:33 +01:00
continue
2015-12-07 12:12:11 +01:00
from fontTools.misc import xmlWriter
tag = table.tableTag
writer = xmlWriter.XMLWriter(sys.stdout)
writer.begintag(tag)
writer.newline()
2015-12-07 21:54:53 +01:00
#table.toXML(writer, font)
2015-12-07 12:12:11 +01:00
decompiled.toXML(writer, font)
writer.endtag(tag)
writer.newline()
2015-10-17 00:47:12 -03:00