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):