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)
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 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.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.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)