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