diff --git a/doc/users/fonts.rst b/doc/users/fonts.rst
index e385c98284c0..d4aff698de02 100644
--- a/doc/users/fonts.rst
+++ b/doc/users/fonts.rst
@@ -128,3 +128,59 @@ This is especially helpful to generate *really lightweight* documents.::
free versions of the proprietary fonts.
This also violates the *what-you-see-is-what-you-get* feature of Matplotlib.
+
+Are we reinventing the wheel?
+-----------------------------
+Internally, a feasible response to the question of 'reinventing the
+wheel would be, well, Yes *and No*. The font-matching algorithm used
+by Matplotlib has been *inspired* by web browsers, more specifically,
+`CSS Specifications `_.
+
+Currently, the simplest way (and the only way) to tell Matplotlib what fonts
+you want it to use for your document is via the **font.family** rcParam,
+see :doc:`Customizing text properties `.
+
+This is similar to how one tells a browser to use multiple font families
+(specified in their order of preference) for their HTML webpages. By using
+**font-family** in their stylesheet, users can essentially trigger a very
+useful feature provided by browers, known as Font-Fallback. For example, the
+following snippet in an HTML markup would:
+
+.. code-block:: html
+
+
+
+
+
+ some text
+
+
+
+For every character/glyph in *"some text"*, the browser will iterate through
+the whole list of font-families, and check whether that character/glyph is
+available in that font-family. As soon as a font is found which has the
+required glyph(s), the browser uses that font to render that character, and
+subsequently moves on to the next character.
+
+How does Matplotlib achieve this?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Currently, Matplotlib can't render a multi-font document. It was initially
+only designed to use a **single font** throughout the document, i.e., no matter
+how many families you pass to **font.family** rcParam, Matplotlib would use the
+very first font it's able to find on your system, and try to render all your
+characters/glyphs from that *and only that* font.
+
+.. note::
+ This is, because the internal font matching was written/adapted
+ from a very old `CSS1 spec `_,
+ **written in 1998**!
+
+ However, allowing multiple fonts for a single document (also enabling
+ Font-Fallback) is one of the goals for 2021's Google Summer of Code project.
+
+ `Read more on Matplotblog `_!
+
diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py
index b937c64fce95..0de7f1bbc0f0 100644
--- a/lib/matplotlib/backends/backend_agg.py
+++ b/lib/matplotlib/backends/backend_agg.py
@@ -35,7 +35,7 @@
from matplotlib.backend_bases import (
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
RendererBase)
-from matplotlib.font_manager import findfont, get_font
+from matplotlib.font_manager import get_font, find_fontsprop
from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
LOAD_DEFAULT, LOAD_NO_AUTOHINT)
from matplotlib.mathtext import MathTextParser
@@ -251,8 +251,8 @@ def _get_agg_font(self, prop):
"""
Get the font for text instance t, caching for efficiency
"""
- fname = findfont(prop)
- font = get_font(fname)
+ finterface = find_fontsprop(prop)
+ font = get_font(finterface)
font.clear()
size = prop.get_size_in_points()
diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py
index 9d45575eb13d..b126b92f8f66 100644
--- a/lib/matplotlib/font_manager.py
+++ b/lib/matplotlib/font_manager.py
@@ -23,6 +23,7 @@
# - setWeights function needs improvement
# - 'light' is an invalid weight value, remove it.
+from collections import OrderedDict
import dataclasses
from functools import lru_cache
import json
@@ -1308,6 +1309,116 @@ def findfont(self, prop, fontext='ttf', directory=None,
prop, fontext, directory, fallback_to_default, rebuild_if_missing,
rc_params)
+ def find_fontsprop(self, prop, fontext='ttf', directory=None,
+ fallback_to_default=True, rebuild_if_missing=True):
+ """
+ Find font families that most closely matches the given properties.
+
+ Parameters
+ ----------
+ prop : str or `~matplotlib.font_manager.FontProperties`
+ The font properties to search for. This can be either a
+ `.FontProperties` object or a string defining a
+ `fontconfig patterns`_.
+
+ fontext : {'ttf', 'afm'}, default: 'ttf'
+ The extension of the font file:
+
+ - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf)
+ - 'afm': Adobe Font Metrics (.afm)
+
+ directory : str, optional
+ If given, only search this directory and its subdirectories.
+
+ fallback_to_default : bool
+ If True, will fallback to the default font family (usually
+ "DejaVu Sans" or "Helvetica") if none of the families were found.
+
+ rebuild_if_missing : bool
+ Whether to rebuild the font cache and search again if the first
+ match appears to point to a nonexisting font (i.e., the font cache
+ contains outdated entries).
+
+ Returns
+ -------
+ OrderedDict
+ key, value pair of families and their corresponding filepaths.
+
+ Notes
+ -----
+ This is a plugin to original findfont API, which only returns a
+ single font for given font properties. Instead, this API returns
+ an OrderedDict containing multiple fonts and their filepaths which
+ closely match the given font properties.
+ Since this internally uses original API, there's no change
+ to the logic of performing the nearest neighbor search.
+ See `findfont` for more details.
+
+ """
+
+ rc_params = tuple(tuple(rcParams[key]) for key in [
+ "font.serif", "font.sans-serif", "font.cursive", "font.fantasy",
+ "font.monospace"])
+
+ prop = FontProperties._from_any(prop)
+ ffamily = prop.get_family()
+
+ fpaths = OrderedDict()
+ for fidx in range(len(ffamily)):
+ cprop = prop.copy()
+
+ # set current prop's family
+ cprop.set_family(ffamily[fidx])
+
+ # do not fall back to default font
+ fpath = self._findfontsprop_cached(
+ ffamily[fidx], cprop, fontext, directory,
+ False, rebuild_if_missing, rc_params
+ )
+ if fpath:
+ fpaths[ffamily[fidx]] = fpath
+
+ # only add default family if no other font was found
+ # and fallback_to_default is enabled
+ if not fpaths:
+ if fallback_to_default:
+ dfamily = self.defaultFamily[fontext]
+ cprop = prop.copy().set_family(dfamily)
+ fpath = self._findfontsprop_cached(
+ dfamily, cprop, fontext, directory,
+ True, rebuild_if_missing, rc_params
+ )
+ fpaths[dfamily] = fpath
+ else:
+ raise ValueError("Failed to find any font, and fallback "
+ "to the default font was disabled.")
+
+ return fpaths
+
+ @lru_cache()
+ def _findfontsprop_cached(
+ self, family, prop, fontext, directory,
+ fallback_to_default, rebuild_if_missing, rc_params
+ ):
+ try:
+ return self._findfont_cached(
+ prop, fontext, directory, fallback_to_default,
+ rebuild_if_missing, rc_params
+ )
+ except ValueError:
+ if not fallback_to_default:
+ if family.lower() in font_family_aliases:
+ _log.warning(
+ "findfont: Generic family %r not found because "
+ "none of the following families were found: %s",
+ family,
+ ", ".join(self._expand_aliases(family))
+ )
+ else:
+ _log.warning(
+ 'findfont: Font family \'%s\' not found.', family
+ )
+
@lru_cache()
def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
rebuild_if_missing, rc_params):
@@ -1401,9 +1512,11 @@ def is_opentype_cff_font(filename):
@lru_cache(64)
-def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id):
+def _get_font(fpaths, hinting_factor, *, _kerning_factor, thread_id):
+ # multiple paths, take first one
+ # for now, which is always guaranteed
return ft2font.FT2Font(
- filename, hinting_factor, _kerning_factor=_kerning_factor)
+ fpaths[0], hinting_factor, _kerning_factor=_kerning_factor)
# FT2Font objects cannot be used across fork()s because they reference the same
@@ -1417,11 +1530,14 @@ def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id):
def get_font(filename, hinting_factor=None):
# Resolving the path avoids embedding the font twice in pdf/ps output if a
# single font is selected using two different relative paths.
- filename = _cached_realpath(filename)
+ if isinstance(filename, OrderedDict):
+ paths = tuple(_cached_realpath(fname) for fname in filename.values())
+ else:
+ paths = (_cached_realpath(filename),)
if hinting_factor is None:
hinting_factor = rcParams['text.hinting_factor']
# also key on the thread ID to prevent segfaults with multi-threading
- return _get_font(filename, hinting_factor,
+ return _get_font(paths, hinting_factor,
_kerning_factor=rcParams['text.kerning_factor'],
thread_id=threading.get_ident())
@@ -1446,3 +1562,4 @@ def _load_fontmanager(*, try_read_cache=True):
fontManager = _load_fontmanager()
findfont = fontManager.findfont
+find_fontsprop = fontManager.find_fontsprop