Repository: fonttools/fonttools

Issue:
[merger] Cursive attachment merging fails when all anchors are NULL
Carried over from https://github.com/googlefonts/fontmake/issues/1022.

When all anchors in all masters are of the form:

```
    pos cursive _0 <anchor NULL> <anchor 25 0>;
```

the merger creates an ExitEntryAnchor like this:

```python
        rec = ot.EntryExitRecord()
        rec.EntryAnchor = ot.Anchor()
        rec.EntryAnchor.Format = 1
        rec.ExitAnchor = ot.Anchor()
        rec.ExitAnchor.Format = 1
        self.EntryExitRecord.append(rec)
```

This means that deep inside `mergeThings`, when trying to merge the entry anchors, we hit this code path:

```python
            if allNone(lst):
                if out is not None:
                    raise FoundANone(self, got=lst)
```

All entry anchors are None, but the target entry anchor is an `Anchor` so merging fails.


Discussion:
is this perhaps a regression introduced with https://github.com/fonttools/fonttools/pull/3209 three weeks ago?
Confirmed, it works at 56731a9f1 but not at b14a29c35.

Bad Behdad. ;-)
how does one reproduces exactly? could you push a failing test case in varLib/merger_test.py so Behdad can fix it
Will do. I could probably fix it myself, TBH; it just needs a custom merger for EntryAnchor/ExitAnchors.

Resolving patch:
diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -936,6 +936,15 @@ def merge(merger, self, lst):
     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]


Test patch:
diff --git a/Tests/varLib/merger_test.py b/Tests/varLib/merger_test.py
--- a/Tests/varLib/merger_test.py
+++ b/Tests/varLib/merger_test.py
@@ -1902,32 +1902,38 @@ def test_sparse_cursive(self):
 
         ds.sources[0].font.newGlyph("a").unicode = ord("a")
         ds.sources[0].font.newGlyph("b").unicode = ord("b")
+        ds.sources[0].font.newGlyph("c").unicode = ord("c")
         ds.sources[
             0
         ].font.features.text = """
         feature curs {
           position cursive a <anchor 400 20> <anchor 0 -20>;
+          position cursive c <anchor NULL> <anchor 0 -20>;
         } curs;
         """
 
         ds.sources[1].font.newGlyph("a").unicode = ord("a")
         ds.sources[1].font.newGlyph("b").unicode = ord("b")
+        ds.sources[1].font.newGlyph("c").unicode = ord("c")
         ds.sources[
             1
         ].font.features.text = """
         feature curs {
           position cursive a <anchor 500 20> <anchor 0 -20>;
           position cursive b <anchor 50 22> <anchor 0 -10>;
+          position cursive c <anchor NULL> <anchor 0 -20>;
         } curs;
         """
 
         ds.sources[2].font.newGlyph("a").unicode = ord("a")
         ds.sources[2].font.newGlyph("b").unicode = ord("b")
+        ds.sources[2].font.newGlyph("c").unicode = ord("c")
         ds.sources[
             2
         ].font.features.text = """
         feature curs {
           position cursive b <anchor 100 40> <anchor 0 -30>;
+          position cursive c <anchor NULL> <anchor 0 -20>;
         } curs;
         """