You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			1401 lines
		
	
	
		
			46 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			1401 lines
		
	
	
		
			46 KiB
		
	
	
	
		
			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 fontTools.ttLib import newTable
 | 
						|
from fontTools.feaLib.lookupDebugInfo import LOOKUP_DEBUG_ENV_VAR, LOOKUP_DEBUG_INFO_KEY
 | 
						|
from operator import setitem
 | 
						|
import os
 | 
						|
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 + " " * (4 - len(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 os.environ.get(LOOKUP_DEBUG_ENV_VAR):
 | 
						|
            if "Debg" not in font:
 | 
						|
                font["Debg"] = newTable("Debg")
 | 
						|
                font["Debg"].data = {}
 | 
						|
            debug = (
 | 
						|
                font["Debg"]
 | 
						|
                .data.setdefault(LOOKUP_DEBUG_INFO_KEY, {})
 | 
						|
                .setdefault(tableTag, {})
 | 
						|
            )
 | 
						|
            for name, lookup in lookupMap.items():
 | 
						|
                debug[str(lookup)] = ["", name, ""]
 | 
						|
 | 
						|
        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-sig") 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())
 |