| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400 |
- # 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())
|