You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1231 lines
37 KiB
1231 lines
37 KiB
#!/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 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
|
|
|
|
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:
|
|
while len(line) < 4:
|
|
line.append('')
|
|
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 == 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]:
|
|
log.warning('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):
|
|
"""Convert a Monotype font layout file to an OpenType layout object
|
|
|
|
A font object must be passed, but this may be a "dummy" font; it is only
|
|
used for sorting glyph sets when making coverage tables and to hold the
|
|
OpenType layout table while it is being built.
|
|
|
|
Args:
|
|
f: A file object.
|
|
font (TTFont): A font object.
|
|
tableTag (string): If provided, asserts that the file contains data for the
|
|
given OpenType table.
|
|
|
|
Returns:
|
|
An object representing the table. (e.g. ``table_G_S_U_B_``)
|
|
"""
|
|
lines = Tokenizer(f)
|
|
return parseTable(lines, font, tableTag=tableTag)
|
|
|
|
|
|
def main(args=None, font=None):
|
|
"""Convert a FontDame OTL file to TTX XML.
|
|
|
|
Writes XML output to stdout.
|
|
|
|
Args:
|
|
args: Command line arguments (``--font``, ``--table``, input files).
|
|
"""
|
|
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)
|
|
|
|
import argparse
|
|
parser = argparse.ArgumentParser(
|
|
"fonttools mtiLib",
|
|
description=main.__doc__,
|
|
)
|
|
|
|
parser.add_argument('--font', '-f', metavar='FILE', dest="font",
|
|
help="Input TTF files (used for glyph classes and sorting coverage tables)")
|
|
parser.add_argument('--table', '-t', metavar='TABLE', dest="tableTag",
|
|
help="Table to fill (sniffed from input file if not provided)")
|
|
parser.add_argument('inputs', metavar='FILE', type=str, nargs='+',
|
|
help="Input FontDame .txt files")
|
|
|
|
args = parser.parse_args(args)
|
|
|
|
if font is None:
|
|
if args.font:
|
|
font = ttLib.TTFont(args.font)
|
|
else:
|
|
font = MockFont()
|
|
|
|
for f in args.inputs:
|
|
log.debug("Processing %s", f)
|
|
with open(f, 'rt', encoding="utf-8") as f:
|
|
table = build(f, font, tableTag=args.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())
|