From 4c24a3e32823ad4fb5ee4e785451266271bb4df4 Mon Sep 17 00:00:00 2001
From: Behdad Esfahbod <behdad@behdad.org>
Date: Fri, 19 May 2023 10:03:50 -0600
Subject: [PATCH] [varLib/merger] Fix merging of SinglePos with pos=0

Fixes https://github.com/fonttools/fonttools/issues/3111
---
 Lib/fontTools/varLib/merger.py | 17 ++++++---------
 Tests/varLib/merger_test.py    | 39 ++++++++++++++++++++++++++++++++++
 requirements.txt               |  1 +
 3 files changed, 47 insertions(+), 10 deletions(-)

diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py
index 7e6392540..c3366cbcd 100644
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -81,7 +81,6 @@ class Merger(object):
         typ = type(thing)
 
         for celf in celf.mro():
-
             mergers = getattr(celf, "mergers", None)
             if mergers is None:
                 break
@@ -318,7 +317,13 @@ def merge(merger, self, lst):
     ):
         self.Value = otBase.ValueRecord(valueFormat, self.Value)
         if valueFormat != 0:
-            merger.mergeThings(self.Value, [v.Value for v in lst])
+            # 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
 
@@ -449,7 +454,6 @@ def _PairPosFormat1_merge(self, lst, merger):
 
 
 def _ClassDef_invert(self, allGlyphs=None):
-
     if isinstance(self, dict):
         classDefs = self
     else:
@@ -505,7 +509,6 @@ def _ClassDef_merge_classify(lst, allGlyphses=None):
 
 
 def _PairPosFormat2_align_matrices(self, lst, font, transparent=False):
-
     matrices = [l.Class1Record for l in lst]
 
     # Align first classes
@@ -1057,7 +1060,6 @@ def merge(merger, self, lst):
         ("XPlacement", "XPlaDevice"),
         ("YPlacement", "YPlaDevice"),
     ]:
-
         assert not hasattr(self, tableName)
 
         if hasattr(self, name):
@@ -1085,7 +1087,6 @@ class MutatorMerger(AligningMerger):
 
 @MutatorMerger.merger(ot.CaretValue)
 def merge(merger, self, lst):
-
     # Hack till we become selfless.
     self.__dict__ = lst[0].__dict__.copy()
 
@@ -1108,7 +1109,6 @@ def merge(merger, self, lst):
 
 @MutatorMerger.merger(ot.Anchor)
 def merge(merger, self, lst):
-
     # Hack till we become selfless.
     self.__dict__ = lst[0].__dict__.copy()
 
@@ -1139,7 +1139,6 @@ def merge(merger, self, lst):
 
 @MutatorMerger.merger(otBase.ValueRecord)
 def merge(merger, self, lst):
-
     # Hack till we become selfless.
     self.__dict__ = lst[0].__dict__.copy()
 
@@ -1150,7 +1149,6 @@ def merge(merger, self, lst):
         ("XPlacement", "XPlaDevice"),
         ("YPlacement", "YPlaDevice"),
     ]:
-
         if not hasattr(self, tableName):
             continue
         dev = getattr(self, tableName)
@@ -1266,7 +1264,6 @@ def merge(merger, self, lst):
         ("XPlacement", "XPlaDevice"),
         ("YPlacement", "YPlaDevice"),
     ]:
-
         if hasattr(self, name):
             value, deviceTable = buildVarDevTable(
                 merger.store_builder, [getattr(a, name, 0) for a in lst]
diff --git a/Tests/varLib/merger_test.py b/Tests/varLib/merger_test.py
index aa7a69984..c92921248 100644
--- a/Tests/varLib/merger_test.py
+++ b/Tests/varLib/merger_test.py
@@ -7,6 +7,7 @@ from fontTools.varLib.models import VariationModel
 from fontTools.ttLib import TTFont
 from fontTools.ttLib.tables import otTables as ot
 from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter
+from io import BytesIO
 import pytest
 
 
@@ -1842,3 +1843,41 @@ class COLRVariationMergerTest:
 
         if colr.table.LayerList:
             assert len({id(p) for p in colr.table.LayerList.Paint}) == after_layer_count
+
+
+class SparsePositioningMergerTest:
+    def test_sparse_positioning_at_default(self):
+        # https://github.com/fonttools/fonttools/issues/3111
+
+        pytest.importorskip("ufo2ft")
+        pytest.importorskip("ufoLib2")
+
+        from fontTools.designspaceLib import DesignSpaceDocument
+        from ufo2ft import compileVariableTTF
+        from ufoLib2 import Font
+
+        ds = DesignSpaceDocument()
+        ds.addAxisDescriptor(
+            name="wght", tag="wght", minimum=100, maximum=900, default=400
+        )
+        ds.addSourceDescriptor(font=Font(), location=dict(wght=100))
+        ds.addSourceDescriptor(font=Font(), location=dict(wght=400))
+        ds.addSourceDescriptor(font=Font(), location=dict(wght=900))
+
+        ds.sources[0].font.newGlyph("a").unicode = ord("a")
+        ds.sources[0].font.newGlyph("b").unicode = ord("b")
+        ds.sources[0].font.features.text = "feature kern { pos a b b' 100; } kern;"
+
+        ds.sources[1].font.newGlyph("a").unicode = ord("a")
+        ds.sources[1].font.newGlyph("b").unicode = ord("b")
+        ds.sources[1].font.features.text = "feature kern { pos a b b' 0; } kern;"
+
+        ds.sources[2].font.newGlyph("a").unicode = ord("a")
+        ds.sources[2].font.newGlyph("b").unicode = ord("b")
+        ds.sources[2].font.features.text = "feature kern { pos a b b' -100; } kern;"
+
+        font = compileVariableTTF(ds, inplace=True)
+        b = BytesIO()
+        font.save(b)
+
+        assert font["GDEF"].table.VarStore.VarData[0].Item[0] == [100, -100]
diff --git a/requirements.txt b/requirements.txt
index d8c98876a..690837eb2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,6 +10,7 @@ fs==2.4.16
 skia-pathops==0.7.3; platform_python_implementation != "PyPy"
 # this is only required to run Tests/cu2qu/{ufo,cli}_test.py
 ufoLib2==0.14.0
+ufo2ft==2.31.0
 pyobjc==9.0; sys_platform == "darwin"
 freetype-py==2.3.0
 uharfbuzz==0.32.0
-- 
2.27.0

