1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402 |
- #!/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 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") 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())
|