Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[without findfont diff] Parsing all families in font_manager #20549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
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
56 changes: 56 additions & 0 deletions doc/users/fonts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_.

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 </tutorials/text/text_props>`.

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

<style>
someTag {
font-family: Arial, Helvetica, sans-serif;
}
</style>

<!-- somewhere in the main body -->
<someTag>
some text
</someTag>


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 <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_,
**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 <https://matplotlib.org/matplotblog/>`_!

6 changes: 3 additions & 3 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
125 changes: 121 additions & 4 deletions lib/matplotlib/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

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

We talked on the phone about renaming this. The should invoke that is is going at a) find multiple fonts b) that the order of the returned fonts matters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Some suggestions:

  1. find_fonts
  2. find_orderedfonts
  3. find_fontsdict
  4. find_fontorder
  5. find_fontset
  6. find_fontfamilies
  7. find_orderedfamily
  8. find_familyordered
  9. find_families
  10. find_fontfamilies

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):
Expand Down Expand Up @@ -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
Expand All @@ -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())

Expand All @@ -1446,3 +1562,4 @@ def _load_fontmanager(*, try_read_cache=True):

fontManager = _load_fontmanager()
findfont = fontManager.findfont
find_fontsprop = fontManager.find_fontsprop