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