def parseLayoutFeatures(font): """ Parse OpenType layout features in the UFO and return a feaLib.ast.FeatureFile instance. """ featxt = tounicode(font.features.text or "", "utf-8") if not featxt: return ast.FeatureFile() buf = UnicodeIO(featxt) # the path is used by the lexer to resolve 'include' statements # and print filename in error messages. For the UFO spec, this # should be the path of the UFO, not the inner features.fea: # https://github.com/unified-font-object/ufo-spec/issues/55 ufoPath = font.path if ufoPath is not None: buf.name = ufoPath glyphNames = set(font.keys()) try: parser = Parser(buf, glyphNames) doc = parser.parse() except IncludedFeaNotFound as e: if ufoPath and os.path.exists(os.path.join(ufoPath, e.args[0])): logger.warning("Please change the file name in the include(...); " "statement to be relative to the UFO itself, " "instead of relative to the 'features.fea' file " "contained in it.") raise return doc
def parseLayoutFeatures(font): """Parse OpenType layout features in the UFO and return a feaLib.ast.FeatureFile instance. """ featxt = font.features.text or "" if not featxt: return ast.FeatureFile() buf = StringIO(featxt) ufoPath = font.path includeDir = None if ufoPath is not None: # The UFO v3 specification says "Any include() statements must be relative to # the UFO path, not to the features.fea file itself". We set the `name` # attribute on the buffer to the actual feature file path, which feaLib will # pick up and use to attribute errors to the correct file, and explicitly set # the include directory to the parent of the UFO. ufoPath = os.path.normpath(ufoPath) buf.name = os.path.join(ufoPath, "features.fea") includeDir = os.path.dirname(ufoPath) glyphNames = set(font.keys()) try: parser = Parser(buf, glyphNames, includeDir=includeDir) doc = parser.parse() except IncludedFeaNotFound as e: if ufoPath and os.path.exists(os.path.join(ufoPath, e.args[0])): logger.warning("Please change the file name in the include(...); " "statement to be relative to the UFO itself, " "instead of relative to the 'features.fea' file " "contained in it.") raise return doc
def __init__(self, featurefile, font=None, glyphNames=None, includeDir=None): self.ff = fontFeatures.FontFeatures() self.markclasses = {} self.currentFeature = None self.currentRoutine = None self.gensym = 1 self.glyphmap = () self.currentLanguage = None if font and not glyphNames: glyphNames = font.getGlyphOrder() if isinstance(featurefile, str): featurefile = io.StringIO(featurefile) self.featurefile = featurefile if glyphNames: self.parser = Parser(self.featurefile, glyphNames=glyphNames, includeDir=includeDir) else: self.parser = Parser(self.featurefile, includeDir=includeDir) self.parser.ast.ValueRecord = fontFeatures.ValueRecord
def check_fea2fea_file(self, name): f = self.getpath("{}.fea".format(name)) p = Parser(f) doc = p.parse() tlines = self.normal_fea(doc.asFea().split("\n")) with open(f, "r", encoding="utf-8") as ofile: olines = self.normal_fea(ofile.readlines()) if olines != tlines: for line in difflib.unified_diff(olines, tlines): sys.stdout.write(line) self.fail("Fea2Fea output is different from expected")
def check_fea2fea_file(self, name): f = self.getpath("{}.fea".format(name)) p = Parser(f) doc = p.parse() tlines = self.normal_fea(doc.asFea().split("\n")) with open(f, "r", encoding="utf-8") as ofile: olines = self.normal_fea(ofile.readlines()) if olines != tlines: for line in difflib.unified_diff(olines, tlines): sys.stderr.write(line) self.fail("Fea2Fea output is different from expected")
def __init__(self, featurefile, font=None): self.ff = fontFeatures.FontFeatures() self.markclasses = {} self.currentFeature = None self.currentRoutine = None self.gensym = 1 self.glyphmap = () self.currentLanguage = None if font: self.glyphmap = font.getReverseGlyphMap() if isinstance(featurefile, str): featurefile = io.StringIO(featurefile) self.featurefile = featurefile self.parser = Parser(self.featurefile, self.glyphmap) self.parser.ast.ValueRecord = fontFeatures.ValueRecord
def parseFea(text): from fontTools.feaLib.parser import Parser if isinstance(text, ast.FeatureFile): return text f = StringIO(text) fea = Parser(f).parse() return fea
def __init__(self, featurefile, font=None): from fontTools.feaLib.parser import Parser self.ff = fontFeatures.FontFeatures() self.markclasses = {} self.currentFeature = None self.currentRoutine = None self.gensym = 1 self.language_systems = [] glyphmap = () if font: glyphmap = font.getReverseGlyphMap() if isinstance(featurefile, str): featurefile = io.StringIO(featurefile) parsetree = Parser(featurefile, glyphmap).parse() self.features_ = {} parsetree.build(self)
def build(self): self.parseTree = Parser(self.featurefile_path).parse() self.parseTree.build(self) for tag in ('GPOS', 'GSUB'): table = self.makeTable(tag) if (table.ScriptList.ScriptCount > 0 or table.FeatureList.FeatureCount > 0 or table.LookupList.LookupCount > 0): fontTable = self.font[tag] = getTableClass(tag)() fontTable.table = table elif tag in self.font: del self.font[tag] gdef = self.makeGDEF() if gdef: self.font["GDEF"] = gdef elif "GDEF" in self.font: del self.font["GDEF"]
def parse(self, text): if not self.tempdir: self.tempdir = tempfile.mkdtemp() self.num_tempfiles += 1 path = os.path.join(self.tempdir, "tmp%d.fea" % self.num_tempfiles) with codecs.open(path, "wb", "utf-8") as outfile: outfile.write(text) return Parser(path).parse()
def _parseFeaSource(featureSource): pos = 0 while True: m = _feaIncludePat.search(featureSource, pos) if m is None: break pos = m.end() lineStart = featureSource.rfind("\n", 0, m.start()) lineEnd = featureSource.find("\n", m.end()) if lineStart == -1: lineStart = 0 if lineEnd == -1: lineEnd = len(featureSource) line = featureSource[lineStart:lineEnd] f = io.StringIO(line) p = FeatureParser(f, followIncludes=False) for st in p.parse().statements: if isinstance(st, IncludeStatement): yield st.filename
def parse(features): names = set() featurefile = UnicodeIO(tounicode(features)) fea = Parser(featurefile, []).parse() for statement in fea.statements: if getattr(statement, "name", None) in ("isol", "ccmp"): for substatement in statement.statements: if hasattr(substatement, "glyphs"): # Single names.update(substatement.glyphs[0].glyphSet()) elif hasattr(substatement, "glyph"): # Multiple names.add(substatement.glyph) return names
def test_build_pre_parsed_ast_featurefile(self): f = StringIO("feature liga {sub f i by f_i;} liga;") tree = Parser(f).parse() font = makeTTFont() addOpenTypeFeatures(font, tree) assert "GSUB" in font
class FeaParser: """Turns a AFDKO feature file or string into a FontFeatures object. Args: featurefile: File object or string. font: Optionally, a TTFont object. """ def __init__(self, featurefile, font=None): self.ff = fontFeatures.FontFeatures() self.markclasses = {} self.currentFeature = None self.currentRoutine = None self.gensym = 1 self.glyphmap = () self.currentLanguage = None if font: self.glyphmap = font.getReverseGlyphMap() if isinstance(featurefile, str): featurefile = io.StringIO(featurefile) self.featurefile = featurefile self.parser = Parser(self.featurefile, self.glyphmap) self.parser.ast.ValueRecord = fontFeatures.ValueRecord def parse(self): """Parse the feature code. Returns: A ``FontFeatures`` object containing the rules of this file. """ # Borrow glyph classes for name, members in self.ff.namedClasses.items(): glyphclass = ast.GlyphClassDefinition( name, ast.GlyphClass([m for m in members])) self.parser.glyphclasses_.define(name, glyphclass) parsetree = self.parser.parse() # Return glyph classes if len(self.parser.glyphclasses_.scopes_): for name, definition in self.parser.glyphclasses_.scopes_[ -1].items(): if isinstance(definition, ast.MarkClass): pass # self.ff.namedClasses[name] = list(definition.glyphs.keys()) else: self.ff.namedClasses[name] = definition.glyphs.glyphs self.features_ = {} parsetree.build(self) return self.ff def _start_routine_if_necessary(self, location): if not self.currentRoutine: self._start_routine(location, "") def _start_routine(self, location, name): location = "%s:%i:%i" % (location) # print("Starting routine at "+location) self._discard_empty_routine() self.currentRoutine = fontFeatures.Routine(name=name, address=location) if not name: self.currentRoutine.name = "unnamed_routine_%i" % self.gensym self.gensym = self.gensym + 1 self.currentRoutineFlag = 0 if self.currentFeature: reference = self.ff.referenceRoutine(self.currentRoutine) self.ff.addFeature(self.currentFeature, [reference]) else: self.ff.routines.append(self.currentRoutine) def start_lookup_block(self, location, name): self._start_routine(location, name) def start_feature(self, location, name): self.currentFeature = name def set_font_revision(self, location, revision): pass def add_name_record(self, *args): pass def add_featureName(self, tag): # XXX support this pass def set_script(self, location, script): self.currentLanguage = [(script, "*")] def set_language(self, location, language, include_default, required): self.currentLanguage = [(self.currentLanguage[0][0], language)] def add_single_subst(self, location, prefix, suffix, mapping, forceChain): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) s = fontFeatures.Substitution(input_=[list(mapping.keys())], replacement=[list(mapping.values())], precontext=[[str(g) for g in group] for group in prefix], postcontext=[[str(g) for g in group] for group in suffix], address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) def add_reverse_chain_single_subst(self, location, prefix, suffix, mapping): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) s = fontFeatures.Substitution(input_=[list(mapping.keys())], replacement=[list(mapping.values())], precontext=[[str(g) for g in group] for group in prefix], postcontext=[[str(g) for g in group] for group in suffix], address=location, languages=self.currentLanguage, reverse=True) self.currentRoutine.addRule(s) def add_multiple_subst(self, location, prefix, glyph, suffix, replacements, forceChain): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) s = fontFeatures.Substitution(input_=[[glyph]], replacement=[[g] for g in replacements], precontext=[[str(g) for g in group] for group in prefix], postcontext=[[str(g) for g in group] for group in suffix], address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) def add_alternate_subst(self, location, prefix, glyph, suffix, replacement): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) s = fontFeatures.Substitution(input_=[[glyph]], replacement=[replacement], precontext=[[str(g) for g in group] for group in prefix], postcontext=[[str(g) for g in group] for group in suffix], address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) def add_ligature_subst(self, location, prefix, glyphs, suffix, replacement, forceChain): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) s = fontFeatures.Substitution(input_=[list(x) for x in glyphs], replacement=[[replacement]], precontext=[[str(g) for g in group] for group in prefix], postcontext=[[str(g) for g in group] for group in suffix], address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) # Find named feature mylookups = [] for x in lookups: if x: mylookups.append([self.ff.routineNamed(y.name) for y in x]) else: mylookups.append(None) s = fontFeatures.Chaining(input_=[list(x) for x in glyphs], precontext=[[str(g) for g in group] for group in prefix], postcontext=[[str(g) for g in group] for group in suffix], lookups=mylookups, address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) add_chain_context_pos = add_chain_context_subst def add_single_pos(self, location, prefix, suffix, pos, forceChain): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) s = fontFeatures.Positioning(glyphs=[p[0] for p in pos], valuerecords=[p[1] for p in pos], precontext=[[str(g) for g in group] for group in prefix], postcontext=[[str(g) for g in group] for group in suffix], address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) s = fontFeatures.Positioning(glyphs=[[glyph1], [glyph2]], valuerecords=[value1, value2], address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) s = fontFeatures.Positioning(glyphs=[glyphclass1, glyphclass2], valuerecords=[value1, value2], address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) basedict, markdict = {}, {} if entryAnchor: basedict = {g: (entryAnchor.x, entryAnchor.y) for g in glyphclass} if exitAnchor: markdict = {g: (exitAnchor.x, exitAnchor.y) for g in glyphclass} s = fontFeatures.Attachment(base_name="cursive_entry", mark_name="cursive_exit", bases=basedict, marks=markdict, address=location, languages=self.currentLanguage) self.currentRoutine.addRule(s) def add_mark_base_pos(self, location, bases, marks): self._start_routine_if_necessary(location) location = "%s:%i:%i" % (location) for baseanchor, markclass in marks: assert len(markclass.definitions) == 1 markanchor = markclass.definitions[0].anchor s = fontFeatures.Attachment( base_name=markclass.name, mark_name=markclass.name, bases={g: (baseanchor.x, baseanchor.y) for g in bases}, marks={ g: (markanchor.x, markanchor.y) for g in markclass.glyphs.keys() }, address=location, languages=self.currentLanguage) s.fontfeatures = self.ff self.currentRoutine.addRule(s) add_mark_mark_pos = add_mark_base_pos def set_lookup_flag(self, location, value, markAttach, markFilter): if self.currentRoutine and value == self.currentRoutineFlag: return # If we're mid-feature, start a new routine here if self.currentFeature: self.end_lookup_block() self._discard_empty_routine() self._start_routine(location, None) self.currentRoutineFlag = value def add_language_system(self, location, script, language): pass def add_lookup_call(self, lookup_name): routine = self.ff.routineNamed(lookup_name) if self.currentFeature: self._discard_empty_routine() self.ff.addFeature(self.currentFeature, [routine]) else: raise ValueError("Huh?") def end_lookup_block(self): if self.currentRoutine: for rule in self.currentRoutine.rules: rule.flags = self.currentRoutineFlag def end_feature(self): self._discard_empty_routine() self.currentFeature = None self.currentLanguage = None if self.currentRoutine: for rule in self.currentRoutine.rules: rule.flags = self.currentRoutineFlag self.currentRoutine = None def _discard_empty_routine(self): if not self.currentFeature: return if self.currentRoutine and not self.currentRoutine.rules: if self.currentRoutine not in self.ff.routines: # print("%s escaped!" % self.currentRoutine.name) return del (self.ff.routines[self.ff.routines.index(self.currentRoutine)]) if self.currentFeature in self.ff.features: del (self.ff.features[self.currentFeature][-1]) pass def add_feature_reference(self, location, featurename): # XXX pass def add_glyphClassDef(self, location, base, ligature, mark, component): for g in base: self.ff.glyphclasses[g] = "base" for g in ligature: self.ff.glyphclasses[g] = "ligature" for g in mark: self.ff.glyphclasses[g] = "mark" for g in component: self.ff.glyphclasses[g] = "component"
class Builder(object): def __init__(self, featurefile_path, font): self.featurefile_path = featurefile_path self.font = font self.default_language_systems_ = set() self.script_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.language_systems = set() self.named_lookups_ = {} self.cur_lookup_ = None self.cur_lookup_name_ = None self.cur_feature_name_ = None self.lookups_ = [] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] self.parseTree = None self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' self.markAttach_ = {} # "acute" --> (4, (file, line, column)) self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4 self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4 def build(self): self.parseTree = Parser(self.featurefile_path).parse() self.parseTree.build(self) for tag in ('GPOS', 'GSUB'): table = self.makeTable(tag) if (table.ScriptList.ScriptCount > 0 or table.FeatureList.FeatureCount > 0 or table.LookupList.LookupCount > 0): fontTable = self.font[tag] = getTableClass(tag)() fontTable.table = table elif tag in self.font: del self.font[tag] gdef = self.makeGDEF() if gdef: self.font["GDEF"] = gdef elif "GDEF" in self.font: del self.font["GDEF"] def get_lookup_(self, location, builder_class): if (self.cur_lookup_ and type(self.cur_lookup_) == builder_class and self.cur_lookup_.lookupflag == self.lookupflag_ and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_): return self.cur_lookup_ if self.cur_lookup_name_ and self.cur_lookup_: raise FeatureLibError( "Within a named lookup block, all rules must be of " "the same lookup type and flag", location) self.cur_lookup_ = builder_class(self.font, location) self.cur_lookup_.lookupflag = self.lookupflag_ self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_ self.lookups_.append(self.cur_lookup_) if self.cur_lookup_name_: # We are starting a lookup rule inside a named lookup block. self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_ if self.cur_feature_name_: # We are starting a lookup rule inside a feature. This includes # lookup rules inside named lookups inside features. for script, lang in self.language_systems: key = (script, lang, self.cur_feature_name_) self.features_.setdefault(key, []).append(self.cur_lookup_) return self.cur_lookup_ def makeGDEF(self): gdef = otTables.GDEF() gdef.Version = 1.0 gdef.GlyphClassDef = otTables.GlyphClassDef() inferredGlyphClass = {} for lookup in self.lookups_: inferredGlyphClass.update(lookup.inferGlyphClasses()) marks = {} # glyph --> markClass for markClass in self.parseTree.markClasses.values(): for markClassDef in markClass.definitions: for glyph in markClassDef.glyphSet(): other = marks.get(glyph) if other not in (None, markClass): name1, name2 = sorted([markClass.name, other.name]) raise FeatureLibError( 'Glyph %s cannot be both in ' 'markClass @%s and @%s' % (glyph, name1, name2), markClassDef.location) marks[glyph] = markClass inferredGlyphClass[glyph] = 3 gdef.GlyphClassDef.classDefs = inferredGlyphClass gdef.AttachList = None gdef.LigCaretList = None markAttachClass = {g: c for g, (c, _) in self.markAttach_.items()} if markAttachClass: gdef.MarkAttachClassDef = otTables.MarkAttachClassDef() gdef.MarkAttachClassDef.classDefs = markAttachClass else: gdef.MarkAttachClassDef = None if self.markFilterSets_: gdef.Version = 0x00010002 m = gdef.MarkGlyphSetsDef = otTables.MarkGlyphSetsDef() m.MarkSetTableFormat = 1 m.MarkSetCount = len(self.markFilterSets_) m.Coverage = [] filterSets = [(id, glyphs) for (glyphs, id) in self.markFilterSets_.items()] for i, glyphs in sorted(filterSets): coverage = otTables.Coverage() coverage.glyphs = sorted(glyphs, key=self.font.getGlyphID) m.Coverage.append(coverage) if (len(gdef.GlyphClassDef.classDefs) == 0 and gdef.MarkAttachClassDef is None): return None result = getTableClass("GDEF")() result.table = gdef return result def makeTable(self, tag): table = getattr(otTables, tag, None)() table.Version = 1.0 table.ScriptList = otTables.ScriptList() table.ScriptList.ScriptRecord = [] table.FeatureList = otTables.FeatureList() table.FeatureList.FeatureRecord = [] table.LookupList = otTables.LookupList() table.LookupList.Lookup = [] for lookup in self.lookups_: lookup.lookup_index = None for i, lookup_builder in enumerate(self.lookups_): if lookup_builder.table != tag: continue # If multiple lookup builders would build equivalent lookups, # emit them only once. This is quadratic in the number of lookups, # but the checks are cheap. If performance ever becomes an issue, # we could hash the lookup content and only compare those with # the same hash value. equivalent_builder = None for other_builder in self.lookups_[:i]: if lookup_builder.equals(other_builder): equivalent_builder = other_builder if equivalent_builder is not None: lookup_builder.lookup_index = equivalent_builder.lookup_index continue lookup_builder.lookup_index = len(table.LookupList.Lookup) table.LookupList.Lookup.append(lookup_builder.build()) # Build a table for mapping (tag, lookup_indices) to feature_index. # For example, ('liga', (2,3,7)) --> 23. feature_indices = {} required_feature_indices = {} # ('latn', 'DEU') --> 23 scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24 for key, lookups in sorted(self.features_.items()): script, lang, feature_tag = key # l.lookup_index will be None when a lookup is not needed # for the table under construction. For example, substitution # rules will have no lookup_index while building GPOS tables. lookup_indices = tuple([ l.lookup_index for l in lookups if l.lookup_index is not None ]) if len(lookup_indices) == 0: continue feature_key = (feature_tag, lookup_indices) feature_index = feature_indices.get(feature_key) if feature_index is None: feature_index = len(table.FeatureList.FeatureRecord) frec = otTables.FeatureRecord() frec.FeatureTag = feature_tag frec.Feature = otTables.Feature() frec.Feature.FeatureParams = None frec.Feature.LookupListIndex = lookup_indices frec.Feature.LookupCount = len(lookup_indices) table.FeatureList.FeatureRecord.append(frec) feature_indices[feature_key] = feature_index scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index) if self.required_features_.get((script, lang)) == feature_tag: required_feature_indices[(script, lang)] = feature_index # Build ScriptList. for script, lang_features in sorted(scripts.items()): srec = otTables.ScriptRecord() srec.ScriptTag = script srec.Script = otTables.Script() srec.Script.DefaultLangSys = None srec.Script.LangSysRecord = [] for lang, feature_indices in sorted(lang_features.items()): langrec = otTables.LangSysRecord() langrec.LangSys = otTables.LangSys() langrec.LangSys.LookupOrder = None req_feature_index = \ required_feature_indices.get((script, lang)) if req_feature_index is None: langrec.LangSys.ReqFeatureIndex = 0xFFFF else: langrec.LangSys.ReqFeatureIndex = req_feature_index langrec.LangSys.FeatureIndex = [ i for i in feature_indices if i != req_feature_index ] langrec.LangSys.FeatureCount = \ len(langrec.LangSys.FeatureIndex) if lang == "dflt": srec.Script.DefaultLangSys = langrec.LangSys else: langrec.LangSysTag = lang srec.Script.LangSysRecord.append(langrec) srec.Script.LangSysCount = len(srec.Script.LangSysRecord) table.ScriptList.ScriptRecord.append(srec) table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord) table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord) table.LookupList.LookupCount = len(table.LookupList.Lookup) return table def add_language_system(self, location, script, language): # OpenType Feature File Specification, section 4.b.i if (script == "DFLT" and language == "dflt" and self.default_language_systems_): raise FeatureLibError( 'If "languagesystem DFLT dflt" is present, it must be ' 'the first of the languagesystem statements', location) if (script, language) in self.default_language_systems_: raise FeatureLibError( '"languagesystem %s %s" has already been specified' % (script.strip(), language.strip()), location) self.default_language_systems_.add((script, language)) def get_default_language_systems_(self): # OpenType Feature File specification, 4.b.i. languagesystem: # If no "languagesystem" statement is present, then the # implementation must behave exactly as though the following # statement were present at the beginning of the feature file: # languagesystem DFLT dflt; if self.default_language_systems_: return frozenset(self.default_language_systems_) else: return frozenset({('DFLT', 'dflt')}) def start_feature(self, location, name): self.language_systems = self.get_default_language_systems_() self.cur_lookup_ = None self.cur_feature_name_ = name def end_feature(self): assert self.cur_feature_name_ is not None self.cur_feature_name_ = None self.language_systems = None self.cur_lookup_ = None def start_lookup_block(self, location, name): if name in self.named_lookups_: raise FeatureLibError( 'Lookup "%s" has already been defined' % name, location) self.cur_lookup_name_ = name self.named_lookups_[name] = None self.cur_lookup_ = None def end_lookup_block(self): assert self.cur_lookup_name_ is not None self.cur_lookup_name_ = None self.cur_lookup_ = None def set_language(self, location, language, include_default, required): assert (len(language) == 4) if self.cur_lookup_name_: raise FeatureLibError( "Within a named lookup block, it is not allowed " "to change the language", location) if self.cur_feature_name_ in ('aalt', 'size'): raise FeatureLibError( "Language statements are not allowed " "within \"feature %s\"" % self.cur_feature_name_, location) self.cur_lookup_ = None if include_default: langsys = set(self.get_default_language_systems_()) else: langsys = set() langsys.add((self.script_, language)) self.language_systems = frozenset(langsys) if required: key = (self.script_, language) if key in self.required_features_: raise FeatureLibError( "Language %s (script %s) has already " "specified feature %s as its required feature" % (language.strip(), self.script_.strip(), self.required_features_[key].strip()), location) self.required_features_[key] = self.cur_feature_name_ def getMarkAttachClass_(self, location, glyphs): id = self.markAttachClassID_.get(glyphs) if id is not None: return id id = len(self.markAttachClassID_) + 1 self.markAttachClassID_[glyphs] = id for glyph in glyphs: if glyph in self.markAttach_: _, loc = self.markAttach_[glyph] raise FeatureLibError( "Glyph %s already has been assigned " "a MarkAttachmentType at %s:%d:%d" % (glyph, loc[0], loc[1], loc[2]), location) self.markAttach_[glyph] = (id, location) return id def getMarkFilterSet_(self, location, glyphs): id = self.markFilterSets_.get(glyphs) if id is not None: return id id = len(self.markFilterSets_) self.markFilterSets_[glyphs] = id return id def set_lookup_flag(self, location, value, markAttach, markFilter): value = value & 0xFF if markAttach: markAttachClass = self.getMarkAttachClass_(location, markAttach) value = value | (markAttachClass << 8) if markFilter: markFilterSet = self.getMarkFilterSet_(location, markFilter) value = value | 0x10 self.lookupflag_markFilterSet_ = markFilterSet else: self.lookupflag_markFilterSet_ = None self.lookupflag_ = value def set_script(self, location, script): if self.cur_lookup_name_: raise FeatureLibError( "Within a named lookup block, it is not allowed " "to change the script", location) if self.cur_feature_name_ in ('aalt', 'size'): raise FeatureLibError( "Script statements are not allowed " "within \"feature %s\"" % self.cur_feature_name_, location) self.cur_lookup_ = None self.script_ = script self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.set_language(location, "dflt", include_default=True, required=False) def find_lookup_builders_(self, lookups): """Helper for building chain contextual substitutions Given a list of lookup names, finds the LookupBuilder for each name. If an input name is None, it gets mapped to a None LookupBuilder. """ lookup_builders = [] for lookup in lookups: if lookup is not None: lookup_builders.append(self.named_lookups_.get(lookup.name)) else: lookup_builders.append(None) return lookup_builders def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextPosBuilder) lookup.rules.append( (prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextSubstBuilder) lookup.substitutions.append( (prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_alternate_subst(self, location, glyph, from_class): lookup = self.get_lookup_(location, AlternateSubstBuilder) if glyph in lookup.alternates: raise FeatureLibError( 'Already defined alternates for glyph "%s"' % glyph, location) lookup.alternates[glyph] = from_class def add_ligature_subst(self, location, glyphs, replacement): lookup = self.get_lookup_(location, LigatureSubstBuilder) lookup.ligatures[glyphs] = replacement def add_multiple_subst(self, location, glyph, replacements): lookup = self.get_lookup_(location, MultipleSubstBuilder) if glyph in lookup.mapping: raise FeatureLibError( 'Already defined substitution for glyph "%s"' % glyph, location) lookup.mapping[glyph] = replacements def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping): lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder) lookup.substitutions.append((old_prefix, old_suffix, mapping)) def add_single_subst(self, location, mapping): lookup = self.get_lookup_(location, SingleSubstBuilder) for (from_glyph, to_glyph) in mapping.items(): if from_glyph in lookup.mapping: raise FeatureLibError( 'Already defined rule for replacing glyph "%s" by "%s"' % (from_glyph, lookup.mapping[from_glyph]), location) lookup.mapping[from_glyph] = to_glyph def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor): lookup = self.get_lookup_(location, CursivePosBuilder) lookup.add_attachment( location, glyphclass, makeOpenTypeAnchor(entryAnchor, otTables.EntryAnchor), makeOpenTypeAnchor(exitAnchor, otTables.ExitAnchor)) def add_marks_(self, location, lookupBuilder, marks): """Helper for add_mark_{base,liga,mark}_pos.""" for _, markClass in marks: for markClassDef in markClass.definitions: for mark in markClassDef.glyphs.glyphSet(): if mark not in lookupBuilder.marks: otMarkAnchor = makeOpenTypeAnchor( markClassDef.anchor, otTables.MarkAnchor) lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor) def add_mark_base_pos(self, location, bases, marks): builder = self.get_lookup_(location, MarkBasePosBuilder) self.add_marks_(location, builder, marks) for baseAnchor, markClass in marks: otBaseAnchor = makeOpenTypeAnchor(baseAnchor, otTables.BaseAnchor) for base in bases: builder.bases.setdefault(base, {})[markClass.name] = (otBaseAnchor) def add_mark_lig_pos(self, location, ligatures, components): builder = self.get_lookup_(location, MarkLigPosBuilder) componentAnchors = [] for marks in components: anchors = {} self.add_marks_(location, builder, marks) for ligAnchor, markClass in marks: anchors[markClass.name] = (makeOpenTypeAnchor( ligAnchor, otTables.LigatureAnchor)) componentAnchors.append(anchors) for glyph in ligatures: builder.ligatures[glyph] = componentAnchors def add_mark_mark_pos(self, location, baseMarks, marks): builder = self.get_lookup_(location, MarkMarkPosBuilder) self.add_marks_(location, builder, marks) for baseAnchor, markClass in marks: otBaseAnchor = makeOpenTypeAnchor(baseAnchor, otTables.Mark2Anchor) for baseMark in baseMarks: builder.baseMarks.setdefault( baseMark, {})[markClass.name] = (otBaseAnchor) def add_pair_pos(self, location, enumerated, glyphclass1, value1, glyphclass2, value2): lookup = self.get_lookup_(location, PairPosBuilder) if enumerated: for glyph in glyphclass1: lookup.add_pair(location, {glyph}, value1, glyphclass2, value2) else: lookup.add_pair(location, glyphclass1, value1, glyphclass2, value2) def add_single_pos(self, location, glyph, valuerecord): lookup = self.get_lookup_(location, SinglePosBuilder) curValue = lookup.mapping.get(glyph) if curValue is not None and curValue != valuerecord: otherLoc = valuerecord.location raise FeatureLibError( 'Already defined different position for glyph "%s" at %s:%d:%d' % (glyph, otherLoc[0], otherLoc[1], otherLoc[2]), location) lookup.mapping[glyph] = valuerecord
def test_substitute_lookups(self): doc = Parser(self.getpath("spec5fi1.fea")).parse() [langsys, ligs, sub, feature] = doc.statements self.assertEqual(feature.statements[0].lookups, [ligs, None, sub]) self.assertEqual(feature.statements[1].lookups, [ligs, None, sub])
reMarkClass = re.compile(r"markClass.*;") markClassDefinitions = reMarkClass.findall( markFeatures) # Save all the markClass definitions # Read the input feature file with open(in_path, "r") as input_fea: # Replace $markClasses with the generated markClassDefinitions if markClassDefinitions != []: features = input_fea.read().replace( "$markClasses", "\n".join(markClassDefinitions)) else: features = input_fea.read().replace("$markClasses", "") if stylename: # Replace the matra I style-based include path for each style print("Replacing $stylename with '%s' if found in feature file" % stylename) features = features.replace("$stylename", stylename) # Write to a temporary file, which we can parse and expand all includes with open("production/features/tmp.fea", "w") as tmp: tmp.write(features) parser = Parser("production/features/tmp.fea") parsed = parser.parse() os.remove("production/features/tmp.fea") # Write the parsed and substituted features to the UFO features with open(out_path + "/features.fea", "w") as fea: fea.write(str(parsed))
def build(self): parsetree = Parser(self.featurefile_path).parse() parsetree.build(self) for tag in ('GPOS', 'GSUB'): fontTable = self.font[tag] = getTableClass(tag)() fontTable.table = self.makeTable(tag)
class Builder(object): def __init__(self, featurefile_path, font): self.featurefile_path = featurefile_path self.font = font self.default_language_systems_ = set() self.script_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.language_systems = set() self.named_lookups_ = {} self.cur_lookup_ = None self.cur_lookup_name_ = None self.cur_feature_name_ = None self.lookups_ = [] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] self.parseTree = None self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' self.markAttach_ = {} # "acute" --> (4, (file, line, column)) self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4 self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4 def build(self): self.parseTree = Parser(self.featurefile_path).parse() self.parseTree.build(self) for tag in ('GPOS', 'GSUB'): table = self.makeTable(tag) if (table.ScriptList.ScriptCount > 0 or table.FeatureList.FeatureCount > 0 or table.LookupList.LookupCount > 0): fontTable = self.font[tag] = getTableClass(tag)() fontTable.table = table elif tag in self.font: del self.font[tag] gdef = self.makeGDEF() if gdef: self.font["GDEF"] = gdef elif "GDEF" in self.font: del self.font["GDEF"] def get_lookup_(self, location, builder_class): if (self.cur_lookup_ and type(self.cur_lookup_) == builder_class and self.cur_lookup_.lookupflag == self.lookupflag_ and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_): return self.cur_lookup_ if self.cur_lookup_name_ and self.cur_lookup_: raise FeatureLibError( "Within a named lookup block, all rules must be of " "the same lookup type and flag", location) self.cur_lookup_ = builder_class(self.font, location) self.cur_lookup_.lookupflag = self.lookupflag_ self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_ self.lookups_.append(self.cur_lookup_) if self.cur_lookup_name_: # We are starting a lookup rule inside a named lookup block. self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_ if self.cur_feature_name_: # We are starting a lookup rule inside a feature. This includes # lookup rules inside named lookups inside features. for script, lang in self.language_systems: key = (script, lang, self.cur_feature_name_) self.features_.setdefault(key, []).append(self.cur_lookup_) return self.cur_lookup_ def makeGDEF(self): gdef = otTables.GDEF() gdef.Version = 1.0 gdef.GlyphClassDef = otTables.GlyphClassDef() inferredGlyphClass = {} for lookup in self.lookups_: inferredGlyphClass.update(lookup.inferGlyphClasses()) marks = {} # glyph --> markClass for markClass in self.parseTree.markClasses.values(): for markClassDef in markClass.definitions: for glyph in markClassDef.glyphSet(): other = marks.get(glyph) if other not in (None, markClass): name1, name2 = sorted([markClass.name, other.name]) raise FeatureLibError( 'Glyph %s cannot be both in ' 'markClass @%s and @%s' % (glyph, name1, name2), markClassDef.location) marks[glyph] = markClass inferredGlyphClass[glyph] = 3 gdef.GlyphClassDef.classDefs = inferredGlyphClass gdef.AttachList = None gdef.LigCaretList = None markAttachClass = {g: c for g, (c, _) in self.markAttach_.items()} if markAttachClass: gdef.MarkAttachClassDef = otTables.MarkAttachClassDef() gdef.MarkAttachClassDef.classDefs = markAttachClass else: gdef.MarkAttachClassDef = None if self.markFilterSets_: gdef.Version = 0x00010002 m = gdef.MarkGlyphSetsDef = otTables.MarkGlyphSetsDef() m.MarkSetTableFormat = 1 m.MarkSetCount = len(self.markFilterSets_) m.Coverage = [] filterSets = [(id, glyphs) for (glyphs, id) in self.markFilterSets_.items()] for i, glyphs in sorted(filterSets): coverage = otTables.Coverage() coverage.glyphs = sorted(glyphs, key=self.font.getGlyphID) m.Coverage.append(coverage) if (len(gdef.GlyphClassDef.classDefs) == 0 and gdef.MarkAttachClassDef is None): return None result = getTableClass("GDEF")() result.table = gdef return result def makeTable(self, tag): table = getattr(otTables, tag, None)() table.Version = 1.0 table.ScriptList = otTables.ScriptList() table.ScriptList.ScriptRecord = [] table.FeatureList = otTables.FeatureList() table.FeatureList.FeatureRecord = [] table.LookupList = otTables.LookupList() table.LookupList.Lookup = [] for lookup in self.lookups_: lookup.lookup_index = None for i, lookup_builder in enumerate(self.lookups_): if lookup_builder.table != tag: continue # If multiple lookup builders would build equivalent lookups, # emit them only once. This is quadratic in the number of lookups, # but the checks are cheap. If performance ever becomes an issue, # we could hash the lookup content and only compare those with # the same hash value. equivalent_builder = None for other_builder in self.lookups_[:i]: if lookup_builder.equals(other_builder): equivalent_builder = other_builder if equivalent_builder is not None: lookup_builder.lookup_index = equivalent_builder.lookup_index continue lookup_builder.lookup_index = len(table.LookupList.Lookup) table.LookupList.Lookup.append(lookup_builder.build()) # Build a table for mapping (tag, lookup_indices) to feature_index. # For example, ('liga', (2,3,7)) --> 23. feature_indices = {} required_feature_indices = {} # ('latn', 'DEU') --> 23 scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24 for key, lookups in sorted(self.features_.items()): script, lang, feature_tag = key # l.lookup_index will be None when a lookup is not needed # for the table under construction. For example, substitution # rules will have no lookup_index while building GPOS tables. lookup_indices = tuple([l.lookup_index for l in lookups if l.lookup_index is not None]) if len(lookup_indices) == 0: continue feature_key = (feature_tag, lookup_indices) feature_index = feature_indices.get(feature_key) if feature_index is None: feature_index = len(table.FeatureList.FeatureRecord) frec = otTables.FeatureRecord() frec.FeatureTag = feature_tag frec.Feature = otTables.Feature() frec.Feature.FeatureParams = None frec.Feature.LookupListIndex = lookup_indices frec.Feature.LookupCount = len(lookup_indices) table.FeatureList.FeatureRecord.append(frec) feature_indices[feature_key] = feature_index scripts.setdefault(script, {}).setdefault(lang, []).append( feature_index) if self.required_features_.get((script, lang)) == feature_tag: required_feature_indices[(script, lang)] = feature_index # Build ScriptList. for script, lang_features in sorted(scripts.items()): srec = otTables.ScriptRecord() srec.ScriptTag = script srec.Script = otTables.Script() srec.Script.DefaultLangSys = None srec.Script.LangSysRecord = [] for lang, feature_indices in sorted(lang_features.items()): langrec = otTables.LangSysRecord() langrec.LangSys = otTables.LangSys() langrec.LangSys.LookupOrder = None req_feature_index = \ required_feature_indices.get((script, lang)) if req_feature_index is None: langrec.LangSys.ReqFeatureIndex = 0xFFFF else: langrec.LangSys.ReqFeatureIndex = req_feature_index langrec.LangSys.FeatureIndex = [i for i in feature_indices if i != req_feature_index] langrec.LangSys.FeatureCount = \ len(langrec.LangSys.FeatureIndex) if lang == "dflt": srec.Script.DefaultLangSys = langrec.LangSys else: langrec.LangSysTag = lang srec.Script.LangSysRecord.append(langrec) srec.Script.LangSysCount = len(srec.Script.LangSysRecord) table.ScriptList.ScriptRecord.append(srec) table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord) table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord) table.LookupList.LookupCount = len(table.LookupList.Lookup) return table def add_language_system(self, location, script, language): # OpenType Feature File Specification, section 4.b.i if (script == "DFLT" and language == "dflt" and self.default_language_systems_): raise FeatureLibError( 'If "languagesystem DFLT dflt" is present, it must be ' 'the first of the languagesystem statements', location) if (script, language) in self.default_language_systems_: raise FeatureLibError( '"languagesystem %s %s" has already been specified' % (script.strip(), language.strip()), location) self.default_language_systems_.add((script, language)) def get_default_language_systems_(self): # OpenType Feature File specification, 4.b.i. languagesystem: # If no "languagesystem" statement is present, then the # implementation must behave exactly as though the following # statement were present at the beginning of the feature file: # languagesystem DFLT dflt; if self.default_language_systems_: return frozenset(self.default_language_systems_) else: return frozenset({('DFLT', 'dflt')}) def start_feature(self, location, name): self.language_systems = self.get_default_language_systems_() self.cur_lookup_ = None self.cur_feature_name_ = name def end_feature(self): assert self.cur_feature_name_ is not None self.cur_feature_name_ = None self.language_systems = None self.cur_lookup_ = None def start_lookup_block(self, location, name): if name in self.named_lookups_: raise FeatureLibError( 'Lookup "%s" has already been defined' % name, location) self.cur_lookup_name_ = name self.named_lookups_[name] = None self.cur_lookup_ = None def end_lookup_block(self): assert self.cur_lookup_name_ is not None self.cur_lookup_name_ = None self.cur_lookup_ = None def set_language(self, location, language, include_default, required): assert(len(language) == 4) if self.cur_lookup_name_: raise FeatureLibError( "Within a named lookup block, it is not allowed " "to change the language", location) if self.cur_feature_name_ in ('aalt', 'size'): raise FeatureLibError( "Language statements are not allowed " "within \"feature %s\"" % self.cur_feature_name_, location) self.cur_lookup_ = None if include_default: langsys = set(self.get_default_language_systems_()) else: langsys = set() langsys.add((self.script_, language)) self.language_systems = frozenset(langsys) if required: key = (self.script_, language) if key in self.required_features_: raise FeatureLibError( "Language %s (script %s) has already " "specified feature %s as its required feature" % ( language.strip(), self.script_.strip(), self.required_features_[key].strip()), location) self.required_features_[key] = self.cur_feature_name_ def getMarkAttachClass_(self, location, glyphs): id = self.markAttachClassID_.get(glyphs) if id is not None: return id id = len(self.markAttachClassID_) + 1 self.markAttachClassID_[glyphs] = id for glyph in glyphs: if glyph in self.markAttach_: _, loc = self.markAttach_[glyph] raise FeatureLibError( "Glyph %s already has been assigned " "a MarkAttachmentType at %s:%d:%d" % ( glyph, loc[0], loc[1], loc[2]), location) self.markAttach_[glyph] = (id, location) return id def getMarkFilterSet_(self, location, glyphs): id = self.markFilterSets_.get(glyphs) if id is not None: return id id = len(self.markFilterSets_) self.markFilterSets_[glyphs] = id return id def set_lookup_flag(self, location, value, markAttach, markFilter): value = value & 0xFF if markAttach: markAttachClass = self.getMarkAttachClass_(location, markAttach) value = value | (markAttachClass << 8) if markFilter: markFilterSet = self.getMarkFilterSet_(location, markFilter) value = value | 0x10 self.lookupflag_markFilterSet_ = markFilterSet else: self.lookupflag_markFilterSet_ = None self.lookupflag_ = value def set_script(self, location, script): if self.cur_lookup_name_: raise FeatureLibError( "Within a named lookup block, it is not allowed " "to change the script", location) if self.cur_feature_name_ in ('aalt', 'size'): raise FeatureLibError( "Script statements are not allowed " "within \"feature %s\"" % self.cur_feature_name_, location) self.cur_lookup_ = None self.script_ = script self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.set_language(location, "dflt", include_default=True, required=False) def find_lookup_builders_(self, lookups): """Helper for building chain contextual substitutions Given a list of lookup names, finds the LookupBuilder for each name. If an input name is None, it gets mapped to a None LookupBuilder. """ lookup_builders = [] for lookup in lookups: if lookup is not None: lookup_builders.append(self.named_lookups_.get(lookup.name)) else: lookup_builders.append(None) return lookup_builders def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextPosBuilder) lookup.rules.append((prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextSubstBuilder) lookup.substitutions.append((prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_alternate_subst(self, location, glyph, from_class): lookup = self.get_lookup_(location, AlternateSubstBuilder) if glyph in lookup.alternates: raise FeatureLibError( 'Already defined alternates for glyph "%s"' % glyph, location) lookup.alternates[glyph] = from_class def add_ligature_subst(self, location, glyphs, replacement): lookup = self.get_lookup_(location, LigatureSubstBuilder) lookup.ligatures[glyphs] = replacement def add_multiple_subst(self, location, glyph, replacements): lookup = self.get_lookup_(location, MultipleSubstBuilder) if glyph in lookup.mapping: raise FeatureLibError( 'Already defined substitution for glyph "%s"' % glyph, location) lookup.mapping[glyph] = replacements def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping): lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder) lookup.substitutions.append((old_prefix, old_suffix, mapping)) def add_single_subst(self, location, mapping): lookup = self.get_lookup_(location, SingleSubstBuilder) for (from_glyph, to_glyph) in mapping.items(): if from_glyph in lookup.mapping: raise FeatureLibError( 'Already defined rule for replacing glyph "%s" by "%s"' % (from_glyph, lookup.mapping[from_glyph]), location) lookup.mapping[from_glyph] = to_glyph def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor): lookup = self.get_lookup_(location, CursivePosBuilder) lookup.add_attachment( location, glyphclass, makeOpenTypeAnchor(entryAnchor, otTables.EntryAnchor), makeOpenTypeAnchor(exitAnchor, otTables.ExitAnchor)) def add_marks_(self, location, lookupBuilder, marks): """Helper for add_mark_{base,liga,mark}_pos.""" for _, markClass in marks: for markClassDef in markClass.definitions: for mark in markClassDef.glyphs.glyphSet(): if mark not in lookupBuilder.marks: otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor, otTables.MarkAnchor) lookupBuilder.marks[mark] = ( markClass.name, otMarkAnchor) def add_mark_base_pos(self, location, bases, marks): builder = self.get_lookup_(location, MarkBasePosBuilder) self.add_marks_(location, builder, marks) for baseAnchor, markClass in marks: otBaseAnchor = makeOpenTypeAnchor(baseAnchor, otTables.BaseAnchor) for base in bases: builder.bases.setdefault(base, {})[markClass.name] = ( otBaseAnchor) def add_mark_lig_pos(self, location, ligatures, components): builder = self.get_lookup_(location, MarkLigPosBuilder) componentAnchors = [] for marks in components: anchors = {} self.add_marks_(location, builder, marks) for ligAnchor, markClass in marks: anchors[markClass.name] = ( makeOpenTypeAnchor(ligAnchor, otTables.LigatureAnchor)) componentAnchors.append(anchors) for glyph in ligatures: builder.ligatures[glyph] = componentAnchors def add_mark_mark_pos(self, location, baseMarks, marks): builder = self.get_lookup_(location, MarkMarkPosBuilder) self.add_marks_(location, builder, marks) for baseAnchor, markClass in marks: otBaseAnchor = makeOpenTypeAnchor(baseAnchor, otTables.Mark2Anchor) for baseMark in baseMarks: builder.baseMarks.setdefault(baseMark, {})[markClass.name] = ( otBaseAnchor) def add_pair_pos(self, location, enumerated, glyphclass1, value1, glyphclass2, value2): lookup = self.get_lookup_(location, PairPosBuilder) if enumerated: for glyph in glyphclass1: lookup.add_pair(location, {glyph}, value1, glyphclass2, value2) else: lookup.add_pair(location, glyphclass1, value1, glyphclass2, value2) def add_single_pos(self, location, glyph, valuerecord): lookup = self.get_lookup_(location, SinglePosBuilder) curValue = lookup.mapping.get(glyph) if curValue is not None and curValue != valuerecord: otherLoc = valuerecord.location raise FeatureLibError( 'Already defined different position for glyph "%s" at %s:%d:%d' % (glyph, otherLoc[0], otherLoc[1], otherLoc[2]), location) lookup.mapping[glyph] = valuerecord
class Builder(object): def __init__(self, featurefile_path, font): self.featurefile_path = featurefile_path self.font = font self.glyphMap = font.getReverseGlyphMap() self.default_language_systems_ = set() self.script_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.language_systems = set() self.named_lookups_ = {} self.cur_lookup_ = None self.cur_lookup_name_ = None self.cur_feature_name_ = None self.lookups_ = [] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] self.parseTree = None self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' # for feature 'aalt' self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' self.aalt_location_ = None self.aalt_alternates_ = {} # for table 'head' self.fontRevision_ = None # 2.71 # for table 'GDEF' self.attachPoints_ = {} # "a" --> {3, 7} self.ligCaretCoords_ = {} # "f_f_i" --> {300, 600} self.ligCaretPoints_ = {} # "f_f_i" --> {3, 7} self.glyphClassDefs_ = {} # "fi" --> (2, (file, line, column)) self.markAttach_ = {} # "acute" --> (4, (file, line, column)) self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4 self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4 def build(self): self.parseTree = Parser(self.featurefile_path).parse() self.parseTree.build(self) self.build_feature_aalt_() self.build_head() for tag in ("GPOS", "GSUB"): table = self.makeTable(tag) if ( table.ScriptList.ScriptCount > 0 or table.FeatureList.FeatureCount > 0 or table.LookupList.LookupCount > 0 ): fontTable = self.font[tag] = getTableClass(tag)() fontTable.table = table elif tag in self.font: del self.font[tag] gdef = self.buildGDEF() if gdef: self.font["GDEF"] = gdef elif "GDEF" in self.font: del self.font["GDEF"] def get_chained_lookup_(self, location, builder_class): result = builder_class(self.font, location) result.lookupflag = self.lookupflag_ result.markFilterSet = self.lookupflag_markFilterSet_ self.lookups_.append(result) return result def add_lookup_to_feature_(self, lookup, feature_name): for script, lang in self.language_systems: key = (script, lang, feature_name) self.features_.setdefault(key, []).append(lookup) def get_lookup_(self, location, builder_class): if ( self.cur_lookup_ and type(self.cur_lookup_) == builder_class and self.cur_lookup_.lookupflag == self.lookupflag_ and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_ ): return self.cur_lookup_ if self.cur_lookup_name_ and self.cur_lookup_: raise FeatureLibError( "Within a named lookup block, all rules must be of " "the same lookup type and flag", location ) self.cur_lookup_ = builder_class(self.font, location) self.cur_lookup_.lookupflag = self.lookupflag_ self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_ self.lookups_.append(self.cur_lookup_) if self.cur_lookup_name_: # We are starting a lookup rule inside a named lookup block. self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_ if self.cur_feature_name_: # We are starting a lookup rule inside a feature. This includes # lookup rules inside named lookups inside features. self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_) return self.cur_lookup_ def build_feature_aalt_(self): if not self.aalt_features_ and not self.aalt_alternates_: return alternates = {g: set(a) for g, a in self.aalt_alternates_.items()} for location, name in self.aalt_features_ + [(None, "aalt")]: feature = [ (script, lang, feature, lookups) for (script, lang, feature), lookups in self.features_.items() if feature == name ] # "aalt" does not have to specify its own lookups, but it might. if not feature and name != "aalt": raise FeatureLibError("Feature %s has not been defined" % name, location) for script, lang, feature, lookups in feature: for lookup in lookups: for glyph, alts in lookup.getAlternateGlyphs().items(): alternates.setdefault(glyph, set()).update(alts) single = {glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1} multi = {glyph: sorted(repl, key=self.font.getGlyphID) for glyph, repl in alternates.items() if len(repl) > 1} if not single and not multi: return self.features_ = { (script, lang, feature): lookups for (script, lang, feature), lookups in self.features_.items() if feature != "aalt" } old_lookups = self.lookups_ self.lookups_ = [] self.start_feature(self.aalt_location_, "aalt") if single: single_lookup = self.get_lookup_(location, SingleSubstBuilder) single_lookup.mapping = single if multi: multi_lookup = self.get_lookup_(location, AlternateSubstBuilder) multi_lookup.alternates = multi self.end_feature() self.lookups_.extend(old_lookups) def build_head(self): if not self.fontRevision_: return table = self.font.get("head") if not table: # this only happens for unit tests table = self.font["head"] = getTableClass("head")() table.decompile(b"\0" * 54, self.font) table.tableVersion = 1.0 table.created = table.modified = 3406620153 # 2011-12-13 11:22:33 table.fontRevision = self.fontRevision_ def buildGDEF(self): gdef = otTables.GDEF() gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_() gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap) gdef.LigCaretList = otl.buildLigCaretList(self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap) gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_() gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_() gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 1.0 if any( (gdef.GlyphClassDef, gdef.AttachList, gdef.LigCaretList, gdef.MarkAttachClassDef, gdef.MarkGlyphSetsDef) ): result = getTableClass("GDEF")() result.table = gdef return result else: return None def buildGDEFGlyphClassDef_(self): inferredGlyphClass = {} for lookup in self.lookups_: inferredGlyphClass.update(lookup.inferGlyphClasses()) for markClass in self.parseTree.markClasses.values(): for markClassDef in markClass.definitions: for glyph in markClassDef.glyphSet(): inferredGlyphClass[glyph] = 3 if self.glyphClassDefs_: classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()} else: classes = inferredGlyphClass if classes: result = otTables.GlyphClassDef() result.classDefs = classes return result else: return None def buildGDEFMarkAttachClassDef_(self): classDefs = {g: c for g, (c, _) in self.markAttach_.items()} if not classDefs: return None result = otTables.MarkAttachClassDef() result.classDefs = classDefs return result def buildGDEFMarkGlyphSetsDef_(self): sets = [None] * len(self.markFilterSets_) for glyphs, id in self.markFilterSets_.items(): sets[id] = glyphs return otl.buildMarkGlyphSetsDef(sets, self.glyphMap) def buildLookups_(self, tag): assert tag in ("GPOS", "GSUB"), tag for lookup in self.lookups_: lookup.lookup_index = None lookups = [] for i, lookup in enumerate(self.lookups_): if lookup.table != tag: continue # TODO: https://github.com/behdad/fonttools/issues/448 # If multiple lookup builders would build equivalent lookups, # emit them only once. This is quadratic in the number of lookups, # but the checks are cheap. If performance ever becomes an issue, # we could hash the lookup content and only compare those with # the same hash value. equivalent = None for other in self.lookups_[:i]: if lookup.equals(other): equivalent = other if equivalent is not None: lookup.lookup_index = equivalent.lookup_index continue lookup.lookup_index = len(lookups) lookups.append(lookup) return [l.build() for l in lookups] def makeTable(self, tag): table = getattr(otTables, tag, None)() table.Version = 1.0 table.ScriptList = otTables.ScriptList() table.ScriptList.ScriptRecord = [] table.FeatureList = otTables.FeatureList() table.FeatureList.FeatureRecord = [] table.LookupList = otTables.LookupList() table.LookupList.Lookup = self.buildLookups_(tag) # Build a table for mapping (tag, lookup_indices) to feature_index. # For example, ('liga', (2,3,7)) --> 23. feature_indices = {} required_feature_indices = {} # ('latn', 'DEU') --> 23 scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24 for key, lookups in sorted(self.features_.items()): script, lang, feature_tag = key # l.lookup_index will be None when a lookup is not needed # for the table under construction. For example, substitution # rules will have no lookup_index while building GPOS tables. lookup_indices = tuple([l.lookup_index for l in lookups if l.lookup_index is not None]) if len(lookup_indices) == 0: continue feature_key = (feature_tag, lookup_indices) feature_index = feature_indices.get(feature_key) if feature_index is None: feature_index = len(table.FeatureList.FeatureRecord) frec = otTables.FeatureRecord() frec.FeatureTag = feature_tag frec.Feature = otTables.Feature() frec.Feature.FeatureParams = None frec.Feature.LookupListIndex = lookup_indices frec.Feature.LookupCount = len(lookup_indices) table.FeatureList.FeatureRecord.append(frec) feature_indices[feature_key] = feature_index scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index) if self.required_features_.get((script, lang)) == feature_tag: required_feature_indices[(script, lang)] = feature_index # Build ScriptList. for script, lang_features in sorted(scripts.items()): srec = otTables.ScriptRecord() srec.ScriptTag = script srec.Script = otTables.Script() srec.Script.DefaultLangSys = None srec.Script.LangSysRecord = [] for lang, feature_indices in sorted(lang_features.items()): langrec = otTables.LangSysRecord() langrec.LangSys = otTables.LangSys() langrec.LangSys.LookupOrder = None req_feature_index = required_feature_indices.get((script, lang)) if req_feature_index is None: langrec.LangSys.ReqFeatureIndex = 0xFFFF else: langrec.LangSys.ReqFeatureIndex = req_feature_index langrec.LangSys.FeatureIndex = [i for i in feature_indices if i != req_feature_index] langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex) if lang == "dflt": srec.Script.DefaultLangSys = langrec.LangSys else: langrec.LangSysTag = lang srec.Script.LangSysRecord.append(langrec) srec.Script.LangSysCount = len(srec.Script.LangSysRecord) table.ScriptList.ScriptRecord.append(srec) table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord) table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord) table.LookupList.LookupCount = len(table.LookupList.Lookup) return table def add_language_system(self, location, script, language): # OpenType Feature File Specification, section 4.b.i if script == "DFLT" and language == "dflt" and self.default_language_systems_: raise FeatureLibError( 'If "languagesystem DFLT dflt" is present, it must be ' "the first of the languagesystem statements", location, ) if (script, language) in self.default_language_systems_: raise FeatureLibError( '"languagesystem %s %s" has already been specified' % (script.strip(), language.strip()), location ) self.default_language_systems_.add((script, language)) def get_default_language_systems_(self): # OpenType Feature File specification, 4.b.i. languagesystem: # If no "languagesystem" statement is present, then the # implementation must behave exactly as though the following # statement were present at the beginning of the feature file: # languagesystem DFLT dflt; if self.default_language_systems_: return frozenset(self.default_language_systems_) else: return frozenset({("DFLT", "dflt")}) def start_feature(self, location, name): self.language_systems = self.get_default_language_systems_() self.cur_lookup_ = None self.cur_feature_name_ = name if name == "aalt": self.aalt_location_ = location def end_feature(self): assert self.cur_feature_name_ is not None self.cur_feature_name_ = None self.language_systems = None self.cur_lookup_ = None def start_lookup_block(self, location, name): if name in self.named_lookups_: raise FeatureLibError('Lookup "%s" has already been defined' % name, location) if self.cur_feature_name_ == "aalt": raise FeatureLibError( "Lookup blocks cannot be placed inside 'aalt' features; " "move it out, and then refer to it with a lookup statement", location, ) self.cur_lookup_name_ = name self.named_lookups_[name] = None self.cur_lookup_ = None def end_lookup_block(self): assert self.cur_lookup_name_ is not None self.cur_lookup_name_ = None self.cur_lookup_ = None def add_lookup_call(self, lookup_name): assert lookup_name in self.named_lookups_, lookup_name self.cur_lookup_ = None lookup = self.named_lookups_[lookup_name] self.add_lookup_to_feature_(lookup, self.cur_feature_name_) def set_font_revision(self, location, revision): self.fontRevision_ = revision def set_language(self, location, language, include_default, required): assert len(language) == 4 if self.cur_feature_name_ in ("aalt", "size"): raise FeatureLibError( "Language statements are not allowed " 'within "feature %s"' % self.cur_feature_name_, location ) self.cur_lookup_ = None if include_default: langsys = set(self.get_default_language_systems_()) else: langsys = set() langsys.add((self.script_, language)) self.language_systems = frozenset(langsys) if required: key = (self.script_, language) if key in self.required_features_: raise FeatureLibError( "Language %s (script %s) has already " "specified feature %s as its required feature" % (language.strip(), self.script_.strip(), self.required_features_[key].strip()), location, ) self.required_features_[key] = self.cur_feature_name_ def getMarkAttachClass_(self, location, glyphs): id = self.markAttachClassID_.get(glyphs) if id is not None: return id id = len(self.markAttachClassID_) + 1 self.markAttachClassID_[glyphs] = id for glyph in glyphs: if glyph in self.markAttach_: _, loc = self.markAttach_[glyph] raise FeatureLibError( "Glyph %s already has been assigned " "a MarkAttachmentType at %s:%d:%d" % (glyph, loc[0], loc[1], loc[2]), location, ) self.markAttach_[glyph] = (id, location) return id def getMarkFilterSet_(self, location, glyphs): id = self.markFilterSets_.get(glyphs) if id is not None: return id id = len(self.markFilterSets_) self.markFilterSets_[glyphs] = id return id def set_lookup_flag(self, location, value, markAttach, markFilter): value = value & 0xFF if markAttach: markAttachClass = self.getMarkAttachClass_(location, markAttach) value = value | (markAttachClass << 8) if markFilter: markFilterSet = self.getMarkFilterSet_(location, markFilter) value = value | 0x10 self.lookupflag_markFilterSet_ = markFilterSet else: self.lookupflag_markFilterSet_ = None self.lookupflag_ = value def set_script(self, location, script): if self.cur_feature_name_ in ("aalt", "size"): raise FeatureLibError( "Script statements are not allowed " 'within "feature %s"' % self.cur_feature_name_, location ) self.cur_lookup_ = None self.script_ = script self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.set_language(location, "dflt", include_default=False, required=False) def find_lookup_builders_(self, lookups): """Helper for building chain contextual substitutions Given a list of lookup names, finds the LookupBuilder for each name. If an input name is None, it gets mapped to a None LookupBuilder. """ lookup_builders = [] for lookup in lookups: if lookup is not None: lookup_builders.append(self.named_lookups_.get(lookup.name)) else: lookup_builders.append(None) return lookup_builders def add_attach_points(self, location, glyphs, contourPoints): for glyph in glyphs: self.attachPoints_.setdefault(glyph, set()).update(contourPoints) def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextPosBuilder) lookup.rules.append((prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextSubstBuilder) lookup.substitutions.append((prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_alternate_subst(self, location, prefix, glyph, suffix, replacement): if self.cur_feature_name_ == "aalt": alts = self.aalt_alternates_.setdefault(glyph, set()) alts.update(replacement) return if prefix or suffix: chain = self.get_lookup_(location, ChainContextSubstBuilder) lookup = self.get_chained_lookup_(location, AlternateSubstBuilder) chain.substitutions.append((prefix, [glyph], suffix, [lookup])) else: lookup = self.get_lookup_(location, AlternateSubstBuilder) if glyph in lookup.alternates: raise FeatureLibError('Already defined alternates for glyph "%s"' % glyph, location) lookup.alternates[glyph] = replacement def add_feature_reference(self, location, featureName): if self.cur_feature_name_ != "aalt": raise FeatureLibError('Feature references are only allowed inside "feature aalt"', location) self.aalt_features_.append((location, featureName)) def add_ligature_subst(self, location, prefix, glyphs, suffix, replacement, forceChain): if prefix or suffix or forceChain: chain = self.get_lookup_(location, ChainContextSubstBuilder) lookup = self.get_chained_lookup_(location, LigatureSubstBuilder) chain.substitutions.append((prefix, glyphs, suffix, [lookup])) else: lookup = self.get_lookup_(location, LigatureSubstBuilder) # OpenType feature file syntax, section 5.d, "Ligature substitution": # "Since the OpenType specification does not allow ligature # substitutions to be specified on target sequences that contain # glyph classes, the implementation software will enumerate # all specific glyph sequences if glyph classes are detected" for g in sorted(itertools.product(*glyphs)): lookup.ligatures[g] = replacement def add_multiple_subst(self, location, prefix, glyph, suffix, replacements): if prefix or suffix: chain = self.get_lookup_(location, ChainContextSubstBuilder) sub = self.get_chained_lookup_(location, MultipleSubstBuilder) sub.mapping[glyph] = replacements chain.substitutions.append((prefix, [{glyph}], suffix, [sub])) return lookup = self.get_lookup_(location, MultipleSubstBuilder) if glyph in lookup.mapping: raise FeatureLibError('Already defined substitution for glyph "%s"' % glyph, location) lookup.mapping[glyph] = replacements def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping): lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder) lookup.substitutions.append((old_prefix, old_suffix, mapping)) def add_single_subst(self, location, prefix, suffix, mapping, forceChain): if self.cur_feature_name_ == "aalt": for (from_glyph, to_glyph) in mapping.items(): alts = self.aalt_alternates_.setdefault(from_glyph, set()) alts.add(to_glyph) return if prefix or suffix or forceChain: self.add_single_subst_chained_(location, prefix, suffix, mapping) return lookup = self.get_lookup_(location, SingleSubstBuilder) for (from_glyph, to_glyph) in mapping.items(): if from_glyph in lookup.mapping: raise FeatureLibError( 'Already defined rule for replacing glyph "%s" by "%s"' % (from_glyph, lookup.mapping[from_glyph]), location, ) lookup.mapping[from_glyph] = to_glyph def find_chainable_SingleSubst_(self, chain, glyphs): """Helper for add_single_subst_chained_()""" for _, _, _, substitutions in chain.substitutions: for sub in substitutions: if isinstance(sub, SingleSubstBuilder) and not any(g in glyphs for g in sub.mapping.keys()): return sub return None def add_single_subst_chained_(self, location, prefix, suffix, mapping): # https://github.com/behdad/fonttools/issues/512 chain = self.get_lookup_(location, ChainContextSubstBuilder) sub = self.find_chainable_SingleSubst_(chain, set(mapping.keys())) if sub is None: sub = self.get_chained_lookup_(location, SingleSubstBuilder) sub.mapping.update(mapping) chain.substitutions.append((prefix, [mapping.keys()], suffix, [sub])) def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor): lookup = self.get_lookup_(location, CursivePosBuilder) lookup.add_attachment(location, glyphclass, makeOpenTypeAnchor(entryAnchor), makeOpenTypeAnchor(exitAnchor)) def add_marks_(self, location, lookupBuilder, marks): """Helper for add_mark_{base,liga,mark}_pos.""" for _, markClass in marks: for markClassDef in markClass.definitions: for mark in markClassDef.glyphs.glyphSet(): if mark not in lookupBuilder.marks: otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor) lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor) def add_mark_base_pos(self, location, bases, marks): builder = self.get_lookup_(location, MarkBasePosBuilder) self.add_marks_(location, builder, marks) for baseAnchor, markClass in marks: otBaseAnchor = makeOpenTypeAnchor(baseAnchor) for base in bases: builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor def add_mark_lig_pos(self, location, ligatures, components): builder = self.get_lookup_(location, MarkLigPosBuilder) componentAnchors = [] for marks in components: anchors = {} self.add_marks_(location, builder, marks) for ligAnchor, markClass in marks: anchors[markClass.name] = makeOpenTypeAnchor(ligAnchor) componentAnchors.append(anchors) for glyph in ligatures: builder.ligatures[glyph] = componentAnchors def add_mark_mark_pos(self, location, baseMarks, marks): builder = self.get_lookup_(location, MarkMarkPosBuilder) self.add_marks_(location, builder, marks) for baseAnchor, markClass in marks: otBaseAnchor = makeOpenTypeAnchor(baseAnchor) for baseMark in baseMarks: builder.baseMarks.setdefault(baseMark, {})[markClass.name] = otBaseAnchor def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2): lookup = self.get_lookup_(location, PairPosBuilder) lookup.addClassPair(location, glyphclass1, value1, glyphclass2, value2) def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): lookup = self.get_lookup_(location, PairPosBuilder) lookup.addGlyphPair(location, glyph1, value1, glyph2, value2) def add_single_pos(self, location, prefix, suffix, pos, forceChain): if prefix or suffix or forceChain: self.add_single_pos_chained_(location, prefix, suffix, pos) else: lookup = self.get_lookup_(location, SinglePosBuilder) for glyphs, value in pos: for glyph in glyphs: lookup.add_pos(location, glyph, value) def add_single_pos_chained_(self, location, prefix, suffix, pos): chain = self.get_lookup_(location, ChainContextPosBuilder) sub = self.get_chained_lookup_(location, SinglePosBuilder) subs = [] for glyphs, value in pos: if value is None: subs.append(None) continue if not glyphs.isdisjoint(sub.mapping.keys()): sub = self.get_chained_lookup_(location, SinglePosBuilder) for glyph in glyphs: sub.add_pos(location, glyph, value) subs.append(sub) assert len(pos) == len(subs), (pos, subs) chain.rules.append((prefix, [g for g, v in pos], suffix, subs)) def setGlyphClass_(self, location, glyph, glyphClass): oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None)) if oldClass and oldClass != glyphClass: raise FeatureLibError( "Glyph %s was assigned to a different class at %s:%s:%s" % (glyph, oldLocation[0], oldLocation[1], oldLocation[2]), location, ) self.glyphClassDefs_[glyph] = (glyphClass, location) def add_glyphClassDef(self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs): for glyph in baseGlyphs: self.setGlyphClass_(location, glyph, 1) for glyph in ligatureGlyphs: self.setGlyphClass_(location, glyph, 2) for glyph in markGlyphs: self.setGlyphClass_(location, glyph, 3) for glyph in componentGlyphs: self.setGlyphClass_(location, glyph, 4) def add_ligatureCaretByIndex_(self, location, glyphs, carets): for glyph in glyphs: self.ligCaretPoints_.setdefault(glyph, set()).update(carets) def add_ligatureCaretByPos_(self, location, glyphs, carets): for glyph in glyphs: self.ligCaretCoords_.setdefault(glyph, set()).update(carets)
class Builder(object): def __init__(self, featurefile_path, font): self.featurefile_path = featurefile_path self.font = font self.glyphMap = font.getReverseGlyphMap() self.default_language_systems_ = set() self.script_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.language_systems = set() self.named_lookups_ = {} self.cur_lookup_ = None self.cur_lookup_name_ = None self.cur_feature_name_ = None self.lookups_ = [] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] self.parseTree = None self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' # for feature 'aalt' self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' self.aalt_location_ = None self.aalt_alternates_ = {} # for table 'head' self.fontRevision_ = None # 2.71 # for table 'GDEF' self.attachPoints_ = {} # "a" --> {3, 7} self.ligCaretCoords_ = {} # "f_f_i" --> {300, 600} self.ligCaretPoints_ = {} # "f_f_i" --> {3, 7} self.glyphClassDefs_ = {} # "fi" --> (2, (file, line, column)) self.markAttach_ = {} # "acute" --> (4, (file, line, column)) self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4 self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4 def build(self): self.parseTree = Parser(self.featurefile_path).parse() self.parseTree.build(self) self.build_feature_aalt_() self.build_head() for tag in ('GPOS', 'GSUB'): table = self.makeTable(tag) if (table.ScriptList.ScriptCount > 0 or table.FeatureList.FeatureCount > 0 or table.LookupList.LookupCount > 0): fontTable = self.font[tag] = getTableClass(tag)() fontTable.table = table elif tag in self.font: del self.font[tag] gdef = self.buildGDEF() if gdef: self.font["GDEF"] = gdef elif "GDEF" in self.font: del self.font["GDEF"] def get_chained_lookup_(self, location, builder_class): result = builder_class(self.font, location) result.lookupflag = self.lookupflag_ result.markFilterSet = self.lookupflag_markFilterSet_ self.lookups_.append(result) return result def add_lookup_to_feature_(self, lookup, feature_name): for script, lang in self.language_systems: key = (script, lang, feature_name) self.features_.setdefault(key, []).append(lookup) def get_lookup_(self, location, builder_class): if (self.cur_lookup_ and type(self.cur_lookup_) == builder_class and self.cur_lookup_.lookupflag == self.lookupflag_ and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_): return self.cur_lookup_ if self.cur_lookup_name_ and self.cur_lookup_: raise FeatureLibError( "Within a named lookup block, all rules must be of " "the same lookup type and flag", location) self.cur_lookup_ = builder_class(self.font, location) self.cur_lookup_.lookupflag = self.lookupflag_ self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_ self.lookups_.append(self.cur_lookup_) if self.cur_lookup_name_: # We are starting a lookup rule inside a named lookup block. self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_ if self.cur_feature_name_: # We are starting a lookup rule inside a feature. This includes # lookup rules inside named lookups inside features. self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_) return self.cur_lookup_ def build_feature_aalt_(self): if not self.aalt_features_ and not self.aalt_alternates_: return alternates = {g: set(a) for g, a in self.aalt_alternates_.items()} for location, name in self.aalt_features_ + [(None, "aalt")]: feature = [(script, lang, feature, lookups) for (script, lang, feature), lookups in self.features_.items() if feature == name] # "aalt" does not have to specify its own lookups, but it might. if not feature and name != "aalt": raise FeatureLibError("Feature %s has not been defined" % name, location) for script, lang, feature, lookups in feature: for lookup in lookups: for glyph, alts in lookup.getAlternateGlyphs().items(): alternates.setdefault(glyph, set()).update(alts) single = {glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1} multi = {glyph: sorted(repl, key=self.font.getGlyphID) for glyph, repl in alternates.items() if len(repl) > 1} if not single and not multi: return self.features_ = {(script, lang, feature): lookups for (script, lang, feature), lookups in self.features_.items() if feature != "aalt"} old_lookups = self.lookups_ self.lookups_ = [] self.start_feature(self.aalt_location_, "aalt") if single: single_lookup = self.get_lookup_(location, SingleSubstBuilder) single_lookup.mapping = single if multi: multi_lookup = self.get_lookup_(location, AlternateSubstBuilder) multi_lookup.alternates = multi self.end_feature() self.lookups_.extend(old_lookups) def build_head(self): if not self.fontRevision_: return table = self.font.get("head") if not table: # this only happens for unit tests table = self.font["head"] = getTableClass("head")() table.decompile(b"\0" * 54, self.font) table.tableVersion = 1.0 table.created = table.modified = 3406620153 # 2011-12-13 11:22:33 table.fontRevision = self.fontRevision_ def buildGDEF(self): gdef = otTables.GDEF() gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_() gdef.AttachList = \ otl.buildAttachList(self.attachPoints_, self.glyphMap) gdef.LigCaretList = \ otl.buildLigCaretList(self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap) gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_() gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_() gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 1.0 if any((gdef.GlyphClassDef, gdef.AttachList, gdef.LigCaretList, gdef.MarkAttachClassDef, gdef.MarkGlyphSetsDef)): result = getTableClass("GDEF")() result.table = gdef return result else: return None def buildGDEFGlyphClassDef_(self): inferredGlyphClass = {} for lookup in self.lookups_: inferredGlyphClass.update(lookup.inferGlyphClasses()) for markClass in self.parseTree.markClasses.values(): for markClassDef in markClass.definitions: for glyph in markClassDef.glyphSet(): inferredGlyphClass[glyph] = 3 if self.glyphClassDefs_: classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()} else: classes = inferredGlyphClass if classes: result = otTables.GlyphClassDef() result.classDefs = classes return result else: return None def buildGDEFMarkAttachClassDef_(self): classDefs = {g: c for g, (c, _) in self.markAttach_.items()} if not classDefs: return None result = otTables.MarkAttachClassDef() result.classDefs = classDefs return result def buildGDEFMarkGlyphSetsDef_(self): sets = [None] * len(self.markFilterSets_) for glyphs, id in self.markFilterSets_.items(): sets[id] = glyphs return otl.buildMarkGlyphSetsDef(sets, self.glyphMap) def buildLookups_(self, tag): assert tag in ('GPOS', 'GSUB'), tag for lookup in self.lookups_: lookup.lookup_index = None lookups = [] for i, lookup in enumerate(self.lookups_): if lookup.table != tag: continue # TODO: https://github.com/behdad/fonttools/issues/448 # If multiple lookup builders would build equivalent lookups, # emit them only once. This is quadratic in the number of lookups, # but the checks are cheap. If performance ever becomes an issue, # we could hash the lookup content and only compare those with # the same hash value. equivalent = None for other in self.lookups_[:i]: if lookup.equals(other): equivalent = other if equivalent is not None: lookup.lookup_index = equivalent.lookup_index continue lookup.lookup_index = len(lookups) lookups.append(lookup) return [l.build() for l in lookups] def makeTable(self, tag): table = getattr(otTables, tag, None)() table.Version = 1.0 table.ScriptList = otTables.ScriptList() table.ScriptList.ScriptRecord = [] table.FeatureList = otTables.FeatureList() table.FeatureList.FeatureRecord = [] table.LookupList = otTables.LookupList() table.LookupList.Lookup = self.buildLookups_(tag) # Build a table for mapping (tag, lookup_indices) to feature_index. # For example, ('liga', (2,3,7)) --> 23. feature_indices = {} required_feature_indices = {} # ('latn', 'DEU') --> 23 scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24 for key, lookups in sorted(self.features_.items()): script, lang, feature_tag = key # l.lookup_index will be None when a lookup is not needed # for the table under construction. For example, substitution # rules will have no lookup_index while building GPOS tables. lookup_indices = tuple([l.lookup_index for l in lookups if l.lookup_index is not None]) if len(lookup_indices) == 0: continue feature_key = (feature_tag, lookup_indices) feature_index = feature_indices.get(feature_key) if feature_index is None: feature_index = len(table.FeatureList.FeatureRecord) frec = otTables.FeatureRecord() frec.FeatureTag = feature_tag frec.Feature = otTables.Feature() frec.Feature.FeatureParams = None frec.Feature.LookupListIndex = lookup_indices frec.Feature.LookupCount = len(lookup_indices) table.FeatureList.FeatureRecord.append(frec) feature_indices[feature_key] = feature_index scripts.setdefault(script, {}).setdefault(lang, []).append( feature_index) if self.required_features_.get((script, lang)) == feature_tag: required_feature_indices[(script, lang)] = feature_index # Build ScriptList. for script, lang_features in sorted(scripts.items()): srec = otTables.ScriptRecord() srec.ScriptTag = script srec.Script = otTables.Script() srec.Script.DefaultLangSys = None srec.Script.LangSysRecord = [] for lang, feature_indices in sorted(lang_features.items()): langrec = otTables.LangSysRecord() langrec.LangSys = otTables.LangSys() langrec.LangSys.LookupOrder = None req_feature_index = \ required_feature_indices.get((script, lang)) if req_feature_index is None: langrec.LangSys.ReqFeatureIndex = 0xFFFF else: langrec.LangSys.ReqFeatureIndex = req_feature_index langrec.LangSys.FeatureIndex = [i for i in feature_indices if i != req_feature_index] langrec.LangSys.FeatureCount = \ len(langrec.LangSys.FeatureIndex) if lang == "dflt": srec.Script.DefaultLangSys = langrec.LangSys else: langrec.LangSysTag = lang srec.Script.LangSysRecord.append(langrec) srec.Script.LangSysCount = len(srec.Script.LangSysRecord) table.ScriptList.ScriptRecord.append(srec) table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord) table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord) table.LookupList.LookupCount = len(table.LookupList.Lookup) return table def add_language_system(self, location, script, language): # OpenType Feature File Specification, section 4.b.i if (script == "DFLT" and language == "dflt" and self.default_language_systems_): raise FeatureLibError( 'If "languagesystem DFLT dflt" is present, it must be ' 'the first of the languagesystem statements', location) if (script, language) in self.default_language_systems_: raise FeatureLibError( '"languagesystem %s %s" has already been specified' % (script.strip(), language.strip()), location) self.default_language_systems_.add((script, language)) def get_default_language_systems_(self): # OpenType Feature File specification, 4.b.i. languagesystem: # If no "languagesystem" statement is present, then the # implementation must behave exactly as though the following # statement were present at the beginning of the feature file: # languagesystem DFLT dflt; if self.default_language_systems_: return frozenset(self.default_language_systems_) else: return frozenset({('DFLT', 'dflt')}) def start_feature(self, location, name): self.language_systems = self.get_default_language_systems_() self.cur_lookup_ = None self.cur_feature_name_ = name if name == "aalt": self.aalt_location_ = location def end_feature(self): assert self.cur_feature_name_ is not None self.cur_feature_name_ = None self.language_systems = None self.cur_lookup_ = None def start_lookup_block(self, location, name): if name in self.named_lookups_: raise FeatureLibError( 'Lookup "%s" has already been defined' % name, location) if self.cur_feature_name_ == "aalt": raise FeatureLibError( "Lookup blocks cannot be placed inside 'aalt' features; " "move it out, and then refer to it with a lookup statement", location) self.cur_lookup_name_ = name self.named_lookups_[name] = None self.cur_lookup_ = None def end_lookup_block(self): assert self.cur_lookup_name_ is not None self.cur_lookup_name_ = None self.cur_lookup_ = None def add_lookup_call(self, lookup_name): assert lookup_name in self.named_lookups_, lookup_name self.cur_lookup_ = None lookup = self.named_lookups_[lookup_name] self.add_lookup_to_feature_(lookup, self.cur_feature_name_) def set_font_revision(self, location, revision): self.fontRevision_ = revision def set_language(self, location, language, include_default, required): assert(len(language) == 4) if self.cur_feature_name_ in ('aalt', 'size'): raise FeatureLibError( "Language statements are not allowed " "within \"feature %s\"" % self.cur_feature_name_, location) self.cur_lookup_ = None if include_default: langsys = set(self.get_default_language_systems_()) else: langsys = set() langsys.add((self.script_, language)) self.language_systems = frozenset(langsys) if required: key = (self.script_, language) if key in self.required_features_: raise FeatureLibError( "Language %s (script %s) has already " "specified feature %s as its required feature" % ( language.strip(), self.script_.strip(), self.required_features_[key].strip()), location) self.required_features_[key] = self.cur_feature_name_ def getMarkAttachClass_(self, location, glyphs): id = self.markAttachClassID_.get(glyphs) if id is not None: return id id = len(self.markAttachClassID_) + 1 self.markAttachClassID_[glyphs] = id for glyph in glyphs: if glyph in self.markAttach_: _, loc = self.markAttach_[glyph] raise FeatureLibError( "Glyph %s already has been assigned " "a MarkAttachmentType at %s:%d:%d" % ( glyph, loc[0], loc[1], loc[2]), location) self.markAttach_[glyph] = (id, location) return id def getMarkFilterSet_(self, location, glyphs): id = self.markFilterSets_.get(glyphs) if id is not None: return id id = len(self.markFilterSets_) self.markFilterSets_[glyphs] = id return id def set_lookup_flag(self, location, value, markAttach, markFilter): value = value & 0xFF if markAttach: markAttachClass = self.getMarkAttachClass_(location, markAttach) value = value | (markAttachClass << 8) if markFilter: markFilterSet = self.getMarkFilterSet_(location, markFilter) value = value | 0x10 self.lookupflag_markFilterSet_ = markFilterSet else: self.lookupflag_markFilterSet_ = None self.lookupflag_ = value def set_script(self, location, script): if self.cur_feature_name_ in ('aalt', 'size'): raise FeatureLibError( "Script statements are not allowed " "within \"feature %s\"" % self.cur_feature_name_, location) self.cur_lookup_ = None self.script_ = script self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None self.set_language(location, "dflt", include_default=False, required=False) def find_lookup_builders_(self, lookups): """Helper for building chain contextual substitutions Given a list of lookup names, finds the LookupBuilder for each name. If an input name is None, it gets mapped to a None LookupBuilder. """ lookup_builders = [] for lookup in lookups: if lookup is not None: lookup_builders.append(self.named_lookups_.get(lookup.name)) else: lookup_builders.append(None) return lookup_builders def add_attach_points(self, location, glyphs, contourPoints): for glyph in glyphs: self.attachPoints_.setdefault(glyph, set()).update(contourPoints) def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextPosBuilder) lookup.rules.append((prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups): lookup = self.get_lookup_(location, ChainContextSubstBuilder) lookup.substitutions.append((prefix, glyphs, suffix, self.find_lookup_builders_(lookups))) def add_alternate_subst(self, location, prefix, glyph, suffix, replacement): if self.cur_feature_name_ == "aalt": alts = self.aalt_alternates_.setdefault(glyph, set()) alts.update(replacement) return if prefix or suffix: chain = self.get_lookup_(location, ChainContextSubstBuilder) lookup = self.get_chained_lookup_(location, AlternateSubstBuilder) chain.substitutions.append((prefix, [glyph], suffix, [lookup])) else: lookup = self.get_lookup_(location, AlternateSubstBuilder) if glyph in lookup.alternates: raise FeatureLibError( 'Already defined alternates for glyph "%s"' % glyph, location) lookup.alternates[glyph] = replacement def add_feature_reference(self, location, featureName): if self.cur_feature_name_ != "aalt": raise FeatureLibError( 'Feature references are only allowed inside "feature aalt"', location) self.aalt_features_.append((location, featureName)) def add_ligature_subst(self, location, prefix, glyphs, suffix, replacement, forceChain): if prefix or suffix or forceChain: chain = self.get_lookup_(location, ChainContextSubstBuilder) lookup = self.get_chained_lookup_(location, LigatureSubstBuilder) chain.substitutions.append((prefix, glyphs, suffix, [lookup])) else: lookup = self.get_lookup_(location, LigatureSubstBuilder) # OpenType feature file syntax, section 5.d, "Ligature substitution": # "Since the OpenType specification does not allow ligature # substitutions to be specified on target sequences that contain # glyph classes, the implementation software will enumerate # all specific glyph sequences if glyph classes are detected" for g in sorted(itertools.product(*glyphs)): lookup.ligatures[g] = replacement def add_multiple_subst(self, location, prefix, glyph, suffix, replacements): if prefix or suffix: chain = self.get_lookup_(location, ChainContextSubstBuilder) sub = self.get_chained_lookup_(location, MultipleSubstBuilder) sub.mapping[glyph] = replacements chain.substitutions.append((prefix, [{glyph}], suffix, [sub])) return lookup = self.get_lookup_(location, MultipleSubstBuilder) if glyph in lookup.mapping: raise FeatureLibError( 'Already defined substitution for glyph "%s"' % glyph, location) lookup.mapping[glyph] = replacements def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping): lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder) lookup.substitutions.append((old_prefix, old_suffix, mapping)) def add_single_subst(self, location, prefix, suffix, mapping, forceChain): if self.cur_feature_name_ == "aalt": for (from_glyph, to_glyph) in mapping.items(): alts = self.aalt_alternates_.setdefault(from_glyph, set()) alts.add(to_glyph) return if prefix or suffix or forceChain: self.add_single_subst_chained_(location, prefix, suffix, mapping) return lookup = self.get_lookup_(location, SingleSubstBuilder) for (from_glyph, to_glyph) in mapping.items(): if from_glyph in lookup.mapping: raise FeatureLibError( 'Already defined rule for replacing glyph "%s" by "%s"' % (from_glyph, lookup.mapping[from_glyph]), location) lookup.mapping[from_glyph] = to_glyph def find_chainable_SingleSubst_(self, chain, glyphs): """Helper for add_single_subst_chained_()""" for _, _, _, substitutions in chain.substitutions: for sub in substitutions: if (isinstance(sub, SingleSubstBuilder) and not any(g in glyphs for g in sub.mapping.keys())): return sub return None def add_single_subst_chained_(self, location, prefix, suffix, mapping): # https://github.com/behdad/fonttools/issues/512 chain = self.get_lookup_(location, ChainContextSubstBuilder) sub = self.find_chainable_SingleSubst_(chain, set(mapping.keys())) if sub is None: sub = self.get_chained_lookup_(location, SingleSubstBuilder) sub.mapping.update(mapping) chain.substitutions.append((prefix, [mapping.keys()], suffix, [sub])) def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor): lookup = self.get_lookup_(location, CursivePosBuilder) lookup.add_attachment( location, glyphclass, makeOpenTypeAnchor(entryAnchor), makeOpenTypeAnchor(exitAnchor)) def add_marks_(self, location, lookupBuilder, marks): """Helper for add_mark_{base,liga,mark}_pos.""" for _, markClass in marks: for markClassDef in markClass.definitions: for mark in markClassDef.glyphs.glyphSet(): if mark not in lookupBuilder.marks: otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor) lookupBuilder.marks[mark] = ( markClass.name, otMarkAnchor) def add_mark_base_pos(self, location, bases, marks): builder = self.get_lookup_(location, MarkBasePosBuilder) self.add_marks_(location, builder, marks) for baseAnchor, markClass in marks: otBaseAnchor = makeOpenTypeAnchor(baseAnchor) for base in bases: builder.bases.setdefault(base, {})[markClass.name] = ( otBaseAnchor) def add_mark_lig_pos(self, location, ligatures, components): builder = self.get_lookup_(location, MarkLigPosBuilder) componentAnchors = [] for marks in components: anchors = {} self.add_marks_(location, builder, marks) for ligAnchor, markClass in marks: anchors[markClass.name] = makeOpenTypeAnchor(ligAnchor) componentAnchors.append(anchors) for glyph in ligatures: builder.ligatures[glyph] = componentAnchors def add_mark_mark_pos(self, location, baseMarks, marks): builder = self.get_lookup_(location, MarkMarkPosBuilder) self.add_marks_(location, builder, marks) for baseAnchor, markClass in marks: otBaseAnchor = makeOpenTypeAnchor(baseAnchor) for baseMark in baseMarks: builder.baseMarks.setdefault(baseMark, {})[markClass.name] = ( otBaseAnchor) def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2): lookup = self.get_lookup_(location, PairPosBuilder) lookup.addClassPair(location, glyphclass1, value1, glyphclass2, value2) def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): lookup = self.get_lookup_(location, PairPosBuilder) lookup.addGlyphPair(location, glyph1, value1, glyph2, value2) def add_single_pos(self, location, prefix, suffix, pos, forceChain): if prefix or suffix or forceChain: self.add_single_pos_chained_(location, prefix, suffix, pos) else: lookup = self.get_lookup_(location, SinglePosBuilder) for glyphs, value in pos: for glyph in glyphs: lookup.add_pos(location, glyph, value) def add_single_pos_chained_(self, location, prefix, suffix, pos): chain = self.get_lookup_(location, ChainContextPosBuilder) sub = self.get_chained_lookup_(location, SinglePosBuilder) subs = [] for glyphs, value in pos: if value is None: subs.append(None) continue if not glyphs.isdisjoint(sub.mapping.keys()): sub = self.get_chained_lookup_(location, SinglePosBuilder) for glyph in glyphs: sub.add_pos(location, glyph, value) subs.append(sub) assert len(pos) == len(subs), (pos, subs) chain.rules.append( (prefix, [g for g, v in pos], suffix, subs)) def setGlyphClass_(self, location, glyph, glyphClass): oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None)) if oldClass and oldClass != glyphClass: raise FeatureLibError( "Glyph %s was assigned to a different class at %s:%s:%s" % (glyph, oldLocation[0], oldLocation[1], oldLocation[2]), location) self.glyphClassDefs_[glyph] = (glyphClass, location) def add_glyphClassDef(self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs): for glyph in baseGlyphs: self.setGlyphClass_(location, glyph, 1) for glyph in ligatureGlyphs: self.setGlyphClass_(location, glyph, 2) for glyph in markGlyphs: self.setGlyphClass_(location, glyph, 3) for glyph in componentGlyphs: self.setGlyphClass_(location, glyph, 4) def add_ligatureCaretByIndex_(self, location, glyphs, carets): for glyph in glyphs: self.ligCaretPoints_.setdefault(glyph, set()).update(carets) def add_ligatureCaretByPos_(self, location, glyphs, carets): for glyph in glyphs: self.ligCaretCoords_.setdefault(glyph, set()).update(carets)