diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py index 69778fdfd..c0fb5a50a 100644 --- a/Lib/fontTools/varLib/merger.py +++ b/Lib/fontTools/varLib/merger.py @@ -4,7 +4,6 @@ Merge OpenType Layout tables (GDEF / GPOS / GSUB). import os import copy import enum -import itertools from operator import ior import logging from fontTools.misc import classifyTools @@ -1126,8 +1125,8 @@ class COLRVariationMerger(VariationMerger): # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase # between variable tables with same varIdxes. self.varIndexCache = {} - # total number of varIdxes (i.e. sum(len(vs) for vs in self.varIndexCache)) - self.varIndexCount = 0 + # 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() @@ -1135,18 +1134,6 @@ class COLRVariationMerger(VariationMerger): def mergeTables(self, font, master_ttfs, tableTags=("COLR",)): VariationMerger.mergeTables(self, font, master_ttfs, tableTags) - @property - def varIdxes(self): - """Return flat list of all the varIdxes generated while merging. - - To be called after mergeTables() for building a DeltaSetIndexMap. - """ - # dict remembers insertion order (as of py37+), and we extend the cache - # of VarIndexBases incrementally as new, unique tuples of varIdxes are found - # while merging, thus we don't need to sort but we just unpack the keys and - # chain the tuples - return [v for v in itertools.chain(*self.varIndexCache.keys())] - def checkFormatEnum(self, out, lst, validate=lambda _: True): fmt = out.Format formatEnum = out.formatEnum @@ -1219,6 +1206,35 @@ class COLRVariationMerger(VariationMerger): 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 = [] @@ -1226,14 +1242,9 @@ class COLRVariationMerger(VariationMerger): baseValue, varIdx = self.storeMastersForAttr(out, lst, attr) setattr(out, attr, baseValue) varIdxes.append(varIdx) - varIdxes = tuple(varIdxes) if any(v != ot.NO_VARIATION_INDEX for v in varIdxes): - # try to reuse an existing VarIndexBase for the same varIdxes, or else - # create a new one at the end of self.varIndexCache (py37+ dicts are ordered) - varIndexBase = self.varIndexCache.setdefault(varIdxes, self.varIndexCount) - if varIndexBase == self.varIndexCount: - self.varIndexCount += len(varIdxes) + varIndexBase = self.storeVariationIndices(varIdxes) return varIndexBase diff --git a/Tests/varLib/merger_test.py b/Tests/varLib/merger_test.py index 1bfedcf7c..08a160ac0 100644 --- a/Tests/varLib/merger_test.py +++ b/Tests/varLib/merger_test.py @@ -283,7 +283,7 @@ class COLRVariationMergerTest: ' ', ' ', ' ', - ' ', + ' ', "", ], [ @@ -293,7 +293,6 @@ class COLRVariationMergerTest: NO_VARIATION_INDEX, NO_VARIATION_INDEX, NO_VARIATION_INDEX, - NO_VARIATION_INDEX, 1, ], id="linear_grad-stop[0].offset-y2", @@ -475,7 +474,7 @@ class COLRVariationMergerTest: ' ', ' ', ' ', - ' ', + " ", "", ], [NO_VARIATION_INDEX, 0], @@ -599,6 +598,106 @@ class COLRVariationMergerTest: ], id="transform-yy-dy", ), + pytest.param( + [ + { + "Format": ot.PaintFormat.PaintTransform, + "Paint": { + "Format": ot.PaintFormat.PaintSweepGradient, + "ColorLine": { + "Extend": ot.ExtendMode.PAD, + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0}, + { + "StopOffset": 1.0, + "PaletteIndex": 1, + "Alpha": 1.0, + }, + ], + }, + "centerX": 0, + "centerY": 0, + "startAngle": -360, + "endAngle": 0, + }, + "Transform": (1.0, 0, 0, 1.0, 0, 0), + }, + { + "Format": ot.PaintFormat.PaintTransform, + "Paint": { + "Format": ot.PaintFormat.PaintSweepGradient, + "ColorLine": { + "Extend": ot.ExtendMode.PAD, + "ColorStop": [ + {"StopOffset": 0.0, "PaletteIndex": 0}, + { + "StopOffset": 1.0, + "PaletteIndex": 1, + "Alpha": 1.0, + }, + ], + }, + "centerX": 256, + "centerY": 0, + "startAngle": -360, + "endAngle": 0, + }, + # Transform.xx below produces the same VarStore delta as the + # above PaintSweepGradient's centerX because, when Fixed16.16 + # is converted to integer, it becomes: + # floatToFixed(1.00390625, 16) == 256 + # Because there is overlap between the varIdxes of the + # PaintVarTransform's Affine2x3 and the PaintSweepGradient's + # the VarIndexBase is reused (0 for both) + "Transform": (1.00390625, 0, 0, 1.0, 10, 0), + }, + ], + [ + '', + ' ', + " ", + ' ', + " ", + ' ', + ' ', + ' ', + ' ', + " ", + " ", + ' ', + ' ', + ' ', + ' ', + " ", + " ", + " ", + ' ', + ' ', + ' ', + ' ', + ' ', + " ", + " ", + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + " ", + "", + ], + [ + 0, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + NO_VARIATION_INDEX, + 1, + NO_VARIATION_INDEX, + ], + id="transform-xx-sweep_grad-centerx-same-varidxbase", + ), ], ) def test_merge_Paint(self, paints, ttFont, expected_xml, expected_varIdxes):