Repository: googlefonts/ufo2ft
Pull request #479 (2021-03-03 13:43:42)

Pass filters argument to compile funcs and PreProcessor
Continues #466 and is needed for https://github.com/googlefonts/fontmake/pull/737.

This also adds BaseFilter.pre to specify if the filter is a pre-filter or a post-filter. It is False, for post-filter, by default, as `loadFilter()` puts filters in postFilters by default.


Patch:
diff --git a/Lib/ufo2ft/__init__.py b/Lib/ufo2ft/__init__.py
index 06c61b7..6825465 100644
--- a/Lib/ufo2ft/__init__.py
+++ b/Lib/ufo2ft/__init__.py
@@ -39,6 +39,7 @@ def compileOTF(
     outlineCompilerClass=OutlineOTFCompiler,
     featureCompilerClass=None,
     featureWriters=None,
+    filters=None,
     glyphOrder=None,
     useProductionNames=None,
     optimizeCFF=CFFOptimization.SUBROUTINIZE,
@@ -75,6 +76,13 @@ def compileOTF(
       writer in the given order. If featureWriters is None, the default
       feature writers [KernFeatureWriter, MarkFeatureWriter] are used.
 
+    *filters* argument is a list of BaseFilters subclasses or pre-initialized
+      instances. Filters with 'pre' attribute set to True will be pre-filters
+      called before the default filters, otherwise they will be post-filters,
+      called after the default filters.
+      Filters will modify glyphs or the glyph set. The default filters cannot
+      be disabled.
+
     *useProductionNames* renames glyphs in TrueType 'post' or OpenType 'CFF '
       tables based on the 'public.postscriptNames' mapping in the UFO lib,
       if present. Otherwise, uniXXXX names are generated from the glyphs'
@@ -117,6 +125,7 @@ def compileOTF(
         overlapsBackend=overlapsBackend,
         layerName=layerName,
         skipExportGlyphs=skipExportGlyphs,
+        filters=filters,
     )
     glyphSet = preProcessor.process()
 
@@ -161,6 +170,7 @@ def compileTTF(
     outlineCompilerClass=OutlineTTFCompiler,
     featureCompilerClass=None,
     featureWriters=None,
+    filters=None,
     glyphOrder=None,
     useProductionNames=None,
     convertCubics=True,
@@ -213,6 +223,7 @@ def compileTTF(
         rememberCurveType=rememberCurveType,
         layerName=layerName,
         skipExportGlyphs=skipExportGlyphs,
+        filters=filters,
     )
     glyphSet = preProcessor.process()
 
@@ -248,6 +259,7 @@ def compileInterpolatableTTFs(
     glyphOrder=None,
     useProductionNames=None,
     cubicConversionError=None,
+    filters=None,
     reverseDirection=True,
     flattenComponents=False,
     inplace=False,
@@ -297,6 +309,7 @@ def compileInterpolatableTTFs(
         reverseDirection=reverseDirection,
         layerNames=layerNames,
         skipExportGlyphs=skipExportGlyphs,
+        filters=filters,
     )
     glyphSets = preProcessor.process()
 
@@ -356,6 +369,7 @@ def compileInterpolatableTTFsFromDS(
     glyphOrder=None,
     useProductionNames=None,
     cubicConversionError=None,
+    filters=None,
     reverseDirection=True,
     flattenComponents=False,
     inplace=False,
@@ -411,6 +425,7 @@ def compileInterpolatableTTFsFromDS(
         glyphOrder=glyphOrder,
         useProductionNames=useProductionNames,
         cubicConversionError=cubicConversionError,
+        filters=filters,
         reverseDirection=reverseDirection,
         flattenComponents=flattenComponents,
         inplace=inplace,
@@ -436,6 +451,7 @@ def compileInterpolatableOTFsFromDS(
     outlineCompilerClass=OutlineOTFCompiler,
     featureCompilerClass=None,
     featureWriters=None,
+    filters=None,
     glyphOrder=None,
     useProductionNames=None,
     roundTolerance=None,
@@ -495,6 +511,7 @@ def compileInterpolatableOTFsFromDS(
                 useProductionNames=useProductionNames,
                 optimizeCFF=CFFOptimization.NONE,
                 roundTolerance=roundTolerance,
+                filters=filters,
                 removeOverlaps=False,
                 overlapsBackend=None,
                 inplace=inplace,
@@ -571,6 +588,7 @@ def compileVariableTTF(
     glyphOrder=None,
     useProductionNames=None,
     cubicConversionError=None,
+    filters=None,
     reverseDirection=True,
     excludeVariationTables=(),
     optimizeGvar=True,
@@ -603,6 +621,7 @@ def compileVariableTTF(
         glyphOrder=glyphOrder,
         useProductionNames=False,  # will rename glyphs after varfont is built
         cubicConversionError=cubicConversionError,
+        filters=filters,
         reverseDirection=reverseDirection,
         flattenComponents=flattenComponents,
         inplace=inplace,
@@ -631,6 +650,7 @@ def compileVariableCFF2(
     glyphOrder=None,
     useProductionNames=None,
     roundTolerance=None,
+    filters=None,
     excludeVariationTables=(),
     inplace=False,
     debugFeatureFile=None,
@@ -663,6 +683,7 @@ def compileVariableCFF2(
         outlineCompilerClass=outlineCompilerClass,
         featureCompilerClass=featureCompilerClass,
         featureWriters=featureWriters,
+        filters=filters,
         glyphOrder=glyphOrder,
         useProductionNames=False,  # will rename glyphs after varfont is built
         roundTolerance=roundTolerance,
diff --git a/Lib/ufo2ft/filters/__init__.py b/Lib/ufo2ft/filters/__init__.py
index 159df68..e43223c 100644
--- a/Lib/ufo2ft/filters/__init__.py
+++ b/Lib/ufo2ft/filters/__init__.py
@@ -2,7 +2,7 @@ import importlib
 import logging
 from inspect import getfullargspec, isclass
 
-from ufo2ft.constants import FILTERS_KEY as UFO2FT_FILTERS_KEY  # keep previous name
+from ufo2ft.constants import FILTERS_KEY
 from ufo2ft.util import _loadPluginFromString
 
 from .base import BaseFilter
@@ -56,7 +56,7 @@ def loadFilters(ufo):
     glyphs, another for the filters that are applied after decomposition.
     """
     preFilters, postFilters = [], []
-    for filterDict in ufo.lib.get(UFO2FT_FILTERS_KEY, []):
+    for filterDict in ufo.lib.get(FILTERS_KEY, []):
         namespace = filterDict.get("namespace", "ufo2ft.filters")
         try:
             filterClass = getFilterClass(filterDict["name"], namespace)
@@ -68,10 +68,11 @@ def loadFilters(ufo):
         filterObj = filterClass(
             include=filterDict.get("include"),
             exclude=filterDict.get("exclude"),
+            pre=filterDict.get("pre", False),
             *filterDict.get("args", []),
             **filterDict.get("kwargs", {}),
         )
-        if filterDict.get("pre"):
+        if filterObj.pre:
             preFilters.append(filterObj)
         else:
             postFilters.append(filterObj)
diff --git a/Lib/ufo2ft/filters/base.py b/Lib/ufo2ft/filters/base.py
index 55847d2..0286414 100644
--- a/Lib/ufo2ft/filters/base.py
+++ b/Lib/ufo2ft/filters/base.py
@@ -18,6 +18,10 @@ class BaseFilter:
     # their default values, which will be set as instance attributes
     _kwargs = {}
 
+    # pre-filter when True, post-filter when False, meaning before or after default
+    # filters
+    _pre = False
+
     def __init__(self, *args, **kwargs):
         self.options = options = SimpleNamespace()
 
@@ -64,6 +68,9 @@ class BaseFilter:
         for key, default in self._kwargs.items():
             setattr(options, key, kwargs.pop(key, default))
 
+        # process special pre argument
+        self.pre = kwargs.pop("pre", self._pre)
+
         # process special include/exclude arguments
         include = kwargs.pop("include", None)
         exclude = kwargs.pop("exclude", None)
diff --git a/Lib/ufo2ft/preProcessor.py b/Lib/ufo2ft/preProcessor.py
index 05ac47d..d51cc8b 100644
--- a/Lib/ufo2ft/preProcessor.py
+++ b/Lib/ufo2ft/preProcessor.py
@@ -31,7 +31,13 @@ class BasePreProcessor:
     """
 
     def __init__(
-        self, ufo, inplace=False, layerName=None, skipExportGlyphs=None, **kwargs
+        self,
+        ufo,
+        inplace=False,
+        layerName=None,
+        skipExportGlyphs=None,
+        filters=None,
+        **kwargs
     ):
         self.ufo = ufo
         self.inplace = inplace
@@ -40,7 +46,11 @@ class BasePreProcessor:
             ufo, layerName, copy=not inplace, skipExportGlyphs=skipExportGlyphs
         )
         self.defaultFilters = self.initDefaultFilters(**kwargs)
-        self.preFilters, self.postFilters = loadFilters(ufo)
+        if filters is None:
+            self.preFilters, self.postFilters = loadFilters(ufo)
+        else:
+            self.preFilters = [f for f in filters if f.pre]
+            self.postFilters = [f for f in filters if not f.pre]
 
     def initDefaultFilters(self, **kwargs):
         return []  # pragma: no cover
@@ -216,6 +226,7 @@ class TTFInterpolatablePreProcessor:
         rememberCurveType=True,
         layerNames=None,
         skipExportGlyphs=None,
+        filters=None,
     ):
         from cu2qu.ufo import DEFAULT_MAX_ERR
 
@@ -243,10 +254,17 @@ class TTFInterpolatablePreProcessor:
         self._rememberCurveType = rememberCurveType
 
         self.preFilters, self.postFilters = [], []
-        for ufo in ufos:
-            pre, post = loadFilters(ufo)
-            self.preFilters.append(pre)
-            self.postFilters.append(post)
+        if filters is None:
+            for ufo in ufos:
+                pre, post = loadFilters(ufo)
+                self.preFilters.append(pre)
+                self.postFilters.append(post)
+        else:
+            pre = [f for f in filters if f.pre]
+            post = [f for f in filters if not f.pre]
+            for _ in ufos:
+                self.preFilters.append(pre)
+                self.postFilters.append(post)
 
     def process(self):
         from cu2qu.ufo import fonts_to_quadratic


Test patch:
diff --git a/tests/filters/filters_test.py b/tests/filters/filters_test.py
index 385878b..2a0e52b 100644
--- a/tests/filters/filters_test.py
+++ b/tests/filters/filters_test.py
@@ -4,7 +4,7 @@ import pytest
 from fontTools.misc.loggingTools import CapturingLogHandler
 
 from ufo2ft.filters import (
-    UFO2FT_FILTERS_KEY,
+    FILTERS_KEY,
     BaseFilter,
     getFilterClass,
     loadFilterFromString,
@@ -66,19 +66,19 @@ class MockGlyph(SimpleNamespace):
 
 def test_loadFilters_empty():
     ufo = MockFont(lib={})
-    assert UFO2FT_FILTERS_KEY not in ufo.lib
+    assert FILTERS_KEY not in ufo.lib
     assert loadFilters(ufo) == ([], [])
 
 
 @pytest.fixture
 def ufo():
     ufo = MockFont(lib={})
-    ufo.lib[UFO2FT_FILTERS_KEY] = [{"name": "Foo Bar", "args": ["foo", "bar"]}]
+    ufo.lib[FILTERS_KEY] = [{"name": "Foo Bar", "args": ["foo", "bar"]}]
     return ufo
 
 
 def test_loadFilters_pre(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["pre"] = True
+    ufo.lib[FILTERS_KEY][0]["pre"] = True
     pre, post = loadFilters(ufo)
     assert len(pre) == 1
     assert not post
@@ -86,8 +86,8 @@ def test_loadFilters_pre(ufo):
 
 
 def test_loadFilters_custom_namespace(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["name"] = "Self Destruct"
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["namespace"] = "my_dangerous_filters"
+    ufo.lib[FILTERS_KEY][0]["name"] = "Self Destruct"
+    ufo.lib[FILTERS_KEY][0]["namespace"] = "my_dangerous_filters"
 
     class SelfDestructFilter(FooBarFilter):
         def filter(self, glyph):
@@ -106,7 +106,7 @@ def test_loadFilters_custom_namespace(ufo):
 
 
 def test_loadFilters_args_missing(ufo):
-    del ufo.lib[UFO2FT_FILTERS_KEY][0]["args"]
+    del ufo.lib[FILTERS_KEY][0]["args"]
 
     with pytest.raises(TypeError) as exc_info:
         loadFilters(ufo)
@@ -115,7 +115,7 @@ def test_loadFilters_args_missing(ufo):
 
 
 def test_loadFilters_args_unsupported(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["args"].append("baz")
+    ufo.lib[FILTERS_KEY][0]["args"].append("baz")
 
     with pytest.raises(TypeError) as exc_info:
         loadFilters(ufo)
@@ -124,8 +124,8 @@ def test_loadFilters_args_unsupported(ufo):
 
 
 def test_loadFilters_args_as_keywords(ufo):
-    del ufo.lib[UFO2FT_FILTERS_KEY][0]["args"]
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["kwargs"] = {"a": "foo", "b": "bar"}
+    del ufo.lib[FILTERS_KEY][0]["args"]
+    ufo.lib[FILTERS_KEY][0]["kwargs"] = {"a": "foo", "b": "bar"}
 
     _, [filter_obj] = loadFilters(ufo)
 
@@ -134,8 +134,8 @@ def test_loadFilters_args_as_keywords(ufo):
 
 
 def test_loadFilters_args_as_duplicated_keywords(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["args"] = ["foo"]
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["kwargs"] = {"a": "foo", "b": "bar"}
+    ufo.lib[FILTERS_KEY][0]["args"] = ["foo"]
+    ufo.lib[FILTERS_KEY][0]["kwargs"] = {"a": "foo", "b": "bar"}
 
     with pytest.raises(TypeError) as exc_info:
         loadFilters(ufo)
@@ -151,7 +151,7 @@ def test_loadFilters_include_all(ufo):
 
 
 def test_loadFilters_include_list(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["include"] = ["a", "b"]
+    ufo.lib[FILTERS_KEY][0]["include"] = ["a", "b"]
 
     _, [filter_obj] = loadFilters(ufo)
 
@@ -161,7 +161,7 @@ def test_loadFilters_include_list(ufo):
 
 
 def test_loadFilters_exclude_list(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["exclude"] = ["a", "b"]
+    ufo.lib[FILTERS_KEY][0]["exclude"] = ["a", "b"]
 
     _, [filter_obj] = loadFilters(ufo)
 
@@ -171,8 +171,8 @@ def test_loadFilters_exclude_list(ufo):
 
 
 def test_loadFilters_both_include_exclude(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["include"] = ["a", "b"]
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["exclude"] = ["c", "d"]
+    ufo.lib[FILTERS_KEY][0]["include"] = ["a", "b"]
+    ufo.lib[FILTERS_KEY][0]["exclude"] = ["c", "d"]
 
     with pytest.raises(ValueError) as exc_info:
         loadFilters(ufo)
@@ -181,7 +181,7 @@ def test_loadFilters_both_include_exclude(ufo):
 
 
 def test_loadFilters_failed(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY].append(dict(name="Non Existent"))
+    ufo.lib[FILTERS_KEY].append(dict(name="Non Existent"))
 
     with CapturingLogHandler(logger, level="ERROR") as captor:
         loadFilters(ufo)
@@ -190,9 +190,9 @@ def test_loadFilters_failed(ufo):
 
 
 def test_loadFilters_kwargs_unsupported(ufo):
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["kwargs"] = {}
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["kwargs"]["c"] = 1
-    ufo.lib[UFO2FT_FILTERS_KEY][0]["kwargs"]["d"] = 2  # unknown
+    ufo.lib[FILTERS_KEY][0]["kwargs"] = {}
+    ufo.lib[FILTERS_KEY][0]["kwargs"]["c"] = 1
+    ufo.lib[FILTERS_KEY][0]["kwargs"]["d"] = 2  # unknown
 
     with pytest.raises(TypeError) as exc_info:
         loadFilters(ufo)
diff --git a/tests/integration_test.py b/tests/integration_test.py
index e56699e..426d1d3 100644
--- a/tests/integration_test.py
+++ b/tests/integration_test.py
@@ -4,6 +4,7 @@ import os
 import sys
 
 import pytest
+from fontTools.pens.boundsPen import BoundsPen
 
 from ufo2ft import (
     compileInterpolatableTTFs,
@@ -13,6 +14,7 @@ from ufo2ft import (
     compileVariableTTF,
 )
 from ufo2ft.constants import KEEP_GLYPH_NAMES
+from ufo2ft.filters import TransformationsFilter
 
 
 def getpath(filename):
@@ -251,6 +253,81 @@ class IntegrationTest:
         ttf = compile_func(designspace, **options)
         expectTTX(ttf, expected_ttx)
 
+    @pytest.mark.parametrize(
+        "compileFunc",
+        [
+            compileOTF,
+            compileTTF,
+        ],
+    )
+    def test_compile_filters(self, compileFunc, FontClass):
+        ufo = FontClass(getpath("LayerFont-Regular.ufo"))
+        filters = [TransformationsFilter(OffsetY=10)]
+        ttf = compileFunc(ufo, filters=filters)
+
+        pen1 = BoundsPen(ufo)
+        glyph = ufo["a"]
+        glyph.draw(pen1)
+
+        glyphSet = ttf.getGlyphSet()
+        tt_glyph = glyphSet["a"]
+        pen2 = BoundsPen(glyphSet)
+        tt_glyph.draw(pen2)
+
+        assert pen1.bounds[0] == pen2.bounds[0]
+        assert pen1.bounds[1] + 10 == pen2.bounds[1]
+        assert pen1.bounds[2] == pen2.bounds[2]
+        assert pen1.bounds[3] + 10 == pen2.bounds[3]
+
+    @pytest.mark.parametrize(
+        "compileFunc",
+        [
+            compileVariableTTF,
+            compileVariableCFF2,
+        ],
+    )
+    def test_compileVariable_filters(self, designspace, compileFunc):
+        filters = [TransformationsFilter(OffsetY=10)]
+        varfont = compileFunc(designspace, filters=filters)
+
+        ufo = designspace.sources[0].font
+        pen1 = BoundsPen(ufo)
+        glyph = ufo["a"]
+        glyph.draw(pen1)
+
+        glyphSet = varfont.getGlyphSet()
+        tt_glyph = glyphSet["a"]
+        pen2 = BoundsPen(glyphSet)
+        tt_glyph.draw(pen2)
+
+        assert pen1.bounds[0] == pen2.bounds[0]
+        assert pen1.bounds[1] + 10 == pen2.bounds[1]
+        assert pen1.bounds[2] == pen2.bounds[2]
+        assert pen1.bounds[3] + 10 == pen2.bounds[3]
+
+    def test_compileInterpolatableTTFs(self, FontClass):
+        ufos = [
+            FontClass(getpath("NestedComponents-Regular.ufo")),
+            FontClass(getpath("NestedComponents-Bold.ufo")),
+        ]
+        filters = [TransformationsFilter(OffsetY=10)]
+        ttfs = compileInterpolatableTTFs(ufos, filters=filters)
+
+        for i, ttf in enumerate(ttfs):
+            glyph = ufos[i]["a"]
+            pen1 = BoundsPen(ufos[i])
+            glyph.draw(pen1)
+
+            glyphSet = ttf.getGlyphSet()
+            tt_glyph = glyphSet["uni0061"]
+            pen2 = BoundsPen(glyphSet)
+            tt_glyph.draw(pen2)
+
+            assert pen1.bounds[0] == pen2.bounds[0]
+            assert pen1.bounds[1] + 10 == pen2.bounds[1]
+            assert pen1.bounds[2] == pen2.bounds[2]
+            assert pen1.bounds[3] + 10 == pen2.bounds[3]
+
 
 if __name__ == "__main__":
     sys.exit(pytest.main(sys.argv))
diff --git a/tests/preProcessor_test.py b/tests/preProcessor_test.py
index 64196f9..328b668 100644
--- a/tests/preProcessor_test.py
+++ b/tests/preProcessor_test.py
@@ -11,7 +11,7 @@ from ufo2ft.constants import (
     COLOR_LAYERS_KEY,
     COLOR_PALETTES_KEY,
 )
-from ufo2ft.filters import UFO2FT_FILTERS_KEY
+from ufo2ft.filters import FILTERS_KEY, loadFilterFromString
 from ufo2ft.filters.explodeColorLayerGlyphs import ExplodeColorLayerGlyphsFilter
 from ufo2ft.preProcessor import (
     TTFInterpolatablePreProcessor,
@@ -77,6 +77,45 @@ class TTFPreProcessorTest:
             assert CURVE_TYPE_LIB_KEY not in ufo.layers.defaultLayer.lib
             assert glyph_has_qcurve(ufo, "c")
 
+    def test_custom_filters(self, FontClass):
+        ufo1 = FontClass(getpath("TestFont.ufo"))
+        ufo1.lib[FILTERS_KEY] = [
+            {"name": "transformations", "kwargs": {"OffsetX": -40}, "pre": True}
+        ]
+        ufo2 = FontClass(getpath("TestFont.ufo"))
+        ufo2.lib[FILTERS_KEY] = [{"name": "transformations", "kwargs": {"OffsetY": 10}}]
+
+        glyphSets0 = TTFPreProcessor(ufo1).process()
+        glyphSets1 = TTFPreProcessor(ufo2).process()
+
+        assert (glyphSets0["a"][0][0].x - glyphSets1["a"][0][0].x) == -40
+        assert (glyphSets1["a"][0][0].y - glyphSets0["a"][0][0].y) == 10
+
+    def test_custom_filters_as_argument(self, FontClass):
+        from ufo2ft.filters import RemoveOverlapsFilter, TransformationsFilter
+
+        ufo1 = FontClass(getpath("TestFont.ufo"))
+        ufo2 = FontClass(getpath("TestFont.ufo"))
+        filter1 = RemoveOverlapsFilter(backend="pathops")
+        filter2 = TransformationsFilter(include=["d"], pre=True, OffsetY=-200)
+        filter3 = TransformationsFilter(OffsetX=10)
+
+        glyphSets0 = TTFPreProcessor(
+            ufo1, filters=[filter1, filter2, filter3]
+        ).process()
+        glyphSets1 = TTFPreProcessor(
+            ufo2, filters=[filter1, filter2, filter3]
+        ).process()
+
+        # Both UFOs have the same filters applied
+        assert (glyphSets0["a"][0][0].x - glyphSets1["a"][0][0].x) == 0
+        # "a" has initially its starting point at (66, 0)
+        assert (glyphSets0["a"][0][0].x, glyphSets0["a"][0][0].y) == (76, 0)
+        assert (glyphSets1["a"][0][0].x, glyphSets1["a"][0][0].y) == (76, 0)
+        # A component was shifted to overlap with another in a pre-filter
+        # filter2, before overlaps were removed in a post-filter filter1
+        assert len(glyphSets0["d"].components) == 0
+
 
 class TTFInterpolatablePreProcessorTest:
     def test_no_inplace(self, FontClass):
@@ -130,13 +169,11 @@ class TTFInterpolatablePreProcessorTest:
 
     def test_custom_filters(self, FontClass):
         ufo1 = FontClass(getpath("TestFont.ufo"))
-        ufo1.lib[UFO2FT_FILTERS_KEY] = [
+        ufo1.lib[FILTERS_KEY] = [
             {"name": "transformations", "kwargs": {"OffsetX": -40}, "pre": True}
         ]
         ufo2 = FontClass(getpath("TestFont.ufo"))
-        ufo2.lib[UFO2FT_FILTERS_KEY] = [
-            {"name": "transformations", "kwargs": {"OffsetY": 10}}
-        ]
+        ufo2.lib[FILTERS_KEY] = [{"name": "transformations", "kwargs": {"OffsetY": 10}}]
         ufos = [ufo1, ufo2]
 
         glyphSets = TTFInterpolatablePreProcessor(ufos).process()
@@ -144,6 +181,30 @@ class TTFInterpolatablePreProcessorTest:
         assert (glyphSets[0]["a"][0][0].x - glyphSets[1]["a"][0][0].x) == -40
         assert (glyphSets[1]["a"][0][0].y - glyphSets[0]["a"][0][0].y) == 10
 
+    def test_custom_filters_as_argument(self, FontClass):
+        ufo1 = FontClass(getpath("TestFont.ufo"))
+        ufo2 = FontClass(getpath("TestFont.ufo"))
+        filter1 = loadFilterFromString("RemoveOverlapsFilter(backend='pathops')")
+        filter2 = loadFilterFromString(
+            "TransformationsFilter(OffsetY=-200, include=['d'], pre=True)"
+        )
+        filter3 = loadFilterFromString("TransformationsFilter(OffsetX=10)")
+        ufos = [ufo1, ufo2]
+
+        glyphSets = TTFInterpolatablePreProcessor(
+            ufos,
+            filters=[filter1, filter2, filter3],
+        ).process()
+
+        # Both UFOs have the same filters applied
+        assert (glyphSets[0]["a"][0][0].x - glyphSets[1]["a"][0][0].x) == 0
+        # "a" has initially its starting point at (66, 0)
+        assert (glyphSets[0]["a"][0][0].x, glyphSets[0]["a"][0][0].y) == (76, 0)
+        assert (glyphSets[1]["a"][0][0].x, glyphSets[1]["a"][0][0].y) == (76, 0)
+        # A component was shifted to overlap with another in a pre-filter
+        # filter2, before overlaps were removed in a post-filter filter1
+        assert len(glyphSets[0]["d"].components) == 0
+
 
 class SkipExportGlyphsTest:
     def test_skip_export_glyphs_filter(self, FontClass):