aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/varLib/merger.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/varLib/merger.py')
-rw-r--r--Lib/fontTools/varLib/merger.py2762
1 files changed, 1464 insertions, 1298 deletions
diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py
index c9a1d3e3..b2c34016 100644
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -21,8 +21,8 @@ from fontTools.varLib.varStore import VarStoreInstancer
from functools import reduce
from fontTools.otlLib.builder import buildSinglePos
from fontTools.otlLib.optimize.gpos import (
- _compression_level_from_env,
- compact_pair_pos,
+ _compression_level_from_env,
+ compact_pair_pos,
)
log = logging.getLogger("fontTools.varLib.merger")
@@ -41,1502 +41,1668 @@ from .errors import (
VarLibMergeError,
)
+
class Merger(object):
+ def __init__(self, font=None):
+ self.font = font
+ # mergeTables populates this from the parent's master ttfs
+ self.ttfs = None
+
+ @classmethod
+ def merger(celf, clazzes, attrs=(None,)):
+ assert celf != Merger, "Subclass Merger instead."
+ if "mergers" not in celf.__dict__:
+ celf.mergers = {}
+ if type(clazzes) in (type, enum.EnumMeta):
+ clazzes = (clazzes,)
+ if type(attrs) == str:
+ attrs = (attrs,)
+
+ def wrapper(method):
+ assert method.__name__ == "merge"
+ done = []
+ for clazz in clazzes:
+ if clazz in done:
+ continue # Support multiple names of a clazz
+ done.append(clazz)
+ mergers = celf.mergers.setdefault(clazz, {})
+ for attr in attrs:
+ assert attr not in mergers, (
+ "Oops, class '%s' has merge function for '%s' defined already."
+ % (clazz.__name__, attr)
+ )
+ mergers[attr] = method
+ return None
+
+ return wrapper
+
+ @classmethod
+ def mergersFor(celf, thing, _default={}):
+ typ = type(thing)
+
+ for celf in celf.mro():
+ mergers = getattr(celf, "mergers", None)
+ if mergers is None:
+ break
+
+ m = celf.mergers.get(typ, None)
+ if m is not None:
+ return m
+
+ return _default
+
+ def mergeObjects(self, out, lst, exclude=()):
+ if hasattr(out, "ensureDecompiled"):
+ out.ensureDecompiled(recurse=False)
+ for item in lst:
+ if hasattr(item, "ensureDecompiled"):
+ item.ensureDecompiled(recurse=False)
+ keys = sorted(vars(out).keys())
+ if not all(keys == sorted(vars(v).keys()) for v in lst):
+ raise KeysDiffer(
+ self, expected=keys, got=[sorted(vars(v).keys()) for v in lst]
+ )
+ mergers = self.mergersFor(out)
+ defaultMerger = mergers.get("*", self.__class__.mergeThings)
+ try:
+ for key in keys:
+ if key in exclude:
+ continue
+ value = getattr(out, key)
+ values = [getattr(table, key) for table in lst]
+ mergerFunc = mergers.get(key, defaultMerger)
+ mergerFunc(self, value, values)
+ except VarLibMergeError as e:
+ e.stack.append("." + key)
+ raise
+
+ def mergeLists(self, out, lst):
+ if not allEqualTo(out, lst, len):
+ raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst])
+ for i, (value, values) in enumerate(zip(out, zip(*lst))):
+ try:
+ self.mergeThings(value, values)
+ except VarLibMergeError as e:
+ e.stack.append("[%d]" % i)
+ raise
+
+ def mergeThings(self, out, lst):
+ if not allEqualTo(out, lst, type):
+ raise MismatchedTypes(
+ self, expected=type(out).__name__, got=[type(x).__name__ for x in lst]
+ )
+ mergerFunc = self.mergersFor(out).get(None, None)
+ if mergerFunc is not None:
+ mergerFunc(self, out, lst)
+ elif isinstance(out, enum.Enum):
+ # need to special-case Enums as have __dict__ but are not regular 'objects',
+ # otherwise mergeObjects/mergeThings get trapped in a RecursionError
+ if not allEqualTo(out, lst):
+ raise ShouldBeConstant(self, expected=out, got=lst)
+ elif hasattr(out, "__dict__"):
+ self.mergeObjects(out, lst)
+ elif isinstance(out, list):
+ self.mergeLists(out, lst)
+ else:
+ if not allEqualTo(out, lst):
+ raise ShouldBeConstant(self, expected=out, got=lst)
+
+ def mergeTables(self, font, master_ttfs, tableTags):
+ for tag in tableTags:
+ if tag not in font:
+ continue
+ try:
+ self.ttfs = master_ttfs
+ self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs])
+ except VarLibMergeError as e:
+ e.stack.append(tag)
+ raise
- def __init__(self, font=None):
- self.font = font
- # mergeTables populates this from the parent's master ttfs
- self.ttfs = None
-
- @classmethod
- def merger(celf, clazzes, attrs=(None,)):
- assert celf != Merger, 'Subclass Merger instead.'
- if 'mergers' not in celf.__dict__:
- celf.mergers = {}
- if type(clazzes) in (type, enum.EnumMeta):
- clazzes = (clazzes,)
- if type(attrs) == str:
- attrs = (attrs,)
- def wrapper(method):
- assert method.__name__ == 'merge'
- done = []
- for clazz in clazzes:
- if clazz in done: continue # Support multiple names of a clazz
- done.append(clazz)
- mergers = celf.mergers.setdefault(clazz, {})
- for attr in attrs:
- assert attr not in mergers, \
- "Oops, class '%s' has merge function for '%s' defined already." % (clazz.__name__, attr)
- mergers[attr] = method
- return None
- return wrapper
-
- @classmethod
- def mergersFor(celf, thing, _default={}):
- typ = type(thing)
-
- for celf in celf.mro():
-
- mergers = getattr(celf, 'mergers', None)
- if mergers is None:
- break;
-
- m = celf.mergers.get(typ, None)
- if m is not None:
- return m
-
- return _default
-
- def mergeObjects(self, out, lst, exclude=()):
- if hasattr(out, "ensureDecompiled"):
- out.ensureDecompiled(recurse=False)
- for item in lst:
- if hasattr(item, "ensureDecompiled"):
- item.ensureDecompiled(recurse=False)
- keys = sorted(vars(out).keys())
- if not all(keys == sorted(vars(v).keys()) for v in lst):
- raise KeysDiffer(self, expected=keys,
- got=[sorted(vars(v).keys()) for v in lst]
- )
- mergers = self.mergersFor(out)
- defaultMerger = mergers.get('*', self.__class__.mergeThings)
- try:
- for key in keys:
- if key in exclude: continue
- value = getattr(out, key)
- values = [getattr(table, key) for table in lst]
- mergerFunc = mergers.get(key, defaultMerger)
- mergerFunc(self, value, values)
- except VarLibMergeError as e:
- e.stack.append('.'+key)
- raise
-
- def mergeLists(self, out, lst):
- if not allEqualTo(out, lst, len):
- raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst])
- for i,(value,values) in enumerate(zip(out, zip(*lst))):
- try:
- self.mergeThings(value, values)
- except VarLibMergeError as e:
- e.stack.append('[%d]' % i)
- raise
-
- def mergeThings(self, out, lst):
- if not allEqualTo(out, lst, type):
- raise MismatchedTypes(self,
- expected=type(out).__name__,
- got=[type(x).__name__ for x in lst]
- )
- mergerFunc = self.mergersFor(out).get(None, None)
- if mergerFunc is not None:
- mergerFunc(self, out, lst)
- elif isinstance(out, enum.Enum):
- # need to special-case Enums as have __dict__ but are not regular 'objects',
- # otherwise mergeObjects/mergeThings get trapped in a RecursionError
- if not allEqualTo(out, lst):
- raise ShouldBeConstant(self, expected=out, got=lst)
- elif hasattr(out, '__dict__'):
- self.mergeObjects(out, lst)
- elif isinstance(out, list):
- self.mergeLists(out, lst)
- else:
- if not allEqualTo(out, lst):
- raise ShouldBeConstant(self, expected=out, got=lst)
-
- def mergeTables(self, font, master_ttfs, tableTags):
- for tag in tableTags:
- if tag not in font: continue
- try:
- self.ttfs = master_ttfs
- self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs])
- except VarLibMergeError as e:
- e.stack.append(tag)
- raise
#
# Aligning merger
#
class AligningMerger(Merger):
- pass
+ pass
+
@AligningMerger.merger(ot.GDEF, "GlyphClassDef")
def merge(merger, self, lst):
- if self is None:
- if not allNone(lst):
- raise NotANone(merger, expected=None, got=lst)
- return
-
- lst = [l.classDefs for l in lst]
- self.classDefs = {}
- # We only care about the .classDefs
- self = self.classDefs
-
- allKeys = set()
- allKeys.update(*[l.keys() for l in lst])
- for k in allKeys:
- allValues = nonNone(l.get(k) for l in lst)
- if not allEqual(allValues):
- raise ShouldBeConstant(merger, expected=allValues[0], got=lst, stack=["." + k])
- if not allValues:
- self[k] = None
- else:
- self[k] = allValues[0]
+ if self is None:
+ if not allNone(lst):
+ raise NotANone(merger, expected=None, got=lst)
+ return
+
+ lst = [l.classDefs for l in lst]
+ self.classDefs = {}
+ # We only care about the .classDefs
+ self = self.classDefs
+
+ allKeys = set()
+ allKeys.update(*[l.keys() for l in lst])
+ for k in allKeys:
+ allValues = nonNone(l.get(k) for l in lst)
+ if not allEqual(allValues):
+ raise ShouldBeConstant(
+ merger, expected=allValues[0], got=lst, stack=["." + k]
+ )
+ if not allValues:
+ self[k] = None
+ else:
+ self[k] = allValues[0]
+
def _SinglePosUpgradeToFormat2(self):
- if self.Format == 2: return self
+ if self.Format == 2:
+ return self
+
+ ret = ot.SinglePos()
+ ret.Format = 2
+ ret.Coverage = self.Coverage
+ ret.ValueFormat = self.ValueFormat
+ ret.Value = [self.Value for _ in ret.Coverage.glyphs]
+ ret.ValueCount = len(ret.Value)
- ret = ot.SinglePos()
- ret.Format = 2
- ret.Coverage = self.Coverage
- ret.ValueFormat = self.ValueFormat
- ret.Value = [self.Value for _ in ret.Coverage.glyphs]
- ret.ValueCount = len(ret.Value)
+ return ret
- return ret
def _merge_GlyphOrders(font, lst, values_lst=None, default=None):
- """Takes font and list of glyph lists (must be sorted by glyph id), and returns
- two things:
- - Combined glyph list,
- - If values_lst is None, return input glyph lists, but padded with None when a glyph
- was missing in a list. Otherwise, return values_lst list-of-list, padded with None
- to match combined glyph lists.
- """
- if values_lst is None:
- dict_sets = [set(l) for l in lst]
- else:
- dict_sets = [{g:v for g,v in zip(l,vs)} for l,vs in zip(lst,values_lst)]
- combined = set()
- combined.update(*dict_sets)
-
- sortKey = font.getReverseGlyphMap().__getitem__
- order = sorted(combined, key=sortKey)
- # Make sure all input glyphsets were in proper order
- if not all(sorted(vs, key=sortKey) == vs for vs in lst):
- raise InconsistentGlyphOrder()
- del combined
-
- paddedValues = None
- if values_lst is None:
- padded = [[glyph if glyph in dict_set else default
- for glyph in order]
- for dict_set in dict_sets]
- else:
- assert len(lst) == len(values_lst)
- padded = [[dict_set[glyph] if glyph in dict_set else default
- for glyph in order]
- for dict_set in dict_sets]
- return order, padded
+ """Takes font and list of glyph lists (must be sorted by glyph id), and returns
+ two things:
+ - Combined glyph list,
+ - If values_lst is None, return input glyph lists, but padded with None when a glyph
+ was missing in a list. Otherwise, return values_lst list-of-list, padded with None
+ to match combined glyph lists.
+ """
+ if values_lst is None:
+ dict_sets = [set(l) for l in lst]
+ else:
+ dict_sets = [{g: v for g, v in zip(l, vs)} for l, vs in zip(lst, values_lst)]
+ combined = set()
+ combined.update(*dict_sets)
+
+ sortKey = font.getReverseGlyphMap().__getitem__
+ order = sorted(combined, key=sortKey)
+ # Make sure all input glyphsets were in proper order
+ if not all(sorted(vs, key=sortKey) == vs for vs in lst):
+ raise InconsistentGlyphOrder()
+ del combined
+
+ paddedValues = None
+ if values_lst is None:
+ padded = [
+ [glyph if glyph in dict_set else default for glyph in order]
+ for dict_set in dict_sets
+ ]
+ else:
+ assert len(lst) == len(values_lst)
+ padded = [
+ [dict_set[glyph] if glyph in dict_set else default for glyph in order]
+ for dict_set in dict_sets
+ ]
+ return order, padded
+
@AligningMerger.merger(otBase.ValueRecord)
def merge(merger, self, lst):
- # Code below sometimes calls us with self being
- # a new object. Copy it from lst and recurse.
- self.__dict__ = lst[0].__dict__.copy()
- merger.mergeObjects(self, lst)
+ # Code below sometimes calls us with self being
+ # a new object. Copy it from lst and recurse.
+ self.__dict__ = lst[0].__dict__.copy()
+ merger.mergeObjects(self, lst)
+
@AligningMerger.merger(ot.Anchor)
def merge(merger, self, lst):
- # Code below sometimes calls us with self being
- # a new object. Copy it from lst and recurse.
- self.__dict__ = lst[0].__dict__.copy()
- merger.mergeObjects(self, lst)
+ # Code below sometimes calls us with self being
+ # a new object. Copy it from lst and recurse.
+ self.__dict__ = lst[0].__dict__.copy()
+ merger.mergeObjects(self, lst)
+
def _Lookup_SinglePos_get_effective_value(merger, subtables, glyph):
- for self in subtables:
- if self is None or \
- type(self) != ot.SinglePos or \
- self.Coverage is None or \
- glyph not in self.Coverage.glyphs:
- continue
- if self.Format == 1:
- return self.Value
- elif self.Format == 2:
- return self.Value[self.Coverage.glyphs.index(glyph)]
- else:
- raise UnsupportedFormat(merger, subtable="single positioning lookup")
- return None
-
-def _Lookup_PairPos_get_effective_value_pair(merger, subtables, firstGlyph, secondGlyph):
- for self in subtables:
- if self is None or \
- type(self) != ot.PairPos or \
- self.Coverage is None or \
- firstGlyph not in self.Coverage.glyphs:
- continue
- if self.Format == 1:
- ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)]
- pvr = ps.PairValueRecord
- for rec in pvr: # TODO Speed up
- if rec.SecondGlyph == secondGlyph:
- return rec
- continue
- elif self.Format == 2:
- klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0)
- klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0)
- return self.Class1Record[klass1].Class2Record[klass2]
- else:
- raise UnsupportedFormat(merger, subtable="pair positioning lookup")
- return None
+ for self in subtables:
+ if (
+ self is None
+ or type(self) != ot.SinglePos
+ or self.Coverage is None
+ or glyph not in self.Coverage.glyphs
+ ):
+ continue
+ if self.Format == 1:
+ return self.Value
+ elif self.Format == 2:
+ return self.Value[self.Coverage.glyphs.index(glyph)]
+ else:
+ raise UnsupportedFormat(merger, subtable="single positioning lookup")
+ return None
+
+
+def _Lookup_PairPos_get_effective_value_pair(
+ merger, subtables, firstGlyph, secondGlyph
+):
+ for self in subtables:
+ if (
+ self is None
+ or type(self) != ot.PairPos
+ or self.Coverage is None
+ or firstGlyph not in self.Coverage.glyphs
+ ):
+ continue
+ if self.Format == 1:
+ ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)]
+ pvr = ps.PairValueRecord
+ for rec in pvr: # TODO Speed up
+ if rec.SecondGlyph == secondGlyph:
+ return rec
+ continue
+ elif self.Format == 2:
+ klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0)
+ klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0)
+ return self.Class1Record[klass1].Class2Record[klass2]
+ else:
+ raise UnsupportedFormat(merger, subtable="pair positioning lookup")
+ return None
+
@AligningMerger.merger(ot.SinglePos)
def merge(merger, self, lst):
- self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0)
- if not (len(lst) == 1 or (valueFormat & ~0xF == 0)):
- raise UnsupportedFormat(merger, subtable="single positioning lookup")
-
- # If all have same coverage table and all are format 1,
- coverageGlyphs = self.Coverage.glyphs
- if all(v.Format == 1 for v in lst) and all(coverageGlyphs == v.Coverage.glyphs for v in lst):
- self.Value = otBase.ValueRecord(valueFormat, self.Value)
- if valueFormat != 0:
- merger.mergeThings(self.Value, [v.Value for v in lst])
- self.ValueFormat = self.Value.getFormat()
- return
-
- # Upgrade everything to Format=2
- self.Format = 2
- lst = [_SinglePosUpgradeToFormat2(v) for v in lst]
-
- # Align them
- glyphs, padded = _merge_GlyphOrders(merger.font,
- [v.Coverage.glyphs for v in lst],
- [v.Value for v in lst])
-
- self.Coverage.glyphs = glyphs
- self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs]
- self.ValueCount = len(self.Value)
-
- for i,values in enumerate(padded):
- for j,glyph in enumerate(glyphs):
- if values[j] is not None: continue
- # Fill in value from other subtables
- # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness
- # is different between used subtable and current subtable!
- # TODO(behdad) Check and warn if that happens?
- v = _Lookup_SinglePos_get_effective_value(merger, merger.lookup_subtables[i], glyph)
- if v is None:
- v = otBase.ValueRecord(valueFormat)
- values[j] = v
-
- merger.mergeLists(self.Value, padded)
-
- # Merge everything else; though, there shouldn't be anything else. :)
- merger.mergeObjects(self, lst,
- exclude=('Format', 'Coverage', 'Value', 'ValueCount', 'ValueFormat'))
- self.ValueFormat = reduce(int.__or__, [v.getEffectiveFormat() for v in self.Value], 0)
+ self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0)
+ if not (len(lst) == 1 or (valueFormat & ~0xF == 0)):
+ raise UnsupportedFormat(merger, subtable="single positioning lookup")
+
+ # If all have same coverage table and all are format 1,
+ coverageGlyphs = self.Coverage.glyphs
+ if all(v.Format == 1 for v in lst) and all(
+ coverageGlyphs == v.Coverage.glyphs for v in lst
+ ):
+ self.Value = otBase.ValueRecord(valueFormat, self.Value)
+ if valueFormat != 0:
+ # If v.Value is None, it means a kerning of 0; we want
+ # it to participate in the model still.
+ # https://github.com/fonttools/fonttools/issues/3111
+ merger.mergeThings(
+ self.Value,
+ [v.Value if v.Value is not None else otBase.ValueRecord() for v in lst],
+ )
+ self.ValueFormat = self.Value.getFormat()
+ return
+
+ # Upgrade everything to Format=2
+ self.Format = 2
+ lst = [_SinglePosUpgradeToFormat2(v) for v in lst]
+
+ # Align them
+ glyphs, padded = _merge_GlyphOrders(
+ merger.font, [v.Coverage.glyphs for v in lst], [v.Value for v in lst]
+ )
+
+ self.Coverage.glyphs = glyphs
+ self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs]
+ self.ValueCount = len(self.Value)
+
+ for i, values in enumerate(padded):
+ for j, glyph in enumerate(glyphs):
+ if values[j] is not None:
+ continue
+ # Fill in value from other subtables
+ # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness
+ # is different between used subtable and current subtable!
+ # TODO(behdad) Check and warn if that happens?
+ v = _Lookup_SinglePos_get_effective_value(
+ merger, merger.lookup_subtables[i], glyph
+ )
+ if v is None:
+ v = otBase.ValueRecord(valueFormat)
+ values[j] = v
+
+ merger.mergeLists(self.Value, padded)
+
+ # Merge everything else; though, there shouldn't be anything else. :)
+ merger.mergeObjects(
+ self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat")
+ )
+ self.ValueFormat = reduce(
+ int.__or__, [v.getEffectiveFormat() for v in self.Value], 0
+ )
+
@AligningMerger.merger(ot.PairSet)
def merge(merger, self, lst):
- # Align them
- glyphs, padded = _merge_GlyphOrders(merger.font,
- [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
- [vs.PairValueRecord for vs in lst])
-
- self.PairValueRecord = pvrs = []
- for glyph in glyphs:
- pvr = ot.PairValueRecord()
- pvr.SecondGlyph = glyph
- pvr.Value1 = otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None
- pvr.Value2 = otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None
- pvrs.append(pvr)
- self.PairValueCount = len(self.PairValueRecord)
-
- for i,values in enumerate(padded):
- for j,glyph in enumerate(glyphs):
- # Fill in value from other subtables
- v = ot.PairValueRecord()
- v.SecondGlyph = glyph
- if values[j] is not None:
- vpair = values[j]
- else:
- vpair = _Lookup_PairPos_get_effective_value_pair(
- merger, merger.lookup_subtables[i], self._firstGlyph, glyph
- )
- if vpair is None:
- v1, v2 = None, None
- else:
- v1 = getattr(vpair, "Value1", None)
- v2 = getattr(vpair, "Value2", None)
- v.Value1 = otBase.ValueRecord(merger.valueFormat1, src=v1) if merger.valueFormat1 else None
- v.Value2 = otBase.ValueRecord(merger.valueFormat2, src=v2) if merger.valueFormat2 else None
- values[j] = v
- del self._firstGlyph
-
- merger.mergeLists(self.PairValueRecord, padded)
+ # Align them
+ glyphs, padded = _merge_GlyphOrders(
+ merger.font,
+ [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
+ [vs.PairValueRecord for vs in lst],
+ )
+
+ self.PairValueRecord = pvrs = []
+ for glyph in glyphs:
+ pvr = ot.PairValueRecord()
+ pvr.SecondGlyph = glyph
+ pvr.Value1 = (
+ otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None
+ )
+ pvr.Value2 = (
+ otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None
+ )
+ pvrs.append(pvr)
+ self.PairValueCount = len(self.PairValueRecord)
+
+ for i, values in enumerate(padded):
+ for j, glyph in enumerate(glyphs):
+ # Fill in value from other subtables
+ v = ot.PairValueRecord()
+ v.SecondGlyph = glyph
+ if values[j] is not None:
+ vpair = values[j]
+ else:
+ vpair = _Lookup_PairPos_get_effective_value_pair(
+ merger, merger.lookup_subtables[i], self._firstGlyph, glyph
+ )
+ if vpair is None:
+ v1, v2 = None, None
+ else:
+ v1 = getattr(vpair, "Value1", None)
+ v2 = getattr(vpair, "Value2", None)
+ v.Value1 = (
+ otBase.ValueRecord(merger.valueFormat1, src=v1)
+ if merger.valueFormat1
+ else None
+ )
+ v.Value2 = (
+ otBase.ValueRecord(merger.valueFormat2, src=v2)
+ if merger.valueFormat2
+ else None
+ )
+ values[j] = v
+ del self._firstGlyph
+
+ merger.mergeLists(self.PairValueRecord, padded)
+
def _PairPosFormat1_merge(self, lst, merger):
- assert allEqual([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools."
+ assert allEqual(
+ [l.ValueFormat2 == 0 for l in lst if l.PairSet]
+ ), "Report bug against fonttools."
+
+ # Merge everything else; makes sure Format is the same.
+ merger.mergeObjects(
+ self,
+ lst,
+ exclude=("Coverage", "PairSet", "PairSetCount", "ValueFormat1", "ValueFormat2"),
+ )
+
+ empty = ot.PairSet()
+ empty.PairValueRecord = []
+ empty.PairValueCount = 0
+
+ # Align them
+ glyphs, padded = _merge_GlyphOrders(
+ merger.font,
+ [v.Coverage.glyphs for v in lst],
+ [v.PairSet for v in lst],
+ default=empty,
+ )
+
+ self.Coverage.glyphs = glyphs
+ self.PairSet = [ot.PairSet() for _ in glyphs]
+ self.PairSetCount = len(self.PairSet)
+ for glyph, ps in zip(glyphs, self.PairSet):
+ ps._firstGlyph = glyph
+
+ merger.mergeLists(self.PairSet, padded)
- # Merge everything else; makes sure Format is the same.
- merger.mergeObjects(self, lst,
- exclude=('Coverage',
- 'PairSet', 'PairSetCount',
- 'ValueFormat1', 'ValueFormat2'))
- empty = ot.PairSet()
- empty.PairValueRecord = []
- empty.PairValueCount = 0
+def _ClassDef_invert(self, allGlyphs=None):
+ if isinstance(self, dict):
+ classDefs = self
+ else:
+ classDefs = self.classDefs if self and self.classDefs else {}
+ m = max(classDefs.values()) if classDefs else 0
- # Align them
- glyphs, padded = _merge_GlyphOrders(merger.font,
- [v.Coverage.glyphs for v in lst],
- [v.PairSet for v in lst],
- default=empty)
+ ret = []
+ for _ in range(m + 1):
+ ret.append(set())
- self.Coverage.glyphs = glyphs
- self.PairSet = [ot.PairSet() for _ in glyphs]
- self.PairSetCount = len(self.PairSet)
- for glyph, ps in zip(glyphs, self.PairSet):
- ps._firstGlyph = glyph
+ for k, v in classDefs.items():
+ ret[v].add(k)
- merger.mergeLists(self.PairSet, padded)
+ # Class-0 is special. It's "everything else".
+ if allGlyphs is None:
+ ret[0] = None
+ else:
+ # Limit all classes to glyphs in allGlyphs.
+ # Collect anything without a non-zero class into class=zero.
+ ret[0] = class0 = set(allGlyphs)
+ for s in ret[1:]:
+ s.intersection_update(class0)
+ class0.difference_update(s)
-def _ClassDef_invert(self, allGlyphs=None):
+ return ret
- if isinstance(self, dict):
- classDefs = self
- else:
- classDefs = self.classDefs if self and self.classDefs else {}
- m = max(classDefs.values()) if classDefs else 0
- ret = []
- for _ in range(m + 1):
- ret.append(set())
+def _ClassDef_merge_classify(lst, allGlyphses=None):
+ self = ot.ClassDef()
+ self.classDefs = classDefs = {}
+ allGlyphsesWasNone = allGlyphses is None
+ if allGlyphsesWasNone:
+ allGlyphses = [None] * len(lst)
- for k,v in classDefs.items():
- ret[v].add(k)
+ classifier = classifyTools.Classifier()
+ for classDef, allGlyphs in zip(lst, allGlyphses):
+ sets = _ClassDef_invert(classDef, allGlyphs)
+ if allGlyphs is None:
+ sets = sets[1:]
+ classifier.update(sets)
+ classes = classifier.getClasses()
- # Class-0 is special. It's "everything else".
- if allGlyphs is None:
- ret[0] = None
- else:
- # Limit all classes to glyphs in allGlyphs.
- # Collect anything without a non-zero class into class=zero.
- ret[0] = class0 = set(allGlyphs)
- for s in ret[1:]:
- s.intersection_update(class0)
- class0.difference_update(s)
+ if allGlyphsesWasNone:
+ classes.insert(0, set())
- return ret
+ for i, classSet in enumerate(classes):
+ if i == 0:
+ continue
+ for g in classSet:
+ classDefs[g] = i
+
+ return self, classes
-def _ClassDef_merge_classify(lst, allGlyphses=None):
- self = ot.ClassDef()
- self.classDefs = classDefs = {}
- allGlyphsesWasNone = allGlyphses is None
- if allGlyphsesWasNone:
- allGlyphses = [None] * len(lst)
-
- classifier = classifyTools.Classifier()
- for classDef,allGlyphs in zip(lst, allGlyphses):
- sets = _ClassDef_invert(classDef, allGlyphs)
- if allGlyphs is None:
- sets = sets[1:]
- classifier.update(sets)
- classes = classifier.getClasses()
-
- if allGlyphsesWasNone:
- classes.insert(0, set())
-
- for i,classSet in enumerate(classes):
- if i == 0:
- continue
- for g in classSet:
- classDefs[g] = i
-
- return self, classes
def _PairPosFormat2_align_matrices(self, lst, font, transparent=False):
+ matrices = [l.Class1Record for l in lst]
+
+ # Align first classes
+ self.ClassDef1, classes = _ClassDef_merge_classify(
+ [l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst]
+ )
+ self.Class1Count = len(classes)
+ new_matrices = []
+ for l, matrix in zip(lst, matrices):
+ nullRow = None
+ coverage = set(l.Coverage.glyphs)
+ classDef1 = l.ClassDef1.classDefs
+ class1Records = []
+ for classSet in classes:
+ exemplarGlyph = next(iter(classSet))
+ if exemplarGlyph not in coverage:
+ # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f,
+ # Fixes https://github.com/googlei18n/fontmake/issues/470
+ # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9
+ # when merger becomes selfless.
+ nullRow = None
+ if nullRow is None:
+ nullRow = ot.Class1Record()
+ class2records = nullRow.Class2Record = []
+ # TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f
+ for _ in range(l.Class2Count):
+ if transparent:
+ rec2 = None
+ else:
+ rec2 = ot.Class2Record()
+ rec2.Value1 = (
+ otBase.ValueRecord(self.ValueFormat1)
+ if self.ValueFormat1
+ else None
+ )
+ rec2.Value2 = (
+ otBase.ValueRecord(self.ValueFormat2)
+ if self.ValueFormat2
+ else None
+ )
+ class2records.append(rec2)
+ rec1 = nullRow
+ else:
+ klass = classDef1.get(exemplarGlyph, 0)
+ rec1 = matrix[klass] # TODO handle out-of-range?
+ class1Records.append(rec1)
+ new_matrices.append(class1Records)
+ matrices = new_matrices
+ del new_matrices
+
+ # Align second classes
+ self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst])
+ self.Class2Count = len(classes)
+ new_matrices = []
+ for l, matrix in zip(lst, matrices):
+ classDef2 = l.ClassDef2.classDefs
+ class1Records = []
+ for rec1old in matrix:
+ oldClass2Records = rec1old.Class2Record
+ rec1new = ot.Class1Record()
+ class2Records = rec1new.Class2Record = []
+ for classSet in classes:
+ if not classSet: # class=0
+ rec2 = oldClass2Records[0]
+ else:
+ exemplarGlyph = next(iter(classSet))
+ klass = classDef2.get(exemplarGlyph, 0)
+ rec2 = oldClass2Records[klass]
+ class2Records.append(copy.deepcopy(rec2))
+ class1Records.append(rec1new)
+ new_matrices.append(class1Records)
+ matrices = new_matrices
+ del new_matrices
+
+ return matrices
- matrices = [l.Class1Record for l in lst]
-
- # Align first classes
- self.ClassDef1, classes = _ClassDef_merge_classify([l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst])
- self.Class1Count = len(classes)
- new_matrices = []
- for l,matrix in zip(lst, matrices):
- nullRow = None
- coverage = set(l.Coverage.glyphs)
- classDef1 = l.ClassDef1.classDefs
- class1Records = []
- for classSet in classes:
- exemplarGlyph = next(iter(classSet))
- if exemplarGlyph not in coverage:
- # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f,
- # Fixes https://github.com/googlei18n/fontmake/issues/470
- # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9
- # when merger becomes selfless.
- nullRow = None
- if nullRow is None:
- nullRow = ot.Class1Record()
- class2records = nullRow.Class2Record = []
- # TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f
- for _ in range(l.Class2Count):
- if transparent:
- rec2 = None
- else:
- rec2 = ot.Class2Record()
- rec2.Value1 = otBase.ValueRecord(self.ValueFormat1) if self.ValueFormat1 else None
- rec2.Value2 = otBase.ValueRecord(self.ValueFormat2) if self.ValueFormat2 else None
- class2records.append(rec2)
- rec1 = nullRow
- else:
- klass = classDef1.get(exemplarGlyph, 0)
- rec1 = matrix[klass] # TODO handle out-of-range?
- class1Records.append(rec1)
- new_matrices.append(class1Records)
- matrices = new_matrices
- del new_matrices
-
- # Align second classes
- self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst])
- self.Class2Count = len(classes)
- new_matrices = []
- for l,matrix in zip(lst, matrices):
- classDef2 = l.ClassDef2.classDefs
- class1Records = []
- for rec1old in matrix:
- oldClass2Records = rec1old.Class2Record
- rec1new = ot.Class1Record()
- class2Records = rec1new.Class2Record = []
- for classSet in classes:
- if not classSet: # class=0
- rec2 = oldClass2Records[0]
- else:
- exemplarGlyph = next(iter(classSet))
- klass = classDef2.get(exemplarGlyph, 0)
- rec2 = oldClass2Records[klass]
- class2Records.append(copy.deepcopy(rec2))
- class1Records.append(rec1new)
- new_matrices.append(class1Records)
- matrices = new_matrices
- del new_matrices
-
- return matrices
def _PairPosFormat2_merge(self, lst, merger):
- assert allEqual([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools."
-
- merger.mergeObjects(self, lst,
- exclude=('Coverage',
- 'ClassDef1', 'Class1Count',
- 'ClassDef2', 'Class2Count',
- 'Class1Record',
- 'ValueFormat1', 'ValueFormat2'))
-
- # Align coverages
- glyphs, _ = _merge_GlyphOrders(merger.font,
- [v.Coverage.glyphs for v in lst])
- self.Coverage.glyphs = glyphs
-
- # Currently, if the coverage of PairPosFormat2 subtables are different,
- # we do NOT bother walking down the subtable list when filling in new
- # rows for alignment. As such, this is only correct if current subtable
- # is the last subtable in the lookup. Ensure that.
- #
- # Note that our canonicalization process merges trailing PairPosFormat2's,
- # so in reality this is rare.
- for l,subtables in zip(lst,merger.lookup_subtables):
- if l.Coverage.glyphs != glyphs:
- assert l == subtables[-1]
-
- matrices = _PairPosFormat2_align_matrices(self, lst, merger.font)
-
- self.Class1Record = list(matrices[0]) # TODO move merger to be selfless
- merger.mergeLists(self.Class1Record, matrices)
+ assert allEqual(
+ [l.ValueFormat2 == 0 for l in lst if l.Class1Record]
+ ), "Report bug against fonttools."
+
+ merger.mergeObjects(
+ self,
+ lst,
+ exclude=(
+ "Coverage",
+ "ClassDef1",
+ "Class1Count",
+ "ClassDef2",
+ "Class2Count",
+ "Class1Record",
+ "ValueFormat1",
+ "ValueFormat2",
+ ),
+ )
+
+ # Align coverages
+ glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst])
+ self.Coverage.glyphs = glyphs
+
+ # Currently, if the coverage of PairPosFormat2 subtables are different,
+ # we do NOT bother walking down the subtable list when filling in new
+ # rows for alignment. As such, this is only correct if current subtable
+ # is the last subtable in the lookup. Ensure that.
+ #
+ # Note that our canonicalization process merges trailing PairPosFormat2's,
+ # so in reality this is rare.
+ for l, subtables in zip(lst, merger.lookup_subtables):
+ if l.Coverage.glyphs != glyphs:
+ assert l == subtables[-1]
+
+ matrices = _PairPosFormat2_align_matrices(self, lst, merger.font)
+
+ self.Class1Record = list(matrices[0]) # TODO move merger to be selfless
+ merger.mergeLists(self.Class1Record, matrices)
+
@AligningMerger.merger(ot.PairPos)
def merge(merger, self, lst):
- merger.valueFormat1 = self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
- merger.valueFormat2 = self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
-
- if self.Format == 1:
- _PairPosFormat1_merge(self, lst, merger)
- elif self.Format == 2:
- _PairPosFormat2_merge(self, lst, merger)
- else:
- raise UnsupportedFormat(merger, subtable="pair positioning lookup")
-
- del merger.valueFormat1, merger.valueFormat2
-
- # Now examine the list of value records, and update to the union of format values,
- # as merge might have created new values.
- vf1 = 0
- vf2 = 0
- if self.Format == 1:
- for pairSet in self.PairSet:
- for pairValueRecord in pairSet.PairValueRecord:
- pv1 = getattr(pairValueRecord, "Value1", None)
- if pv1 is not None:
- vf1 |= pv1.getFormat()
- pv2 = getattr(pairValueRecord, "Value2", None)
- if pv2 is not None:
- vf2 |= pv2.getFormat()
- elif self.Format == 2:
- for class1Record in self.Class1Record:
- for class2Record in class1Record.Class2Record:
- pv1 = getattr(class2Record, "Value1", None)
- if pv1 is not None:
- vf1 |= pv1.getFormat()
- pv2 = getattr(class2Record, "Value2", None)
- if pv2 is not None:
- vf2 |= pv2.getFormat()
- self.ValueFormat1 = vf1
- self.ValueFormat2 = vf2
-
-def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'):
- self.ClassCount = max(l.ClassCount for l in lst)
-
- MarkCoverageGlyphs, MarkRecords = \
- _merge_GlyphOrders(merger.font,
- [getattr(l, Mark+'Coverage').glyphs for l in lst],
- [getattr(l, Mark+'Array').MarkRecord for l in lst])
- getattr(self, Mark+'Coverage').glyphs = MarkCoverageGlyphs
-
- BaseCoverageGlyphs, BaseRecords = \
- _merge_GlyphOrders(merger.font,
- [getattr(l, Base+'Coverage').glyphs for l in lst],
- [getattr(getattr(l, Base+'Array'), Base+'Record') for l in lst])
- getattr(self, Base+'Coverage').glyphs = BaseCoverageGlyphs
-
- # MarkArray
- records = []
- for g,glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)):
- allClasses = [r.Class for r in glyphRecords if r is not None]
-
- # TODO Right now we require that all marks have same class in
- # all masters that cover them. This is not required.
- #
- # We can relax that by just requiring that all marks that have
- # the same class in a master, have the same class in every other
- # master. Indeed, if, say, a sparse master only covers one mark,
- # that mark probably will get class 0, which would possibly be
- # different from its class in other masters.
- #
- # We can even go further and reclassify marks to support any
- # input. But, since, it's unlikely that two marks being both,
- # say, "top" in one master, and one being "top" and other being
- # "top-right" in another master, we shouldn't do that, as any
- # failures in that case will probably signify mistakes in the
- # input masters.
-
- if not allEqual(allClasses):
- raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses)
- else:
- rec = ot.MarkRecord()
- rec.Class = allClasses[0]
- allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords]
- if allNone(allAnchors):
- anchor = None
- else:
- anchor = ot.Anchor()
- anchor.Format = 1
- merger.mergeThings(anchor, allAnchors)
- rec.MarkAnchor = anchor
- records.append(rec)
- array = ot.MarkArray()
- array.MarkRecord = records
- array.MarkCount = len(records)
- setattr(self, Mark+"Array", array)
-
- # BaseArray
- records = []
- for g,glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)):
- if allNone(glyphRecords):
- rec = None
- else:
- rec = getattr(ot, Base+'Record')()
- anchors = []
- setattr(rec, Base+'Anchor', anchors)
- glyphAnchors = [[] if r is None else getattr(r, Base+'Anchor')
- for r in glyphRecords]
- for l in glyphAnchors:
- l.extend([None] * (self.ClassCount - len(l)))
- for allAnchors in zip(*glyphAnchors):
- if allNone(allAnchors):
- anchor = None
- else:
- anchor = ot.Anchor()
- anchor.Format = 1
- merger.mergeThings(anchor, allAnchors)
- anchors.append(anchor)
- records.append(rec)
- array = getattr(ot, Base+'Array')()
- setattr(array, Base+'Record', records)
- setattr(array, Base+'Count', len(records))
- setattr(self, Base+'Array', array)
+ merger.valueFormat1 = self.ValueFormat1 = reduce(
+ int.__or__, [l.ValueFormat1 for l in lst], 0
+ )
+ merger.valueFormat2 = self.ValueFormat2 = reduce(
+ int.__or__, [l.ValueFormat2 for l in lst], 0
+ )
+
+ if self.Format == 1:
+ _PairPosFormat1_merge(self, lst, merger)
+ elif self.Format == 2:
+ _PairPosFormat2_merge(self, lst, merger)
+ else:
+ raise UnsupportedFormat(merger, subtable="pair positioning lookup")
+
+ del merger.valueFormat1, merger.valueFormat2
+
+ # Now examine the list of value records, and update to the union of format values,
+ # as merge might have created new values.
+ vf1 = 0
+ vf2 = 0
+ if self.Format == 1:
+ for pairSet in self.PairSet:
+ for pairValueRecord in pairSet.PairValueRecord:
+ pv1 = getattr(pairValueRecord, "Value1", None)
+ if pv1 is not None:
+ vf1 |= pv1.getFormat()
+ pv2 = getattr(pairValueRecord, "Value2", None)
+ if pv2 is not None:
+ vf2 |= pv2.getFormat()
+ elif self.Format == 2:
+ for class1Record in self.Class1Record:
+ for class2Record in class1Record.Class2Record:
+ pv1 = getattr(class2Record, "Value1", None)
+ if pv1 is not None:
+ vf1 |= pv1.getFormat()
+ pv2 = getattr(class2Record, "Value2", None)
+ if pv2 is not None:
+ vf2 |= pv2.getFormat()
+ self.ValueFormat1 = vf1
+ self.ValueFormat2 = vf2
+
+
+def _MarkBasePosFormat1_merge(self, lst, merger, Mark="Mark", Base="Base"):
+ self.ClassCount = max(l.ClassCount for l in lst)
+
+ MarkCoverageGlyphs, MarkRecords = _merge_GlyphOrders(
+ merger.font,
+ [getattr(l, Mark + "Coverage").glyphs for l in lst],
+ [getattr(l, Mark + "Array").MarkRecord for l in lst],
+ )
+ getattr(self, Mark + "Coverage").glyphs = MarkCoverageGlyphs
+
+ BaseCoverageGlyphs, BaseRecords = _merge_GlyphOrders(
+ merger.font,
+ [getattr(l, Base + "Coverage").glyphs for l in lst],
+ [getattr(getattr(l, Base + "Array"), Base + "Record") for l in lst],
+ )
+ getattr(self, Base + "Coverage").glyphs = BaseCoverageGlyphs
+
+ # MarkArray
+ records = []
+ for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)):
+ allClasses = [r.Class for r in glyphRecords if r is not None]
+
+ # TODO Right now we require that all marks have same class in
+ # all masters that cover them. This is not required.
+ #
+ # We can relax that by just requiring that all marks that have
+ # the same class in a master, have the same class in every other
+ # master. Indeed, if, say, a sparse master only covers one mark,
+ # that mark probably will get class 0, which would possibly be
+ # different from its class in other masters.
+ #
+ # We can even go further and reclassify marks to support any
+ # input. But, since, it's unlikely that two marks being both,
+ # say, "top" in one master, and one being "top" and other being
+ # "top-right" in another master, we shouldn't do that, as any
+ # failures in that case will probably signify mistakes in the
+ # input masters.
+
+ if not allEqual(allClasses):
+ raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses)
+ else:
+ rec = ot.MarkRecord()
+ rec.Class = allClasses[0]
+ allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords]
+ if allNone(allAnchors):
+ anchor = None
+ else:
+ anchor = ot.Anchor()
+ anchor.Format = 1
+ merger.mergeThings(anchor, allAnchors)
+ rec.MarkAnchor = anchor
+ records.append(rec)
+ array = ot.MarkArray()
+ array.MarkRecord = records
+ array.MarkCount = len(records)
+ setattr(self, Mark + "Array", array)
+
+ # BaseArray
+ records = []
+ for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)):
+ if allNone(glyphRecords):
+ rec = None
+ else:
+ rec = getattr(ot, Base + "Record")()
+ anchors = []
+ setattr(rec, Base + "Anchor", anchors)
+ glyphAnchors = [
+ [] if r is None else getattr(r, Base + "Anchor") for r in glyphRecords
+ ]
+ for l in glyphAnchors:
+ l.extend([None] * (self.ClassCount - len(l)))
+ for allAnchors in zip(*glyphAnchors):
+ if allNone(allAnchors):
+ anchor = None
+ else:
+ anchor = ot.Anchor()
+ anchor.Format = 1
+ merger.mergeThings(anchor, allAnchors)
+ anchors.append(anchor)
+ records.append(rec)
+ array = getattr(ot, Base + "Array")()
+ setattr(array, Base + "Record", records)
+ setattr(array, Base + "Count", len(records))
+ setattr(self, Base + "Array", array)
+
@AligningMerger.merger(ot.MarkBasePos)
def merge(merger, self, lst):
- if not allEqualTo(self.Format, (l.Format for l in lst)):
- raise InconsistentFormats(
- merger,
- subtable="mark-to-base positioning lookup",
- expected=self.Format,
- got=[l.Format for l in lst]
- )
- if self.Format == 1:
- _MarkBasePosFormat1_merge(self, lst, merger)
- else:
- raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup")
+ if not allEqualTo(self.Format, (l.Format for l in lst)):
+ raise InconsistentFormats(
+ merger,
+ subtable="mark-to-base positioning lookup",
+ expected=self.Format,
+ got=[l.Format for l in lst],
+ )
+ if self.Format == 1:
+ _MarkBasePosFormat1_merge(self, lst, merger)
+ else:
+ raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup")
+
@AligningMerger.merger(ot.MarkMarkPos)
def merge(merger, self, lst):
- if not allEqualTo(self.Format, (l.Format for l in lst)):
- raise InconsistentFormats(
- merger,
- subtable="mark-to-mark positioning lookup",
- expected=self.Format,
- got=[l.Format for l in lst]
- )
- if self.Format == 1:
- _MarkBasePosFormat1_merge(self, lst, merger, 'Mark1', 'Mark2')
- else:
- raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup")
+ if not allEqualTo(self.Format, (l.Format for l in lst)):
+ raise InconsistentFormats(
+ merger,
+ subtable="mark-to-mark positioning lookup",
+ expected=self.Format,
+ got=[l.Format for l in lst],
+ )
+ if self.Format == 1:
+ _MarkBasePosFormat1_merge(self, lst, merger, "Mark1", "Mark2")
+ else:
+ raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup")
-def _PairSet_flatten(lst, font):
- self = ot.PairSet()
- self.Coverage = ot.Coverage()
-
- # Align them
- glyphs, padded = _merge_GlyphOrders(font,
- [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
- [vs.PairValueRecord for vs in lst])
-
- self.Coverage.glyphs = glyphs
- self.PairValueRecord = pvrs = []
- for values in zip(*padded):
- for v in values:
- if v is not None:
- pvrs.append(v)
- break
- else:
- assert False
- self.PairValueCount = len(self.PairValueRecord)
-
- return self
-def _Lookup_PairPosFormat1_subtables_flatten(lst, font):
- assert allEqual([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools."
+def _PairSet_flatten(lst, font):
+ self = ot.PairSet()
+ self.Coverage = ot.Coverage()
+
+ # Align them
+ glyphs, padded = _merge_GlyphOrders(
+ font,
+ [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
+ [vs.PairValueRecord for vs in lst],
+ )
+
+ self.Coverage.glyphs = glyphs
+ self.PairValueRecord = pvrs = []
+ for values in zip(*padded):
+ for v in values:
+ if v is not None:
+ pvrs.append(v)
+ break
+ else:
+ assert False
+ self.PairValueCount = len(self.PairValueRecord)
+
+ return self
- self = ot.PairPos()
- self.Format = 1
- self.Coverage = ot.Coverage()
- self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
- self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
- # Align them
- glyphs, padded = _merge_GlyphOrders(font,
- [v.Coverage.glyphs for v in lst],
- [v.PairSet for v in lst])
+def _Lookup_PairPosFormat1_subtables_flatten(lst, font):
+ assert allEqual(
+ [l.ValueFormat2 == 0 for l in lst if l.PairSet]
+ ), "Report bug against fonttools."
+
+ self = ot.PairPos()
+ self.Format = 1
+ self.Coverage = ot.Coverage()
+ self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
+ self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
+
+ # Align them
+ glyphs, padded = _merge_GlyphOrders(
+ font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst]
+ )
+
+ self.Coverage.glyphs = glyphs
+ self.PairSet = [
+ _PairSet_flatten([v for v in values if v is not None], font)
+ for values in zip(*padded)
+ ]
+ self.PairSetCount = len(self.PairSet)
+ return self
- self.Coverage.glyphs = glyphs
- self.PairSet = [_PairSet_flatten([v for v in values if v is not None], font)
- for values in zip(*padded)]
- self.PairSetCount = len(self.PairSet)
- return self
def _Lookup_PairPosFormat2_subtables_flatten(lst, font):
- assert allEqual([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools."
+ assert allEqual(
+ [l.ValueFormat2 == 0 for l in lst if l.Class1Record]
+ ), "Report bug against fonttools."
+
+ self = ot.PairPos()
+ self.Format = 2
+ self.Coverage = ot.Coverage()
+ self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
+ self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
- self = ot.PairPos()
- self.Format = 2
- self.Coverage = ot.Coverage()
- self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
- self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
+ # Align them
+ glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst])
+ self.Coverage.glyphs = glyphs
- # Align them
- glyphs, _ = _merge_GlyphOrders(font,
- [v.Coverage.glyphs for v in lst])
- self.Coverage.glyphs = glyphs
+ matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True)
- matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True)
+ matrix = self.Class1Record = []
+ for rows in zip(*matrices):
+ row = ot.Class1Record()
+ matrix.append(row)
+ row.Class2Record = []
+ row = row.Class2Record
+ for cols in zip(*list(r.Class2Record for r in rows)):
+ col = next(iter(c for c in cols if c is not None))
+ row.append(col)
- matrix = self.Class1Record = []
- for rows in zip(*matrices):
- row = ot.Class1Record()
- matrix.append(row)
- row.Class2Record = []
- row = row.Class2Record
- for cols in zip(*list(r.Class2Record for r in rows)):
- col = next(iter(c for c in cols if c is not None))
- row.append(col)
+ return self
- return self
def _Lookup_PairPos_subtables_canonicalize(lst, font):
- """Merge multiple Format1 subtables at the beginning of lst,
- and merge multiple consecutive Format2 subtables that have the same
- Class2 (ie. were split because of offset overflows). Returns new list."""
- lst = list(lst)
+ """Merge multiple Format1 subtables at the beginning of lst,
+ and merge multiple consecutive Format2 subtables that have the same
+ Class2 (ie. were split because of offset overflows). Returns new list."""
+ lst = list(lst)
- l = len(lst)
- i = 0
- while i < l and lst[i].Format == 1:
- i += 1
- lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)]
+ l = len(lst)
+ i = 0
+ while i < l and lst[i].Format == 1:
+ i += 1
+ lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)]
- l = len(lst)
- i = l
- while i > 0 and lst[i - 1].Format == 2:
- i -= 1
- lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)]
+ l = len(lst)
+ i = l
+ while i > 0 and lst[i - 1].Format == 2:
+ i -= 1
+ lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)]
+
+ return lst
- return lst
def _Lookup_SinglePos_subtables_flatten(lst, font, min_inclusive_rec_format):
- glyphs, _ = _merge_GlyphOrders(font,
- [v.Coverage.glyphs for v in lst], None)
- num_glyphs = len(glyphs)
- new = ot.SinglePos()
- new.Format = 2
- new.ValueFormat = min_inclusive_rec_format
- new.Coverage = ot.Coverage()
- new.Coverage.glyphs = glyphs
- new.ValueCount = num_glyphs
- new.Value = [None] * num_glyphs
- for singlePos in lst:
- if singlePos.Format == 1:
- val_rec = singlePos.Value
- for gname in singlePos.Coverage.glyphs:
- i = glyphs.index(gname)
- new.Value[i] = copy.deepcopy(val_rec)
- elif singlePos.Format == 2:
- for j, gname in enumerate(singlePos.Coverage.glyphs):
- val_rec = singlePos.Value[j]
- i = glyphs.index(gname)
- new.Value[i] = copy.deepcopy(val_rec)
- return [new]
+ glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst], None)
+ num_glyphs = len(glyphs)
+ new = ot.SinglePos()
+ new.Format = 2
+ new.ValueFormat = min_inclusive_rec_format
+ new.Coverage = ot.Coverage()
+ new.Coverage.glyphs = glyphs
+ new.ValueCount = num_glyphs
+ new.Value = [None] * num_glyphs
+ for singlePos in lst:
+ if singlePos.Format == 1:
+ val_rec = singlePos.Value
+ for gname in singlePos.Coverage.glyphs:
+ i = glyphs.index(gname)
+ new.Value[i] = copy.deepcopy(val_rec)
+ elif singlePos.Format == 2:
+ for j, gname in enumerate(singlePos.Coverage.glyphs):
+ val_rec = singlePos.Value[j]
+ i = glyphs.index(gname)
+ new.Value[i] = copy.deepcopy(val_rec)
+ return [new]
+
+
+@AligningMerger.merger(ot.CursivePos)
+def merge(merger, self, lst):
+ # Align them
+ glyphs, padded = _merge_GlyphOrders(
+ merger.font,
+ [l.Coverage.glyphs for l in lst],
+ [l.EntryExitRecord for l in lst],
+ )
+
+ self.Format = 1
+ self.Coverage = ot.Coverage()
+ self.Coverage.glyphs = glyphs
+ self.EntryExitRecord = []
+ for _ in glyphs:
+ rec = ot.EntryExitRecord()
+ rec.EntryAnchor = ot.Anchor()
+ rec.EntryAnchor.Format = 1
+ rec.ExitAnchor = ot.Anchor()
+ rec.ExitAnchor.Format = 1
+ self.EntryExitRecord.append(rec)
+ merger.mergeLists(self.EntryExitRecord, padded)
+ self.EntryExitCount = len(self.EntryExitRecord)
+
+
+@AligningMerger.merger(ot.EntryExitRecord)
+def merge(merger, self, lst):
+ if all(master.EntryAnchor is None for master in lst):
+ self.EntryAnchor = None
+ if all(master.ExitAnchor is None for master in lst):
+ self.ExitAnchor = None
+ merger.mergeObjects(self, lst)
+
@AligningMerger.merger(ot.Lookup)
def merge(merger, self, lst):
- subtables = merger.lookup_subtables = [l.SubTable for l in lst]
-
- # Remove Extension subtables
- for l,sts in list(zip(lst,subtables))+[(self,self.SubTable)]:
- if not sts:
- continue
- if sts[0].__class__.__name__.startswith('Extension'):
- if not allEqual([st.__class__ for st in sts]):
- raise InconsistentExtensions(
- merger,
- expected="Extension",
- got=[st.__class__.__name__ for st in sts]
- )
- if not allEqual([st.ExtensionLookupType for st in sts]):
- raise InconsistentExtensions(merger)
- l.LookupType = sts[0].ExtensionLookupType
- new_sts = [st.ExtSubTable for st in sts]
- del sts[:]
- sts.extend(new_sts)
-
- isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos)
-
- if isPairPos:
- # AFDKO and feaLib sometimes generate two Format1 subtables instead of one.
- # Merge those before continuing.
- # https://github.com/fonttools/fonttools/issues/719
- self.SubTable = _Lookup_PairPos_subtables_canonicalize(self.SubTable, merger.font)
- subtables = merger.lookup_subtables = [_Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables]
- else:
- isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos)
- if isSinglePos:
- numSubtables = [len(st) for st in subtables]
- if not all([nums == numSubtables[0] for nums in numSubtables]):
- # Flatten list of SinglePos subtables to single Format 2 subtable,
- # with all value records set to the rec format type.
- # We use buildSinglePos() to optimize the lookup after merging.
- valueFormatList = [t.ValueFormat for st in subtables for t in st]
- # Find the minimum value record that can accomodate all the singlePos subtables.
- mirf = reduce(ior, valueFormatList)
- self.SubTable = _Lookup_SinglePos_subtables_flatten(self.SubTable, merger.font, mirf)
- subtables = merger.lookup_subtables = [
- _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf) for st in subtables]
- flattened = True
- else:
- flattened = False
-
- merger.mergeLists(self.SubTable, subtables)
- self.SubTableCount = len(self.SubTable)
-
- if isPairPos:
- # If format-1 subtable created during canonicalization is empty, remove it.
- assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1
- if not self.SubTable[0].Coverage.glyphs:
- self.SubTable.pop(0)
- self.SubTableCount -= 1
-
- # If format-2 subtable created during canonicalization is empty, remove it.
- assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2
- if not self.SubTable[-1].Coverage.glyphs:
- self.SubTable.pop(-1)
- self.SubTableCount -= 1
-
- # Compact the merged subtables
- # This is a good moment to do it because the compaction should create
- # smaller subtables, which may prevent overflows from happening.
- # Keep reading the value from the ENV until ufo2ft switches to the config system
- level = merger.font.cfg.get(
- "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
- default=_compression_level_from_env(),
+ subtables = merger.lookup_subtables = [l.SubTable for l in lst]
+
+ # Remove Extension subtables
+ for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]:
+ if not sts:
+ continue
+ if sts[0].__class__.__name__.startswith("Extension"):
+ if not allEqual([st.__class__ for st in sts]):
+ raise InconsistentExtensions(
+ merger,
+ expected="Extension",
+ got=[st.__class__.__name__ for st in sts],
+ )
+ if not allEqual([st.ExtensionLookupType for st in sts]):
+ raise InconsistentExtensions(merger)
+ l.LookupType = sts[0].ExtensionLookupType
+ new_sts = [st.ExtSubTable for st in sts]
+ del sts[:]
+ sts.extend(new_sts)
+
+ isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos)
+
+ if isPairPos:
+ # AFDKO and feaLib sometimes generate two Format1 subtables instead of one.
+ # Merge those before continuing.
+ # https://github.com/fonttools/fonttools/issues/719
+ self.SubTable = _Lookup_PairPos_subtables_canonicalize(
+ self.SubTable, merger.font
)
- if level != 0:
- log.info("Compacting GPOS...")
- self.SubTable = compact_pair_pos(merger.font, level, self.SubTable)
- self.SubTableCount = len(self.SubTable)
-
- elif isSinglePos and flattened:
- singlePosTable = self.SubTable[0]
- glyphs = singlePosTable.Coverage.glyphs
- # We know that singlePosTable is Format 2, as this is set
- # in _Lookup_SinglePos_subtables_flatten.
- singlePosMapping = {
- gname: valRecord
- for gname, valRecord in zip(glyphs, singlePosTable.Value)
- }
- self.SubTable = buildSinglePos(singlePosMapping, merger.font.getReverseGlyphMap())
- merger.mergeObjects(self, lst, exclude=['SubTable', 'SubTableCount'])
-
- del merger.lookup_subtables
+ subtables = merger.lookup_subtables = [
+ _Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables
+ ]
+ else:
+ isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos)
+ if isSinglePos:
+ numSubtables = [len(st) for st in subtables]
+ if not all([nums == numSubtables[0] for nums in numSubtables]):
+ # Flatten list of SinglePos subtables to single Format 2 subtable,
+ # with all value records set to the rec format type.
+ # We use buildSinglePos() to optimize the lookup after merging.
+ valueFormatList = [t.ValueFormat for st in subtables for t in st]
+ # Find the minimum value record that can accomodate all the singlePos subtables.
+ mirf = reduce(ior, valueFormatList)
+ self.SubTable = _Lookup_SinglePos_subtables_flatten(
+ self.SubTable, merger.font, mirf
+ )
+ subtables = merger.lookup_subtables = [
+ _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf)
+ for st in subtables
+ ]
+ flattened = True
+ else:
+ flattened = False
+
+ merger.mergeLists(self.SubTable, subtables)
+ self.SubTableCount = len(self.SubTable)
+
+ if isPairPos:
+ # If format-1 subtable created during canonicalization is empty, remove it.
+ assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1
+ if not self.SubTable[0].Coverage.glyphs:
+ self.SubTable.pop(0)
+ self.SubTableCount -= 1
+
+ # If format-2 subtable created during canonicalization is empty, remove it.
+ assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2
+ if not self.SubTable[-1].Coverage.glyphs:
+ self.SubTable.pop(-1)
+ self.SubTableCount -= 1
+
+ # Compact the merged subtables
+ # This is a good moment to do it because the compaction should create
+ # smaller subtables, which may prevent overflows from happening.
+ # Keep reading the value from the ENV until ufo2ft switches to the config system
+ level = merger.font.cfg.get(
+ "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
+ default=_compression_level_from_env(),
+ )
+ if level != 0:
+ log.info("Compacting GPOS...")
+ self.SubTable = compact_pair_pos(merger.font, level, self.SubTable)
+ self.SubTableCount = len(self.SubTable)
+
+ elif isSinglePos and flattened:
+ singlePosTable = self.SubTable[0]
+ glyphs = singlePosTable.Coverage.glyphs
+ # We know that singlePosTable is Format 2, as this is set
+ # in _Lookup_SinglePos_subtables_flatten.
+ singlePosMapping = {
+ gname: valRecord for gname, valRecord in zip(glyphs, singlePosTable.Value)
+ }
+ self.SubTable = buildSinglePos(
+ singlePosMapping, merger.font.getReverseGlyphMap()
+ )
+ merger.mergeObjects(self, lst, exclude=["SubTable", "SubTableCount"])
+
+ del merger.lookup_subtables
+
#
# InstancerMerger
#
+
class InstancerMerger(AligningMerger):
- """A merger that takes multiple master fonts, and instantiates
- an instance."""
+ """A merger that takes multiple master fonts, and instantiates
+ an instance."""
+
+ def __init__(self, font, model, location):
+ Merger.__init__(self, font)
+ self.model = model
+ self.location = location
+ self.scalars = model.getScalars(location)
- def __init__(self, font, model, location):
- Merger.__init__(self, font)
- self.model = model
- self.location = location
- self.scalars = model.getScalars(location)
@InstancerMerger.merger(ot.CaretValue)
def merge(merger, self, lst):
- assert self.Format == 1
- Coords = [a.Coordinate for a in lst]
- model = merger.model
- scalars = merger.scalars
- self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars))
+ assert self.Format == 1
+ Coords = [a.Coordinate for a in lst]
+ model = merger.model
+ scalars = merger.scalars
+ self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars))
+
@InstancerMerger.merger(ot.Anchor)
def merge(merger, self, lst):
- assert self.Format == 1
- XCoords = [a.XCoordinate for a in lst]
- YCoords = [a.YCoordinate for a in lst]
- model = merger.model
- scalars = merger.scalars
- self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars))
- self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars))
+ assert self.Format == 1
+ XCoords = [a.XCoordinate for a in lst]
+ YCoords = [a.YCoordinate for a in lst]
+ model = merger.model
+ scalars = merger.scalars
+ self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars))
+ self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars))
+
@InstancerMerger.merger(otBase.ValueRecord)
def merge(merger, self, lst):
- model = merger.model
- scalars = merger.scalars
- # TODO Handle differing valueformats
- for name, tableName in [('XAdvance','XAdvDevice'),
- ('YAdvance','YAdvDevice'),
- ('XPlacement','XPlaDevice'),
- ('YPlacement','YPlaDevice')]:
-
- assert not hasattr(self, tableName)
-
- if hasattr(self, name):
- values = [getattr(a, name, 0) for a in lst]
- value = otRound(model.interpolateFromMastersAndScalars(values, scalars))
- setattr(self, name, value)
+ model = merger.model
+ scalars = merger.scalars
+ # TODO Handle differing valueformats
+ for name, tableName in [
+ ("XAdvance", "XAdvDevice"),
+ ("YAdvance", "YAdvDevice"),
+ ("XPlacement", "XPlaDevice"),
+ ("YPlacement", "YPlaDevice"),
+ ]:
+ assert not hasattr(self, tableName)
+
+ if hasattr(self, name):
+ values = [getattr(a, name, 0) for a in lst]
+ value = otRound(model.interpolateFromMastersAndScalars(values, scalars))
+ setattr(self, name, value)
#
# MutatorMerger
#
+
class MutatorMerger(AligningMerger):
- """A merger that takes a variable font, and instantiates
- an instance. While there's no "merging" to be done per se,
- the operation can benefit from many operations that the
- aligning merger does."""
+ """A merger that takes a variable font, and instantiates
+ an instance. While there's no "merging" to be done per se,
+ the operation can benefit from many operations that the
+ aligning merger does."""
+
+ def __init__(self, font, instancer, deleteVariations=True):
+ Merger.__init__(self, font)
+ self.instancer = instancer
+ self.deleteVariations = deleteVariations
- def __init__(self, font, instancer, deleteVariations=True):
- Merger.__init__(self, font)
- self.instancer = instancer
- self.deleteVariations = deleteVariations
@MutatorMerger.merger(ot.CaretValue)
def merge(merger, self, lst):
+ # Hack till we become selfless.
+ self.__dict__ = lst[0].__dict__.copy()
- # Hack till we become selfless.
- self.__dict__ = lst[0].__dict__.copy()
+ if self.Format != 3:
+ return
- if self.Format != 3:
- return
+ instancer = merger.instancer
+ dev = self.DeviceTable
+ if merger.deleteVariations:
+ del self.DeviceTable
+ if dev:
+ assert dev.DeltaFormat == 0x8000
+ varidx = (dev.StartSize << 16) + dev.EndSize
+ delta = otRound(instancer[varidx])
+ self.Coordinate += delta
- instancer = merger.instancer
- dev = self.DeviceTable
- if merger.deleteVariations:
- del self.DeviceTable
- if dev:
- assert dev.DeltaFormat == 0x8000
- varidx = (dev.StartSize << 16) + dev.EndSize
- delta = otRound(instancer[varidx])
- self.Coordinate += delta
+ if merger.deleteVariations:
+ self.Format = 1
- if merger.deleteVariations:
- self.Format = 1
@MutatorMerger.merger(ot.Anchor)
def merge(merger, self, lst):
+ # Hack till we become selfless.
+ self.__dict__ = lst[0].__dict__.copy()
- # Hack till we become selfless.
- self.__dict__ = lst[0].__dict__.copy()
+ if self.Format != 3:
+ return
- if self.Format != 3:
- return
+ instancer = merger.instancer
+ for v in "XY":
+ tableName = v + "DeviceTable"
+ if not hasattr(self, tableName):
+ continue
+ dev = getattr(self, tableName)
+ if merger.deleteVariations:
+ delattr(self, tableName)
+ if dev is None:
+ continue
- instancer = merger.instancer
- for v in "XY":
- tableName = v+'DeviceTable'
- if not hasattr(self, tableName):
- continue
- dev = getattr(self, tableName)
- if merger.deleteVariations:
- delattr(self, tableName)
- if dev is None:
- continue
+ assert dev.DeltaFormat == 0x8000
+ varidx = (dev.StartSize << 16) + dev.EndSize
+ delta = otRound(instancer[varidx])
- assert dev.DeltaFormat == 0x8000
- varidx = (dev.StartSize << 16) + dev.EndSize
- delta = otRound(instancer[varidx])
+ attr = v + "Coordinate"
+ setattr(self, attr, getattr(self, attr) + delta)
- attr = v+'Coordinate'
- setattr(self, attr, getattr(self, attr) + delta)
+ if merger.deleteVariations:
+ self.Format = 1
- if merger.deleteVariations:
- self.Format = 1
@MutatorMerger.merger(otBase.ValueRecord)
def merge(merger, self, lst):
-
- # Hack till we become selfless.
- self.__dict__ = lst[0].__dict__.copy()
-
- instancer = merger.instancer
- for name, tableName in [('XAdvance','XAdvDevice'),
- ('YAdvance','YAdvDevice'),
- ('XPlacement','XPlaDevice'),
- ('YPlacement','YPlaDevice')]:
-
- if not hasattr(self, tableName):
- continue
- dev = getattr(self, tableName)
- if merger.deleteVariations:
- delattr(self, tableName)
- if dev is None:
- continue
-
- assert dev.DeltaFormat == 0x8000
- varidx = (dev.StartSize << 16) + dev.EndSize
- delta = otRound(instancer[varidx])
-
- setattr(self, name, getattr(self, name, 0) + delta)
+ # Hack till we become selfless.
+ self.__dict__ = lst[0].__dict__.copy()
+
+ instancer = merger.instancer
+ for name, tableName in [
+ ("XAdvance", "XAdvDevice"),
+ ("YAdvance", "YAdvDevice"),
+ ("XPlacement", "XPlaDevice"),
+ ("YPlacement", "YPlaDevice"),
+ ]:
+ if not hasattr(self, tableName):
+ continue
+ dev = getattr(self, tableName)
+ if merger.deleteVariations:
+ delattr(self, tableName)
+ if dev is None:
+ continue
+
+ assert dev.DeltaFormat == 0x8000
+ varidx = (dev.StartSize << 16) + dev.EndSize
+ delta = otRound(instancer[varidx])
+
+ setattr(self, name, getattr(self, name, 0) + delta)
#
# VariationMerger
#
+
class VariationMerger(AligningMerger):
- """A merger that takes multiple master fonts, and builds a
- variable font."""
+ """A merger that takes multiple master fonts, and builds a
+ variable font."""
- def __init__(self, model, axisTags, font):
- Merger.__init__(self, font)
- self.store_builder = varStore.OnlineVarStoreBuilder(axisTags)
- self.setModel(model)
+ def __init__(self, model, axisTags, font):
+ Merger.__init__(self, font)
+ self.store_builder = varStore.OnlineVarStoreBuilder(axisTags)
+ self.setModel(model)
- def setModel(self, model):
- self.model = model
- self.store_builder.setModel(model)
+ def setModel(self, model):
+ self.model = model
+ self.store_builder.setModel(model)
- def mergeThings(self, out, lst):
- masterModel = None
- origTTFs = None
- if None in lst:
- if allNone(lst):
- if out is not None:
- raise FoundANone(self, got=lst)
- return
+ def mergeThings(self, out, lst):
+ masterModel = None
+ origTTFs = None
+ if None in lst:
+ if allNone(lst):
+ if out is not None:
+ raise FoundANone(self, got=lst)
+ return
- # temporarily subset the list of master ttfs to the ones for which
- # master values are not None
- origTTFs = self.ttfs
- if self.ttfs:
- self.ttfs = subList([v is not None for v in lst], self.ttfs)
+ # temporarily subset the list of master ttfs to the ones for which
+ # master values are not None
+ origTTFs = self.ttfs
+ if self.ttfs:
+ self.ttfs = subList([v is not None for v in lst], self.ttfs)
- masterModel = self.model
- model, lst = masterModel.getSubModel(lst)
- self.setModel(model)
+ masterModel = self.model
+ model, lst = masterModel.getSubModel(lst)
+ self.setModel(model)
- super(VariationMerger, self).mergeThings(out, lst)
+ super(VariationMerger, self).mergeThings(out, lst)
- if masterModel:
- self.setModel(masterModel)
- if origTTFs:
- self.ttfs = origTTFs
+ if masterModel:
+ self.setModel(masterModel)
+ if origTTFs:
+ self.ttfs = origTTFs
def buildVarDevTable(store_builder, master_values):
- if allEqual(master_values):
- return master_values[0], None
- base, varIdx = store_builder.storeMasters(master_values)
- return base, builder.buildVarDevTable(varIdx)
+ if allEqual(master_values):
+ return master_values[0], None
+ base, varIdx = store_builder.storeMasters(master_values)
+ return base, builder.buildVarDevTable(varIdx)
+
@VariationMerger.merger(ot.BaseCoord)
def merge(merger, self, lst):
- if self.Format != 1:
- raise UnsupportedFormat(merger, subtable="a baseline coordinate")
- self.Coordinate, DeviceTable = buildVarDevTable(merger.store_builder, [a.Coordinate for a in lst])
- if DeviceTable:
- self.Format = 3
- self.DeviceTable = DeviceTable
+ if self.Format != 1:
+ raise UnsupportedFormat(merger, subtable="a baseline coordinate")
+ self.Coordinate, DeviceTable = buildVarDevTable(
+ merger.store_builder, [a.Coordinate for a in lst]
+ )
+ if DeviceTable:
+ self.Format = 3
+ self.DeviceTable = DeviceTable
+
@VariationMerger.merger(ot.CaretValue)
def merge(merger, self, lst):
- if self.Format != 1:
- raise UnsupportedFormat(merger, subtable="a caret")
- self.Coordinate, DeviceTable = buildVarDevTable(merger.store_builder, [a.Coordinate for a in lst])
- if DeviceTable:
- self.Format = 3
- self.DeviceTable = DeviceTable
+ if self.Format != 1:
+ raise UnsupportedFormat(merger, subtable="a caret")
+ self.Coordinate, DeviceTable = buildVarDevTable(
+ merger.store_builder, [a.Coordinate for a in lst]
+ )
+ if DeviceTable:
+ self.Format = 3
+ self.DeviceTable = DeviceTable
+
@VariationMerger.merger(ot.Anchor)
def merge(merger, self, lst):
- if self.Format != 1:
- raise UnsupportedFormat(merger, subtable="an anchor")
- self.XCoordinate, XDeviceTable = buildVarDevTable(merger.store_builder, [a.XCoordinate for a in lst])
- self.YCoordinate, YDeviceTable = buildVarDevTable(merger.store_builder, [a.YCoordinate for a in lst])
- if XDeviceTable or YDeviceTable:
- self.Format = 3
- self.XDeviceTable = XDeviceTable
- self.YDeviceTable = YDeviceTable
+ if self.Format != 1:
+ raise UnsupportedFormat(merger, subtable="an anchor")
+ self.XCoordinate, XDeviceTable = buildVarDevTable(
+ merger.store_builder, [a.XCoordinate for a in lst]
+ )
+ self.YCoordinate, YDeviceTable = buildVarDevTable(
+ merger.store_builder, [a.YCoordinate for a in lst]
+ )
+ if XDeviceTable or YDeviceTable:
+ self.Format = 3
+ self.XDeviceTable = XDeviceTable
+ self.YDeviceTable = YDeviceTable
+
@VariationMerger.merger(otBase.ValueRecord)
def merge(merger, self, lst):
- for name, tableName in [('XAdvance','XAdvDevice'),
- ('YAdvance','YAdvDevice'),
- ('XPlacement','XPlaDevice'),
- ('YPlacement','YPlaDevice')]:
-
- if hasattr(self, name):
- value, deviceTable = buildVarDevTable(merger.store_builder,
- [getattr(a, name, 0) for a in lst])
- setattr(self, name, value)
- if deviceTable:
- setattr(self, tableName, deviceTable)
+ for name, tableName in [
+ ("XAdvance", "XAdvDevice"),
+ ("YAdvance", "YAdvDevice"),
+ ("XPlacement", "XPlaDevice"),
+ ("YPlacement", "YPlaDevice"),
+ ]:
+ if hasattr(self, name):
+ value, deviceTable = buildVarDevTable(
+ merger.store_builder, [getattr(a, name, 0) for a in lst]
+ )
+ setattr(self, name, value)
+ if deviceTable:
+ setattr(self, tableName, deviceTable)
class COLRVariationMerger(VariationMerger):
- """A specialized VariationMerger that takes multiple master fonts containing
- COLRv1 tables, and builds a variable COLR font.
-
- COLR tables are special in that variable subtables can be associated with
- multiple delta-set indices (via VarIndexBase).
- They also contain tables that must change their type (not simply the Format)
- as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes
- care of that too.
- """
-
- def __init__(self, model, axisTags, font, allowLayerReuse=True):
- VariationMerger.__init__(self, model, axisTags, font)
- # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase
- # between variable tables with same varIdxes.
- self.varIndexCache = {}
- # flat list of all the varIdxes generated while merging
- self.varIdxes = []
- # set of id()s of the subtables that contain variations after merging
- # and need to be upgraded to the associated VarType.
- self.varTableIds = set()
- # we keep these around for rebuilding a LayerList while merging PaintColrLayers
- self.layers = []
- self.layerReuseCache = None
- if allowLayerReuse:
- self.layerReuseCache = LayerReuseCache()
- # flag to ensure BaseGlyphList is fully merged before LayerList gets processed
- self._doneBaseGlyphs = False
-
- def mergeTables(self, font, master_ttfs, tableTags=("COLR",)):
- if "COLR" in tableTags and "COLR" in font:
- # The merger modifies the destination COLR table in-place. If this contains
- # multiple PaintColrLayers referencing the same layers from LayerList, it's
- # a problem because we may risk modifying the same paint more than once, or
- # worse, fail while attempting to do that.
- # We don't know whether the master COLR table was built with layer reuse
- # disabled, thus to be safe we rebuild its LayerList so that it contains only
- # unique layers referenced from non-overlapping PaintColrLayers throughout
- # the base paint graphs.
- self.expandPaintColrLayers(font["COLR"].table)
- VariationMerger.mergeTables(self, font, master_ttfs, tableTags)
-
- def checkFormatEnum(self, out, lst, validate=lambda _: True):
- fmt = out.Format
- formatEnum = out.formatEnum
- ok = False
- try:
- fmt = formatEnum(fmt)
- except ValueError:
- pass
- else:
- ok = validate(fmt)
- if not ok:
- raise UnsupportedFormat(
- self, subtable=type(out).__name__, value=fmt
- )
- expected = fmt
- got = []
- for v in lst:
- fmt = getattr(v, "Format", None)
- try:
- fmt = formatEnum(fmt)
- except ValueError:
- pass
- got.append(fmt)
- if not allEqualTo(expected, got):
- raise InconsistentFormats(
- self,
- subtable=type(out).__name__,
- expected=expected,
- got=got,
- )
- return expected
-
- def mergeSparseDict(self, out, lst):
- for k in out.keys():
- try:
- self.mergeThings(out[k], [v.get(k) for v in lst])
- except VarLibMergeError as e:
- e.stack.append(f"[{k!r}]")
- raise
-
- def mergeAttrs(self, out, lst, attrs):
- for attr in attrs:
- value = getattr(out, attr)
- values = [getattr(item, attr) for item in lst]
- try:
- self.mergeThings(value, values)
- except VarLibMergeError as e:
- e.stack.append(f".{attr}")
- raise
-
- def storeMastersForAttr(self, out, lst, attr):
- master_values = [getattr(item, attr) for item in lst]
-
- # VarStore treats deltas for fixed-size floats as integers, so we
- # must convert master values to int before storing them in the builder
- # then back to float.
- is_fixed_size_float = False
- conv = out.getConverterByName(attr)
- if isinstance(conv, BaseFixedValue):
- is_fixed_size_float = True
- master_values = [conv.toInt(v) for v in master_values]
-
- baseValue = master_values[0]
- varIdx = ot.NO_VARIATION_INDEX
- if not allEqual(master_values):
- baseValue, varIdx = self.store_builder.storeMasters(master_values)
-
- if is_fixed_size_float:
- baseValue = conv.fromInt(baseValue)
-
- return baseValue, varIdx
-
- def storeVariationIndices(self, varIdxes) -> int:
- # try to reuse an existing VarIndexBase for the same varIdxes, or else
- # create a new one
- key = tuple(varIdxes)
- varIndexBase = self.varIndexCache.get(key)
-
- if varIndexBase is None:
- # scan for a full match anywhere in the self.varIdxes
- for i in range(len(self.varIdxes) - len(varIdxes) + 1):
- if self.varIdxes[i:i+len(varIdxes)] == varIdxes:
- self.varIndexCache[key] = varIndexBase = i
- break
-
- if varIndexBase is None:
- # try find a partial match at the end of the self.varIdxes
- for n in range(len(varIdxes)-1, 0, -1):
- if self.varIdxes[-n:] == varIdxes[:n]:
- varIndexBase = len(self.varIdxes) - n
- self.varIndexCache[key] = varIndexBase
- self.varIdxes.extend(varIdxes[n:])
- break
-
- if varIndexBase is None:
- # no match found, append at the end
- self.varIndexCache[key] = varIndexBase = len(self.varIdxes)
- self.varIdxes.extend(varIdxes)
-
- return varIndexBase
-
- def mergeVariableAttrs(self, out, lst, attrs) -> int:
- varIndexBase = ot.NO_VARIATION_INDEX
- varIdxes = []
- for attr in attrs:
- baseValue, varIdx = self.storeMastersForAttr(out, lst, attr)
- setattr(out, attr, baseValue)
- varIdxes.append(varIdx)
-
- if any(v != ot.NO_VARIATION_INDEX for v in varIdxes):
- varIndexBase = self.storeVariationIndices(varIdxes)
-
- return varIndexBase
-
- @classmethod
- def convertSubTablesToVarType(cls, table):
- for path in dfs_base_table(
- table,
- skip_root=True,
- predicate=lambda path: (
- getattr(type(path[-1].value), "VarType", None) is not None
- )
- ):
- st = path[-1]
- subTable = st.value
- varType = type(subTable).VarType
- newSubTable = varType()
- newSubTable.__dict__.update(subTable.__dict__)
- newSubTable.populateDefaults()
- parent = path[-2].value
- if st.index is not None:
- getattr(parent, st.name)[st.index] = newSubTable
- else:
- setattr(parent, st.name, newSubTable)
-
- @staticmethod
- def expandPaintColrLayers(colr):
- """Rebuild LayerList without PaintColrLayers reuse.
-
- Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph
- which are irrelevant for this); any layers referenced via PaintColrLayers are
- collected into a new LayerList and duplicated when reuse is detected, to ensure
- that all paints are distinct objects at the end of the process.
- PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap
- is left. Also, any consecutively nested PaintColrLayers are flattened.
- The COLR table's LayerList is replaced with the new unique layers.
- A side effect is also that any layer from the old LayerList which is not
- referenced by any PaintColrLayers is dropped.
- """
- if not colr.LayerList:
- # if no LayerList, there's nothing to expand
- return
- uniqueLayerIDs = set()
- newLayerList = []
- for rec in colr.BaseGlyphList.BaseGlyphPaintRecord:
- frontier = [rec.Paint]
- while frontier:
- paint = frontier.pop()
- if paint.Format == ot.PaintFormat.PaintColrGlyph:
- # don't traverse these, we treat them as constant for merging
- continue
- elif paint.Format == ot.PaintFormat.PaintColrLayers:
- # de-treeify any nested PaintColrLayers, append unique copies to
- # the new layer list and update PaintColrLayers index/count
- children = list(_flatten_layers(paint, colr))
- first_layer_index = len(newLayerList)
- for layer in children:
- if id(layer) in uniqueLayerIDs:
- layer = copy.deepcopy(layer)
- assert id(layer) not in uniqueLayerIDs
- newLayerList.append(layer)
- uniqueLayerIDs.add(id(layer))
- paint.FirstLayerIndex = first_layer_index
- paint.NumLayers = len(children)
- else:
- children = paint.getChildren(colr)
- frontier.extend(reversed(children))
- # sanity check all the new layers are distinct objects
- assert len(newLayerList) == len(uniqueLayerIDs)
- colr.LayerList.Paint = newLayerList
- colr.LayerList.LayerCount = len(newLayerList)
+ """A specialized VariationMerger that takes multiple master fonts containing
+ COLRv1 tables, and builds a variable COLR font.
+
+ COLR tables are special in that variable subtables can be associated with
+ multiple delta-set indices (via VarIndexBase).
+ They also contain tables that must change their type (not simply the Format)
+ as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes
+ care of that too.
+ """
+
+ def __init__(self, model, axisTags, font, allowLayerReuse=True):
+ VariationMerger.__init__(self, model, axisTags, font)
+ # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase
+ # between variable tables with same varIdxes.
+ self.varIndexCache = {}
+ # flat list of all the varIdxes generated while merging
+ self.varIdxes = []
+ # set of id()s of the subtables that contain variations after merging
+ # and need to be upgraded to the associated VarType.
+ self.varTableIds = set()
+ # we keep these around for rebuilding a LayerList while merging PaintColrLayers
+ self.layers = []
+ self.layerReuseCache = None
+ if allowLayerReuse:
+ self.layerReuseCache = LayerReuseCache()
+ # flag to ensure BaseGlyphList is fully merged before LayerList gets processed
+ self._doneBaseGlyphs = False
+
+ def mergeTables(self, font, master_ttfs, tableTags=("COLR",)):
+ if "COLR" in tableTags and "COLR" in font:
+ # The merger modifies the destination COLR table in-place. If this contains
+ # multiple PaintColrLayers referencing the same layers from LayerList, it's
+ # a problem because we may risk modifying the same paint more than once, or
+ # worse, fail while attempting to do that.
+ # We don't know whether the master COLR table was built with layer reuse
+ # disabled, thus to be safe we rebuild its LayerList so that it contains only
+ # unique layers referenced from non-overlapping PaintColrLayers throughout
+ # the base paint graphs.
+ self.expandPaintColrLayers(font["COLR"].table)
+ VariationMerger.mergeTables(self, font, master_ttfs, tableTags)
+
+ def checkFormatEnum(self, out, lst, validate=lambda _: True):
+ fmt = out.Format
+ formatEnum = out.formatEnum
+ ok = False
+ try:
+ fmt = formatEnum(fmt)
+ except ValueError:
+ pass
+ else:
+ ok = validate(fmt)
+ if not ok:
+ raise UnsupportedFormat(self, subtable=type(out).__name__, value=fmt)
+ expected = fmt
+ got = []
+ for v in lst:
+ fmt = getattr(v, "Format", None)
+ try:
+ fmt = formatEnum(fmt)
+ except ValueError:
+ pass
+ got.append(fmt)
+ if not allEqualTo(expected, got):
+ raise InconsistentFormats(
+ self,
+ subtable=type(out).__name__,
+ expected=expected,
+ got=got,
+ )
+ return expected
+
+ def mergeSparseDict(self, out, lst):
+ for k in out.keys():
+ try:
+ self.mergeThings(out[k], [v.get(k) for v in lst])
+ except VarLibMergeError as e:
+ e.stack.append(f"[{k!r}]")
+ raise
+
+ def mergeAttrs(self, out, lst, attrs):
+ for attr in attrs:
+ value = getattr(out, attr)
+ values = [getattr(item, attr) for item in lst]
+ try:
+ self.mergeThings(value, values)
+ except VarLibMergeError as e:
+ e.stack.append(f".{attr}")
+ raise
+
+ def storeMastersForAttr(self, out, lst, attr):
+ master_values = [getattr(item, attr) for item in lst]
+
+ # VarStore treats deltas for fixed-size floats as integers, so we
+ # must convert master values to int before storing them in the builder
+ # then back to float.
+ is_fixed_size_float = False
+ conv = out.getConverterByName(attr)
+ if isinstance(conv, BaseFixedValue):
+ is_fixed_size_float = True
+ master_values = [conv.toInt(v) for v in master_values]
+
+ baseValue = master_values[0]
+ varIdx = ot.NO_VARIATION_INDEX
+ if not allEqual(master_values):
+ baseValue, varIdx = self.store_builder.storeMasters(master_values)
+
+ if is_fixed_size_float:
+ baseValue = conv.fromInt(baseValue)
+
+ return baseValue, varIdx
+
+ def storeVariationIndices(self, varIdxes) -> int:
+ # try to reuse an existing VarIndexBase for the same varIdxes, or else
+ # create a new one
+ key = tuple(varIdxes)
+ varIndexBase = self.varIndexCache.get(key)
+
+ if varIndexBase is None:
+ # scan for a full match anywhere in the self.varIdxes
+ for i in range(len(self.varIdxes) - len(varIdxes) + 1):
+ if self.varIdxes[i : i + len(varIdxes)] == varIdxes:
+ self.varIndexCache[key] = varIndexBase = i
+ break
+
+ if varIndexBase is None:
+ # try find a partial match at the end of the self.varIdxes
+ for n in range(len(varIdxes) - 1, 0, -1):
+ if self.varIdxes[-n:] == varIdxes[:n]:
+ varIndexBase = len(self.varIdxes) - n
+ self.varIndexCache[key] = varIndexBase
+ self.varIdxes.extend(varIdxes[n:])
+ break
+
+ if varIndexBase is None:
+ # no match found, append at the end
+ self.varIndexCache[key] = varIndexBase = len(self.varIdxes)
+ self.varIdxes.extend(varIdxes)
+
+ return varIndexBase
+
+ def mergeVariableAttrs(self, out, lst, attrs) -> int:
+ varIndexBase = ot.NO_VARIATION_INDEX
+ varIdxes = []
+ for attr in attrs:
+ baseValue, varIdx = self.storeMastersForAttr(out, lst, attr)
+ setattr(out, attr, baseValue)
+ varIdxes.append(varIdx)
+
+ if any(v != ot.NO_VARIATION_INDEX for v in varIdxes):
+ varIndexBase = self.storeVariationIndices(varIdxes)
+
+ return varIndexBase
+
+ @classmethod
+ def convertSubTablesToVarType(cls, table):
+ for path in dfs_base_table(
+ table,
+ skip_root=True,
+ predicate=lambda path: (
+ getattr(type(path[-1].value), "VarType", None) is not None
+ ),
+ ):
+ st = path[-1]
+ subTable = st.value
+ varType = type(subTable).VarType
+ newSubTable = varType()
+ newSubTable.__dict__.update(subTable.__dict__)
+ newSubTable.populateDefaults()
+ parent = path[-2].value
+ if st.index is not None:
+ getattr(parent, st.name)[st.index] = newSubTable
+ else:
+ setattr(parent, st.name, newSubTable)
+
+ @staticmethod
+ def expandPaintColrLayers(colr):
+ """Rebuild LayerList without PaintColrLayers reuse.
+
+ Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph
+ which are irrelevant for this); any layers referenced via PaintColrLayers are
+ collected into a new LayerList and duplicated when reuse is detected, to ensure
+ that all paints are distinct objects at the end of the process.
+ PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap
+ is left. Also, any consecutively nested PaintColrLayers are flattened.
+ The COLR table's LayerList is replaced with the new unique layers.
+ A side effect is also that any layer from the old LayerList which is not
+ referenced by any PaintColrLayers is dropped.
+ """
+ if not colr.LayerList:
+ # if no LayerList, there's nothing to expand
+ return
+ uniqueLayerIDs = set()
+ newLayerList = []
+ for rec in colr.BaseGlyphList.BaseGlyphPaintRecord:
+ frontier = [rec.Paint]
+ while frontier:
+ paint = frontier.pop()
+ if paint.Format == ot.PaintFormat.PaintColrGlyph:
+ # don't traverse these, we treat them as constant for merging
+ continue
+ elif paint.Format == ot.PaintFormat.PaintColrLayers:
+ # de-treeify any nested PaintColrLayers, append unique copies to
+ # the new layer list and update PaintColrLayers index/count
+ children = list(_flatten_layers(paint, colr))
+ first_layer_index = len(newLayerList)
+ for layer in children:
+ if id(layer) in uniqueLayerIDs:
+ layer = copy.deepcopy(layer)
+ assert id(layer) not in uniqueLayerIDs
+ newLayerList.append(layer)
+ uniqueLayerIDs.add(id(layer))
+ paint.FirstLayerIndex = first_layer_index
+ paint.NumLayers = len(children)
+ else:
+ children = paint.getChildren(colr)
+ frontier.extend(reversed(children))
+ # sanity check all the new layers are distinct objects
+ assert len(newLayerList) == len(uniqueLayerIDs)
+ colr.LayerList.Paint = newLayerList
+ colr.LayerList.LayerCount = len(newLayerList)
@COLRVariationMerger.merger(ot.BaseGlyphList)
def merge(merger, self, lst):
- # ignore BaseGlyphCount, allow sparse glyph sets across masters
- out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord}
- masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst]
+ # ignore BaseGlyphCount, allow sparse glyph sets across masters
+ out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord}
+ masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst]
- for i, g in enumerate(out.keys()):
- try:
- # missing base glyphs don't participate in the merge
- merger.mergeThings(out[g], [v.get(g) for v in masters])
- except VarLibMergeError as e:
- e.stack.append(f".BaseGlyphPaintRecord[{i}]")
- e.cause["location"] = f"base glyph {g!r}"
- raise
+ for i, g in enumerate(out.keys()):
+ try:
+ # missing base glyphs don't participate in the merge
+ merger.mergeThings(out[g], [v.get(g) for v in masters])
+ except VarLibMergeError as e:
+ e.stack.append(f".BaseGlyphPaintRecord[{i}]")
+ e.cause["location"] = f"base glyph {g!r}"
+ raise
- merger._doneBaseGlyphs = True
+ merger._doneBaseGlyphs = True
@COLRVariationMerger.merger(ot.LayerList)
def merge(merger, self, lst):
- # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers
- # found while traversing the paint graphs rooted at BaseGlyphPaintRecords.
- assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList"
- # Simply flush the final list of layers and go home.
- self.LayerCount = len(merger.layers)
- self.Paint = merger.layers
+ # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers
+ # found while traversing the paint graphs rooted at BaseGlyphPaintRecords.
+ assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList"
+ # Simply flush the final list of layers and go home.
+ self.LayerCount = len(merger.layers)
+ self.Paint = merger.layers
def _flatten_layers(root, colr):
- assert root.Format == ot.PaintFormat.PaintColrLayers
- for paint in root.getChildren(colr):
- if paint.Format == ot.PaintFormat.PaintColrLayers:
- yield from _flatten_layers(paint, colr)
- else:
- yield paint
+ assert root.Format == ot.PaintFormat.PaintColrLayers
+ for paint in root.getChildren(colr):
+ if paint.Format == ot.PaintFormat.PaintColrLayers:
+ yield from _flatten_layers(paint, colr)
+ else:
+ yield paint
def _merge_PaintColrLayers(self, out, lst):
- # we only enforce that the (flat) number of layers is the same across all masters
- # but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets.
-
- out_layers = list(_flatten_layers(out, self.font["COLR"].table))
-
- # sanity check ttfs are subset to current values (see VariationMerger.mergeThings)
- # before matching each master PaintColrLayers to its respective COLR by position
- assert len(self.ttfs) == len(lst)
- master_layerses = [
- list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table))
- for i in range(len(lst))
- ]
-
- try:
- self.mergeLists(out_layers, master_layerses)
- except VarLibMergeError as e:
- # NOTE: This attribute doesn't actually exist in PaintColrLayers but it's
- # handy to have it in the stack trace for debugging.
- e.stack.append(".Layers")
- raise
-
- # following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers
- # but I couldn't find a nice way to share the code between the two...
-
- if self.layerReuseCache is not None:
- # successful reuse can make the list smaller
- out_layers = self.layerReuseCache.try_reuse(out_layers)
-
- # if the list is still too big we need to tree-fy it
- is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT
- out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT)
-
- # We now have a tree of sequences with Paint leaves.
- # Convert the sequences into PaintColrLayers.
- def listToColrLayers(paint):
- if isinstance(paint, list):
- layers = [listToColrLayers(l) for l in paint]
- paint = ot.Paint()
- paint.Format = int(ot.PaintFormat.PaintColrLayers)
- paint.NumLayers = len(layers)
- paint.FirstLayerIndex = len(self.layers)
- self.layers.extend(layers)
- if self.layerReuseCache is not None:
- self.layerReuseCache.add(layers, paint.FirstLayerIndex)
- return paint
-
- out_layers = [listToColrLayers(l) for l in out_layers]
-
- if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers:
- # special case when the reuse cache finds a single perfect PaintColrLayers match
- # (it can only come from a successful reuse, _flatten_layers has gotten rid of
- # all nested PaintColrLayers already); we assign it directly and avoid creating
- # an extra table
- out.NumLayers = out_layers[0].NumLayers
- out.FirstLayerIndex = out_layers[0].FirstLayerIndex
- else:
- out.NumLayers = len(out_layers)
- out.FirstLayerIndex = len(self.layers)
-
- self.layers.extend(out_layers)
-
- # Register our parts for reuse provided we aren't a tree
- # If we are a tree the leaves registered for reuse and that will suffice
- if self.layerReuseCache is not None and not is_tree:
- self.layerReuseCache.add(out_layers, out.FirstLayerIndex)
+ # we only enforce that the (flat) number of layers is the same across all masters
+ # but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets.
+
+ out_layers = list(_flatten_layers(out, self.font["COLR"].table))
+
+ # sanity check ttfs are subset to current values (see VariationMerger.mergeThings)
+ # before matching each master PaintColrLayers to its respective COLR by position
+ assert len(self.ttfs) == len(lst)
+ master_layerses = [
+ list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table))
+ for i in range(len(lst))
+ ]
+
+ try:
+ self.mergeLists(out_layers, master_layerses)
+ except VarLibMergeError as e:
+ # NOTE: This attribute doesn't actually exist in PaintColrLayers but it's
+ # handy to have it in the stack trace for debugging.
+ e.stack.append(".Layers")
+ raise
+
+ # following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers
+ # but I couldn't find a nice way to share the code between the two...
+
+ if self.layerReuseCache is not None:
+ # successful reuse can make the list smaller
+ out_layers = self.layerReuseCache.try_reuse(out_layers)
+
+ # if the list is still too big we need to tree-fy it
+ is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT
+ out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT)
+
+ # We now have a tree of sequences with Paint leaves.
+ # Convert the sequences into PaintColrLayers.
+ def listToColrLayers(paint):
+ if isinstance(paint, list):
+ layers = [listToColrLayers(l) for l in paint]
+ paint = ot.Paint()
+ paint.Format = int(ot.PaintFormat.PaintColrLayers)
+ paint.NumLayers = len(layers)
+ paint.FirstLayerIndex = len(self.layers)
+ self.layers.extend(layers)
+ if self.layerReuseCache is not None:
+ self.layerReuseCache.add(layers, paint.FirstLayerIndex)
+ return paint
+
+ out_layers = [listToColrLayers(l) for l in out_layers]
+
+ if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers:
+ # special case when the reuse cache finds a single perfect PaintColrLayers match
+ # (it can only come from a successful reuse, _flatten_layers has gotten rid of
+ # all nested PaintColrLayers already); we assign it directly and avoid creating
+ # an extra table
+ out.NumLayers = out_layers[0].NumLayers
+ out.FirstLayerIndex = out_layers[0].FirstLayerIndex
+ else:
+ out.NumLayers = len(out_layers)
+ out.FirstLayerIndex = len(self.layers)
+
+ self.layers.extend(out_layers)
+
+ # Register our parts for reuse provided we aren't a tree
+ # If we are a tree the leaves registered for reuse and that will suffice
+ if self.layerReuseCache is not None and not is_tree:
+ self.layerReuseCache.add(out_layers, out.FirstLayerIndex)
@COLRVariationMerger.merger((ot.Paint, ot.ClipBox))
def merge(merger, self, lst):
- fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable())
+ fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable())
- if fmt is ot.PaintFormat.PaintColrLayers:
- _merge_PaintColrLayers(merger, self, lst)
- return
+ if fmt is ot.PaintFormat.PaintColrLayers:
+ _merge_PaintColrLayers(merger, self, lst)
+ return
- varFormat = fmt.as_variable()
+ varFormat = fmt.as_variable()
- varAttrs = ()
- if varFormat is not None:
- varAttrs = otBase.getVariableAttrs(type(self), varFormat)
- staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
+ varAttrs = ()
+ if varFormat is not None:
+ varAttrs = otBase.getVariableAttrs(type(self), varFormat)
+ staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
- merger.mergeAttrs(self, lst, staticAttrs)
+ merger.mergeAttrs(self, lst, staticAttrs)
- varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
+ varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
- subTables = [st.value for st in self.iterSubTables()]
+ subTables = [st.value for st in self.iterSubTables()]
- # Convert table to variable if itself has variations or any subtables have
- isVariable = (
- varIndexBase != ot.NO_VARIATION_INDEX
- or any(id(table) in merger.varTableIds for table in subTables)
- )
+ # Convert table to variable if itself has variations or any subtables have
+ isVariable = varIndexBase != ot.NO_VARIATION_INDEX or any(
+ id(table) in merger.varTableIds for table in subTables
+ )
- if isVariable:
- if varAttrs:
- # Some PaintVar* don't have any scalar attributes that can vary,
- # only indirect offsets to other variable subtables, thus have
- # no VarIndexBase of their own (e.g. PaintVarTransform)
- self.VarIndexBase = varIndexBase
+ if isVariable:
+ if varAttrs:
+ # Some PaintVar* don't have any scalar attributes that can vary,
+ # only indirect offsets to other variable subtables, thus have
+ # no VarIndexBase of their own (e.g. PaintVarTransform)
+ self.VarIndexBase = varIndexBase
- if subTables:
- # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc.
- merger.convertSubTablesToVarType(self)
+ if subTables:
+ # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc.
+ merger.convertSubTablesToVarType(self)
- assert varFormat is not None
- self.Format = int(varFormat)
+ assert varFormat is not None
+ self.Format = int(varFormat)
@COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop))
def merge(merger, self, lst):
- varType = type(self).VarType
+ varType = type(self).VarType
- varAttrs = otBase.getVariableAttrs(varType)
- staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
+ varAttrs = otBase.getVariableAttrs(varType)
+ staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
- merger.mergeAttrs(self, lst, staticAttrs)
+ merger.mergeAttrs(self, lst, staticAttrs)
- varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
+ varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
- if varIndexBase != ot.NO_VARIATION_INDEX:
- self.VarIndexBase = varIndexBase
- # mark as having variations so the parent table will convert to Var{Type}
- merger.varTableIds.add(id(self))
+ if varIndexBase != ot.NO_VARIATION_INDEX:
+ self.VarIndexBase = varIndexBase
+ # mark as having variations so the parent table will convert to Var{Type}
+ merger.varTableIds.add(id(self))
@COLRVariationMerger.merger(ot.ColorLine)
def merge(merger, self, lst):
- merger.mergeAttrs(self, lst, (c.name for c in self.getConverters()))
+ merger.mergeAttrs(self, lst, (c.name for c in self.getConverters()))
- if any(id(stop) in merger.varTableIds for stop in self.ColorStop):
- merger.convertSubTablesToVarType(self)
- merger.varTableIds.add(id(self))
+ if any(id(stop) in merger.varTableIds for stop in self.ColorStop):
+ merger.convertSubTablesToVarType(self)
+ merger.varTableIds.add(id(self))
@COLRVariationMerger.merger(ot.ClipList, "clips")
def merge(merger, self, lst):
- # 'sparse' in that we allow non-default masters to omit ClipBox entries
- # for some/all glyphs (i.e. they don't participate)
- merger.mergeSparseDict(self, lst)
+ # 'sparse' in that we allow non-default masters to omit ClipBox entries
+ # for some/all glyphs (i.e. they don't participate)
+ merger.mergeSparseDict(self, lst)