|
"""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. |
|
""" |
|
|
|
|
|
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) |
|
|
|
|
|
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}" |
|
) |
|
|
|
|
|
lookupMap = buildSubstitutionLookups( |
|
font["GSUB"].table, allSubstitutions, processLast |
|
) |
|
|
|
|
|
|
|
|
|
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'}])] |
|
|
|
""" |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
initMapInit = ((hashdict(), 0),) |
|
boxMap = OrderedDict(initMapInit) |
|
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 |
|
|
|
|
|
items = [] |
|
for box, rank in sorted( |
|
boxMap.items(), key=(lambda BoxAndRank: -bit_count(BoxAndRank[1])) |
|
): |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.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 |
|
intersection[axisTag] = minimum, maximum |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
min1, max1 = intersection[axisTag] |
|
min2, max2 = bot[axisTag] |
|
if min1 <= min2 and max2 <= max1: |
|
continue |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if extruding: |
|
return intersection, bot |
|
extruding = True |
|
fullyInside = False |
|
|
|
|
|
if min1 <= min2: |
|
|
|
minimum = max(max1, min2) |
|
maximum = max2 |
|
elif max2 <= max1: |
|
|
|
minimum = min2 |
|
maximum = min(min1, max2) |
|
else: |
|
|
|
return intersection, bot |
|
|
|
remainder[axisTag] = minimum, maximum |
|
|
|
if fullyInside: |
|
|
|
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)} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 table.Version < 0x00010001: |
|
table.Version = 0x00010001 |
|
|
|
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: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def buildGSUB(): |
|
"""Build a GSUB table from scratch.""" |
|
fontTable = newTable("GSUB") |
|
gsub = fontTable.table = ot.GSUB() |
|
gsub.Version = 0x00010001 |
|
|
|
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 |
|
|
|
|
|
@ShifterVisitor.register_attr(ot.Feature, "LookupListIndex") |
|
def visit(visitor, obj, attr, value): |
|
shift = visitor.shift |
|
value = [l + shift for l in value] |
|
setattr(obj, attr, value) |
|
|
|
|
|
@ShifterVisitor.register_attr( |
|
(ot.SubstLookupRecord, ot.PosLookupRecord), "LookupListIndex" |
|
) |
|
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.""" |
|
|
|
|
|
|
|
|
|
firstIndex = len(gsub.LookupList.Lookup) if processLast else 0 |
|
lookupMap = {} |
|
for i, substitutionMap in enumerate(allSubstitutions): |
|
lookupMap[substitutionMap] = firstIndex + i |
|
|
|
if not processLast: |
|
|
|
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. |
|
""" |
|
|
|
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))) |
|
) |
|
|
|
|
|
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) |
|
|