Spaces:
Running
Running
"""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) | |