Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/release/next_whats_new/mathtext_font_fallback.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Font fallback in mathtext
-------------------------

Mathematical text (``mathtext`` and ``$...$`` strings) now falls back to other
fonts when a character is missing from the selected math font, mirroring the
behavior of regular text. Previously, characters absent from the math fonts --
such as CJK characters -- were replaced by a dummy glyph; they now render using
the configured fonts and their fallback chain (with the Last Resort font as a
final fallback), just like regular text.
15 changes: 14 additions & 1 deletion lib/matplotlib/_mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from . import cbook
from ._mathtext_data import (
latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni)
from .font_manager import FontProperties, findfont, get_font
from .font_manager import FontProperties, findfont, fontManager, get_font
from .ft2font import FT2Font, Kerning, LoadFlags


Expand Down Expand Up @@ -683,6 +683,19 @@ def _get_glyph(self, fontname: str, font_class: str,
if (fontname in ('it', 'regular', 'normal')
and isinstance(self, StixFonts)):
return self._get_glyph('rm', font_class, sym)
# Before substituting a dummy glyph, fall back through the
# configured text fonts and their fallback chain (system fonts
# and the Last Resort font), mirroring the regular-text path, so
# characters the math fonts lack -- such as CJK characters --
# still render. See
# https://github.com/matplotlib/matplotlib/issues/29173.
text_font = get_font(
fontManager._find_fonts_by_props(self.default_font_prop))
for fallback_font in [text_font, *text_font._fallbacks]:
if fallback_font.get_char_index(uniindex, _fallback=False):
_log.info("Substituting symbol %a from font %s.",
sym, fallback_font.family_name)
return fallback_font, uniindex, False
_log.warning("Font %r does not have a glyph for %a [U+%x], "
"substituting with a dummy symbol.",
new_fontname, sym, uniindex)
Expand Down
6 changes: 5 additions & 1 deletion lib/matplotlib/ft2font.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,17 @@ class FT2Font(Buffer):
features: tuple[str, ...] | None = ...,
language: str | tuple[tuple[str, int, int], ...] | None = ...,
) -> list[LayoutItem]: ...
@property
def _fallbacks(self) -> list[FT2Font]: ...
def clear(self) -> None: ...
def draw_glyph_to_bitmap(
self, image: NDArray[np.uint8], x: int, y: int, glyph: Glyph, antialiased: bool = ...
) -> None: ...
def draw_glyphs_to_bitmap(self, antialiased: bool = ...) -> None: ...
def get_bitmap_offset(self) -> tuple[int, int]: ...
def get_char_index(self, codepoint: CharacterCodeType) -> GlyphIndexType: ...
def get_char_index(
self, codepoint: CharacterCodeType, *, _fallback: bool = ...
) -> GlyphIndexType: ...
def get_charmap(self) -> dict[CharacterCodeType, GlyphIndexType]: ...
def get_descent(self) -> int: ...
def get_glyph_name(self, index: GlyphIndexType) -> str: ...
Expand Down
14 changes: 14 additions & 0 deletions lib/matplotlib/tests/test_mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,20 @@ def test_mathtext_fallback(fallback, fontlist):
mpl.font_manager.fontManager.ttflist.pop()


@pytest.mark.parametrize("char", ['中', '한', 'あ']) # Chinese, Korean, Japanese
def test_mathtext_cjk_fallback(char):
# Characters that the math fonts lack -- such as CJK characters -- should
# fall back through the font's fallback chain (mirroring regular text)
# rather than being replaced by a dummy glyph.
# https://github.com/matplotlib/matplotlib/issues/29173
fonts = _mathtext.DejaVuSansFonts(fm.FontProperties(), LoadFlags.NO_HINTING)
font, num, _ = fonts._get_glyph('rm', 'normal', char)
# The original code point is preserved (not the 0xA4 dummy currency sign)...
assert num == ord(char)
# ... and the returned font actually contains a glyph for it.
assert font.get_char_index(num, _fallback=False) != 0


def test_math_to_image(tmp_path):
mathtext.math_to_image('$x^2$', tmp_path / 'example.png')
mathtext.math_to_image('$x^2$', io.BytesIO())
Expand Down
3 changes: 3 additions & 0 deletions src/ft2font_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1922,6 +1922,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
.def_property_readonly(
"fname", &PyFT2Font_fname,
"The original filename for this object.")
.def_property_readonly(
"_fallbacks", [](PyFT2Font *self) { return self->fallbacks; },
"The fallback fonts used to find glyphs missing from this font.")

.def_buffer([](PyFT2Font &self) -> py::buffer_info {
return self.get_image().request();
Expand Down
Loading