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

1"""Module to build FeatureVariation tables: 

2https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table 

3 

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 

13 

14from .errors import VarLibError, VarLibValidationError 

15 

16 

17def addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"): 

18 """Add conditional substitutions to a Variable Font. 

19 

20 The `conditionalSubstitutions` argument is a list of (Region, Substitutions) 

21 tuples. 

22 

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. 

31 

32 The minimum and maximum values are expressed in normalized coordinates. 

33 

34 A Substitution is a dict mapping source glyph names to substitute glyph names. 

35 

36 Example: 

37 

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) 

46 

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 """ 

54 

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 

58 

59 _checkSubstitutionGlyphsExist( 

60 glyphNames=set(font.getGlyphOrder()), 

61 substitutions=conditionalSubstitutions, 

62 ) 

63 

64 substitutions = overlayFeatureVariations(conditionalSubstitutions) 

65 

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 ) 

80 

81 # setup lookups 

82 lookupMap = buildSubstitutionLookups( 

83 font["GSUB"].table, allSubstitutions, processLast 

84 ) 

85 

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 ) 

94 

95 addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTags) 

96 

97 

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 

106 

107 

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 ) 

119 

120 

121def overlayFeatureVariations(conditionalSubstitutions): 

122 """Compute overlaps between all conditional substitutions. 

123 

124 The `conditionalSubstitutions` argument is a list of (Region, Substitutions) 

125 tuples. 

126 

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. 

135 

136 The minimum and maximum values are expressed in normalized coordinates. 

137 

138 A Substitution is a dict mapping source glyph names to substitute glyph names. 

139 

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. 

148 

149 Example:: 

150 

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'}])] 

164 

165 """ 

166 

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 

177 

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 

197 

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 

216 

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 

234 

235 

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# 

244 

245 

246def overlayBox(top, bot): 

247 """Overlays ``top`` box on top of ``bot`` box. 

248 

249 Returns two items: 

250 

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 """ 

256 

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 

269 

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. 

280 

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 

297 

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. 

301 

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 

308 

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 

321 

322 remainder[axisTag] = minimum, maximum 

323 

324 if fullyInside: 

325 # bot is fully within intersection. Remainder is empty. 

326 return intersection, None 

327 

328 return intersection, remainder 

329 

330 

331def cleanupBox(box): 

332 """Return a sparse copy of `box`, without redundant (default) values. 

333 

334 >>> cleanupBox({}) 

335 {} 

336 >>> cleanupBox({'wdth': (0.0, 1.0)}) 

337 {'wdth': (0.0, 1.0)} 

338 >>> cleanupBox({'wdth': (-1.0, 1.0)}) 

339 {} 

340 

341 """ 

342 return {tag: limit for tag, limit in box.items() if limit != (-1.0, 1.0)} 

343 

344 

345# 

346# Low level implementation 

347# 

348 

349 

350def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="rvrn"): 

351 """Low level implementation of addFeatureVariations that directly 

352 models the possibilities of the FeatureVariations table.""" 

353 

354 featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag) 

355 processLast = "rvrn" not in featureTags or len(featureTags) > 1 

356 

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 

369 

370 varFeatureIndices = set() 

371 

372 existingTags = { 

373 feature.FeatureTag 

374 for feature in table.FeatureList.FeatureRecord 

375 if feature.FeatureTag in featureTags 

376 } 

377 

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) 

386 

387 sortFeatureList(table) 

388 

389 for varFeature in varFeatures: 

390 varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature) 

391 

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) 

403 

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 ) 

412 

413 axisIndices = { 

414 axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes) 

415 } 

416 

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 ) 

437 

438 records.append( 

439 buildFeatureTableSubstitutionRecord( 

440 varFeatureIndex, combinedLookupIndices 

441 ) 

442 ) 

443 featureVariationRecords.append( 

444 buildFeatureVariationRecord(conditionTable, records) 

445 ) 

446 

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) 

459 

460 

461# 

462# Building GSUB/FeatureVariations internals 

463# 

464 

465 

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 

471 

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 = [] 

478 

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 

485 

486 langrec = ot.LangSysRecord() 

487 langrec.LangSys = ot.LangSys() 

488 langrec.LangSys.ReqFeatureIndex = 0xFFFF 

489 langrec.LangSys.FeatureIndex = [] 

490 srec.Script.DefaultLangSys = langrec.LangSys 

491 

492 gsub.ScriptList.ScriptRecord.append(srec) 

493 gsub.ScriptList.ScriptCount = 1 

494 gsub.FeatureVariations = None 

495 

496 return fontTable 

497 

498 

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) 

513 

514 

515class ShifterVisitor(TTVisitor): 

516 def __init__(self, shift): 

517 self.shift = shift 

518 

519 

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) 

525 

526 

527@ShifterVisitor.register_attr( 

528 (ot.SubstLookupRecord, ot.PosLookupRecord), "LookupListIndex" 

529) 

530def visit(visitor, obj, attr, value): 

531 setattr(obj, attr, visitor.shift + value) 

532 

533 

534def buildSubstitutionLookups(gsub, allSubstitutions, processLast=False): 

535 """Build the lookups for the glyph substitutions, return a dict mapping 

536 the substitution to lookup indices.""" 

537 

538 # Insert lookups at the beginning of the lookup vector 

539 # https://github.com/googlefonts/fontmake/issues/950 

540 

541 firstIndex = len(gsub.LookupList.Lookup) if processLast else 0 

542 lookupMap = {} 

543 for i, substitutionMap in enumerate(allSubstitutions): 

544 lookupMap[substitutionMap] = firstIndex + i 

545 

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) 

552 

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 

563 

564 

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 

572 

573 

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 

582 

583 

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 

595 

596 

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 

605 

606 

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 

615 

616 

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 ) 

631 

632 # Remap the feature indices 

633 remapFeatures(table, featureRemap) 

634 

635 

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) 

645 

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] 

650 

651 

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] 

656 

657 

658if __name__ == "__main__": 

659 import doctest, sys 

660 

661 sys.exit(doctest.testmod().failed)