1from collections import namedtuple, OrderedDict 2import os 3from fontTools.misc.fixedTools import fixedToFloat 4from fontTools.misc.roundTools import otRound 5from fontTools import ttLib 6from fontTools.ttLib.tables import otTables as ot 7from fontTools.ttLib.tables.otBase import ( 8 ValueRecord, 9 valueRecordFormatDict, 10 OTLOffsetOverflowError, 11 OTTableWriter, 12 CountReference, 13) 14from fontTools.ttLib.tables import otBase 15from fontTools.feaLib.ast import STATNameStatement 16from fontTools.otlLib.optimize.gpos import ( 17 _compression_level_from_env, 18 compact_lookup, 19) 20from fontTools.otlLib.error import OpenTypeLibError 21from functools import reduce 22import logging 23import copy 24 25 26log = logging.getLogger(__name__) 27 28 29def buildCoverage(glyphs, glyphMap): 30 """Builds a coverage table. 31 32 Coverage tables (as defined in the `OpenType spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#coverage-table>`__) 33 are used in all OpenType Layout lookups apart from the Extension type, and 34 define the glyphs involved in a layout subtable. This allows shaping engines 35 to compare the glyph stream with the coverage table and quickly determine 36 whether a subtable should be involved in a shaping operation. 37 38 This function takes a list of glyphs and a glyphname-to-ID map, and 39 returns a ``Coverage`` object representing the coverage table. 40 41 Example:: 42 43 glyphMap = font.getReverseGlyphMap() 44 glyphs = [ "A", "B", "C" ] 45 coverage = buildCoverage(glyphs, glyphMap) 46 47 Args: 48 glyphs: a sequence of glyph names. 49 glyphMap: a glyph name to ID map, typically returned from 50 ``font.getReverseGlyphMap()``. 51 52 Returns: 53 An ``otTables.Coverage`` object or ``None`` if there are no glyphs 54 supplied. 55 """ 56 57 if not glyphs: 58 return None 59 self = ot.Coverage() 60 try: 61 self.glyphs = sorted(set(glyphs), key=glyphMap.__getitem__) 62 except KeyError as e: 63 raise ValueError(f"Could not find glyph {e} in font") from e 64 65 return self 66 67 68LOOKUP_FLAG_RIGHT_TO_LEFT = 0x0001 69LOOKUP_FLAG_IGNORE_BASE_GLYPHS = 0x0002 70LOOKUP_FLAG_IGNORE_LIGATURES = 0x0004 71LOOKUP_FLAG_IGNORE_MARKS = 0x0008 72LOOKUP_FLAG_USE_MARK_FILTERING_SET = 0x0010 73 74 75def buildLookup(subtables, flags=0, markFilterSet=None): 76 """Turns a collection of rules into a lookup. 77 78 A Lookup (as defined in the `OpenType Spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#lookupTbl>`__) 79 wraps the individual rules in a layout operation (substitution or 80 positioning) in a data structure expressing their overall lookup type - 81 for example, single substitution, mark-to-base attachment, and so on - 82 as well as the lookup flags and any mark filtering sets. You may import 83 the following constants to express lookup flags: 84 85 - ``LOOKUP_FLAG_RIGHT_TO_LEFT`` 86 - ``LOOKUP_FLAG_IGNORE_BASE_GLYPHS`` 87 - ``LOOKUP_FLAG_IGNORE_LIGATURES`` 88 - ``LOOKUP_FLAG_IGNORE_MARKS`` 89 - ``LOOKUP_FLAG_USE_MARK_FILTERING_SET`` 90 91 Args: 92 subtables: A list of layout subtable objects (e.g. 93 ``MultipleSubst``, ``PairPos``, etc.) or ``None``. 94 flags (int): This lookup's flags. 95 markFilterSet: Either ``None`` if no mark filtering set is used, or 96 an integer representing the filtering set to be used for this 97 lookup. If a mark filtering set is provided, 98 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 99 flags. 100 101 Returns: 102 An ``otTables.Lookup`` object or ``None`` if there are no subtables 103 supplied. 104 """ 105 if subtables is None: 106 return None 107 subtables = [st for st in subtables if st is not None] 108 if not subtables: 109 return None 110 assert all( 111 t.LookupType == subtables[0].LookupType for t in subtables 112 ), "all subtables must have the same LookupType; got %s" % repr( 113 [t.LookupType for t in subtables] 114 ) 115 self = ot.Lookup() 116 self.LookupType = subtables[0].LookupType 117 self.LookupFlag = flags 118 self.SubTable = subtables 119 self.SubTableCount = len(self.SubTable) 120 if markFilterSet is not None: 121 self.LookupFlag |= LOOKUP_FLAG_USE_MARK_FILTERING_SET 122 assert isinstance(markFilterSet, int), markFilterSet 123 self.MarkFilteringSet = markFilterSet 124 else: 125 assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, ( 126 "if markFilterSet is None, flags must not set " 127 "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags 128 ) 129 return self 130 131 132class LookupBuilder(object): 133 SUBTABLE_BREAK_ = "SUBTABLE_BREAK" 134 135 def __init__(self, font, location, table, lookup_type): 136 self.font = font 137 self.glyphMap = font.getReverseGlyphMap() 138 self.location = location 139 self.table, self.lookup_type = table, lookup_type 140 self.lookupflag = 0 141 self.markFilterSet = None 142 self.lookup_index = None # assigned when making final tables 143 assert table in ("GPOS", "GSUB") 144 145 def equals(self, other): 146 return ( 147 isinstance(other, self.__class__) 148 and self.table == other.table 149 and self.lookupflag == other.lookupflag 150 and self.markFilterSet == other.markFilterSet 151 ) 152 153 def inferGlyphClasses(self): 154 """Infers glyph glasses for the GDEF table, such as {"cedilla":3}.""" 155 return {} 156 157 def getAlternateGlyphs(self): 158 """Helper for building 'aalt' features.""" 159 return {} 160 161 def buildLookup_(self, subtables): 162 return buildLookup(subtables, self.lookupflag, self.markFilterSet) 163 164 def buildMarkClasses_(self, marks): 165 """{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1} 166 167 Helper for MarkBasePostBuilder, MarkLigPosBuilder, and 168 MarkMarkPosBuilder. Seems to return the same numeric IDs 169 for mark classes as the AFDKO makeotf tool. 170 """ 171 ids = {} 172 for mark in sorted(marks.keys(), key=self.font.getGlyphID): 173 markClassName, _markAnchor = marks[mark] 174 if markClassName not in ids: 175 ids[markClassName] = len(ids) 176 return ids 177 178 def setBacktrackCoverage_(self, prefix, subtable): 179 subtable.BacktrackGlyphCount = len(prefix) 180 subtable.BacktrackCoverage = [] 181 for p in reversed(prefix): 182 coverage = buildCoverage(p, self.glyphMap) 183 subtable.BacktrackCoverage.append(coverage) 184 185 def setLookAheadCoverage_(self, suffix, subtable): 186 subtable.LookAheadGlyphCount = len(suffix) 187 subtable.LookAheadCoverage = [] 188 for s in suffix: 189 coverage = buildCoverage(s, self.glyphMap) 190 subtable.LookAheadCoverage.append(coverage) 191 192 def setInputCoverage_(self, glyphs, subtable): 193 subtable.InputGlyphCount = len(glyphs) 194 subtable.InputCoverage = [] 195 for g in glyphs: 196 coverage = buildCoverage(g, self.glyphMap) 197 subtable.InputCoverage.append(coverage) 198 199 def setCoverage_(self, glyphs, subtable): 200 subtable.GlyphCount = len(glyphs) 201 subtable.Coverage = [] 202 for g in glyphs: 203 coverage = buildCoverage(g, self.glyphMap) 204 subtable.Coverage.append(coverage) 205 206 def build_subst_subtables(self, mapping, klass): 207 substitutions = [{}] 208 for key in mapping: 209 if key[0] == self.SUBTABLE_BREAK_: 210 substitutions.append({}) 211 else: 212 substitutions[-1][key] = mapping[key] 213 subtables = [klass(s) for s in substitutions] 214 return subtables 215 216 def add_subtable_break(self, location): 217 """Add an explicit subtable break. 218 219 Args: 220 location: A string or tuple representing the location in the 221 original source which produced this break, or ``None`` if 222 no location is provided. 223 """ 224 log.warning( 225 OpenTypeLibError( 226 'unsupported "subtable" statement for lookup type', location 227 ) 228 ) 229 230 231class AlternateSubstBuilder(LookupBuilder): 232 """Builds an Alternate Substitution (GSUB3) lookup. 233 234 Users are expected to manually add alternate glyph substitutions to 235 the ``alternates`` attribute after the object has been initialized, 236 e.g.:: 237 238 builder.alternates["A"] = ["A.alt1", "A.alt2"] 239 240 Attributes: 241 font (``fontTools.TTLib.TTFont``): A font object. 242 location: A string or tuple representing the location in the original 243 source which produced this lookup. 244 alternates: An ordered dictionary of alternates, mapping glyph names 245 to a list of names of alternates. 246 lookupflag (int): The lookup's flag 247 markFilterSet: Either ``None`` if no mark filtering set is used, or 248 an integer representing the filtering set to be used for this 249 lookup. If a mark filtering set is provided, 250 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 251 flags. 252 """ 253 254 def __init__(self, font, location): 255 LookupBuilder.__init__(self, font, location, "GSUB", 3) 256 self.alternates = OrderedDict() 257 258 def equals(self, other): 259 return LookupBuilder.equals(self, other) and self.alternates == other.alternates 260 261 def build(self): 262 """Build the lookup. 263 264 Returns: 265 An ``otTables.Lookup`` object representing the alternate 266 substitution lookup. 267 """ 268 subtables = self.build_subst_subtables( 269 self.alternates, buildAlternateSubstSubtable 270 ) 271 return self.buildLookup_(subtables) 272 273 def getAlternateGlyphs(self): 274 return self.alternates 275 276 def add_subtable_break(self, location): 277 self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ 278 279 280class ChainContextualRule( 281 namedtuple("ChainContextualRule", ["prefix", "glyphs", "suffix", "lookups"]) 282): 283 @property 284 def is_subtable_break(self): 285 return self.prefix == LookupBuilder.SUBTABLE_BREAK_ 286 287 288class ChainContextualRuleset: 289 def __init__(self): 290 self.rules = [] 291 292 def addRule(self, rule): 293 self.rules.append(rule) 294 295 @property 296 def hasPrefixOrSuffix(self): 297 # Do we have any prefixes/suffixes? If this is False for all 298 # rulesets, we can express the whole lookup as GPOS5/GSUB7. 299 for rule in self.rules: 300 if len(rule.prefix) > 0 or len(rule.suffix) > 0: 301 return True 302 return False 303 304 @property 305 def hasAnyGlyphClasses(self): 306 # Do we use glyph classes anywhere in the rules? If this is False 307 # we can express this subtable as a Format 1. 308 for rule in self.rules: 309 for coverage in (rule.prefix, rule.glyphs, rule.suffix): 310 if any(len(x) > 1 for x in coverage): 311 return True 312 return False 313 314 def format2ClassDefs(self): 315 PREFIX, GLYPHS, SUFFIX = 0, 1, 2 316 classDefBuilders = [] 317 for ix in [PREFIX, GLYPHS, SUFFIX]: 318 context = [] 319 for r in self.rules: 320 context.append(r[ix]) 321 classes = self._classBuilderForContext(context) 322 if not classes: 323 return None 324 classDefBuilders.append(classes) 325 return classDefBuilders 326 327 def _classBuilderForContext(self, context): 328 classdefbuilder = ClassDefBuilder(useClass0=False) 329 for position in context: 330 for glyphset in position: 331 glyphs = set(glyphset) 332 if not classdefbuilder.canAdd(glyphs): 333 return None 334 classdefbuilder.add(glyphs) 335 return classdefbuilder 336 337 338class ChainContextualBuilder(LookupBuilder): 339 def equals(self, other): 340 return LookupBuilder.equals(self, other) and self.rules == other.rules 341 342 def rulesets(self): 343 # Return a list of ChainContextRuleset objects, taking explicit 344 # subtable breaks into account 345 ruleset = [ChainContextualRuleset()] 346 for rule in self.rules: 347 if rule.is_subtable_break: 348 ruleset.append(ChainContextualRuleset()) 349 continue 350 ruleset[-1].addRule(rule) 351 # Squish any empty subtables 352 return [x for x in ruleset if len(x.rules) > 0] 353 354 def getCompiledSize_(self, subtables): 355 if not subtables: 356 return 0 357 # We need to make a copy here because compiling 358 # modifies the subtable (finalizing formats etc.) 359 table = self.buildLookup_(copy.deepcopy(subtables)) 360 w = OTTableWriter() 361 table.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 candidates_by_size = [] 414 for i in [1, 2, 3]: 415 if candidates[i]: 416 try: 417 size = self.getCompiledSize_(candidates[i]) 418 except OTLOffsetOverflowError as e: 419 log.warning( 420 "Contextual format %i at %s overflowed (%s)" 421 % (i, str(self.location), e) 422 ) 423 else: 424 candidates_by_size.append((size, candidates[i])) 425 426 if not candidates_by_size: 427 raise OpenTypeLibError("All candidates overflowed", self.location) 428 429 _min_size, winner = min(candidates_by_size, key=lambda x: x[0]) 430 subtables.extend(winner) 431 432 # If we are not chaining, lookup type will be automatically fixed by 433 # buildLookup_ 434 return self.buildLookup_(subtables) 435 436 def buildFormat1Subtable(self, ruleset, chaining=True): 437 st = self.newSubtable_(chaining=chaining) 438 st.Format = 1 439 st.populateDefaults() 440 coverage = set() 441 rulesetsByFirstGlyph = {} 442 ruleAttr = self.ruleAttr_(format=1, chaining=chaining) 443 444 for rule in ruleset.rules: 445 ruleAsSubtable = self.newRule_(format=1, chaining=chaining) 446 447 if chaining: 448 ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix) 449 ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix) 450 ruleAsSubtable.Backtrack = [list(x)[0] for x in reversed(rule.prefix)] 451 ruleAsSubtable.LookAhead = [list(x)[0] for x in rule.suffix] 452 453 ruleAsSubtable.InputGlyphCount = len(rule.glyphs) 454 else: 455 ruleAsSubtable.GlyphCount = len(rule.glyphs) 456 457 ruleAsSubtable.Input = [list(x)[0] for x in rule.glyphs[1:]] 458 459 self.buildLookupList(rule, ruleAsSubtable) 460 461 firstGlyph = list(rule.glyphs[0])[0] 462 if firstGlyph not in rulesetsByFirstGlyph: 463 coverage.add(firstGlyph) 464 rulesetsByFirstGlyph[firstGlyph] = [] 465 rulesetsByFirstGlyph[firstGlyph].append(ruleAsSubtable) 466 467 st.Coverage = buildCoverage(coverage, self.glyphMap) 468 ruleSets = [] 469 for g in st.Coverage.glyphs: 470 ruleSet = self.newRuleSet_(format=1, chaining=chaining) 471 setattr(ruleSet, ruleAttr, rulesetsByFirstGlyph[g]) 472 setattr(ruleSet, f"{ruleAttr}Count", len(rulesetsByFirstGlyph[g])) 473 ruleSets.append(ruleSet) 474 475 setattr(st, self.ruleSetAttr_(format=1, chaining=chaining), ruleSets) 476 setattr( 477 st, self.ruleSetAttr_(format=1, chaining=chaining) + "Count", len(ruleSets) 478 ) 479 480 return st 481 482 def buildFormat2Subtable(self, ruleset, classdefs, chaining=True): 483 st = self.newSubtable_(chaining=chaining) 484 st.Format = 2 485 st.populateDefaults() 486 487 if chaining: 488 ( 489 st.BacktrackClassDef, 490 st.InputClassDef, 491 st.LookAheadClassDef, 492 ) = [c.build() for c in classdefs] 493 else: 494 st.ClassDef = classdefs[1].build() 495 496 inClasses = classdefs[1].classes() 497 498 classSets = [] 499 for _ in inClasses: 500 classSet = self.newRuleSet_(format=2, chaining=chaining) 501 classSets.append(classSet) 502 503 coverage = set() 504 classRuleAttr = self.ruleAttr_(format=2, chaining=chaining) 505 506 for rule in ruleset.rules: 507 ruleAsSubtable = self.newRule_(format=2, chaining=chaining) 508 if chaining: 509 ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix) 510 ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix) 511 # The glyphs in the rule may be list, tuple, odict_keys... 512 # Order is not important anyway because they are guaranteed 513 # to be members of the same class. 514 ruleAsSubtable.Backtrack = [ 515 st.BacktrackClassDef.classDefs[list(x)[0]] 516 for x in reversed(rule.prefix) 517 ] 518 ruleAsSubtable.LookAhead = [ 519 st.LookAheadClassDef.classDefs[list(x)[0]] for x in rule.suffix 520 ] 521 522 ruleAsSubtable.InputGlyphCount = len(rule.glyphs) 523 ruleAsSubtable.Input = [ 524 st.InputClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:] 525 ] 526 setForThisRule = classSets[ 527 st.InputClassDef.classDefs[list(rule.glyphs[0])[0]] 528 ] 529 else: 530 ruleAsSubtable.GlyphCount = len(rule.glyphs) 531 ruleAsSubtable.Class = [ # The spec calls this InputSequence 532 st.ClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:] 533 ] 534 setForThisRule = classSets[ 535 st.ClassDef.classDefs[list(rule.glyphs[0])[0]] 536 ] 537 538 self.buildLookupList(rule, ruleAsSubtable) 539 coverage |= set(rule.glyphs[0]) 540 541 getattr(setForThisRule, classRuleAttr).append(ruleAsSubtable) 542 setattr( 543 setForThisRule, 544 f"{classRuleAttr}Count", 545 getattr(setForThisRule, f"{classRuleAttr}Count") + 1, 546 ) 547 setattr(st, self.ruleSetAttr_(format=2, chaining=chaining), classSets) 548 setattr( 549 st, self.ruleSetAttr_(format=2, chaining=chaining) + "Count", len(classSets) 550 ) 551 st.Coverage = buildCoverage(coverage, self.glyphMap) 552 return st 553 554 def buildFormat3Subtable(self, rule, chaining=True): 555 st = self.newSubtable_(chaining=chaining) 556 st.Format = 3 557 if chaining: 558 self.setBacktrackCoverage_(rule.prefix, st) 559 self.setLookAheadCoverage_(rule.suffix, st) 560 self.setInputCoverage_(rule.glyphs, st) 561 else: 562 self.setCoverage_(rule.glyphs, st) 563 self.buildLookupList(rule, st) 564 return st 565 566 def buildLookupList(self, rule, st): 567 for sequenceIndex, lookupList in enumerate(rule.lookups): 568 if lookupList is not None: 569 if not isinstance(lookupList, list): 570 # Can happen with synthesised lookups 571 lookupList = [lookupList] 572 for l in lookupList: 573 if l.lookup_index is None: 574 if isinstance(self, ChainContextPosBuilder): 575 other = "substitution" 576 else: 577 other = "positioning" 578 raise OpenTypeLibError( 579 "Missing index of the specified " 580 f"lookup, might be a {other} lookup", 581 self.location, 582 ) 583 rec = self.newLookupRecord_(st) 584 rec.SequenceIndex = sequenceIndex 585 rec.LookupListIndex = l.lookup_index 586 587 def add_subtable_break(self, location): 588 self.rules.append( 589 ChainContextualRule( 590 self.SUBTABLE_BREAK_, 591 self.SUBTABLE_BREAK_, 592 self.SUBTABLE_BREAK_, 593 [self.SUBTABLE_BREAK_], 594 ) 595 ) 596 597 def newSubtable_(self, chaining=True): 598 subtablename = f"Context{self.subtable_type}" 599 if chaining: 600 subtablename = "Chain" + subtablename 601 st = getattr(ot, subtablename)() # ot.ChainContextPos()/ot.ChainSubst()/etc. 602 setattr(st, f"{self.subtable_type}Count", 0) 603 setattr(st, f"{self.subtable_type}LookupRecord", []) 604 return st 605 606 # Format 1 and format 2 GSUB5/GSUB6/GPOS7/GPOS8 rulesets and rules form a family: 607 # 608 # format 1 ruleset format 1 rule format 2 ruleset format 2 rule 609 # GSUB5 SubRuleSet SubRule SubClassSet SubClassRule 610 # GSUB6 ChainSubRuleSet ChainSubRule ChainSubClassSet ChainSubClassRule 611 # GPOS7 PosRuleSet PosRule PosClassSet PosClassRule 612 # GPOS8 ChainPosRuleSet ChainPosRule ChainPosClassSet ChainPosClassRule 613 # 614 # The following functions generate the attribute names and subtables according 615 # to this naming convention. 616 def ruleSetAttr_(self, format=1, chaining=True): 617 if format == 1: 618 formatType = "Rule" 619 elif format == 2: 620 formatType = "Class" 621 else: 622 raise AssertionError(formatType) 623 subtablename = f"{self.subtable_type[0:3]}{formatType}Set" # Sub, not Subst. 624 if chaining: 625 subtablename = "Chain" + subtablename 626 return subtablename 627 628 def ruleAttr_(self, format=1, chaining=True): 629 if format == 1: 630 formatType = "" 631 elif format == 2: 632 formatType = "Class" 633 else: 634 raise AssertionError(formatType) 635 subtablename = f"{self.subtable_type[0:3]}{formatType}Rule" # Sub, not Subst. 636 if chaining: 637 subtablename = "Chain" + subtablename 638 return subtablename 639 640 def newRuleSet_(self, format=1, chaining=True): 641 st = getattr( 642 ot, self.ruleSetAttr_(format, chaining) 643 )() # ot.ChainPosRuleSet()/ot.SubRuleSet()/etc. 644 st.populateDefaults() 645 return st 646 647 def newRule_(self, format=1, chaining=True): 648 st = getattr( 649 ot, self.ruleAttr_(format, chaining) 650 )() # ot.ChainPosClassRule()/ot.SubClassRule()/etc. 651 st.populateDefaults() 652 return st 653 654 def attachSubtableWithCount_( 655 self, st, subtable_name, count_name, existing=None, index=None, chaining=False 656 ): 657 if chaining: 658 subtable_name = "Chain" + subtable_name 659 count_name = "Chain" + count_name 660 661 if not hasattr(st, count_name): 662 setattr(st, count_name, 0) 663 setattr(st, subtable_name, []) 664 665 if existing: 666 new_subtable = existing 667 else: 668 # Create a new, empty subtable from otTables 669 new_subtable = getattr(ot, subtable_name)() 670 671 setattr(st, count_name, getattr(st, count_name) + 1) 672 673 if index: 674 getattr(st, subtable_name).insert(index, new_subtable) 675 else: 676 getattr(st, subtable_name).append(new_subtable) 677 678 return new_subtable 679 680 def newLookupRecord_(self, st): 681 return self.attachSubtableWithCount_( 682 st, 683 f"{self.subtable_type}LookupRecord", 684 f"{self.subtable_type}Count", 685 chaining=False, 686 ) # Oddly, it isn't ChainSubstLookupRecord 687 688 689class ChainContextPosBuilder(ChainContextualBuilder): 690 """Builds a Chained Contextual Positioning (GPOS8) lookup. 691 692 Users are expected to manually add rules to the ``rules`` attribute after 693 the object has been initialized, e.g.:: 694 695 # pos [A B] [C D] x' lookup lu1 y' z' lookup lu2 E; 696 697 prefix = [ ["A", "B"], ["C", "D"] ] 698 suffix = [ ["E"] ] 699 glyphs = [ ["x"], ["y"], ["z"] ] 700 lookups = [ [lu1], None, [lu2] ] 701 builder.rules.append( (prefix, glyphs, suffix, lookups) ) 702 703 Attributes: 704 font (``fontTools.TTLib.TTFont``): A font object. 705 location: A string or tuple representing the location in the original 706 source which produced this lookup. 707 rules: A list of tuples representing the rules in this lookup. 708 lookupflag (int): The lookup's flag 709 markFilterSet: Either ``None`` if no mark filtering set is used, or 710 an integer representing the filtering set to be used for this 711 lookup. If a mark filtering set is provided, 712 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 713 flags. 714 """ 715 716 def __init__(self, font, location): 717 LookupBuilder.__init__(self, font, location, "GPOS", 8) 718 self.rules = [] 719 self.subtable_type = "Pos" 720 721 def find_chainable_single_pos(self, lookups, glyphs, value): 722 """Helper for add_single_pos_chained_()""" 723 res = None 724 for lookup in lookups[::-1]: 725 if lookup == self.SUBTABLE_BREAK_: 726 return res 727 if isinstance(lookup, SinglePosBuilder) and all( 728 lookup.can_add(glyph, value) for glyph in glyphs 729 ): 730 res = lookup 731 return res 732 733 734class ChainContextSubstBuilder(ChainContextualBuilder): 735 """Builds a Chained Contextual Substitution (GSUB6) lookup. 736 737 Users are expected to manually add rules to the ``rules`` attribute after 738 the object has been initialized, e.g.:: 739 740 # sub [A B] [C D] x' lookup lu1 y' z' lookup lu2 E; 741 742 prefix = [ ["A", "B"], ["C", "D"] ] 743 suffix = [ ["E"] ] 744 glyphs = [ ["x"], ["y"], ["z"] ] 745 lookups = [ [lu1], None, [lu2] ] 746 builder.rules.append( (prefix, glyphs, suffix, lookups) ) 747 748 Attributes: 749 font (``fontTools.TTLib.TTFont``): A font object. 750 location: A string or tuple representing the location in the original 751 source which produced this lookup. 752 rules: A list of tuples representing the rules in this lookup. 753 lookupflag (int): The lookup's flag 754 markFilterSet: Either ``None`` if no mark filtering set is used, or 755 an integer representing the filtering set to be used for this 756 lookup. If a mark filtering set is provided, 757 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 758 flags. 759 """ 760 761 def __init__(self, font, location): 762 LookupBuilder.__init__(self, font, location, "GSUB", 6) 763 self.rules = [] # (prefix, input, suffix, lookups) 764 self.subtable_type = "Subst" 765 766 def getAlternateGlyphs(self): 767 result = {} 768 for rule in self.rules: 769 if rule.is_subtable_break: 770 continue 771 for lookups in rule.lookups: 772 if not isinstance(lookups, list): 773 lookups = [lookups] 774 for lookup in lookups: 775 if lookup is not None: 776 alts = lookup.getAlternateGlyphs() 777 for glyph, replacements in alts.items(): 778 alts_for_glyph = result.setdefault(glyph, []) 779 alts_for_glyph.extend( 780 g for g in replacements if g not in alts_for_glyph 781 ) 782 return result 783 784 def find_chainable_single_subst(self, mapping): 785 """Helper for add_single_subst_chained_()""" 786 res = None 787 for rule in self.rules[::-1]: 788 if rule.is_subtable_break: 789 return res 790 for sub in rule.lookups: 791 if isinstance(sub, SingleSubstBuilder) and not any( 792 g in mapping and mapping[g] != sub.mapping[g] for g in sub.mapping 793 ): 794 res = sub 795 return res 796 797 798class LigatureSubstBuilder(LookupBuilder): 799 """Builds a Ligature Substitution (GSUB4) lookup. 800 801 Users are expected to manually add ligatures to the ``ligatures`` 802 attribute after the object has been initialized, e.g.:: 803 804 # sub f i by f_i; 805 builder.ligatures[("f","f","i")] = "f_f_i" 806 807 Attributes: 808 font (``fontTools.TTLib.TTFont``): A font object. 809 location: A string or tuple representing the location in the original 810 source which produced this lookup. 811 ligatures: An ordered dictionary mapping a tuple of glyph names to the 812 ligature glyphname. 813 lookupflag (int): The lookup's flag 814 markFilterSet: Either ``None`` if no mark filtering set is used, or 815 an integer representing the filtering set to be used for this 816 lookup. If a mark filtering set is provided, 817 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 818 flags. 819 """ 820 821 def __init__(self, font, location): 822 LookupBuilder.__init__(self, font, location, "GSUB", 4) 823 self.ligatures = OrderedDict() # {('f','f','i'): 'f_f_i'} 824 825 def equals(self, other): 826 return LookupBuilder.equals(self, other) and self.ligatures == other.ligatures 827 828 def build(self): 829 """Build the lookup. 830 831 Returns: 832 An ``otTables.Lookup`` object representing the ligature 833 substitution lookup. 834 """ 835 subtables = self.build_subst_subtables( 836 self.ligatures, buildLigatureSubstSubtable 837 ) 838 return self.buildLookup_(subtables) 839 840 def add_subtable_break(self, location): 841 self.ligatures[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ 842 843 844class MultipleSubstBuilder(LookupBuilder): 845 """Builds a Multiple Substitution (GSUB2) lookup. 846 847 Users are expected to manually add substitutions to the ``mapping`` 848 attribute after the object has been initialized, e.g.:: 849 850 # sub uni06C0 by uni06D5.fina hamza.above; 851 builder.mapping["uni06C0"] = [ "uni06D5.fina", "hamza.above"] 852 853 Attributes: 854 font (``fontTools.TTLib.TTFont``): A font object. 855 location: A string or tuple representing the location in the original 856 source which produced this lookup. 857 mapping: An ordered dictionary mapping a glyph name to a list of 858 substituted glyph names. 859 lookupflag (int): The lookup's flag 860 markFilterSet: Either ``None`` if no mark filtering set is used, or 861 an integer representing the filtering set to be used for this 862 lookup. If a mark filtering set is provided, 863 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 864 flags. 865 """ 866 867 def __init__(self, font, location): 868 LookupBuilder.__init__(self, font, location, "GSUB", 2) 869 self.mapping = OrderedDict() 870 871 def equals(self, other): 872 return LookupBuilder.equals(self, other) and self.mapping == other.mapping 873 874 def build(self): 875 subtables = self.build_subst_subtables(self.mapping, buildMultipleSubstSubtable) 876 return self.buildLookup_(subtables) 877 878 def add_subtable_break(self, location): 879 self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ 880 881 882class CursivePosBuilder(LookupBuilder): 883 """Builds a Cursive Positioning (GPOS3) lookup. 884 885 Attributes: 886 font (``fontTools.TTLib.TTFont``): A font object. 887 location: A string or tuple representing the location in the original 888 source which produced this lookup. 889 attachments: An ordered dictionary mapping a glyph name to a two-element 890 tuple of ``otTables.Anchor`` objects. 891 lookupflag (int): The lookup's flag 892 markFilterSet: Either ``None`` if no mark filtering set is used, or 893 an integer representing the filtering set to be used for this 894 lookup. If a mark filtering set is provided, 895 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 896 flags. 897 """ 898 899 def __init__(self, font, location): 900 LookupBuilder.__init__(self, font, location, "GPOS", 3) 901 self.attachments = {} 902 903 def equals(self, other): 904 return ( 905 LookupBuilder.equals(self, other) and self.attachments == other.attachments 906 ) 907 908 def add_attachment(self, location, glyphs, entryAnchor, exitAnchor): 909 """Adds attachment information to the cursive positioning lookup. 910 911 Args: 912 location: A string or tuple representing the location in the 913 original source which produced this lookup. (Unused.) 914 glyphs: A list of glyph names sharing these entry and exit 915 anchor locations. 916 entryAnchor: A ``otTables.Anchor`` object representing the 917 entry anchor, or ``None`` if no entry anchor is present. 918 exitAnchor: A ``otTables.Anchor`` object representing the 919 exit anchor, or ``None`` if no exit anchor is present. 920 """ 921 for glyph in glyphs: 922 self.attachments[glyph] = (entryAnchor, exitAnchor) 923 924 def build(self): 925 """Build the lookup. 926 927 Returns: 928 An ``otTables.Lookup`` object representing the cursive 929 positioning lookup. 930 """ 931 st = buildCursivePosSubtable(self.attachments, self.glyphMap) 932 return self.buildLookup_([st]) 933 934 935class MarkBasePosBuilder(LookupBuilder): 936 """Builds a Mark-To-Base Positioning (GPOS4) lookup. 937 938 Users are expected to manually add marks and bases to the ``marks`` 939 and ``bases`` attributes after the object has been initialized, e.g.:: 940 941 builder.marks["acute"] = (0, a1) 942 builder.marks["grave"] = (0, a1) 943 builder.marks["cedilla"] = (1, a2) 944 builder.bases["a"] = {0: a3, 1: a5} 945 builder.bases["b"] = {0: a4, 1: a5} 946 947 Attributes: 948 font (``fontTools.TTLib.TTFont``): A font object. 949 location: A string or tuple representing the location in the original 950 source which produced this lookup. 951 marks: An dictionary mapping a glyph name to a two-element 952 tuple containing a mark class ID and ``otTables.Anchor`` object. 953 bases: An dictionary mapping a glyph name to a dictionary of 954 mark class IDs and ``otTables.Anchor`` object. 955 lookupflag (int): The lookup's flag 956 markFilterSet: Either ``None`` if no mark filtering set is used, or 957 an integer representing the filtering set to be used for this 958 lookup. If a mark filtering set is provided, 959 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 960 flags. 961 """ 962 963 def __init__(self, font, location): 964 LookupBuilder.__init__(self, font, location, "GPOS", 4) 965 self.marks = {} # glyphName -> (markClassName, anchor) 966 self.bases = {} # glyphName -> {markClassName: anchor} 967 968 def equals(self, other): 969 return ( 970 LookupBuilder.equals(self, other) 971 and self.marks == other.marks 972 and self.bases == other.bases 973 ) 974 975 def inferGlyphClasses(self): 976 result = {glyph: 1 for glyph in self.bases} 977 result.update({glyph: 3 for glyph in self.marks}) 978 return result 979 980 def build(self): 981 """Build the lookup. 982 983 Returns: 984 An ``otTables.Lookup`` object representing the mark-to-base 985 positioning lookup. 986 """ 987 markClasses = self.buildMarkClasses_(self.marks) 988 marks = {} 989 for mark, (mc, anchor) in self.marks.items(): 990 if mc not in markClasses: 991 raise ValueError( 992 "Mark class %s not found for mark glyph %s" % (mc, mark) 993 ) 994 marks[mark] = (markClasses[mc], anchor) 995 bases = {} 996 for glyph, anchors in self.bases.items(): 997 bases[glyph] = {} 998 for mc, anchor in anchors.items(): 999 if mc not in markClasses: 1000 raise ValueError( 1001 "Mark class %s not found for base glyph %s" % (mc, glyph) 1002 ) 1003 bases[glyph][markClasses[mc]] = anchor 1004 subtables = buildMarkBasePos(marks, bases, self.glyphMap) 1005 return self.buildLookup_(subtables) 1006 1007 1008class MarkLigPosBuilder(LookupBuilder): 1009 """Builds a Mark-To-Ligature Positioning (GPOS5) lookup. 1010 1011 Users are expected to manually add marks and bases to the ``marks`` 1012 and ``ligatures`` attributes after the object has been initialized, e.g.:: 1013 1014 builder.marks["acute"] = (0, a1) 1015 builder.marks["grave"] = (0, a1) 1016 builder.marks["cedilla"] = (1, a2) 1017 builder.ligatures["f_i"] = [ 1018 { 0: a3, 1: a5 }, # f 1019 { 0: a4, 1: a5 } # i 1020 ] 1021 1022 Attributes: 1023 font (``fontTools.TTLib.TTFont``): A font object. 1024 location: A string or tuple representing the location in the original 1025 source which produced this lookup. 1026 marks: An dictionary mapping a glyph name to a two-element 1027 tuple containing a mark class ID and ``otTables.Anchor`` object. 1028 ligatures: An dictionary mapping a glyph name to an array with one 1029 element for each ligature component. Each array element should be 1030 a dictionary mapping mark class IDs to ``otTables.Anchor`` objects. 1031 lookupflag (int): The lookup's flag 1032 markFilterSet: Either ``None`` if no mark filtering set is used, or 1033 an integer representing the filtering set to be used for this 1034 lookup. If a mark filtering set is provided, 1035 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 1036 flags. 1037 """ 1038 1039 def __init__(self, font, location): 1040 LookupBuilder.__init__(self, font, location, "GPOS", 5) 1041 self.marks = {} # glyphName -> (markClassName, anchor) 1042 self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...] 1043 1044 def equals(self, other): 1045 return ( 1046 LookupBuilder.equals(self, other) 1047 and self.marks == other.marks 1048 and self.ligatures == other.ligatures 1049 ) 1050 1051 def inferGlyphClasses(self): 1052 result = {glyph: 2 for glyph in self.ligatures} 1053 result.update({glyph: 3 for glyph in self.marks}) 1054 return result 1055 1056 def build(self): 1057 """Build the lookup. 1058 1059 Returns: 1060 An ``otTables.Lookup`` object representing the mark-to-ligature 1061 positioning lookup. 1062 """ 1063 markClasses = self.buildMarkClasses_(self.marks) 1064 marks = { 1065 mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() 1066 } 1067 ligs = {} 1068 for lig, components in self.ligatures.items(): 1069 ligs[lig] = [] 1070 for c in components: 1071 ligs[lig].append({markClasses[mc]: a for mc, a in c.items()}) 1072 subtables = buildMarkLigPos(marks, ligs, self.glyphMap) 1073 return self.buildLookup_(subtables) 1074 1075 1076class MarkMarkPosBuilder(LookupBuilder): 1077 """Builds a Mark-To-Mark Positioning (GPOS6) lookup. 1078 1079 Users are expected to manually add marks and bases to the ``marks`` 1080 and ``baseMarks`` attributes after the object has been initialized, e.g.:: 1081 1082 builder.marks["acute"] = (0, a1) 1083 builder.marks["grave"] = (0, a1) 1084 builder.marks["cedilla"] = (1, a2) 1085 builder.baseMarks["acute"] = {0: a3} 1086 1087 Attributes: 1088 font (``fontTools.TTLib.TTFont``): A font object. 1089 location: A string or tuple representing the location in the original 1090 source which produced this lookup. 1091 marks: An dictionary mapping a glyph name to a two-element 1092 tuple containing a mark class ID and ``otTables.Anchor`` object. 1093 baseMarks: An dictionary mapping a glyph name to a dictionary 1094 containing one item: a mark class ID and a ``otTables.Anchor`` object. 1095 lookupflag (int): The lookup's flag 1096 markFilterSet: Either ``None`` if no mark filtering set is used, or 1097 an integer representing the filtering set to be used for this 1098 lookup. If a mark filtering set is provided, 1099 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 1100 flags. 1101 """ 1102 1103 def __init__(self, font, location): 1104 LookupBuilder.__init__(self, font, location, "GPOS", 6) 1105 self.marks = {} # glyphName -> (markClassName, anchor) 1106 self.baseMarks = {} # glyphName -> {markClassName: anchor} 1107 1108 def equals(self, other): 1109 return ( 1110 LookupBuilder.equals(self, other) 1111 and self.marks == other.marks 1112 and self.baseMarks == other.baseMarks 1113 ) 1114 1115 def inferGlyphClasses(self): 1116 result = {glyph: 3 for glyph in self.baseMarks} 1117 result.update({glyph: 3 for glyph in self.marks}) 1118 return result 1119 1120 def build(self): 1121 """Build the lookup. 1122 1123 Returns: 1124 An ``otTables.Lookup`` object representing the mark-to-mark 1125 positioning lookup. 1126 """ 1127 markClasses = self.buildMarkClasses_(self.marks) 1128 markClassList = sorted(markClasses.keys(), key=markClasses.get) 1129 marks = { 1130 mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() 1131 } 1132 1133 st = ot.MarkMarkPos() 1134 st.Format = 1 1135 st.ClassCount = len(markClasses) 1136 st.Mark1Coverage = buildCoverage(marks, self.glyphMap) 1137 st.Mark2Coverage = buildCoverage(self.baseMarks, self.glyphMap) 1138 st.Mark1Array = buildMarkArray(marks, self.glyphMap) 1139 st.Mark2Array = ot.Mark2Array() 1140 st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs) 1141 st.Mark2Array.Mark2Record = [] 1142 for base in st.Mark2Coverage.glyphs: 1143 anchors = [self.baseMarks[base].get(mc) for mc in markClassList] 1144 st.Mark2Array.Mark2Record.append(buildMark2Record(anchors)) 1145 return self.buildLookup_([st]) 1146 1147 1148class ReverseChainSingleSubstBuilder(LookupBuilder): 1149 """Builds a Reverse Chaining Contextual Single Substitution (GSUB8) lookup. 1150 1151 Users are expected to manually add substitutions to the ``substitutions`` 1152 attribute after the object has been initialized, e.g.:: 1153 1154 # reversesub [a e n] d' by d.alt; 1155 prefix = [ ["a", "e", "n"] ] 1156 suffix = [] 1157 mapping = { "d": "d.alt" } 1158 builder.substitutions.append( (prefix, suffix, mapping) ) 1159 1160 Attributes: 1161 font (``fontTools.TTLib.TTFont``): A font object. 1162 location: A string or tuple representing the location in the original 1163 source which produced this lookup. 1164 substitutions: A three-element tuple consisting of a prefix sequence, 1165 a suffix sequence, and a dictionary of single substitutions. 1166 lookupflag (int): The lookup's flag 1167 markFilterSet: Either ``None`` if no mark filtering set is used, or 1168 an integer representing the filtering set to be used for this 1169 lookup. If a mark filtering set is provided, 1170 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 1171 flags. 1172 """ 1173 1174 def __init__(self, font, location): 1175 LookupBuilder.__init__(self, font, location, "GSUB", 8) 1176 self.rules = [] # (prefix, suffix, mapping) 1177 1178 def equals(self, other): 1179 return LookupBuilder.equals(self, other) and self.rules == other.rules 1180 1181 def build(self): 1182 """Build the lookup. 1183 1184 Returns: 1185 An ``otTables.Lookup`` object representing the chained 1186 contextual substitution lookup. 1187 """ 1188 subtables = [] 1189 for prefix, suffix, mapping in self.rules: 1190 st = ot.ReverseChainSingleSubst() 1191 st.Format = 1 1192 self.setBacktrackCoverage_(prefix, st) 1193 self.setLookAheadCoverage_(suffix, st) 1194 st.Coverage = buildCoverage(mapping.keys(), self.glyphMap) 1195 st.GlyphCount = len(mapping) 1196 st.Substitute = [mapping[g] for g in st.Coverage.glyphs] 1197 subtables.append(st) 1198 return self.buildLookup_(subtables) 1199 1200 def add_subtable_break(self, location): 1201 # Nothing to do here, each substitution is in its own subtable. 1202 pass 1203 1204 1205class SingleSubstBuilder(LookupBuilder): 1206 """Builds a Single Substitution (GSUB1) lookup. 1207 1208 Users are expected to manually add substitutions to the ``mapping`` 1209 attribute after the object has been initialized, e.g.:: 1210 1211 # sub x by y; 1212 builder.mapping["x"] = "y" 1213 1214 Attributes: 1215 font (``fontTools.TTLib.TTFont``): A font object. 1216 location: A string or tuple representing the location in the original 1217 source which produced this lookup. 1218 mapping: A dictionary mapping a single glyph name to another glyph name. 1219 lookupflag (int): The lookup's flag 1220 markFilterSet: Either ``None`` if no mark filtering set is used, or 1221 an integer representing the filtering set to be used for this 1222 lookup. If a mark filtering set is provided, 1223 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 1224 flags. 1225 """ 1226 1227 def __init__(self, font, location): 1228 LookupBuilder.__init__(self, font, location, "GSUB", 1) 1229 self.mapping = OrderedDict() 1230 1231 def equals(self, other): 1232 return LookupBuilder.equals(self, other) and self.mapping == other.mapping 1233 1234 def build(self): 1235 """Build the lookup. 1236 1237 Returns: 1238 An ``otTables.Lookup`` object representing the multiple 1239 substitution lookup. 1240 """ 1241 subtables = self.build_subst_subtables(self.mapping, buildSingleSubstSubtable) 1242 return self.buildLookup_(subtables) 1243 1244 def getAlternateGlyphs(self): 1245 return {glyph: [repl] for glyph, repl in self.mapping.items()} 1246 1247 def add_subtable_break(self, location): 1248 self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_ 1249 1250 1251class ClassPairPosSubtableBuilder(object): 1252 """Builds class-based Pair Positioning (GPOS2 format 2) subtables. 1253 1254 Note that this does *not* build a GPOS2 ``otTables.Lookup`` directly, 1255 but builds a list of ``otTables.PairPos`` subtables. It is used by the 1256 :class:`PairPosBuilder` below. 1257 1258 Attributes: 1259 builder (PairPosBuilder): A pair positioning lookup builder. 1260 """ 1261 1262 def __init__(self, builder): 1263 self.builder_ = builder 1264 self.classDef1_, self.classDef2_ = None, None 1265 self.values_ = {} # (glyphclass1, glyphclass2) --> (value1, value2) 1266 self.forceSubtableBreak_ = False 1267 self.subtables_ = [] 1268 1269 def addPair(self, gc1, value1, gc2, value2): 1270 """Add a pair positioning rule. 1271 1272 Args: 1273 gc1: A set of glyph names for the "left" glyph 1274 value1: An ``otTables.ValueRecord`` object for the left glyph's 1275 positioning. 1276 gc2: A set of glyph names for the "right" glyph 1277 value2: An ``otTables.ValueRecord`` object for the right glyph's 1278 positioning. 1279 """ 1280 mergeable = ( 1281 not self.forceSubtableBreak_ 1282 and self.classDef1_ is not None 1283 and self.classDef1_.canAdd(gc1) 1284 and self.classDef2_ is not None 1285 and self.classDef2_.canAdd(gc2) 1286 ) 1287 if not mergeable: 1288 self.flush_() 1289 self.classDef1_ = ClassDefBuilder(useClass0=True) 1290 self.classDef2_ = ClassDefBuilder(useClass0=False) 1291 self.values_ = {} 1292 self.classDef1_.add(gc1) 1293 self.classDef2_.add(gc2) 1294 self.values_[(gc1, gc2)] = (value1, value2) 1295 1296 def addSubtableBreak(self): 1297 """Add an explicit subtable break at this point.""" 1298 self.forceSubtableBreak_ = True 1299 1300 def subtables(self): 1301 """Return the list of ``otTables.PairPos`` subtables constructed.""" 1302 self.flush_() 1303 return self.subtables_ 1304 1305 def flush_(self): 1306 if self.classDef1_ is None or self.classDef2_ is None: 1307 return 1308 st = buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap) 1309 if st.Coverage is None: 1310 return 1311 self.subtables_.append(st) 1312 self.forceSubtableBreak_ = False 1313 1314 1315class PairPosBuilder(LookupBuilder): 1316 """Builds a Pair Positioning (GPOS2) lookup. 1317 1318 Attributes: 1319 font (``fontTools.TTLib.TTFont``): A font object. 1320 location: A string or tuple representing the location in the original 1321 source which produced this lookup. 1322 pairs: An array of class-based pair positioning tuples. Usually 1323 manipulated with the :meth:`addClassPair` method below. 1324 glyphPairs: A dictionary mapping a tuple of glyph names to a tuple 1325 of ``otTables.ValueRecord`` objects. Usually manipulated with the 1326 :meth:`addGlyphPair` method below. 1327 lookupflag (int): The lookup's flag 1328 markFilterSet: Either ``None`` if no mark filtering set is used, or 1329 an integer representing the filtering set to be used for this 1330 lookup. If a mark filtering set is provided, 1331 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 1332 flags. 1333 """ 1334 1335 def __init__(self, font, location): 1336 LookupBuilder.__init__(self, font, location, "GPOS", 2) 1337 self.pairs = [] # [(gc1, value1, gc2, value2)*] 1338 self.glyphPairs = {} # (glyph1, glyph2) --> (value1, value2) 1339 self.locations = {} # (gc1, gc2) --> (filepath, line, column) 1340 1341 def addClassPair(self, location, glyphclass1, value1, glyphclass2, value2): 1342 """Add a class pair positioning rule to the current lookup. 1343 1344 Args: 1345 location: A string or tuple representing the location in the 1346 original source which produced this rule. Unused. 1347 glyphclass1: A set of glyph names for the "left" glyph in the pair. 1348 value1: A ``otTables.ValueRecord`` for positioning the left glyph. 1349 glyphclass2: A set of glyph names for the "right" glyph in the pair. 1350 value2: A ``otTables.ValueRecord`` for positioning the right glyph. 1351 """ 1352 self.pairs.append((glyphclass1, value1, glyphclass2, value2)) 1353 1354 def addGlyphPair(self, location, glyph1, value1, glyph2, value2): 1355 """Add a glyph pair positioning rule to the current lookup. 1356 1357 Args: 1358 location: A string or tuple representing the location in the 1359 original source which produced this rule. 1360 glyph1: A glyph name for the "left" glyph in the pair. 1361 value1: A ``otTables.ValueRecord`` for positioning the left glyph. 1362 glyph2: A glyph name for the "right" glyph in the pair. 1363 value2: A ``otTables.ValueRecord`` for positioning the right glyph. 1364 """ 1365 key = (glyph1, glyph2) 1366 oldValue = self.glyphPairs.get(key, None) 1367 if oldValue is not None: 1368 # the Feature File spec explicitly allows specific pairs generated 1369 # by an 'enum' rule to be overridden by preceding single pairs 1370 otherLoc = self.locations[key] 1371 log.debug( 1372 "Already defined position for pair %s %s at %s; " 1373 "choosing the first value", 1374 glyph1, 1375 glyph2, 1376 otherLoc, 1377 ) 1378 else: 1379 self.glyphPairs[key] = (value1, value2) 1380 self.locations[key] = location 1381 1382 def add_subtable_break(self, location): 1383 self.pairs.append( 1384 ( 1385 self.SUBTABLE_BREAK_, 1386 self.SUBTABLE_BREAK_, 1387 self.SUBTABLE_BREAK_, 1388 self.SUBTABLE_BREAK_, 1389 ) 1390 ) 1391 1392 def equals(self, other): 1393 return ( 1394 LookupBuilder.equals(self, other) 1395 and self.glyphPairs == other.glyphPairs 1396 and self.pairs == other.pairs 1397 ) 1398 1399 def build(self): 1400 """Build the lookup. 1401 1402 Returns: 1403 An ``otTables.Lookup`` object representing the pair positioning 1404 lookup. 1405 """ 1406 builders = {} 1407 builder = ClassPairPosSubtableBuilder(self) 1408 for glyphclass1, value1, glyphclass2, value2 in self.pairs: 1409 if glyphclass1 is self.SUBTABLE_BREAK_: 1410 builder.addSubtableBreak() 1411 continue 1412 builder.addPair(glyphclass1, value1, glyphclass2, value2) 1413 subtables = [] 1414 if self.glyphPairs: 1415 subtables.extend(buildPairPosGlyphs(self.glyphPairs, self.glyphMap)) 1416 subtables.extend(builder.subtables()) 1417 lookup = self.buildLookup_(subtables) 1418 1419 # Compact the lookup 1420 # This is a good moment to do it because the compaction should create 1421 # smaller subtables, which may prevent overflows from happening. 1422 # Keep reading the value from the ENV until ufo2ft switches to the config system 1423 level = self.font.cfg.get( 1424 "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", 1425 default=_compression_level_from_env(), 1426 ) 1427 if level != 0: 1428 log.info("Compacting GPOS...") 1429 compact_lookup(self.font, level, lookup) 1430 1431 return lookup 1432 1433 1434class SinglePosBuilder(LookupBuilder): 1435 """Builds a Single Positioning (GPOS1) lookup. 1436 1437 Attributes: 1438 font (``fontTools.TTLib.TTFont``): A font object. 1439 location: A string or tuple representing the location in the original 1440 source which produced this lookup. 1441 mapping: A dictionary mapping a glyph name to a ``otTables.ValueRecord`` 1442 objects. Usually manipulated with the :meth:`add_pos` method below. 1443 lookupflag (int): The lookup's flag 1444 markFilterSet: Either ``None`` if no mark filtering set is used, or 1445 an integer representing the filtering set to be used for this 1446 lookup. If a mark filtering set is provided, 1447 `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's 1448 flags. 1449 """ 1450 1451 def __init__(self, font, location): 1452 LookupBuilder.__init__(self, font, location, "GPOS", 1) 1453 self.locations = {} # glyph -> (filename, line, column) 1454 self.mapping = {} # glyph -> ot.ValueRecord 1455 1456 def add_pos(self, location, glyph, otValueRecord): 1457 """Add a single positioning rule. 1458 1459 Args: 1460 location: A string or tuple representing the location in the 1461 original source which produced this lookup. 1462 glyph: A glyph name. 1463 otValueRection: A ``otTables.ValueRecord`` used to position the 1464 glyph. 1465 """ 1466 if not self.can_add(glyph, otValueRecord): 1467 otherLoc = self.locations[glyph] 1468 raise OpenTypeLibError( 1469 'Already defined different position for glyph "%s" at %s' 1470 % (glyph, otherLoc), 1471 location, 1472 ) 1473 if otValueRecord: 1474 self.mapping[glyph] = otValueRecord 1475 self.locations[glyph] = location 1476 1477 def can_add(self, glyph, value): 1478 assert isinstance(value, ValueRecord) 1479 curValue = self.mapping.get(glyph) 1480 return curValue is None or curValue == value 1481 1482 def equals(self, other): 1483 return LookupBuilder.equals(self, other) and self.mapping == other.mapping 1484 1485 def build(self): 1486 """Build the lookup. 1487 1488 Returns: 1489 An ``otTables.Lookup`` object representing the single positioning 1490 lookup. 1491 """ 1492 subtables = buildSinglePos(self.mapping, self.glyphMap) 1493 return self.buildLookup_(subtables) 1494 1495 1496# GSUB 1497 1498 1499def buildSingleSubstSubtable(mapping): 1500 """Builds a single substitution (GSUB1) subtable. 1501 1502 Note that if you are implementing a layout compiler, you may find it more 1503 flexible to use 1504 :py:class:`fontTools.otlLib.lookupBuilders.SingleSubstBuilder` instead. 1505 1506 Args: 1507 mapping: A dictionary mapping input glyph names to output glyph names. 1508 1509 Returns: 1510 An ``otTables.SingleSubst`` object, or ``None`` if the mapping dictionary 1511 is empty. 1512 """ 1513 if not mapping: 1514 return None 1515 self = ot.SingleSubst() 1516 self.mapping = dict(mapping) 1517 return self 1518 1519 1520def buildMultipleSubstSubtable(mapping): 1521 """Builds a multiple substitution (GSUB2) subtable. 1522 1523 Note that if you are implementing a layout compiler, you may find it more 1524 flexible to use 1525 :py:class:`fontTools.otlLib.lookupBuilders.MultipleSubstBuilder` instead. 1526 1527 Example:: 1528 1529 # sub uni06C0 by uni06D5.fina hamza.above 1530 # sub uni06C2 by uni06C1.fina hamza.above; 1531 1532 subtable = buildMultipleSubstSubtable({ 1533 "uni06C0": [ "uni06D5.fina", "hamza.above"], 1534 "uni06C2": [ "uni06D1.fina", "hamza.above"] 1535 }) 1536 1537 Args: 1538 mapping: A dictionary mapping input glyph names to a list of output 1539 glyph names. 1540 1541 Returns: 1542 An ``otTables.MultipleSubst`` object or ``None`` if the mapping dictionary 1543 is empty. 1544 """ 1545 if not mapping: 1546 return None 1547 self = ot.MultipleSubst() 1548 self.mapping = dict(mapping) 1549 return self 1550 1551 1552def buildAlternateSubstSubtable(mapping): 1553 """Builds an alternate substitution (GSUB3) subtable. 1554 1555 Note that if you are implementing a layout compiler, you may find it more 1556 flexible to use 1557 :py:class:`fontTools.otlLib.lookupBuilders.AlternateSubstBuilder` instead. 1558 1559 Args: 1560 mapping: A dictionary mapping input glyph names to a list of output 1561 glyph names. 1562 1563 Returns: 1564 An ``otTables.AlternateSubst`` object or ``None`` if the mapping dictionary 1565 is empty. 1566 """ 1567 if not mapping: 1568 return None 1569 self = ot.AlternateSubst() 1570 self.alternates = dict(mapping) 1571 return self 1572 1573 1574def buildLigatureSubstSubtable(mapping): 1575 """Builds a ligature substitution (GSUB4) subtable. 1576 1577 Note that if you are implementing a layout compiler, you may find it more 1578 flexible to use 1579 :py:class:`fontTools.otlLib.lookupBuilders.LigatureSubstBuilder` instead. 1580 1581 Example:: 1582 1583 # sub f f i by f_f_i; 1584 # sub f i by f_i; 1585 1586 subtable = buildLigatureSubstSubtable({ 1587 ("f", "f", "i"): "f_f_i", 1588 ("f", "i"): "f_i", 1589 }) 1590 1591 Args: 1592 mapping: A dictionary mapping tuples of glyph names to output 1593 glyph names. 1594 1595 Returns: 1596 An ``otTables.LigatureSubst`` object or ``None`` if the mapping dictionary 1597 is empty. 1598 """ 1599 1600 if not mapping: 1601 return None 1602 self = ot.LigatureSubst() 1603 # The following single line can replace the rest of this function 1604 # with fontTools >= 3.1: 1605 # self.ligatures = dict(mapping) 1606 self.ligatures = {} 1607 for components in sorted(mapping.keys(), key=self._getLigatureSortKey): 1608 ligature = ot.Ligature() 1609 ligature.Component = components[1:] 1610 ligature.CompCount = len(ligature.Component) + 1 1611 ligature.LigGlyph = mapping[components] 1612 firstGlyph = components[0] 1613 self.ligatures.setdefault(firstGlyph, []).append(ligature) 1614 return self 1615 1616 1617# GPOS 1618 1619 1620def buildAnchor(x, y, point=None, deviceX=None, deviceY=None): 1621 """Builds an Anchor table. 1622 1623 This determines the appropriate anchor format based on the passed parameters. 1624 1625 Args: 1626 x (int): X coordinate. 1627 y (int): Y coordinate. 1628 point (int): Index of glyph contour point, if provided. 1629 deviceX (``otTables.Device``): X coordinate device table, if provided. 1630 deviceY (``otTables.Device``): Y coordinate device table, if provided. 1631 1632 Returns: 1633 An ``otTables.Anchor`` object. 1634 """ 1635 self = ot.Anchor() 1636 self.XCoordinate, self.YCoordinate = x, y 1637 self.Format = 1 1638 if point is not None: 1639 self.AnchorPoint = point 1640 self.Format = 2 1641 if deviceX is not None or deviceY is not None: 1642 assert ( 1643 self.Format == 1 1644 ), "Either point, or both of deviceX/deviceY, must be None." 1645 self.XDeviceTable = deviceX 1646 self.YDeviceTable = deviceY 1647 self.Format = 3 1648 return self 1649 1650 1651def buildBaseArray(bases, numMarkClasses, glyphMap): 1652 """Builds a base array record. 1653 1654 As part of building mark-to-base positioning rules, you will need to define 1655 a ``BaseArray`` record, which "defines for each base glyph an array of 1656 anchors, one for each mark class." This function builds the base array 1657 subtable. 1658 1659 Example:: 1660 1661 bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} 1662 basearray = buildBaseArray(bases, 2, font.getReverseGlyphMap()) 1663 1664 Args: 1665 bases (dict): A dictionary mapping anchors to glyphs; the keys being 1666 glyph names, and the values being dictionaries mapping mark class ID 1667 to the appropriate ``otTables.Anchor`` object used for attaching marks 1668 of that class. 1669 numMarkClasses (int): The total number of mark classes for which anchors 1670 are defined. 1671 glyphMap: a glyph name to ID map, typically returned from 1672 ``font.getReverseGlyphMap()``. 1673 1674 Returns: 1675 An ``otTables.BaseArray`` object. 1676 """ 1677 self = ot.BaseArray() 1678 self.BaseRecord = [] 1679 for base in sorted(bases, key=glyphMap.__getitem__): 1680 b = bases[base] 1681 anchors = [b.get(markClass) for markClass in range(numMarkClasses)] 1682 self.BaseRecord.append(buildBaseRecord(anchors)) 1683 self.BaseCount = len(self.BaseRecord) 1684 return self 1685 1686 1687def buildBaseRecord(anchors): 1688 # [otTables.Anchor, otTables.Anchor, ...] --> otTables.BaseRecord 1689 self = ot.BaseRecord() 1690 self.BaseAnchor = anchors 1691 return self 1692 1693 1694def buildComponentRecord(anchors): 1695 """Builds a component record. 1696 1697 As part of building mark-to-ligature positioning rules, you will need to 1698 define ``ComponentRecord`` objects, which contain "an array of offsets... 1699 to the Anchor tables that define all the attachment points used to attach 1700 marks to the component." This function builds the component record. 1701 1702 Args: 1703 anchors: A list of ``otTables.Anchor`` objects or ``None``. 1704 1705 Returns: 1706 A ``otTables.ComponentRecord`` object or ``None`` if no anchors are 1707 supplied. 1708 """ 1709 if not anchors: 1710 return None 1711 self = ot.ComponentRecord() 1712 self.LigatureAnchor = anchors 1713 return self 1714 1715 1716def buildCursivePosSubtable(attach, glyphMap): 1717 """Builds a cursive positioning (GPOS3) subtable. 1718 1719 Cursive positioning lookups are made up of a coverage table of glyphs, 1720 and a set of ``EntryExitRecord`` records containing the anchors for 1721 each glyph. This function builds the cursive positioning subtable. 1722 1723 Example:: 1724 1725 subtable = buildCursivePosSubtable({ 1726 "AlifIni": (None, buildAnchor(0, 50)), 1727 "BehMed": (buildAnchor(500,250), buildAnchor(0,50)), 1728 # ... 1729 }, font.getReverseGlyphMap()) 1730 1731 Args: 1732 attach (dict): A mapping between glyph names and a tuple of two 1733 ``otTables.Anchor`` objects representing entry and exit anchors. 1734 glyphMap: a glyph name to ID map, typically returned from 1735 ``font.getReverseGlyphMap()``. 1736 1737 Returns: 1738 An ``otTables.CursivePos`` object, or ``None`` if the attachment 1739 dictionary was empty. 1740 """ 1741 if not attach: 1742 return None 1743 self = ot.CursivePos() 1744 self.Format = 1 1745 self.Coverage = buildCoverage(attach.keys(), glyphMap) 1746 self.EntryExitRecord = [] 1747 for glyph in self.Coverage.glyphs: 1748 entryAnchor, exitAnchor = attach[glyph] 1749 rec = ot.EntryExitRecord() 1750 rec.EntryAnchor = entryAnchor 1751 rec.ExitAnchor = exitAnchor 1752 self.EntryExitRecord.append(rec) 1753 self.EntryExitCount = len(self.EntryExitRecord) 1754 return self 1755 1756 1757def buildDevice(deltas): 1758 """Builds a Device record as part of a ValueRecord or Anchor. 1759 1760 Device tables specify size-specific adjustments to value records 1761 and anchors to reflect changes based on the resolution of the output. 1762 For example, one could specify that an anchor's Y position should be 1763 increased by 1 pixel when displayed at 8 pixels per em. This routine 1764 builds device records. 1765 1766 Args: 1767 deltas: A dictionary mapping pixels-per-em sizes to the delta 1768 adjustment in pixels when the font is displayed at that size. 1769 1770 Returns: 1771 An ``otTables.Device`` object if any deltas were supplied, or 1772 ``None`` otherwise. 1773 """ 1774 if not deltas: 1775 return None 1776 self = ot.Device() 1777 keys = deltas.keys() 1778 self.StartSize = startSize = min(keys) 1779 self.EndSize = endSize = max(keys) 1780 assert 0 <= startSize <= endSize 1781 self.DeltaValue = deltaValues = [ 1782 deltas.get(size, 0) for size in range(startSize, endSize + 1) 1783 ] 1784 maxDelta = max(deltaValues) 1785 minDelta = min(deltaValues) 1786 assert minDelta > -129 and maxDelta < 128 1787 if minDelta > -3 and maxDelta < 2: 1788 self.DeltaFormat = 1 1789 elif minDelta > -9 and maxDelta < 8: 1790 self.DeltaFormat = 2 1791 else: 1792 self.DeltaFormat = 3 1793 return self 1794 1795 1796def buildLigatureArray(ligs, numMarkClasses, glyphMap): 1797 """Builds a LigatureArray subtable. 1798 1799 As part of building a mark-to-ligature lookup, you will need to define 1800 the set of anchors (for each mark class) on each component of the ligature 1801 where marks can be attached. For example, for an Arabic divine name ligature 1802 (lam lam heh), you may want to specify mark attachment positioning for 1803 superior marks (fatha, etc.) and inferior marks (kasra, etc.) on each glyph 1804 of the ligature. This routine builds the ligature array record. 1805 1806 Example:: 1807 1808 buildLigatureArray({ 1809 "lam-lam-heh": [ 1810 { 0: superiorAnchor1, 1: inferiorAnchor1 }, # attach points for lam1 1811 { 0: superiorAnchor2, 1: inferiorAnchor2 }, # attach points for lam2 1812 { 0: superiorAnchor3, 1: inferiorAnchor3 }, # attach points for heh 1813 ] 1814 }, 2, font.getReverseGlyphMap()) 1815 1816 Args: 1817 ligs (dict): A mapping of ligature names to an array of dictionaries: 1818 for each component glyph in the ligature, an dictionary mapping 1819 mark class IDs to anchors. 1820 numMarkClasses (int): The number of mark classes. 1821 glyphMap: a glyph name to ID map, typically returned from 1822 ``font.getReverseGlyphMap()``. 1823 1824 Returns: 1825 An ``otTables.LigatureArray`` object if deltas were supplied. 1826 """ 1827 self = ot.LigatureArray() 1828 self.LigatureAttach = [] 1829 for lig in sorted(ligs, key=glyphMap.__getitem__): 1830 anchors = [] 1831 for component in ligs[lig]: 1832 anchors.append([component.get(mc) for mc in range(numMarkClasses)]) 1833 self.LigatureAttach.append(buildLigatureAttach(anchors)) 1834 self.LigatureCount = len(self.LigatureAttach) 1835 return self 1836 1837 1838def buildLigatureAttach(components): 1839 # [[Anchor, Anchor], [Anchor, Anchor, Anchor]] --> LigatureAttach 1840 self = ot.LigatureAttach() 1841 self.ComponentRecord = [buildComponentRecord(c) for c in components] 1842 self.ComponentCount = len(self.ComponentRecord) 1843 return self 1844 1845 1846def buildMarkArray(marks, glyphMap): 1847 """Builds a mark array subtable. 1848 1849 As part of building mark-to-* positioning rules, you will need to define 1850 a MarkArray subtable, which "defines the class and the anchor point 1851 for a mark glyph." This function builds the mark array subtable. 1852 1853 Example:: 1854 1855 mark = { 1856 "acute": (0, buildAnchor(300,712)), 1857 # ... 1858 } 1859 markarray = buildMarkArray(marks, font.getReverseGlyphMap()) 1860 1861 Args: 1862 marks (dict): A dictionary mapping anchors to glyphs; the keys being 1863 glyph names, and the values being a tuple of mark class number and 1864 an ``otTables.Anchor`` object representing the mark's attachment 1865 point. 1866 glyphMap: a glyph name to ID map, typically returned from 1867 ``font.getReverseGlyphMap()``. 1868 1869 Returns: 1870 An ``otTables.MarkArray`` object. 1871 """ 1872 self = ot.MarkArray() 1873 self.MarkRecord = [] 1874 for mark in sorted(marks.keys(), key=glyphMap.__getitem__): 1875 markClass, anchor = marks[mark] 1876 markrec = buildMarkRecord(markClass, anchor) 1877 self.MarkRecord.append(markrec) 1878 self.MarkCount = len(self.MarkRecord) 1879 return self 1880 1881 1882def buildMarkBasePos(marks, bases, glyphMap): 1883 """Build a list of MarkBasePos (GPOS4) subtables. 1884 1885 This routine turns a set of marks and bases into a list of mark-to-base 1886 positioning subtables. Currently the list will contain a single subtable 1887 containing all marks and bases, although at a later date it may return the 1888 optimal list of subtables subsetting the marks and bases into groups which 1889 save space. See :func:`buildMarkBasePosSubtable` below. 1890 1891 Note that if you are implementing a layout compiler, you may find it more 1892 flexible to use 1893 :py:class:`fontTools.otlLib.lookupBuilders.MarkBasePosBuilder` instead. 1894 1895 Example:: 1896 1897 # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... 1898 1899 marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} 1900 bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} 1901 markbaseposes = buildMarkBasePos(marks, bases, font.getReverseGlyphMap()) 1902 1903 Args: 1904 marks (dict): A dictionary mapping anchors to glyphs; the keys being 1905 glyph names, and the values being a tuple of mark class number and 1906 an ``otTables.Anchor`` object representing the mark's attachment 1907 point. (See :func:`buildMarkArray`.) 1908 bases (dict): A dictionary mapping anchors to glyphs; the keys being 1909 glyph names, and the values being dictionaries mapping mark class ID 1910 to the appropriate ``otTables.Anchor`` object used for attaching marks 1911 of that class. (See :func:`buildBaseArray`.) 1912 glyphMap: a glyph name to ID map, typically returned from 1913 ``font.getReverseGlyphMap()``. 1914 1915 Returns: 1916 A list of ``otTables.MarkBasePos`` objects. 1917 """ 1918 # TODO: Consider emitting multiple subtables to save space. 1919 # Partition the marks and bases into disjoint subsets, so that 1920 # MarkBasePos rules would only access glyphs from a single 1921 # subset. This would likely lead to smaller mark/base 1922 # matrices, so we might be able to omit many of the empty 1923 # anchor tables that we currently produce. Of course, this 1924 # would only work if the MarkBasePos rules of real-world fonts 1925 # allow partitioning into multiple subsets. We should find out 1926 # whether this is the case; if so, implement the optimization. 1927 # On the other hand, a very large number of subtables could 1928 # slow down layout engines; so this would need profiling. 1929 return [buildMarkBasePosSubtable(marks, bases, glyphMap)] 1930 1931 1932def buildMarkBasePosSubtable(marks, bases, glyphMap): 1933 """Build a single MarkBasePos (GPOS4) subtable. 1934 1935 This builds a mark-to-base lookup subtable containing all of the referenced 1936 marks and bases. See :func:`buildMarkBasePos`. 1937 1938 Args: 1939 marks (dict): A dictionary mapping anchors to glyphs; the keys being 1940 glyph names, and the values being a tuple of mark class number and 1941 an ``otTables.Anchor`` object representing the mark's attachment 1942 point. (See :func:`buildMarkArray`.) 1943 bases (dict): A dictionary mapping anchors to glyphs; the keys being 1944 glyph names, and the values being dictionaries mapping mark class ID 1945 to the appropriate ``otTables.Anchor`` object used for attaching marks 1946 of that class. (See :func:`buildBaseArray`.) 1947 glyphMap: a glyph name to ID map, typically returned from 1948 ``font.getReverseGlyphMap()``. 1949 1950 Returns: 1951 A ``otTables.MarkBasePos`` object. 1952 """ 1953 self = ot.MarkBasePos() 1954 self.Format = 1 1955 self.MarkCoverage = buildCoverage(marks, glyphMap) 1956 self.MarkArray = buildMarkArray(marks, glyphMap) 1957 self.ClassCount = max([mc for mc, _ in marks.values()]) + 1 1958 self.BaseCoverage = buildCoverage(bases, glyphMap) 1959 self.BaseArray = buildBaseArray(bases, self.ClassCount, glyphMap) 1960 return self 1961 1962 1963def buildMarkLigPos(marks, ligs, glyphMap): 1964 """Build a list of MarkLigPos (GPOS5) subtables. 1965 1966 This routine turns a set of marks and ligatures into a list of mark-to-ligature 1967 positioning subtables. Currently the list will contain a single subtable 1968 containing all marks and ligatures, although at a later date it may return 1969 the optimal list of subtables subsetting the marks and ligatures into groups 1970 which save space. See :func:`buildMarkLigPosSubtable` below. 1971 1972 Note that if you are implementing a layout compiler, you may find it more 1973 flexible to use 1974 :py:class:`fontTools.otlLib.lookupBuilders.MarkLigPosBuilder` instead. 1975 1976 Example:: 1977 1978 # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... 1979 marks = { 1980 "acute": (0, a1), 1981 "grave": (0, a1), 1982 "cedilla": (1, a2) 1983 } 1984 ligs = { 1985 "f_i": [ 1986 { 0: a3, 1: a5 }, # f 1987 { 0: a4, 1: a5 } # i 1988 ], 1989 # "c_t": [{...}, {...}] 1990 } 1991 markligposes = buildMarkLigPos(marks, ligs, 1992 font.getReverseGlyphMap()) 1993 1994 Args: 1995 marks (dict): A dictionary mapping anchors to glyphs; the keys being 1996 glyph names, and the values being a tuple of mark class number and 1997 an ``otTables.Anchor`` object representing the mark's attachment 1998 point. (See :func:`buildMarkArray`.) 1999 ligs (dict): A mapping of ligature names to an array of dictionaries: 2000 for each component glyph in the ligature, an dictionary mapping 2001 mark class IDs to anchors. (See :func:`buildLigatureArray`.) 2002 glyphMap: a glyph name to ID map, typically returned from 2003 ``font.getReverseGlyphMap()``. 2004 2005 Returns: 2006 A list of ``otTables.MarkLigPos`` objects. 2007 2008 """ 2009 # TODO: Consider splitting into multiple subtables to save space, 2010 # as with MarkBasePos, this would be a trade-off that would need 2011 # profiling. And, depending on how typical fonts are structured, 2012 # it might not be worth doing at all. 2013 return [buildMarkLigPosSubtable(marks, ligs, glyphMap)] 2014 2015 2016def buildMarkLigPosSubtable(marks, ligs, glyphMap): 2017 """Build a single MarkLigPos (GPOS5) subtable. 2018 2019 This builds a mark-to-base lookup subtable containing all of the referenced 2020 marks and bases. See :func:`buildMarkLigPos`. 2021 2022 Args: 2023 marks (dict): A dictionary mapping anchors to glyphs; the keys being 2024 glyph names, and the values being a tuple of mark class number and 2025 an ``otTables.Anchor`` object representing the mark's attachment 2026 point. (See :func:`buildMarkArray`.) 2027 ligs (dict): A mapping of ligature names to an array of dictionaries: 2028 for each component glyph in the ligature, an dictionary mapping 2029 mark class IDs to anchors. (See :func:`buildLigatureArray`.) 2030 glyphMap: a glyph name to ID map, typically returned from 2031 ``font.getReverseGlyphMap()``. 2032 2033 Returns: 2034 A ``otTables.MarkLigPos`` object. 2035 """ 2036 self = ot.MarkLigPos() 2037 self.Format = 1 2038 self.MarkCoverage = buildCoverage(marks, glyphMap) 2039 self.MarkArray = buildMarkArray(marks, glyphMap) 2040 self.ClassCount = max([mc for mc, _ in marks.values()]) + 1 2041 self.LigatureCoverage = buildCoverage(ligs, glyphMap) 2042 self.LigatureArray = buildLigatureArray(ligs, self.ClassCount, glyphMap) 2043 return self 2044 2045 2046def buildMarkRecord(classID, anchor): 2047 assert isinstance(classID, int) 2048 assert isinstance(anchor, ot.Anchor) 2049 self = ot.MarkRecord() 2050 self.Class = classID 2051 self.MarkAnchor = anchor 2052 return self 2053 2054 2055def buildMark2Record(anchors): 2056 # [otTables.Anchor, otTables.Anchor, ...] --> otTables.Mark2Record 2057 self = ot.Mark2Record() 2058 self.Mark2Anchor = anchors 2059 return self 2060 2061 2062def _getValueFormat(f, values, i): 2063 # Helper for buildPairPos{Glyphs|Classes}Subtable. 2064 if f is not None: 2065 return f 2066 mask = 0 2067 for value in values: 2068 if value is not None and value[i] is not None: 2069 mask |= value[i].getFormat() 2070 return mask 2071 2072 2073def buildPairPosClassesSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None): 2074 """Builds a class pair adjustment (GPOS2 format 2) subtable. 2075 2076 Kerning tables are generally expressed as pair positioning tables using 2077 class-based pair adjustments. This routine builds format 2 PairPos 2078 subtables. 2079 2080 Note that if you are implementing a layout compiler, you may find it more 2081 flexible to use 2082 :py:class:`fontTools.otlLib.lookupBuilders.ClassPairPosSubtableBuilder` 2083 instead, as this takes care of ensuring that the supplied pairs can be 2084 formed into non-overlapping classes and emitting individual subtables 2085 whenever the non-overlapping requirement means that a new subtable is 2086 required. 2087 2088 Example:: 2089 2090 pairs = {} 2091 2092 pairs[( 2093 [ "K", "X" ], 2094 [ "W", "V" ] 2095 )] = ( buildValue(xAdvance=+5), buildValue() ) 2096 # pairs[(... , ...)] = (..., ...) 2097 2098 pairpos = buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap()) 2099 2100 Args: 2101 pairs (dict): Pair positioning data; the keys being a two-element 2102 tuple of lists of glyphnames, and the values being a two-element 2103 tuple of ``otTables.ValueRecord`` objects. 2104 glyphMap: a glyph name to ID map, typically returned from 2105 ``font.getReverseGlyphMap()``. 2106 valueFormat1: Force the "left" value records to the given format. 2107 valueFormat2: Force the "right" value records to the given format. 2108 2109 Returns: 2110 A ``otTables.PairPos`` object. 2111 """ 2112 coverage = set() 2113 classDef1 = ClassDefBuilder(useClass0=True) 2114 classDef2 = ClassDefBuilder(useClass0=False) 2115 for gc1, gc2 in sorted(pairs): 2116 coverage.update(gc1) 2117 classDef1.add(gc1) 2118 classDef2.add(gc2) 2119 self = ot.PairPos() 2120 self.Format = 2 2121 valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0) 2122 valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1) 2123 self.Coverage = buildCoverage(coverage, glyphMap) 2124 self.ClassDef1 = classDef1.build() 2125 self.ClassDef2 = classDef2.build() 2126 classes1 = classDef1.classes() 2127 classes2 = classDef2.classes() 2128 self.Class1Record = [] 2129 for c1 in classes1: 2130 rec1 = ot.Class1Record() 2131 rec1.Class2Record = [] 2132 self.Class1Record.append(rec1) 2133 for c2 in classes2: 2134 rec2 = ot.Class2Record() 2135 val1, val2 = pairs.get((c1, c2), (None, None)) 2136 rec2.Value1 = ( 2137 ValueRecord(src=val1, valueFormat=valueFormat1) 2138 if valueFormat1 2139 else None 2140 ) 2141 rec2.Value2 = ( 2142 ValueRecord(src=val2, valueFormat=valueFormat2) 2143 if valueFormat2 2144 else None 2145 ) 2146 rec1.Class2Record.append(rec2) 2147 self.Class1Count = len(self.Class1Record) 2148 self.Class2Count = len(classes2) 2149 return self 2150 2151 2152def buildPairPosGlyphs(pairs, glyphMap): 2153 """Builds a list of glyph-based pair adjustment (GPOS2 format 1) subtables. 2154 2155 This organises a list of pair positioning adjustments into subtables based 2156 on common value record formats. 2157 2158 Note that if you are implementing a layout compiler, you may find it more 2159 flexible to use 2160 :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` 2161 instead. 2162 2163 Example:: 2164 2165 pairs = { 2166 ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ), 2167 ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ), 2168 # ... 2169 } 2170 2171 subtables = buildPairPosGlyphs(pairs, font.getReverseGlyphMap()) 2172 2173 Args: 2174 pairs (dict): Pair positioning data; the keys being a two-element 2175 tuple of glyphnames, and the values being a two-element 2176 tuple of ``otTables.ValueRecord`` objects. 2177 glyphMap: a glyph name to ID map, typically returned from 2178 ``font.getReverseGlyphMap()``. 2179 2180 Returns: 2181 A list of ``otTables.PairPos`` objects. 2182 """ 2183 2184 p = {} # (formatA, formatB) --> {(glyphA, glyphB): (valA, valB)} 2185 for (glyphA, glyphB), (valA, valB) in pairs.items(): 2186 formatA = valA.getFormat() if valA is not None else 0 2187 formatB = valB.getFormat() if valB is not None else 0 2188 pos = p.setdefault((formatA, formatB), {}) 2189 pos[(glyphA, glyphB)] = (valA, valB) 2190 return [ 2191 buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB) 2192 for ((formatA, formatB), pos) in sorted(p.items()) 2193 ] 2194 2195 2196def buildPairPosGlyphsSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None): 2197 """Builds a single glyph-based pair adjustment (GPOS2 format 1) subtable. 2198 2199 This builds a PairPos subtable from a dictionary of glyph pairs and 2200 their positioning adjustments. See also :func:`buildPairPosGlyphs`. 2201 2202 Note that if you are implementing a layout compiler, you may find it more 2203 flexible to use 2204 :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` instead. 2205 2206 Example:: 2207 2208 pairs = { 2209 ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ), 2210 ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ), 2211 # ... 2212 } 2213 2214 pairpos = buildPairPosGlyphsSubtable(pairs, font.getReverseGlyphMap()) 2215 2216 Args: 2217 pairs (dict): Pair positioning data; the keys being a two-element 2218 tuple of glyphnames, and the values being a two-element 2219 tuple of ``otTables.ValueRecord`` objects. 2220 glyphMap: a glyph name to ID map, typically returned from 2221 ``font.getReverseGlyphMap()``. 2222 valueFormat1: Force the "left" value records to the given format. 2223 valueFormat2: Force the "right" value records to the given format. 2224 2225 Returns: 2226 A ``otTables.PairPos`` object. 2227 """ 2228 self = ot.PairPos() 2229 self.Format = 1 2230 valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0) 2231 valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1) 2232 p = {} 2233 for (glyphA, glyphB), (valA, valB) in pairs.items(): 2234 p.setdefault(glyphA, []).append((glyphB, valA, valB)) 2235 self.Coverage = buildCoverage({g for g, _ in pairs.keys()}, glyphMap) 2236 self.PairSet = [] 2237 for glyph in self.Coverage.glyphs: 2238 ps = ot.PairSet() 2239 ps.PairValueRecord = [] 2240 self.PairSet.append(ps) 2241 for glyph2, val1, val2 in sorted(p[glyph], key=lambda x: glyphMap[x[0]]): 2242 pvr = ot.PairValueRecord() 2243 pvr.SecondGlyph = glyph2 2244 pvr.Value1 = ( 2245 ValueRecord(src=val1, valueFormat=valueFormat1) 2246 if valueFormat1 2247 else None 2248 ) 2249 pvr.Value2 = ( 2250 ValueRecord(src=val2, valueFormat=valueFormat2) 2251 if valueFormat2 2252 else None 2253 ) 2254 ps.PairValueRecord.append(pvr) 2255 ps.PairValueCount = len(ps.PairValueRecord) 2256 self.PairSetCount = len(self.PairSet) 2257 return self 2258 2259 2260def buildSinglePos(mapping, glyphMap): 2261 """Builds a list of single adjustment (GPOS1) subtables. 2262 2263 This builds a list of SinglePos subtables from a dictionary of glyph 2264 names and their positioning adjustments. The format of the subtables are 2265 determined to optimize the size of the resulting subtables. 2266 See also :func:`buildSinglePosSubtable`. 2267 2268 Note that if you are implementing a layout compiler, you may find it more 2269 flexible to use 2270 :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead. 2271 2272 Example:: 2273 2274 mapping = { 2275 "V": buildValue({ "xAdvance" : +5 }), 2276 # ... 2277 } 2278 2279 subtables = buildSinglePos(pairs, font.getReverseGlyphMap()) 2280 2281 Args: 2282 mapping (dict): A mapping between glyphnames and 2283 ``otTables.ValueRecord`` objects. 2284 glyphMap: a glyph name to ID map, typically returned from 2285 ``font.getReverseGlyphMap()``. 2286 2287 Returns: 2288 A list of ``otTables.SinglePos`` objects. 2289 """ 2290 result, handled = [], set() 2291 # In SinglePos format 1, the covered glyphs all share the same ValueRecord. 2292 # In format 2, each glyph has its own ValueRecord, but these records 2293 # all have the same properties (eg., all have an X but no Y placement). 2294 coverages, masks, values = {}, {}, {} 2295 for glyph, value in mapping.items(): 2296 key = _getSinglePosValueKey(value) 2297 coverages.setdefault(key, []).append(glyph) 2298 masks.setdefault(key[0], []).append(key) 2299 values[key] = value 2300 2301 # If a ValueRecord is shared between multiple glyphs, we generate 2302 # a SinglePos format 1 subtable; that is the most compact form. 2303 for key, glyphs in coverages.items(): 2304 # 5 ushorts is the length of introducing another sublookup 2305 if len(glyphs) * _getSinglePosValueSize(key) > 5: 2306 format1Mapping = {g: values[key] for g in glyphs} 2307 result.append(buildSinglePosSubtable(format1Mapping, glyphMap)) 2308 handled.add(key) 2309 2310 # In the remaining ValueRecords, look for those whose valueFormat 2311 # (the set of used properties) is shared between multiple records. 2312 # These will get encoded in format 2. 2313 for valueFormat, keys in masks.items(): 2314 f2 = [k for k in keys if k not in handled] 2315 if len(f2) > 1: 2316 format2Mapping = {} 2317 for k in f2: 2318 format2Mapping.update((g, values[k]) for g in coverages[k]) 2319 result.append(buildSinglePosSubtable(format2Mapping, glyphMap)) 2320 handled.update(f2) 2321 2322 # The remaining ValueRecords are only used by a few glyphs, normally 2323 # one. We encode these in format 1 again. 2324 for key, glyphs in coverages.items(): 2325 if key not in handled: 2326 for g in glyphs: 2327 st = buildSinglePosSubtable({g: values[key]}, glyphMap) 2328 result.append(st) 2329 2330 # When the OpenType layout engine traverses the subtables, it will 2331 # stop after the first matching subtable. Therefore, we sort the 2332 # resulting subtables by decreasing coverage size; this increases 2333 # the chance that the layout engine can do an early exit. (Of course, 2334 # this would only be true if all glyphs were equally frequent, which 2335 # is not really the case; but we do not know their distribution). 2336 # If two subtables cover the same number of glyphs, we sort them 2337 # by glyph ID so that our output is deterministic. 2338 result.sort(key=lambda t: _getSinglePosTableKey(t, glyphMap)) 2339 return result 2340 2341 2342def buildSinglePosSubtable(values, glyphMap): 2343 """Builds a single adjustment (GPOS1) subtable. 2344 2345 This builds a list of SinglePos subtables from a dictionary of glyph 2346 names and their positioning adjustments. The format of the subtable is 2347 determined to optimize the size of the output. 2348 See also :func:`buildSinglePos`. 2349 2350 Note that if you are implementing a layout compiler, you may find it more 2351 flexible to use 2352 :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead. 2353 2354 Example:: 2355 2356 mapping = { 2357 "V": buildValue({ "xAdvance" : +5 }), 2358 # ... 2359 } 2360 2361 subtable = buildSinglePos(pairs, font.getReverseGlyphMap()) 2362 2363 Args: 2364 mapping (dict): A mapping between glyphnames and 2365 ``otTables.ValueRecord`` objects. 2366 glyphMap: a glyph name to ID map, typically returned from 2367 ``font.getReverseGlyphMap()``. 2368 2369 Returns: 2370 A ``otTables.SinglePos`` object. 2371 """ 2372 self = ot.SinglePos() 2373 self.Coverage = buildCoverage(values.keys(), glyphMap) 2374 valueFormat = self.ValueFormat = reduce( 2375 int.__or__, [v.getFormat() for v in values.values()], 0 2376 ) 2377 valueRecords = [ 2378 ValueRecord(src=values[g], valueFormat=valueFormat) 2379 for g in self.Coverage.glyphs 2380 ] 2381 if all(v == valueRecords[0] for v in valueRecords): 2382 self.Format = 1 2383 if self.ValueFormat != 0: 2384 self.Value = valueRecords[0] 2385 else: 2386 self.Value = None 2387 else: 2388 self.Format = 2 2389 self.Value = valueRecords 2390 self.ValueCount = len(self.Value) 2391 return self 2392 2393 2394def _getSinglePosTableKey(subtable, glyphMap): 2395 assert isinstance(subtable, ot.SinglePos), subtable 2396 glyphs = subtable.Coverage.glyphs 2397 return (-len(glyphs), glyphMap[glyphs[0]]) 2398 2399 2400def _getSinglePosValueKey(valueRecord): 2401 # otBase.ValueRecord --> (2, ("YPlacement": 12)) 2402 assert isinstance(valueRecord, ValueRecord), valueRecord 2403 valueFormat, result = 0, [] 2404 for name, value in valueRecord.__dict__.items(): 2405 if isinstance(value, ot.Device): 2406 result.append((name, _makeDeviceTuple(value))) 2407 else: 2408 result.append((name, value)) 2409 valueFormat |= valueRecordFormatDict[name][0] 2410 result.sort() 2411 result.insert(0, valueFormat) 2412 return tuple(result) 2413 2414 2415_DeviceTuple = namedtuple("_DeviceTuple", "DeltaFormat StartSize EndSize DeltaValue") 2416 2417 2418def _makeDeviceTuple(device): 2419 # otTables.Device --> tuple, for making device tables unique 2420 return _DeviceTuple( 2421 device.DeltaFormat, 2422 device.StartSize, 2423 device.EndSize, 2424 () if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue), 2425 ) 2426 2427 2428def _getSinglePosValueSize(valueKey): 2429 # Returns how many ushorts this valueKey (short form of ValueRecord) takes up 2430 count = 0 2431 for _, v in valueKey[1:]: 2432 if isinstance(v, _DeviceTuple): 2433 count += len(v.DeltaValue) + 3 2434 else: 2435 count += 1 2436 return count 2437 2438 2439def buildValue(value): 2440 """Builds a positioning value record. 2441 2442 Value records are used to specify coordinates and adjustments for 2443 positioning and attaching glyphs. Many of the positioning functions 2444 in this library take ``otTables.ValueRecord`` objects as arguments. 2445 This function builds value records from dictionaries. 2446 2447 Args: 2448 value (dict): A dictionary with zero or more of the following keys: 2449 - ``xPlacement`` 2450 - ``yPlacement`` 2451 - ``xAdvance`` 2452 - ``yAdvance`` 2453 - ``xPlaDevice`` 2454 - ``yPlaDevice`` 2455 - ``xAdvDevice`` 2456 - ``yAdvDevice`` 2457 2458 Returns: 2459 An ``otTables.ValueRecord`` object. 2460 """ 2461 self = ValueRecord() 2462 for k, v in value.items(): 2463 setattr(self, k, v) 2464 return self 2465 2466 2467# GDEF 2468 2469 2470def buildAttachList(attachPoints, glyphMap): 2471 """Builds an AttachList subtable. 2472 2473 A GDEF table may contain an Attachment Point List table (AttachList) 2474 which stores the contour indices of attachment points for glyphs with 2475 attachment points. This routine builds AttachList subtables. 2476 2477 Args: 2478 attachPoints (dict): A mapping between glyph names and a list of 2479 contour indices. 2480 2481 Returns: 2482 An ``otTables.AttachList`` object if attachment points are supplied, 2483 or ``None`` otherwise. 2484 """ 2485 if not attachPoints: 2486 return None 2487 self = ot.AttachList() 2488 self.Coverage = buildCoverage(attachPoints.keys(), glyphMap) 2489 self.AttachPoint = [buildAttachPoint(attachPoints[g]) for g in self.Coverage.glyphs] 2490 self.GlyphCount = len(self.AttachPoint) 2491 return self 2492 2493 2494def buildAttachPoint(points): 2495 # [4, 23, 41] --> otTables.AttachPoint 2496 # Only used by above. 2497 if not points: 2498 return None 2499 self = ot.AttachPoint() 2500 self.PointIndex = sorted(set(points)) 2501 self.PointCount = len(self.PointIndex) 2502 return self 2503 2504 2505def buildCaretValueForCoord(coord): 2506 # 500 --> otTables.CaretValue, format 1 2507 # (500, DeviceTable) --> otTables.CaretValue, format 3 2508 self = ot.CaretValue() 2509 if isinstance(coord, tuple): 2510 self.Format = 3 2511 self.Coordinate, self.DeviceTable = coord 2512 else: 2513 self.Format = 1 2514 self.Coordinate = coord 2515 return self 2516 2517 2518def buildCaretValueForPoint(point): 2519 # 4 --> otTables.CaretValue, format 2 2520 self = ot.CaretValue() 2521 self.Format = 2 2522 self.CaretValuePoint = point 2523 return self 2524 2525 2526def buildLigCaretList(coords, points, glyphMap): 2527 """Builds a ligature caret list table. 2528 2529 Ligatures appear as a single glyph representing multiple characters; however 2530 when, for example, editing text containing a ``f_i`` ligature, the user may 2531 want to place the cursor between the ``f`` and the ``i``. The ligature caret 2532 list in the GDEF table specifies the position to display the "caret" (the 2533 character insertion indicator, typically a flashing vertical bar) "inside" 2534 the ligature to represent an insertion point. The insertion positions may 2535 be specified either by coordinate or by contour point. 2536 2537 Example:: 2538 2539 coords = { 2540 "f_f_i": [300, 600] # f|fi cursor at 300 units, ff|i cursor at 600. 2541 } 2542 points = { 2543 "c_t": [28] # c|t cursor appears at coordinate of contour point 28. 2544 } 2545 ligcaretlist = buildLigCaretList(coords, points, font.getReverseGlyphMap()) 2546 2547 Args: 2548 coords: A mapping between glyph names and a list of coordinates for 2549 the insertion point of each ligature component after the first one. 2550 points: A mapping between glyph names and a list of contour points for 2551 the insertion point of each ligature component after the first one. 2552 glyphMap: a glyph name to ID map, typically returned from 2553 ``font.getReverseGlyphMap()``. 2554 2555 Returns: 2556 A ``otTables.LigCaretList`` object if any carets are present, or 2557 ``None`` otherwise.""" 2558 glyphs = set(coords.keys()) if coords else set() 2559 if points: 2560 glyphs.update(points.keys()) 2561 carets = {g: buildLigGlyph(coords.get(g), points.get(g)) for g in glyphs} 2562 carets = {g: c for g, c in carets.items() if c is not None} 2563 if not carets: 2564 return None 2565 self = ot.LigCaretList() 2566 self.Coverage = buildCoverage(carets.keys(), glyphMap) 2567 self.LigGlyph = [carets[g] for g in self.Coverage.glyphs] 2568 self.LigGlyphCount = len(self.LigGlyph) 2569 return self 2570 2571 2572def buildLigGlyph(coords, points): 2573 # ([500], [4]) --> otTables.LigGlyph; None for empty coords/points 2574 carets = [] 2575 if coords: 2576 coords = sorted(coords, key=lambda c: c[0] if isinstance(c, tuple) else c) 2577 carets.extend([buildCaretValueForCoord(c) for c in coords]) 2578 if points: 2579 carets.extend([buildCaretValueForPoint(p) for p in sorted(points)]) 2580 if not carets: 2581 return None 2582 self = ot.LigGlyph() 2583 self.CaretValue = carets 2584 self.CaretCount = len(self.CaretValue) 2585 return self 2586 2587 2588def buildMarkGlyphSetsDef(markSets, glyphMap): 2589 """Builds a mark glyph sets definition table. 2590 2591 OpenType Layout lookups may choose to use mark filtering sets to consider 2592 or ignore particular combinations of marks. These sets are specified by 2593 setting a flag on the lookup, but the mark filtering sets are defined in 2594 the ``GDEF`` table. This routine builds the subtable containing the mark 2595 glyph set definitions. 2596 2597 Example:: 2598 2599 set0 = set("acute", "grave") 2600 set1 = set("caron", "grave") 2601 2602 markglyphsets = buildMarkGlyphSetsDef([set0, set1], font.getReverseGlyphMap()) 2603 2604 Args: 2605 2606 markSets: A list of sets of glyphnames. 2607 glyphMap: a glyph name to ID map, typically returned from 2608 ``font.getReverseGlyphMap()``. 2609 2610 Returns 2611 An ``otTables.MarkGlyphSetsDef`` object. 2612 """ 2613 if not markSets: 2614 return None 2615 self = ot.MarkGlyphSetsDef() 2616 self.MarkSetTableFormat = 1 2617 self.Coverage = [buildCoverage(m, glyphMap) for m in markSets] 2618 self.MarkSetCount = len(self.Coverage) 2619 return self 2620 2621 2622class ClassDefBuilder(object): 2623 """Helper for building ClassDef tables.""" 2624 2625 def __init__(self, useClass0): 2626 self.classes_ = set() 2627 self.glyphs_ = {} 2628 self.useClass0_ = useClass0 2629 2630 def canAdd(self, glyphs): 2631 if isinstance(glyphs, (set, frozenset)): 2632 glyphs = sorted(glyphs) 2633 glyphs = tuple(glyphs) 2634 if glyphs in self.classes_: 2635 return True 2636 for glyph in glyphs: 2637 if glyph in self.glyphs_: 2638 return False 2639 return True 2640 2641 def add(self, glyphs): 2642 if isinstance(glyphs, (set, frozenset)): 2643 glyphs = sorted(glyphs) 2644 glyphs = tuple(glyphs) 2645 if glyphs in self.classes_: 2646 return 2647 self.classes_.add(glyphs) 2648 for glyph in glyphs: 2649 if glyph in self.glyphs_: 2650 raise OpenTypeLibError( 2651 f"Glyph {glyph} is already present in class.", None 2652 ) 2653 self.glyphs_[glyph] = glyphs 2654 2655 def classes(self): 2656 # In ClassDef1 tables, class id #0 does not need to be encoded 2657 # because zero is the default. Therefore, we use id #0 for the 2658 # glyph class that has the largest number of members. However, 2659 # in other tables than ClassDef1, 0 means "every other glyph" 2660 # so we should not use that ID for any real glyph classes; 2661 # we implement this by inserting an empty set at position 0. 2662 # 2663 # TODO: Instead of counting the number of glyphs in each class, 2664 # we should determine the encoded size. If the glyphs in a large 2665 # class form a contiguous range, the encoding is actually quite 2666 # compact, whereas a non-contiguous set might need a lot of bytes 2667 # in the output file. We don't get this right with the key below. 2668 result = sorted(self.classes_, key=lambda s: (-len(s), s)) 2669 if not self.useClass0_: 2670 result.insert(0, frozenset()) 2671 return result 2672 2673 def build(self): 2674 glyphClasses = {} 2675 for classID, glyphs in enumerate(self.classes()): 2676 if classID == 0: 2677 continue 2678 for glyph in glyphs: 2679 glyphClasses[glyph] = classID 2680 classDef = ot.ClassDef() 2681 classDef.classDefs = glyphClasses 2682 return classDef 2683 2684 2685AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16) 2686AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16) 2687 2688 2689def buildStatTable( 2690 ttFont, axes, locations=None, elidedFallbackName=2, windowsNames=True, macNames=True 2691): 2692 """Add a 'STAT' table to 'ttFont'. 2693 2694 'axes' is a list of dictionaries describing axes and their 2695 values. 2696 2697 Example:: 2698 2699 axes = [ 2700 dict( 2701 tag="wght", 2702 name="Weight", 2703 ordering=0, # optional 2704 values=[ 2705 dict(value=100, name='Thin'), 2706 dict(value=300, name='Light'), 2707 dict(value=400, name='Regular', flags=0x2), 2708 dict(value=900, name='Black'), 2709 ], 2710 ) 2711 ] 2712 2713 Each axis dict must have 'tag' and 'name' items. 'tag' maps 2714 to the 'AxisTag' field. 'name' can be a name ID (int), a string, 2715 or a dictionary containing multilingual names (see the 2716 addMultilingualName() name table method), and will translate to 2717 the AxisNameID field. 2718 2719 An axis dict may contain an 'ordering' item that maps to the 2720 AxisOrdering field. If omitted, the order of the axes list is 2721 used to calculate AxisOrdering fields. 2722 2723 The axis dict may contain a 'values' item, which is a list of 2724 dictionaries describing AxisValue records belonging to this axis. 2725 2726 Each value dict must have a 'name' item, which can be a name ID 2727 (int), a string, or a dictionary containing multilingual names, 2728 like the axis name. It translates to the ValueNameID field. 2729 2730 Optionally the value dict can contain a 'flags' item. It maps to 2731 the AxisValue Flags field, and will be 0 when omitted. 2732 2733 The format of the AxisValue is determined by the remaining contents 2734 of the value dictionary: 2735 2736 If the value dict contains a 'value' item, an AxisValue record 2737 Format 1 is created. If in addition to the 'value' item it contains 2738 a 'linkedValue' item, an AxisValue record Format 3 is built. 2739 2740 If the value dict contains a 'nominalValue' item, an AxisValue 2741 record Format 2 is built. Optionally it may contain 'rangeMinValue' 2742 and 'rangeMaxValue' items. These map to -Infinity and +Infinity 2743 respectively if omitted. 2744 2745 You cannot specify Format 4 AxisValue tables this way, as they are 2746 not tied to a single axis, and specify a name for a location that 2747 is defined by multiple axes values. Instead, you need to supply the 2748 'locations' argument. 2749 2750 The optional 'locations' argument specifies AxisValue Format 4 2751 tables. It should be a list of dicts, where each dict has a 'name' 2752 item, which works just like the value dicts above, an optional 2753 'flags' item (defaulting to 0x0), and a 'location' dict. A 2754 location dict key is an axis tag, and the associated value is the 2755 location on the specified axis. They map to the AxisIndex and Value 2756 fields of the AxisValueRecord. 2757 2758 Example:: 2759 2760 locations = [ 2761 dict(name='Regular ABCD', location=dict(wght=300, ABCD=100)), 2762 dict(name='Bold ABCD XYZ', location=dict(wght=600, ABCD=200)), 2763 ] 2764 2765 The optional 'elidedFallbackName' argument can be a name ID (int), 2766 a string, a dictionary containing multilingual names, or a list of 2767 STATNameStatements. It translates to the ElidedFallbackNameID field. 2768 2769 The 'ttFont' argument must be a TTFont instance that already has a 2770 'name' table. If a 'STAT' table already exists, it will be 2771 overwritten by the newly created one. 2772 """ 2773 ttFont["STAT"] = ttLib.newTable("STAT") 2774 statTable = ttFont["STAT"].table = ot.STAT() 2775 statTable.ElidedFallbackNameID = _addName( 2776 ttFont, elidedFallbackName, windows=windowsNames, mac=macNames 2777 ) 2778 2779 # 'locations' contains data for AxisValue Format 4 2780 axisRecords, axisValues = _buildAxisRecords( 2781 axes, ttFont, windowsNames=windowsNames, macNames=macNames 2782 ) 2783 if not locations: 2784 statTable.Version = 0x00010001 2785 else: 2786 # We'll be adding Format 4 AxisValue records, which 2787 # requires a higher table version 2788 statTable.Version = 0x00010002 2789 multiAxisValues = _buildAxisValuesFormat4( 2790 locations, axes, ttFont, windowsNames=windowsNames, macNames=macNames 2791 ) 2792 axisValues = multiAxisValues + axisValues 2793 ttFont["name"].names.sort() 2794 2795 # Store AxisRecords 2796 axisRecordArray = ot.AxisRecordArray() 2797 axisRecordArray.Axis = axisRecords 2798 # XXX these should not be hard-coded but computed automatically 2799 statTable.DesignAxisRecordSize = 8 2800 statTable.DesignAxisRecord = axisRecordArray 2801 statTable.DesignAxisCount = len(axisRecords) 2802 2803 statTable.AxisValueCount = 0 2804 statTable.AxisValueArray = None 2805 if axisValues: 2806 # Store AxisValueRecords 2807 axisValueArray = ot.AxisValueArray() 2808 axisValueArray.AxisValue = axisValues 2809 statTable.AxisValueArray = axisValueArray 2810 statTable.AxisValueCount = len(axisValues) 2811 2812 2813def _buildAxisRecords(axes, ttFont, windowsNames=True, macNames=True): 2814 axisRecords = [] 2815 axisValues = [] 2816 for axisRecordIndex, axisDict in enumerate(axes): 2817 axis = ot.AxisRecord() 2818 axis.AxisTag = axisDict["tag"] 2819 axis.AxisNameID = _addName( 2820 ttFont, axisDict["name"], 256, windows=windowsNames, mac=macNames 2821 ) 2822 axis.AxisOrdering = axisDict.get("ordering", axisRecordIndex) 2823 axisRecords.append(axis) 2824 2825 for axisVal in axisDict.get("values", ()): 2826 axisValRec = ot.AxisValue() 2827 axisValRec.AxisIndex = axisRecordIndex 2828 axisValRec.Flags = axisVal.get("flags", 0) 2829 axisValRec.ValueNameID = _addName( 2830 ttFont, axisVal["name"], windows=windowsNames, mac=macNames 2831 ) 2832 2833 if "value" in axisVal: 2834 axisValRec.Value = axisVal["value"] 2835 if "linkedValue" in axisVal: 2836 axisValRec.Format = 3 2837 axisValRec.LinkedValue = axisVal["linkedValue"] 2838 else: 2839 axisValRec.Format = 1 2840 elif "nominalValue" in axisVal: 2841 axisValRec.Format = 2 2842 axisValRec.NominalValue = axisVal["nominalValue"] 2843 axisValRec.RangeMinValue = axisVal.get( 2844 "rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY 2845 ) 2846 axisValRec.RangeMaxValue = axisVal.get( 2847 "rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY 2848 ) 2849 else: 2850 raise ValueError("Can't determine format for AxisValue") 2851 2852 axisValues.append(axisValRec) 2853 return axisRecords, axisValues 2854 2855 2856def _buildAxisValuesFormat4(locations, axes, ttFont, windowsNames=True, macNames=True): 2857 axisTagToIndex = {} 2858 for axisRecordIndex, axisDict in enumerate(axes): 2859 axisTagToIndex[axisDict["tag"]] = axisRecordIndex 2860 2861 axisValues = [] 2862 for axisLocationDict in locations: 2863 axisValRec = ot.AxisValue() 2864 axisValRec.Format = 4 2865 axisValRec.ValueNameID = _addName( 2866 ttFont, axisLocationDict["name"], windows=windowsNames, mac=macNames 2867 ) 2868 axisValRec.Flags = axisLocationDict.get("flags", 0) 2869 axisValueRecords = [] 2870 for tag, value in axisLocationDict["location"].items(): 2871 avr = ot.AxisValueRecord() 2872 avr.AxisIndex = axisTagToIndex[tag] 2873 avr.Value = value 2874 axisValueRecords.append(avr) 2875 axisValueRecords.sort(key=lambda avr: avr.AxisIndex) 2876 axisValRec.AxisCount = len(axisValueRecords) 2877 axisValRec.AxisValueRecord = axisValueRecords 2878 axisValues.append(axisValRec) 2879 return axisValues 2880 2881 2882def _addName(ttFont, value, minNameID=0, windows=True, mac=True): 2883 nameTable = ttFont["name"] 2884 if isinstance(value, int): 2885 # Already a nameID 2886 return value 2887 if isinstance(value, str): 2888 names = dict(en=value) 2889 elif isinstance(value, dict): 2890 names = value 2891 elif isinstance(value, list): 2892 nameID = nameTable._findUnusedNameID() 2893 for nameRecord in value: 2894 if isinstance(nameRecord, STATNameStatement): 2895 nameTable.setName( 2896 nameRecord.string, 2897 nameID, 2898 nameRecord.platformID, 2899 nameRecord.platEncID, 2900 nameRecord.langID, 2901 ) 2902 else: 2903 raise TypeError("value must be a list of STATNameStatements") 2904 return nameID 2905 else: 2906 raise TypeError("value must be int, str, dict or list") 2907 return nameTable.addMultilingualName( 2908 names, ttFont=ttFont, windows=windows, mac=mac, minNameID=minNameID 2909 ) 2910 2911 2912def buildMathTable( 2913 ttFont, 2914 constants=None, 2915 italicsCorrections=None, 2916 topAccentAttachments=None, 2917 extendedShapes=None, 2918 mathKerns=None, 2919 minConnectorOverlap=0, 2920 vertGlyphVariants=None, 2921 horizGlyphVariants=None, 2922 vertGlyphAssembly=None, 2923 horizGlyphAssembly=None, 2924): 2925 """ 2926 Add a 'MATH' table to 'ttFont'. 2927 2928 'constants' is a dictionary of math constants. The keys are the constant 2929 names from the MATH table specification (with capital first letter), and the 2930 values are the constant values as numbers. 2931 2932 'italicsCorrections' is a dictionary of italic corrections. The keys are the 2933 glyph names, and the values are the italic corrections as numbers. 2934 2935 'topAccentAttachments' is a dictionary of top accent attachments. The keys 2936 are the glyph names, and the values are the top accent horizontal positions 2937 as numbers. 2938 2939 'extendedShapes' is a set of extended shape glyphs. 2940 2941 'mathKerns' is a dictionary of math kerns. The keys are the glyph names, and 2942 the values are dictionaries. The keys of these dictionaries are the side 2943 names ('TopRight', 'TopLeft', 'BottomRight', 'BottomLeft'), and the values 2944 are tuples of two lists. The first list contains the correction heights as 2945 numbers, and the second list contains the kern values as numbers. 2946 2947 'minConnectorOverlap' is the minimum connector overlap as a number. 2948 2949 'vertGlyphVariants' is a dictionary of vertical glyph variants. The keys are 2950 the glyph names, and the values are tuples of glyph name and full advance height. 2951 2952 'horizGlyphVariants' is a dictionary of horizontal glyph variants. The keys 2953 are the glyph names, and the values are tuples of glyph name and full 2954 advance width. 2955 2956 'vertGlyphAssembly' is a dictionary of vertical glyph assemblies. The keys 2957 are the glyph names, and the values are tuples of assembly parts and italics 2958 correction. The assembly parts are tuples of glyph name, flags, start 2959 connector length, end connector length, and full advance height. 2960 2961 'horizGlyphAssembly' is a dictionary of horizontal glyph assemblies. The 2962 keys are the glyph names, and the values are tuples of assembly parts 2963 and italics correction. The assembly parts are tuples of glyph name, flags, 2964 start connector length, end connector length, and full advance width. 2965 2966 Where a number is expected, an integer or a float can be used. The floats 2967 will be rounded. 2968 2969 Example:: 2970 2971 constants = { 2972 "ScriptPercentScaleDown": 70, 2973 "ScriptScriptPercentScaleDown": 50, 2974 "DelimitedSubFormulaMinHeight": 24, 2975 "DisplayOperatorMinHeight": 60, 2976 ... 2977 } 2978 italicsCorrections = { 2979 "fitalic-math": 100, 2980 "fbolditalic-math": 120, 2981 ... 2982 } 2983 topAccentAttachments = { 2984 "circumflexcomb": 500, 2985 "acutecomb": 400, 2986 "A": 300, 2987 "B": 340, 2988 ... 2989 } 2990 extendedShapes = {"parenleft", "parenright", ...} 2991 mathKerns = { 2992 "A": { 2993 "TopRight": ([-50, -100], [10, 20, 30]), 2994 "TopLeft": ([50, 100], [10, 20, 30]), 2995 ... 2996 }, 2997 ... 2998 } 2999 vertGlyphVariants = { 3000 "parenleft": [("parenleft", 700), ("parenleft.size1", 1000), ...], 3001 "parenright": [("parenright", 700), ("parenright.size1", 1000), ...], 3002 ... 3003 } 3004 vertGlyphAssembly = { 3005 "braceleft": [ 3006 ( 3007 ("braceleft.bottom", 0, 0, 200, 500), 3008 ("braceleft.extender", 1, 200, 200, 200)), 3009 ("braceleft.middle", 0, 100, 100, 700), 3010 ("braceleft.extender", 1, 200, 200, 200), 3011 ("braceleft.top", 0, 200, 0, 500), 3012 ), 3013 100, 3014 ], 3015 ... 3016 } 3017 """ 3018 glyphMap = ttFont.getReverseGlyphMap() 3019 3020 ttFont["MATH"] = math = ttLib.newTable("MATH") 3021 math.table = table = ot.MATH() 3022 table.Version = 0x00010000 3023 table.populateDefaults() 3024 3025 table.MathConstants = _buildMathConstants(constants) 3026 table.MathGlyphInfo = _buildMathGlyphInfo( 3027 glyphMap, 3028 italicsCorrections, 3029 topAccentAttachments, 3030 extendedShapes, 3031 mathKerns, 3032 ) 3033 table.MathVariants = _buildMathVariants( 3034 glyphMap, 3035 minConnectorOverlap, 3036 vertGlyphVariants, 3037 horizGlyphVariants, 3038 vertGlyphAssembly, 3039 horizGlyphAssembly, 3040 ) 3041 3042 3043def _buildMathConstants(constants): 3044 if not constants: 3045 return None 3046 3047 mathConstants = ot.MathConstants() 3048 for conv in mathConstants.getConverters(): 3049 value = otRound(constants.get(conv.name, 0)) 3050 if conv.tableClass: 3051 assert issubclass(conv.tableClass, ot.MathValueRecord) 3052 value = _mathValueRecord(value) 3053 setattr(mathConstants, conv.name, value) 3054 return mathConstants 3055 3056 3057def _buildMathGlyphInfo( 3058 glyphMap, 3059 italicsCorrections, 3060 topAccentAttachments, 3061 extendedShapes, 3062 mathKerns, 3063): 3064 if not any([extendedShapes, italicsCorrections, topAccentAttachments, mathKerns]): 3065 return None 3066 3067 info = ot.MathGlyphInfo() 3068 info.populateDefaults() 3069 3070 if italicsCorrections: 3071 coverage = buildCoverage(italicsCorrections.keys(), glyphMap) 3072 info.MathItalicsCorrectionInfo = ot.MathItalicsCorrectionInfo() 3073 info.MathItalicsCorrectionInfo.Coverage = coverage 3074 info.MathItalicsCorrectionInfo.ItalicsCorrectionCount = len(coverage.glyphs) 3075 info.MathItalicsCorrectionInfo.ItalicsCorrection = [ 3076 _mathValueRecord(italicsCorrections[n]) for n in coverage.glyphs 3077 ] 3078 3079 if topAccentAttachments: 3080 coverage = buildCoverage(topAccentAttachments.keys(), glyphMap) 3081 info.MathTopAccentAttachment = ot.MathTopAccentAttachment() 3082 info.MathTopAccentAttachment.TopAccentCoverage = coverage 3083 info.MathTopAccentAttachment.TopAccentAttachmentCount = len(coverage.glyphs) 3084 info.MathTopAccentAttachment.TopAccentAttachment = [ 3085 _mathValueRecord(topAccentAttachments[n]) for n in coverage.glyphs 3086 ] 3087 3088 if extendedShapes: 3089 info.ExtendedShapeCoverage = buildCoverage(extendedShapes, glyphMap) 3090 3091 if mathKerns: 3092 coverage = buildCoverage(mathKerns.keys(), glyphMap) 3093 info.MathKernInfo = ot.MathKernInfo() 3094 info.MathKernInfo.MathKernCoverage = coverage 3095 info.MathKernInfo.MathKernCount = len(coverage.glyphs) 3096 info.MathKernInfo.MathKernInfoRecords = [] 3097 for glyph in coverage.glyphs: 3098 record = ot.MathKernInfoRecord() 3099 for side in {"TopRight", "TopLeft", "BottomRight", "BottomLeft"}: 3100 if side in mathKerns[glyph]: 3101 correctionHeights, kernValues = mathKerns[glyph][side] 3102 assert len(correctionHeights) == len(kernValues) - 1 3103 kern = ot.MathKern() 3104 kern.HeightCount = len(correctionHeights) 3105 kern.CorrectionHeight = [ 3106 _mathValueRecord(h) for h in correctionHeights 3107 ] 3108 kern.KernValue = [_mathValueRecord(v) for v in kernValues] 3109 setattr(record, f"{side}MathKern", kern) 3110 info.MathKernInfo.MathKernInfoRecords.append(record) 3111 3112 return info 3113 3114 3115def _buildMathVariants( 3116 glyphMap, 3117 minConnectorOverlap, 3118 vertGlyphVariants, 3119 horizGlyphVariants, 3120 vertGlyphAssembly, 3121 horizGlyphAssembly, 3122): 3123 if not any( 3124 [vertGlyphVariants, horizGlyphVariants, vertGlyphAssembly, horizGlyphAssembly] 3125 ): 3126 return None 3127 3128 variants = ot.MathVariants() 3129 variants.populateDefaults() 3130 3131 variants.MinConnectorOverlap = minConnectorOverlap 3132 3133 if vertGlyphVariants or vertGlyphAssembly: 3134 variants.VertGlyphCoverage, variants.VertGlyphConstruction = ( 3135 _buildMathGlyphConstruction( 3136 glyphMap, 3137 vertGlyphVariants, 3138 vertGlyphAssembly, 3139 ) 3140 ) 3141 3142 if horizGlyphVariants or horizGlyphAssembly: 3143 variants.HorizGlyphCoverage, variants.HorizGlyphConstruction = ( 3144 _buildMathGlyphConstruction( 3145 glyphMap, 3146 horizGlyphVariants, 3147 horizGlyphAssembly, 3148 ) 3149 ) 3150 3151 return variants 3152 3153 3154def _buildMathGlyphConstruction(glyphMap, variants, assemblies): 3155 glyphs = set() 3156 if variants: 3157 glyphs.update(variants.keys()) 3158 if assemblies: 3159 glyphs.update(assemblies.keys()) 3160 coverage = buildCoverage(glyphs, glyphMap) 3161 constructions = [] 3162 3163 for glyphName in coverage.glyphs: 3164 construction = ot.MathGlyphConstruction() 3165 construction.populateDefaults() 3166 3167 if variants and glyphName in variants: 3168 construction.VariantCount = len(variants[glyphName]) 3169 construction.MathGlyphVariantRecord = [] 3170 for variantName, advance in variants[glyphName]: 3171 record = ot.MathGlyphVariantRecord() 3172 record.VariantGlyph = variantName 3173 record.AdvanceMeasurement = otRound(advance) 3174 construction.MathGlyphVariantRecord.append(record) 3175 3176 if assemblies and glyphName in assemblies: 3177 parts, ic = assemblies[glyphName] 3178 construction.GlyphAssembly = ot.GlyphAssembly() 3179 construction.GlyphAssembly.ItalicsCorrection = _mathValueRecord(ic) 3180 construction.GlyphAssembly.PartCount = len(parts) 3181 construction.GlyphAssembly.PartRecords = [] 3182 for part in parts: 3183 part_name, flags, start, end, advance = part 3184 record = ot.GlyphPartRecord() 3185 record.glyph = part_name 3186 record.PartFlags = int(flags) 3187 record.StartConnectorLength = otRound(start) 3188 record.EndConnectorLength = otRound(end) 3189 record.FullAdvance = otRound(advance) 3190 construction.GlyphAssembly.PartRecords.append(record) 3191 3192 constructions.append(construction) 3193 3194 return coverage, constructions 3195 3196 3197def _mathValueRecord(value): 3198 value_record = ot.MathValueRecord() 3199 value_record.Value = otRound(value) 3200 return value_record 3201