Coverage for /usr/lib/python3/dist-packages/fontTools/varLib/featureVars.py: 11%
307 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""Module to build FeatureVariation tables:
2https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table
4NOTE: The API is experimental and subject to change.
5"""
6from fontTools.misc.dictTools import hashdict
7from fontTools.misc.intTools import bit_count
8from fontTools.ttLib import newTable
9from fontTools.ttLib.tables import otTables as ot
10from fontTools.ttLib.ttVisitor import TTVisitor
11from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
12from collections import OrderedDict
14from .errors import VarLibError, VarLibValidationError
17def addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"):
18 """Add conditional substitutions to a Variable Font.
20 The `conditionalSubstitutions` argument is a list of (Region, Substitutions)
21 tuples.
23 A Region is a list of Boxes. A Box is a dict mapping axisTags to
24 (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are
25 interpretted as extending to end of axis in each direction. A Box represents
26 an orthogonal 'rectangular' subset of an N-dimensional design space.
27 A Region represents a more complex subset of an N-dimensional design space,
28 ie. the union of all the Boxes in the Region.
29 For efficiency, Boxes within a Region should ideally not overlap, but
30 functionality is not compromised if they do.
32 The minimum and maximum values are expressed in normalized coordinates.
34 A Substitution is a dict mapping source glyph names to substitute glyph names.
36 Example:
38 # >>> f = TTFont(srcPath)
39 # >>> condSubst = [
40 # ... # A list of (Region, Substitution) tuples.
41 # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
42 # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
43 # ... ]
44 # >>> addFeatureVariations(f, condSubst)
45 # >>> f.save(dstPath)
47 The `featureTag` parameter takes either a str or a iterable of str (the single str
48 is kept for backwards compatibility), and defines which feature(s) will be
49 associated with the feature variations.
50 Note, if this is "rvrn", then the substitution lookup will be inserted at the
51 beginning of the lookup list so that it is processed before others, otherwise
52 for any other feature tags it will be appended last.
53 """
55 # process first when "rvrn" is the only listed tag
56 featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag)
57 processLast = "rvrn" not in featureTags or len(featureTags) > 1
59 _checkSubstitutionGlyphsExist(
60 glyphNames=set(font.getGlyphOrder()),
61 substitutions=conditionalSubstitutions,
62 )
64 substitutions = overlayFeatureVariations(conditionalSubstitutions)
66 # turn substitution dicts into tuples of tuples, so they are hashable
67 conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable(
68 substitutions
69 )
70 if "GSUB" not in font:
71 font["GSUB"] = buildGSUB()
72 else:
73 existingTags = _existingVariableFeatures(font["GSUB"].table).intersection(
74 featureTags
75 )
76 if existingTags:
77 raise VarLibError(
78 f"FeatureVariations already exist for feature tag(s): {existingTags}"
79 )
81 # setup lookups
82 lookupMap = buildSubstitutionLookups(
83 font["GSUB"].table, allSubstitutions, processLast
84 )
86 # addFeatureVariationsRaw takes a list of
87 # ( {condition}, [ lookup indices ] )
88 # so rearrange our lookups to match
89 conditionsAndLookups = []
90 for conditionSet, substitutions in conditionalSubstitutions:
91 conditionsAndLookups.append(
92 (conditionSet, [lookupMap[s] for s in substitutions])
93 )
95 addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTags)
98def _existingVariableFeatures(table):
99 existingFeatureVarsTags = set()
100 if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None:
101 features = table.FeatureList.FeatureRecord
102 for fvr in table.FeatureVariations.FeatureVariationRecord:
103 for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord:
104 existingFeatureVarsTags.add(features[ftsr.FeatureIndex].FeatureTag)
105 return existingFeatureVarsTags
108def _checkSubstitutionGlyphsExist(glyphNames, substitutions):
109 referencedGlyphNames = set()
110 for _, substitution in substitutions:
111 referencedGlyphNames |= substitution.keys()
112 referencedGlyphNames |= set(substitution.values())
113 missing = referencedGlyphNames - glyphNames
114 if missing:
115 raise VarLibValidationError(
116 "Missing glyphs are referenced in conditional substitution rules:"
117 f" {', '.join(missing)}"
118 )
121def overlayFeatureVariations(conditionalSubstitutions):
122 """Compute overlaps between all conditional substitutions.
124 The `conditionalSubstitutions` argument is a list of (Region, Substitutions)
125 tuples.
127 A Region is a list of Boxes. A Box is a dict mapping axisTags to
128 (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are
129 interpretted as extending to end of axis in each direction. A Box represents
130 an orthogonal 'rectangular' subset of an N-dimensional design space.
131 A Region represents a more complex subset of an N-dimensional design space,
132 ie. the union of all the Boxes in the Region.
133 For efficiency, Boxes within a Region should ideally not overlap, but
134 functionality is not compromised if they do.
136 The minimum and maximum values are expressed in normalized coordinates.
138 A Substitution is a dict mapping source glyph names to substitute glyph names.
140 Returns data is in similar but different format. Overlaps of distinct
141 substitution Boxes (*not* Regions) are explicitly listed as distinct rules,
142 and rules with the same Box merged. The more specific rules appear earlier
143 in the resulting list. Moreover, instead of just a dictionary of substitutions,
144 a list of dictionaries is returned for substitutions corresponding to each
145 unique space, with each dictionary being identical to one of the input
146 substitution dictionaries. These dictionaries are not merged to allow data
147 sharing when they are converted into font tables.
149 Example::
151 >>> condSubst = [
152 ... # A list of (Region, Substitution) tuples.
153 ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
154 ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
155 ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
156 ... ([{"wght": (0.5, 1.0), "wdth": (-1, 1.0)}], {"dollar": "dollar.rvrn"}),
157 ... ]
158 >>> from pprint import pprint
159 >>> pprint(overlayFeatureVariations(condSubst))
160 [({'wdth': (0.5, 1.0), 'wght': (0.5, 1.0)},
161 [{'dollar': 'dollar.rvrn'}, {'cent': 'cent.rvrn'}]),
162 ({'wdth': (0.5, 1.0)}, [{'cent': 'cent.rvrn'}]),
163 ({'wght': (0.5, 1.0)}, [{'dollar': 'dollar.rvrn'}])]
165 """
167 # Merge same-substitutions rules, as this creates fewer number oflookups.
168 merged = OrderedDict()
169 for value, key in conditionalSubstitutions:
170 key = hashdict(key)
171 if key in merged:
172 merged[key].extend(value)
173 else:
174 merged[key] = value
175 conditionalSubstitutions = [(v, dict(k)) for k, v in merged.items()]
176 del merged
178 # Merge same-region rules, as this is cheaper.
179 # Also convert boxes to hashdict()
180 #
181 # Reversing is such that earlier entries win in case of conflicting substitution
182 # rules for the same region.
183 merged = OrderedDict()
184 for key, value in reversed(conditionalSubstitutions):
185 key = tuple(
186 sorted(
187 (hashdict(cleanupBox(k)) for k in key),
188 key=lambda d: tuple(sorted(d.items())),
189 )
190 )
191 if key in merged:
192 merged[key].update(value)
193 else:
194 merged[key] = dict(value)
195 conditionalSubstitutions = list(reversed(merged.items()))
196 del merged
198 # Overlay
199 #
200 # Rank is the bit-set of the index of all contributing layers.
201 initMapInit = ((hashdict(), 0),) # Initializer representing the entire space
202 boxMap = OrderedDict(initMapInit) # Map from Box to Rank
203 for i, (currRegion, _) in enumerate(conditionalSubstitutions):
204 newMap = OrderedDict(initMapInit)
205 currRank = 1 << i
206 for box, rank in boxMap.items():
207 for currBox in currRegion:
208 intersection, remainder = overlayBox(currBox, box)
209 if intersection is not None:
210 intersection = hashdict(intersection)
211 newMap[intersection] = newMap.get(intersection, 0) | rank | currRank
212 if remainder is not None:
213 remainder = hashdict(remainder)
214 newMap[remainder] = newMap.get(remainder, 0) | rank
215 boxMap = newMap
217 # Generate output
218 items = []
219 for box, rank in sorted(
220 boxMap.items(), key=(lambda BoxAndRank: -bit_count(BoxAndRank[1]))
221 ):
222 # Skip any box that doesn't have any substitution.
223 if rank == 0:
224 continue
225 substsList = []
226 i = 0
227 while rank:
228 if rank & 1:
229 substsList.append(conditionalSubstitutions[i][1])
230 rank >>= 1
231 i += 1
232 items.append((dict(box), substsList))
233 return items
236#
237# Terminology:
238#
239# A 'Box' is a dict representing an orthogonal "rectangular" bit of N-dimensional space.
240# The keys in the dict are axis tags, the values are (minValue, maxValue) tuples.
241# Missing dimensions (keys) are substituted by the default min and max values
242# from the corresponding axes.
243#
246def overlayBox(top, bot):
247 """Overlays ``top`` box on top of ``bot`` box.
249 Returns two items:
251 * Box for intersection of ``top`` and ``bot``, or None if they don't intersect.
252 * Box for remainder of ``bot``. Remainder box might not be exact (since the
253 remainder might not be a simple box), but is inclusive of the exact
254 remainder.
255 """
257 # Intersection
258 intersection = {}
259 intersection.update(top)
260 intersection.update(bot)
261 for axisTag in set(top) & set(bot):
262 min1, max1 = top[axisTag]
263 min2, max2 = bot[axisTag]
264 minimum = max(min1, min2)
265 maximum = min(max1, max2)
266 if not minimum < maximum:
267 return None, bot # Do not intersect
268 intersection[axisTag] = minimum, maximum
270 # Remainder
271 #
272 # Remainder is empty if bot's each axis range lies within that of intersection.
273 #
274 # Remainder is shrank if bot's each, except for exactly one, axis range lies
275 # within that of intersection, and that one axis, it extrudes out of the
276 # intersection only on one side.
277 #
278 # Bot is returned in full as remainder otherwise, as true remainder is not
279 # representable as a single box.
281 remainder = dict(bot)
282 extruding = False
283 fullyInside = True
284 for axisTag in top:
285 if axisTag in bot:
286 continue
287 extruding = True
288 fullyInside = False
289 break
290 for axisTag in bot:
291 if axisTag not in top:
292 continue # Axis range lies fully within
293 min1, max1 = intersection[axisTag]
294 min2, max2 = bot[axisTag]
295 if min1 <= min2 and max2 <= max1:
296 continue # Axis range lies fully within
298 # Bot's range doesn't fully lie within that of top's for this axis.
299 # We know they intersect, so it cannot lie fully without either; so they
300 # overlap.
302 # If we have had an overlapping axis before, remainder is not
303 # representable as a box, so return full bottom and go home.
304 if extruding:
305 return intersection, bot
306 extruding = True
307 fullyInside = False
309 # Otherwise, cut remainder on this axis and continue.
310 if min1 <= min2:
311 # Right side survives.
312 minimum = max(max1, min2)
313 maximum = max2
314 elif max2 <= max1:
315 # Left side survives.
316 minimum = min2
317 maximum = min(min1, max2)
318 else:
319 # Remainder leaks out from both sides. Can't cut either.
320 return intersection, bot
322 remainder[axisTag] = minimum, maximum
324 if fullyInside:
325 # bot is fully within intersection. Remainder is empty.
326 return intersection, None
328 return intersection, remainder
331def cleanupBox(box):
332 """Return a sparse copy of `box`, without redundant (default) values.
334 >>> cleanupBox({})
335 {}
336 >>> cleanupBox({'wdth': (0.0, 1.0)})
337 {'wdth': (0.0, 1.0)}
338 >>> cleanupBox({'wdth': (-1.0, 1.0)})
339 {}
341 """
342 return {tag: limit for tag, limit in box.items() if limit != (-1.0, 1.0)}
345#
346# Low level implementation
347#
350def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="rvrn"):
351 """Low level implementation of addFeatureVariations that directly
352 models the possibilities of the FeatureVariations table."""
354 featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag)
355 processLast = "rvrn" not in featureTags or len(featureTags) > 1
357 #
358 # if a <featureTag> feature is not present:
359 # make empty <featureTag> feature
360 # sort features, get <featureTag> feature index
361 # add <featureTag> feature to all scripts
362 # if a <featureTag> feature is present:
363 # reuse <featureTag> feature index
364 # make lookups
365 # add feature variations
366 #
367 if table.Version < 0x00010001:
368 table.Version = 0x00010001 # allow table.FeatureVariations
370 varFeatureIndices = set()
372 existingTags = {
373 feature.FeatureTag
374 for feature in table.FeatureList.FeatureRecord
375 if feature.FeatureTag in featureTags
376 }
378 newTags = set(featureTags) - existingTags
379 if newTags:
380 varFeatures = []
381 for featureTag in sorted(newTags):
382 varFeature = buildFeatureRecord(featureTag, [])
383 table.FeatureList.FeatureRecord.append(varFeature)
384 varFeatures.append(varFeature)
385 table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
387 sortFeatureList(table)
389 for varFeature in varFeatures:
390 varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature)
392 for scriptRecord in table.ScriptList.ScriptRecord:
393 if scriptRecord.Script.DefaultLangSys is None:
394 raise VarLibError(
395 "Feature variations require that the script "
396 f"'{scriptRecord.ScriptTag}' defines a default language system."
397 )
398 langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord]
399 for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems:
400 langSys.FeatureIndex.append(varFeatureIndex)
401 langSys.FeatureCount = len(langSys.FeatureIndex)
402 varFeatureIndices.add(varFeatureIndex)
404 if existingTags:
405 # indices may have changed if we inserted new features and sorted feature list
406 # so we must do this after the above
407 varFeatureIndices.update(
408 index
409 for index, feature in enumerate(table.FeatureList.FeatureRecord)
410 if feature.FeatureTag in existingTags
411 )
413 axisIndices = {
414 axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes)
415 }
417 featureVariationRecords = []
418 for conditionSet, lookupIndices in conditionalSubstitutions:
419 conditionTable = []
420 for axisTag, (minValue, maxValue) in sorted(conditionSet.items()):
421 if minValue > maxValue:
422 raise VarLibValidationError(
423 "A condition set has a minimum value above the maximum value."
424 )
425 ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue)
426 conditionTable.append(ct)
427 records = []
428 for varFeatureIndex in sorted(varFeatureIndices):
429 existingLookupIndices = table.FeatureList.FeatureRecord[
430 varFeatureIndex
431 ].Feature.LookupListIndex
432 combinedLookupIndices = (
433 existingLookupIndices + lookupIndices
434 if processLast
435 else lookupIndices + existingLookupIndices
436 )
438 records.append(
439 buildFeatureTableSubstitutionRecord(
440 varFeatureIndex, combinedLookupIndices
441 )
442 )
443 featureVariationRecords.append(
444 buildFeatureVariationRecord(conditionTable, records)
445 )
447 if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None:
448 if table.FeatureVariations.Version != 0x00010000:
449 raise VarLibError(
450 "Unsupported FeatureVariations table version: "
451 f"0x{table.FeatureVariations.Version:08x} (expected 0x00010000)."
452 )
453 table.FeatureVariations.FeatureVariationRecord.extend(featureVariationRecords)
454 table.FeatureVariations.FeatureVariationCount = len(
455 table.FeatureVariations.FeatureVariationRecord
456 )
457 else:
458 table.FeatureVariations = buildFeatureVariations(featureVariationRecords)
461#
462# Building GSUB/FeatureVariations internals
463#
466def buildGSUB():
467 """Build a GSUB table from scratch."""
468 fontTable = newTable("GSUB")
469 gsub = fontTable.table = ot.GSUB()
470 gsub.Version = 0x00010001 # allow gsub.FeatureVariations
472 gsub.ScriptList = ot.ScriptList()
473 gsub.ScriptList.ScriptRecord = []
474 gsub.FeatureList = ot.FeatureList()
475 gsub.FeatureList.FeatureRecord = []
476 gsub.LookupList = ot.LookupList()
477 gsub.LookupList.Lookup = []
479 srec = ot.ScriptRecord()
480 srec.ScriptTag = "DFLT"
481 srec.Script = ot.Script()
482 srec.Script.DefaultLangSys = None
483 srec.Script.LangSysRecord = []
484 srec.Script.LangSysCount = 0
486 langrec = ot.LangSysRecord()
487 langrec.LangSys = ot.LangSys()
488 langrec.LangSys.ReqFeatureIndex = 0xFFFF
489 langrec.LangSys.FeatureIndex = []
490 srec.Script.DefaultLangSys = langrec.LangSys
492 gsub.ScriptList.ScriptRecord.append(srec)
493 gsub.ScriptList.ScriptCount = 1
494 gsub.FeatureVariations = None
496 return fontTable
499def makeSubstitutionsHashable(conditionalSubstitutions):
500 """Turn all the substitution dictionaries in sorted tuples of tuples so
501 they are hashable, to detect duplicates so we don't write out redundant
502 data."""
503 allSubstitutions = set()
504 condSubst = []
505 for conditionSet, substitutionMaps in conditionalSubstitutions:
506 substitutions = []
507 for substitutionMap in substitutionMaps:
508 subst = tuple(sorted(substitutionMap.items()))
509 substitutions.append(subst)
510 allSubstitutions.add(subst)
511 condSubst.append((conditionSet, substitutions))
512 return condSubst, sorted(allSubstitutions)
515class ShifterVisitor(TTVisitor):
516 def __init__(self, shift):
517 self.shift = shift
520@ShifterVisitor.register_attr(ot.Feature, "LookupListIndex") # GSUB/GPOS
521def visit(visitor, obj, attr, value):
522 shift = visitor.shift
523 value = [l + shift for l in value]
524 setattr(obj, attr, value)
527@ShifterVisitor.register_attr(
528 (ot.SubstLookupRecord, ot.PosLookupRecord), "LookupListIndex"
529)
530def visit(visitor, obj, attr, value):
531 setattr(obj, attr, visitor.shift + value)
534def buildSubstitutionLookups(gsub, allSubstitutions, processLast=False):
535 """Build the lookups for the glyph substitutions, return a dict mapping
536 the substitution to lookup indices."""
538 # Insert lookups at the beginning of the lookup vector
539 # https://github.com/googlefonts/fontmake/issues/950
541 firstIndex = len(gsub.LookupList.Lookup) if processLast else 0
542 lookupMap = {}
543 for i, substitutionMap in enumerate(allSubstitutions):
544 lookupMap[substitutionMap] = firstIndex + i
546 if not processLast:
547 # Shift all lookup indices in gsub by len(allSubstitutions)
548 shift = len(allSubstitutions)
549 visitor = ShifterVisitor(shift)
550 visitor.visit(gsub.FeatureList.FeatureRecord)
551 visitor.visit(gsub.LookupList.Lookup)
553 for i, subst in enumerate(allSubstitutions):
554 substMap = dict(subst)
555 lookup = buildLookup([buildSingleSubstSubtable(substMap)])
556 if processLast:
557 gsub.LookupList.Lookup.append(lookup)
558 else:
559 gsub.LookupList.Lookup.insert(i, lookup)
560 assert gsub.LookupList.Lookup[lookupMap[subst]] is lookup
561 gsub.LookupList.LookupCount = len(gsub.LookupList.Lookup)
562 return lookupMap
565def buildFeatureVariations(featureVariationRecords):
566 """Build the FeatureVariations subtable."""
567 fv = ot.FeatureVariations()
568 fv.Version = 0x00010000
569 fv.FeatureVariationRecord = featureVariationRecords
570 fv.FeatureVariationCount = len(featureVariationRecords)
571 return fv
574def buildFeatureRecord(featureTag, lookupListIndices):
575 """Build a FeatureRecord."""
576 fr = ot.FeatureRecord()
577 fr.FeatureTag = featureTag
578 fr.Feature = ot.Feature()
579 fr.Feature.LookupListIndex = lookupListIndices
580 fr.Feature.populateDefaults()
581 return fr
584def buildFeatureVariationRecord(conditionTable, substitutionRecords):
585 """Build a FeatureVariationRecord."""
586 fvr = ot.FeatureVariationRecord()
587 fvr.ConditionSet = ot.ConditionSet()
588 fvr.ConditionSet.ConditionTable = conditionTable
589 fvr.ConditionSet.ConditionCount = len(conditionTable)
590 fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution()
591 fvr.FeatureTableSubstitution.Version = 0x00010000
592 fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords
593 fvr.FeatureTableSubstitution.SubstitutionCount = len(substitutionRecords)
594 return fvr
597def buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices):
598 """Build a FeatureTableSubstitutionRecord."""
599 ftsr = ot.FeatureTableSubstitutionRecord()
600 ftsr.FeatureIndex = featureIndex
601 ftsr.Feature = ot.Feature()
602 ftsr.Feature.LookupListIndex = lookupListIndices
603 ftsr.Feature.LookupCount = len(lookupListIndices)
604 return ftsr
607def buildConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue):
608 """Build a ConditionTable."""
609 ct = ot.ConditionTable()
610 ct.Format = 1
611 ct.AxisIndex = axisIndex
612 ct.FilterRangeMinValue = filterRangeMinValue
613 ct.FilterRangeMaxValue = filterRangeMaxValue
614 return ct
617def sortFeatureList(table):
618 """Sort the feature list by feature tag, and remap the feature indices
619 elsewhere. This is needed after the feature list has been modified.
620 """
621 # decorate, sort, undecorate, because we need to make an index remapping table
622 tagIndexFea = [
623 (fea.FeatureTag, index, fea)
624 for index, fea in enumerate(table.FeatureList.FeatureRecord)
625 ]
626 tagIndexFea.sort()
627 table.FeatureList.FeatureRecord = [fea for tag, index, fea in tagIndexFea]
628 featureRemap = dict(
629 zip([index for tag, index, fea in tagIndexFea], range(len(tagIndexFea)))
630 )
632 # Remap the feature indices
633 remapFeatures(table, featureRemap)
636def remapFeatures(table, featureRemap):
637 """Go through the scripts list, and remap feature indices."""
638 for scriptIndex, script in enumerate(table.ScriptList.ScriptRecord):
639 defaultLangSys = script.Script.DefaultLangSys
640 if defaultLangSys is not None:
641 _remapLangSys(defaultLangSys, featureRemap)
642 for langSysRecordIndex, langSysRec in enumerate(script.Script.LangSysRecord):
643 langSys = langSysRec.LangSys
644 _remapLangSys(langSys, featureRemap)
646 if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None:
647 for fvr in table.FeatureVariations.FeatureVariationRecord:
648 for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord:
649 ftsr.FeatureIndex = featureRemap[ftsr.FeatureIndex]
652def _remapLangSys(langSys, featureRemap):
653 if langSys.ReqFeatureIndex != 0xFFFF:
654 langSys.ReqFeatureIndex = featureRemap[langSys.ReqFeatureIndex]
655 langSys.FeatureIndex = [featureRemap[index] for index in langSys.FeatureIndex]
658if __name__ == "__main__":
659 import doctest, sys
661 sys.exit(doctest.testmod().failed)