1""" 2Merge OpenType Layout tables (GDEF / GPOS / GSUB). 3""" 4 5import os 6import copy 7import enum 8from operator import ior 9import logging 10from fontTools.colorLib.builder import MAX_PAINT_COLR_LAYER_COUNT, LayerReuseCache 11from fontTools.misc import classifyTools 12from fontTools.misc.roundTools import otRound 13from fontTools.misc.treeTools import build_n_ary_tree 14from fontTools.ttLib.tables import otTables as ot 15from fontTools.ttLib.tables import otBase as otBase 16from fontTools.ttLib.tables.otConverters import BaseFixedValue 17from fontTools.ttLib.tables.otTraverse import dfs_base_table 18from fontTools.ttLib.tables.DefaultTable import DefaultTable 19from fontTools.varLib import builder, models, varStore 20from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo, subList 21from fontTools.varLib.varStore import VarStoreInstancer 22from functools import reduce 23from fontTools.otlLib.builder import buildSinglePos 24from fontTools.otlLib.optimize.gpos import ( 25 _compression_level_from_env, 26 compact_pair_pos, 27) 28 29log = logging.getLogger("fontTools.varLib.merger") 30 31from .errors import ( 32 ShouldBeConstant, 33 FoundANone, 34 MismatchedTypes, 35 NotANone, 36 LengthsDiffer, 37 KeysDiffer, 38 InconsistentGlyphOrder, 39 InconsistentExtensions, 40 InconsistentFormats, 41 UnsupportedFormat, 42 VarLibMergeError, 43) 44 45 46class Merger(object): 47 def __init__(self, font=None): 48 self.font = font 49 # mergeTables populates this from the parent's master ttfs 50 self.ttfs = None 51 52 @classmethod 53 def merger(celf, clazzes, attrs=(None,)): 54 assert celf != Merger, "Subclass Merger instead." 55 if "mergers" not in celf.__dict__: 56 celf.mergers = {} 57 if type(clazzes) in (type, enum.EnumMeta): 58 clazzes = (clazzes,) 59 if type(attrs) == str: 60 attrs = (attrs,) 61 62 def wrapper(method): 63 assert method.__name__ == "merge" 64 done = [] 65 for clazz in clazzes: 66 if clazz in done: 67 continue # Support multiple names of a clazz 68 done.append(clazz) 69 mergers = celf.mergers.setdefault(clazz, {}) 70 for attr in attrs: 71 assert attr not in mergers, ( 72 "Oops, class '%s' has merge function for '%s' defined already." 73 % (clazz.__name__, attr) 74 ) 75 mergers[attr] = method 76 return None 77 78 return wrapper 79 80 @classmethod 81 def mergersFor(celf, thing, _default={}): 82 typ = type(thing) 83 84 for celf in celf.mro(): 85 mergers = getattr(celf, "mergers", None) 86 if mergers is None: 87 break 88 89 m = celf.mergers.get(typ, None) 90 if m is not None: 91 return m 92 93 return _default 94 95 def mergeObjects(self, out, lst, exclude=()): 96 if hasattr(out, "ensureDecompiled"): 97 out.ensureDecompiled(recurse=False) 98 for item in lst: 99 if hasattr(item, "ensureDecompiled"): 100 item.ensureDecompiled(recurse=False) 101 keys = sorted(vars(out).keys()) 102 if not all(keys == sorted(vars(v).keys()) for v in lst): 103 raise KeysDiffer( 104 self, expected=keys, got=[sorted(vars(v).keys()) for v in lst] 105 ) 106 mergers = self.mergersFor(out) 107 defaultMerger = mergers.get("*", self.__class__.mergeThings) 108 try: 109 for key in keys: 110 if key in exclude: 111 continue 112 value = getattr(out, key) 113 values = [getattr(table, key) for table in lst] 114 mergerFunc = mergers.get(key, defaultMerger) 115 mergerFunc(self, value, values) 116 except VarLibMergeError as e: 117 e.stack.append("." + key) 118 raise 119 120 def mergeLists(self, out, lst): 121 if not allEqualTo(out, lst, len): 122 raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst]) 123 for i, (value, values) in enumerate(zip(out, zip(*lst))): 124 try: 125 self.mergeThings(value, values) 126 except VarLibMergeError as e: 127 e.stack.append("[%d]" % i) 128 raise 129 130 def mergeThings(self, out, lst): 131 if not allEqualTo(out, lst, type): 132 raise MismatchedTypes( 133 self, expected=type(out).__name__, got=[type(x).__name__ for x in lst] 134 ) 135 mergerFunc = self.mergersFor(out).get(None, None) 136 if mergerFunc is not None: 137 mergerFunc(self, out, lst) 138 elif isinstance(out, enum.Enum): 139 # need to special-case Enums as have __dict__ but are not regular 'objects', 140 # otherwise mergeObjects/mergeThings get trapped in a RecursionError 141 if not allEqualTo(out, lst): 142 raise ShouldBeConstant(self, expected=out, got=lst) 143 elif hasattr(out, "__dict__"): 144 self.mergeObjects(out, lst) 145 elif isinstance(out, list): 146 self.mergeLists(out, lst) 147 else: 148 if not allEqualTo(out, lst): 149 raise ShouldBeConstant(self, expected=out, got=lst) 150 151 def mergeTables(self, font, master_ttfs, tableTags): 152 for tag in tableTags: 153 if tag not in font: 154 continue 155 try: 156 self.ttfs = master_ttfs 157 self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs]) 158 except VarLibMergeError as e: 159 e.stack.append(tag) 160 raise 161 162 163# 164# Aligning merger 165# 166class AligningMerger(Merger): 167 pass 168 169 170@AligningMerger.merger(ot.GDEF, "GlyphClassDef") 171def merge(merger, self, lst): 172 if self is None: 173 if not allNone(lst): 174 raise NotANone(merger, expected=None, got=lst) 175 return 176 177 lst = [l.classDefs for l in lst] 178 self.classDefs = {} 179 # We only care about the .classDefs 180 self = self.classDefs 181 182 allKeys = set() 183 allKeys.update(*[l.keys() for l in lst]) 184 for k in allKeys: 185 allValues = nonNone(l.get(k) for l in lst) 186 if not allEqual(allValues): 187 raise ShouldBeConstant( 188 merger, expected=allValues[0], got=lst, stack=["." + k] 189 ) 190 if not allValues: 191 self[k] = None 192 else: 193 self[k] = allValues[0] 194 195 196def _SinglePosUpgradeToFormat2(self): 197 if self.Format == 2: 198 return self 199 200 ret = ot.SinglePos() 201 ret.Format = 2 202 ret.Coverage = self.Coverage 203 ret.ValueFormat = self.ValueFormat 204 ret.Value = [self.Value for _ in ret.Coverage.glyphs] 205 ret.ValueCount = len(ret.Value) 206 207 return ret 208 209 210def _merge_GlyphOrders(font, lst, values_lst=None, default=None): 211 """Takes font and list of glyph lists (must be sorted by glyph id), and returns 212 two things: 213 - Combined glyph list, 214 - If values_lst is None, return input glyph lists, but padded with None when a glyph 215 was missing in a list. Otherwise, return values_lst list-of-list, padded with None 216 to match combined glyph lists. 217 """ 218 if values_lst is None: 219 dict_sets = [set(l) for l in lst] 220 else: 221 dict_sets = [{g: v for g, v in zip(l, vs)} for l, vs in zip(lst, values_lst)] 222 combined = set() 223 combined.update(*dict_sets) 224 225 sortKey = font.getReverseGlyphMap().__getitem__ 226 order = sorted(combined, key=sortKey) 227 # Make sure all input glyphsets were in proper order 228 if not all(sorted(vs, key=sortKey) == vs for vs in lst): 229 raise InconsistentGlyphOrder() 230 del combined 231 232 paddedValues = None 233 if values_lst is None: 234 padded = [ 235 [glyph if glyph in dict_set else default for glyph in order] 236 for dict_set in dict_sets 237 ] 238 else: 239 assert len(lst) == len(values_lst) 240 padded = [ 241 [dict_set[glyph] if glyph in dict_set else default for glyph in order] 242 for dict_set in dict_sets 243 ] 244 return order, padded 245 246 247@AligningMerger.merger(otBase.ValueRecord) 248def merge(merger, self, lst): 249 # Code below sometimes calls us with self being 250 # a new object. Copy it from lst and recurse. 251 self.__dict__ = lst[0].__dict__.copy() 252 merger.mergeObjects(self, lst) 253 254 255@AligningMerger.merger(ot.Anchor) 256def merge(merger, self, lst): 257 # Code below sometimes calls us with self being 258 # a new object. Copy it from lst and recurse. 259 self.__dict__ = lst[0].__dict__.copy() 260 merger.mergeObjects(self, lst) 261 262 263def _Lookup_SinglePos_get_effective_value(merger, subtables, glyph): 264 for self in subtables: 265 if ( 266 self is None 267 or type(self) != ot.SinglePos 268 or self.Coverage is None 269 or glyph not in self.Coverage.glyphs 270 ): 271 continue 272 if self.Format == 1: 273 return self.Value 274 elif self.Format == 2: 275 return self.Value[self.Coverage.glyphs.index(glyph)] 276 else: 277 raise UnsupportedFormat(merger, subtable="single positioning lookup") 278 return None 279 280 281def _Lookup_PairPos_get_effective_value_pair( 282 merger, subtables, firstGlyph, secondGlyph 283): 284 for self in subtables: 285 if ( 286 self is None 287 or type(self) != ot.PairPos 288 or self.Coverage is None 289 or firstGlyph not in self.Coverage.glyphs 290 ): 291 continue 292 if self.Format == 1: 293 ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)] 294 pvr = ps.PairValueRecord 295 for rec in pvr: # TODO Speed up 296 if rec.SecondGlyph == secondGlyph: 297 return rec 298 continue 299 elif self.Format == 2: 300 klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0) 301 klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0) 302 return self.Class1Record[klass1].Class2Record[klass2] 303 else: 304 raise UnsupportedFormat(merger, subtable="pair positioning lookup") 305 return None 306 307 308@AligningMerger.merger(ot.SinglePos) 309def merge(merger, self, lst): 310 self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0) 311 if not (len(lst) == 1 or (valueFormat & ~0xF == 0)): 312 raise UnsupportedFormat(merger, subtable="single positioning lookup") 313 314 # If all have same coverage table and all are format 1, 315 coverageGlyphs = self.Coverage.glyphs 316 if all(v.Format == 1 for v in lst) and all( 317 coverageGlyphs == v.Coverage.glyphs for v in lst 318 ): 319 self.Value = otBase.ValueRecord(valueFormat, self.Value) 320 if valueFormat != 0: 321 # If v.Value is None, it means a kerning of 0; we want 322 # it to participate in the model still. 323 # https://github.com/fonttools/fonttools/issues/3111 324 merger.mergeThings( 325 self.Value, 326 [v.Value if v.Value is not None else otBase.ValueRecord() for v in lst], 327 ) 328 self.ValueFormat = self.Value.getFormat() 329 return 330 331 # Upgrade everything to Format=2 332 self.Format = 2 333 lst = [_SinglePosUpgradeToFormat2(v) for v in lst] 334 335 # Align them 336 glyphs, padded = _merge_GlyphOrders( 337 merger.font, [v.Coverage.glyphs for v in lst], [v.Value for v in lst] 338 ) 339 340 self.Coverage.glyphs = glyphs 341 self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs] 342 self.ValueCount = len(self.Value) 343 344 for i, values in enumerate(padded): 345 for j, glyph in enumerate(glyphs): 346 if values[j] is not None: 347 continue 348 # Fill in value from other subtables 349 # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness 350 # is different between used subtable and current subtable! 351 # TODO(behdad) Check and warn if that happens? 352 v = _Lookup_SinglePos_get_effective_value( 353 merger, merger.lookup_subtables[i], glyph 354 ) 355 if v is None: 356 v = otBase.ValueRecord(valueFormat) 357 values[j] = v 358 359 merger.mergeLists(self.Value, padded) 360 361 # Merge everything else; though, there shouldn't be anything else. :) 362 merger.mergeObjects( 363 self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat") 364 ) 365 self.ValueFormat = reduce( 366 int.__or__, [v.getEffectiveFormat() for v in self.Value], 0 367 ) 368 369 370@AligningMerger.merger(ot.PairSet) 371def merge(merger, self, lst): 372 # Align them 373 glyphs, padded = _merge_GlyphOrders( 374 merger.font, 375 [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], 376 [vs.PairValueRecord for vs in lst], 377 ) 378 379 self.PairValueRecord = pvrs = [] 380 for glyph in glyphs: 381 pvr = ot.PairValueRecord() 382 pvr.SecondGlyph = glyph 383 pvr.Value1 = ( 384 otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None 385 ) 386 pvr.Value2 = ( 387 otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None 388 ) 389 pvrs.append(pvr) 390 self.PairValueCount = len(self.PairValueRecord) 391 392 for i, values in enumerate(padded): 393 for j, glyph in enumerate(glyphs): 394 # Fill in value from other subtables 395 v = ot.PairValueRecord() 396 v.SecondGlyph = glyph 397 if values[j] is not None: 398 vpair = values[j] 399 else: 400 vpair = _Lookup_PairPos_get_effective_value_pair( 401 merger, merger.lookup_subtables[i], self._firstGlyph, glyph 402 ) 403 if vpair is None: 404 v1, v2 = None, None 405 else: 406 v1 = getattr(vpair, "Value1", None) 407 v2 = getattr(vpair, "Value2", None) 408 v.Value1 = ( 409 otBase.ValueRecord(merger.valueFormat1, src=v1) 410 if merger.valueFormat1 411 else None 412 ) 413 v.Value2 = ( 414 otBase.ValueRecord(merger.valueFormat2, src=v2) 415 if merger.valueFormat2 416 else None 417 ) 418 values[j] = v 419 del self._firstGlyph 420 421 merger.mergeLists(self.PairValueRecord, padded) 422 423 424def _PairPosFormat1_merge(self, lst, merger): 425 assert allEqual( 426 [l.ValueFormat2 == 0 for l in lst if l.PairSet] 427 ), "Report bug against fonttools." 428 429 # Merge everything else; makes sure Format is the same. 430 merger.mergeObjects( 431 self, 432 lst, 433 exclude=("Coverage", "PairSet", "PairSetCount", "ValueFormat1", "ValueFormat2"), 434 ) 435 436 empty = ot.PairSet() 437 empty.PairValueRecord = [] 438 empty.PairValueCount = 0 439 440 # Align them 441 glyphs, padded = _merge_GlyphOrders( 442 merger.font, 443 [v.Coverage.glyphs for v in lst], 444 [v.PairSet for v in lst], 445 default=empty, 446 ) 447 448 self.Coverage.glyphs = glyphs 449 self.PairSet = [ot.PairSet() for _ in glyphs] 450 self.PairSetCount = len(self.PairSet) 451 for glyph, ps in zip(glyphs, self.PairSet): 452 ps._firstGlyph = glyph 453 454 merger.mergeLists(self.PairSet, padded) 455 456 457def _ClassDef_invert(self, allGlyphs=None): 458 if isinstance(self, dict): 459 classDefs = self 460 else: 461 classDefs = self.classDefs if self and self.classDefs else {} 462 m = max(classDefs.values()) if classDefs else 0 463 464 ret = [] 465 for _ in range(m + 1): 466 ret.append(set()) 467 468 for k, v in classDefs.items(): 469 ret[v].add(k) 470 471 # Class-0 is special. It's "everything else". 472 if allGlyphs is None: 473 ret[0] = None 474 else: 475 # Limit all classes to glyphs in allGlyphs. 476 # Collect anything without a non-zero class into class=zero. 477 ret[0] = class0 = set(allGlyphs) 478 for s in ret[1:]: 479 s.intersection_update(class0) 480 class0.difference_update(s) 481 482 return ret 483 484 485def _ClassDef_merge_classify(lst, allGlyphses=None): 486 self = ot.ClassDef() 487 self.classDefs = classDefs = {} 488 allGlyphsesWasNone = allGlyphses is None 489 if allGlyphsesWasNone: 490 allGlyphses = [None] * len(lst) 491 492 classifier = classifyTools.Classifier() 493 for classDef, allGlyphs in zip(lst, allGlyphses): 494 sets = _ClassDef_invert(classDef, allGlyphs) 495 if allGlyphs is None: 496 sets = sets[1:] 497 classifier.update(sets) 498 classes = classifier.getClasses() 499 500 if allGlyphsesWasNone: 501 classes.insert(0, set()) 502 503 for i, classSet in enumerate(classes): 504 if i == 0: 505 continue 506 for g in classSet: 507 classDefs[g] = i 508 509 return self, classes 510 511 512def _PairPosFormat2_align_matrices(self, lst, font, transparent=False): 513 matrices = [l.Class1Record for l in lst] 514 515 # Align first classes 516 self.ClassDef1, classes = _ClassDef_merge_classify( 517 [l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst] 518 ) 519 self.Class1Count = len(classes) 520 new_matrices = [] 521 for l, matrix in zip(lst, matrices): 522 nullRow = None 523 coverage = set(l.Coverage.glyphs) 524 classDef1 = l.ClassDef1.classDefs 525 class1Records = [] 526 for classSet in classes: 527 exemplarGlyph = next(iter(classSet)) 528 if exemplarGlyph not in coverage: 529 # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f, 530 # Fixes https://github.com/googlei18n/fontmake/issues/470 531 # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9 532 # when merger becomes selfless. 533 nullRow = None 534 if nullRow is None: 535 nullRow = ot.Class1Record() 536 class2records = nullRow.Class2Record = [] 537 # TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f 538 for _ in range(l.Class2Count): 539 if transparent: 540 rec2 = None 541 else: 542 rec2 = ot.Class2Record() 543 rec2.Value1 = ( 544 otBase.ValueRecord(self.ValueFormat1) 545 if self.ValueFormat1 546 else None 547 ) 548 rec2.Value2 = ( 549 otBase.ValueRecord(self.ValueFormat2) 550 if self.ValueFormat2 551 else None 552 ) 553 class2records.append(rec2) 554 rec1 = nullRow 555 else: 556 klass = classDef1.get(exemplarGlyph, 0) 557 rec1 = matrix[klass] # TODO handle out-of-range? 558 class1Records.append(rec1) 559 new_matrices.append(class1Records) 560 matrices = new_matrices 561 del new_matrices 562 563 # Align second classes 564 self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst]) 565 self.Class2Count = len(classes) 566 new_matrices = [] 567 for l, matrix in zip(lst, matrices): 568 classDef2 = l.ClassDef2.classDefs 569 class1Records = [] 570 for rec1old in matrix: 571 oldClass2Records = rec1old.Class2Record 572 rec1new = ot.Class1Record() 573 class2Records = rec1new.Class2Record = [] 574 for classSet in classes: 575 if not classSet: # class=0 576 rec2 = oldClass2Records[0] 577 else: 578 exemplarGlyph = next(iter(classSet)) 579 klass = classDef2.get(exemplarGlyph, 0) 580 rec2 = oldClass2Records[klass] 581 class2Records.append(copy.deepcopy(rec2)) 582 class1Records.append(rec1new) 583 new_matrices.append(class1Records) 584 matrices = new_matrices 585 del new_matrices 586 587 return matrices 588 589 590def _PairPosFormat2_merge(self, lst, merger): 591 assert allEqual( 592 [l.ValueFormat2 == 0 for l in lst if l.Class1Record] 593 ), "Report bug against fonttools." 594 595 merger.mergeObjects( 596 self, 597 lst, 598 exclude=( 599 "Coverage", 600 "ClassDef1", 601 "Class1Count", 602 "ClassDef2", 603 "Class2Count", 604 "Class1Record", 605 "ValueFormat1", 606 "ValueFormat2", 607 ), 608 ) 609 610 # Align coverages 611 glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst]) 612 self.Coverage.glyphs = glyphs 613 614 # Currently, if the coverage of PairPosFormat2 subtables are different, 615 # we do NOT bother walking down the subtable list when filling in new 616 # rows for alignment. As such, this is only correct if current subtable 617 # is the last subtable in the lookup. Ensure that. 618 # 619 # Note that our canonicalization process merges trailing PairPosFormat2's, 620 # so in reality this is rare. 621 for l, subtables in zip(lst, merger.lookup_subtables): 622 if l.Coverage.glyphs != glyphs: 623 assert l == subtables[-1] 624 625 matrices = _PairPosFormat2_align_matrices(self, lst, merger.font) 626 627 self.Class1Record = list(matrices[0]) # TODO move merger to be selfless 628 merger.mergeLists(self.Class1Record, matrices) 629 630 631@AligningMerger.merger(ot.PairPos) 632def merge(merger, self, lst): 633 merger.valueFormat1 = self.ValueFormat1 = reduce( 634 int.__or__, [l.ValueFormat1 for l in lst], 0 635 ) 636 merger.valueFormat2 = self.ValueFormat2 = reduce( 637 int.__or__, [l.ValueFormat2 for l in lst], 0 638 ) 639 640 if self.Format == 1: 641 _PairPosFormat1_merge(self, lst, merger) 642 elif self.Format == 2: 643 _PairPosFormat2_merge(self, lst, merger) 644 else: 645 raise UnsupportedFormat(merger, subtable="pair positioning lookup") 646 647 del merger.valueFormat1, merger.valueFormat2 648 649 # Now examine the list of value records, and update to the union of format values, 650 # as merge might have created new values. 651 vf1 = 0 652 vf2 = 0 653 if self.Format == 1: 654 for pairSet in self.PairSet: 655 for pairValueRecord in pairSet.PairValueRecord: 656 pv1 = getattr(pairValueRecord, "Value1", None) 657 if pv1 is not None: 658 vf1 |= pv1.getFormat() 659 pv2 = getattr(pairValueRecord, "Value2", None) 660 if pv2 is not None: 661 vf2 |= pv2.getFormat() 662 elif self.Format == 2: 663 for class1Record in self.Class1Record: 664 for class2Record in class1Record.Class2Record: 665 pv1 = getattr(class2Record, "Value1", None) 666 if pv1 is not None: 667 vf1 |= pv1.getFormat() 668 pv2 = getattr(class2Record, "Value2", None) 669 if pv2 is not None: 670 vf2 |= pv2.getFormat() 671 self.ValueFormat1 = vf1 672 self.ValueFormat2 = vf2 673 674 675def _MarkBasePosFormat1_merge(self, lst, merger, Mark="Mark", Base="Base"): 676 self.ClassCount = max(l.ClassCount for l in lst) 677 678 MarkCoverageGlyphs, MarkRecords = _merge_GlyphOrders( 679 merger.font, 680 [getattr(l, Mark + "Coverage").glyphs for l in lst], 681 [getattr(l, Mark + "Array").MarkRecord for l in lst], 682 ) 683 getattr(self, Mark + "Coverage").glyphs = MarkCoverageGlyphs 684 685 BaseCoverageGlyphs, BaseRecords = _merge_GlyphOrders( 686 merger.font, 687 [getattr(l, Base + "Coverage").glyphs for l in lst], 688 [getattr(getattr(l, Base + "Array"), Base + "Record") for l in lst], 689 ) 690 getattr(self, Base + "Coverage").glyphs = BaseCoverageGlyphs 691 692 # MarkArray 693 records = [] 694 for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)): 695 allClasses = [r.Class for r in glyphRecords if r is not None] 696 697 # TODO Right now we require that all marks have same class in 698 # all masters that cover them. This is not required. 699 # 700 # We can relax that by just requiring that all marks that have 701 # the same class in a master, have the same class in every other 702 # master. Indeed, if, say, a sparse master only covers one mark, 703 # that mark probably will get class 0, which would possibly be 704 # different from its class in other masters. 705 # 706 # We can even go further and reclassify marks to support any 707 # input. But, since, it's unlikely that two marks being both, 708 # say, "top" in one master, and one being "top" and other being 709 # "top-right" in another master, we shouldn't do that, as any 710 # failures in that case will probably signify mistakes in the 711 # input masters. 712 713 if not allEqual(allClasses): 714 raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses) 715 else: 716 rec = ot.MarkRecord() 717 rec.Class = allClasses[0] 718 allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords] 719 if allNone(allAnchors): 720 anchor = None 721 else: 722 anchor = ot.Anchor() 723 anchor.Format = 1 724 merger.mergeThings(anchor, allAnchors) 725 rec.MarkAnchor = anchor 726 records.append(rec) 727 array = ot.MarkArray() 728 array.MarkRecord = records 729 array.MarkCount = len(records) 730 setattr(self, Mark + "Array", array) 731 732 # BaseArray 733 records = [] 734 for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)): 735 if allNone(glyphRecords): 736 rec = None 737 else: 738 rec = getattr(ot, Base + "Record")() 739 anchors = [] 740 setattr(rec, Base + "Anchor", anchors) 741 glyphAnchors = [ 742 [] if r is None else getattr(r, Base + "Anchor") for r in glyphRecords 743 ] 744 for l in glyphAnchors: 745 l.extend([None] * (self.ClassCount - len(l))) 746 for allAnchors in zip(*glyphAnchors): 747 if allNone(allAnchors): 748 anchor = None 749 else: 750 anchor = ot.Anchor() 751 anchor.Format = 1 752 merger.mergeThings(anchor, allAnchors) 753 anchors.append(anchor) 754 records.append(rec) 755 array = getattr(ot, Base + "Array")() 756 setattr(array, Base + "Record", records) 757 setattr(array, Base + "Count", len(records)) 758 setattr(self, Base + "Array", array) 759 760 761@AligningMerger.merger(ot.MarkBasePos) 762def merge(merger, self, lst): 763 if not allEqualTo(self.Format, (l.Format for l in lst)): 764 raise InconsistentFormats( 765 merger, 766 subtable="mark-to-base positioning lookup", 767 expected=self.Format, 768 got=[l.Format for l in lst], 769 ) 770 if self.Format == 1: 771 _MarkBasePosFormat1_merge(self, lst, merger) 772 else: 773 raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup") 774 775 776@AligningMerger.merger(ot.MarkMarkPos) 777def merge(merger, self, lst): 778 if not allEqualTo(self.Format, (l.Format for l in lst)): 779 raise InconsistentFormats( 780 merger, 781 subtable="mark-to-mark positioning lookup", 782 expected=self.Format, 783 got=[l.Format for l in lst], 784 ) 785 if self.Format == 1: 786 _MarkBasePosFormat1_merge(self, lst, merger, "Mark1", "Mark2") 787 else: 788 raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup") 789 790 791def _PairSet_flatten(lst, font): 792 self = ot.PairSet() 793 self.Coverage = ot.Coverage() 794 795 # Align them 796 glyphs, padded = _merge_GlyphOrders( 797 font, 798 [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], 799 [vs.PairValueRecord for vs in lst], 800 ) 801 802 self.Coverage.glyphs = glyphs 803 self.PairValueRecord = pvrs = [] 804 for values in zip(*padded): 805 for v in values: 806 if v is not None: 807 pvrs.append(v) 808 break 809 else: 810 assert False 811 self.PairValueCount = len(self.PairValueRecord) 812 813 return self 814 815 816def _Lookup_PairPosFormat1_subtables_flatten(lst, font): 817 assert allEqual( 818 [l.ValueFormat2 == 0 for l in lst if l.PairSet] 819 ), "Report bug against fonttools." 820 821 self = ot.PairPos() 822 self.Format = 1 823 self.Coverage = ot.Coverage() 824 self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) 825 self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) 826 827 # Align them 828 glyphs, padded = _merge_GlyphOrders( 829 font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst] 830 ) 831 832 self.Coverage.glyphs = glyphs 833 self.PairSet = [ 834 _PairSet_flatten([v for v in values if v is not None], font) 835 for values in zip(*padded) 836 ] 837 self.PairSetCount = len(self.PairSet) 838 return self 839 840 841def _Lookup_PairPosFormat2_subtables_flatten(lst, font): 842 assert allEqual( 843 [l.ValueFormat2 == 0 for l in lst if l.Class1Record] 844 ), "Report bug against fonttools." 845 846 self = ot.PairPos() 847 self.Format = 2 848 self.Coverage = ot.Coverage() 849 self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) 850 self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) 851 852 # Align them 853 glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst]) 854 self.Coverage.glyphs = glyphs 855 856 matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True) 857 858 matrix = self.Class1Record = [] 859 for rows in zip(*matrices): 860 row = ot.Class1Record() 861 matrix.append(row) 862 row.Class2Record = [] 863 row = row.Class2Record 864 for cols in zip(*list(r.Class2Record for r in rows)): 865 col = next(iter(c for c in cols if c is not None)) 866 row.append(col) 867 868 return self 869 870 871def _Lookup_PairPos_subtables_canonicalize(lst, font): 872 """Merge multiple Format1 subtables at the beginning of lst, 873 and merge multiple consecutive Format2 subtables that have the same 874 Class2 (ie. were split because of offset overflows). Returns new list.""" 875 lst = list(lst) 876 877 l = len(lst) 878 i = 0 879 while i < l and lst[i].Format == 1: 880 i += 1 881 lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)] 882 883 l = len(lst) 884 i = l 885 while i > 0 and lst[i - 1].Format == 2: 886 i -= 1 887 lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)] 888 889 return lst 890 891 892def _Lookup_SinglePos_subtables_flatten(lst, font, min_inclusive_rec_format): 893 glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst], None) 894 num_glyphs = len(glyphs) 895 new = ot.SinglePos() 896 new.Format = 2 897 new.ValueFormat = min_inclusive_rec_format 898 new.Coverage = ot.Coverage() 899 new.Coverage.glyphs = glyphs 900 new.ValueCount = num_glyphs 901 new.Value = [None] * num_glyphs 902 for singlePos in lst: 903 if singlePos.Format == 1: 904 val_rec = singlePos.Value 905 for gname in singlePos.Coverage.glyphs: 906 i = glyphs.index(gname) 907 new.Value[i] = copy.deepcopy(val_rec) 908 elif singlePos.Format == 2: 909 for j, gname in enumerate(singlePos.Coverage.glyphs): 910 val_rec = singlePos.Value[j] 911 i = glyphs.index(gname) 912 new.Value[i] = copy.deepcopy(val_rec) 913 return [new] 914 915 916@AligningMerger.merger(ot.CursivePos) 917def merge(merger, self, lst): 918 # Align them 919 glyphs, padded = _merge_GlyphOrders( 920 merger.font, 921 [l.Coverage.glyphs for l in lst], 922 [l.EntryExitRecord for l in lst], 923 ) 924 925 self.Format = 1 926 self.Coverage = ot.Coverage() 927 self.Coverage.glyphs = glyphs 928 self.EntryExitRecord = [] 929 for _ in glyphs: 930 rec = ot.EntryExitRecord() 931 rec.EntryAnchor = ot.Anchor() 932 rec.EntryAnchor.Format = 1 933 rec.ExitAnchor = ot.Anchor() 934 rec.ExitAnchor.Format = 1 935 self.EntryExitRecord.append(rec) 936 merger.mergeLists(self.EntryExitRecord, padded) 937 self.EntryExitCount = len(self.EntryExitRecord) 938 939 940@AligningMerger.merger(ot.EntryExitRecord) 941def merge(merger, self, lst): 942 if all(master.EntryAnchor is None for master in lst): 943 self.EntryAnchor = None 944 if all(master.ExitAnchor is None for master in lst): 945 self.ExitAnchor = None 946 merger.mergeObjects(self, lst) 947 948 949@AligningMerger.merger(ot.Lookup) 950def merge(merger, self, lst): 951 subtables = merger.lookup_subtables = [l.SubTable for l in lst] 952 953 # Remove Extension subtables 954 for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]: 955 if not sts: 956 continue 957 if sts[0].__class__.__name__.startswith("Extension"): 958 if not allEqual([st.__class__ for st in sts]): 959 raise InconsistentExtensions( 960 merger, 961 expected="Extension", 962 got=[st.__class__.__name__ for st in sts], 963 ) 964 if not allEqual([st.ExtensionLookupType for st in sts]): 965 raise InconsistentExtensions(merger) 966 l.LookupType = sts[0].ExtensionLookupType 967 new_sts = [st.ExtSubTable for st in sts] 968 del sts[:] 969 sts.extend(new_sts) 970 971 isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos) 972 973 if isPairPos: 974 # AFDKO and feaLib sometimes generate two Format1 subtables instead of one. 975 # Merge those before continuing. 976 # https://github.com/fonttools/fonttools/issues/719 977 self.SubTable = _Lookup_PairPos_subtables_canonicalize( 978 self.SubTable, merger.font 979 ) 980 subtables = merger.lookup_subtables = [ 981 _Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables 982 ] 983 else: 984 isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos) 985 if isSinglePos: 986 numSubtables = [len(st) for st in subtables] 987 if not all([nums == numSubtables[0] for nums in numSubtables]): 988 # Flatten list of SinglePos subtables to single Format 2 subtable, 989 # with all value records set to the rec format type. 990 # We use buildSinglePos() to optimize the lookup after merging. 991 valueFormatList = [t.ValueFormat for st in subtables for t in st] 992 # Find the minimum value record that can accomodate all the singlePos subtables. 993 mirf = reduce(ior, valueFormatList) 994 self.SubTable = _Lookup_SinglePos_subtables_flatten( 995 self.SubTable, merger.font, mirf 996 ) 997 subtables = merger.lookup_subtables = [ 998 _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf) 999 for st in subtables 1000 ] 1001 flattened = True 1002 else: 1003 flattened = False 1004 1005 merger.mergeLists(self.SubTable, subtables) 1006 self.SubTableCount = len(self.SubTable) 1007 1008 if isPairPos: 1009 # If format-1 subtable created during canonicalization is empty, remove it. 1010 assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1 1011 if not self.SubTable[0].Coverage.glyphs: 1012 self.SubTable.pop(0) 1013 self.SubTableCount -= 1 1014 1015 # If format-2 subtable created during canonicalization is empty, remove it. 1016 assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2 1017 if not self.SubTable[-1].Coverage.glyphs: 1018 self.SubTable.pop(-1) 1019 self.SubTableCount -= 1 1020 1021 # Compact the merged subtables 1022 # This is a good moment to do it because the compaction should create 1023 # smaller subtables, which may prevent overflows from happening. 1024 # Keep reading the value from the ENV until ufo2ft switches to the config system 1025 level = merger.font.cfg.get( 1026 "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", 1027 default=_compression_level_from_env(), 1028 ) 1029 if level != 0: 1030 log.info("Compacting GPOS...") 1031 self.SubTable = compact_pair_pos(merger.font, level, self.SubTable) 1032 self.SubTableCount = len(self.SubTable) 1033 1034 elif isSinglePos and flattened: 1035 singlePosTable = self.SubTable[0] 1036 glyphs = singlePosTable.Coverage.glyphs 1037 # We know that singlePosTable is Format 2, as this is set 1038 # in _Lookup_SinglePos_subtables_flatten. 1039 singlePosMapping = { 1040 gname: valRecord for gname, valRecord in zip(glyphs, singlePosTable.Value) 1041 } 1042 self.SubTable = buildSinglePos( 1043 singlePosMapping, merger.font.getReverseGlyphMap() 1044 ) 1045 merger.mergeObjects(self, lst, exclude=["SubTable", "SubTableCount"]) 1046 1047 del merger.lookup_subtables 1048 1049 1050# 1051# InstancerMerger 1052# 1053 1054 1055class InstancerMerger(AligningMerger): 1056 """A merger that takes multiple master fonts, and instantiates 1057 an instance.""" 1058 1059 def __init__(self, font, model, location): 1060 Merger.__init__(self, font) 1061 self.model = model 1062 self.location = location 1063 self.masterScalars = model.getMasterScalars(location) 1064 1065 1066@InstancerMerger.merger(ot.CaretValue) 1067def merge(merger, self, lst): 1068 assert self.Format == 1 1069 Coords = [a.Coordinate for a in lst] 1070 model = merger.model 1071 masterScalars = merger.masterScalars 1072 self.Coordinate = otRound( 1073 model.interpolateFromValuesAndScalars(Coords, masterScalars) 1074 ) 1075 1076 1077@InstancerMerger.merger(ot.Anchor) 1078def merge(merger, self, lst): 1079 assert self.Format == 1 1080 XCoords = [a.XCoordinate for a in lst] 1081 YCoords = [a.YCoordinate for a in lst] 1082 model = merger.model 1083 masterScalars = merger.masterScalars 1084 self.XCoordinate = otRound( 1085 model.interpolateFromValuesAndScalars(XCoords, masterScalars) 1086 ) 1087 self.YCoordinate = otRound( 1088 model.interpolateFromValuesAndScalars(YCoords, masterScalars) 1089 ) 1090 1091 1092@InstancerMerger.merger(otBase.ValueRecord) 1093def merge(merger, self, lst): 1094 model = merger.model 1095 masterScalars = merger.masterScalars 1096 # TODO Handle differing valueformats 1097 for name, tableName in [ 1098 ("XAdvance", "XAdvDevice"), 1099 ("YAdvance", "YAdvDevice"), 1100 ("XPlacement", "XPlaDevice"), 1101 ("YPlacement", "YPlaDevice"), 1102 ]: 1103 assert not hasattr(self, tableName) 1104 1105 if hasattr(self, name): 1106 values = [getattr(a, name, 0) for a in lst] 1107 value = otRound( 1108 model.interpolateFromValuesAndScalars(values, masterScalars) 1109 ) 1110 setattr(self, name, value) 1111 1112 1113# 1114# MutatorMerger 1115# 1116 1117 1118class MutatorMerger(AligningMerger): 1119 """A merger that takes a variable font, and instantiates 1120 an instance. While there's no "merging" to be done per se, 1121 the operation can benefit from many operations that the 1122 aligning merger does.""" 1123 1124 def __init__(self, font, instancer, deleteVariations=True): 1125 Merger.__init__(self, font) 1126 self.instancer = instancer 1127 self.deleteVariations = deleteVariations 1128 1129 1130@MutatorMerger.merger(ot.CaretValue) 1131def merge(merger, self, lst): 1132 # Hack till we become selfless. 1133 self.__dict__ = lst[0].__dict__.copy() 1134 1135 if self.Format != 3: 1136 return 1137 1138 instancer = merger.instancer 1139 dev = self.DeviceTable 1140 if merger.deleteVariations: 1141 del self.DeviceTable 1142 if dev: 1143 assert dev.DeltaFormat == 0x8000 1144 varidx = (dev.StartSize << 16) + dev.EndSize 1145 delta = otRound(instancer[varidx]) 1146 self.Coordinate += delta 1147 1148 if merger.deleteVariations: 1149 self.Format = 1 1150 1151 1152@MutatorMerger.merger(ot.Anchor) 1153def merge(merger, self, lst): 1154 # Hack till we become selfless. 1155 self.__dict__ = lst[0].__dict__.copy() 1156 1157 if self.Format != 3: 1158 return 1159 1160 instancer = merger.instancer 1161 for v in "XY": 1162 tableName = v + "DeviceTable" 1163 if not hasattr(self, tableName): 1164 continue 1165 dev = getattr(self, tableName) 1166 if merger.deleteVariations: 1167 delattr(self, tableName) 1168 if dev is None: 1169 continue 1170 1171 assert dev.DeltaFormat == 0x8000 1172 varidx = (dev.StartSize << 16) + dev.EndSize 1173 delta = otRound(instancer[varidx]) 1174 1175 attr = v + "Coordinate" 1176 setattr(self, attr, getattr(self, attr) + delta) 1177 1178 if merger.deleteVariations: 1179 self.Format = 1 1180 1181 1182@MutatorMerger.merger(otBase.ValueRecord) 1183def merge(merger, self, lst): 1184 # Hack till we become selfless. 1185 self.__dict__ = lst[0].__dict__.copy() 1186 1187 instancer = merger.instancer 1188 for name, tableName in [ 1189 ("XAdvance", "XAdvDevice"), 1190 ("YAdvance", "YAdvDevice"), 1191 ("XPlacement", "XPlaDevice"), 1192 ("YPlacement", "YPlaDevice"), 1193 ]: 1194 if not hasattr(self, tableName): 1195 continue 1196 dev = getattr(self, tableName) 1197 if merger.deleteVariations: 1198 delattr(self, tableName) 1199 if dev is None: 1200 continue 1201 1202 assert dev.DeltaFormat == 0x8000 1203 varidx = (dev.StartSize << 16) + dev.EndSize 1204 delta = otRound(instancer[varidx]) 1205 1206 setattr(self, name, getattr(self, name, 0) + delta) 1207 1208 1209# 1210# VariationMerger 1211# 1212 1213 1214class VariationMerger(AligningMerger): 1215 """A merger that takes multiple master fonts, and builds a 1216 variable font.""" 1217 1218 def __init__(self, model, axisTags, font): 1219 Merger.__init__(self, font) 1220 self.store_builder = varStore.OnlineVarStoreBuilder(axisTags) 1221 self.setModel(model) 1222 1223 def setModel(self, model): 1224 self.model = model 1225 self.store_builder.setModel(model) 1226 1227 def mergeThings(self, out, lst): 1228 masterModel = None 1229 origTTFs = None 1230 if None in lst: 1231 if allNone(lst): 1232 if out is not None: 1233 raise FoundANone(self, got=lst) 1234 return 1235 1236 # temporarily subset the list of master ttfs to the ones for which 1237 # master values are not None 1238 origTTFs = self.ttfs 1239 if self.ttfs: 1240 self.ttfs = subList([v is not None for v in lst], self.ttfs) 1241 1242 masterModel = self.model 1243 model, lst = masterModel.getSubModel(lst) 1244 self.setModel(model) 1245 1246 super(VariationMerger, self).mergeThings(out, lst) 1247 1248 if masterModel: 1249 self.setModel(masterModel) 1250 if origTTFs: 1251 self.ttfs = origTTFs 1252 1253 1254def buildVarDevTable(store_builder, master_values): 1255 if allEqual(master_values): 1256 return master_values[0], None 1257 base, varIdx = store_builder.storeMasters(master_values) 1258 return base, builder.buildVarDevTable(varIdx) 1259 1260 1261@VariationMerger.merger(ot.BaseCoord) 1262def merge(merger, self, lst): 1263 if self.Format != 1: 1264 raise UnsupportedFormat(merger, subtable="a baseline coordinate") 1265 self.Coordinate, DeviceTable = buildVarDevTable( 1266 merger.store_builder, [a.Coordinate for a in lst] 1267 ) 1268 if DeviceTable: 1269 self.Format = 3 1270 self.DeviceTable = DeviceTable 1271 1272 1273@VariationMerger.merger(ot.CaretValue) 1274def merge(merger, self, lst): 1275 if self.Format != 1: 1276 raise UnsupportedFormat(merger, subtable="a caret") 1277 self.Coordinate, DeviceTable = buildVarDevTable( 1278 merger.store_builder, [a.Coordinate for a in lst] 1279 ) 1280 if DeviceTable: 1281 self.Format = 3 1282 self.DeviceTable = DeviceTable 1283 1284 1285@VariationMerger.merger(ot.Anchor) 1286def merge(merger, self, lst): 1287 if self.Format != 1: 1288 raise UnsupportedFormat(merger, subtable="an anchor") 1289 self.XCoordinate, XDeviceTable = buildVarDevTable( 1290 merger.store_builder, [a.XCoordinate for a in lst] 1291 ) 1292 self.YCoordinate, YDeviceTable = buildVarDevTable( 1293 merger.store_builder, [a.YCoordinate for a in lst] 1294 ) 1295 if XDeviceTable or YDeviceTable: 1296 self.Format = 3 1297 self.XDeviceTable = XDeviceTable 1298 self.YDeviceTable = YDeviceTable 1299 1300 1301@VariationMerger.merger(otBase.ValueRecord) 1302def merge(merger, self, lst): 1303 for name, tableName in [ 1304 ("XAdvance", "XAdvDevice"), 1305 ("YAdvance", "YAdvDevice"), 1306 ("XPlacement", "XPlaDevice"), 1307 ("YPlacement", "YPlaDevice"), 1308 ]: 1309 if hasattr(self, name): 1310 value, deviceTable = buildVarDevTable( 1311 merger.store_builder, [getattr(a, name, 0) for a in lst] 1312 ) 1313 setattr(self, name, value) 1314 if deviceTable: 1315 setattr(self, tableName, deviceTable) 1316 1317 1318class COLRVariationMerger(VariationMerger): 1319 """A specialized VariationMerger that takes multiple master fonts containing 1320 COLRv1 tables, and builds a variable COLR font. 1321 1322 COLR tables are special in that variable subtables can be associated with 1323 multiple delta-set indices (via VarIndexBase). 1324 They also contain tables that must change their type (not simply the Format) 1325 as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes 1326 care of that too. 1327 """ 1328 1329 def __init__(self, model, axisTags, font, allowLayerReuse=True): 1330 VariationMerger.__init__(self, model, axisTags, font) 1331 # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase 1332 # between variable tables with same varIdxes. 1333 self.varIndexCache = {} 1334 # flat list of all the varIdxes generated while merging 1335 self.varIdxes = [] 1336 # set of id()s of the subtables that contain variations after merging 1337 # and need to be upgraded to the associated VarType. 1338 self.varTableIds = set() 1339 # we keep these around for rebuilding a LayerList while merging PaintColrLayers 1340 self.layers = [] 1341 self.layerReuseCache = None 1342 if allowLayerReuse: 1343 self.layerReuseCache = LayerReuseCache() 1344 # flag to ensure BaseGlyphList is fully merged before LayerList gets processed 1345 self._doneBaseGlyphs = False 1346 1347 def mergeTables(self, font, master_ttfs, tableTags=("COLR",)): 1348 if "COLR" in tableTags and "COLR" in font: 1349 # The merger modifies the destination COLR table in-place. If this contains 1350 # multiple PaintColrLayers referencing the same layers from LayerList, it's 1351 # a problem because we may risk modifying the same paint more than once, or 1352 # worse, fail while attempting to do that. 1353 # We don't know whether the master COLR table was built with layer reuse 1354 # disabled, thus to be safe we rebuild its LayerList so that it contains only 1355 # unique layers referenced from non-overlapping PaintColrLayers throughout 1356 # the base paint graphs. 1357 self.expandPaintColrLayers(font["COLR"].table) 1358 VariationMerger.mergeTables(self, font, master_ttfs, tableTags) 1359 1360 def checkFormatEnum(self, out, lst, validate=lambda _: True): 1361 fmt = out.Format 1362 formatEnum = out.formatEnum 1363 ok = False 1364 try: 1365 fmt = formatEnum(fmt) 1366 except ValueError: 1367 pass 1368 else: 1369 ok = validate(fmt) 1370 if not ok: 1371 raise UnsupportedFormat(self, subtable=type(out).__name__, value=fmt) 1372 expected = fmt 1373 got = [] 1374 for v in lst: 1375 fmt = getattr(v, "Format", None) 1376 try: 1377 fmt = formatEnum(fmt) 1378 except ValueError: 1379 pass 1380 got.append(fmt) 1381 if not allEqualTo(expected, got): 1382 raise InconsistentFormats( 1383 self, 1384 subtable=type(out).__name__, 1385 expected=expected, 1386 got=got, 1387 ) 1388 return expected 1389 1390 def mergeSparseDict(self, out, lst): 1391 for k in out.keys(): 1392 try: 1393 self.mergeThings(out[k], [v.get(k) for v in lst]) 1394 except VarLibMergeError as e: 1395 e.stack.append(f"[{k!r}]") 1396 raise 1397 1398 def mergeAttrs(self, out, lst, attrs): 1399 for attr in attrs: 1400 value = getattr(out, attr) 1401 values = [getattr(item, attr) for item in lst] 1402 try: 1403 self.mergeThings(value, values) 1404 except VarLibMergeError as e: 1405 e.stack.append(f".{attr}") 1406 raise 1407 1408 def storeMastersForAttr(self, out, lst, attr): 1409 master_values = [getattr(item, attr) for item in lst] 1410 1411 # VarStore treats deltas for fixed-size floats as integers, so we 1412 # must convert master values to int before storing them in the builder 1413 # then back to float. 1414 is_fixed_size_float = False 1415 conv = out.getConverterByName(attr) 1416 if isinstance(conv, BaseFixedValue): 1417 is_fixed_size_float = True 1418 master_values = [conv.toInt(v) for v in master_values] 1419 1420 baseValue = master_values[0] 1421 varIdx = ot.NO_VARIATION_INDEX 1422 if not allEqual(master_values): 1423 baseValue, varIdx = self.store_builder.storeMasters(master_values) 1424 1425 if is_fixed_size_float: 1426 baseValue = conv.fromInt(baseValue) 1427 1428 return baseValue, varIdx 1429 1430 def storeVariationIndices(self, varIdxes) -> int: 1431 # try to reuse an existing VarIndexBase for the same varIdxes, or else 1432 # create a new one 1433 key = tuple(varIdxes) 1434 varIndexBase = self.varIndexCache.get(key) 1435 1436 if varIndexBase is None: 1437 # scan for a full match anywhere in the self.varIdxes 1438 for i in range(len(self.varIdxes) - len(varIdxes) + 1): 1439 if self.varIdxes[i : i + len(varIdxes)] == varIdxes: 1440 self.varIndexCache[key] = varIndexBase = i 1441 break 1442 1443 if varIndexBase is None: 1444 # try find a partial match at the end of the self.varIdxes 1445 for n in range(len(varIdxes) - 1, 0, -1): 1446 if self.varIdxes[-n:] == varIdxes[:n]: 1447 varIndexBase = len(self.varIdxes) - n 1448 self.varIndexCache[key] = varIndexBase 1449 self.varIdxes.extend(varIdxes[n:]) 1450 break 1451 1452 if varIndexBase is None: 1453 # no match found, append at the end 1454 self.varIndexCache[key] = varIndexBase = len(self.varIdxes) 1455 self.varIdxes.extend(varIdxes) 1456 1457 return varIndexBase 1458 1459 def mergeVariableAttrs(self, out, lst, attrs) -> int: 1460 varIndexBase = ot.NO_VARIATION_INDEX 1461 varIdxes = [] 1462 for attr in attrs: 1463 baseValue, varIdx = self.storeMastersForAttr(out, lst, attr) 1464 setattr(out, attr, baseValue) 1465 varIdxes.append(varIdx) 1466 1467 if any(v != ot.NO_VARIATION_INDEX for v in varIdxes): 1468 varIndexBase = self.storeVariationIndices(varIdxes) 1469 1470 return varIndexBase 1471 1472 @classmethod 1473 def convertSubTablesToVarType(cls, table): 1474 for path in dfs_base_table( 1475 table, 1476 skip_root=True, 1477 predicate=lambda path: ( 1478 getattr(type(path[-1].value), "VarType", None) is not None 1479 ), 1480 ): 1481 st = path[-1] 1482 subTable = st.value 1483 varType = type(subTable).VarType 1484 newSubTable = varType() 1485 newSubTable.__dict__.update(subTable.__dict__) 1486 newSubTable.populateDefaults() 1487 parent = path[-2].value 1488 if st.index is not None: 1489 getattr(parent, st.name)[st.index] = newSubTable 1490 else: 1491 setattr(parent, st.name, newSubTable) 1492 1493 @staticmethod 1494 def expandPaintColrLayers(colr): 1495 """Rebuild LayerList without PaintColrLayers reuse. 1496 1497 Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph 1498 which are irrelevant for this); any layers referenced via PaintColrLayers are 1499 collected into a new LayerList and duplicated when reuse is detected, to ensure 1500 that all paints are distinct objects at the end of the process. 1501 PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap 1502 is left. Also, any consecutively nested PaintColrLayers are flattened. 1503 The COLR table's LayerList is replaced with the new unique layers. 1504 A side effect is also that any layer from the old LayerList which is not 1505 referenced by any PaintColrLayers is dropped. 1506 """ 1507 if not colr.LayerList: 1508 # if no LayerList, there's nothing to expand 1509 return 1510 uniqueLayerIDs = set() 1511 newLayerList = [] 1512 for rec in colr.BaseGlyphList.BaseGlyphPaintRecord: 1513 frontier = [rec.Paint] 1514 while frontier: 1515 paint = frontier.pop() 1516 if paint.Format == ot.PaintFormat.PaintColrGlyph: 1517 # don't traverse these, we treat them as constant for merging 1518 continue 1519 elif paint.Format == ot.PaintFormat.PaintColrLayers: 1520 # de-treeify any nested PaintColrLayers, append unique copies to 1521 # the new layer list and update PaintColrLayers index/count 1522 children = list(_flatten_layers(paint, colr)) 1523 first_layer_index = len(newLayerList) 1524 for layer in children: 1525 if id(layer) in uniqueLayerIDs: 1526 layer = copy.deepcopy(layer) 1527 assert id(layer) not in uniqueLayerIDs 1528 newLayerList.append(layer) 1529 uniqueLayerIDs.add(id(layer)) 1530 paint.FirstLayerIndex = first_layer_index 1531 paint.NumLayers = len(children) 1532 else: 1533 children = paint.getChildren(colr) 1534 frontier.extend(reversed(children)) 1535 # sanity check all the new layers are distinct objects 1536 assert len(newLayerList) == len(uniqueLayerIDs) 1537 colr.LayerList.Paint = newLayerList 1538 colr.LayerList.LayerCount = len(newLayerList) 1539 1540 1541@COLRVariationMerger.merger(ot.BaseGlyphList) 1542def merge(merger, self, lst): 1543 # ignore BaseGlyphCount, allow sparse glyph sets across masters 1544 out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord} 1545 masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst] 1546 1547 for i, g in enumerate(out.keys()): 1548 try: 1549 # missing base glyphs don't participate in the merge 1550 merger.mergeThings(out[g], [v.get(g) for v in masters]) 1551 except VarLibMergeError as e: 1552 e.stack.append(f".BaseGlyphPaintRecord[{i}]") 1553 e.cause["location"] = f"base glyph {g!r}" 1554 raise 1555 1556 merger._doneBaseGlyphs = True 1557 1558 1559@COLRVariationMerger.merger(ot.LayerList) 1560def merge(merger, self, lst): 1561 # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers 1562 # found while traversing the paint graphs rooted at BaseGlyphPaintRecords. 1563 assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList" 1564 # Simply flush the final list of layers and go home. 1565 self.LayerCount = len(merger.layers) 1566 self.Paint = merger.layers 1567 1568 1569def _flatten_layers(root, colr): 1570 assert root.Format == ot.PaintFormat.PaintColrLayers 1571 for paint in root.getChildren(colr): 1572 if paint.Format == ot.PaintFormat.PaintColrLayers: 1573 yield from _flatten_layers(paint, colr) 1574 else: 1575 yield paint 1576 1577 1578def _merge_PaintColrLayers(self, out, lst): 1579 # we only enforce that the (flat) number of layers is the same across all masters 1580 # but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets. 1581 1582 out_layers = list(_flatten_layers(out, self.font["COLR"].table)) 1583 1584 # sanity check ttfs are subset to current values (see VariationMerger.mergeThings) 1585 # before matching each master PaintColrLayers to its respective COLR by position 1586 assert len(self.ttfs) == len(lst) 1587 master_layerses = [ 1588 list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table)) 1589 for i in range(len(lst)) 1590 ] 1591 1592 try: 1593 self.mergeLists(out_layers, master_layerses) 1594 except VarLibMergeError as e: 1595 # NOTE: This attribute doesn't actually exist in PaintColrLayers but it's 1596 # handy to have it in the stack trace for debugging. 1597 e.stack.append(".Layers") 1598 raise 1599 1600 # following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers 1601 # but I couldn't find a nice way to share the code between the two... 1602 1603 if self.layerReuseCache is not None: 1604 # successful reuse can make the list smaller 1605 out_layers = self.layerReuseCache.try_reuse(out_layers) 1606 1607 # if the list is still too big we need to tree-fy it 1608 is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT 1609 out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT) 1610 1611 # We now have a tree of sequences with Paint leaves. 1612 # Convert the sequences into PaintColrLayers. 1613 def listToColrLayers(paint): 1614 if isinstance(paint, list): 1615 layers = [listToColrLayers(l) for l in paint] 1616 paint = ot.Paint() 1617 paint.Format = int(ot.PaintFormat.PaintColrLayers) 1618 paint.NumLayers = len(layers) 1619 paint.FirstLayerIndex = len(self.layers) 1620 self.layers.extend(layers) 1621 if self.layerReuseCache is not None: 1622 self.layerReuseCache.add(layers, paint.FirstLayerIndex) 1623 return paint 1624 1625 out_layers = [listToColrLayers(l) for l in out_layers] 1626 1627 if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers: 1628 # special case when the reuse cache finds a single perfect PaintColrLayers match 1629 # (it can only come from a successful reuse, _flatten_layers has gotten rid of 1630 # all nested PaintColrLayers already); we assign it directly and avoid creating 1631 # an extra table 1632 out.NumLayers = out_layers[0].NumLayers 1633 out.FirstLayerIndex = out_layers[0].FirstLayerIndex 1634 else: 1635 out.NumLayers = len(out_layers) 1636 out.FirstLayerIndex = len(self.layers) 1637 1638 self.layers.extend(out_layers) 1639 1640 # Register our parts for reuse provided we aren't a tree 1641 # If we are a tree the leaves registered for reuse and that will suffice 1642 if self.layerReuseCache is not None and not is_tree: 1643 self.layerReuseCache.add(out_layers, out.FirstLayerIndex) 1644 1645 1646@COLRVariationMerger.merger((ot.Paint, ot.ClipBox)) 1647def merge(merger, self, lst): 1648 fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable()) 1649 1650 if fmt is ot.PaintFormat.PaintColrLayers: 1651 _merge_PaintColrLayers(merger, self, lst) 1652 return 1653 1654 varFormat = fmt.as_variable() 1655 1656 varAttrs = () 1657 if varFormat is not None: 1658 varAttrs = otBase.getVariableAttrs(type(self), varFormat) 1659 staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs) 1660 1661 merger.mergeAttrs(self, lst, staticAttrs) 1662 1663 varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) 1664 1665 subTables = [st.value for st in self.iterSubTables()] 1666 1667 # Convert table to variable if itself has variations or any subtables have 1668 isVariable = varIndexBase != ot.NO_VARIATION_INDEX or any( 1669 id(table) in merger.varTableIds for table in subTables 1670 ) 1671 1672 if isVariable: 1673 if varAttrs: 1674 # Some PaintVar* don't have any scalar attributes that can vary, 1675 # only indirect offsets to other variable subtables, thus have 1676 # no VarIndexBase of their own (e.g. PaintVarTransform) 1677 self.VarIndexBase = varIndexBase 1678 1679 if subTables: 1680 # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc. 1681 merger.convertSubTablesToVarType(self) 1682 1683 assert varFormat is not None 1684 self.Format = int(varFormat) 1685 1686 1687@COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop)) 1688def merge(merger, self, lst): 1689 varType = type(self).VarType 1690 1691 varAttrs = otBase.getVariableAttrs(varType) 1692 staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs) 1693 1694 merger.mergeAttrs(self, lst, staticAttrs) 1695 1696 varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) 1697 1698 if varIndexBase != ot.NO_VARIATION_INDEX: 1699 self.VarIndexBase = varIndexBase 1700 # mark as having variations so the parent table will convert to Var{Type} 1701 merger.varTableIds.add(id(self)) 1702 1703 1704@COLRVariationMerger.merger(ot.ColorLine) 1705def merge(merger, self, lst): 1706 merger.mergeAttrs(self, lst, (c.name for c in self.getConverters())) 1707 1708 if any(id(stop) in merger.varTableIds for stop in self.ColorStop): 1709 merger.convertSubTablesToVarType(self) 1710 merger.varTableIds.add(id(self)) 1711 1712 1713@COLRVariationMerger.merger(ot.ClipList, "clips") 1714def merge(merger, self, lst): 1715 # 'sparse' in that we allow non-default masters to omit ClipBox entries 1716 # for some/all glyphs (i.e. they don't participate) 1717 merger.mergeSparseDict(self, lst) 1718