Coverage for /usr/lib/python3/dist-packages/fontTools/otlLib/builder.py: 17%

1121 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1from collections import namedtuple, OrderedDict 

2import os 

3from fontTools.misc.fixedTools import fixedToFloat 

4from fontTools import ttLib 

5from fontTools.ttLib.tables import otTables as ot 

6from fontTools.ttLib.tables.otBase import ( 

7 ValueRecord, 

8 valueRecordFormatDict, 

9 OTTableWriter, 

10 CountReference, 

11) 

12from fontTools.ttLib.tables import otBase 

13from fontTools.feaLib.ast import STATNameStatement 

14from fontTools.otlLib.optimize.gpos import ( 

15 _compression_level_from_env, 

16 compact_lookup, 

17) 

18from fontTools.otlLib.error import OpenTypeLibError 

19from functools import reduce 

20import logging 

21import copy 

22 

23 

24log = logging.getLogger(__name__) 

25 

26 

27def buildCoverage(glyphs, glyphMap): 

28 """Builds a coverage table. 

29 

30 Coverage tables (as defined in the `OpenType spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#coverage-table>`__) 

31 are used in all OpenType Layout lookups apart from the Extension type, and 

32 define the glyphs involved in a layout subtable. This allows shaping engines 

33 to compare the glyph stream with the coverage table and quickly determine 

34 whether a subtable should be involved in a shaping operation. 

35 

36 This function takes a list of glyphs and a glyphname-to-ID map, and 

37 returns a ``Coverage`` object representing the coverage table. 

38 

39 Example:: 

40 

41 glyphMap = font.getReverseGlyphMap() 

42 glyphs = [ "A", "B", "C" ] 

43 coverage = buildCoverage(glyphs, glyphMap) 

44 

45 Args: 

46 glyphs: a sequence of glyph names. 

47 glyphMap: a glyph name to ID map, typically returned from 

48 ``font.getReverseGlyphMap()``. 

49 

50 Returns: 

51 An ``otTables.Coverage`` object or ``None`` if there are no glyphs 

52 supplied. 

53 """ 

54 

55 if not glyphs: 

56 return None 

57 self = ot.Coverage() 

58 try: 

59 self.glyphs = sorted(set(glyphs), key=glyphMap.__getitem__) 

60 except KeyError as e: 

61 raise ValueError(f"Could not find glyph {e} in font") from e 

62 

63 return self 

64 

65 

66LOOKUP_FLAG_RIGHT_TO_LEFT = 0x0001 

67LOOKUP_FLAG_IGNORE_BASE_GLYPHS = 0x0002 

68LOOKUP_FLAG_IGNORE_LIGATURES = 0x0004 

69LOOKUP_FLAG_IGNORE_MARKS = 0x0008 

70LOOKUP_FLAG_USE_MARK_FILTERING_SET = 0x0010 

71 

72 

73def buildLookup(subtables, flags=0, markFilterSet=None): 

74 """Turns a collection of rules into a lookup. 

75 

76 A Lookup (as defined in the `OpenType Spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#lookupTbl>`__) 

77 wraps the individual rules in a layout operation (substitution or 

78 positioning) in a data structure expressing their overall lookup type - 

79 for example, single substitution, mark-to-base attachment, and so on - 

80 as well as the lookup flags and any mark filtering sets. You may import 

81 the following constants to express lookup flags: 

82 

83 - ``LOOKUP_FLAG_RIGHT_TO_LEFT`` 

84 - ``LOOKUP_FLAG_IGNORE_BASE_GLYPHS`` 

85 - ``LOOKUP_FLAG_IGNORE_LIGATURES`` 

86 - ``LOOKUP_FLAG_IGNORE_MARKS`` 

87 - ``LOOKUP_FLAG_USE_MARK_FILTERING_SET`` 

88 

89 Args: 

90 subtables: A list of layout subtable objects (e.g. 

91 ``MultipleSubst``, ``PairPos``, etc.) or ``None``. 

92 flags (int): This lookup's flags. 

93 markFilterSet: Either ``None`` if no mark filtering set is used, or 

94 an integer representing the filtering set to be used for this 

95 lookup. If a mark filtering set is provided, 

96 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

97 flags. 

98 

99 Returns: 

100 An ``otTables.Lookup`` object or ``None`` if there are no subtables 

101 supplied. 

102 """ 

103 if subtables is None: 

104 return None 

105 subtables = [st for st in subtables if st is not None] 

106 if not subtables: 

107 return None 

108 assert all( 

109 t.LookupType == subtables[0].LookupType for t in subtables 

110 ), "all subtables must have the same LookupType; got %s" % repr( 

111 [t.LookupType for t in subtables] 

112 ) 

113 self = ot.Lookup() 

114 self.LookupType = subtables[0].LookupType 

115 self.LookupFlag = flags 

116 self.SubTable = subtables 

117 self.SubTableCount = len(self.SubTable) 

118 if markFilterSet is not None: 

119 self.LookupFlag |= LOOKUP_FLAG_USE_MARK_FILTERING_SET 

120 assert isinstance(markFilterSet, int), markFilterSet 

121 self.MarkFilteringSet = markFilterSet 

122 else: 

123 assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, ( 

124 "if markFilterSet is None, flags must not set " 

125 "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags 

126 ) 

127 return self 

128 

129 

130class LookupBuilder(object): 

131 SUBTABLE_BREAK_ = "SUBTABLE_BREAK" 

132 

133 def __init__(self, font, location, table, lookup_type): 

134 self.font = font 

135 self.glyphMap = font.getReverseGlyphMap() 

136 self.location = location 

137 self.table, self.lookup_type = table, lookup_type 

138 self.lookupflag = 0 

139 self.markFilterSet = None 

140 self.lookup_index = None # assigned when making final tables 

141 assert table in ("GPOS", "GSUB") 

142 

143 def equals(self, other): 

144 return ( 

145 isinstance(other, self.__class__) 

146 and self.table == other.table 

147 and self.lookupflag == other.lookupflag 

148 and self.markFilterSet == other.markFilterSet 

149 ) 

150 

151 def inferGlyphClasses(self): 

152 """Infers glyph glasses for the GDEF table, such as {"cedilla":3}.""" 

153 return {} 

154 

155 def getAlternateGlyphs(self): 

156 """Helper for building 'aalt' features.""" 

157 return {} 

158 

159 def buildLookup_(self, subtables): 

160 return buildLookup(subtables, self.lookupflag, self.markFilterSet) 

161 

162 def buildMarkClasses_(self, marks): 

163 """{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1} 

164 

165 Helper for MarkBasePostBuilder, MarkLigPosBuilder, and 

166 MarkMarkPosBuilder. Seems to return the same numeric IDs 

167 for mark classes as the AFDKO makeotf tool. 

168 """ 

169 ids = {} 

170 for mark in sorted(marks.keys(), key=self.font.getGlyphID): 

171 markClassName, _markAnchor = marks[mark] 

172 if markClassName not in ids: 

173 ids[markClassName] = len(ids) 

174 return ids 

175 

176 def setBacktrackCoverage_(self, prefix, subtable): 

177 subtable.BacktrackGlyphCount = len(prefix) 

178 subtable.BacktrackCoverage = [] 

179 for p in reversed(prefix): 

180 coverage = buildCoverage(p, self.glyphMap) 

181 subtable.BacktrackCoverage.append(coverage) 

182 

183 def setLookAheadCoverage_(self, suffix, subtable): 

184 subtable.LookAheadGlyphCount = len(suffix) 

185 subtable.LookAheadCoverage = [] 

186 for s in suffix: 

187 coverage = buildCoverage(s, self.glyphMap) 

188 subtable.LookAheadCoverage.append(coverage) 

189 

190 def setInputCoverage_(self, glyphs, subtable): 

191 subtable.InputGlyphCount = len(glyphs) 

192 subtable.InputCoverage = [] 

193 for g in glyphs: 

194 coverage = buildCoverage(g, self.glyphMap) 

195 subtable.InputCoverage.append(coverage) 

196 

197 def setCoverage_(self, glyphs, subtable): 

198 subtable.GlyphCount = len(glyphs) 

199 subtable.Coverage = [] 

200 for g in glyphs: 

201 coverage = buildCoverage(g, self.glyphMap) 

202 subtable.Coverage.append(coverage) 

203 

204 def build_subst_subtables(self, mapping, klass): 

205 substitutions = [{}] 

206 for key in mapping: 

207 if key[0] == self.SUBTABLE_BREAK_: 

208 substitutions.append({}) 

209 else: 

210 substitutions[-1][key] = mapping[key] 

211 subtables = [klass(s) for s in substitutions] 

212 return subtables 

213 

214 def add_subtable_break(self, location): 

215 """Add an explicit subtable break. 

216 

217 Args: 

218 location: A string or tuple representing the location in the 

219 original source which produced this break, or ``None`` if 

220 no location is provided. 

221 """ 

222 log.warning( 

223 OpenTypeLibError( 

224 'unsupported "subtable" statement for lookup type', location 

225 ) 

226 ) 

227 

228 

229class AlternateSubstBuilder(LookupBuilder): 

230 """Builds an Alternate Substitution (GSUB3) lookup. 

231 

232 Users are expected to manually add alternate glyph substitutions to 

233 the ``alternates`` attribute after the object has been initialized, 

234 e.g.:: 

235 

236 builder.alternates["A"] = ["A.alt1", "A.alt2"] 

237 

238 Attributes: 

239 font (``fontTools.TTLib.TTFont``): A font object. 

240 location: A string or tuple representing the location in the original 

241 source which produced this lookup. 

242 alternates: An ordered dictionary of alternates, mapping glyph names 

243 to a list of names of alternates. 

244 lookupflag (int): The lookup's flag 

245 markFilterSet: Either ``None`` if no mark filtering set is used, or 

246 an integer representing the filtering set to be used for this 

247 lookup. If a mark filtering set is provided, 

248 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

249 flags. 

250 """ 

251 

252 def __init__(self, font, location): 

253 LookupBuilder.__init__(self, font, location, "GSUB", 3) 

254 self.alternates = OrderedDict() 

255 

256 def equals(self, other): 

257 return LookupBuilder.equals(self, other) and self.alternates == other.alternates 

258 

259 def build(self): 

260 """Build the lookup. 

261 

262 Returns: 

263 An ``otTables.Lookup`` object representing the alternate 

264 substitution lookup. 

265 """ 

266 subtables = self.build_subst_subtables( 

267 self.alternates, buildAlternateSubstSubtable 

268 ) 

269 return self.buildLookup_(subtables) 

270 

271 def getAlternateGlyphs(self): 

272 return self.alternates 

273 

274 def add_subtable_break(self, location): 

275 self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ 

276 

277 

278class ChainContextualRule( 

279 namedtuple("ChainContextualRule", ["prefix", "glyphs", "suffix", "lookups"]) 

280): 

281 @property 

282 def is_subtable_break(self): 

283 return self.prefix == LookupBuilder.SUBTABLE_BREAK_ 

284 

285 

286class ChainContextualRuleset: 

287 def __init__(self): 

288 self.rules = [] 

289 

290 def addRule(self, rule): 

291 self.rules.append(rule) 

292 

293 @property 

294 def hasPrefixOrSuffix(self): 

295 # Do we have any prefixes/suffixes? If this is False for all 

296 # rulesets, we can express the whole lookup as GPOS5/GSUB7. 

297 for rule in self.rules: 

298 if len(rule.prefix) > 0 or len(rule.suffix) > 0: 

299 return True 

300 return False 

301 

302 @property 

303 def hasAnyGlyphClasses(self): 

304 # Do we use glyph classes anywhere in the rules? If this is False 

305 # we can express this subtable as a Format 1. 

306 for rule in self.rules: 

307 for coverage in (rule.prefix, rule.glyphs, rule.suffix): 

308 if any(len(x) > 1 for x in coverage): 

309 return True 

310 return False 

311 

312 def format2ClassDefs(self): 

313 PREFIX, GLYPHS, SUFFIX = 0, 1, 2 

314 classDefBuilders = [] 

315 for ix in [PREFIX, GLYPHS, SUFFIX]: 

316 context = [] 

317 for r in self.rules: 

318 context.append(r[ix]) 

319 classes = self._classBuilderForContext(context) 

320 if not classes: 

321 return None 

322 classDefBuilders.append(classes) 

323 return classDefBuilders 

324 

325 def _classBuilderForContext(self, context): 

326 classdefbuilder = ClassDefBuilder(useClass0=False) 

327 for position in context: 

328 for glyphset in position: 

329 glyphs = set(glyphset) 

330 if not classdefbuilder.canAdd(glyphs): 

331 return None 

332 classdefbuilder.add(glyphs) 

333 return classdefbuilder 

334 

335 

336class ChainContextualBuilder(LookupBuilder): 

337 def equals(self, other): 

338 return LookupBuilder.equals(self, other) and self.rules == other.rules 

339 

340 def rulesets(self): 

341 # Return a list of ChainContextRuleset objects, taking explicit 

342 # subtable breaks into account 

343 ruleset = [ChainContextualRuleset()] 

344 for rule in self.rules: 

345 if rule.is_subtable_break: 

346 ruleset.append(ChainContextualRuleset()) 

347 continue 

348 ruleset[-1].addRule(rule) 

349 # Squish any empty subtables 

350 return [x for x in ruleset if len(x.rules) > 0] 

351 

352 def getCompiledSize_(self, subtables): 

353 size = 0 

354 for st in subtables: 

355 w = OTTableWriter() 

356 w["LookupType"] = CountReference( 

357 {"LookupType": st.LookupType}, "LookupType" 

358 ) 

359 # We need to make a copy here because compiling 

360 # modifies the subtable (finalizing formats etc.) 

361 copy.deepcopy(st).compile(w, self.font) 

362 size += len(w.getAllData()) 

363 return size 

364 

365 def build(self): 

366 """Build the lookup. 

367 

368 Returns: 

369 An ``otTables.Lookup`` object representing the chained 

370 contextual positioning lookup. 

371 """ 

372 subtables = [] 

373 

374 rulesets = self.rulesets() 

375 chaining = any(ruleset.hasPrefixOrSuffix for ruleset in rulesets) 

376 

377 # https://github.com/fonttools/fonttools/issues/2539 

378 # 

379 # Unfortunately, as of 2022-03-07, Apple's CoreText renderer does not 

380 # correctly process GPOS7 lookups, so for now we force contextual 

381 # positioning lookups to be chaining (GPOS8). 

382 # 

383 # This seems to be fixed as of macOS 13.2, but we keep disabling this 

384 # for now until we are no longer concerned about old macOS versions. 

385 # But we allow people to opt-out of this with the config key below. 

386 write_gpos7 = self.font.cfg.get("fontTools.otlLib.builder:WRITE_GPOS7") 

387 # horrible separation of concerns breach 

388 if not write_gpos7 and self.subtable_type == "Pos": 

389 chaining = True 

390 

391 for ruleset in rulesets: 

392 # Determine format strategy. We try to build formats 1, 2 and 3 

393 # subtables and then work out which is best. candidates list holds 

394 # the subtables in each format for this ruleset (including a dummy 

395 # "format 0" to make the addressing match the format numbers). 

396 

397 # We can always build a format 3 lookup by accumulating each of 

398 # the rules into a list, so start with that. 

399 candidates = [None, None, None, []] 

400 for rule in ruleset.rules: 

401 candidates[3].append(self.buildFormat3Subtable(rule, chaining)) 

402 

403 # Can we express the whole ruleset as a format 2 subtable? 

404 classdefs = ruleset.format2ClassDefs() 

405 if classdefs: 

406 candidates[2] = [ 

407 self.buildFormat2Subtable(ruleset, classdefs, chaining) 

408 ] 

409 

410 if not ruleset.hasAnyGlyphClasses: 

411 candidates[1] = [self.buildFormat1Subtable(ruleset, chaining)] 

412 

413 for i in [1, 2, 3]: 

414 if candidates[i]: 

415 try: 

416 self.getCompiledSize_(candidates[i]) 

417 except Exception as e: 

418 log.warning( 

419 "Contextual format %i at %s overflowed (%s)" 

420 % (i, str(self.location), e) 

421 ) 

422 candidates[i] = None 

423 

424 candidates = [x for x in candidates if x is not None] 

425 if not candidates: 

426 raise OpenTypeLibError("All candidates overflowed", self.location) 

427 

428 winner = min(candidates, key=self.getCompiledSize_) 

429 subtables.extend(winner) 

430 

431 # If we are not chaining, lookup type will be automatically fixed by 

432 # buildLookup_ 

433 return self.buildLookup_(subtables) 

434 

435 def buildFormat1Subtable(self, ruleset, chaining=True): 

436 st = self.newSubtable_(chaining=chaining) 

437 st.Format = 1 

438 st.populateDefaults() 

439 coverage = set() 

440 rulesetsByFirstGlyph = {} 

441 ruleAttr = self.ruleAttr_(format=1, chaining=chaining) 

442 

443 for rule in ruleset.rules: 

444 ruleAsSubtable = self.newRule_(format=1, chaining=chaining) 

445 

446 if chaining: 

447 ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix) 

448 ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix) 

449 ruleAsSubtable.Backtrack = [list(x)[0] for x in reversed(rule.prefix)] 

450 ruleAsSubtable.LookAhead = [list(x)[0] for x in rule.suffix] 

451 

452 ruleAsSubtable.InputGlyphCount = len(rule.glyphs) 

453 else: 

454 ruleAsSubtable.GlyphCount = len(rule.glyphs) 

455 

456 ruleAsSubtable.Input = [list(x)[0] for x in rule.glyphs[1:]] 

457 

458 self.buildLookupList(rule, ruleAsSubtable) 

459 

460 firstGlyph = list(rule.glyphs[0])[0] 

461 if firstGlyph not in rulesetsByFirstGlyph: 

462 coverage.add(firstGlyph) 

463 rulesetsByFirstGlyph[firstGlyph] = [] 

464 rulesetsByFirstGlyph[firstGlyph].append(ruleAsSubtable) 

465 

466 st.Coverage = buildCoverage(coverage, self.glyphMap) 

467 ruleSets = [] 

468 for g in st.Coverage.glyphs: 

469 ruleSet = self.newRuleSet_(format=1, chaining=chaining) 

470 setattr(ruleSet, ruleAttr, rulesetsByFirstGlyph[g]) 

471 setattr(ruleSet, f"{ruleAttr}Count", len(rulesetsByFirstGlyph[g])) 

472 ruleSets.append(ruleSet) 

473 

474 setattr(st, self.ruleSetAttr_(format=1, chaining=chaining), ruleSets) 

475 setattr( 

476 st, self.ruleSetAttr_(format=1, chaining=chaining) + "Count", len(ruleSets) 

477 ) 

478 

479 return st 

480 

481 def buildFormat2Subtable(self, ruleset, classdefs, chaining=True): 

482 st = self.newSubtable_(chaining=chaining) 

483 st.Format = 2 

484 st.populateDefaults() 

485 

486 if chaining: 

487 ( 

488 st.BacktrackClassDef, 

489 st.InputClassDef, 

490 st.LookAheadClassDef, 

491 ) = [c.build() for c in classdefs] 

492 else: 

493 st.ClassDef = classdefs[1].build() 

494 

495 inClasses = classdefs[1].classes() 

496 

497 classSets = [] 

498 for _ in inClasses: 

499 classSet = self.newRuleSet_(format=2, chaining=chaining) 

500 classSets.append(classSet) 

501 

502 coverage = set() 

503 classRuleAttr = self.ruleAttr_(format=2, chaining=chaining) 

504 

505 for rule in ruleset.rules: 

506 ruleAsSubtable = self.newRule_(format=2, chaining=chaining) 

507 if chaining: 

508 ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix) 

509 ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix) 

510 # The glyphs in the rule may be list, tuple, odict_keys... 

511 # Order is not important anyway because they are guaranteed 

512 # to be members of the same class. 

513 ruleAsSubtable.Backtrack = [ 

514 st.BacktrackClassDef.classDefs[list(x)[0]] 

515 for x in reversed(rule.prefix) 

516 ] 

517 ruleAsSubtable.LookAhead = [ 

518 st.LookAheadClassDef.classDefs[list(x)[0]] for x in rule.suffix 

519 ] 

520 

521 ruleAsSubtable.InputGlyphCount = len(rule.glyphs) 

522 ruleAsSubtable.Input = [ 

523 st.InputClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:] 

524 ] 

525 setForThisRule = classSets[ 

526 st.InputClassDef.classDefs[list(rule.glyphs[0])[0]] 

527 ] 

528 else: 

529 ruleAsSubtable.GlyphCount = len(rule.glyphs) 

530 ruleAsSubtable.Class = [ # The spec calls this InputSequence 

531 st.ClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:] 

532 ] 

533 setForThisRule = classSets[ 

534 st.ClassDef.classDefs[list(rule.glyphs[0])[0]] 

535 ] 

536 

537 self.buildLookupList(rule, ruleAsSubtable) 

538 coverage |= set(rule.glyphs[0]) 

539 

540 getattr(setForThisRule, classRuleAttr).append(ruleAsSubtable) 

541 setattr( 

542 setForThisRule, 

543 f"{classRuleAttr}Count", 

544 getattr(setForThisRule, f"{classRuleAttr}Count") + 1, 

545 ) 

546 setattr(st, self.ruleSetAttr_(format=2, chaining=chaining), classSets) 

547 setattr( 

548 st, self.ruleSetAttr_(format=2, chaining=chaining) + "Count", len(classSets) 

549 ) 

550 st.Coverage = buildCoverage(coverage, self.glyphMap) 

551 return st 

552 

553 def buildFormat3Subtable(self, rule, chaining=True): 

554 st = self.newSubtable_(chaining=chaining) 

555 st.Format = 3 

556 if chaining: 

557 self.setBacktrackCoverage_(rule.prefix, st) 

558 self.setLookAheadCoverage_(rule.suffix, st) 

559 self.setInputCoverage_(rule.glyphs, st) 

560 else: 

561 self.setCoverage_(rule.glyphs, st) 

562 self.buildLookupList(rule, st) 

563 return st 

564 

565 def buildLookupList(self, rule, st): 

566 for sequenceIndex, lookupList in enumerate(rule.lookups): 

567 if lookupList is not None: 

568 if not isinstance(lookupList, list): 

569 # Can happen with synthesised lookups 

570 lookupList = [lookupList] 

571 for l in lookupList: 

572 if l.lookup_index is None: 

573 if isinstance(self, ChainContextPosBuilder): 

574 other = "substitution" 

575 else: 

576 other = "positioning" 

577 raise OpenTypeLibError( 

578 "Missing index of the specified " 

579 f"lookup, might be a {other} lookup", 

580 self.location, 

581 ) 

582 rec = self.newLookupRecord_(st) 

583 rec.SequenceIndex = sequenceIndex 

584 rec.LookupListIndex = l.lookup_index 

585 

586 def add_subtable_break(self, location): 

587 self.rules.append( 

588 ChainContextualRule( 

589 self.SUBTABLE_BREAK_, 

590 self.SUBTABLE_BREAK_, 

591 self.SUBTABLE_BREAK_, 

592 [self.SUBTABLE_BREAK_], 

593 ) 

594 ) 

595 

596 def newSubtable_(self, chaining=True): 

597 subtablename = f"Context{self.subtable_type}" 

598 if chaining: 

599 subtablename = "Chain" + subtablename 

600 st = getattr(ot, subtablename)() # ot.ChainContextPos()/ot.ChainSubst()/etc. 

601 setattr(st, f"{self.subtable_type}Count", 0) 

602 setattr(st, f"{self.subtable_type}LookupRecord", []) 

603 return st 

604 

605 # Format 1 and format 2 GSUB5/GSUB6/GPOS7/GPOS8 rulesets and rules form a family: 

606 # 

607 # format 1 ruleset format 1 rule format 2 ruleset format 2 rule 

608 # GSUB5 SubRuleSet SubRule SubClassSet SubClassRule 

609 # GSUB6 ChainSubRuleSet ChainSubRule ChainSubClassSet ChainSubClassRule 

610 # GPOS7 PosRuleSet PosRule PosClassSet PosClassRule 

611 # GPOS8 ChainPosRuleSet ChainPosRule ChainPosClassSet ChainPosClassRule 

612 # 

613 # The following functions generate the attribute names and subtables according 

614 # to this naming convention. 

615 def ruleSetAttr_(self, format=1, chaining=True): 

616 if format == 1: 

617 formatType = "Rule" 

618 elif format == 2: 

619 formatType = "Class" 

620 else: 

621 raise AssertionError(formatType) 

622 subtablename = f"{self.subtable_type[0:3]}{formatType}Set" # Sub, not Subst. 

623 if chaining: 

624 subtablename = "Chain" + subtablename 

625 return subtablename 

626 

627 def ruleAttr_(self, format=1, chaining=True): 

628 if format == 1: 

629 formatType = "" 

630 elif format == 2: 

631 formatType = "Class" 

632 else: 

633 raise AssertionError(formatType) 

634 subtablename = f"{self.subtable_type[0:3]}{formatType}Rule" # Sub, not Subst. 

635 if chaining: 

636 subtablename = "Chain" + subtablename 

637 return subtablename 

638 

639 def newRuleSet_(self, format=1, chaining=True): 

640 st = getattr( 

641 ot, self.ruleSetAttr_(format, chaining) 

642 )() # ot.ChainPosRuleSet()/ot.SubRuleSet()/etc. 

643 st.populateDefaults() 

644 return st 

645 

646 def newRule_(self, format=1, chaining=True): 

647 st = getattr( 

648 ot, self.ruleAttr_(format, chaining) 

649 )() # ot.ChainPosClassRule()/ot.SubClassRule()/etc. 

650 st.populateDefaults() 

651 return st 

652 

653 def attachSubtableWithCount_( 

654 self, st, subtable_name, count_name, existing=None, index=None, chaining=False 

655 ): 

656 if chaining: 

657 subtable_name = "Chain" + subtable_name 

658 count_name = "Chain" + count_name 

659 

660 if not hasattr(st, count_name): 

661 setattr(st, count_name, 0) 

662 setattr(st, subtable_name, []) 

663 

664 if existing: 

665 new_subtable = existing 

666 else: 

667 # Create a new, empty subtable from otTables 

668 new_subtable = getattr(ot, subtable_name)() 

669 

670 setattr(st, count_name, getattr(st, count_name) + 1) 

671 

672 if index: 

673 getattr(st, subtable_name).insert(index, new_subtable) 

674 else: 

675 getattr(st, subtable_name).append(new_subtable) 

676 

677 return new_subtable 

678 

679 def newLookupRecord_(self, st): 

680 return self.attachSubtableWithCount_( 

681 st, 

682 f"{self.subtable_type}LookupRecord", 

683 f"{self.subtable_type}Count", 

684 chaining=False, 

685 ) # Oddly, it isn't ChainSubstLookupRecord 

686 

687 

688class ChainContextPosBuilder(ChainContextualBuilder): 

689 """Builds a Chained Contextual Positioning (GPOS8) lookup. 

690 

691 Users are expected to manually add rules to the ``rules`` attribute after 

692 the object has been initialized, e.g.:: 

693 

694 # pos [A B] [C D] x' lookup lu1 y' z' lookup lu2 E; 

695 

696 prefix = [ ["A", "B"], ["C", "D"] ] 

697 suffix = [ ["E"] ] 

698 glyphs = [ ["x"], ["y"], ["z"] ] 

699 lookups = [ [lu1], None, [lu2] ] 

700 builder.rules.append( (prefix, glyphs, suffix, lookups) ) 

701 

702 Attributes: 

703 font (``fontTools.TTLib.TTFont``): A font object. 

704 location: A string or tuple representing the location in the original 

705 source which produced this lookup. 

706 rules: A list of tuples representing the rules in this lookup. 

707 lookupflag (int): The lookup's flag 

708 markFilterSet: Either ``None`` if no mark filtering set is used, or 

709 an integer representing the filtering set to be used for this 

710 lookup. If a mark filtering set is provided, 

711 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

712 flags. 

713 """ 

714 

715 def __init__(self, font, location): 

716 LookupBuilder.__init__(self, font, location, "GPOS", 8) 

717 self.rules = [] 

718 self.subtable_type = "Pos" 

719 

720 def find_chainable_single_pos(self, lookups, glyphs, value): 

721 """Helper for add_single_pos_chained_()""" 

722 res = None 

723 for lookup in lookups[::-1]: 

724 if lookup == self.SUBTABLE_BREAK_: 

725 return res 

726 if isinstance(lookup, SinglePosBuilder) and all( 

727 lookup.can_add(glyph, value) for glyph in glyphs 

728 ): 

729 res = lookup 

730 return res 

731 

732 

733class ChainContextSubstBuilder(ChainContextualBuilder): 

734 """Builds a Chained Contextual Substitution (GSUB6) lookup. 

735 

736 Users are expected to manually add rules to the ``rules`` attribute after 

737 the object has been initialized, e.g.:: 

738 

739 # sub [A B] [C D] x' lookup lu1 y' z' lookup lu2 E; 

740 

741 prefix = [ ["A", "B"], ["C", "D"] ] 

742 suffix = [ ["E"] ] 

743 glyphs = [ ["x"], ["y"], ["z"] ] 

744 lookups = [ [lu1], None, [lu2] ] 

745 builder.rules.append( (prefix, glyphs, suffix, lookups) ) 

746 

747 Attributes: 

748 font (``fontTools.TTLib.TTFont``): A font object. 

749 location: A string or tuple representing the location in the original 

750 source which produced this lookup. 

751 rules: A list of tuples representing the rules in this lookup. 

752 lookupflag (int): The lookup's flag 

753 markFilterSet: Either ``None`` if no mark filtering set is used, or 

754 an integer representing the filtering set to be used for this 

755 lookup. If a mark filtering set is provided, 

756 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

757 flags. 

758 """ 

759 

760 def __init__(self, font, location): 

761 LookupBuilder.__init__(self, font, location, "GSUB", 6) 

762 self.rules = [] # (prefix, input, suffix, lookups) 

763 self.subtable_type = "Subst" 

764 

765 def getAlternateGlyphs(self): 

766 result = {} 

767 for rule in self.rules: 

768 if rule.is_subtable_break: 

769 continue 

770 for lookups in rule.lookups: 

771 if not isinstance(lookups, list): 

772 lookups = [lookups] 

773 for lookup in lookups: 

774 if lookup is not None: 

775 alts = lookup.getAlternateGlyphs() 

776 for glyph, replacements in alts.items(): 

777 result.setdefault(glyph, set()).update(replacements) 

778 return result 

779 

780 def find_chainable_single_subst(self, mapping): 

781 """Helper for add_single_subst_chained_()""" 

782 res = None 

783 for rule in self.rules[::-1]: 

784 if rule.is_subtable_break: 

785 return res 

786 for sub in rule.lookups: 

787 if isinstance(sub, SingleSubstBuilder) and not any( 

788 g in mapping and mapping[g] != sub.mapping[g] for g in sub.mapping 

789 ): 

790 res = sub 

791 return res 

792 

793 

794class LigatureSubstBuilder(LookupBuilder): 

795 """Builds a Ligature Substitution (GSUB4) lookup. 

796 

797 Users are expected to manually add ligatures to the ``ligatures`` 

798 attribute after the object has been initialized, e.g.:: 

799 

800 # sub f i by f_i; 

801 builder.ligatures[("f","f","i")] = "f_f_i" 

802 

803 Attributes: 

804 font (``fontTools.TTLib.TTFont``): A font object. 

805 location: A string or tuple representing the location in the original 

806 source which produced this lookup. 

807 ligatures: An ordered dictionary mapping a tuple of glyph names to the 

808 ligature glyphname. 

809 lookupflag (int): The lookup's flag 

810 markFilterSet: Either ``None`` if no mark filtering set is used, or 

811 an integer representing the filtering set to be used for this 

812 lookup. If a mark filtering set is provided, 

813 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

814 flags. 

815 """ 

816 

817 def __init__(self, font, location): 

818 LookupBuilder.__init__(self, font, location, "GSUB", 4) 

819 self.ligatures = OrderedDict() # {('f','f','i'): 'f_f_i'} 

820 

821 def equals(self, other): 

822 return LookupBuilder.equals(self, other) and self.ligatures == other.ligatures 

823 

824 def build(self): 

825 """Build the lookup. 

826 

827 Returns: 

828 An ``otTables.Lookup`` object representing the ligature 

829 substitution lookup. 

830 """ 

831 subtables = self.build_subst_subtables( 

832 self.ligatures, buildLigatureSubstSubtable 

833 ) 

834 return self.buildLookup_(subtables) 

835 

836 def add_subtable_break(self, location): 

837 self.ligatures[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ 

838 

839 

840class MultipleSubstBuilder(LookupBuilder): 

841 """Builds a Multiple Substitution (GSUB2) lookup. 

842 

843 Users are expected to manually add substitutions to the ``mapping`` 

844 attribute after the object has been initialized, e.g.:: 

845 

846 # sub uni06C0 by uni06D5.fina hamza.above; 

847 builder.mapping["uni06C0"] = [ "uni06D5.fina", "hamza.above"] 

848 

849 Attributes: 

850 font (``fontTools.TTLib.TTFont``): A font object. 

851 location: A string or tuple representing the location in the original 

852 source which produced this lookup. 

853 mapping: An ordered dictionary mapping a glyph name to a list of 

854 substituted glyph names. 

855 lookupflag (int): The lookup's flag 

856 markFilterSet: Either ``None`` if no mark filtering set is used, or 

857 an integer representing the filtering set to be used for this 

858 lookup. If a mark filtering set is provided, 

859 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

860 flags. 

861 """ 

862 

863 def __init__(self, font, location): 

864 LookupBuilder.__init__(self, font, location, "GSUB", 2) 

865 self.mapping = OrderedDict() 

866 

867 def equals(self, other): 

868 return LookupBuilder.equals(self, other) and self.mapping == other.mapping 

869 

870 def build(self): 

871 subtables = self.build_subst_subtables(self.mapping, buildMultipleSubstSubtable) 

872 return self.buildLookup_(subtables) 

873 

874 def add_subtable_break(self, location): 

875 self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ 

876 

877 

878class CursivePosBuilder(LookupBuilder): 

879 """Builds a Cursive Positioning (GPOS3) lookup. 

880 

881 Attributes: 

882 font (``fontTools.TTLib.TTFont``): A font object. 

883 location: A string or tuple representing the location in the original 

884 source which produced this lookup. 

885 attachments: An ordered dictionary mapping a glyph name to a two-element 

886 tuple of ``otTables.Anchor`` objects. 

887 lookupflag (int): The lookup's flag 

888 markFilterSet: Either ``None`` if no mark filtering set is used, or 

889 an integer representing the filtering set to be used for this 

890 lookup. If a mark filtering set is provided, 

891 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

892 flags. 

893 """ 

894 

895 def __init__(self, font, location): 

896 LookupBuilder.__init__(self, font, location, "GPOS", 3) 

897 self.attachments = {} 

898 

899 def equals(self, other): 

900 return ( 

901 LookupBuilder.equals(self, other) and self.attachments == other.attachments 

902 ) 

903 

904 def add_attachment(self, location, glyphs, entryAnchor, exitAnchor): 

905 """Adds attachment information to the cursive positioning lookup. 

906 

907 Args: 

908 location: A string or tuple representing the location in the 

909 original source which produced this lookup. (Unused.) 

910 glyphs: A list of glyph names sharing these entry and exit 

911 anchor locations. 

912 entryAnchor: A ``otTables.Anchor`` object representing the 

913 entry anchor, or ``None`` if no entry anchor is present. 

914 exitAnchor: A ``otTables.Anchor`` object representing the 

915 exit anchor, or ``None`` if no exit anchor is present. 

916 """ 

917 for glyph in glyphs: 

918 self.attachments[glyph] = (entryAnchor, exitAnchor) 

919 

920 def build(self): 

921 """Build the lookup. 

922 

923 Returns: 

924 An ``otTables.Lookup`` object representing the cursive 

925 positioning lookup. 

926 """ 

927 st = buildCursivePosSubtable(self.attachments, self.glyphMap) 

928 return self.buildLookup_([st]) 

929 

930 

931class MarkBasePosBuilder(LookupBuilder): 

932 """Builds a Mark-To-Base Positioning (GPOS4) lookup. 

933 

934 Users are expected to manually add marks and bases to the ``marks`` 

935 and ``bases`` attributes after the object has been initialized, e.g.:: 

936 

937 builder.marks["acute"] = (0, a1) 

938 builder.marks["grave"] = (0, a1) 

939 builder.marks["cedilla"] = (1, a2) 

940 builder.bases["a"] = {0: a3, 1: a5} 

941 builder.bases["b"] = {0: a4, 1: a5} 

942 

943 Attributes: 

944 font (``fontTools.TTLib.TTFont``): A font object. 

945 location: A string or tuple representing the location in the original 

946 source which produced this lookup. 

947 marks: An dictionary mapping a glyph name to a two-element 

948 tuple containing a mark class ID and ``otTables.Anchor`` object. 

949 bases: An dictionary mapping a glyph name to a dictionary of 

950 mark class IDs and ``otTables.Anchor`` object. 

951 lookupflag (int): The lookup's flag 

952 markFilterSet: Either ``None`` if no mark filtering set is used, or 

953 an integer representing the filtering set to be used for this 

954 lookup. If a mark filtering set is provided, 

955 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

956 flags. 

957 """ 

958 

959 def __init__(self, font, location): 

960 LookupBuilder.__init__(self, font, location, "GPOS", 4) 

961 self.marks = {} # glyphName -> (markClassName, anchor) 

962 self.bases = {} # glyphName -> {markClassName: anchor} 

963 

964 def equals(self, other): 

965 return ( 

966 LookupBuilder.equals(self, other) 

967 and self.marks == other.marks 

968 and self.bases == other.bases 

969 ) 

970 

971 def inferGlyphClasses(self): 

972 result = {glyph: 1 for glyph in self.bases} 

973 result.update({glyph: 3 for glyph in self.marks}) 

974 return result 

975 

976 def build(self): 

977 """Build the lookup. 

978 

979 Returns: 

980 An ``otTables.Lookup`` object representing the mark-to-base 

981 positioning lookup. 

982 """ 

983 markClasses = self.buildMarkClasses_(self.marks) 

984 marks = {} 

985 for mark, (mc, anchor) in self.marks.items(): 

986 if mc not in markClasses: 

987 raise ValueError( 

988 "Mark class %s not found for mark glyph %s" % (mc, mark) 

989 ) 

990 marks[mark] = (markClasses[mc], anchor) 

991 bases = {} 

992 for glyph, anchors in self.bases.items(): 

993 bases[glyph] = {} 

994 for mc, anchor in anchors.items(): 

995 if mc not in markClasses: 

996 raise ValueError( 

997 "Mark class %s not found for base glyph %s" % (mc, glyph) 

998 ) 

999 bases[glyph][markClasses[mc]] = anchor 

1000 subtables = buildMarkBasePos(marks, bases, self.glyphMap) 

1001 return self.buildLookup_(subtables) 

1002 

1003 

1004class MarkLigPosBuilder(LookupBuilder): 

1005 """Builds a Mark-To-Ligature Positioning (GPOS5) lookup. 

1006 

1007 Users are expected to manually add marks and bases to the ``marks`` 

1008 and ``ligatures`` attributes after the object has been initialized, e.g.:: 

1009 

1010 builder.marks["acute"] = (0, a1) 

1011 builder.marks["grave"] = (0, a1) 

1012 builder.marks["cedilla"] = (1, a2) 

1013 builder.ligatures["f_i"] = [ 

1014 { 0: a3, 1: a5 }, # f 

1015 { 0: a4, 1: a5 } # i 

1016 ] 

1017 

1018 Attributes: 

1019 font (``fontTools.TTLib.TTFont``): A font object. 

1020 location: A string or tuple representing the location in the original 

1021 source which produced this lookup. 

1022 marks: An dictionary mapping a glyph name to a two-element 

1023 tuple containing a mark class ID and ``otTables.Anchor`` object. 

1024 ligatures: An dictionary mapping a glyph name to an array with one 

1025 element for each ligature component. Each array element should be 

1026 a dictionary mapping mark class IDs to ``otTables.Anchor`` objects. 

1027 lookupflag (int): The lookup's flag 

1028 markFilterSet: Either ``None`` if no mark filtering set is used, or 

1029 an integer representing the filtering set to be used for this 

1030 lookup. If a mark filtering set is provided, 

1031 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

1032 flags. 

1033 """ 

1034 

1035 def __init__(self, font, location): 

1036 LookupBuilder.__init__(self, font, location, "GPOS", 5) 

1037 self.marks = {} # glyphName -> (markClassName, anchor) 

1038 self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...] 

1039 

1040 def equals(self, other): 

1041 return ( 

1042 LookupBuilder.equals(self, other) 

1043 and self.marks == other.marks 

1044 and self.ligatures == other.ligatures 

1045 ) 

1046 

1047 def inferGlyphClasses(self): 

1048 result = {glyph: 2 for glyph in self.ligatures} 

1049 result.update({glyph: 3 for glyph in self.marks}) 

1050 return result 

1051 

1052 def build(self): 

1053 """Build the lookup. 

1054 

1055 Returns: 

1056 An ``otTables.Lookup`` object representing the mark-to-ligature 

1057 positioning lookup. 

1058 """ 

1059 markClasses = self.buildMarkClasses_(self.marks) 

1060 marks = { 

1061 mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() 

1062 } 

1063 ligs = {} 

1064 for lig, components in self.ligatures.items(): 

1065 ligs[lig] = [] 

1066 for c in components: 

1067 ligs[lig].append({markClasses[mc]: a for mc, a in c.items()}) 

1068 subtables = buildMarkLigPos(marks, ligs, self.glyphMap) 

1069 return self.buildLookup_(subtables) 

1070 

1071 

1072class MarkMarkPosBuilder(LookupBuilder): 

1073 """Builds a Mark-To-Mark Positioning (GPOS6) lookup. 

1074 

1075 Users are expected to manually add marks and bases to the ``marks`` 

1076 and ``baseMarks`` attributes after the object has been initialized, e.g.:: 

1077 

1078 builder.marks["acute"] = (0, a1) 

1079 builder.marks["grave"] = (0, a1) 

1080 builder.marks["cedilla"] = (1, a2) 

1081 builder.baseMarks["acute"] = {0: a3} 

1082 

1083 Attributes: 

1084 font (``fontTools.TTLib.TTFont``): A font object. 

1085 location: A string or tuple representing the location in the original 

1086 source which produced this lookup. 

1087 marks: An dictionary mapping a glyph name to a two-element 

1088 tuple containing a mark class ID and ``otTables.Anchor`` object. 

1089 baseMarks: An dictionary mapping a glyph name to a dictionary 

1090 containing one item: a mark class ID and a ``otTables.Anchor`` object. 

1091 lookupflag (int): The lookup's flag 

1092 markFilterSet: Either ``None`` if no mark filtering set is used, or 

1093 an integer representing the filtering set to be used for this 

1094 lookup. If a mark filtering set is provided, 

1095 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

1096 flags. 

1097 """ 

1098 

1099 def __init__(self, font, location): 

1100 LookupBuilder.__init__(self, font, location, "GPOS", 6) 

1101 self.marks = {} # glyphName -> (markClassName, anchor) 

1102 self.baseMarks = {} # glyphName -> {markClassName: anchor} 

1103 

1104 def equals(self, other): 

1105 return ( 

1106 LookupBuilder.equals(self, other) 

1107 and self.marks == other.marks 

1108 and self.baseMarks == other.baseMarks 

1109 ) 

1110 

1111 def inferGlyphClasses(self): 

1112 result = {glyph: 3 for glyph in self.baseMarks} 

1113 result.update({glyph: 3 for glyph in self.marks}) 

1114 return result 

1115 

1116 def build(self): 

1117 """Build the lookup. 

1118 

1119 Returns: 

1120 An ``otTables.Lookup`` object representing the mark-to-mark 

1121 positioning lookup. 

1122 """ 

1123 markClasses = self.buildMarkClasses_(self.marks) 

1124 markClassList = sorted(markClasses.keys(), key=markClasses.get) 

1125 marks = { 

1126 mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() 

1127 } 

1128 

1129 st = ot.MarkMarkPos() 

1130 st.Format = 1 

1131 st.ClassCount = len(markClasses) 

1132 st.Mark1Coverage = buildCoverage(marks, self.glyphMap) 

1133 st.Mark2Coverage = buildCoverage(self.baseMarks, self.glyphMap) 

1134 st.Mark1Array = buildMarkArray(marks, self.glyphMap) 

1135 st.Mark2Array = ot.Mark2Array() 

1136 st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs) 

1137 st.Mark2Array.Mark2Record = [] 

1138 for base in st.Mark2Coverage.glyphs: 

1139 anchors = [self.baseMarks[base].get(mc) for mc in markClassList] 

1140 st.Mark2Array.Mark2Record.append(buildMark2Record(anchors)) 

1141 return self.buildLookup_([st]) 

1142 

1143 

1144class ReverseChainSingleSubstBuilder(LookupBuilder): 

1145 """Builds a Reverse Chaining Contextual Single Substitution (GSUB8) lookup. 

1146 

1147 Users are expected to manually add substitutions to the ``substitutions`` 

1148 attribute after the object has been initialized, e.g.:: 

1149 

1150 # reversesub [a e n] d' by d.alt; 

1151 prefix = [ ["a", "e", "n"] ] 

1152 suffix = [] 

1153 mapping = { "d": "d.alt" } 

1154 builder.substitutions.append( (prefix, suffix, mapping) ) 

1155 

1156 Attributes: 

1157 font (``fontTools.TTLib.TTFont``): A font object. 

1158 location: A string or tuple representing the location in the original 

1159 source which produced this lookup. 

1160 substitutions: A three-element tuple consisting of a prefix sequence, 

1161 a suffix sequence, and a dictionary of single substitutions. 

1162 lookupflag (int): The lookup's flag 

1163 markFilterSet: Either ``None`` if no mark filtering set is used, or 

1164 an integer representing the filtering set to be used for this 

1165 lookup. If a mark filtering set is provided, 

1166 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

1167 flags. 

1168 """ 

1169 

1170 def __init__(self, font, location): 

1171 LookupBuilder.__init__(self, font, location, "GSUB", 8) 

1172 self.rules = [] # (prefix, suffix, mapping) 

1173 

1174 def equals(self, other): 

1175 return LookupBuilder.equals(self, other) and self.rules == other.rules 

1176 

1177 def build(self): 

1178 """Build the lookup. 

1179 

1180 Returns: 

1181 An ``otTables.Lookup`` object representing the chained 

1182 contextual substitution lookup. 

1183 """ 

1184 subtables = [] 

1185 for prefix, suffix, mapping in self.rules: 

1186 st = ot.ReverseChainSingleSubst() 

1187 st.Format = 1 

1188 self.setBacktrackCoverage_(prefix, st) 

1189 self.setLookAheadCoverage_(suffix, st) 

1190 st.Coverage = buildCoverage(mapping.keys(), self.glyphMap) 

1191 st.GlyphCount = len(mapping) 

1192 st.Substitute = [mapping[g] for g in st.Coverage.glyphs] 

1193 subtables.append(st) 

1194 return self.buildLookup_(subtables) 

1195 

1196 def add_subtable_break(self, location): 

1197 # Nothing to do here, each substitution is in its own subtable. 

1198 pass 

1199 

1200 

1201class SingleSubstBuilder(LookupBuilder): 

1202 """Builds a Single Substitution (GSUB1) lookup. 

1203 

1204 Users are expected to manually add substitutions to the ``mapping`` 

1205 attribute after the object has been initialized, e.g.:: 

1206 

1207 # sub x by y; 

1208 builder.mapping["x"] = "y" 

1209 

1210 Attributes: 

1211 font (``fontTools.TTLib.TTFont``): A font object. 

1212 location: A string or tuple representing the location in the original 

1213 source which produced this lookup. 

1214 mapping: A dictionary mapping a single glyph name to another glyph name. 

1215 lookupflag (int): The lookup's flag 

1216 markFilterSet: Either ``None`` if no mark filtering set is used, or 

1217 an integer representing the filtering set to be used for this 

1218 lookup. If a mark filtering set is provided, 

1219 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

1220 flags. 

1221 """ 

1222 

1223 def __init__(self, font, location): 

1224 LookupBuilder.__init__(self, font, location, "GSUB", 1) 

1225 self.mapping = OrderedDict() 

1226 

1227 def equals(self, other): 

1228 return LookupBuilder.equals(self, other) and self.mapping == other.mapping 

1229 

1230 def build(self): 

1231 """Build the lookup. 

1232 

1233 Returns: 

1234 An ``otTables.Lookup`` object representing the multiple 

1235 substitution lookup. 

1236 """ 

1237 subtables = self.build_subst_subtables(self.mapping, buildSingleSubstSubtable) 

1238 return self.buildLookup_(subtables) 

1239 

1240 def getAlternateGlyphs(self): 

1241 return {glyph: set([repl]) for glyph, repl in self.mapping.items()} 

1242 

1243 def add_subtable_break(self, location): 

1244 self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ 

1245 

1246 

1247class ClassPairPosSubtableBuilder(object): 

1248 """Builds class-based Pair Positioning (GPOS2 format 2) subtables. 

1249 

1250 Note that this does *not* build a GPOS2 ``otTables.Lookup`` directly, 

1251 but builds a list of ``otTables.PairPos`` subtables. It is used by the 

1252 :class:`PairPosBuilder` below. 

1253 

1254 Attributes: 

1255 builder (PairPosBuilder): A pair positioning lookup builder. 

1256 """ 

1257 

1258 def __init__(self, builder): 

1259 self.builder_ = builder 

1260 self.classDef1_, self.classDef2_ = None, None 

1261 self.values_ = {} # (glyphclass1, glyphclass2) --> (value1, value2) 

1262 self.forceSubtableBreak_ = False 

1263 self.subtables_ = [] 

1264 

1265 def addPair(self, gc1, value1, gc2, value2): 

1266 """Add a pair positioning rule. 

1267 

1268 Args: 

1269 gc1: A set of glyph names for the "left" glyph 

1270 value1: An ``otTables.ValueRecord`` object for the left glyph's 

1271 positioning. 

1272 gc2: A set of glyph names for the "right" glyph 

1273 value2: An ``otTables.ValueRecord`` object for the right glyph's 

1274 positioning. 

1275 """ 

1276 mergeable = ( 

1277 not self.forceSubtableBreak_ 

1278 and self.classDef1_ is not None 

1279 and self.classDef1_.canAdd(gc1) 

1280 and self.classDef2_ is not None 

1281 and self.classDef2_.canAdd(gc2) 

1282 ) 

1283 if not mergeable: 

1284 self.flush_() 

1285 self.classDef1_ = ClassDefBuilder(useClass0=True) 

1286 self.classDef2_ = ClassDefBuilder(useClass0=False) 

1287 self.values_ = {} 

1288 self.classDef1_.add(gc1) 

1289 self.classDef2_.add(gc2) 

1290 self.values_[(gc1, gc2)] = (value1, value2) 

1291 

1292 def addSubtableBreak(self): 

1293 """Add an explicit subtable break at this point.""" 

1294 self.forceSubtableBreak_ = True 

1295 

1296 def subtables(self): 

1297 """Return the list of ``otTables.PairPos`` subtables constructed.""" 

1298 self.flush_() 

1299 return self.subtables_ 

1300 

1301 def flush_(self): 

1302 if self.classDef1_ is None or self.classDef2_ is None: 

1303 return 

1304 st = buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap) 

1305 if st.Coverage is None: 

1306 return 

1307 self.subtables_.append(st) 

1308 self.forceSubtableBreak_ = False 

1309 

1310 

1311class PairPosBuilder(LookupBuilder): 

1312 """Builds a Pair Positioning (GPOS2) lookup. 

1313 

1314 Attributes: 

1315 font (``fontTools.TTLib.TTFont``): A font object. 

1316 location: A string or tuple representing the location in the original 

1317 source which produced this lookup. 

1318 pairs: An array of class-based pair positioning tuples. Usually 

1319 manipulated with the :meth:`addClassPair` method below. 

1320 glyphPairs: A dictionary mapping a tuple of glyph names to a tuple 

1321 of ``otTables.ValueRecord`` objects. Usually manipulated with the 

1322 :meth:`addGlyphPair` method below. 

1323 lookupflag (int): The lookup's flag 

1324 markFilterSet: Either ``None`` if no mark filtering set is used, or 

1325 an integer representing the filtering set to be used for this 

1326 lookup. If a mark filtering set is provided, 

1327 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

1328 flags. 

1329 """ 

1330 

1331 def __init__(self, font, location): 

1332 LookupBuilder.__init__(self, font, location, "GPOS", 2) 

1333 self.pairs = [] # [(gc1, value1, gc2, value2)*] 

1334 self.glyphPairs = {} # (glyph1, glyph2) --> (value1, value2) 

1335 self.locations = {} # (gc1, gc2) --> (filepath, line, column) 

1336 

1337 def addClassPair(self, location, glyphclass1, value1, glyphclass2, value2): 

1338 """Add a class pair positioning rule to the current lookup. 

1339 

1340 Args: 

1341 location: A string or tuple representing the location in the 

1342 original source which produced this rule. Unused. 

1343 glyphclass1: A set of glyph names for the "left" glyph in the pair. 

1344 value1: A ``otTables.ValueRecord`` for positioning the left glyph. 

1345 glyphclass2: A set of glyph names for the "right" glyph in the pair. 

1346 value2: A ``otTables.ValueRecord`` for positioning the right glyph. 

1347 """ 

1348 self.pairs.append((glyphclass1, value1, glyphclass2, value2)) 

1349 

1350 def addGlyphPair(self, location, glyph1, value1, glyph2, value2): 

1351 """Add a glyph pair positioning rule to the current lookup. 

1352 

1353 Args: 

1354 location: A string or tuple representing the location in the 

1355 original source which produced this rule. 

1356 glyph1: A glyph name for the "left" glyph in the pair. 

1357 value1: A ``otTables.ValueRecord`` for positioning the left glyph. 

1358 glyph2: A glyph name for the "right" glyph in the pair. 

1359 value2: A ``otTables.ValueRecord`` for positioning the right glyph. 

1360 """ 

1361 key = (glyph1, glyph2) 

1362 oldValue = self.glyphPairs.get(key, None) 

1363 if oldValue is not None: 

1364 # the Feature File spec explicitly allows specific pairs generated 

1365 # by an 'enum' rule to be overridden by preceding single pairs 

1366 otherLoc = self.locations[key] 

1367 log.debug( 

1368 "Already defined position for pair %s %s at %s; " 

1369 "choosing the first value", 

1370 glyph1, 

1371 glyph2, 

1372 otherLoc, 

1373 ) 

1374 else: 

1375 self.glyphPairs[key] = (value1, value2) 

1376 self.locations[key] = location 

1377 

1378 def add_subtable_break(self, location): 

1379 self.pairs.append( 

1380 ( 

1381 self.SUBTABLE_BREAK_, 

1382 self.SUBTABLE_BREAK_, 

1383 self.SUBTABLE_BREAK_, 

1384 self.SUBTABLE_BREAK_, 

1385 ) 

1386 ) 

1387 

1388 def equals(self, other): 

1389 return ( 

1390 LookupBuilder.equals(self, other) 

1391 and self.glyphPairs == other.glyphPairs 

1392 and self.pairs == other.pairs 

1393 ) 

1394 

1395 def build(self): 

1396 """Build the lookup. 

1397 

1398 Returns: 

1399 An ``otTables.Lookup`` object representing the pair positioning 

1400 lookup. 

1401 """ 

1402 builders = {} 

1403 builder = ClassPairPosSubtableBuilder(self) 

1404 for glyphclass1, value1, glyphclass2, value2 in self.pairs: 

1405 if glyphclass1 is self.SUBTABLE_BREAK_: 

1406 builder.addSubtableBreak() 

1407 continue 

1408 builder.addPair(glyphclass1, value1, glyphclass2, value2) 

1409 subtables = [] 

1410 if self.glyphPairs: 

1411 subtables.extend(buildPairPosGlyphs(self.glyphPairs, self.glyphMap)) 

1412 subtables.extend(builder.subtables()) 

1413 lookup = self.buildLookup_(subtables) 

1414 

1415 # Compact the lookup 

1416 # This is a good moment to do it because the compaction should create 

1417 # smaller subtables, which may prevent overflows from happening. 

1418 # Keep reading the value from the ENV until ufo2ft switches to the config system 

1419 level = self.font.cfg.get( 

1420 "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", 

1421 default=_compression_level_from_env(), 

1422 ) 

1423 if level != 0: 

1424 log.info("Compacting GPOS...") 

1425 compact_lookup(self.font, level, lookup) 

1426 

1427 return lookup 

1428 

1429 

1430class SinglePosBuilder(LookupBuilder): 

1431 """Builds a Single Positioning (GPOS1) lookup. 

1432 

1433 Attributes: 

1434 font (``fontTools.TTLib.TTFont``): A font object. 

1435 location: A string or tuple representing the location in the original 

1436 source which produced this lookup. 

1437 mapping: A dictionary mapping a glyph name to a ``otTables.ValueRecord`` 

1438 objects. Usually manipulated with the :meth:`add_pos` method below. 

1439 lookupflag (int): The lookup's flag 

1440 markFilterSet: Either ``None`` if no mark filtering set is used, or 

1441 an integer representing the filtering set to be used for this 

1442 lookup. If a mark filtering set is provided, 

1443 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 

1444 flags. 

1445 """ 

1446 

1447 def __init__(self, font, location): 

1448 LookupBuilder.__init__(self, font, location, "GPOS", 1) 

1449 self.locations = {} # glyph -> (filename, line, column) 

1450 self.mapping = {} # glyph -> ot.ValueRecord 

1451 

1452 def add_pos(self, location, glyph, otValueRecord): 

1453 """Add a single positioning rule. 

1454 

1455 Args: 

1456 location: A string or tuple representing the location in the 

1457 original source which produced this lookup. 

1458 glyph: A glyph name. 

1459 otValueRection: A ``otTables.ValueRecord`` used to position the 

1460 glyph. 

1461 """ 

1462 if not self.can_add(glyph, otValueRecord): 

1463 otherLoc = self.locations[glyph] 

1464 raise OpenTypeLibError( 

1465 'Already defined different position for glyph "%s" at %s' 

1466 % (glyph, otherLoc), 

1467 location, 

1468 ) 

1469 if otValueRecord: 

1470 self.mapping[glyph] = otValueRecord 

1471 self.locations[glyph] = location 

1472 

1473 def can_add(self, glyph, value): 

1474 assert isinstance(value, ValueRecord) 

1475 curValue = self.mapping.get(glyph) 

1476 return curValue is None or curValue == value 

1477 

1478 def equals(self, other): 

1479 return LookupBuilder.equals(self, other) and self.mapping == other.mapping 

1480 

1481 def build(self): 

1482 """Build the lookup. 

1483 

1484 Returns: 

1485 An ``otTables.Lookup`` object representing the single positioning 

1486 lookup. 

1487 """ 

1488 subtables = buildSinglePos(self.mapping, self.glyphMap) 

1489 return self.buildLookup_(subtables) 

1490 

1491 

1492# GSUB 

1493 

1494 

1495def buildSingleSubstSubtable(mapping): 

1496 """Builds a single substitution (GSUB1) subtable. 

1497 

1498 Note that if you are implementing a layout compiler, you may find it more 

1499 flexible to use 

1500 :py:class:`fontTools.otlLib.lookupBuilders.SingleSubstBuilder` instead. 

1501 

1502 Args: 

1503 mapping: A dictionary mapping input glyph names to output glyph names. 

1504 

1505 Returns: 

1506 An ``otTables.SingleSubst`` object, or ``None`` if the mapping dictionary 

1507 is empty. 

1508 """ 

1509 if not mapping: 

1510 return None 

1511 self = ot.SingleSubst() 

1512 self.mapping = dict(mapping) 

1513 return self 

1514 

1515 

1516def buildMultipleSubstSubtable(mapping): 

1517 """Builds a multiple substitution (GSUB2) subtable. 

1518 

1519 Note that if you are implementing a layout compiler, you may find it more 

1520 flexible to use 

1521 :py:class:`fontTools.otlLib.lookupBuilders.MultipleSubstBuilder` instead. 

1522 

1523 Example:: 

1524 

1525 # sub uni06C0 by uni06D5.fina hamza.above 

1526 # sub uni06C2 by uni06C1.fina hamza.above; 

1527 

1528 subtable = buildMultipleSubstSubtable({ 

1529 "uni06C0": [ "uni06D5.fina", "hamza.above"], 

1530 "uni06C2": [ "uni06D1.fina", "hamza.above"] 

1531 }) 

1532 

1533 Args: 

1534 mapping: A dictionary mapping input glyph names to a list of output 

1535 glyph names. 

1536 

1537 Returns: 

1538 An ``otTables.MultipleSubst`` object or ``None`` if the mapping dictionary 

1539 is empty. 

1540 """ 

1541 if not mapping: 

1542 return None 

1543 self = ot.MultipleSubst() 

1544 self.mapping = dict(mapping) 

1545 return self 

1546 

1547 

1548def buildAlternateSubstSubtable(mapping): 

1549 """Builds an alternate substitution (GSUB3) subtable. 

1550 

1551 Note that if you are implementing a layout compiler, you may find it more 

1552 flexible to use 

1553 :py:class:`fontTools.otlLib.lookupBuilders.AlternateSubstBuilder` instead. 

1554 

1555 Args: 

1556 mapping: A dictionary mapping input glyph names to a list of output 

1557 glyph names. 

1558 

1559 Returns: 

1560 An ``otTables.AlternateSubst`` object or ``None`` if the mapping dictionary 

1561 is empty. 

1562 """ 

1563 if not mapping: 

1564 return None 

1565 self = ot.AlternateSubst() 

1566 self.alternates = dict(mapping) 

1567 return self 

1568 

1569 

1570def _getLigatureKey(components): 

1571 # Computes a key for ordering ligatures in a GSUB Type-4 lookup. 

1572 

1573 # When building the OpenType lookup, we need to make sure that 

1574 # the longest sequence of components is listed first, so we 

1575 # use the negative length as the primary key for sorting. 

1576 # To make buildLigatureSubstSubtable() deterministic, we use the 

1577 # component sequence as the secondary key. 

1578 

1579 # For example, this will sort (f,f,f) < (f,f,i) < (f,f) < (f,i) < (f,l). 

1580 return (-len(components), components) 

1581 

1582 

1583def buildLigatureSubstSubtable(mapping): 

1584 """Builds a ligature substitution (GSUB4) subtable. 

1585 

1586 Note that if you are implementing a layout compiler, you may find it more 

1587 flexible to use 

1588 :py:class:`fontTools.otlLib.lookupBuilders.LigatureSubstBuilder` instead. 

1589 

1590 Example:: 

1591 

1592 # sub f f i by f_f_i; 

1593 # sub f i by f_i; 

1594 

1595 subtable = buildLigatureSubstSubtable({ 

1596 ("f", "f", "i"): "f_f_i", 

1597 ("f", "i"): "f_i", 

1598 }) 

1599 

1600 Args: 

1601 mapping: A dictionary mapping tuples of glyph names to output 

1602 glyph names. 

1603 

1604 Returns: 

1605 An ``otTables.LigatureSubst`` object or ``None`` if the mapping dictionary 

1606 is empty. 

1607 """ 

1608 

1609 if not mapping: 

1610 return None 

1611 self = ot.LigatureSubst() 

1612 # The following single line can replace the rest of this function 

1613 # with fontTools >= 3.1: 

1614 # self.ligatures = dict(mapping) 

1615 self.ligatures = {} 

1616 for components in sorted(mapping.keys(), key=_getLigatureKey): 

1617 ligature = ot.Ligature() 

1618 ligature.Component = components[1:] 

1619 ligature.CompCount = len(ligature.Component) + 1 

1620 ligature.LigGlyph = mapping[components] 

1621 firstGlyph = components[0] 

1622 self.ligatures.setdefault(firstGlyph, []).append(ligature) 

1623 return self 

1624 

1625 

1626# GPOS 

1627 

1628 

1629def buildAnchor(x, y, point=None, deviceX=None, deviceY=None): 

1630 """Builds an Anchor table. 

1631 

1632 This determines the appropriate anchor format based on the passed parameters. 

1633 

1634 Args: 

1635 x (int): X coordinate. 

1636 y (int): Y coordinate. 

1637 point (int): Index of glyph contour point, if provided. 

1638 deviceX (``otTables.Device``): X coordinate device table, if provided. 

1639 deviceY (``otTables.Device``): Y coordinate device table, if provided. 

1640 

1641 Returns: 

1642 An ``otTables.Anchor`` object. 

1643 """ 

1644 self = ot.Anchor() 

1645 self.XCoordinate, self.YCoordinate = x, y 

1646 self.Format = 1 

1647 if point is not None: 

1648 self.AnchorPoint = point 

1649 self.Format = 2 

1650 if deviceX is not None or deviceY is not None: 

1651 assert ( 

1652 self.Format == 1 

1653 ), "Either point, or both of deviceX/deviceY, must be None." 

1654 self.XDeviceTable = deviceX 

1655 self.YDeviceTable = deviceY 

1656 self.Format = 3 

1657 return self 

1658 

1659 

1660def buildBaseArray(bases, numMarkClasses, glyphMap): 

1661 """Builds a base array record. 

1662 

1663 As part of building mark-to-base positioning rules, you will need to define 

1664 a ``BaseArray`` record, which "defines for each base glyph an array of 

1665 anchors, one for each mark class." This function builds the base array 

1666 subtable. 

1667 

1668 Example:: 

1669 

1670 bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} 

1671 basearray = buildBaseArray(bases, 2, font.getReverseGlyphMap()) 

1672 

1673 Args: 

1674 bases (dict): A dictionary mapping anchors to glyphs; the keys being 

1675 glyph names, and the values being dictionaries mapping mark class ID 

1676 to the appropriate ``otTables.Anchor`` object used for attaching marks 

1677 of that class. 

1678 numMarkClasses (int): The total number of mark classes for which anchors 

1679 are defined. 

1680 glyphMap: a glyph name to ID map, typically returned from 

1681 ``font.getReverseGlyphMap()``. 

1682 

1683 Returns: 

1684 An ``otTables.BaseArray`` object. 

1685 """ 

1686 self = ot.BaseArray() 

1687 self.BaseRecord = [] 

1688 for base in sorted(bases, key=glyphMap.__getitem__): 

1689 b = bases[base] 

1690 anchors = [b.get(markClass) for markClass in range(numMarkClasses)] 

1691 self.BaseRecord.append(buildBaseRecord(anchors)) 

1692 self.BaseCount = len(self.BaseRecord) 

1693 return self 

1694 

1695 

1696def buildBaseRecord(anchors): 

1697 # [otTables.Anchor, otTables.Anchor, ...] --> otTables.BaseRecord 

1698 self = ot.BaseRecord() 

1699 self.BaseAnchor = anchors 

1700 return self 

1701 

1702 

1703def buildComponentRecord(anchors): 

1704 """Builds a component record. 

1705 

1706 As part of building mark-to-ligature positioning rules, you will need to 

1707 define ``ComponentRecord`` objects, which contain "an array of offsets... 

1708 to the Anchor tables that define all the attachment points used to attach 

1709 marks to the component." This function builds the component record. 

1710 

1711 Args: 

1712 anchors: A list of ``otTables.Anchor`` objects or ``None``. 

1713 

1714 Returns: 

1715 A ``otTables.ComponentRecord`` object or ``None`` if no anchors are 

1716 supplied. 

1717 """ 

1718 if not anchors: 

1719 return None 

1720 self = ot.ComponentRecord() 

1721 self.LigatureAnchor = anchors 

1722 return self 

1723 

1724 

1725def buildCursivePosSubtable(attach, glyphMap): 

1726 """Builds a cursive positioning (GPOS3) subtable. 

1727 

1728 Cursive positioning lookups are made up of a coverage table of glyphs, 

1729 and a set of ``EntryExitRecord`` records containing the anchors for 

1730 each glyph. This function builds the cursive positioning subtable. 

1731 

1732 Example:: 

1733 

1734 subtable = buildCursivePosSubtable({ 

1735 "AlifIni": (None, buildAnchor(0, 50)), 

1736 "BehMed": (buildAnchor(500,250), buildAnchor(0,50)), 

1737 # ... 

1738 }, font.getReverseGlyphMap()) 

1739 

1740 Args: 

1741 attach (dict): A mapping between glyph names and a tuple of two 

1742 ``otTables.Anchor`` objects representing entry and exit anchors. 

1743 glyphMap: a glyph name to ID map, typically returned from 

1744 ``font.getReverseGlyphMap()``. 

1745 

1746 Returns: 

1747 An ``otTables.CursivePos`` object, or ``None`` if the attachment 

1748 dictionary was empty. 

1749 """ 

1750 if not attach: 

1751 return None 

1752 self = ot.CursivePos() 

1753 self.Format = 1 

1754 self.Coverage = buildCoverage(attach.keys(), glyphMap) 

1755 self.EntryExitRecord = [] 

1756 for glyph in self.Coverage.glyphs: 

1757 entryAnchor, exitAnchor = attach[glyph] 

1758 rec = ot.EntryExitRecord() 

1759 rec.EntryAnchor = entryAnchor 

1760 rec.ExitAnchor = exitAnchor 

1761 self.EntryExitRecord.append(rec) 

1762 self.EntryExitCount = len(self.EntryExitRecord) 

1763 return self 

1764 

1765 

1766def buildDevice(deltas): 

1767 """Builds a Device record as part of a ValueRecord or Anchor. 

1768 

1769 Device tables specify size-specific adjustments to value records 

1770 and anchors to reflect changes based on the resolution of the output. 

1771 For example, one could specify that an anchor's Y position should be 

1772 increased by 1 pixel when displayed at 8 pixels per em. This routine 

1773 builds device records. 

1774 

1775 Args: 

1776 deltas: A dictionary mapping pixels-per-em sizes to the delta 

1777 adjustment in pixels when the font is displayed at that size. 

1778 

1779 Returns: 

1780 An ``otTables.Device`` object if any deltas were supplied, or 

1781 ``None`` otherwise. 

1782 """ 

1783 if not deltas: 

1784 return None 

1785 self = ot.Device() 

1786 keys = deltas.keys() 

1787 self.StartSize = startSize = min(keys) 

1788 self.EndSize = endSize = max(keys) 

1789 assert 0 <= startSize <= endSize 

1790 self.DeltaValue = deltaValues = [ 

1791 deltas.get(size, 0) for size in range(startSize, endSize + 1) 

1792 ] 

1793 maxDelta = max(deltaValues) 

1794 minDelta = min(deltaValues) 

1795 assert minDelta > -129 and maxDelta < 128 

1796 if minDelta > -3 and maxDelta < 2: 

1797 self.DeltaFormat = 1 

1798 elif minDelta > -9 and maxDelta < 8: 

1799 self.DeltaFormat = 2 

1800 else: 

1801 self.DeltaFormat = 3 

1802 return self 

1803 

1804 

1805def buildLigatureArray(ligs, numMarkClasses, glyphMap): 

1806 """Builds a LigatureArray subtable. 

1807 

1808 As part of building a mark-to-ligature lookup, you will need to define 

1809 the set of anchors (for each mark class) on each component of the ligature 

1810 where marks can be attached. For example, for an Arabic divine name ligature 

1811 (lam lam heh), you may want to specify mark attachment positioning for 

1812 superior marks (fatha, etc.) and inferior marks (kasra, etc.) on each glyph 

1813 of the ligature. This routine builds the ligature array record. 

1814 

1815 Example:: 

1816 

1817 buildLigatureArray({ 

1818 "lam-lam-heh": [ 

1819 { 0: superiorAnchor1, 1: inferiorAnchor1 }, # attach points for lam1 

1820 { 0: superiorAnchor2, 1: inferiorAnchor2 }, # attach points for lam2 

1821 { 0: superiorAnchor3, 1: inferiorAnchor3 }, # attach points for heh 

1822 ] 

1823 }, 2, font.getReverseGlyphMap()) 

1824 

1825 Args: 

1826 ligs (dict): A mapping of ligature names to an array of dictionaries: 

1827 for each component glyph in the ligature, an dictionary mapping 

1828 mark class IDs to anchors. 

1829 numMarkClasses (int): The number of mark classes. 

1830 glyphMap: a glyph name to ID map, typically returned from 

1831 ``font.getReverseGlyphMap()``. 

1832 

1833 Returns: 

1834 An ``otTables.LigatureArray`` object if deltas were supplied. 

1835 """ 

1836 self = ot.LigatureArray() 

1837 self.LigatureAttach = [] 

1838 for lig in sorted(ligs, key=glyphMap.__getitem__): 

1839 anchors = [] 

1840 for component in ligs[lig]: 

1841 anchors.append([component.get(mc) for mc in range(numMarkClasses)]) 

1842 self.LigatureAttach.append(buildLigatureAttach(anchors)) 

1843 self.LigatureCount = len(self.LigatureAttach) 

1844 return self 

1845 

1846 

1847def buildLigatureAttach(components): 

1848 # [[Anchor, Anchor], [Anchor, Anchor, Anchor]] --> LigatureAttach 

1849 self = ot.LigatureAttach() 

1850 self.ComponentRecord = [buildComponentRecord(c) for c in components] 

1851 self.ComponentCount = len(self.ComponentRecord) 

1852 return self 

1853 

1854 

1855def buildMarkArray(marks, glyphMap): 

1856 """Builds a mark array subtable. 

1857 

1858 As part of building mark-to-* positioning rules, you will need to define 

1859 a MarkArray subtable, which "defines the class and the anchor point 

1860 for a mark glyph." This function builds the mark array subtable. 

1861 

1862 Example:: 

1863 

1864 mark = { 

1865 "acute": (0, buildAnchor(300,712)), 

1866 # ... 

1867 } 

1868 markarray = buildMarkArray(marks, font.getReverseGlyphMap()) 

1869 

1870 Args: 

1871 marks (dict): A dictionary mapping anchors to glyphs; the keys being 

1872 glyph names, and the values being a tuple of mark class number and 

1873 an ``otTables.Anchor`` object representing the mark's attachment 

1874 point. 

1875 glyphMap: a glyph name to ID map, typically returned from 

1876 ``font.getReverseGlyphMap()``. 

1877 

1878 Returns: 

1879 An ``otTables.MarkArray`` object. 

1880 """ 

1881 self = ot.MarkArray() 

1882 self.MarkRecord = [] 

1883 for mark in sorted(marks.keys(), key=glyphMap.__getitem__): 

1884 markClass, anchor = marks[mark] 

1885 markrec = buildMarkRecord(markClass, anchor) 

1886 self.MarkRecord.append(markrec) 

1887 self.MarkCount = len(self.MarkRecord) 

1888 return self 

1889 

1890 

1891def buildMarkBasePos(marks, bases, glyphMap): 

1892 """Build a list of MarkBasePos (GPOS4) subtables. 

1893 

1894 This routine turns a set of marks and bases into a list of mark-to-base 

1895 positioning subtables. Currently the list will contain a single subtable 

1896 containing all marks and bases, although at a later date it may return the 

1897 optimal list of subtables subsetting the marks and bases into groups which 

1898 save space. See :func:`buildMarkBasePosSubtable` below. 

1899 

1900 Note that if you are implementing a layout compiler, you may find it more 

1901 flexible to use 

1902 :py:class:`fontTools.otlLib.lookupBuilders.MarkBasePosBuilder` instead. 

1903 

1904 Example:: 

1905 

1906 # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... 

1907 

1908 marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} 

1909 bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} 

1910 markbaseposes = buildMarkBasePos(marks, bases, font.getReverseGlyphMap()) 

1911 

1912 Args: 

1913 marks (dict): A dictionary mapping anchors to glyphs; the keys being 

1914 glyph names, and the values being a tuple of mark class number and 

1915 an ``otTables.Anchor`` object representing the mark's attachment 

1916 point. (See :func:`buildMarkArray`.) 

1917 bases (dict): A dictionary mapping anchors to glyphs; the keys being 

1918 glyph names, and the values being dictionaries mapping mark class ID 

1919 to the appropriate ``otTables.Anchor`` object used for attaching marks 

1920 of that class. (See :func:`buildBaseArray`.) 

1921 glyphMap: a glyph name to ID map, typically returned from 

1922 ``font.getReverseGlyphMap()``. 

1923 

1924 Returns: 

1925 A list of ``otTables.MarkBasePos`` objects. 

1926 """ 

1927 # TODO: Consider emitting multiple subtables to save space. 

1928 # Partition the marks and bases into disjoint subsets, so that 

1929 # MarkBasePos rules would only access glyphs from a single 

1930 # subset. This would likely lead to smaller mark/base 

1931 # matrices, so we might be able to omit many of the empty 

1932 # anchor tables that we currently produce. Of course, this 

1933 # would only work if the MarkBasePos rules of real-world fonts 

1934 # allow partitioning into multiple subsets. We should find out 

1935 # whether this is the case; if so, implement the optimization. 

1936 # On the other hand, a very large number of subtables could 

1937 # slow down layout engines; so this would need profiling. 

1938 return [buildMarkBasePosSubtable(marks, bases, glyphMap)] 

1939 

1940 

1941def buildMarkBasePosSubtable(marks, bases, glyphMap): 

1942 """Build a single MarkBasePos (GPOS4) subtable. 

1943 

1944 This builds a mark-to-base lookup subtable containing all of the referenced 

1945 marks and bases. See :func:`buildMarkBasePos`. 

1946 

1947 Args: 

1948 marks (dict): A dictionary mapping anchors to glyphs; the keys being 

1949 glyph names, and the values being a tuple of mark class number and 

1950 an ``otTables.Anchor`` object representing the mark's attachment 

1951 point. (See :func:`buildMarkArray`.) 

1952 bases (dict): A dictionary mapping anchors to glyphs; the keys being 

1953 glyph names, and the values being dictionaries mapping mark class ID 

1954 to the appropriate ``otTables.Anchor`` object used for attaching marks 

1955 of that class. (See :func:`buildBaseArray`.) 

1956 glyphMap: a glyph name to ID map, typically returned from 

1957 ``font.getReverseGlyphMap()``. 

1958 

1959 Returns: 

1960 A ``otTables.MarkBasePos`` object. 

1961 """ 

1962 self = ot.MarkBasePos() 

1963 self.Format = 1 

1964 self.MarkCoverage = buildCoverage(marks, glyphMap) 

1965 self.MarkArray = buildMarkArray(marks, glyphMap) 

1966 self.ClassCount = max([mc for mc, _ in marks.values()]) + 1 

1967 self.BaseCoverage = buildCoverage(bases, glyphMap) 

1968 self.BaseArray = buildBaseArray(bases, self.ClassCount, glyphMap) 

1969 return self 

1970 

1971 

1972def buildMarkLigPos(marks, ligs, glyphMap): 

1973 """Build a list of MarkLigPos (GPOS5) subtables. 

1974 

1975 This routine turns a set of marks and ligatures into a list of mark-to-ligature 

1976 positioning subtables. Currently the list will contain a single subtable 

1977 containing all marks and ligatures, although at a later date it may return 

1978 the optimal list of subtables subsetting the marks and ligatures into groups 

1979 which save space. See :func:`buildMarkLigPosSubtable` below. 

1980 

1981 Note that if you are implementing a layout compiler, you may find it more 

1982 flexible to use 

1983 :py:class:`fontTools.otlLib.lookupBuilders.MarkLigPosBuilder` instead. 

1984 

1985 Example:: 

1986 

1987 # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... 

1988 marks = { 

1989 "acute": (0, a1), 

1990 "grave": (0, a1), 

1991 "cedilla": (1, a2) 

1992 } 

1993 ligs = { 

1994 "f_i": [ 

1995 { 0: a3, 1: a5 }, # f 

1996 { 0: a4, 1: a5 } # i 

1997 ], 

1998 # "c_t": [{...}, {...}] 

1999 } 

2000 markligposes = buildMarkLigPos(marks, ligs, 

2001 font.getReverseGlyphMap()) 

2002 

2003 Args: 

2004 marks (dict): A dictionary mapping anchors to glyphs; the keys being 

2005 glyph names, and the values being a tuple of mark class number and 

2006 an ``otTables.Anchor`` object representing the mark's attachment 

2007 point. (See :func:`buildMarkArray`.) 

2008 ligs (dict): A mapping of ligature names to an array of dictionaries: 

2009 for each component glyph in the ligature, an dictionary mapping 

2010 mark class IDs to anchors. (See :func:`buildLigatureArray`.) 

2011 glyphMap: a glyph name to ID map, typically returned from 

2012 ``font.getReverseGlyphMap()``. 

2013 

2014 Returns: 

2015 A list of ``otTables.MarkLigPos`` objects. 

2016 

2017 """ 

2018 # TODO: Consider splitting into multiple subtables to save space, 

2019 # as with MarkBasePos, this would be a trade-off that would need 

2020 # profiling. And, depending on how typical fonts are structured, 

2021 # it might not be worth doing at all. 

2022 return [buildMarkLigPosSubtable(marks, ligs, glyphMap)] 

2023 

2024 

2025def buildMarkLigPosSubtable(marks, ligs, glyphMap): 

2026 """Build a single MarkLigPos (GPOS5) subtable. 

2027 

2028 This builds a mark-to-base lookup subtable containing all of the referenced 

2029 marks and bases. See :func:`buildMarkLigPos`. 

2030 

2031 Args: 

2032 marks (dict): A dictionary mapping anchors to glyphs; the keys being 

2033 glyph names, and the values being a tuple of mark class number and 

2034 an ``otTables.Anchor`` object representing the mark's attachment 

2035 point. (See :func:`buildMarkArray`.) 

2036 ligs (dict): A mapping of ligature names to an array of dictionaries: 

2037 for each component glyph in the ligature, an dictionary mapping 

2038 mark class IDs to anchors. (See :func:`buildLigatureArray`.) 

2039 glyphMap: a glyph name to ID map, typically returned from 

2040 ``font.getReverseGlyphMap()``. 

2041 

2042 Returns: 

2043 A ``otTables.MarkLigPos`` object. 

2044 """ 

2045 self = ot.MarkLigPos() 

2046 self.Format = 1 

2047 self.MarkCoverage = buildCoverage(marks, glyphMap) 

2048 self.MarkArray = buildMarkArray(marks, glyphMap) 

2049 self.ClassCount = max([mc for mc, _ in marks.values()]) + 1 

2050 self.LigatureCoverage = buildCoverage(ligs, glyphMap) 

2051 self.LigatureArray = buildLigatureArray(ligs, self.ClassCount, glyphMap) 

2052 return self 

2053 

2054 

2055def buildMarkRecord(classID, anchor): 

2056 assert isinstance(classID, int) 

2057 assert isinstance(anchor, ot.Anchor) 

2058 self = ot.MarkRecord() 

2059 self.Class = classID 

2060 self.MarkAnchor = anchor 

2061 return self 

2062 

2063 

2064def buildMark2Record(anchors): 

2065 # [otTables.Anchor, otTables.Anchor, ...] --> otTables.Mark2Record 

2066 self = ot.Mark2Record() 

2067 self.Mark2Anchor = anchors 

2068 return self 

2069 

2070 

2071def _getValueFormat(f, values, i): 

2072 # Helper for buildPairPos{Glyphs|Classes}Subtable. 

2073 if f is not None: 

2074 return f 

2075 mask = 0 

2076 for value in values: 

2077 if value is not None and value[i] is not None: 

2078 mask |= value[i].getFormat() 

2079 return mask 

2080 

2081 

2082def buildPairPosClassesSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None): 

2083 """Builds a class pair adjustment (GPOS2 format 2) subtable. 

2084 

2085 Kerning tables are generally expressed as pair positioning tables using 

2086 class-based pair adjustments. This routine builds format 2 PairPos 

2087 subtables. 

2088 

2089 Note that if you are implementing a layout compiler, you may find it more 

2090 flexible to use 

2091 :py:class:`fontTools.otlLib.lookupBuilders.ClassPairPosSubtableBuilder` 

2092 instead, as this takes care of ensuring that the supplied pairs can be 

2093 formed into non-overlapping classes and emitting individual subtables 

2094 whenever the non-overlapping requirement means that a new subtable is 

2095 required. 

2096 

2097 Example:: 

2098 

2099 pairs = {} 

2100 

2101 pairs[( 

2102 [ "K", "X" ], 

2103 [ "W", "V" ] 

2104 )] = ( buildValue(xAdvance=+5), buildValue() ) 

2105 # pairs[(... , ...)] = (..., ...) 

2106 

2107 pairpos = buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap()) 

2108 

2109 Args: 

2110 pairs (dict): Pair positioning data; the keys being a two-element 

2111 tuple of lists of glyphnames, and the values being a two-element 

2112 tuple of ``otTables.ValueRecord`` objects. 

2113 glyphMap: a glyph name to ID map, typically returned from 

2114 ``font.getReverseGlyphMap()``. 

2115 valueFormat1: Force the "left" value records to the given format. 

2116 valueFormat2: Force the "right" value records to the given format. 

2117 

2118 Returns: 

2119 A ``otTables.PairPos`` object. 

2120 """ 

2121 coverage = set() 

2122 classDef1 = ClassDefBuilder(useClass0=True) 

2123 classDef2 = ClassDefBuilder(useClass0=False) 

2124 for gc1, gc2 in sorted(pairs): 

2125 coverage.update(gc1) 

2126 classDef1.add(gc1) 

2127 classDef2.add(gc2) 

2128 self = ot.PairPos() 

2129 self.Format = 2 

2130 valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0) 

2131 valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1) 

2132 self.Coverage = buildCoverage(coverage, glyphMap) 

2133 self.ClassDef1 = classDef1.build() 

2134 self.ClassDef2 = classDef2.build() 

2135 classes1 = classDef1.classes() 

2136 classes2 = classDef2.classes() 

2137 self.Class1Record = [] 

2138 for c1 in classes1: 

2139 rec1 = ot.Class1Record() 

2140 rec1.Class2Record = [] 

2141 self.Class1Record.append(rec1) 

2142 for c2 in classes2: 

2143 rec2 = ot.Class2Record() 

2144 val1, val2 = pairs.get((c1, c2), (None, None)) 

2145 rec2.Value1 = ( 

2146 ValueRecord(src=val1, valueFormat=valueFormat1) 

2147 if valueFormat1 

2148 else None 

2149 ) 

2150 rec2.Value2 = ( 

2151 ValueRecord(src=val2, valueFormat=valueFormat2) 

2152 if valueFormat2 

2153 else None 

2154 ) 

2155 rec1.Class2Record.append(rec2) 

2156 self.Class1Count = len(self.Class1Record) 

2157 self.Class2Count = len(classes2) 

2158 return self 

2159 

2160 

2161def buildPairPosGlyphs(pairs, glyphMap): 

2162 """Builds a list of glyph-based pair adjustment (GPOS2 format 1) subtables. 

2163 

2164 This organises a list of pair positioning adjustments into subtables based 

2165 on common value record formats. 

2166 

2167 Note that if you are implementing a layout compiler, you may find it more 

2168 flexible to use 

2169 :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` 

2170 instead. 

2171 

2172 Example:: 

2173 

2174 pairs = { 

2175 ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ), 

2176 ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ), 

2177 # ... 

2178 } 

2179 

2180 subtables = buildPairPosGlyphs(pairs, font.getReverseGlyphMap()) 

2181 

2182 Args: 

2183 pairs (dict): Pair positioning data; the keys being a two-element 

2184 tuple of glyphnames, and the values being a two-element 

2185 tuple of ``otTables.ValueRecord`` objects. 

2186 glyphMap: a glyph name to ID map, typically returned from 

2187 ``font.getReverseGlyphMap()``. 

2188 

2189 Returns: 

2190 A list of ``otTables.PairPos`` objects. 

2191 """ 

2192 

2193 p = {} # (formatA, formatB) --> {(glyphA, glyphB): (valA, valB)} 

2194 for (glyphA, glyphB), (valA, valB) in pairs.items(): 

2195 formatA = valA.getFormat() if valA is not None else 0 

2196 formatB = valB.getFormat() if valB is not None else 0 

2197 pos = p.setdefault((formatA, formatB), {}) 

2198 pos[(glyphA, glyphB)] = (valA, valB) 

2199 return [ 

2200 buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB) 

2201 for ((formatA, formatB), pos) in sorted(p.items()) 

2202 ] 

2203 

2204 

2205def buildPairPosGlyphsSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None): 

2206 """Builds a single glyph-based pair adjustment (GPOS2 format 1) subtable. 

2207 

2208 This builds a PairPos subtable from a dictionary of glyph pairs and 

2209 their positioning adjustments. See also :func:`buildPairPosGlyphs`. 

2210 

2211 Note that if you are implementing a layout compiler, you may find it more 

2212 flexible to use 

2213 :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` instead. 

2214 

2215 Example:: 

2216 

2217 pairs = { 

2218 ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ), 

2219 ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ), 

2220 # ... 

2221 } 

2222 

2223 pairpos = buildPairPosGlyphsSubtable(pairs, font.getReverseGlyphMap()) 

2224 

2225 Args: 

2226 pairs (dict): Pair positioning data; the keys being a two-element 

2227 tuple of glyphnames, and the values being a two-element 

2228 tuple of ``otTables.ValueRecord`` objects. 

2229 glyphMap: a glyph name to ID map, typically returned from 

2230 ``font.getReverseGlyphMap()``. 

2231 valueFormat1: Force the "left" value records to the given format. 

2232 valueFormat2: Force the "right" value records to the given format. 

2233 

2234 Returns: 

2235 A ``otTables.PairPos`` object. 

2236 """ 

2237 self = ot.PairPos() 

2238 self.Format = 1 

2239 valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0) 

2240 valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1) 

2241 p = {} 

2242 for (glyphA, glyphB), (valA, valB) in pairs.items(): 

2243 p.setdefault(glyphA, []).append((glyphB, valA, valB)) 

2244 self.Coverage = buildCoverage({g for g, _ in pairs.keys()}, glyphMap) 

2245 self.PairSet = [] 

2246 for glyph in self.Coverage.glyphs: 

2247 ps = ot.PairSet() 

2248 ps.PairValueRecord = [] 

2249 self.PairSet.append(ps) 

2250 for glyph2, val1, val2 in sorted(p[glyph], key=lambda x: glyphMap[x[0]]): 

2251 pvr = ot.PairValueRecord() 

2252 pvr.SecondGlyph = glyph2 

2253 pvr.Value1 = ( 

2254 ValueRecord(src=val1, valueFormat=valueFormat1) 

2255 if valueFormat1 

2256 else None 

2257 ) 

2258 pvr.Value2 = ( 

2259 ValueRecord(src=val2, valueFormat=valueFormat2) 

2260 if valueFormat2 

2261 else None 

2262 ) 

2263 ps.PairValueRecord.append(pvr) 

2264 ps.PairValueCount = len(ps.PairValueRecord) 

2265 self.PairSetCount = len(self.PairSet) 

2266 return self 

2267 

2268 

2269def buildSinglePos(mapping, glyphMap): 

2270 """Builds a list of single adjustment (GPOS1) subtables. 

2271 

2272 This builds a list of SinglePos subtables from a dictionary of glyph 

2273 names and their positioning adjustments. The format of the subtables are 

2274 determined to optimize the size of the resulting subtables. 

2275 See also :func:`buildSinglePosSubtable`. 

2276 

2277 Note that if you are implementing a layout compiler, you may find it more 

2278 flexible to use 

2279 :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead. 

2280 

2281 Example:: 

2282 

2283 mapping = { 

2284 "V": buildValue({ "xAdvance" : +5 }), 

2285 # ... 

2286 } 

2287 

2288 subtables = buildSinglePos(pairs, font.getReverseGlyphMap()) 

2289 

2290 Args: 

2291 mapping (dict): A mapping between glyphnames and 

2292 ``otTables.ValueRecord`` objects. 

2293 glyphMap: a glyph name to ID map, typically returned from 

2294 ``font.getReverseGlyphMap()``. 

2295 

2296 Returns: 

2297 A list of ``otTables.SinglePos`` objects. 

2298 """ 

2299 result, handled = [], set() 

2300 # In SinglePos format 1, the covered glyphs all share the same ValueRecord. 

2301 # In format 2, each glyph has its own ValueRecord, but these records 

2302 # all have the same properties (eg., all have an X but no Y placement). 

2303 coverages, masks, values = {}, {}, {} 

2304 for glyph, value in mapping.items(): 

2305 key = _getSinglePosValueKey(value) 

2306 coverages.setdefault(key, []).append(glyph) 

2307 masks.setdefault(key[0], []).append(key) 

2308 values[key] = value 

2309 

2310 # If a ValueRecord is shared between multiple glyphs, we generate 

2311 # a SinglePos format 1 subtable; that is the most compact form. 

2312 for key, glyphs in coverages.items(): 

2313 # 5 ushorts is the length of introducing another sublookup 

2314 if len(glyphs) * _getSinglePosValueSize(key) > 5: 

2315 format1Mapping = {g: values[key] for g in glyphs} 

2316 result.append(buildSinglePosSubtable(format1Mapping, glyphMap)) 

2317 handled.add(key) 

2318 

2319 # In the remaining ValueRecords, look for those whose valueFormat 

2320 # (the set of used properties) is shared between multiple records. 

2321 # These will get encoded in format 2. 

2322 for valueFormat, keys in masks.items(): 

2323 f2 = [k for k in keys if k not in handled] 

2324 if len(f2) > 1: 

2325 format2Mapping = {} 

2326 for k in f2: 

2327 format2Mapping.update((g, values[k]) for g in coverages[k]) 

2328 result.append(buildSinglePosSubtable(format2Mapping, glyphMap)) 

2329 handled.update(f2) 

2330 

2331 # The remaining ValueRecords are only used by a few glyphs, normally 

2332 # one. We encode these in format 1 again. 

2333 for key, glyphs in coverages.items(): 

2334 if key not in handled: 

2335 for g in glyphs: 

2336 st = buildSinglePosSubtable({g: values[key]}, glyphMap) 

2337 result.append(st) 

2338 

2339 # When the OpenType layout engine traverses the subtables, it will 

2340 # stop after the first matching subtable. Therefore, we sort the 

2341 # resulting subtables by decreasing coverage size; this increases 

2342 # the chance that the layout engine can do an early exit. (Of course, 

2343 # this would only be true if all glyphs were equally frequent, which 

2344 # is not really the case; but we do not know their distribution). 

2345 # If two subtables cover the same number of glyphs, we sort them 

2346 # by glyph ID so that our output is deterministic. 

2347 result.sort(key=lambda t: _getSinglePosTableKey(t, glyphMap)) 

2348 return result 

2349 

2350 

2351def buildSinglePosSubtable(values, glyphMap): 

2352 """Builds a single adjustment (GPOS1) subtable. 

2353 

2354 This builds a list of SinglePos subtables from a dictionary of glyph 

2355 names and their positioning adjustments. The format of the subtable is 

2356 determined to optimize the size of the output. 

2357 See also :func:`buildSinglePos`. 

2358 

2359 Note that if you are implementing a layout compiler, you may find it more 

2360 flexible to use 

2361 :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead. 

2362 

2363 Example:: 

2364 

2365 mapping = { 

2366 "V": buildValue({ "xAdvance" : +5 }), 

2367 # ... 

2368 } 

2369 

2370 subtable = buildSinglePos(pairs, font.getReverseGlyphMap()) 

2371 

2372 Args: 

2373 mapping (dict): A mapping between glyphnames and 

2374 ``otTables.ValueRecord`` objects. 

2375 glyphMap: a glyph name to ID map, typically returned from 

2376 ``font.getReverseGlyphMap()``. 

2377 

2378 Returns: 

2379 A ``otTables.SinglePos`` object. 

2380 """ 

2381 self = ot.SinglePos() 

2382 self.Coverage = buildCoverage(values.keys(), glyphMap) 

2383 valueFormat = self.ValueFormat = reduce( 

2384 int.__or__, [v.getFormat() for v in values.values()], 0 

2385 ) 

2386 valueRecords = [ 

2387 ValueRecord(src=values[g], valueFormat=valueFormat) 

2388 for g in self.Coverage.glyphs 

2389 ] 

2390 if all(v == valueRecords[0] for v in valueRecords): 

2391 self.Format = 1 

2392 if self.ValueFormat != 0: 

2393 self.Value = valueRecords[0] 

2394 else: 

2395 self.Value = None 

2396 else: 

2397 self.Format = 2 

2398 self.Value = valueRecords 

2399 self.ValueCount = len(self.Value) 

2400 return self 

2401 

2402 

2403def _getSinglePosTableKey(subtable, glyphMap): 

2404 assert isinstance(subtable, ot.SinglePos), subtable 

2405 glyphs = subtable.Coverage.glyphs 

2406 return (-len(glyphs), glyphMap[glyphs[0]]) 

2407 

2408 

2409def _getSinglePosValueKey(valueRecord): 

2410 # otBase.ValueRecord --> (2, ("YPlacement": 12)) 

2411 assert isinstance(valueRecord, ValueRecord), valueRecord 

2412 valueFormat, result = 0, [] 

2413 for name, value in valueRecord.__dict__.items(): 

2414 if isinstance(value, ot.Device): 

2415 result.append((name, _makeDeviceTuple(value))) 

2416 else: 

2417 result.append((name, value)) 

2418 valueFormat |= valueRecordFormatDict[name][0] 

2419 result.sort() 

2420 result.insert(0, valueFormat) 

2421 return tuple(result) 

2422 

2423 

2424_DeviceTuple = namedtuple("_DeviceTuple", "DeltaFormat StartSize EndSize DeltaValue") 

2425 

2426 

2427def _makeDeviceTuple(device): 

2428 # otTables.Device --> tuple, for making device tables unique 

2429 return _DeviceTuple( 

2430 device.DeltaFormat, 

2431 device.StartSize, 

2432 device.EndSize, 

2433 () if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue), 

2434 ) 

2435 

2436 

2437def _getSinglePosValueSize(valueKey): 

2438 # Returns how many ushorts this valueKey (short form of ValueRecord) takes up 

2439 count = 0 

2440 for _, v in valueKey[1:]: 

2441 if isinstance(v, _DeviceTuple): 

2442 count += len(v.DeltaValue) + 3 

2443 else: 

2444 count += 1 

2445 return count 

2446 

2447 

2448def buildValue(value): 

2449 """Builds a positioning value record. 

2450 

2451 Value records are used to specify coordinates and adjustments for 

2452 positioning and attaching glyphs. Many of the positioning functions 

2453 in this library take ``otTables.ValueRecord`` objects as arguments. 

2454 This function builds value records from dictionaries. 

2455 

2456 Args: 

2457 value (dict): A dictionary with zero or more of the following keys: 

2458 - ``xPlacement`` 

2459 - ``yPlacement`` 

2460 - ``xAdvance`` 

2461 - ``yAdvance`` 

2462 - ``xPlaDevice`` 

2463 - ``yPlaDevice`` 

2464 - ``xAdvDevice`` 

2465 - ``yAdvDevice`` 

2466 

2467 Returns: 

2468 An ``otTables.ValueRecord`` object. 

2469 """ 

2470 self = ValueRecord() 

2471 for k, v in value.items(): 

2472 setattr(self, k, v) 

2473 return self 

2474 

2475 

2476# GDEF 

2477 

2478 

2479def buildAttachList(attachPoints, glyphMap): 

2480 """Builds an AttachList subtable. 

2481 

2482 A GDEF table may contain an Attachment Point List table (AttachList) 

2483 which stores the contour indices of attachment points for glyphs with 

2484 attachment points. This routine builds AttachList subtables. 

2485 

2486 Args: 

2487 attachPoints (dict): A mapping between glyph names and a list of 

2488 contour indices. 

2489 

2490 Returns: 

2491 An ``otTables.AttachList`` object if attachment points are supplied, 

2492 or ``None`` otherwise. 

2493 """ 

2494 if not attachPoints: 

2495 return None 

2496 self = ot.AttachList() 

2497 self.Coverage = buildCoverage(attachPoints.keys(), glyphMap) 

2498 self.AttachPoint = [buildAttachPoint(attachPoints[g]) for g in self.Coverage.glyphs] 

2499 self.GlyphCount = len(self.AttachPoint) 

2500 return self 

2501 

2502 

2503def buildAttachPoint(points): 

2504 # [4, 23, 41] --> otTables.AttachPoint 

2505 # Only used by above. 

2506 if not points: 

2507 return None 

2508 self = ot.AttachPoint() 

2509 self.PointIndex = sorted(set(points)) 

2510 self.PointCount = len(self.PointIndex) 

2511 return self 

2512 

2513 

2514def buildCaretValueForCoord(coord): 

2515 # 500 --> otTables.CaretValue, format 1 

2516 # (500, DeviceTable) --> otTables.CaretValue, format 3 

2517 self = ot.CaretValue() 

2518 if isinstance(coord, tuple): 

2519 self.Format = 3 

2520 self.Coordinate, self.DeviceTable = coord 

2521 else: 

2522 self.Format = 1 

2523 self.Coordinate = coord 

2524 return self 

2525 

2526 

2527def buildCaretValueForPoint(point): 

2528 # 4 --> otTables.CaretValue, format 2 

2529 self = ot.CaretValue() 

2530 self.Format = 2 

2531 self.CaretValuePoint = point 

2532 return self 

2533 

2534 

2535def buildLigCaretList(coords, points, glyphMap): 

2536 """Builds a ligature caret list table. 

2537 

2538 Ligatures appear as a single glyph representing multiple characters; however 

2539 when, for example, editing text containing a ``f_i`` ligature, the user may 

2540 want to place the cursor between the ``f`` and the ``i``. The ligature caret 

2541 list in the GDEF table specifies the position to display the "caret" (the 

2542 character insertion indicator, typically a flashing vertical bar) "inside" 

2543 the ligature to represent an insertion point. The insertion positions may 

2544 be specified either by coordinate or by contour point. 

2545 

2546 Example:: 

2547 

2548 coords = { 

2549 "f_f_i": [300, 600] # f|fi cursor at 300 units, ff|i cursor at 600. 

2550 } 

2551 points = { 

2552 "c_t": [28] # c|t cursor appears at coordinate of contour point 28. 

2553 } 

2554 ligcaretlist = buildLigCaretList(coords, points, font.getReverseGlyphMap()) 

2555 

2556 Args: 

2557 coords: A mapping between glyph names and a list of coordinates for 

2558 the insertion point of each ligature component after the first one. 

2559 points: A mapping between glyph names and a list of contour points for 

2560 the insertion point of each ligature component after the first one. 

2561 glyphMap: a glyph name to ID map, typically returned from 

2562 ``font.getReverseGlyphMap()``. 

2563 

2564 Returns: 

2565 A ``otTables.LigCaretList`` object if any carets are present, or 

2566 ``None`` otherwise.""" 

2567 glyphs = set(coords.keys()) if coords else set() 

2568 if points: 

2569 glyphs.update(points.keys()) 

2570 carets = {g: buildLigGlyph(coords.get(g), points.get(g)) for g in glyphs} 

2571 carets = {g: c for g, c in carets.items() if c is not None} 

2572 if not carets: 

2573 return None 

2574 self = ot.LigCaretList() 

2575 self.Coverage = buildCoverage(carets.keys(), glyphMap) 

2576 self.LigGlyph = [carets[g] for g in self.Coverage.glyphs] 

2577 self.LigGlyphCount = len(self.LigGlyph) 

2578 return self 

2579 

2580 

2581def buildLigGlyph(coords, points): 

2582 # ([500], [4]) --> otTables.LigGlyph; None for empty coords/points 

2583 carets = [] 

2584 if coords: 

2585 coords = sorted(coords, key=lambda c: c[0] if isinstance(c, tuple) else c) 

2586 carets.extend([buildCaretValueForCoord(c) for c in coords]) 

2587 if points: 

2588 carets.extend([buildCaretValueForPoint(p) for p in sorted(points)]) 

2589 if not carets: 

2590 return None 

2591 self = ot.LigGlyph() 

2592 self.CaretValue = carets 

2593 self.CaretCount = len(self.CaretValue) 

2594 return self 

2595 

2596 

2597def buildMarkGlyphSetsDef(markSets, glyphMap): 

2598 """Builds a mark glyph sets definition table. 

2599 

2600 OpenType Layout lookups may choose to use mark filtering sets to consider 

2601 or ignore particular combinations of marks. These sets are specified by 

2602 setting a flag on the lookup, but the mark filtering sets are defined in 

2603 the ``GDEF`` table. This routine builds the subtable containing the mark 

2604 glyph set definitions. 

2605 

2606 Example:: 

2607 

2608 set0 = set("acute", "grave") 

2609 set1 = set("caron", "grave") 

2610 

2611 markglyphsets = buildMarkGlyphSetsDef([set0, set1], font.getReverseGlyphMap()) 

2612 

2613 Args: 

2614 

2615 markSets: A list of sets of glyphnames. 

2616 glyphMap: a glyph name to ID map, typically returned from 

2617 ``font.getReverseGlyphMap()``. 

2618 

2619 Returns 

2620 An ``otTables.MarkGlyphSetsDef`` object. 

2621 """ 

2622 if not markSets: 

2623 return None 

2624 self = ot.MarkGlyphSetsDef() 

2625 self.MarkSetTableFormat = 1 

2626 self.Coverage = [buildCoverage(m, glyphMap) for m in markSets] 

2627 self.MarkSetCount = len(self.Coverage) 

2628 return self 

2629 

2630 

2631class ClassDefBuilder(object): 

2632 """Helper for building ClassDef tables.""" 

2633 

2634 def __init__(self, useClass0): 

2635 self.classes_ = set() 

2636 self.glyphs_ = {} 

2637 self.useClass0_ = useClass0 

2638 

2639 def canAdd(self, glyphs): 

2640 if isinstance(glyphs, (set, frozenset)): 

2641 glyphs = sorted(glyphs) 

2642 glyphs = tuple(glyphs) 

2643 if glyphs in self.classes_: 

2644 return True 

2645 for glyph in glyphs: 

2646 if glyph in self.glyphs_: 

2647 return False 

2648 return True 

2649 

2650 def add(self, glyphs): 

2651 if isinstance(glyphs, (set, frozenset)): 

2652 glyphs = sorted(glyphs) 

2653 glyphs = tuple(glyphs) 

2654 if glyphs in self.classes_: 

2655 return 

2656 self.classes_.add(glyphs) 

2657 for glyph in glyphs: 

2658 if glyph in self.glyphs_: 

2659 raise OpenTypeLibError( 

2660 f"Glyph {glyph} is already present in class.", None 

2661 ) 

2662 self.glyphs_[glyph] = glyphs 

2663 

2664 def classes(self): 

2665 # In ClassDef1 tables, class id #0 does not need to be encoded 

2666 # because zero is the default. Therefore, we use id #0 for the 

2667 # glyph class that has the largest number of members. However, 

2668 # in other tables than ClassDef1, 0 means "every other glyph" 

2669 # so we should not use that ID for any real glyph classes; 

2670 # we implement this by inserting an empty set at position 0. 

2671 # 

2672 # TODO: Instead of counting the number of glyphs in each class, 

2673 # we should determine the encoded size. If the glyphs in a large 

2674 # class form a contiguous range, the encoding is actually quite 

2675 # compact, whereas a non-contiguous set might need a lot of bytes 

2676 # in the output file. We don't get this right with the key below. 

2677 result = sorted(self.classes_, key=lambda s: (-len(s), s)) 

2678 if not self.useClass0_: 

2679 result.insert(0, frozenset()) 

2680 return result 

2681 

2682 def build(self): 

2683 glyphClasses = {} 

2684 for classID, glyphs in enumerate(self.classes()): 

2685 if classID == 0: 

2686 continue 

2687 for glyph in glyphs: 

2688 glyphClasses[glyph] = classID 

2689 classDef = ot.ClassDef() 

2690 classDef.classDefs = glyphClasses 

2691 return classDef 

2692 

2693 

2694AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16) 

2695AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16) 

2696 

2697 

2698def buildStatTable( 

2699 ttFont, axes, locations=None, elidedFallbackName=2, windowsNames=True, macNames=True 

2700): 

2701 """Add a 'STAT' table to 'ttFont'. 

2702 

2703 'axes' is a list of dictionaries describing axes and their 

2704 values. 

2705 

2706 Example:: 

2707 

2708 axes = [ 

2709 dict( 

2710 tag="wght", 

2711 name="Weight", 

2712 ordering=0, # optional 

2713 values=[ 

2714 dict(value=100, name='Thin'), 

2715 dict(value=300, name='Light'), 

2716 dict(value=400, name='Regular', flags=0x2), 

2717 dict(value=900, name='Black'), 

2718 ], 

2719 ) 

2720 ] 

2721 

2722 Each axis dict must have 'tag' and 'name' items. 'tag' maps 

2723 to the 'AxisTag' field. 'name' can be a name ID (int), a string, 

2724 or a dictionary containing multilingual names (see the 

2725 addMultilingualName() name table method), and will translate to 

2726 the AxisNameID field. 

2727 

2728 An axis dict may contain an 'ordering' item that maps to the 

2729 AxisOrdering field. If omitted, the order of the axes list is 

2730 used to calculate AxisOrdering fields. 

2731 

2732 The axis dict may contain a 'values' item, which is a list of 

2733 dictionaries describing AxisValue records belonging to this axis. 

2734 

2735 Each value dict must have a 'name' item, which can be a name ID 

2736 (int), a string, or a dictionary containing multilingual names, 

2737 like the axis name. It translates to the ValueNameID field. 

2738 

2739 Optionally the value dict can contain a 'flags' item. It maps to 

2740 the AxisValue Flags field, and will be 0 when omitted. 

2741 

2742 The format of the AxisValue is determined by the remaining contents 

2743 of the value dictionary: 

2744 

2745 If the value dict contains a 'value' item, an AxisValue record 

2746 Format 1 is created. If in addition to the 'value' item it contains 

2747 a 'linkedValue' item, an AxisValue record Format 3 is built. 

2748 

2749 If the value dict contains a 'nominalValue' item, an AxisValue 

2750 record Format 2 is built. Optionally it may contain 'rangeMinValue' 

2751 and 'rangeMaxValue' items. These map to -Infinity and +Infinity 

2752 respectively if omitted. 

2753 

2754 You cannot specify Format 4 AxisValue tables this way, as they are 

2755 not tied to a single axis, and specify a name for a location that 

2756 is defined by multiple axes values. Instead, you need to supply the 

2757 'locations' argument. 

2758 

2759 The optional 'locations' argument specifies AxisValue Format 4 

2760 tables. It should be a list of dicts, where each dict has a 'name' 

2761 item, which works just like the value dicts above, an optional 

2762 'flags' item (defaulting to 0x0), and a 'location' dict. A 

2763 location dict key is an axis tag, and the associated value is the 

2764 location on the specified axis. They map to the AxisIndex and Value 

2765 fields of the AxisValueRecord. 

2766 

2767 Example:: 

2768 

2769 locations = [ 

2770 dict(name='Regular ABCD', location=dict(wght=300, ABCD=100)), 

2771 dict(name='Bold ABCD XYZ', location=dict(wght=600, ABCD=200)), 

2772 ] 

2773 

2774 The optional 'elidedFallbackName' argument can be a name ID (int), 

2775 a string, a dictionary containing multilingual names, or a list of 

2776 STATNameStatements. It translates to the ElidedFallbackNameID field. 

2777 

2778 The 'ttFont' argument must be a TTFont instance that already has a 

2779 'name' table. If a 'STAT' table already exists, it will be 

2780 overwritten by the newly created one. 

2781 """ 

2782 ttFont["STAT"] = ttLib.newTable("STAT") 

2783 statTable = ttFont["STAT"].table = ot.STAT() 

2784 nameTable = ttFont["name"] 

2785 statTable.ElidedFallbackNameID = _addName( 

2786 nameTable, elidedFallbackName, windows=windowsNames, mac=macNames 

2787 ) 

2788 

2789 # 'locations' contains data for AxisValue Format 4 

2790 axisRecords, axisValues = _buildAxisRecords( 

2791 axes, nameTable, windowsNames=windowsNames, macNames=macNames 

2792 ) 

2793 if not locations: 

2794 statTable.Version = 0x00010001 

2795 else: 

2796 # We'll be adding Format 4 AxisValue records, which 

2797 # requires a higher table version 

2798 statTable.Version = 0x00010002 

2799 multiAxisValues = _buildAxisValuesFormat4( 

2800 locations, axes, nameTable, windowsNames=windowsNames, macNames=macNames 

2801 ) 

2802 axisValues = multiAxisValues + axisValues 

2803 nameTable.names.sort() 

2804 

2805 # Store AxisRecords 

2806 axisRecordArray = ot.AxisRecordArray() 

2807 axisRecordArray.Axis = axisRecords 

2808 # XXX these should not be hard-coded but computed automatically 

2809 statTable.DesignAxisRecordSize = 8 

2810 statTable.DesignAxisRecord = axisRecordArray 

2811 statTable.DesignAxisCount = len(axisRecords) 

2812 

2813 statTable.AxisValueCount = 0 

2814 statTable.AxisValueArray = None 

2815 if axisValues: 

2816 # Store AxisValueRecords 

2817 axisValueArray = ot.AxisValueArray() 

2818 axisValueArray.AxisValue = axisValues 

2819 statTable.AxisValueArray = axisValueArray 

2820 statTable.AxisValueCount = len(axisValues) 

2821 

2822 

2823def _buildAxisRecords(axes, nameTable, windowsNames=True, macNames=True): 

2824 axisRecords = [] 

2825 axisValues = [] 

2826 for axisRecordIndex, axisDict in enumerate(axes): 

2827 axis = ot.AxisRecord() 

2828 axis.AxisTag = axisDict["tag"] 

2829 axis.AxisNameID = _addName( 

2830 nameTable, axisDict["name"], 256, windows=windowsNames, mac=macNames 

2831 ) 

2832 axis.AxisOrdering = axisDict.get("ordering", axisRecordIndex) 

2833 axisRecords.append(axis) 

2834 

2835 for axisVal in axisDict.get("values", ()): 

2836 axisValRec = ot.AxisValue() 

2837 axisValRec.AxisIndex = axisRecordIndex 

2838 axisValRec.Flags = axisVal.get("flags", 0) 

2839 axisValRec.ValueNameID = _addName( 

2840 nameTable, axisVal["name"], windows=windowsNames, mac=macNames 

2841 ) 

2842 

2843 if "value" in axisVal: 

2844 axisValRec.Value = axisVal["value"] 

2845 if "linkedValue" in axisVal: 

2846 axisValRec.Format = 3 

2847 axisValRec.LinkedValue = axisVal["linkedValue"] 

2848 else: 

2849 axisValRec.Format = 1 

2850 elif "nominalValue" in axisVal: 

2851 axisValRec.Format = 2 

2852 axisValRec.NominalValue = axisVal["nominalValue"] 

2853 axisValRec.RangeMinValue = axisVal.get( 

2854 "rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY 

2855 ) 

2856 axisValRec.RangeMaxValue = axisVal.get( 

2857 "rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY 

2858 ) 

2859 else: 

2860 raise ValueError("Can't determine format for AxisValue") 

2861 

2862 axisValues.append(axisValRec) 

2863 return axisRecords, axisValues 

2864 

2865 

2866def _buildAxisValuesFormat4( 

2867 locations, axes, nameTable, windowsNames=True, macNames=True 

2868): 

2869 axisTagToIndex = {} 

2870 for axisRecordIndex, axisDict in enumerate(axes): 

2871 axisTagToIndex[axisDict["tag"]] = axisRecordIndex 

2872 

2873 axisValues = [] 

2874 for axisLocationDict in locations: 

2875 axisValRec = ot.AxisValue() 

2876 axisValRec.Format = 4 

2877 axisValRec.ValueNameID = _addName( 

2878 nameTable, axisLocationDict["name"], windows=windowsNames, mac=macNames 

2879 ) 

2880 axisValRec.Flags = axisLocationDict.get("flags", 0) 

2881 axisValueRecords = [] 

2882 for tag, value in axisLocationDict["location"].items(): 

2883 avr = ot.AxisValueRecord() 

2884 avr.AxisIndex = axisTagToIndex[tag] 

2885 avr.Value = value 

2886 axisValueRecords.append(avr) 

2887 axisValueRecords.sort(key=lambda avr: avr.AxisIndex) 

2888 axisValRec.AxisCount = len(axisValueRecords) 

2889 axisValRec.AxisValueRecord = axisValueRecords 

2890 axisValues.append(axisValRec) 

2891 return axisValues 

2892 

2893 

2894def _addName(nameTable, value, minNameID=0, windows=True, mac=True): 

2895 if isinstance(value, int): 

2896 # Already a nameID 

2897 return value 

2898 if isinstance(value, str): 

2899 names = dict(en=value) 

2900 elif isinstance(value, dict): 

2901 names = value 

2902 elif isinstance(value, list): 

2903 nameID = nameTable._findUnusedNameID() 

2904 for nameRecord in value: 

2905 if isinstance(nameRecord, STATNameStatement): 

2906 nameTable.setName( 

2907 nameRecord.string, 

2908 nameID, 

2909 nameRecord.platformID, 

2910 nameRecord.platEncID, 

2911 nameRecord.langID, 

2912 ) 

2913 else: 

2914 raise TypeError("value must be a list of STATNameStatements") 

2915 return nameID 

2916 else: 

2917 raise TypeError("value must be int, str, dict or list") 

2918 return nameTable.addMultilingualName( 

2919 names, windows=windows, mac=mac, minNameID=minNameID 

2920 )