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
121 changes: 120 additions & 1 deletion lib/matplotlib/_mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
import matplotlib as mpl
from . import cbook
from ._mathtext_data import (
latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni)
latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni,
unicode_math_lut, greek_uppercase_domain, greek_lowercase_domain)
from .font_manager import FontProperties, findfont, get_font
from .ft2font import FT2Font, Kerning, LoadFlags

Expand Down Expand Up @@ -740,6 +741,124 @@ class DejaVuSansFonts(DejaVuFonts):
}


class UnicodeMathFonts(TruetypeFonts):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, math fonts should have tables with various layout metrics. We currently have those hard-coded in the various FontsConstantsBase subclasses, and they are likely incorrect for an arbitrary math font.

So this will likely need to parse this data out of the font and implement at least get_axis_height that was added in #31046, get_xheight maybe using #31050, and get_quad from #31110. But it is likely that you will want to refactor some of those remaining uses of the constants so that they fetch the information from the fonts as well.

Copy link
Author

@llohse llohse Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fully agree. Doing this may involve some refactoring though, because the FontsConstantsBase subclass could not be determined purely from fontname but would be dynamically populated based on the loaded OpenType font.

That said, I made some experiments locally. Unfortunately, Freetype does not parse the MATH table. We could use fonttools, which is a hard dependency anyway.
There are several open questions how to map the OpenType layout metrics to the legacy TeX-inspired variables used in mathtext. Does it make sense to postpone that to a separate PR and focus on the basics here?

"""
A font handling class for Unicode mathematics fonts.

In addition to what TruetypeFonts provides, this class:

- supports mapping alphanumeric characters (latin and greek) to different alphabet
styles (such as bold, italic, fraktur, script, double-struck, ...) defined in
the Unicode standard.

"""

def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags):
TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags)
prop = mpl.rcParams['mathtext.mathfont'] # type: ignore[index]
font = findfont(prop)
self.fontmap['mathfont'] = font

_slanted_symbols = set(r"\int \oint".split())

def _get_font(self, font: str | int) -> FT2Font:
# work around, since we only populate 'mathfont' in the fontmap
if font not in ('default', 'regular'):
font = 'mathfont'

return super()._get_font(font)

def _get_glyph(self, fontname: str, font_class: str,
sym: str) -> tuple[FT2Font, CharacterCodeType, bool]:
# `fontname' is one of rm cal it tt sf bf bfit default bb frak scr regular
# font_class is not currently supported

# 1) map symbol to unicode index
try:
uniindex = get_unicode_index(sym)
found_symbol = True
except ValueError:
uniindex = ord('?')
found_symbol = False
_log.warning("No TeX to Unicode mapping for %a.", sym)

if fontname in ('default', 'regular'):
slanted = False
font = self._get_font(fontname)
return font, uniindex, slanted

# 2) remap unicode index based on fontname and font_class (bf, it, ...)
# Unicode mathematical alphanumeric symbols define all (relevant) variants

# from here on: use the Math font
new_fontname = 'mathfont'

def _is_digit(codepoint: CharacterCodeType) -> bool:
return 0x30 <= codepoint <= 0x39

def _is_latin_uppercase(codepoint: CharacterCodeType) -> bool:
return 0x41 <= codepoint <= 0x5a

def _is_latin_lowercase(codepoint: CharacterCodeType) -> bool:
return 0x61 <= codepoint <= 0x7a

def _is_greek_uppercase(codepoint: CharacterCodeType) -> bool:
return codepoint in greek_uppercase_domain

def _is_greek_lowercase(codepoint: CharacterCodeType) -> bool:
return codepoint in greek_lowercase_domain

# check if character is digit, latin letter, or greek letter
if _is_digit(uniindex):
# handle digits
_alphabet_map = {
'rm': 'up',
'it': 'up', # convention! digits always upright - not handled in Parser
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to #29253 (comment) I think.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I tried to replicate the logic of StixFonts here. This would have to be fixed in the parser and in all font classes at once, I believe. The parser would have to explicitly distinguish between just the math environment and \mathit.
One might even consider to pass that information to _get_glyph and implement the italic vs non-italic logic for math environments there instead of doing that in the parser..

'tt': 'tt',
'sf': 'sfup',
'bf': 'bfup',
'bfit': 'bfup',
'bb': 'bb',
}
alphabet = _alphabet_map.get(fontname, 'up')
alphabet_lut = unicode_math_lut.get(alphabet, {})
new_uniindex = alphabet_lut.get(uniindex, uniindex)
elif _is_latin_uppercase(uniindex) or _is_latin_lowercase(uniindex):
_alphabet_map = {
'rm': 'up',
'cal': 'scr',
'it': 'it',
'tt': 'tt',
'sf': 'sfup',
'bf': 'bfup',
'bfit': 'bfit',
'bb': 'bb',
'frak': 'frak',
'scr': 'scr'
}
alphabet = _alphabet_map.get(fontname, 'up')
alphabet_lut = unicode_math_lut.get(alphabet, {})
new_uniindex = alphabet_lut.get(uniindex, uniindex)
elif _is_greek_uppercase(uniindex) or _is_greek_lowercase(uniindex):
_alphabet_map = {
'rm': 'up',
'it': 'it',
'bf': 'bfup',
'bfit': 'bfit',
}
alphabet = _alphabet_map.get(fontname, 'up')
alphabet_lut = unicode_math_lut.get(alphabet, {})
new_uniindex = alphabet_lut.get(uniindex, uniindex)
else:
alphabet = 'up'
new_uniindex = uniindex

slanted = (alphabet == 'it') or sym in self._slanted_symbols
font = self._get_font(new_fontname)

return font, new_uniindex, slanted


class StixFonts(UnicodeFonts):
"""
A font handling class for the STIX fonts.
Expand Down
Loading
Loading