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