Spaces:
Paused
Paused
| """Module to build FeatureVariation tables: | |
| https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table | |
| NOTE: The API is experimental and subject to change. | |
| """ | |
| from fontTools.misc.dictTools import hashdict | |
| from fontTools.misc.intTools import bit_count | |
| from fontTools.ttLib import newTable | |
| from fontTools.ttLib.tables import otTables as ot | |
| from fontTools.ttLib.ttVisitor import TTVisitor | |
| from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable | |
| from collections import OrderedDict | |
| from .errors import VarLibError, VarLibValidationError | |
| def addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"): | |
| """Add conditional substitutions to a Variable Font. | |
| The `conditionalSubstitutions` argument is a list of (Region, Substitutions) | |
| tuples. | |
| A Region is a list of Boxes. A Box is a dict mapping axisTags to | |
| (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are | |
| interpretted as extending to end of axis in each direction. A Box represents | |
| an orthogonal 'rectangular' subset of an N-dimensional design space. | |
| A Region represents a more complex subset of an N-dimensional design space, | |
| ie. the union of all the Boxes in the Region. | |
| For efficiency, Boxes within a Region should ideally not overlap, but | |
| functionality is not compromised if they do. | |
| The minimum and maximum values are expressed in normalized coordinates. | |
| A Substitution is a dict mapping source glyph names to substitute glyph names. | |
| Example: | |
| # >>> f = TTFont(srcPath) | |
| # >>> condSubst = [ | |
| # ... # A list of (Region, Substitution) tuples. | |
| # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), | |
| # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), | |
| # ... ] | |
| # >>> addFeatureVariations(f, condSubst) | |
| # >>> f.save(dstPath) | |
| The `featureTag` parameter takes either a str or a iterable of str (the single str | |
| is kept for backwards compatibility), and defines which feature(s) will be | |
| associated with the feature variations. | |
| Note, if this is "rvrn", then the substitution lookup will be inserted at the | |
| beginning of the lookup list so that it is processed before others, otherwise | |
| for any other feature tags it will be appended last. | |
| """ | |
| # process first when "rvrn" is the only listed tag | |
| featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag) | |
| processLast = "rvrn" not in featureTags or len(featureTags) > 1 | |
| _checkSubstitutionGlyphsExist( | |
| glyphNames=set(font.getGlyphOrder()), | |
| substitutions=conditionalSubstitutions, | |
| ) | |
| substitutions = overlayFeatureVariations(conditionalSubstitutions) | |
| # turn substitution dicts into tuples of tuples, so they are hashable | |
| conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable( | |
| substitutions | |
| ) | |
| if "GSUB" not in font: | |
| font["GSUB"] = buildGSUB() | |
| else: | |
| existingTags = _existingVariableFeatures(font["GSUB"].table).intersection( | |
| featureTags | |
| ) | |
| if existingTags: | |
| raise VarLibError( | |
| f"FeatureVariations already exist for feature tag(s): {existingTags}" | |
| ) | |
| # setup lookups | |
| lookupMap = buildSubstitutionLookups( | |
| font["GSUB"].table, allSubstitutions, processLast | |
| ) | |
| # addFeatureVariationsRaw takes a list of | |
| # ( {condition}, [ lookup indices ] ) | |
| # so rearrange our lookups to match | |
| conditionsAndLookups = [] | |
| for conditionSet, substitutions in conditionalSubstitutions: | |
| conditionsAndLookups.append( | |
| (conditionSet, [lookupMap[s] for s in substitutions]) | |
| ) | |
| addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTags) | |
| def _existingVariableFeatures(table): | |
| existingFeatureVarsTags = set() | |
| if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None: | |
| features = table.FeatureList.FeatureRecord | |
| for fvr in table.FeatureVariations.FeatureVariationRecord: | |
| for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord: | |
| existingFeatureVarsTags.add(features[ftsr.FeatureIndex].FeatureTag) | |
| return existingFeatureVarsTags | |
| def _checkSubstitutionGlyphsExist(glyphNames, substitutions): | |
| referencedGlyphNames = set() | |
| for _, substitution in substitutions: | |
| referencedGlyphNames |= substitution.keys() | |
| referencedGlyphNames |= set(substitution.values()) | |
| missing = referencedGlyphNames - glyphNames | |
| if missing: | |
| raise VarLibValidationError( | |
| "Missing glyphs are referenced in conditional substitution rules:" | |
| f" {', '.join(missing)}" | |
| ) | |
| def overlayFeatureVariations(conditionalSubstitutions): | |
| """Compute overlaps between all conditional substitutions. | |
| The `conditionalSubstitutions` argument is a list of (Region, Substitutions) | |
| tuples. | |
| A Region is a list of Boxes. A Box is a dict mapping axisTags to | |
| (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are | |
| interpretted as extending to end of axis in each direction. A Box represents | |
| an orthogonal 'rectangular' subset of an N-dimensional design space. | |
| A Region represents a more complex subset of an N-dimensional design space, | |
| ie. the union of all the Boxes in the Region. | |
| For efficiency, Boxes within a Region should ideally not overlap, but | |
| functionality is not compromised if they do. | |
| The minimum and maximum values are expressed in normalized coordinates. | |
| A Substitution is a dict mapping source glyph names to substitute glyph names. | |
| Returns data is in similar but different format. Overlaps of distinct | |
| substitution Boxes (*not* Regions) are explicitly listed as distinct rules, | |
| and rules with the same Box merged. The more specific rules appear earlier | |
| in the resulting list. Moreover, instead of just a dictionary of substitutions, | |
| a list of dictionaries is returned for substitutions corresponding to each | |
| unique space, with each dictionary being identical to one of the input | |
| substitution dictionaries. These dictionaries are not merged to allow data | |
| sharing when they are converted into font tables. | |
| Example:: | |
| >>> condSubst = [ | |
| ... # A list of (Region, Substitution) tuples. | |
| ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), | |
| ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), | |
| ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), | |
| ... ([{"wght": (0.5, 1.0), "wdth": (-1, 1.0)}], {"dollar": "dollar.rvrn"}), | |
| ... ] | |
| >>> from pprint import pprint | |
| >>> pprint(overlayFeatureVariations(condSubst)) | |
| [({'wdth': (0.5, 1.0), 'wght': (0.5, 1.0)}, | |
| [{'dollar': 'dollar.rvrn'}, {'cent': 'cent.rvrn'}]), | |
| ({'wdth': (0.5, 1.0)}, [{'cent': 'cent.rvrn'}]), | |
| ({'wght': (0.5, 1.0)}, [{'dollar': 'dollar.rvrn'}])] | |
| """ | |
| # Merge same-substitutions rules, as this creates fewer number oflookups. | |
| merged = OrderedDict() | |
| for value, key in conditionalSubstitutions: | |
| key = hashdict(key) | |
| if key in merged: | |
| merged[key].extend(value) | |
| else: | |
| merged[key] = value | |
| conditionalSubstitutions = [(v, dict(k)) for k, v in merged.items()] | |
| del merged | |
| # Merge same-region rules, as this is cheaper. | |
| # Also convert boxes to hashdict() | |
| # | |
| # Reversing is such that earlier entries win in case of conflicting substitution | |
| # rules for the same region. | |
| merged = OrderedDict() | |
| for key, value in reversed(conditionalSubstitutions): | |
| key = tuple( | |
| sorted( | |
| (hashdict(cleanupBox(k)) for k in key), | |
| key=lambda d: tuple(sorted(d.items())), | |
| ) | |
| ) | |
| if key in merged: | |
| merged[key].update(value) | |
| else: | |
| merged[key] = dict(value) | |
| conditionalSubstitutions = list(reversed(merged.items())) | |
| del merged | |
| # Overlay | |
| # | |
| # Rank is the bit-set of the index of all contributing layers. | |
| initMapInit = ((hashdict(), 0),) # Initializer representing the entire space | |
| boxMap = OrderedDict(initMapInit) # Map from Box to Rank | |
| for i, (currRegion, _) in enumerate(conditionalSubstitutions): | |
| newMap = OrderedDict(initMapInit) | |
| currRank = 1 << i | |
| for box, rank in boxMap.items(): | |
| for currBox in currRegion: | |
| intersection, remainder = overlayBox(currBox, box) | |
| if intersection is not None: | |
| intersection = hashdict(intersection) | |
| newMap[intersection] = newMap.get(intersection, 0) | rank | currRank | |
| if remainder is not None: | |
| remainder = hashdict(remainder) | |
| newMap[remainder] = newMap.get(remainder, 0) | rank | |
| boxMap = newMap | |
| # Generate output | |
| items = [] | |
| for box, rank in sorted( | |
| boxMap.items(), key=(lambda BoxAndRank: -bit_count(BoxAndRank[1])) | |
| ): | |
| # Skip any box that doesn't have any substitution. | |
| if rank == 0: | |
| continue | |
| substsList = [] | |
| i = 0 | |
| while rank: | |
| if rank & 1: | |
| substsList.append(conditionalSubstitutions[i][1]) | |
| rank >>= 1 | |
| i += 1 | |
| items.append((dict(box), substsList)) | |
| return items | |
| # | |
| # Terminology: | |
| # | |
| # A 'Box' is a dict representing an orthogonal "rectangular" bit of N-dimensional space. | |
| # The keys in the dict are axis tags, the values are (minValue, maxValue) tuples. | |
| # Missing dimensions (keys) are substituted by the default min and max values | |
| # from the corresponding axes. | |
| # | |
| def overlayBox(top, bot): | |
| """Overlays ``top`` box on top of ``bot`` box. | |
| Returns two items: | |
| * Box for intersection of ``top`` and ``bot``, or None if they don't intersect. | |
| * Box for remainder of ``bot``. Remainder box might not be exact (since the | |
| remainder might not be a simple box), but is inclusive of the exact | |
| remainder. | |
| """ | |
| # Intersection | |
| intersection = {} | |
| intersection.update(top) | |
| intersection.update(bot) | |
| for axisTag in set(top) & set(bot): | |
| min1, max1 = top[axisTag] | |
| min2, max2 = bot[axisTag] | |
| minimum = max(min1, min2) | |
| maximum = min(max1, max2) | |
| if not minimum < maximum: | |
| return None, bot # Do not intersect | |
| intersection[axisTag] = minimum, maximum | |
| # Remainder | |
| # | |
| # Remainder is empty if bot's each axis range lies within that of intersection. | |
| # | |
| # Remainder is shrank if bot's each, except for exactly one, axis range lies | |
| # within that of intersection, and that one axis, it extrudes out of the | |
| # intersection only on one side. | |
| # | |
| # Bot is returned in full as remainder otherwise, as true remainder is not | |
| # representable as a single box. | |
| remainder = dict(bot) | |
| extruding = False | |
| fullyInside = True | |
| for axisTag in top: | |
| if axisTag in bot: | |
| continue | |
| extruding = True | |
| fullyInside = False | |
| break | |
| for axisTag in bot: | |
| if axisTag not in top: | |
| continue # Axis range lies fully within | |
| min1, max1 = intersection[axisTag] | |
| min2, max2 = bot[axisTag] | |
| if min1 <= min2 and max2 <= max1: | |
| continue # Axis range lies fully within | |
| # Bot's range doesn't fully lie within that of top's for this axis. | |
| # We know they intersect, so it cannot lie fully without either; so they | |
| # overlap. | |
| # If we have had an overlapping axis before, remainder is not | |
| # representable as a box, so return full bottom and go home. | |
| if extruding: | |
| return intersection, bot | |
| extruding = True | |
| fullyInside = False | |
| # Otherwise, cut remainder on this axis and continue. | |
| if min1 <= min2: | |
| # Right side survives. | |
| minimum = max(max1, min2) | |
| maximum = max2 | |
| elif max2 <= max1: | |
| # Left side survives. | |
| minimum = min2 | |
| maximum = min(min1, max2) | |
| else: | |
| # Remainder leaks out from both sides. Can't cut either. | |
| return intersection, bot | |
| remainder[axisTag] = minimum, maximum | |
| if fullyInside: | |
| # bot is fully within intersection. Remainder is empty. | |
| return intersection, None | |
| return intersection, remainder | |
| def cleanupBox(box): | |
| """Return a sparse copy of `box`, without redundant (default) values. | |
| >>> cleanupBox({}) | |
| {} | |
| >>> cleanupBox({'wdth': (0.0, 1.0)}) | |
| {'wdth': (0.0, 1.0)} | |
| >>> cleanupBox({'wdth': (-1.0, 1.0)}) | |
| {} | |
| """ | |
| return {tag: limit for tag, limit in box.items() if limit != (-1.0, 1.0)} | |
| # | |
| # Low level implementation | |
| # | |
| def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="rvrn"): | |
| """Low level implementation of addFeatureVariations that directly | |
| models the possibilities of the FeatureVariations table.""" | |
| featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag) | |
| processLast = "rvrn" not in featureTags or len(featureTags) > 1 | |
| # | |
| # if a <featureTag> feature is not present: | |
| # make empty <featureTag> feature | |
| # sort features, get <featureTag> feature index | |
| # add <featureTag> feature to all scripts | |
| # if a <featureTag> feature is present: | |
| # reuse <featureTag> feature index | |
| # make lookups | |
| # add feature variations | |
| # | |
| if table.Version < 0x00010001: | |
| table.Version = 0x00010001 # allow table.FeatureVariations | |
| varFeatureIndices = set() | |
| existingTags = { | |
| feature.FeatureTag | |
| for feature in table.FeatureList.FeatureRecord | |
| if feature.FeatureTag in featureTags | |
| } | |
| newTags = set(featureTags) - existingTags | |
| if newTags: | |
| varFeatures = [] | |
| for featureTag in sorted(newTags): | |
| varFeature = buildFeatureRecord(featureTag, []) | |
| table.FeatureList.FeatureRecord.append(varFeature) | |
| varFeatures.append(varFeature) | |
| table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord) | |
| sortFeatureList(table) | |
| for varFeature in varFeatures: | |
| varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature) | |
| for scriptRecord in table.ScriptList.ScriptRecord: | |
| if scriptRecord.Script.DefaultLangSys is None: | |
| raise VarLibError( | |
| "Feature variations require that the script " | |
| f"'{scriptRecord.ScriptTag}' defines a default language system." | |
| ) | |
| langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord] | |
| for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems: | |
| langSys.FeatureIndex.append(varFeatureIndex) | |
| langSys.FeatureCount = len(langSys.FeatureIndex) | |
| varFeatureIndices.add(varFeatureIndex) | |
| if existingTags: | |
| # indices may have changed if we inserted new features and sorted feature list | |
| # so we must do this after the above | |
| varFeatureIndices.update( | |
| index | |
| for index, feature in enumerate(table.FeatureList.FeatureRecord) | |
| if feature.FeatureTag in existingTags | |
| ) | |
| axisIndices = { | |
| axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes) | |
| } | |
| hasFeatureVariations = ( | |
| hasattr(table, "FeatureVariations") and table.FeatureVariations is not None | |
| ) | |
| featureVariationRecords = [] | |
| for conditionSet, lookupIndices in conditionalSubstitutions: | |
| conditionTable = [] | |
| for axisTag, (minValue, maxValue) in sorted(conditionSet.items()): | |
| if minValue > maxValue: | |
| raise VarLibValidationError( | |
| "A condition set has a minimum value above the maximum value." | |
| ) | |
| ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue) | |
| conditionTable.append(ct) | |
| records = [] | |
| for varFeatureIndex in sorted(varFeatureIndices): | |
| existingLookupIndices = table.FeatureList.FeatureRecord[ | |
| varFeatureIndex | |
| ].Feature.LookupListIndex | |
| combinedLookupIndices = ( | |
| existingLookupIndices + lookupIndices | |
| if processLast | |
| else lookupIndices + existingLookupIndices | |
| ) | |
| records.append( | |
| buildFeatureTableSubstitutionRecord( | |
| varFeatureIndex, combinedLookupIndices | |
| ) | |
| ) | |
| if hasFeatureVariations and ( | |
| fvr := findFeatureVariationRecord(table.FeatureVariations, conditionTable) | |
| ): | |
| fvr.FeatureTableSubstitution.SubstitutionRecord.extend(records) | |
| fvr.FeatureTableSubstitution.SubstitutionCount = len( | |
| fvr.FeatureTableSubstitution.SubstitutionRecord | |
| ) | |
| else: | |
| featureVariationRecords.append( | |
| buildFeatureVariationRecord(conditionTable, records) | |
| ) | |
| if hasFeatureVariations: | |
| if table.FeatureVariations.Version != 0x00010000: | |
| raise VarLibError( | |
| "Unsupported FeatureVariations table version: " | |
| f"0x{table.FeatureVariations.Version:08x} (expected 0x00010000)." | |
| ) | |
| table.FeatureVariations.FeatureVariationRecord.extend(featureVariationRecords) | |
| table.FeatureVariations.FeatureVariationCount = len( | |
| table.FeatureVariations.FeatureVariationRecord | |
| ) | |
| else: | |
| table.FeatureVariations = buildFeatureVariations(featureVariationRecords) | |
| # | |
| # Building GSUB/FeatureVariations internals | |
| # | |
| def buildGSUB(): | |
| """Build a GSUB table from scratch.""" | |
| fontTable = newTable("GSUB") | |
| gsub = fontTable.table = ot.GSUB() | |
| gsub.Version = 0x00010001 # allow gsub.FeatureVariations | |
| gsub.ScriptList = ot.ScriptList() | |
| gsub.ScriptList.ScriptRecord = [] | |
| gsub.FeatureList = ot.FeatureList() | |
| gsub.FeatureList.FeatureRecord = [] | |
| gsub.LookupList = ot.LookupList() | |
| gsub.LookupList.Lookup = [] | |
| srec = ot.ScriptRecord() | |
| srec.ScriptTag = "DFLT" | |
| srec.Script = ot.Script() | |
| srec.Script.DefaultLangSys = None | |
| srec.Script.LangSysRecord = [] | |
| srec.Script.LangSysCount = 0 | |
| langrec = ot.LangSysRecord() | |
| langrec.LangSys = ot.LangSys() | |
| langrec.LangSys.ReqFeatureIndex = 0xFFFF | |
| langrec.LangSys.FeatureIndex = [] | |
| srec.Script.DefaultLangSys = langrec.LangSys | |
| gsub.ScriptList.ScriptRecord.append(srec) | |
| gsub.ScriptList.ScriptCount = 1 | |
| gsub.FeatureVariations = None | |
| return fontTable | |
| def makeSubstitutionsHashable(conditionalSubstitutions): | |
| """Turn all the substitution dictionaries in sorted tuples of tuples so | |
| they are hashable, to detect duplicates so we don't write out redundant | |
| data.""" | |
| allSubstitutions = set() | |
| condSubst = [] | |
| for conditionSet, substitutionMaps in conditionalSubstitutions: | |
| substitutions = [] | |
| for substitutionMap in substitutionMaps: | |
| subst = tuple(sorted(substitutionMap.items())) | |
| substitutions.append(subst) | |
| allSubstitutions.add(subst) | |
| condSubst.append((conditionSet, substitutions)) | |
| return condSubst, sorted(allSubstitutions) | |
| class ShifterVisitor(TTVisitor): | |
| def __init__(self, shift): | |
| self.shift = shift | |
| # GSUB/GPOS | |
| def visit(visitor, obj, attr, value): | |
| shift = visitor.shift | |
| value = [l + shift for l in value] | |
| setattr(obj, attr, value) | |
| def visit(visitor, obj, attr, value): | |
| setattr(obj, attr, visitor.shift + value) | |
| def buildSubstitutionLookups(gsub, allSubstitutions, processLast=False): | |
| """Build the lookups for the glyph substitutions, return a dict mapping | |
| the substitution to lookup indices.""" | |
| # Insert lookups at the beginning of the lookup vector | |
| # https://github.com/googlefonts/fontmake/issues/950 | |
| firstIndex = len(gsub.LookupList.Lookup) if processLast else 0 | |
| lookupMap = {} | |
| for i, substitutionMap in enumerate(allSubstitutions): | |
| lookupMap[substitutionMap] = firstIndex + i | |
| if not processLast: | |
| # Shift all lookup indices in gsub by len(allSubstitutions) | |
| shift = len(allSubstitutions) | |
| visitor = ShifterVisitor(shift) | |
| visitor.visit(gsub.FeatureList.FeatureRecord) | |
| visitor.visit(gsub.LookupList.Lookup) | |
| for i, subst in enumerate(allSubstitutions): | |
| substMap = dict(subst) | |
| lookup = buildLookup([buildSingleSubstSubtable(substMap)]) | |
| if processLast: | |
| gsub.LookupList.Lookup.append(lookup) | |
| else: | |
| gsub.LookupList.Lookup.insert(i, lookup) | |
| assert gsub.LookupList.Lookup[lookupMap[subst]] is lookup | |
| gsub.LookupList.LookupCount = len(gsub.LookupList.Lookup) | |
| return lookupMap | |
| def buildFeatureVariations(featureVariationRecords): | |
| """Build the FeatureVariations subtable.""" | |
| fv = ot.FeatureVariations() | |
| fv.Version = 0x00010000 | |
| fv.FeatureVariationRecord = featureVariationRecords | |
| fv.FeatureVariationCount = len(featureVariationRecords) | |
| return fv | |
| def buildFeatureRecord(featureTag, lookupListIndices): | |
| """Build a FeatureRecord.""" | |
| fr = ot.FeatureRecord() | |
| fr.FeatureTag = featureTag | |
| fr.Feature = ot.Feature() | |
| fr.Feature.LookupListIndex = lookupListIndices | |
| fr.Feature.populateDefaults() | |
| return fr | |
| def buildFeatureVariationRecord(conditionTable, substitutionRecords): | |
| """Build a FeatureVariationRecord.""" | |
| fvr = ot.FeatureVariationRecord() | |
| fvr.ConditionSet = ot.ConditionSet() | |
| fvr.ConditionSet.ConditionTable = conditionTable | |
| fvr.ConditionSet.ConditionCount = len(conditionTable) | |
| fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution() | |
| fvr.FeatureTableSubstitution.Version = 0x00010000 | |
| fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords | |
| fvr.FeatureTableSubstitution.SubstitutionCount = len(substitutionRecords) | |
| return fvr | |
| def buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices): | |
| """Build a FeatureTableSubstitutionRecord.""" | |
| ftsr = ot.FeatureTableSubstitutionRecord() | |
| ftsr.FeatureIndex = featureIndex | |
| ftsr.Feature = ot.Feature() | |
| ftsr.Feature.LookupListIndex = lookupListIndices | |
| ftsr.Feature.LookupCount = len(lookupListIndices) | |
| return ftsr | |
| def buildConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue): | |
| """Build a ConditionTable.""" | |
| ct = ot.ConditionTable() | |
| ct.Format = 1 | |
| ct.AxisIndex = axisIndex | |
| ct.FilterRangeMinValue = filterRangeMinValue | |
| ct.FilterRangeMaxValue = filterRangeMaxValue | |
| return ct | |
| def findFeatureVariationRecord(featureVariations, conditionTable): | |
| """Find a FeatureVariationRecord that has the same conditionTable.""" | |
| if featureVariations.Version != 0x00010000: | |
| raise VarLibError( | |
| "Unsupported FeatureVariations table version: " | |
| f"0x{featureVariations.Version:08x} (expected 0x00010000)." | |
| ) | |
| for fvr in featureVariations.FeatureVariationRecord: | |
| if conditionTable == fvr.ConditionSet.ConditionTable: | |
| return fvr | |
| return None | |
| def sortFeatureList(table): | |
| """Sort the feature list by feature tag, and remap the feature indices | |
| elsewhere. This is needed after the feature list has been modified. | |
| """ | |
| # decorate, sort, undecorate, because we need to make an index remapping table | |
| tagIndexFea = [ | |
| (fea.FeatureTag, index, fea) | |
| for index, fea in enumerate(table.FeatureList.FeatureRecord) | |
| ] | |
| tagIndexFea.sort() | |
| table.FeatureList.FeatureRecord = [fea for tag, index, fea in tagIndexFea] | |
| featureRemap = dict( | |
| zip([index for tag, index, fea in tagIndexFea], range(len(tagIndexFea))) | |
| ) | |
| # Remap the feature indices | |
| remapFeatures(table, featureRemap) | |
| def remapFeatures(table, featureRemap): | |
| """Go through the scripts list, and remap feature indices.""" | |
| for scriptIndex, script in enumerate(table.ScriptList.ScriptRecord): | |
| defaultLangSys = script.Script.DefaultLangSys | |
| if defaultLangSys is not None: | |
| _remapLangSys(defaultLangSys, featureRemap) | |
| for langSysRecordIndex, langSysRec in enumerate(script.Script.LangSysRecord): | |
| langSys = langSysRec.LangSys | |
| _remapLangSys(langSys, featureRemap) | |
| if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None: | |
| for fvr in table.FeatureVariations.FeatureVariationRecord: | |
| for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord: | |
| ftsr.FeatureIndex = featureRemap[ftsr.FeatureIndex] | |
| def _remapLangSys(langSys, featureRemap): | |
| if langSys.ReqFeatureIndex != 0xFFFF: | |
| langSys.ReqFeatureIndex = featureRemap[langSys.ReqFeatureIndex] | |
| langSys.FeatureIndex = [featureRemap[index] for index in langSys.FeatureIndex] | |
| if __name__ == "__main__": | |
| import doctest, sys | |
| sys.exit(doctest.testmod().failed) | |