| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- from io import BytesIO, StringIO
- import gc
- import multiprocessing
- import os
- from pathlib import Path
- from PIL import Image
- import shutil
- import sys
- import warnings
- import numpy as np
- import pytest
- import matplotlib as mpl
- from matplotlib.font_manager import (
- findfont, findSystemFonts, FontEntry, FontProperties, fontManager,
- json_dump, json_load, get_font, is_opentype_cff_font,
- MSUserFontDirectories, _get_fontconfig_fonts, ttfFontProperty)
- from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure
- from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing
- has_fclist = shutil.which('fc-list') is not None
- def test_font_priority():
- with rc_context(rc={
- 'font.sans-serif':
- ['cmmi10', 'Bitstream Vera Sans']}):
- fontfile = findfont(FontProperties(family=["sans-serif"]))
- assert Path(fontfile).name == 'cmmi10.ttf'
- # Smoketest get_charmap, which isn't used internally anymore
- font = get_font(fontfile)
- 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(tmp_path):
- # Can't open a NamedTemporaryFile twice on Windows, so use a temporary
- # directory instead.
- json_dump(fontManager, tmp_path / "fontlist.json")
- copy = json_load(tmp_path / "fontlist.json")
- 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(sys.platform == "win32" or 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 seguisbi.ttf 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 wqy-zenhei.ttc 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_noto():
- fp = FontProperties(family=["Noto Sans CJK SC", "Noto Sans CJK JP"])
- name = Path(findfont(fp)).name
- if name not in ("NotoSansCJKsc-Regular.otf", "NotoSansCJK-Regular.ttc"):
- pytest.skip(f"Noto Sans CJK SC font may be missing (found {name})")
- fig, ax = plt.subplots()
- ax.text(0.5, 0.5, 'Hello, 你好', fontproperties=fp)
- for fmt in ["raw", "svg", "pdf", "ps"]:
- fig.savefig(BytesIO(), format=fmt)
- def test_find_invalid(tmp_path):
- 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='font file or a binary-mode file'):
- FT2Font(StringIO()) # type: ignore[arg-type]
- @pytest.mark.skipif(sys.platform != 'linux' or not has_fclist,
- reason='only Linux with fontconfig installed')
- 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()
- def test_addfont_as_path():
- """Smoke test that addfont() accepts pathlib.Path."""
- font_test_file = 'mpltest.ttf'
- path = Path(__file__).parent / font_test_file
- try:
- fontManager.addfont(path)
- added, = (font for font in fontManager.ttflist
- if font.fname.endswith(font_test_file))
- fontManager.ttflist.remove(added)
- finally:
- to_remove = [font for font in fontManager.ttflist
- if font.fname.endswith(font_test_file)]
- for font in to_remove:
- fontManager.ttflist.remove(font)
- @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.")
- pytest.xfail("We need to update the registry for this test to work")
- 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 LoadFlags
- import matplotlib.font_manager as fm
- def loud_excepthook(args):
- raise RuntimeError("error in thread!")
- threading.excepthook = loud_excepthook
- N = 10
- b = threading.Barrier(N)
- def bad_idea(n):
- b.wait(timeout=5)
- for j in range(100):
- font = fm.get_font(fm.findfont("DejaVu Sans"))
- font.set_text(str(n), 0.0, flags=LoadFlags.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(timeout=9)
- if t.is_alive():
- raise RuntimeError("thread failed to join")
- def test_fontcache_thread_safe():
- pytest.importorskip('threading')
- subprocess_run_helper(_test_threading, timeout=10)
- def test_lockfilefailure(tmp_path):
- # The logic here:
- # 1. get a temp directory from pytest
- # 2. import matplotlib which makes sure it exists
- # 3. get the cache dir (where we check it is writable)
- # 4. make it not writable
- # 5. try to write into it via font manager
- proc = subprocess_run_for_testing(
- [
- sys.executable,
- "-c",
- "import matplotlib;"
- "import os;"
- "p = matplotlib.get_cachedir();"
- "os.chmod(p, 0o555);"
- "import matplotlib.font_manager;"
- ],
- env={**os.environ, 'MPLCONFIGDIR': str(tmp_path)},
- check=True
- )
- def test_fontentry_dataclass():
- fontent = FontEntry(name='font-name')
- png = fontent._repr_png_()
- img = Image.open(BytesIO(png))
- assert img.width > 0
- assert img.height > 0
- html = fontent._repr_html_()
- assert html.startswith("<img src=\"data:image/png;base64")
- def test_fontentry_dataclass_invalid_path():
- with pytest.raises(FileNotFoundError):
- fontent = FontEntry(fname='/random', name='font-name')
- fontent._repr_html_()
- @pytest.mark.skipif(sys.platform == 'win32', reason='Linux or OS only')
- def test_get_font_names():
- paths_mpl = [cbook._get_data_path('fonts', subdir) for subdir in ['ttf']]
- fonts_mpl = findSystemFonts(paths_mpl, fontext='ttf')
- fonts_system = findSystemFonts(fontext='ttf')
- ttf_fonts = []
- for path in fonts_mpl + fonts_system:
- try:
- font = ft2font.FT2Font(path)
- prop = ttfFontProperty(font)
- ttf_fonts.append(prop.name)
- except Exception:
- pass
- available_fonts = sorted(list(set(ttf_fonts)))
- mpl_font_names = sorted(fontManager.get_font_names())
- assert set(available_fonts) == set(mpl_font_names)
- assert len(available_fonts) == len(mpl_font_names)
- assert available_fonts == mpl_font_names
- def test_donot_cache_tracebacks():
- class SomeObject:
- pass
- def inner():
- x = SomeObject()
- fig = mfigure.Figure()
- ax = fig.subplots()
- fig.text(.5, .5, 'aardvark', family='doesnotexist')
- with BytesIO() as out:
- with warnings.catch_warnings():
- warnings.filterwarnings('ignore')
- fig.savefig(out, format='raw')
- inner()
- for obj in gc.get_objects():
- if isinstance(obj, SomeObject):
- pytest.fail("object from inner stack still alive")
- def test_fontproperties_init_deprecation():
- """
- Test the deprecated API of FontProperties.__init__.
- The deprecation does not change behavior, it only adds a deprecation warning
- via a decorator. Therefore, the purpose of this test is limited to check
- which calls do and do not issue deprecation warnings. Behavior is still
- tested via the existing regular tests.
- """
- with pytest.warns(mpl.MatplotlibDeprecationWarning):
- # multiple positional arguments
- FontProperties("Times", "italic")
- with pytest.warns(mpl.MatplotlibDeprecationWarning):
- # Mixed positional and keyword arguments
- FontProperties("Times", size=10)
- with pytest.warns(mpl.MatplotlibDeprecationWarning):
- # passing a family list positionally
- FontProperties(["Times"])
- # still accepted:
- FontProperties(family="Times", style="italic")
- FontProperties(family="Times")
- FontProperties("Times") # works as pattern and family
- FontProperties("serif-24:style=oblique:weight=bold") # pattern
- # also still accepted:
- # passing as pattern via family kwarg was not covered by the docs but
- # historically worked. This is left unchanged for now.
- # AFAICT, we cannot detect this: We can determine whether a string
- # works as pattern, but that doesn't help, because there are strings
- # that are both pattern and family. We would need to identify, whether
- # a string is *not* a valid family.
- # Since this case is not covered by docs, I've refrained from jumping
- # extra hoops to detect this possible API misuse.
- FontProperties(family="serif-24:style=oblique:weight=bold")
|