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