From b7dd61b9c991e22cfc53bab4470f83ffaeb6b815 Mon Sep 17 00:00:00 2001 From: PragnyaKhandelwal Date: Mon, 23 Mar 2026 15:47:52 +0530 Subject: [PATCH 1/7] ENH: expose font_file in brain.add_text --- doc/changes/dev/12543.newfeature.rst | 1 + mne/viz/_brain/_brain.py | 5 +++++ mne/viz/_brain/tests/test_brain.py | 2 +- mne/viz/backends/_abstract.py | 15 ++++++++++++++- mne/viz/backends/_pyvista.py | 20 +++++++++++++++++--- mne/viz/backends/tests/test_renderer.py | 1 + 6 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 doc/changes/dev/12543.newfeature.rst diff --git a/doc/changes/dev/12543.newfeature.rst b/doc/changes/dev/12543.newfeature.rst new file mode 100644 index 00000000000..226b20c6c43 --- /dev/null +++ b/doc/changes/dev/12543.newfeature.rst @@ -0,0 +1 @@ +Added a ``font_file`` parameter to ``brain.add_text()`` so custom font files (``.ttf``/``.ttc``) can be provided for improved Unicode glyph rendering in PyVista-backed brain figures. \ No newline at end of file diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 226cf158682..675e7604e2b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2796,6 +2796,7 @@ def add_text( col=0, font_size=None, justification=None, + font_file=None, ): """Add a text to the visualization. @@ -2824,6 +2825,9 @@ def add_text( The font size to use. justification : str | None The text justification. + font_file : path-like | None + Path to a ``.ttf`` or ``.ttc`` font file to use for rendering + the text. This can be helpful for Unicode glyph coverage. """ _validate_type(name, (str, None), "name") name = text if name is None else name @@ -2840,6 +2844,7 @@ def add_text( color=color, size=font_size, justification=justification, + font_file=font_file, ) if "text" not in self._actors: self._actors["text"] = dict() diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 7f0d178e538..7a1f4cd8268 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -538,7 +538,7 @@ def __init__(self): with pytest.raises(ValueError, match="already exists"): brain.add_text(x=0, y=0, text="foo") brain.remove_text("foo") - brain.add_text(x=0, y=0, text="foo") + brain.add_text(x=0, y=0, text="foo", font_file=None) brain.remove_text() brain.close() diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index 2a2b0abe4ed..4c81b1c4457 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -474,7 +474,16 @@ def quiver3d( pass @abstractclassmethod - def text2d(self, x_window, y_window, text, size=14, color="white"): + def text2d( + self, + x_window, + y_window, + text, + size=14, + color="white", + justification=None, + font_file=None, + ): """Add 2d text in the scene. Parameters @@ -493,6 +502,10 @@ def text2d(self, x_window, y_window, text, size=14, color="white"): The color of the text as a tuple (red, green, blue) of float values between 0 and 1 or a valid color name (i.e. 'white' or 'w'). + justification : str | None + The text justification. + font_file : path-like | None + Path to a ``.ttf`` or ``.ttc`` font file for rendering text. """ pass diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 500319467fd..5c2a5d0e233 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -720,13 +720,27 @@ def quiver3d( return actor, mesh def text2d( - self, x_window, y_window, text, size=14, color="white", justification=None + self, + x_window, + y_window, + text, + size=14, + color="white", + justification=None, + font_file=None, ): size = 14 if size is None else size position = (x_window, y_window) - actor = self.plotter.add_text( - text, position=position, font_size=size, color=color, viewport=True + kwargs = dict( + text=text, + position=position, + font_size=size, + color=color, + viewport=True, ) + if font_file is not None: + kwargs["font_file"] = font_file + actor = self.plotter.add_text(**kwargs) if isinstance(justification, str): if justification == "left": actor.GetTextProperty().SetJustificationToLeft() diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index 09833c6d503..f4a31f0e9f3 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -167,6 +167,7 @@ def test_3d_backend(renderer): text=txt_text, size=txt_size, justification="right", + font_file=None, ) rend.text3d(x=0, y=0, z=0, text=txt_text, scale=1.0) rend.set_camera( From b3c5ff6d5c6016f301a7e7267824027c7e6d2b09 Mon Sep 17 00:00:00 2001 From: PragnyaKhandelwal Date: Mon, 23 Mar 2026 16:09:13 +0530 Subject: [PATCH 2/7] DOC: fix towncrier fragment for PR #13778 --- doc/changes/dev/12543.newfeature.rst | 1 - doc/changes/dev/13778.newfeature.rst | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 doc/changes/dev/12543.newfeature.rst create mode 100644 doc/changes/dev/13778.newfeature.rst diff --git a/doc/changes/dev/12543.newfeature.rst b/doc/changes/dev/12543.newfeature.rst deleted file mode 100644 index 226b20c6c43..00000000000 --- a/doc/changes/dev/12543.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Added a ``font_file`` parameter to ``brain.add_text()`` so custom font files (``.ttf``/``.ttc``) can be provided for improved Unicode glyph rendering in PyVista-backed brain figures. \ No newline at end of file diff --git a/doc/changes/dev/13778.newfeature.rst b/doc/changes/dev/13778.newfeature.rst new file mode 100644 index 00000000000..14fffb37f2d --- /dev/null +++ b/doc/changes/dev/13778.newfeature.rst @@ -0,0 +1 @@ +Added a ``font_file`` parameter to :meth:`mne.viz.Brain.add_text` to allow custom ``.ttf``/``.ttc`` fonts for improved Unicode glyph rendering, by `Pragnya Khandelwal`_. \ No newline at end of file From be9d750639fffe30a685209dc6748b2078b84f26 Mon Sep 17 00:00:00 2001 From: PragnyaKhandelwal Date: Mon, 23 Mar 2026 18:21:46 +0530 Subject: [PATCH 3/7] ENH: address review on font_file passthrough --- mne/viz/backends/_pyvista.py | 3 +-- mne/viz/backends/tests/test_renderer.py | 30 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 5c2a5d0e233..6ca985f90ca 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -737,9 +737,8 @@ def text2d( font_size=size, color=color, viewport=True, + font_file=font_file, ) - if font_file is not None: - kwargs["font_file"] = font_file actor = self.plotter.add_text(**kwargs) if isinstance(justification, str): if justification == "left": diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index f4a31f0e9f3..69389721cb2 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -176,6 +176,36 @@ def test_3d_backend(renderer): rend.show() +def test_text2d_font_file_passthrough(renderer, monkeypatch): + """Test that text2d forwards font_file values to the backend.""" + rend = renderer.create_3d_figure( + size=(300, 300), + bgcolor="black", + smooth_shading=True, + scene=False, + ) + seen = [] + + class _DummyActor: + def GetTextProperty(self): + return None + + def SetVisibility(self, value): + return None + + def _add_text(**kwargs): + seen.append(kwargs) + return _DummyActor() + + monkeypatch.setattr(rend.plotter, "add_text", _add_text) + rend.text2d(x_window=0.0, y_window=0.0, text="label", font_file="dummy.ttf") + rend.text2d(x_window=0.0, y_window=0.0, text="label", font_file=None) + + assert seen[0]["font_file"] == "dummy.ttf" + assert "font_file" in seen[1] + assert seen[1]["font_file"] is None + + def test_get_3d_backend(renderer): """Test get_3d_backend function call for side-effects.""" # Test twice to ensure the first call had no side-effect From db15469391f61d299f6c58b079d79f65298150a6 Mon Sep 17 00:00:00 2001 From: PragnyaKhandelwal Date: Wed, 25 Mar 2026 00:53:00 +0530 Subject: [PATCH 4/7] DOC: improve font_file docs and test with real font --- mne/viz/_brain/_brain.py | 8 +++-- mne/viz/_brain/tests/test_brain.py | 2 +- mne/viz/backends/_abstract.py | 9 ++++-- mne/viz/backends/tests/test_renderer.py | 40 ++++++------------------- 4 files changed, 21 insertions(+), 38 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 675e7604e2b..0ade801f7d6 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2825,9 +2825,11 @@ def add_text( The font size to use. justification : str | None The text justification. - font_file : path-like | None - Path to a ``.ttf`` or ``.ttc`` font file to use for rendering - the text. This can be helpful for Unicode glyph coverage. + font_file : str | None + Path to an absolute path of a font file to use for rendering + the text. FreeType is used for loading, supporting many font + formats beyond ``.ttf`` and ``.ttc``. This can be helpful for + non-ASCII glyph coverage. """ _validate_type(name, (str, None), "name") name = text if name is None else name diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 7a1f4cd8268..7f0d178e538 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -538,7 +538,7 @@ def __init__(self): with pytest.raises(ValueError, match="already exists"): brain.add_text(x=0, y=0, text="foo") brain.remove_text("foo") - brain.add_text(x=0, y=0, text="foo", font_file=None) + brain.add_text(x=0, y=0, text="foo") brain.remove_text() brain.close() diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index 4c81b1c4457..6a4d2581a9e 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -503,9 +503,12 @@ def text2d( values between 0 and 1 or a valid color name (i.e. 'white' or 'w'). justification : str | None - The text justification. - font_file : path-like | None - Path to a ``.ttf`` or ``.ttc`` font file for rendering text. + The text justification. Can be 'left', 'center', or 'right'. + font_file : str | None + Path to an absolute path of a font file to use for rendering + the text. FreeType is used for loading, supporting many font + formats beyond ``.ttf`` and ``.ttc``. This can be helpful for + non-ASCII glyph coverage. """ pass diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index 69389721cb2..5e6f3ac285a 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -8,6 +8,7 @@ import numpy as np import pytest +from matplotlib.font_manager import findfont from mne.utils import run_subprocess from mne.viz import Figure3D, get_3d_backend, set_3d_backend @@ -167,7 +168,14 @@ def test_3d_backend(renderer): text=txt_text, size=txt_size, justification="right", - font_file=None, + ) + # test font_file passthrough with a real font from matplotlib + font_path = findfont(prop={}) + rend.text2d( + x_window=txt_x + 0.1, + y_window=txt_y + 0.1, + text="font test", + font_file=font_path, ) rend.text3d(x=0, y=0, z=0, text=txt_text, scale=1.0) rend.set_camera( @@ -176,36 +184,6 @@ def test_3d_backend(renderer): rend.show() -def test_text2d_font_file_passthrough(renderer, monkeypatch): - """Test that text2d forwards font_file values to the backend.""" - rend = renderer.create_3d_figure( - size=(300, 300), - bgcolor="black", - smooth_shading=True, - scene=False, - ) - seen = [] - - class _DummyActor: - def GetTextProperty(self): - return None - - def SetVisibility(self, value): - return None - - def _add_text(**kwargs): - seen.append(kwargs) - return _DummyActor() - - monkeypatch.setattr(rend.plotter, "add_text", _add_text) - rend.text2d(x_window=0.0, y_window=0.0, text="label", font_file="dummy.ttf") - rend.text2d(x_window=0.0, y_window=0.0, text="label", font_file=None) - - assert seen[0]["font_file"] == "dummy.ttf" - assert "font_file" in seen[1] - assert seen[1]["font_file"] is None - - def test_get_3d_backend(renderer): """Test get_3d_backend function call for side-effects.""" # Test twice to ensure the first call had no side-effect From a34397becdb8b50bafab87fb158e53837eb9be78 Mon Sep 17 00:00:00 2001 From: PragnyaKhandelwal Date: Wed, 25 Mar 2026 12:04:43 +0530 Subject: [PATCH 5/7] updated font style as "serif" --- mne/viz/backends/tests/test_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index 5e6f3ac285a..399aced2cfd 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -170,7 +170,7 @@ def test_3d_backend(renderer): justification="right", ) # test font_file passthrough with a real font from matplotlib - font_path = findfont(prop={}) + font_path = findfont("serif") rend.text2d( x_window=txt_x + 0.1, y_window=txt_y + 0.1, From ede3710b1aec148c872322bf1661b2db4eb16a1d Mon Sep 17 00:00:00 2001 From: PragnyaKhandelwal Date: Mon, 30 Mar 2026 22:13:41 +0530 Subject: [PATCH 6/7] ENH: pass text2d args explicitly in pyvista backend --- mne/viz/backends/_pyvista.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 6ca985f90ca..b24084e60f5 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -731,7 +731,7 @@ def text2d( ): size = 14 if size is None else size position = (x_window, y_window) - kwargs = dict( + actor = self.plotter.add_text( text=text, position=position, font_size=size, @@ -739,7 +739,6 @@ def text2d( viewport=True, font_file=font_file, ) - actor = self.plotter.add_text(**kwargs) if isinstance(justification, str): if justification == "left": actor.GetTextProperty().SetJustificationToLeft() From bda38f590220087f3a6ee98e9a988b58164a30fe Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 31 Mar 2026 10:29:47 -0500 Subject: [PATCH 7/7] Apply suggestions from code review Co-authored-by: Daniel McCloy --- doc/changes/dev/13778.newfeature.rst | 2 +- mne/viz/_brain/_brain.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/changes/dev/13778.newfeature.rst b/doc/changes/dev/13778.newfeature.rst index 14fffb37f2d..ad9dbafe8ac 100644 --- a/doc/changes/dev/13778.newfeature.rst +++ b/doc/changes/dev/13778.newfeature.rst @@ -1 +1 @@ -Added a ``font_file`` parameter to :meth:`mne.viz.Brain.add_text` to allow custom ``.ttf``/``.ttc`` fonts for improved Unicode glyph rendering, by `Pragnya Khandelwal`_. \ No newline at end of file +Added a ``font_file`` parameter to :meth:`mne.viz.Brain.add_text` to support rendering glyphs not available in the default font, by `Pragnya Khandelwal`_. \ No newline at end of file diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 0ade801f7d6..ad7c7a40995 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2827,9 +2827,8 @@ def add_text( The text justification. font_file : str | None Path to an absolute path of a font file to use for rendering - the text. FreeType is used for loading, supporting many font - formats beyond ``.ttf`` and ``.ttc``. This can be helpful for - non-ASCII glyph coverage. + the text. See https://freetype.org/freetype2/docs/index.html for a list of + supported font file formats. """ _validate_type(name, (str, None), "name") name = text if name is None else name