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