From b715ddb39340129a5684d1a53b44d5804c0a800e Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 18:10:34 +0530 Subject: [PATCH 01/15] Allow font manager to parse all families --- lib/matplotlib/font_manager.py | 65 +++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 9d45575eb13d..fd9b4c139a2e 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 @@ -1019,6 +1020,19 @@ def json_load(filename): return json.load(fh, object_hook=_json_decode) +class FontsPath: + """Class to hold the result of findfont""" + def __init__(self, file_paths): + self._filepaths = None + self.set_filepaths(file_paths) + + def set_filepaths(self, file_paths): + self._filepaths = file_paths + + def get_filepaths(self): + return self._filepaths + + def _normalize_font_family(family): if isinstance(family, str): family = [family] @@ -1304,16 +1318,39 @@ def findfont(self, prop, fontext='ttf', directory=None, rc_params = tuple(tuple(rcParams[key]) for key in [ "font.serif", "font.sans-serif", "font.cursive", "font.fantasy", "font.monospace"]) - return self._findfont_cached( - prop, fontext, directory, fallback_to_default, rebuild_if_missing, - rc_params) + + prop = FontProperties._from_any(prop) + ffamily = prop.get_family() + + # maintain two dicts, one for available paths, + # the other for fallback paths + fpaths, fbpaths = OrderedDict(), OrderedDict() + for fidx in range(len(ffamily)): + prop = prop.copy() + + # set current prop's family + prop.set_family(ffamily[fidx]) + + fpath = self._findfont_cached( + FontProperties._from_any(prop), fontext, directory, + fallback_to_default, rebuild_if_missing, rc_params) + + # if fontfile isn't found, fpath will be a FontsPath object + if isinstance(fpath, FontsPath): + fbpaths.update(fpath.get_filepaths()) + else: + fpaths[ffamily[fidx]] = fpath + + # append fallback font(s) to the very end + fpaths.update(fbpaths) + + return FontsPath(fpaths) + @lru_cache() def _findfont_cached(self, prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params): - prop = FontProperties._from_any(prop) - fname = prop.get_file() if fname is not None: return fname @@ -1401,7 +1438,10 @@ def is_opentype_cff_font(filename): @lru_cache(64) -def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id): +def _get_font(filenames, hinting_factor, *, _kerning_factor, thread_id): + # TODO: allow multiple files (future PR) + # for now just pass the first element + filename = filenames[0] return ft2font.FT2Font( filename, hinting_factor, _kerning_factor=_kerning_factor) @@ -1417,11 +1457,20 @@ 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, FontsPath): + filenames = [] + for fname in filename.get_filepaths().values(): + filenames.append(_cached_realpath(fname)) + else: + filenames = [_cached_realpath(filename)] if hinting_factor is None: hinting_factor = rcParams['text.hinting_factor'] + + # convert to tuple so its hashable + filenames = tuple(filenames) + # also key on the thread ID to prevent segfaults with multi-threading - return _get_font(filename, hinting_factor, + return _get_font(filenames, hinting_factor, _kerning_factor=rcParams['text.kerning_factor'], thread_id=threading.get_ident()) From 2b58d1603ead6079aa4ff3734fee61e5b403f9bc Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 18:57:03 +0530 Subject: [PATCH 02/15] Refactor and remove FontsPath class --- lib/matplotlib/font_manager.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index fd9b4c139a2e..672d4ea6fcc7 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1020,19 +1020,6 @@ def json_load(filename): return json.load(fh, object_hook=_json_decode) -class FontsPath: - """Class to hold the result of findfont""" - def __init__(self, file_paths): - self._filepaths = None - self.set_filepaths(file_paths) - - def set_filepaths(self, file_paths): - self._filepaths = file_paths - - def get_filepaths(self): - return self._filepaths - - def _normalize_font_family(family): if isinstance(family, str): family = [family] @@ -1335,16 +1322,16 @@ def findfont(self, prop, fontext='ttf', directory=None, FontProperties._from_any(prop), fontext, directory, fallback_to_default, rebuild_if_missing, rc_params) - # if fontfile isn't found, fpath will be a FontsPath object - if isinstance(fpath, FontsPath): - fbpaths.update(fpath.get_filepaths()) + # if fontfile isn't found, fpath will be an OrderedDict + if isinstance(fpath, OrderedDict): + fbpaths.update(fpath) else: fpaths[ffamily[fidx]] = fpath # append fallback font(s) to the very end fpaths.update(fbpaths) - return FontsPath(fpaths) + return fpaths @lru_cache() @@ -1457,9 +1444,9 @@ def _get_font(filenames, 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. - if isinstance(filename, FontsPath): + if isinstance(filename, OrderedDict): filenames = [] - for fname in filename.get_filepaths().values(): + for fname in filename.values(): filenames.append(_cached_realpath(fname)) else: filenames = [_cached_realpath(filename)] From 1235063bd1609754540dec73343c99aeb3979391 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 18:58:37 +0530 Subject: [PATCH 03/15] Pass only first font for PDF backend --- lib/matplotlib/backends/backend_pdf.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 10063bd9a7b3..55138fddd7ea 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -829,11 +829,16 @@ def fontName(self, fontprop): if isinstance(fontprop, str): filename = fontprop - elif mpl.rcParams['pdf.use14corefonts']: - filename = findfont( - fontprop, fontext='afm', directory=RendererPdf._afm_font_dir) else: - filename = findfont(fontprop) + if mpl.rcParams['pdf.use14corefonts']: + filename = findfont( + fontprop, fontext='afm', directory=RendererPdf._afm_font_dir) + else: + filename = findfont(fontprop) + + # TODO: allow multiple fonts for PDF backend + # for now settle with the first element + filename = next(iter(filename.values())) Fx = self.fontNames.get(filename) if Fx is None: From df939d55e4879bbfb8f9d81da48a84f7361aa976 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 18:58:48 +0530 Subject: [PATCH 04/15] Pass only first font for mathtext --- lib/matplotlib/_mathtext.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 3cc90e1f7501..e9306606d1e6 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -242,6 +242,9 @@ def destroy(self): def _get_font(self, font): if font in self.fontmap: basename = self.fontmap[font] + # TODO: allow multiple fonts + # for now settle with the first element + basename = next(iter(basename.values())) else: basename = font cached_font = self._fonts.get(basename) From 5215f1f16167350e433a1287d1b7c174b1f99e95 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 18:59:16 +0530 Subject: [PATCH 05/15] Fix check for cmr10 path --- lib/matplotlib/ticker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 2831069ab222..0419111c6754 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -483,10 +483,12 @@ def __init__(self, useOffset=None, useMathText=None, useLocale=None): ), fallback_to_default=False, ) + # visit all values + ufont = ufont.values() except ValueError: ufont = None - if ufont == str(cbook._get_data_path("fonts/ttf/cmr10.ttf")): + if str(cbook._get_data_path("fonts/ttf/cmr10.ttf")) in ufont: _api.warn_external( "cmr10 font should ideally be used with " "mathtext, set axes.formatter.use_mathtext to True" From dc8e5ee57a4edc717c02bfbe9e232d64eb21e32f Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 18:59:44 +0530 Subject: [PATCH 06/15] Fix mathtext test with first font --- lib/matplotlib/tests/test_mathtext.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 22079ccf9874..63d020963f6e 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -225,6 +225,8 @@ def test_mathfont_rendering(baseline_images, fontset, index, text): def test_fontinfo(): fontpath = mpl.font_manager.findfont("DejaVu Sans") + # get the first element + fontpath = next(iter(fontpath.values())) font = mpl.ft2font.FT2Font(fontpath) table = font.get_sfnt_table("head") assert table['version'] == (1, 0) From fb43b67b859b5377dd05a15f4f7d6f2072b3f80b Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 19:19:53 +0530 Subject: [PATCH 07/15] Flake8 fixes --- lib/matplotlib/backends/backend_pdf.py | 6 +++--- lib/matplotlib/font_manager.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 55138fddd7ea..7b60257f90c3 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -830,9 +830,9 @@ def fontName(self, fontprop): if isinstance(fontprop, str): filename = fontprop else: - if mpl.rcParams['pdf.use14corefonts']: - filename = findfont( - fontprop, fontext='afm', directory=RendererPdf._afm_font_dir) + if mpl.rcParams["pdf.use14corefonts"]: + filename = findfont(fontprop, fontext="afm", + directory=RendererPdf._afm_font_dir) else: filename = findfont(fontprop) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 672d4ea6fcc7..0181b6d69361 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1333,7 +1333,6 @@ def findfont(self, prop, fontext='ttf', directory=None, return fpaths - @lru_cache() def _findfont_cached(self, prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params): From 4aa661d4d1fc2fe0af03656861cfec097bfb5860 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 19:49:26 +0530 Subject: [PATCH 08/15] Fix check again --- lib/matplotlib/ticker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 0419111c6754..6e11a5dda818 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -488,7 +488,10 @@ def __init__(self, useOffset=None, useMathText=None, useLocale=None): except ValueError: ufont = None - if str(cbook._get_data_path("fonts/ttf/cmr10.ttf")) in ufont: + if ( + ufont is not None and + str(cbook._get_data_path("fonts/ttf/cmr10.ttf")) in ufont + ): _api.warn_external( "cmr10 font should ideally be used with " "mathtext, set axes.formatter.use_mathtext to True" From af4cef6b69d5b244fa941e947d791e2ab135a578 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 19:49:46 +0530 Subject: [PATCH 09/15] Pass only first font for PGF --- lib/matplotlib/backends/backend_pgf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 575d2d263496..457c3fa1a9d5 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -50,7 +50,13 @@ def get_fontspec(): for family, command in zip(families, commands): # 1) Forward slashes also work on Windows, so don't mess with # backslashes. 2) The dirname needs to include a separator. - path = pathlib.Path(fm.findfont(family)) + path = fm.findfont(family) + + # TODO: Allow multiple fonts + # for now stick with the first font + path = next(iter(path.values())) + + path = pathlib.Path(path) latex_fontspec.append(r"\%s{%s}[Path=\detokenize{%s}]" % ( command, path.name, path.parent.as_posix() + "/")) From 9d7883167e82e70473e2454e426f43b60ca97354 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 20:07:06 +0530 Subject: [PATCH 10/15] Embed only first font --- lib/matplotlib/font_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 0181b6d69361..f36bb7e58bcd 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1098,7 +1098,9 @@ def addfont(self, path): def defaultFont(self): # Lazily evaluated (findfont then caches the result) to avoid including # the venv path in the json serialization. - return {ext: self.findfont(family, fontext=ext) + + # TODO: allow embedding multiple fonts + return {ext: next(iter(self.findfont(family, fontext=ext).values())) for ext, family in self.defaultFamily.items()} def get_default_weight(self): From a2da73d280002058858e36d99f33ea426cf4f516 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 20:07:39 +0530 Subject: [PATCH 11/15] Pass only first font for pdf_ps --- lib/matplotlib/backends/_backend_pdf_ps.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 780e79bf71b8..a66c3d675972 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -108,6 +108,10 @@ def get_text_width_height_descent(self, s, prop, ismath): def _get_font_afm(self, prop): fname = font_manager.findfont( prop, fontext="afm", directory=self._afm_font_dir) + + # TODO: allow multiple font caching + # for now pass the first font + fname = next(iter(fname.values())) return _cached_get_afm_from_fname(fname) def _get_font_ttf(self, prop): From fb8c80a7519f086fdc36ef45f8e2efe4985b9e7c Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Wed, 23 Jun 2021 20:08:01 +0530 Subject: [PATCH 12/15] Fix font manager test --- lib/matplotlib/tests/test_font_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 4cad797b3757..d5c9be39c47a 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -24,7 +24,8 @@ def test_font_priority(): 'font.sans-serif': ['cmmi10', 'Bitstream Vera Sans']}): font = findfont(FontProperties(family=["sans-serif"])) - assert Path(font).name == 'cmmi10.ttf' + # first font should be cmmi10.ttf + assert Path(next(iter(font.values()))).name == 'cmmi10.ttf' # Smoketest get_charmap, which isn't used internally anymore font = get_font(font) @@ -110,7 +111,7 @@ def test_utf16m_sfnt(): def test_find_ttc(): fp = FontProperties(family=["WenQuanYi Zen Hei"]) - if Path(findfont(fp)).name != "wqy-zenhei.ttc": + if "wqy-zenhei.ttc" not in map(lambda x: Path(x).name, findfont(fp)): pytest.skip("Font may be missing") fig, ax = plt.subplots() From 804a4571f43be0f8bf5e60bc1b82c98c423e89c7 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Thu, 24 Jun 2021 03:51:40 +0530 Subject: [PATCH 13/15] Fix text tests --- lib/matplotlib/tests/test_text.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index dccb74ba0038..cb1c5d64e288 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -27,7 +27,7 @@ def test_font_styles(): def find_matplotlib_font(**kw): prop = FontProperties(**kw) path = findfont(prop, directory=mpl.get_data_path()) - return FontProperties(fname=path) + return FontProperties(fname=next(iter(path.values()))) from matplotlib.font_manager import FontProperties, findfont warnings.filterwarnings( @@ -198,6 +198,7 @@ def test_antialiasing(): def test_afm_kerning(): fn = mpl.font_manager.findfont("Helvetica", fontext="afm") + fn = next(iter(fn.values())) with open(fn, 'rb') as fh: afm = mpl.afm.AFM(fh) assert afm.string_width_height('VAVAVAVAVAVA') == (7174.0, 718) From 1dd195cf38afe2c6d8a396015cf4c9b58a8883be Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Fri, 25 Jun 2021 17:03:22 +0530 Subject: [PATCH 14/15] Variable name fix --- lib/matplotlib/font_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index f36bb7e58bcd..2234bf29e37d 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1315,13 +1315,13 @@ def findfont(self, prop, fontext='ttf', directory=None, # the other for fallback paths fpaths, fbpaths = OrderedDict(), OrderedDict() for fidx in range(len(ffamily)): - prop = prop.copy() + cprop = prop.copy() # set current prop's family - prop.set_family(ffamily[fidx]) + cprop.set_family(ffamily[fidx]) fpath = self._findfont_cached( - FontProperties._from_any(prop), fontext, directory, + FontProperties._from_any(cprop), fontext, directory, fallback_to_default, rebuild_if_missing, rc_params) # if fontfile isn't found, fpath will be an OrderedDict From 6c4c4100f7619de8a064dab6a6aa9356195155b0 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Sat, 26 Jun 2021 03:19:06 +0530 Subject: [PATCH 15/15] Fix findfont examples --- examples/misc/logos2.py | 4 ++-- examples/text_labels_and_annotations/font_table.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/misc/logos2.py b/examples/misc/logos2.py index 528f09e92c18..bae838be6ad0 100644 --- a/examples/misc/logos2.py +++ b/examples/misc/logos2.py @@ -20,10 +20,10 @@ def get_font_properties(): # The original font is Calibri, if that is not installed, we fall back # to Carlito, which is metrically equivalent. - if 'Calibri' in matplotlib.font_manager.findfont('Calibri:bold'): + if 'Calibri' in matplotlib.font_manager.findfont('Calibri:bold').keys(): return matplotlib.font_manager.FontProperties(family='Calibri', weight='bold') - if 'Carlito' in matplotlib.font_manager.findfont('Carlito:bold'): + if 'Carlito' in matplotlib.font_manager.findfont('Carlito:bold').keys(): print('Original font not found. Falling back to Carlito. ' 'The logo text will not be in the correct font.') return matplotlib.font_manager.FontProperties(family='Carlito', diff --git a/examples/text_labels_and_annotations/font_table.py b/examples/text_labels_and_annotations/font_table.py index e9296430ac13..0ef5877c0d42 100644 --- a/examples/text_labels_and_annotations/font_table.py +++ b/examples/text_labels_and_annotations/font_table.py @@ -34,6 +34,7 @@ def print_glyphs(path): """ if path is None: path = fm.findfont(fm.FontProperties()) # The default font. + path = next(iter(path.values())) # Get the first filepath font = FT2Font(path) @@ -60,6 +61,7 @@ def draw_font_table(path): """ if path is None: path = fm.findfont(fm.FontProperties()) # The default font. + path = next(iter(path.values())) # Get the first filepath font = FT2Font(path) # A charmap is a mapping of "character codes" (in the sense of a character