Repository: fonttools/fonttools

Issue:
CFF specializer error with fontmake variable-cff2 output
I have a font that has a glyph with a stray point that gives an error within the CFF specializer.

This happens only when outputting with `-o variable-cff2`, not with `-o variable`.

The glyph contains one "normal" shape (a line and a curve) and a stray (single, loose) point.

If I remove the stray point the compile works. But if I delete the other shape, the compile *also* works, so the problem is not *just* with the stray point.

It seems to have to do with the exact coordinates of the shape: I have tried to draw a shape with the same topology (but different coordinates), but failed to reproduce the problem.

Now, of course the source data is buggy, but it's at least odd that this only fails for cff2 output.

I have attached a zip with a .designspace file and two UFOs, each containing a single glyph.

```
Traceback (most recent call last):
  File "xxx/venv/lib/python3.12/site-packages/fontmake/font_project.py", line 1208, in run_from_designspace
    self._run_from_designspace_interpolatable(
  File "xxx/venv/lib/python3.12/site-packages/fontmake/font_project.py", line 1326, in _run_from_designspace_interpolatable
    self.build_variable_fonts(
  File "xxx/venv/lib/python3.12/site-packages/fontmake/font_project.py", line 470, in build_variable_fonts
    fonts = ufo2ft.compileVariableCFF2s(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "xxx/venv/lib/python3.12/site-packages/ufo2ft/__init__.py", line 295, in compileVariableCFF2s
    return VariableCFF2sCompiler(**kwargs).compile_variable(designSpaceDoc)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "xxx/venv/lib/python3.12/site-packages/ufo2ft/_compilers/baseCompiler.py", line 420, in compile_variable
    vfNameToTTFont = self._merge(designSpaceDoc, excludeVariationTables)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "xxx/venv/lib/python3.12/site-packages/ufo2ft/_compilers/interpolatableOTFCompiler.py", line 32, in _merge
    return varLib.build_many(
           ^^^^^^^^^^^^^^^^^^
  File "xxx/venv/lib/python3.12/site-packages/fontTools/varLib/__init__.py", line 1141, in build_many
    vf = build(
         ^^^^^^
  File "xxx/venv/lib/python3.12/site-packages/fontTools/varLib/__init__.py", line 1241, in build
    _add_CFF2(vf, model, master_fonts)
  File "xxx/venv/lib/python3.12/site-packages/fontTools/varLib/__init__.py", line 869, in _add_CFF2
    merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
  File "xxx/venv/lib/python3.12/site-packages/fontTools/varLib/cff.py", line 266, in merge_region_fonts
    cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "xxx/venv/lib/python3.12/site-packages/fontTools/varLib/cff.py", line 338, in merge_charstrings
    new_cs = var_pen.getCharString(
             ^^^^^^^^^^^^^^^^^^^^^^
  File "xxx/venv/lib/python3.12/site-packages/fontTools/varLib/cff.py", line 624, in getCharString
    commands = specializeCommands(
               ^^^^^^^^^^^^^^^^^^^
  File "xxx/venv/lib/python3.12/site-packages/fontTools/cffLib/specializer.py", line 583, in specializeCommands
    commands[i - 1] = ("rmoveto", [v1[0] + v2[0], v1[1] + v2[1]])
                                                  ~~~~~~^~~~~~~
TypeError: can only concatenate list (not "int") to list
```

[specializer-bug.zip](https://github.com/user-attachments/files/20417738/specializer-bug.zip)

Hints from issue comments:
@behdad: I find it curious that passing `--optimize-cff=0` to fontmake still tries to run `specializeCommands`. And yes, I can reproduce the bug.@behdad: My bad. Here's the working patch:
```diff
diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py
index 5fddcb67d..d42aa6748 100644
--- a/Lib/fontTools/cffLib/specializer.py
+++ b/Lib/fontTools/cffLib/specializer.py
@@ -580,7 +580,7 @@ def specializeCommands(
     for i in range(len(commands) - 1, 0, -1):
         if "rmoveto" == commands[i][0] == commands[i - 1][0]:
             v1, v2 = commands[i - 1][1], commands[i][1]
-            commands[i - 1] = ("rmoveto", [v1[0] + v2[0], v1[1] + v2[1]])
+            commands[i - 1] = ("rmoveto", [_addArgs(v1[0], v2[0]), _addArgs(v1[1], v2[1])])
             del commands[i]
 
     # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
```

@justvanrossum Any chance you can add some kind of a test?@behdad: 
> [@justvanrossum](https://github.com/justvanrossum) Any chance you can add some kind of a test?

Nevermind. PR sent with test.

Solution patch:
diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py
index 5fddcb67d..974060c40 100644
--- a/Lib/fontTools/cffLib/specializer.py
+++ b/Lib/fontTools/cffLib/specializer.py
@@ -580,7 +580,10 @@ def specializeCommands(
     for i in range(len(commands) - 1, 0, -1):
         if "rmoveto" == commands[i][0] == commands[i - 1][0]:
             v1, v2 = commands[i - 1][1], commands[i][1]
-            commands[i - 1] = ("rmoveto", [v1[0] + v2[0], v1[1] + v2[1]])
+            commands[i - 1] = (
+                "rmoveto",
+                [_addArgs(v1[0], v2[0]), _addArgs(v1[1], v2[1])],
+            )
             del commands[i]
 
     # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.


Test patch:
diff --git a/Tests/cffLib/specializer_test.py b/Tests/cffLib/specializer_test.py
index ebe702fee..ba6c93623 100644
--- a/Tests/cffLib/specializer_test.py
+++ b/Tests/cffLib/specializer_test.py
@@ -361,6 +361,10 @@ class CFFSpecializeProgramTest:
             ("100 0 0 rmoveto", "100 0 hmoveto"),  # test_rmoveto_zero_width
             (".55 -.8 rmoveto", "0.55 -0.8 rmoveto"),  # test_rmoveto
             ("55 -8 rmoveto " * 3, "165 -24 rmoveto"),  # test_rmoveto_mult
+            (
+                "0.55 -0.8 rmoveto 10 20 1 blend 30 rmoveto",
+                "10.55 20 1 blend 29.2 rmoveto",
+            ),  # test_rmoveto_blend
             ("100.5 50 -5.8 rmoveto", None),  # test_rmoveto_width
             # rlineto
             ("0 0 rlineto", ""),  # test_rlineto_zero
@@ -568,8 +572,10 @@ class CFFSpecializeProgramTest:
     def test_specialize(self, charstr, expected):
         if expected is None:
             expected = charstr
+        numRegions = 2
+        getNumRegions = lambda iv: numRegions
         expected = expected.strip()
-        specialized = charstr_specialize(charstr)
+        specialized = charstr_specialize(charstr, getNumRegions=getNumRegions)
         assert specialized == expected, (specialized, expected)
 
     # maxstack CFF=48, specializer uses up to 47