from io import BytesIO, StringIO import multiprocessing import os from pathlib import Path import shutil import subprocess import sys import warnings import numpy as np import pytest from matplotlib.font_manager import ( findfont, findSystemFonts, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, MSUserFontDirectories, _get_fontconfig_fonts) from matplotlib import pyplot as plt, rc_context has_fclist = shutil.which('fc-list') is not None def test_font_priority(): with rc_context(rc={ 'font.sans-serif': ['cmmi10', 'Bitstream Vera Sans']}): font = findfont(FontProperties(family=["sans-serif"])) assert Path(font).name == 'cmmi10.ttf' # Smoketest get_charmap, which isn't used internally anymore font = get_font(font) cmap = font.get_charmap() assert len(cmap) == 131 assert cmap[8729] == 30 def test_score_weight(): assert 0 == fontManager.score_weight("regular", "regular") assert 0 == fontManager.score_weight("bold", "bold") assert (0 < fontManager.score_weight(400, 400) < fontManager.score_weight("normal", "bold")) assert (0 < fontManager.score_weight("normal", "regular") < fontManager.score_weight("normal", "bold")) assert (fontManager.score_weight("normal", "regular") == fontManager.score_weight(400, 400)) def test_json_serialization(tmpdir): # Can't open a NamedTemporaryFile twice on Windows, so use a temporary # directory instead. path = Path(tmpdir, "fontlist.json") json_dump(fontManager, path) copy = json_load(path) with warnings.catch_warnings(): warnings.filterwarnings('ignore', 'findfont: Font family.*not found') for prop in ({'family': 'STIXGeneral'}, {'family': 'Bitstream Vera Sans', 'weight': 700}, {'family': 'no such font family'}): fp = FontProperties(**prop) assert (fontManager.findfont(fp, rebuild_if_missing=False) == copy.findfont(fp, rebuild_if_missing=False)) def test_otf(): fname = '/usr/share/fonts/opentype/freefont/FreeMono.otf' if Path(fname).exists(): assert is_opentype_cff_font(fname) for f in fontManager.ttflist: if 'otf' in f.fname: with open(f.fname, 'rb') as fd: res = fd.read(4) == b'OTTO' assert res == is_opentype_cff_font(f.fname) @pytest.mark.skipif(not has_fclist, reason='no fontconfig installed') def test_get_fontconfig_fonts(): assert len(_get_fontconfig_fonts()) > 1 @pytest.mark.parametrize('factor', [2, 4, 6, 8]) def test_hinting_factor(factor): font = findfont(FontProperties(family=["sans-serif"])) font1 = get_font(font, hinting_factor=1) font1.clear() font1.set_size(12, 100) font1.set_text('abc') expected = font1.get_width_height() hinted_font = get_font(font, hinting_factor=factor) hinted_font.clear() hinted_font.set_size(12, 100) hinted_font.set_text('abc') # Check that hinting only changes text layout by a small (10%) amount. np.testing.assert_allclose(hinted_font.get_width_height(), expected, rtol=0.1) def test_utf16m_sfnt(): try: # seguisbi = Microsoft Segoe UI Semibold entry = next(entry for entry in fontManager.ttflist if Path(entry.fname).name == "seguisbi.ttf") except StopIteration: pytest.skip("Couldn't find font to test against.") else: # Check that we successfully read "semibold" from the font's sfnt table # and set its weight accordingly. assert entry.weight == 600 def test_find_ttc(): fp = FontProperties(family=["WenQuanYi Zen Hei"]) if Path(findfont(fp)).name != "wqy-zenhei.ttc": pytest.skip("Font may be missing") fig, ax = plt.subplots() ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: fig.savefig(BytesIO(), format=fmt) def test_find_invalid(tmpdir): tmp_path = Path(tmpdir) with pytest.raises(FileNotFoundError): get_font(tmp_path / 'non-existent-font-name.ttf') with pytest.raises(FileNotFoundError): get_font(str(tmp_path / 'non-existent-font-name.ttf')) with pytest.raises(FileNotFoundError): get_font(bytes(tmp_path / 'non-existent-font-name.ttf')) # Not really public, but get_font doesn't expose non-filename constructor. from matplotlib.ft2font import FT2Font with pytest.raises(TypeError, match='path or binary-mode file'): FT2Font(StringIO()) @pytest.mark.skipif(sys.platform != 'linux', reason='Linux only') def test_user_fonts_linux(tmpdir, monkeypatch): font_test_file = 'mpltest.ttf' # Precondition: the test font should not be available fonts = findSystemFonts() if any(font_test_file in font for font in fonts): pytest.skip(f'{font_test_file} already exists in system fonts') # Prepare a temporary user font directory user_fonts_dir = tmpdir.join('fonts') user_fonts_dir.ensure(dir=True) shutil.copyfile(Path(__file__).parent / font_test_file, user_fonts_dir.join(font_test_file)) with monkeypatch.context() as m: m.setenv('XDG_DATA_HOME', str(tmpdir)) _get_fontconfig_fonts.cache_clear() # Now, the font should be available fonts = findSystemFonts() assert any(font_test_file in font for font in fonts) # Make sure the temporary directory is no longer cached. _get_fontconfig_fonts.cache_clear() @pytest.mark.skipif(sys.platform != 'win32', reason='Windows only') def test_user_fonts_win32(): if not (os.environ.get('APPVEYOR') or os.environ.get('TF_BUILD')): pytest.xfail("This test should only run on CI (appveyor or azure) " "as the developer's font directory should remain " "unchanged.") font_test_file = 'mpltest.ttf' # Precondition: the test font should not be available fonts = findSystemFonts() if any(font_test_file in font for font in fonts): pytest.skip(f'{font_test_file} already exists in system fonts') user_fonts_dir = MSUserFontDirectories[0] # Make sure that the user font directory exists (this is probably not the # case on Windows versions < 1809) os.makedirs(user_fonts_dir) # Copy the test font to the user font directory shutil.copy(Path(__file__).parent / font_test_file, user_fonts_dir) # Now, the font should be available fonts = findSystemFonts() assert any(font_test_file in font for font in fonts) def _model_handler(_): fig, ax = plt.subplots() fig.savefig(BytesIO(), format="pdf") plt.close() @pytest.mark.skipif(not hasattr(os, "register_at_fork"), reason="Cannot register at_fork handlers") def test_fork(): _model_handler(0) # Make sure the font cache is filled. ctx = multiprocessing.get_context("fork") with ctx.Pool(processes=2) as pool: pool.map(_model_handler, range(2)) def test_missing_family(caplog): plt.rcParams["font.sans-serif"] = ["this-font-does-not-exist"] with caplog.at_level("WARNING"): findfont("sans") assert [rec.getMessage() for rec in caplog.records] == [ "findfont: Font family ['sans'] not found. " "Falling back to DejaVu Sans.", "findfont: Generic family 'sans' not found because none of the " "following families were found: this-font-does-not-exist", ] def _test_threading(): import threading from matplotlib.ft2font import LOAD_NO_HINTING import matplotlib.font_manager as fm N = 10 b = threading.Barrier(N) def bad_idea(n): b.wait() for j in range(100): font = fm.get_font(fm.findfont("DejaVu Sans")) font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING) threads = [ threading.Thread(target=bad_idea, name=f"bad_thread_{j}", args=(j,)) for j in range(N) ] for t in threads: t.start() for t in threads: t.join() def test_fontcache_thread_safe(): pytest.importorskip('threading') import inspect proc = subprocess.run( [sys.executable, "-c", inspect.getsource(_test_threading) + '\n_test_threading()'] ) if proc.returncode: pytest.fail("The subprocess returned with non-zero exit status " f"{proc.returncode}.")