blob: 56d876caaa2b5585d22ecb66451ad7b703a5af22 [file] [log] [blame]
#!/usr/bin/python
# FontDame-to-FontTools for OpenType Layout tables
#
# Source language spec is available at:
# http://monotype.github.io/OpenType_Table_Source/otl_source.html
# https://github.com/Monotype/OpenType_Table_Source/
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.misc.py23 import *
from fontTools import ttLib
from fontTools.ttLib.tables._c_m_a_p import cmap_classes
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict
from fontTools.otlLib import builder as otl
from contextlib import contextmanager
from operator import setitem
import logging
import warnings
class MtiLibError(Exception): pass
class ReferenceNotFoundError(MtiLibError): pass
class FeatureNotFoundError(ReferenceNotFoundError): pass
class LookupNotFoundError(ReferenceNotFoundError): pass
log = logging.getLogger("fontTools.mtiLib")
def makeGlyph(s):
if s[:2] in ['U ', 'u ']:
return ttLib.TTFont._makeGlyphName(int(s[2:], 16))
elif s[:2] == '# ':
return "glyph%.5d" % int(s[2:])
assert s.find(' ') < 0, "Space found in glyph name: %s" % s
assert s, "Glyph name is empty"
return s
def makeGlyphs(l):
return [makeGlyph(g) for g in l]
def mapLookup(sym, mapping):
# Lookups are addressed by name. So resolved them using a map if available.
# Fallback to parsing as lookup index if a map isn't provided.
if mapping is not None:
try:
idx = mapping[sym]
except KeyError:
raise LookupNotFoundError(sym)
else:
idx = int(sym)
return idx
def mapFeature(sym, mapping):
# Features are referenced by index according the spec. So, if symbol is an
# integer, use it directly. Otherwise look up in the map if provided.
try:
idx = int(sym)
except ValueError:
try:
idx = mapping[sym]
except KeyError:
raise FeatureNotFoundError(sym)
return idx
def setReference(mapper, mapping, sym, setter, collection, key):
try:
mapped = mapper(sym, mapping)
except ReferenceNotFoundError as e:
try:
if mapping is not None:
mapping.addDeferredMapping(lambda ref: setter(collection, key, ref), sym, e)
return
except AttributeError:
pass
raise
setter(collection, key, mapped)
class DeferredMapping(dict):
def __init__(self):
self._deferredMappings = []
def addDeferredMapping(self, setter, sym, e):
log.debug("Adding deferred mapping for symbol '%s' %s", sym, type(e).__name__)
self._deferredMappings.append((setter,sym, e))
def applyDeferredMappings(self):
for setter,sym,e in self._deferredMappings:
log.debug("Applying deferred mapping for symbol '%s' %s", sym, type(e).__name__)
try:
mapped = self[sym]
except KeyError:
raise e
setter(mapped)
log.debug("Set to %s", mapped)
self._deferredMappings = []
def parseScriptList(lines, featureMap=None):
self = ot.ScriptList()
records = []
with lines.between('script table'):
for line in lines:
scriptTag, langSysTag, defaultFeature, features = line
log.debug("Adding script %s language-system %s", scriptTag, langSysTag)
langSys = ot.LangSys()
langSys.LookupOrder = None
if defaultFeature:
setReference(mapFeature, featureMap, defaultFeature, setattr, langSys, 'ReqFeatureIndex')
else:
langSys.ReqFeatureIndex = 0xFFFF
syms = stripSplitComma(features)
langSys.FeatureIndex = theList = [3] * len(syms)
for i,sym in enumerate(syms):
setReference(mapFeature, featureMap, sym, setitem, theList, i)
langSys.FeatureCount = len(langSys.FeatureIndex)
script = [s for s in records if s.ScriptTag == scriptTag]
if script:
script = script[0].Script
else:
scriptRec = ot.ScriptRecord()
scriptRec.ScriptTag = scriptTag
scriptRec.Script = ot.Script()
records.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)
for script in records:
script.Script.LangSysRecord = sorted(script.Script.LangSysRecord, key=lambda rec: rec.LangSysTag)
self.ScriptRecord = sorted(records, key=lambda rec: rec.ScriptTag)
self.ScriptCount = len(self.ScriptRecord)
return self
def parseFeatureList(lines, lookupMap=None, featureMap=None):
self = ot.FeatureList()
self.FeatureRecord = []
with lines.between('feature table'):
for line in lines:
name, featureTag, lookups = line
if featureMap is not None:
assert name not in featureMap, "Duplicate feature name: %s" % name
featureMap[name] = len(self.FeatureRecord)
# If feature name is integer, make sure it matches its index.
try:
assert int(name) == len(self.FeatureRecord), "%d %d" % (name, len(self.FeatureRecord))
except ValueError:
pass
featureRec = ot.FeatureRecord()
featureRec.FeatureTag = featureTag
featureRec.Feature = ot.Feature()
self.FeatureRecord.append(featureRec)
feature = featureRec.Feature
feature.FeatureParams = None
syms = stripSplitComma(lookups)
feature.LookupListIndex = theList = [None] * len(syms)
for i,sym in enumerate(syms):
setReference(mapLookup, lookupMap, sym, setitem, theList, i)
feature.LookupCount = len(feature.LookupListIndex)
self.FeatureCount = len(self.FeatureRecord)
return self
def parseLookupFlags(lines):
flags = 0
filterset = None
allFlags = [
'righttoleft',
'ignorebaseglyphs',
'ignoreligatures',
'ignoremarks',
'markattachmenttype',
'markfiltertype',
]
while lines.peeks()[0].lower() in allFlags:
line = next(lines)
flag = {
'righttoleft': 0x0001,
'ignorebaseglyphs': 0x0002,
'ignoreligatures': 0x0004,
'ignoremarks': 0x0008,
}.get(line[0].lower())
if flag:
assert line[1].lower() in ['yes', 'no'], line[1]
if line[1].lower() == 'yes':
flags |= flag
continue
if line[0].lower() == 'markattachmenttype':
flags |= int(line[1]) << 8
continue
if line[0].lower() == 'markfiltertype':
flags |= 0x10
filterset = int(line[1])
return flags, filterset
def parseSingleSubst(lines, font, _lookupMap=None):
mapping = {}
for line in lines:
assert len(line) == 2, line
line = makeGlyphs(line)
mapping[line[0]] = line[1]
return otl.buildSingleSubstSubtable(mapping)
def parseMultiple(lines, font, _lookupMap=None):
mapping = {}
for line in lines:
line = makeGlyphs(line)
mapping[line[0]] = line[1:]
return otl.buildMultipleSubstSubtable(mapping)
def parseAlternate(lines, font, _lookupMap=None):
mapping = {}
for line in lines:
line = makeGlyphs(line)
mapping[line[0]] = line[1:]
return otl.buildAlternateSubstSubtable(mapping)
def parseLigature(lines, font, _lookupMap=None):
mapping = {}
for line in lines:
assert len(line) >= 2, line
line = makeGlyphs(line)
mapping[tuple(line[1:])] = line[0]
return otl.buildLigatureSubstSubtable(mapping)
def parseSinglePos(lines, font, _lookupMap=None):
values = {}
for line in lines:
assert len(line) == 3, line
w = line[0].title().replace(' ', '')
assert w in valueRecordFormatDict
g = makeGlyph(line[1])
v = int(line[2])
if g not in values:
values[g] = ValueRecord()
assert not hasattr(values[g], w), (g, w)
setattr(values[g], w, v)
return otl.buildSinglePosSubtable(values, font.getReverseGlyphMap())
def parsePair(lines, font, _lookupMap=None):
self = ot.PairPos()
self.ValueFormat1 = self.ValueFormat2 = 0
typ = lines.peeks()[0].split()[0].lower()
if typ in ('left', 'right'):
self.Format = 1
values = {}
for line in lines:
assert len(line) == 4, line
side = line[0].split()[0].lower()
assert side in ('left', 'right'), side
what = line[0][len(side):].title().replace(' ', '')
mask = valueRecordFormatDict[what][0]
glyph1, glyph2 = makeGlyphs(line[1:3])
value = int(line[3])
if not glyph1 in values: values[glyph1] = {}
if not glyph2 in values[glyph1]: values[glyph1][glyph2] = (ValueRecord(),ValueRecord())
rec2 = values[glyph1][glyph2]
if side == 'left':
self.ValueFormat1 |= mask
vr = rec2[0]
else:
self.ValueFormat2 |= mask
vr = rec2[1]
assert not hasattr(vr, what), (vr, what)
setattr(vr, what, value)
self.Coverage = makeCoverage(set(values.keys()), font)
self.PairSet = []
for glyph1 in self.Coverage.glyphs:
values1 = values[glyph1]
pairset = ot.PairSet()
records = pairset.PairValueRecord = []
for glyph2 in sorted(values1.keys(), key=font.getGlyphID):
values2 = values1[glyph2]
pair = ot.PairValueRecord()
pair.SecondGlyph = glyph2
pair.Value1 = values2[0]
pair.Value2 = values2[1] if self.ValueFormat2 else None
records.append(pair)
pairset.PairValueCount = len(pairset.PairValueRecord)
self.PairSet.append(pairset)
self.PairSetCount = len(self.PairSet)
elif typ.endswith('class'):
self.Format = 2
classDefs = [None, None]
while lines.peeks()[0].endswith("class definition begin"):
typ = lines.peek()[0][:-len("class definition begin")].lower()
idx,klass = {
'first': (0,ot.ClassDef1),
'second': (1,ot.ClassDef2),
}[typ]
assert classDefs[idx] is None
classDefs[idx] = parseClassDef(lines, font, klass=klass)
self.ClassDef1, self.ClassDef2 = classDefs
self.Class1Count, self.Class2Count = (1+max(c.classDefs.values()) for c in classDefs)
self.Class1Record = [ot.Class1Record() for i in range(self.Class1Count)]
for rec1 in self.Class1Record:
rec1.Class2Record = [ot.Class2Record() for j in range(self.Class2Count)]
for rec2 in rec1.Class2Record:
rec2.Value1 = ValueRecord()
rec2.Value2 = ValueRecord()
for line in lines:
assert len(line) == 4, line
side = line[0].split()[0].lower()
assert side in ('left', 'right'), side
what = line[0][len(side):].title().replace(' ', '')
mask = valueRecordFormatDict[what][0]
class1, class2, value = (int(x) for x in line[1:4])
rec2 = self.Class1Record[class1].Class2Record[class2]
if side == 'left':
self.ValueFormat1 |= mask
vr = rec2.Value1
else:
self.ValueFormat2 |= mask
vr = rec2.Value2
assert not hasattr(vr, what), (vr, what)
setattr(vr, what, value)
for rec1 in self.Class1Record:
for rec2 in rec1.Class2Record:
rec2.Value1 = ValueRecord(self.ValueFormat1, rec2.Value1)
rec2.Value2 = ValueRecord(self.ValueFormat2, rec2.Value2) \
if self.ValueFormat2 else None
self.Coverage = makeCoverage(set(self.ClassDef1.classDefs.keys()), font)
else:
assert 0, typ
return self
def parseKernset(lines, font, _lookupMap=None):
typ = lines.peeks()[0].split()[0].lower()
if typ in ('left', 'right'):
with lines.until(("firstclass definition begin", "secondclass definition begin")):
return parsePair(lines, font)
return parsePair(lines, font)
def makeAnchor(data, klass=ot.Anchor):
assert len(data) <= 2
anchor = klass()
anchor.Format = 1
anchor.XCoordinate,anchor.YCoordinate = intSplitComma(data[0])
if len(data) > 1 and data[1] != '':
anchor.Format = 2
anchor.AnchorPoint = int(data[1])
return anchor
def parseCursive(lines, font, _lookupMap=None):
records = {}
for line in lines:
assert len(line) in [3,4], line
idx,klass = {
'entry': (0,ot.EntryAnchor),
'exit': (1,ot.ExitAnchor),
}[line[0]]
glyph = makeGlyph(line[1])
if glyph not in records:
records[glyph] = [None,None]
assert records[glyph][idx] is None, (glyph, idx)
records[glyph][idx] = makeAnchor(line[2:], klass)
return otl.buildCursivePosSubtable(records, font.getReverseGlyphMap())
def makeMarkRecords(data, coverage, c):
records = []
for glyph in coverage.glyphs:
klass, anchor = data[glyph]
record = c.MarkRecordClass()
record.Class = klass
setattr(record, c.MarkAnchor, anchor)
records.append(record)
return records
def makeBaseRecords(data, coverage, c, classCount):
records = []
idx = {}
for glyph in coverage.glyphs:
idx[glyph] = len(records)
record = c.BaseRecordClass()
anchors = [None] * classCount
setattr(record, c.BaseAnchor, anchors)
records.append(record)
for (glyph,klass),anchor in data.items():
record = records[idx[glyph]]
anchors = getattr(record, c.BaseAnchor)
assert anchors[klass] is None, (glyph, klass)
anchors[klass] = anchor
return records
def makeLigatureRecords(data, coverage, c, classCount):
records = [None] * len(coverage.glyphs)
idx = {g:i for i,g in enumerate(coverage.glyphs)}
for (glyph,klass,compIdx,compCount),anchor in data.items():
record = records[idx[glyph]]
if record is None:
record = records[idx[glyph]] = ot.LigatureAttach()
record.ComponentCount = compCount
record.ComponentRecord = [ot.ComponentRecord() for i in range(compCount)]
for compRec in record.ComponentRecord:
compRec.LigatureAnchor = [None] * classCount
assert record.ComponentCount == compCount, (glyph, record.ComponentCount, compCount)
anchors = record.ComponentRecord[compIdx - 1].LigatureAnchor
assert anchors[klass] is None, (glyph, compIdx, klass)
anchors[klass] = anchor
return records
def parseMarkToSomething(lines, font, c):
self = c.Type()
self.Format = 1
markData = {}
baseData = {}
Data = {
'mark': (markData, c.MarkAnchorClass),
'base': (baseData, c.BaseAnchorClass),
'ligature': (baseData, c.BaseAnchorClass),
}
maxKlass = 0
for line in lines:
typ = line[0]
assert typ in ('mark', 'base', 'ligature')
glyph = makeGlyph(line[1])
data, anchorClass = Data[typ]
extraItems = 2 if typ == 'ligature' else 0
extras = tuple(int(i) for i in line[2:2+extraItems])
klass = int(line[2+extraItems])
anchor = makeAnchor(line[3+extraItems:], anchorClass)
if typ == 'mark':
key,value = glyph,(klass,anchor)
else:
key,value = ((glyph,klass)+extras),anchor
assert key not in data, key
data[key] = value
maxKlass = max(maxKlass, klass)
# Mark
markCoverage = makeCoverage(set(markData.keys()), font, c.MarkCoverageClass)
markArray = c.MarkArrayClass()
markRecords = makeMarkRecords(markData, markCoverage, c)
setattr(markArray, c.MarkRecord, markRecords)
setattr(markArray, c.MarkCount, len(markRecords))
setattr(self, c.MarkCoverage, markCoverage)
setattr(self, c.MarkArray, markArray)
self.ClassCount = maxKlass + 1
# Base
self.classCount = 0 if not baseData else 1+max(k[1] for k,v in baseData.items())
baseCoverage = makeCoverage(set([k[0] for k in baseData.keys()]), font, c.BaseCoverageClass)
baseArray = c.BaseArrayClass()
if c.Base == 'Ligature':
baseRecords = makeLigatureRecords(baseData, baseCoverage, c, self.classCount)
else:
baseRecords = makeBaseRecords(baseData, baseCoverage, c, self.classCount)
setattr(baseArray, c.BaseRecord, baseRecords)
setattr(baseArray, c.BaseCount, len(baseRecords))
setattr(self, c.BaseCoverage, baseCoverage)
setattr(self, c.BaseArray, baseArray)
return self
class MarkHelper(object):
def __init__(self):
for Which in ('Mark', 'Base'):
for What in ('Coverage', 'Array', 'Count', 'Record', 'Anchor'):
key = Which + What
if Which == 'Mark' and What in ('Count', 'Record', 'Anchor'):
value = key
else:
value = getattr(self, Which) + What
if value == 'LigatureRecord':
value = 'LigatureAttach'
setattr(self, key, value)
if What != 'Count':
klass = getattr(ot, value)
setattr(self, key+'Class', klass)
class MarkToBaseHelper(MarkHelper):
Mark = 'Mark'
Base = 'Base'
Type = ot.MarkBasePos
class MarkToMarkHelper(MarkHelper):
Mark = 'Mark1'
Base = 'Mark2'
Type = ot.MarkMarkPos
class MarkToLigatureHelper(MarkHelper):
Mark = 'Mark'
Base = 'Ligature'
Type = ot.MarkLigPos
def parseMarkToBase(lines, font, _lookupMap=None):
return parseMarkToSomething(lines, font, MarkToBaseHelper())
def parseMarkToMark(lines, font, _lookupMap=None):
return parseMarkToSomething(lines, font, MarkToMarkHelper())
def parseMarkToLigature(lines, font, _lookupMap=None):
return parseMarkToSomething(lines, font, MarkToLigatureHelper())
def stripSplitComma(line):
return [s.strip() for s in line.split(',')] if line else []
def intSplitComma(line):
return [int(i) for i in line.split(',')] if line else []
# 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'
InputIdx = 1
DataLen = 3
else:
Chain = ''
InputIdx = 0
DataLen = 1
ChainTyp = Chain+Typ
self.Typ = Typ
self.Type = Type
self.Chain = Chain
self.ChainTyp = ChainTyp
self.InputIdx = InputIdx
self.DataLen = DataLen
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)
SetContextData = None
SetChainContextData = None
RuleData = lambda r:(r.Input,)
ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
def SetRuleData(r, d):
(r.Input,) = d
(r.GlyphCount,) = (len(x)+1 for x in d)
def ChainSetRuleData(r, d):
(r.Backtrack, r.Input, r.LookAhead) = d
(r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2]))
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)
def SetContextData(r, d):
(r.ClassDef,) = d
def SetChainContextData(r, d):
(r.BacktrackClassDef,
r.InputClassDef,
r.LookAheadClassDef) = d
RuleData = lambda r:(r.Class,)
ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
def SetRuleData(r, d):
(r.Class,) = d
(r.GlyphCount,) = (len(x)+1 for x in d)
def ChainSetRuleData(r, d):
(r.Backtrack, r.Input, r.LookAhead) = d
(r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2]))
elif Format == 3:
Coverage = lambda r: r.Coverage[0]
ChainCoverage = lambda r: r.InputCoverage[0]
ContextData = None
ChainContextData = None
SetContextData = None
SetChainContextData = None
RuleData = lambda r: r.Coverage
ChainRuleData = lambda r:(r.BacktrackCoverage +
r.InputCoverage +
r.LookAheadCoverage)
def SetRuleData(r, d):
(r.Coverage,) = d
(r.GlyphCount,) = (len(x) for x in d)
def ChainSetRuleData(r, d):
(r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d
(r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(x) for x in d)
else:
assert 0, "unknown format: %s" % Format
if Chain:
self.Coverage = ChainCoverage
self.ContextData = ChainContextData
self.SetContextData = SetChainContextData
self.RuleData = ChainRuleData
self.SetRuleData = ChainSetRuleData
else:
self.Coverage = Coverage
self.ContextData = ContextData
self.SetContextData = SetContextData
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, lookupMap=None):
klass = getattr(ot, klassName)
lst = []
for item in items:
rec = klass()
item = stripSplitComma(item)
assert len(item) == 2, item
idx = int(item[0])
assert idx > 0, idx
rec.SequenceIndex = idx - 1
setReference(mapLookup, lookupMap, item[1], setattr, rec, 'LookupListIndex')
lst.append(rec)
return lst
def makeClassDef(classDefs, font, klass=ot.Coverage):
if not classDefs: return None
self = klass()
self.classDefs = dict(classDefs)
return self
def parseClassDef(lines, font, klass=ot.ClassDef):
classDefs = {}
with lines.between('class definition'):
for line in lines:
glyph = makeGlyph(line[0])
assert glyph not in classDefs, glyph
classDefs[glyph] = int(line[1])
return makeClassDef(classDefs, font, klass)
def makeCoverage(glyphs, font, klass=ot.Coverage):
if not glyphs: return None
if isinstance(glyphs, set):
glyphs = sorted(glyphs)
coverage = klass()
coverage.glyphs = sorted(set(glyphs), key=font.getGlyphID)
return coverage
def parseCoverage(lines, font, klass=ot.Coverage):
glyphs = []
with lines.between('coverage definition'):
for line in lines:
glyphs.append(makeGlyph(line[0]))
return makeCoverage(glyphs, font, klass)
def bucketizeRules(self, c, rules, bucketKeys):
buckets = {}
for seq,recs in rules:
buckets.setdefault(seq[c.InputIdx][0], []).append((tuple(s[1 if i==c.InputIdx else 0:] for i,s in enumerate(seq)), 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)()
c.SetRuleData(rule, 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(lines, font, Type, lookupMap=None):
self = getattr(ot, Type)()
typ = lines.peeks()[0].split()[0].lower()
if typ == 'glyph':
self.Format = 1
log.debug("Parsing %s format %s", Type, self.Format)
c = ContextHelper(Type, self.Format)
rules = []
for line in lines:
assert line[0].lower() == 'glyph', line[0]
while len(line) < 1+c.DataLen: line.append('')
seq = tuple(makeGlyphs(stripSplitComma(i)) for i in line[1:1+c.DataLen])
recs = parseLookupRecords(line[1+c.DataLen:], c.LookupRecord, lookupMap)
rules.append((seq, recs))
firstGlyphs = set(seq[c.InputIdx][0] for seq,recs in rules)
self.Coverage = makeCoverage(firstGlyphs, font)
bucketizeRules(self, c, rules, self.Coverage.glyphs)
elif typ.endswith('class'):
self.Format = 2
log.debug("Parsing %s format %s", Type, self.Format)
c = ContextHelper(Type, self.Format)
classDefs = [None] * c.DataLen
while lines.peeks()[0].endswith("class definition begin"):
typ = lines.peek()[0][:-len("class definition begin")].lower()
idx,klass = {
1: {
'': (0,ot.ClassDef),
},
3: {
'backtrack': (0,ot.BacktrackClassDef),
'': (1,ot.InputClassDef),
'lookahead': (2,ot.LookAheadClassDef),
},
}[c.DataLen][typ]
assert classDefs[idx] is None, idx
classDefs[idx] = parseClassDef(lines, font, klass=klass)
c.SetContextData(self, classDefs)
rules = []
for line in lines:
assert line[0].lower().startswith('class'), line[0]
while len(line) < 1+c.DataLen: line.append('')
seq = tuple(intSplitComma(i) for i in line[1:1+c.DataLen])
recs = parseLookupRecords(line[1+c.DataLen:], c.LookupRecord, lookupMap)
rules.append((seq, recs))
firstClasses = set(seq[c.InputIdx][0] for seq,recs in rules)
firstGlyphs = set(g for g,c in classDefs[c.InputIdx].classDefs.items() if c in firstClasses)
self.Coverage = makeCoverage(firstGlyphs, font)
bucketizeRules(self, c, rules, range(max(firstClasses) + 1))
elif typ.endswith('coverage'):
self.Format = 3
log.debug("Parsing %s format %s", Type, self.Format)
c = ContextHelper(Type, self.Format)
coverages = tuple([] for i in range(c.DataLen))
while lines.peeks()[0].endswith("coverage definition begin"):
typ = lines.peek()[0][:-len("coverage definition begin")].lower()
idx,klass = {
1: {
'': (0,ot.Coverage),
},
3: {
'backtrack': (0,ot.BacktrackCoverage),
'input': (1,ot.InputCoverage),
'lookahead': (2,ot.LookAheadCoverage),
},
}[c.DataLen][typ]
coverages[idx].append(parseCoverage(lines, font, klass=klass))
c.SetRuleData(self, coverages)
lines = list(lines)
assert len(lines) == 1
line = lines[0]
assert line[0].lower() == 'coverage', line[0]
recs = parseLookupRecords(line[1:], c.LookupRecord, lookupMap)
setattr(self, c.Type+'Count', len(recs))
setattr(self, c.LookupRecord, recs)
else:
assert 0, typ
return self
def parseContextSubst(lines, font, lookupMap=None):
return parseContext(lines, font, "ContextSubst", lookupMap=lookupMap)
def parseContextPos(lines, font, lookupMap=None):
return parseContext(lines, font, "ContextPos", lookupMap=lookupMap)
def parseChainedSubst(lines, font, lookupMap=None):
return parseContext(lines, font, "ChainContextSubst", lookupMap=lookupMap)
def parseChainedPos(lines, font, lookupMap=None):
return parseContext(lines, font, "ChainContextPos", lookupMap=lookupMap)
def parseReverseChainedSubst(lines, font, _lookupMap=None):
self = ot.ReverseChainSingleSubst()
self.Format = 1
coverages = ([], [])
while lines.peeks()[0].endswith("coverage definition begin"):
typ = lines.peek()[0][:-len("coverage definition begin")].lower()
idx,klass = {
'backtrack': (0,ot.BacktrackCoverage),
'lookahead': (1,ot.LookAheadCoverage),
}[typ]
coverages[idx].append(parseCoverage(lines, font, klass=klass))
self.BacktrackCoverage = coverages[0]
self.BacktrackGlyphCount = len(self.BacktrackCoverage)
self.LookAheadCoverage = coverages[1]
self.LookAheadGlyphCount = len(self.LookAheadCoverage)
mapping = {}
for line in lines:
assert len(line) == 2, line
line = makeGlyphs(line)
mapping[line[0]] = line[1]
self.Coverage = makeCoverage(set(mapping.keys()), font)
self.Substitute = [mapping[k] for k in self.Coverage.glyphs]
self.GlyphCount = len(self.Substitute)
return self
def parseLookup(lines, tableTag, font, lookupMap=None):
line = lines.expect('lookup')
_, name, typ = line
log.debug("Parsing lookup type %s %s", typ, name)
lookup = ot.Lookup()
lookup.LookupFlag,filterset = parseLookupFlags(lines)
if filterset is not None:
lookup.MarkFilteringSet = filterset
lookup.LookupType, parseLookupSubTable = {
'GSUB': {
'single': (1, parseSingleSubst),
'multiple': (2, parseMultiple),
'alternate': (3, parseAlternate),
'ligature': (4, parseLigature),
'context': (5, parseContextSubst),
'chained': (6, parseChainedSubst),
'reversechained':(8, parseReverseChainedSubst),
},
'GPOS': {
'single': (1, parseSinglePos),
'pair': (2, parsePair),
'kernset': (2, parseKernset),
'cursive': (3, parseCursive),
'mark to base': (4, parseMarkToBase),
'mark to ligature':(5, parseMarkToLigature),
'mark to mark': (6, parseMarkToMark),
'context': (7, parseContextPos),
'chained': (8, parseChainedPos),
},
}[tableTag][typ]
with lines.until('lookup end'):
subtables = []
while lines.peek():
with lines.until(('% subtable', 'subtable end')):
while lines.peek():
subtable = parseLookupSubTable(lines, font, lookupMap)
assert lookup.LookupType == subtable.LookupType
subtables.append(subtable)
if lines.peeks()[0] in ('% subtable', 'subtable end'):
next(lines)
lines.expect('lookup end')
lookup.SubTable = subtables
lookup.SubTableCount = len(lookup.SubTable)
if lookup.SubTableCount is 0:
# Remove this return when following is fixed:
# https://github.com/fonttools/fonttools/issues/789
return None
return lookup
def parseGSUBGPOS(lines, font, tableTag):
container = ttLib.getTableClass(tableTag)()
lookupMap = DeferredMapping()
featureMap = DeferredMapping()
assert tableTag in ('GSUB', 'GPOS')
log.debug("Parsing %s", tableTag)
self = getattr(ot, tableTag)()
self.Version = 0x00010000
fields = {
'script table begin':
('ScriptList',
lambda lines: parseScriptList (lines, featureMap)),
'feature table begin':
('FeatureList',
lambda lines: parseFeatureList (lines, lookupMap, featureMap)),
'lookup':
('LookupList',
None),
}
for attr,parser in fields.values():
setattr(self, attr, None)
while lines.peek() is not None:
typ = lines.peek()[0].lower()
if typ not in fields:
log.debug('Skipping %s', lines.peek())
next(lines)
continue
attr,parser = fields[typ]
if typ == 'lookup':
if self.LookupList is None:
self.LookupList = ot.LookupList()
self.LookupList.Lookup = []
_, name, _ = lines.peek()
lookup = parseLookup(lines, tableTag, font, lookupMap)
if lookupMap is not None:
assert name not in lookupMap, "Duplicate lookup name: %s" % name
lookupMap[name] = len(self.LookupList.Lookup)
else:
assert int(name) == len(self.LookupList.Lookup), "%d %d" % (name, len(self.Lookup))
self.LookupList.Lookup.append(lookup)
else:
assert getattr(self, attr) is None, attr
setattr(self, attr, parser(lines))
if self.LookupList:
self.LookupList.LookupCount = len(self.LookupList.Lookup)
if lookupMap is not None:
lookupMap.applyDeferredMappings()
if featureMap is not None:
featureMap.applyDeferredMappings()
container.table = self
return container
def parseGSUB(lines, font):
return parseGSUBGPOS(lines, font, 'GSUB')
def parseGPOS(lines, font):
return parseGSUBGPOS(lines, font, 'GPOS')
def parseAttachList(lines, font):
points = {}
with lines.between('attachment list'):
for line in lines:
glyph = makeGlyph(line[0])
assert glyph not in points, glyph
points[glyph] = [int(i) for i in line[1:]]
return otl.buildAttachList(points, font.getReverseGlyphMap())
def parseCaretList(lines, font):
carets = {}
with lines.between('carets'):
for line in lines:
glyph = makeGlyph(line[0])
assert glyph not in carets, glyph
num = int(line[1])
thisCarets = [int(i) for i in line[2:]]
assert num == len(thisCarets), line
carets[glyph] = thisCarets
return otl.buildLigCaretList(carets, {}, font.getReverseGlyphMap())
def makeMarkFilteringSets(sets, font):
self = ot.MarkGlyphSetsDef()
self.MarkSetTableFormat = 1
self.MarkSetCount = 1 + max(sets.keys())
self.Coverage = [None] * self.MarkSetCount
for k,v in sorted(sets.items()):
self.Coverage[k] = makeCoverage(set(v), font)
return self
def parseMarkFilteringSets(lines, font):
sets = {}
with lines.between('set definition'):
for line in lines:
assert len(line) == 2, line
glyph = makeGlyph(line[0])
# TODO accept set names
st = int(line[1])
if st not in sets:
sets[st] = []
sets[st].append(glyph)
return makeMarkFilteringSets(sets, font)
def parseGDEF(lines, font):
container = ttLib.getTableClass('GDEF')()
log.debug("Parsing GDEF")
self = ot.GDEF()
fields = {
'class definition begin':
('GlyphClassDef',
lambda lines, font: parseClassDef(lines, font, klass=ot.GlyphClassDef)),
'attachment list begin':
('AttachList', parseAttachList),
'carets begin':
('LigCaretList', parseCaretList),
'mark attachment class definition begin':
('MarkAttachClassDef',
lambda lines, font: parseClassDef(lines, font, klass=ot.MarkAttachClassDef)),
'markfilter set definition begin':
('MarkGlyphSetsDef', parseMarkFilteringSets),
}
for attr,parser in fields.values():
setattr(self, attr, None)
while lines.peek() is not None:
typ = lines.peek()[0].lower()
if typ not in fields:
log.debug('Skipping %s', typ)
next(lines)
continue
attr,parser = fields[typ]
assert getattr(self, attr) is None, attr
setattr(self, attr, parser(lines, font))
self.Version = 0x00010000 if self.MarkGlyphSetsDef is None else 0x00010002
container.table = self
return container
def parseCmap(lines, font):
container = ttLib.getTableClass('cmap')()
log.debug("Parsing cmap")
tables = []
while lines.peek() is not None:
lines.expect('cmap subtable %d' % len(tables))
platId, encId, fmt, lang = [
parseCmapId(lines, field)
for field in ('platformID', 'encodingID', 'format', 'language')]
table = cmap_classes[fmt](fmt)
table.platformID = platId
table.platEncID = encId
table.language = lang
table.cmap = {}
line = next(lines)
while line[0] != 'end subtable':
table.cmap[int(line[0], 16)] = line[1]
line = next(lines)
tables.append(table)
container.tableVersion = 0
container.tables = tables
return container
def parseCmapId(lines, field):
line = next(lines)
assert field == line[0]
return int(line[1])
def parseTable(lines, font, tableTag=None):
log.debug("Parsing table")
line = lines.peeks()
tag = None
if line[0].split()[0] == 'FontDame':
tag = line[0].split()[1]
elif ' '.join(line[0].split()[:3]) == 'Font Chef Table':
tag = line[0].split()[3]
if tag is not None:
next(lines)
tag = tag.ljust(4)
if tableTag is None:
tableTag = tag
else:
assert tableTag == tag, (tableTag, tag)
assert tableTag is not None, "Don't know what table to parse and data doesn't specify"
return {
'GSUB': parseGSUB,
'GPOS': parseGPOS,
'GDEF': parseGDEF,
'cmap': parseCmap,
}[tableTag](lines, font)
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)
try:
self.filename = f.name
except:
self.filename = None
self.lines = iter(lines)
self.line = ''
self.lineno = 0
self.stoppers = []
self.buffer = None
def __iter__(self):
return self
def _next_line(self):
self.lineno += 1
line = self.line = next(self.lines)
line = [s.strip() for s in line.split('\t')]
if len(line) == 1 and not line[0]:
del line[0]
if line and not line[-1]:
warnings.warn('trailing tab found on line %d: %s' % (self.lineno, self.line))
while line and not line[-1]:
del line[-1]
return line
def _next_nonempty(self):
while True:
line = self._next_line()
# Skip comments and empty lines
if line and line[0] and (line[0][0] != '%' or line[0] == '% subtable'):
return line
def _next_buffered(self):
if self.buffer:
ret = self.buffer
self.buffer = None
return ret
else:
return self._next_nonempty()
def __next__(self):
line = self._next_buffered()
if line[0].lower() in self.stoppers:
self.buffer = line
raise StopIteration
return line
def next(self):
return self.__next__()
def peek(self):
if not self.buffer:
try:
self.buffer = self._next_nonempty()
except StopIteration:
return None
if self.buffer[0].lower() in self.stoppers:
return None
return self.buffer
def peeks(self):
ret = self.peek()
return ret if ret is not None else ('',)
@contextmanager
def between(self, tag):
start = tag + ' begin'
end = tag + ' end'
self.expectendswith(start)
self.stoppers.append(end)
yield
del self.stoppers[-1]
self.expect(tag + ' end')
@contextmanager
def until(self, tags):
if type(tags) is not tuple:
tags = (tags,)
self.stoppers.extend(tags)
yield
del self.stoppers[-len(tags):]
def expect(self, s):
line = next(self)
tag = line[0].lower()
assert tag == s, "Expected '%s', got '%s'" % (s, tag)
return line
def expectendswith(self, s):
line = next(self)
tag = line[0].lower()
assert tag.endswith(s), "Expected '*%s', got '%s'" % (s, tag)
return line
def build(f, font, tableTag=None):
lines = Tokenizer(f)
return parseTable(lines, font, tableTag=tableTag)
def main(args=None, font=None):
import sys
from fontTools import configLogger
from fontTools.misc.testTools import MockFont
if args is None:
args = sys.argv[1:]
# configure the library logger (for >= WARNING)
configLogger()
# comment this out to enable debug messages from mtiLib's logger
# log.setLevel(logging.DEBUG)
if font is None:
font = MockFont()
tableTag = None
if args[0].startswith('-t'):
tableTag = args[0][2:]
del args[0]
for f in args:
log.debug("Processing %s", f)
table = build(open(f, 'rt', encoding="utf-8"), font, tableTag=tableTag)
blob = table.compile(font) # Make sure it compiles
decompiled = table.__class__()
decompiled.decompile(blob, font) # Make sure it decompiles!
#continue
from fontTools.misc import xmlWriter
tag = table.tableTag
writer = xmlWriter.XMLWriter(sys.stdout)
writer.begintag(tag)
writer.newline()
#table.toXML(writer, font)
decompiled.toXML(writer, font)
writer.endtag(tag)
writer.newline()
if __name__ == '__main__':
import sys
sys.exit(main())