Repository: fonttools/fonttools

Issue:
[subset] FeatureVariations subsetted incorrectly
Take this font by @arrowtype which contains loads of GSUB rvrn FeatureVariations substitutions:
https://github.com/arrowtype/recursive/releases/download/1.027/recursive-MONO_CASL_wght_slnt_ital--full_gsub--2019_11_30-20_07.ttf

If you run `fonttools subset --unicodes='*'` on it, the "rvrn" feature and all its lookups are dropped, even though "rvrn" is in the subsetter's default list of layout features to keep. Also, if one explicitly adds "rvrn" to the `--layout-features+=rvrn` option, still it is being subsetted away.

I think the problem is that the `prune_pre_subset` stage is modifying the GSUB.table.FeatureList.FeatureRecord array (dropping feature records for tags not included in the options.layout_features). But then later, since the FeatureVariations references the feature records by their indexes and these may have changed inthe pre-subset pruning, the FeatureVariations record may be incorrectly dropped.

I have this patch that fixes the problem, but before I send a PR I want to make a good test case that reproduces the issue. I'll work on that later.

```diff
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index 581c3948..fea85f0e 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -1307,6 +1307,9 @@ def subset_features(self, feature_indices):
        self.ensureDecompiled()
        self.SubstitutionRecord = [r for r in self.SubstitutionRecord
                                     if r.FeatureIndex in feature_indices]
+       feature_index_map = dict(zip(feature_indices, range(len(feature_indices))))
+       for r in self.SubstitutionRecord:
+               r.FeatureIndex = feature_index_map[r.FeatureIndex]
        self.SubstitutionCount = len(self.SubstitutionRecord)
        return bool(self.SubstitutionCount)

```

Thanks @arrowtype for reporting this bug! :+1


Discussion:
Thanks for taking the time to investigate and start creating a patch for it, Cosimo!
Correct.

Also see how that's done in LangSys:
```
@_add_method(otTables.DefaultLangSys,
                         otTables.LangSys)
def subset_features(self, feature_indices):
        if self.ReqFeatureIndex in feature_indices:
                self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex)
        else:
                self.ReqFeatureIndex = 65535
        self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices]
        # Now map them.
        self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex
                                                      if f in feature_indices]
        self.FeatureCount = len(self.FeatureIndex)
        return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
```

Resolving patch:
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -1307,6 +1307,9 @@ def subset_features(self, feature_indices):
 	self.ensureDecompiled()
 	self.SubstitutionRecord = [r for r in self.SubstitutionRecord
 				     if r.FeatureIndex in feature_indices]
+	# remap feature indices
+	for r in self.SubstitutionRecord:
+		r.FeatureIndex = feature_indices.index(r.FeatureIndex)
 	self.SubstitutionCount = len(self.SubstitutionRecord)
 	return bool(self.SubstitutionCount)
 


Test patch:
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -1,5 +1,7 @@
+import io
 from fontTools.misc.py23 import *
 from fontTools import subset
+from fontTools.fontBuilder import FontBuilder
 from fontTools.ttLib import TTFont, newTable
 from fontTools.misc.loggingTools import CapturingLogHandler
 import difflib
@@ -728,5 +730,43 @@ def test_subset_flavor(self):
         self.assertEqual(ttf.flavor, None)
 
 
+def test_subset_feature_variations():
+    fb = FontBuilder(unitsPerEm=100)
+    fb.setupGlyphOrder([".notdef", "f", "f_f", "dollar", "dollar.rvrn"])
+    fb.setupCharacterMap({ord("f"): "f", ord("$"): "dollar"})
+    fb.setupNameTable({"familyName": "TestFeatureVars", "styleName": "Regular"})
+    fb.setupPost()
+    fb.setupFvar(axes=[("wght", 100, 400, 900, "Weight")], instances=[])
+    fb.addOpenTypeFeatures("""\
+        feature dlig {
+            sub f f by f_f;
+        } dlig;
+    """)
+    fb.addFeatureVariations(
+        [([{"wght": (0.20886, 1.0)}], {"dollar": "dollar.rvrn"})],
+        featureTag="rvrn"
+    )
+    buf = io.BytesIO()
+    fb.save(buf)
+    buf.seek(0)
+
+    font = TTFont(buf)
+
+    options = subset.Options()
+    subsetter = subset.Subsetter(options)
+    subsetter.populate(unicodes=[ord("f"), ord("$")])
+    subsetter.subset(font)
+
+    featureTags = {
+        r.FeatureTag for r in font["GSUB"].table.FeatureList.FeatureRecord
+    }
+    # 'dlig' is discretionary so it is dropped by default
+    assert "dlig" not in featureTags
+    assert "f_f" not in font.getGlyphOrder()
+    # 'rvrn' is required so it is kept by default
+    assert "rvrn" in featureTags
+    assert "dollar.rvrn" in font.getGlyphOrder()
+
+
 if __name__ == "__main__":
     sys.exit(unittest.main())