Coverage for /usr/lib/python3/dist-packages/matplotlib/font_manager.py: 24%
596 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1"""
2A module for finding, managing, and using fonts across platforms.
4This module provides a single `FontManager` instance, ``fontManager``, that can
5be shared across backends and platforms. The `findfont`
6function returns the best TrueType (TTF) font file in the local or
7system font path that matches the specified `FontProperties`
8instance. The `FontManager` also handles Adobe Font Metrics
9(AFM) font files for use by the PostScript backend.
11The design is based on the `W3C Cascading Style Sheet, Level 1 (CSS1)
12font specification <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_.
13Future versions may implement the Level 2 or 2.1 specifications.
14"""
16# KNOWN ISSUES
17#
18# - documentation
19# - font variant is untested
20# - font stretch is incomplete
21# - font size is incomplete
22# - default font algorithm needs improvement and testing
23# - setWeights function needs improvement
24# - 'light' is an invalid weight value, remove it.
26from base64 import b64encode
27import copy
28import dataclasses
29from functools import lru_cache
30from io import BytesIO
31import json
32import logging
33from numbers import Number
34import os
35from pathlib import Path
36import re
37import subprocess
38import sys
39import threading
41import matplotlib as mpl
42from matplotlib import _api, _afm, cbook, ft2font
43from matplotlib._fontconfig_pattern import (
44 parse_fontconfig_pattern, generate_fontconfig_pattern)
45from matplotlib.rcsetup import _validators
47_log = logging.getLogger(__name__)
49font_scalings = {
50 'xx-small': 0.579,
51 'x-small': 0.694,
52 'small': 0.833,
53 'medium': 1.0,
54 'large': 1.200,
55 'x-large': 1.440,
56 'xx-large': 1.728,
57 'larger': 1.2,
58 'smaller': 0.833,
59 None: 1.0,
60}
61stretch_dict = {
62 'ultra-condensed': 100,
63 'extra-condensed': 200,
64 'condensed': 300,
65 'semi-condensed': 400,
66 'normal': 500,
67 'semi-expanded': 600,
68 'semi-extended': 600,
69 'expanded': 700,
70 'extended': 700,
71 'extra-expanded': 800,
72 'extra-extended': 800,
73 'ultra-expanded': 900,
74 'ultra-extended': 900,
75}
76weight_dict = {
77 'ultralight': 100,
78 'light': 200,
79 'normal': 400,
80 'regular': 400,
81 'book': 400,
82 'medium': 500,
83 'roman': 500,
84 'semibold': 600,
85 'demibold': 600,
86 'demi': 600,
87 'bold': 700,
88 'heavy': 800,
89 'extra bold': 800,
90 'black': 900,
91}
92_weight_regexes = [
93 # From fontconfig's FcFreeTypeQueryFaceInternal; not the same as
94 # weight_dict!
95 ("thin", 100),
96 ("extralight", 200),
97 ("ultralight", 200),
98 ("demilight", 350),
99 ("semilight", 350),
100 ("light", 300), # Needs to come *after* demi/semilight!
101 ("book", 380),
102 ("regular", 400),
103 ("normal", 400),
104 ("medium", 500),
105 ("demibold", 600),
106 ("demi", 600),
107 ("semibold", 600),
108 ("extrabold", 800),
109 ("superbold", 800),
110 ("ultrabold", 800),
111 ("bold", 700), # Needs to come *after* extra/super/ultrabold!
112 ("ultrablack", 1000),
113 ("superblack", 1000),
114 ("extrablack", 1000),
115 (r"\bultra", 1000),
116 ("black", 900), # Needs to come *after* ultra/super/extrablack!
117 ("heavy", 900),
118]
119font_family_aliases = {
120 'serif',
121 'sans-serif',
122 'sans serif',
123 'cursive',
124 'fantasy',
125 'monospace',
126 'sans',
127}
130# OS Font paths
131try:
132 _HOME = Path.home()
133except Exception: # Exceptions thrown by home() are not specified...
134 _HOME = Path(os.devnull) # Just an arbitrary path with no children.
135MSFolders = \
136 r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
137MSFontDirectories = [
138 r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts',
139 r'SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts']
140MSUserFontDirectories = [
141 str(_HOME / 'AppData/Local/Microsoft/Windows/Fonts'),
142 str(_HOME / 'AppData/Roaming/Microsoft/Windows/Fonts'),
143]
144X11FontDirectories = [
145 # an old standard installation point
146 "/usr/X11R6/lib/X11/fonts/TTF/",
147 "/usr/X11/lib/X11/fonts",
148 # here is the new standard location for fonts
149 "/usr/share/fonts/",
150 # documented as a good place to install new fonts
151 "/usr/local/share/fonts/",
152 # common application, not really useful
153 "/usr/lib/openoffice/share/fonts/truetype/",
154 # user fonts
155 str((Path(os.environ.get('XDG_DATA_HOME') or _HOME / ".local/share"))
156 / "fonts"),
157 str(_HOME / ".fonts"),
158]
159OSXFontDirectories = [
160 "/Library/Fonts/",
161 "/Network/Library/Fonts/",
162 "/System/Library/Fonts/",
163 # fonts installed via MacPorts
164 "/opt/local/share/fonts",
165 # user fonts
166 str(_HOME / "Library/Fonts"),
167]
170def get_fontext_synonyms(fontext):
171 """
172 Return a list of file extensions that are synonyms for
173 the given file extension *fileext*.
174 """
175 return {
176 'afm': ['afm'],
177 'otf': ['otf', 'ttc', 'ttf'],
178 'ttc': ['otf', 'ttc', 'ttf'],
179 'ttf': ['otf', 'ttc', 'ttf'],
180 }[fontext]
183def list_fonts(directory, extensions):
184 """
185 Return a list of all fonts matching any of the extensions, found
186 recursively under the directory.
187 """
188 extensions = ["." + ext for ext in extensions]
189 if sys.platform == 'win32' and directory == win32FontDirectory():
190 return [os.path.join(directory, filename)
191 for filename in os.listdir(directory)
192 if os.path.isfile(filename)]
193 else:
194 return [os.path.join(dirpath, filename)
195 # os.walk ignores access errors, unlike Path.glob.
196 for dirpath, _, filenames in os.walk(directory)
197 for filename in filenames
198 if Path(filename).suffix.lower() in extensions]
201def win32FontDirectory():
202 r"""
203 Return the user-specified font directory for Win32. This is
204 looked up from the registry key ::
206 \\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\Fonts
208 If the key is not found, ``%WINDIR%\Fonts`` will be returned.
209 """
210 import winreg
211 try:
212 with winreg.OpenKey(winreg.HKEY_CURRENT_USER, MSFolders) as user:
213 return winreg.QueryValueEx(user, 'Fonts')[0]
214 except OSError:
215 return os.path.join(os.environ['WINDIR'], 'Fonts')
218def _win32RegistryFonts(reg_domain, base_dir):
219 r"""
220 Search for fonts in the Windows registry.
222 Parameters
223 ----------
224 reg_domain : int
225 The top level registry domain (e.g. HKEY_LOCAL_MACHINE).
227 base_dir : str
228 The path to the folder where the font files are usually located (e.g.
229 C:\Windows\Fonts). If only the filename of the font is stored in the
230 registry, the absolute path is built relative to this base directory.
232 Returns
233 -------
234 `set`
235 `pathlib.Path` objects with the absolute path to the font files found.
237 """
238 import winreg
239 items = set()
241 for reg_path in MSFontDirectories:
242 try:
243 with winreg.OpenKey(reg_domain, reg_path) as local:
244 for j in range(winreg.QueryInfoKey(local)[1]):
245 # value may contain the filename of the font or its
246 # absolute path.
247 key, value, tp = winreg.EnumValue(local, j)
248 if not isinstance(value, str):
249 continue
250 try:
251 # If value contains already an absolute path, then it
252 # is not changed further.
253 path = Path(base_dir, value).resolve()
254 except RuntimeError:
255 # Don't fail with invalid entries.
256 continue
258 items.add(path)
259 except (OSError, MemoryError):
260 continue
262 return items
265# Also remove _win32RegistryFonts when this is removed.
266@_api.deprecated("3.5")
267def win32InstalledFonts(directory=None, fontext='ttf'):
268 """
269 Search for fonts in the specified font directory, or use the
270 system directories if none given. Additionally, it is searched for user
271 fonts installed. A list of TrueType font filenames are returned by default,
272 or AFM fonts if *fontext* == 'afm'.
273 """
274 import winreg
276 if directory is None:
277 directory = win32FontDirectory()
279 fontext = ['.' + ext for ext in get_fontext_synonyms(fontext)]
281 items = set()
283 # System fonts
284 items.update(_win32RegistryFonts(winreg.HKEY_LOCAL_MACHINE, directory))
286 # User fonts
287 for userdir in MSUserFontDirectories:
288 items.update(_win32RegistryFonts(winreg.HKEY_CURRENT_USER, userdir))
290 # Keep only paths with matching file extension.
291 return [str(path) for path in items if path.suffix.lower() in fontext]
294def _get_win32_installed_fonts():
295 """List the font paths known to the Windows registry."""
296 import winreg
297 items = set()
298 # Search and resolve fonts listed in the registry.
299 for domain, base_dirs in [
300 (winreg.HKEY_LOCAL_MACHINE, [win32FontDirectory()]), # System.
301 (winreg.HKEY_CURRENT_USER, MSUserFontDirectories), # User.
302 ]:
303 for base_dir in base_dirs:
304 for reg_path in MSFontDirectories:
305 try:
306 with winreg.OpenKey(domain, reg_path) as local:
307 for j in range(winreg.QueryInfoKey(local)[1]):
308 # value may contain the filename of the font or its
309 # absolute path.
310 key, value, tp = winreg.EnumValue(local, j)
311 if not isinstance(value, str):
312 continue
313 try:
314 # If value contains already an absolute path,
315 # then it is not changed further.
316 path = Path(base_dir, value).resolve()
317 except RuntimeError:
318 # Don't fail with invalid entries.
319 continue
320 items.add(path)
321 except (OSError, MemoryError):
322 continue
323 return items
326@lru_cache()
327def _get_fontconfig_fonts():
328 """Cache and list the font paths known to ``fc-list``."""
329 try:
330 if b'--format' not in subprocess.check_output(['fc-list', '--help']):
331 _log.warning( # fontconfig 2.7 implemented --format.
332 'Matplotlib needs fontconfig>=2.7 to query system fonts.')
333 return []
334 out = subprocess.check_output(['fc-list', '--format=%{file}\\n'])
335 except (OSError, subprocess.CalledProcessError):
336 return []
337 return [Path(os.fsdecode(fname)) for fname in out.split(b'\n')]
340@_api.deprecated("3.5")
341def get_fontconfig_fonts(fontext='ttf'):
342 """List font filenames known to ``fc-list`` having the given extension."""
343 fontext = ['.' + ext for ext in get_fontext_synonyms(fontext)]
344 return [str(path) for path in _get_fontconfig_fonts()
345 if path.suffix.lower() in fontext]
348def findSystemFonts(fontpaths=None, fontext='ttf'):
349 """
350 Search for fonts in the specified font paths. If no paths are
351 given, will use a standard set of system paths, as well as the
352 list of fonts tracked by fontconfig if fontconfig is installed and
353 available. A list of TrueType fonts are returned by default with
354 AFM fonts as an option.
355 """
356 fontfiles = set()
357 fontexts = get_fontext_synonyms(fontext)
359 if fontpaths is None:
360 if sys.platform == 'win32':
361 installed_fonts = _get_win32_installed_fonts()
362 fontpaths = MSUserFontDirectories + [win32FontDirectory()]
363 else:
364 installed_fonts = _get_fontconfig_fonts()
365 if sys.platform == 'darwin':
366 fontpaths = [*X11FontDirectories, *OSXFontDirectories]
367 else:
368 fontpaths = X11FontDirectories
369 fontfiles.update(str(path) for path in installed_fonts
370 if path.suffix.lower()[1:] in fontexts)
372 elif isinstance(fontpaths, str):
373 fontpaths = [fontpaths]
375 for path in fontpaths:
376 fontfiles.update(map(os.path.abspath, list_fonts(path, fontexts)))
378 return [fname for fname in fontfiles if os.path.exists(fname)]
381def _fontentry_helper_repr_png(fontent):
382 from matplotlib.figure import Figure # Circular import.
383 fig = Figure()
384 font_path = Path(fontent.fname) if fontent.fname != '' else None
385 fig.text(0, 0, fontent.name, font=font_path)
386 with BytesIO() as buf:
387 fig.savefig(buf, bbox_inches='tight', transparent=True)
388 return buf.getvalue()
391def _fontentry_helper_repr_html(fontent):
392 png_stream = _fontentry_helper_repr_png(fontent)
393 png_b64 = b64encode(png_stream).decode()
394 return f"<img src=\"data:image/png;base64, {png_b64}\" />"
397FontEntry = dataclasses.make_dataclass(
398 'FontEntry', [
399 ('fname', str, dataclasses.field(default='')),
400 ('name', str, dataclasses.field(default='')),
401 ('style', str, dataclasses.field(default='normal')),
402 ('variant', str, dataclasses.field(default='normal')),
403 ('weight', str, dataclasses.field(default='normal')),
404 ('stretch', str, dataclasses.field(default='normal')),
405 ('size', str, dataclasses.field(default='medium')),
406 ],
407 namespace={
408 '__doc__': """
409 A class for storing Font properties.
411 It is used when populating the font lookup dictionary.
412 """,
413 '_repr_html_': lambda self: _fontentry_helper_repr_html(self),
414 '_repr_png_': lambda self: _fontentry_helper_repr_png(self),
415 }
416)
419def ttfFontProperty(font):
420 """
421 Extract information from a TrueType font file.
423 Parameters
424 ----------
425 font : `.FT2Font`
426 The TrueType font file from which information will be extracted.
428 Returns
429 -------
430 `FontEntry`
431 The extracted font properties.
433 """
434 name = font.family_name
436 # Styles are: italic, oblique, and normal (default)
438 sfnt = font.get_sfnt()
439 mac_key = (1, # platform: macintosh
440 0, # id: roman
441 0) # langid: english
442 ms_key = (3, # platform: microsoft
443 1, # id: unicode_cs
444 0x0409) # langid: english_united_states
446 # These tables are actually mac_roman-encoded, but mac_roman support may be
447 # missing in some alternative Python implementations and we are only going
448 # to look for ASCII substrings, where any ASCII-compatible encoding works
449 # - or big-endian UTF-16, since important Microsoft fonts use that.
450 sfnt2 = (sfnt.get((*mac_key, 2), b'').decode('latin-1').lower() or
451 sfnt.get((*ms_key, 2), b'').decode('utf_16_be').lower())
452 sfnt4 = (sfnt.get((*mac_key, 4), b'').decode('latin-1').lower() or
453 sfnt.get((*ms_key, 4), b'').decode('utf_16_be').lower())
455 if sfnt4.find('oblique') >= 0:
456 style = 'oblique'
457 elif sfnt4.find('italic') >= 0:
458 style = 'italic'
459 elif sfnt2.find('regular') >= 0:
460 style = 'normal'
461 elif font.style_flags & ft2font.ITALIC:
462 style = 'italic'
463 else:
464 style = 'normal'
466 # Variants are: small-caps and normal (default)
468 # !!!! Untested
469 if name.lower() in ['capitals', 'small-caps']:
470 variant = 'small-caps'
471 else:
472 variant = 'normal'
474 # The weight-guessing algorithm is directly translated from fontconfig
475 # 2.13.1's FcFreeTypeQueryFaceInternal (fcfreetype.c).
476 wws_subfamily = 22
477 typographic_subfamily = 16
478 font_subfamily = 2
479 styles = [
480 sfnt.get((*mac_key, wws_subfamily), b'').decode('latin-1'),
481 sfnt.get((*mac_key, typographic_subfamily), b'').decode('latin-1'),
482 sfnt.get((*mac_key, font_subfamily), b'').decode('latin-1'),
483 sfnt.get((*ms_key, wws_subfamily), b'').decode('utf-16-be'),
484 sfnt.get((*ms_key, typographic_subfamily), b'').decode('utf-16-be'),
485 sfnt.get((*ms_key, font_subfamily), b'').decode('utf-16-be'),
486 ]
487 styles = [*filter(None, styles)] or [font.style_name]
489 def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal.
490 # OS/2 table weight.
491 os2 = font.get_sfnt_table("OS/2")
492 if os2 and os2["version"] != 0xffff:
493 return os2["usWeightClass"]
494 # PostScript font info weight.
495 try:
496 ps_font_info_weight = (
497 font.get_ps_font_info()["weight"].replace(" ", "") or "")
498 except ValueError:
499 pass
500 else:
501 for regex, weight in _weight_regexes:
502 if re.fullmatch(regex, ps_font_info_weight, re.I):
503 return weight
504 # Style name weight.
505 for style in styles:
506 style = style.replace(" ", "")
507 for regex, weight in _weight_regexes:
508 if re.search(regex, style, re.I):
509 return weight
510 if font.style_flags & ft2font.BOLD:
511 return 700 # "bold"
512 return 500 # "medium", not "regular"!
514 weight = int(get_weight())
516 # Stretch can be absolute and relative
517 # Absolute stretches are: ultra-condensed, extra-condensed, condensed,
518 # semi-condensed, normal, semi-expanded, expanded, extra-expanded,
519 # and ultra-expanded.
520 # Relative stretches are: wider, narrower
521 # Child value is: inherit
523 if any(word in sfnt4 for word in ['narrow', 'condensed', 'cond']):
524 stretch = 'condensed'
525 elif 'demi cond' in sfnt4:
526 stretch = 'semi-condensed'
527 elif any(word in sfnt4 for word in ['wide', 'expanded', 'extended']):
528 stretch = 'expanded'
529 else:
530 stretch = 'normal'
532 # Sizes can be absolute and relative.
533 # Absolute sizes are: xx-small, x-small, small, medium, large, x-large,
534 # and xx-large.
535 # Relative sizes are: larger, smaller
536 # Length value is an absolute font size, e.g., 12pt
537 # Percentage values are in 'em's. Most robust specification.
539 if not font.scalable:
540 raise NotImplementedError("Non-scalable fonts are not supported")
541 size = 'scalable'
543 return FontEntry(font.fname, name, style, variant, weight, stretch, size)
546def afmFontProperty(fontpath, font):
547 """
548 Extract information from an AFM font file.
550 Parameters
551 ----------
552 font : AFM
553 The AFM font file from which information will be extracted.
555 Returns
556 -------
557 `FontEntry`
558 The extracted font properties.
559 """
561 name = font.get_familyname()
562 fontname = font.get_fontname().lower()
564 # Styles are: italic, oblique, and normal (default)
566 if font.get_angle() != 0 or 'italic' in name.lower():
567 style = 'italic'
568 elif 'oblique' in name.lower():
569 style = 'oblique'
570 else:
571 style = 'normal'
573 # Variants are: small-caps and normal (default)
575 # !!!! Untested
576 if name.lower() in ['capitals', 'small-caps']:
577 variant = 'small-caps'
578 else:
579 variant = 'normal'
581 weight = font.get_weight().lower()
582 if weight not in weight_dict:
583 weight = 'normal'
585 # Stretch can be absolute and relative
586 # Absolute stretches are: ultra-condensed, extra-condensed, condensed,
587 # semi-condensed, normal, semi-expanded, expanded, extra-expanded,
588 # and ultra-expanded.
589 # Relative stretches are: wider, narrower
590 # Child value is: inherit
591 if 'demi cond' in fontname:
592 stretch = 'semi-condensed'
593 elif any(word in fontname for word in ['narrow', 'cond']):
594 stretch = 'condensed'
595 elif any(word in fontname for word in ['wide', 'expanded', 'extended']):
596 stretch = 'expanded'
597 else:
598 stretch = 'normal'
600 # Sizes can be absolute and relative.
601 # Absolute sizes are: xx-small, x-small, small, medium, large, x-large,
602 # and xx-large.
603 # Relative sizes are: larger, smaller
604 # Length value is an absolute font size, e.g., 12pt
605 # Percentage values are in 'em's. Most robust specification.
607 # All AFM fonts are apparently scalable.
609 size = 'scalable'
611 return FontEntry(fontpath, name, style, variant, weight, stretch, size)
614class FontProperties:
615 """
616 A class for storing and manipulating font properties.
618 The font properties are the six properties described in the
619 `W3C Cascading Style Sheet, Level 1
620 <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ font
621 specification and *math_fontfamily* for math fonts:
623 - family: A list of font names in decreasing order of priority.
624 The items may include a generic font family name, either 'sans-serif',
625 'serif', 'cursive', 'fantasy', or 'monospace'. In that case, the actual
626 font to be used will be looked up from the associated rcParam during the
627 search process in `.findfont`. Default: :rc:`font.family`
629 - style: Either 'normal', 'italic' or 'oblique'.
630 Default: :rc:`font.style`
632 - variant: Either 'normal' or 'small-caps'.
633 Default: :rc:`font.variant`
635 - stretch: A numeric value in the range 0-1000 or one of
636 'ultra-condensed', 'extra-condensed', 'condensed',
637 'semi-condensed', 'normal', 'semi-expanded', 'expanded',
638 'extra-expanded' or 'ultra-expanded'. Default: :rc:`font.stretch`
640 - weight: A numeric value in the range 0-1000 or one of
641 'ultralight', 'light', 'normal', 'regular', 'book', 'medium',
642 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy',
643 'extra bold', 'black'. Default: :rc:`font.weight`
645 - size: Either a relative value of 'xx-small', 'x-small',
646 'small', 'medium', 'large', 'x-large', 'xx-large' or an
647 absolute font size, e.g., 10. Default: :rc:`font.size`
649 - math_fontfamily: The family of fonts used to render math text.
650 Supported values are: 'dejavusans', 'dejavuserif', 'cm',
651 'stix', 'stixsans' and 'custom'. Default: :rc:`mathtext.fontset`
653 Alternatively, a font may be specified using the absolute path to a font
654 file, by using the *fname* kwarg. However, in this case, it is typically
655 simpler to just pass the path (as a `pathlib.Path`, not a `str`) to the
656 *font* kwarg of the `.Text` object.
658 The preferred usage of font sizes is to use the relative values,
659 e.g., 'large', instead of absolute font sizes, e.g., 12. This
660 approach allows all text sizes to be made larger or smaller based
661 on the font manager's default font size.
663 This class will also accept a fontconfig_ pattern_, if it is the only
664 argument provided. This support does not depend on fontconfig; we are
665 merely borrowing its pattern syntax for use here.
667 .. _fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/
668 .. _pattern:
669 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
671 Note that Matplotlib's internal font manager and fontconfig use a
672 different algorithm to lookup fonts, so the results of the same pattern
673 may be different in Matplotlib than in other applications that use
674 fontconfig.
675 """
677 def __init__(self, family=None, style=None, variant=None, weight=None,
678 stretch=None, size=None,
679 fname=None, # if set, it's a hardcoded filename to use
680 math_fontfamily=None):
681 self.set_family(family)
682 self.set_style(style)
683 self.set_variant(variant)
684 self.set_weight(weight)
685 self.set_stretch(stretch)
686 self.set_file(fname)
687 self.set_size(size)
688 self.set_math_fontfamily(math_fontfamily)
689 # Treat family as a fontconfig pattern if it is the only parameter
690 # provided. Even in that case, call the other setters first to set
691 # attributes not specified by the pattern to the rcParams defaults.
692 if (isinstance(family, str)
693 and style is None and variant is None and weight is None
694 and stretch is None and size is None and fname is None):
695 self.set_fontconfig_pattern(family)
697 @classmethod
698 def _from_any(cls, arg):
699 """
700 Generic constructor which can build a `.FontProperties` from any of the
701 following:
703 - a `.FontProperties`: it is passed through as is;
704 - `None`: a `.FontProperties` using rc values is used;
705 - an `os.PathLike`: it is used as path to the font file;
706 - a `str`: it is parsed as a fontconfig pattern;
707 - a `dict`: it is passed as ``**kwargs`` to `.FontProperties`.
708 """
709 if isinstance(arg, cls):
710 return arg
711 elif arg is None:
712 return cls()
713 elif isinstance(arg, os.PathLike):
714 return cls(fname=arg)
715 elif isinstance(arg, str):
716 return cls(arg)
717 else:
718 return cls(**arg)
720 def __hash__(self):
721 l = (tuple(self.get_family()),
722 self.get_slant(),
723 self.get_variant(),
724 self.get_weight(),
725 self.get_stretch(),
726 self.get_size(),
727 self.get_file(),
728 self.get_math_fontfamily())
729 return hash(l)
731 def __eq__(self, other):
732 return hash(self) == hash(other)
734 def __str__(self):
735 return self.get_fontconfig_pattern()
737 def get_family(self):
738 """
739 Return a list of individual font family names or generic family names.
741 The font families or generic font families (which will be resolved
742 from their respective rcParams when searching for a matching font) in
743 the order of preference.
744 """
745 return self._family
747 def get_name(self):
748 """
749 Return the name of the font that best matches the font properties.
750 """
751 return get_font(findfont(self)).family_name
753 def get_style(self):
754 """
755 Return the font style. Values are: 'normal', 'italic' or 'oblique'.
756 """
757 return self._slant
759 def get_variant(self):
760 """
761 Return the font variant. Values are: 'normal' or 'small-caps'.
762 """
763 return self._variant
765 def get_weight(self):
766 """
767 Set the font weight. Options are: A numeric value in the
768 range 0-1000 or one of 'light', 'normal', 'regular', 'book',
769 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold',
770 'heavy', 'extra bold', 'black'
771 """
772 return self._weight
774 def get_stretch(self):
775 """
776 Return the font stretch or width. Options are: 'ultra-condensed',
777 'extra-condensed', 'condensed', 'semi-condensed', 'normal',
778 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'.
779 """
780 return self._stretch
782 def get_size(self):
783 """
784 Return the font size.
785 """
786 return self._size
788 def get_file(self):
789 """
790 Return the filename of the associated font.
791 """
792 return self._file
794 def get_fontconfig_pattern(self):
795 """
796 Get a fontconfig_ pattern_ suitable for looking up the font as
797 specified with fontconfig's ``fc-match`` utility.
799 This support does not depend on fontconfig; we are merely borrowing its
800 pattern syntax for use here.
801 """
802 return generate_fontconfig_pattern(self)
804 def set_family(self, family):
805 """
806 Change the font family. Can be either an alias (generic name
807 is CSS parlance), such as: 'serif', 'sans-serif', 'cursive',
808 'fantasy', or 'monospace', a real font name or a list of real
809 font names. Real font names are not supported when
810 :rc:`text.usetex` is `True`. Default: :rc:`font.family`
811 """
812 if family is None:
813 family = mpl.rcParams['font.family']
814 if isinstance(family, str):
815 family = [family]
816 self._family = family
818 def set_style(self, style):
819 """
820 Set the font style.
822 Parameters
823 ----------
824 style : {'normal', 'italic', 'oblique'}, default: :rc:`font.style`
825 """
826 if style is None:
827 style = mpl.rcParams['font.style']
828 _api.check_in_list(['normal', 'italic', 'oblique'], style=style)
829 self._slant = style
831 def set_variant(self, variant):
832 """
833 Set the font variant.
835 Parameters
836 ----------
837 variant : {'normal', 'small-caps'}, default: :rc:`font.variant`
838 """
839 if variant is None:
840 variant = mpl.rcParams['font.variant']
841 _api.check_in_list(['normal', 'small-caps'], variant=variant)
842 self._variant = variant
844 def set_weight(self, weight):
845 """
846 Set the font weight.
848 Parameters
849 ----------
850 weight : int or {'ultralight', 'light', 'normal', 'regular', 'book', \
851'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', \
852'extra bold', 'black'}, default: :rc:`font.weight`
853 If int, must be in the range 0-1000.
854 """
855 if weight is None:
856 weight = mpl.rcParams['font.weight']
857 if weight in weight_dict:
858 self._weight = weight
859 return
860 try:
861 weight = int(weight)
862 except ValueError:
863 pass
864 else:
865 if 0 <= weight <= 1000:
866 self._weight = weight
867 return
868 raise ValueError(f"{weight=} is invalid")
870 def set_stretch(self, stretch):
871 """
872 Set the font stretch or width.
874 Parameters
875 ----------
876 stretch : int or {'ultra-condensed', 'extra-condensed', 'condensed', \
877'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', \
878'ultra-expanded'}, default: :rc:`font.stretch`
879 If int, must be in the range 0-1000.
880 """
881 if stretch is None:
882 stretch = mpl.rcParams['font.stretch']
883 if stretch in stretch_dict:
884 self._stretch = stretch
885 return
886 try:
887 stretch = int(stretch)
888 except ValueError:
889 pass
890 else:
891 if 0 <= stretch <= 1000:
892 self._stretch = stretch
893 return
894 raise ValueError(f"{stretch=} is invalid")
896 def set_size(self, size):
897 """
898 Set the font size.
900 Parameters
901 ----------
902 size : float or {'xx-small', 'x-small', 'small', 'medium', \
903'large', 'x-large', 'xx-large'}, default: :rc:`font.size`
904 If a float, the font size in points. The string values denote
905 sizes relative to the default font size.
906 """
907 if size is None:
908 size = mpl.rcParams['font.size']
909 try:
910 size = float(size)
911 except ValueError:
912 try:
913 scale = font_scalings[size]
914 except KeyError as err:
915 raise ValueError(
916 "Size is invalid. Valid font size are "
917 + ", ".join(map(str, font_scalings))) from err
918 else:
919 size = scale * FontManager.get_default_size()
920 if size < 1.0:
921 _log.info('Fontsize %1.2f < 1.0 pt not allowed by FreeType. '
922 'Setting fontsize = 1 pt', size)
923 size = 1.0
924 self._size = size
926 def set_file(self, file):
927 """
928 Set the filename of the fontfile to use. In this case, all
929 other properties will be ignored.
930 """
931 self._file = os.fspath(file) if file is not None else None
933 def set_fontconfig_pattern(self, pattern):
934 """
935 Set the properties by parsing a fontconfig_ *pattern*.
937 This support does not depend on fontconfig; we are merely borrowing its
938 pattern syntax for use here.
939 """
940 for key, val in parse_fontconfig_pattern(pattern).items():
941 if type(val) == list:
942 getattr(self, "set_" + key)(val[0])
943 else:
944 getattr(self, "set_" + key)(val)
946 def get_math_fontfamily(self):
947 """
948 Return the name of the font family used for math text.
950 The default font is :rc:`mathtext.fontset`.
951 """
952 return self._math_fontfamily
954 def set_math_fontfamily(self, fontfamily):
955 """
956 Set the font family for text in math mode.
958 If not set explicitly, :rc:`mathtext.fontset` will be used.
960 Parameters
961 ----------
962 fontfamily : str
963 The name of the font family.
965 Available font families are defined in the
966 matplotlibrc.template file
967 :ref:`here <customizing-with-matplotlibrc-files>`
969 See Also
970 --------
971 .text.Text.get_math_fontfamily
972 """
973 if fontfamily is None:
974 fontfamily = mpl.rcParams['mathtext.fontset']
975 else:
976 valid_fonts = _validators['mathtext.fontset'].valid.values()
977 # _check_in_list() Validates the parameter math_fontfamily as
978 # if it were passed to rcParams['mathtext.fontset']
979 _api.check_in_list(valid_fonts, math_fontfamily=fontfamily)
980 self._math_fontfamily = fontfamily
982 def copy(self):
983 """Return a copy of self."""
984 return copy.copy(self)
986 # Aliases
987 set_name = set_family
988 get_slant = get_style
989 set_slant = set_style
990 get_size_in_points = get_size
993class _JSONEncoder(json.JSONEncoder):
994 def default(self, o):
995 if isinstance(o, FontManager):
996 return dict(o.__dict__, __class__='FontManager')
997 elif isinstance(o, FontEntry):
998 d = dict(o.__dict__, __class__='FontEntry')
999 try:
1000 # Cache paths of fonts shipped with Matplotlib relative to the
1001 # Matplotlib data path, which helps in the presence of venvs.
1002 d["fname"] = str(
1003 Path(d["fname"]).relative_to(mpl.get_data_path()))
1004 except ValueError:
1005 pass
1006 return d
1007 else:
1008 return super().default(o)
1011def _json_decode(o):
1012 cls = o.pop('__class__', None)
1013 if cls is None:
1014 return o
1015 elif cls == 'FontManager':
1016 r = FontManager.__new__(FontManager)
1017 r.__dict__.update(o)
1018 return r
1019 elif cls == 'FontEntry':
1020 r = FontEntry.__new__(FontEntry)
1021 r.__dict__.update(o)
1022 if not os.path.isabs(r.fname):
1023 r.fname = os.path.join(mpl.get_data_path(), r.fname)
1024 return r
1025 else:
1026 raise ValueError("Don't know how to deserialize __class__=%s" % cls)
1029def json_dump(data, filename):
1030 """
1031 Dump `FontManager` *data* as JSON to the file named *filename*.
1033 See Also
1034 --------
1035 json_load
1037 Notes
1038 -----
1039 File paths that are children of the Matplotlib data path (typically, fonts
1040 shipped with Matplotlib) are stored relative to that data path (to remain
1041 valid across virtualenvs).
1043 This function temporarily locks the output file to prevent multiple
1044 processes from overwriting one another's output.
1045 """
1046 with cbook._lock_path(filename), open(filename, 'w') as fh:
1047 try:
1048 json.dump(data, fh, cls=_JSONEncoder, indent=2)
1049 except OSError as e:
1050 _log.warning('Could not save font_manager cache {}'.format(e))
1053def json_load(filename):
1054 """
1055 Load a `FontManager` from the JSON file named *filename*.
1057 See Also
1058 --------
1059 json_dump
1060 """
1061 with open(filename) as fh:
1062 return json.load(fh, object_hook=_json_decode)
1065class FontManager:
1066 """
1067 On import, the `FontManager` singleton instance creates a list of ttf and
1068 afm fonts and caches their `FontProperties`. The `FontManager.findfont`
1069 method does a nearest neighbor search to find the font that most closely
1070 matches the specification. If no good enough match is found, the default
1071 font is returned.
1072 """
1073 # Increment this version number whenever the font cache data
1074 # format or behavior has changed and requires an existing font
1075 # cache files to be rebuilt.
1076 __version__ = 330
1078 def __init__(self, size=None, weight='normal'):
1079 self._version = self.__version__
1081 self.__default_weight = weight
1082 self.default_size = size
1084 # Create list of font paths.
1085 paths = [cbook._get_data_path('fonts', subdir)
1086 for subdir in ['ttf', 'afm', 'pdfcorefonts']]
1087 _log.debug('font search path %s', str(paths))
1089 self.defaultFamily = {
1090 'ttf': 'DejaVu Sans',
1091 'afm': 'Helvetica'}
1093 self.afmlist = []
1094 self.ttflist = []
1096 # Delay the warning by 5s.
1097 timer = threading.Timer(5, lambda: _log.warning(
1098 'Matplotlib is building the font cache; this may take a moment.'))
1099 timer.start()
1100 try:
1101 for fontext in ["afm", "ttf"]:
1102 for path in [*findSystemFonts(paths, fontext=fontext),
1103 *findSystemFonts(fontext=fontext)]:
1104 try:
1105 self.addfont(path)
1106 except OSError as exc:
1107 _log.info("Failed to open font file %s: %s", path, exc)
1108 except Exception as exc:
1109 _log.info("Failed to extract font properties from %s: "
1110 "%s", path, exc)
1111 finally:
1112 timer.cancel()
1114 def addfont(self, path):
1115 """
1116 Cache the properties of the font at *path* to make it available to the
1117 `FontManager`. The type of font is inferred from the path suffix.
1119 Parameters
1120 ----------
1121 path : str or path-like
1122 """
1123 # Convert to string in case of a path as
1124 # afmFontProperty and FT2Font expect this
1125 path = os.fsdecode(path)
1126 if Path(path).suffix.lower() == ".afm":
1127 with open(path, "rb") as fh:
1128 font = _afm.AFM(fh)
1129 prop = afmFontProperty(path, font)
1130 self.afmlist.append(prop)
1131 else:
1132 font = ft2font.FT2Font(path)
1133 prop = ttfFontProperty(font)
1134 self.ttflist.append(prop)
1135 self._findfont_cached.cache_clear()
1137 @property
1138 def defaultFont(self):
1139 # Lazily evaluated (findfont then caches the result) to avoid including
1140 # the venv path in the json serialization.
1141 return {ext: self.findfont(family, fontext=ext)
1142 for ext, family in self.defaultFamily.items()}
1144 def get_default_weight(self):
1145 """
1146 Return the default font weight.
1147 """
1148 return self.__default_weight
1150 @staticmethod
1151 def get_default_size():
1152 """
1153 Return the default font size.
1154 """
1155 return mpl.rcParams['font.size']
1157 def set_default_weight(self, weight):
1158 """
1159 Set the default font weight. The initial value is 'normal'.
1160 """
1161 self.__default_weight = weight
1163 @staticmethod
1164 def _expand_aliases(family):
1165 if family in ('sans', 'sans serif'):
1166 family = 'sans-serif'
1167 return mpl.rcParams['font.' + family]
1169 # Each of the scoring functions below should return a value between
1170 # 0.0 (perfect match) and 1.0 (terrible match)
1171 def score_family(self, families, family2):
1172 """
1173 Return a match score between the list of font families in
1174 *families* and the font family name *family2*.
1176 An exact match at the head of the list returns 0.0.
1178 A match further down the list will return between 0 and 1.
1180 No match will return 1.0.
1181 """
1182 if not isinstance(families, (list, tuple)):
1183 families = [families]
1184 elif len(families) == 0:
1185 return 1.0
1186 family2 = family2.lower()
1187 step = 1 / len(families)
1188 for i, family1 in enumerate(families):
1189 family1 = family1.lower()
1190 if family1 in font_family_aliases:
1191 options = [*map(str.lower, self._expand_aliases(family1))]
1192 if family2 in options:
1193 idx = options.index(family2)
1194 return (i + (idx / len(options))) * step
1195 elif family1 == family2:
1196 # The score should be weighted by where in the
1197 # list the font was found.
1198 return i * step
1199 return 1.0
1201 def score_style(self, style1, style2):
1202 """
1203 Return a match score between *style1* and *style2*.
1205 An exact match returns 0.0.
1207 A match between 'italic' and 'oblique' returns 0.1.
1209 No match returns 1.0.
1210 """
1211 if style1 == style2:
1212 return 0.0
1213 elif (style1 in ('italic', 'oblique')
1214 and style2 in ('italic', 'oblique')):
1215 return 0.1
1216 return 1.0
1218 def score_variant(self, variant1, variant2):
1219 """
1220 Return a match score between *variant1* and *variant2*.
1222 An exact match returns 0.0, otherwise 1.0.
1223 """
1224 if variant1 == variant2:
1225 return 0.0
1226 else:
1227 return 1.0
1229 def score_stretch(self, stretch1, stretch2):
1230 """
1231 Return a match score between *stretch1* and *stretch2*.
1233 The result is the absolute value of the difference between the
1234 CSS numeric values of *stretch1* and *stretch2*, normalized
1235 between 0.0 and 1.0.
1236 """
1237 try:
1238 stretchval1 = int(stretch1)
1239 except ValueError:
1240 stretchval1 = stretch_dict.get(stretch1, 500)
1241 try:
1242 stretchval2 = int(stretch2)
1243 except ValueError:
1244 stretchval2 = stretch_dict.get(stretch2, 500)
1245 return abs(stretchval1 - stretchval2) / 1000.0
1247 def score_weight(self, weight1, weight2):
1248 """
1249 Return a match score between *weight1* and *weight2*.
1251 The result is 0.0 if both weight1 and weight 2 are given as strings
1252 and have the same value.
1254 Otherwise, the result is the absolute value of the difference between
1255 the CSS numeric values of *weight1* and *weight2*, normalized between
1256 0.05 and 1.0.
1257 """
1258 # exact match of the weight names, e.g. weight1 == weight2 == "regular"
1259 if cbook._str_equal(weight1, weight2):
1260 return 0.0
1261 w1 = weight1 if isinstance(weight1, Number) else weight_dict[weight1]
1262 w2 = weight2 if isinstance(weight2, Number) else weight_dict[weight2]
1263 return 0.95 * (abs(w1 - w2) / 1000) + 0.05
1265 def score_size(self, size1, size2):
1266 """
1267 Return a match score between *size1* and *size2*.
1269 If *size2* (the size specified in the font file) is 'scalable', this
1270 function always returns 0.0, since any font size can be generated.
1272 Otherwise, the result is the absolute distance between *size1* and
1273 *size2*, normalized so that the usual range of font sizes (6pt -
1274 72pt) will lie between 0.0 and 1.0.
1275 """
1276 if size2 == 'scalable':
1277 return 0.0
1278 # Size value should have already been
1279 try:
1280 sizeval1 = float(size1)
1281 except ValueError:
1282 sizeval1 = self.default_size * font_scalings[size1]
1283 try:
1284 sizeval2 = float(size2)
1285 except ValueError:
1286 return 1.0
1287 return abs(sizeval1 - sizeval2) / 72
1289 def findfont(self, prop, fontext='ttf', directory=None,
1290 fallback_to_default=True, rebuild_if_missing=True):
1291 """
1292 Find a font that most closely matches the given font properties.
1294 Parameters
1295 ----------
1296 prop : str or `~matplotlib.font_manager.FontProperties`
1297 The font properties to search for. This can be either a
1298 `.FontProperties` object or a string defining a
1299 `fontconfig patterns`_.
1301 fontext : {'ttf', 'afm'}, default: 'ttf'
1302 The extension of the font file:
1304 - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf)
1305 - 'afm': Adobe Font Metrics (.afm)
1307 directory : str, optional
1308 If given, only search this directory and its subdirectories.
1310 fallback_to_default : bool
1311 If True, will fall back to the default font family (usually
1312 "DejaVu Sans" or "Helvetica") if the first lookup hard-fails.
1314 rebuild_if_missing : bool
1315 Whether to rebuild the font cache and search again if the first
1316 match appears to point to a nonexisting font (i.e., the font cache
1317 contains outdated entries).
1319 Returns
1320 -------
1321 str
1322 The filename of the best matching font.
1324 Notes
1325 -----
1326 This performs a nearest neighbor search. Each font is given a
1327 similarity score to the target font properties. The first font with
1328 the highest score is returned. If no matches below a certain
1329 threshold are found, the default font (usually DejaVu Sans) is
1330 returned.
1332 The result is cached, so subsequent lookups don't have to
1333 perform the O(n) nearest neighbor search.
1335 See the `W3C Cascading Style Sheet, Level 1
1336 <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ documentation
1337 for a description of the font finding algorithm.
1339 .. _fontconfig patterns:
1340 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
1341 """
1342 # Pass the relevant rcParams (and the font manager, as `self`) to
1343 # _findfont_cached so to prevent using a stale cache entry after an
1344 # rcParam was changed.
1345 rc_params = tuple(tuple(mpl.rcParams[key]) for key in [
1346 "font.serif", "font.sans-serif", "font.cursive", "font.fantasy",
1347 "font.monospace"])
1348 ret = self._findfont_cached(
1349 prop, fontext, directory, fallback_to_default, rebuild_if_missing,
1350 rc_params)
1351 if isinstance(ret, Exception):
1352 raise ret
1353 return ret
1355 def get_font_names(self):
1356 """Return the list of available fonts."""
1357 return list(set([font.name for font in self.ttflist]))
1359 def _find_fonts_by_props(self, prop, fontext='ttf', directory=None,
1360 fallback_to_default=True, rebuild_if_missing=True):
1361 """
1362 Find font families that most closely match the given properties.
1364 Parameters
1365 ----------
1366 prop : str or `~matplotlib.font_manager.FontProperties`
1367 The font properties to search for. This can be either a
1368 `.FontProperties` object or a string defining a
1369 `fontconfig patterns`_.
1371 fontext : {'ttf', 'afm'}, default: 'ttf'
1372 The extension of the font file:
1374 - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf)
1375 - 'afm': Adobe Font Metrics (.afm)
1377 directory : str, optional
1378 If given, only search this directory and its subdirectories.
1380 fallback_to_default : bool
1381 If True, will fall back to the default font family (usually
1382 "DejaVu Sans" or "Helvetica") if none of the families were found.
1384 rebuild_if_missing : bool
1385 Whether to rebuild the font cache and search again if the first
1386 match appears to point to a nonexisting font (i.e., the font cache
1387 contains outdated entries).
1389 Returns
1390 -------
1391 list[str]
1392 The paths of the fonts found
1394 Notes
1395 -----
1396 This is an extension/wrapper of the original findfont API, which only
1397 returns a single font for given font properties. Instead, this API
1398 returns a dict containing multiple fonts and their filepaths
1399 which closely match the given font properties. Since this internally
1400 uses the original API, there's no change to the logic of performing the
1401 nearest neighbor search. See `findfont` for more details.
1402 """
1404 prop = FontProperties._from_any(prop)
1406 fpaths = []
1407 for family in prop.get_family():
1408 cprop = prop.copy()
1409 cprop.set_family(family) # set current prop's family
1411 try:
1412 fpaths.append(
1413 self.findfont(
1414 cprop, fontext, directory,
1415 fallback_to_default=False, # don't fallback to default
1416 rebuild_if_missing=rebuild_if_missing,
1417 )
1418 )
1419 except ValueError:
1420 if family in font_family_aliases:
1421 _log.warning(
1422 "findfont: Generic family %r not found because "
1423 "none of the following families were found: %s",
1424 family, ", ".join(self._expand_aliases(family))
1425 )
1426 else:
1427 _log.warning("findfont: Font family %r not found.", family)
1429 # only add default family if no other font was found and
1430 # fallback_to_default is enabled
1431 if not fpaths:
1432 if fallback_to_default:
1433 dfamily = self.defaultFamily[fontext]
1434 cprop = prop.copy()
1435 cprop.set_family(dfamily)
1436 fpaths.append(
1437 self.findfont(
1438 cprop, fontext, directory,
1439 fallback_to_default=True,
1440 rebuild_if_missing=rebuild_if_missing,
1441 )
1442 )
1443 else:
1444 raise ValueError("Failed to find any font, and fallback "
1445 "to the default font was disabled")
1447 return fpaths
1449 @lru_cache(1024)
1450 def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
1451 rebuild_if_missing, rc_params):
1453 prop = FontProperties._from_any(prop)
1455 fname = prop.get_file()
1456 if fname is not None:
1457 return fname
1459 if fontext == 'afm':
1460 fontlist = self.afmlist
1461 else:
1462 fontlist = self.ttflist
1464 best_score = 1e64
1465 best_font = None
1467 _log.debug('findfont: Matching %s.', prop)
1468 for font in fontlist:
1469 if (directory is not None and
1470 Path(directory) not in Path(font.fname).parents):
1471 continue
1472 # Matching family should have top priority, so multiply it by 10.
1473 score = (self.score_family(prop.get_family(), font.name) * 10
1474 + self.score_style(prop.get_style(), font.style)
1475 + self.score_variant(prop.get_variant(), font.variant)
1476 + self.score_weight(prop.get_weight(), font.weight)
1477 + self.score_stretch(prop.get_stretch(), font.stretch)
1478 + self.score_size(prop.get_size(), font.size))
1479 _log.debug('findfont: score(%s) = %s', font, score)
1480 if score < best_score:
1481 best_score = score
1482 best_font = font
1483 if score == 0:
1484 break
1486 if best_font is None or best_score >= 10.0:
1487 if fallback_to_default:
1488 _log.warning(
1489 'findfont: Font family %s not found. Falling back to %s.',
1490 prop.get_family(), self.defaultFamily[fontext])
1491 for family in map(str.lower, prop.get_family()):
1492 if family in font_family_aliases:
1493 _log.warning(
1494 "findfont: Generic family %r not found because "
1495 "none of the following families were found: %s",
1496 family, ", ".join(self._expand_aliases(family)))
1497 default_prop = prop.copy()
1498 default_prop.set_family(self.defaultFamily[fontext])
1499 return self.findfont(default_prop, fontext, directory,
1500 fallback_to_default=False)
1501 else:
1502 # This return instead of raise is intentional, as we wish to
1503 # cache the resulting exception, which will not occur if it was
1504 # actually raised.
1505 return ValueError(f"Failed to find font {prop}, and fallback "
1506 f"to the default font was disabled")
1507 else:
1508 _log.debug('findfont: Matching %s to %s (%r) with score of %f.',
1509 prop, best_font.name, best_font.fname, best_score)
1510 result = best_font.fname
1512 if not os.path.isfile(result):
1513 if rebuild_if_missing:
1514 _log.info(
1515 'findfont: Found a missing font file. Rebuilding cache.')
1516 new_fm = _load_fontmanager(try_read_cache=False)
1517 # Replace self by the new fontmanager, because users may have
1518 # a reference to this specific instance.
1519 # TODO: _load_fontmanager should really be (used by) a method
1520 # modifying the instance in place.
1521 vars(self).update(vars(new_fm))
1522 return self.findfont(
1523 prop, fontext, directory, rebuild_if_missing=False)
1524 else:
1525 # This return instead of raise is intentional, as we wish to
1526 # cache the resulting exception, which will not occur if it was
1527 # actually raised.
1528 return ValueError("No valid font could be found")
1530 return _cached_realpath(result)
1533@lru_cache()
1534def is_opentype_cff_font(filename):
1535 """
1536 Return whether the given font is a Postscript Compact Font Format Font
1537 embedded in an OpenType wrapper. Used by the PostScript and PDF backends
1538 that can not subset these fonts.
1539 """
1540 if os.path.splitext(filename)[1].lower() == '.otf':
1541 with open(filename, 'rb') as fd:
1542 return fd.read(4) == b"OTTO"
1543 else:
1544 return False
1547@lru_cache(64)
1548def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id):
1549 first_fontpath, *rest = font_filepaths
1550 return ft2font.FT2Font(
1551 first_fontpath, hinting_factor,
1552 _fallback_list=[
1553 ft2font.FT2Font(
1554 fpath, hinting_factor,
1555 _kerning_factor=_kerning_factor
1556 )
1557 for fpath in rest
1558 ],
1559 _kerning_factor=_kerning_factor
1560 )
1563# FT2Font objects cannot be used across fork()s because they reference the same
1564# FT_Library object. While invalidating *all* existing FT2Fonts after a fork
1565# would be too complicated to be worth it, the main way FT2Fonts get reused is
1566# via the cache of _get_font, which we can empty upon forking (not on Windows,
1567# which has no fork() or register_at_fork()).
1568if hasattr(os, "register_at_fork"):
1569 os.register_at_fork(after_in_child=_get_font.cache_clear)
1572@lru_cache(64)
1573def _cached_realpath(path):
1574 # Resolving the path avoids embedding the font twice in pdf/ps output if a
1575 # single font is selected using two different relative paths.
1576 return os.path.realpath(path)
1579@_api.rename_parameter('3.6', "filepath", "font_filepaths")
1580def get_font(font_filepaths, hinting_factor=None):
1581 """
1582 Get an `.ft2font.FT2Font` object given a list of file paths.
1584 Parameters
1585 ----------
1586 font_filepaths : Iterable[str, Path, bytes], str, Path, bytes
1587 Relative or absolute paths to the font files to be used.
1589 If a single string, bytes, or `pathlib.Path`, then it will be treated
1590 as a list with that entry only.
1592 If more than one filepath is passed, then the returned FT2Font object
1593 will fall back through the fonts, in the order given, to find a needed
1594 glyph.
1596 Returns
1597 -------
1598 `.ft2font.FT2Font`
1600 """
1601 if isinstance(font_filepaths, (str, Path, bytes)):
1602 paths = (_cached_realpath(font_filepaths),)
1603 else:
1604 paths = tuple(_cached_realpath(fname) for fname in font_filepaths)
1606 if hinting_factor is None:
1607 hinting_factor = mpl.rcParams['text.hinting_factor']
1609 return _get_font(
1610 # must be a tuple to be cached
1611 paths,
1612 hinting_factor,
1613 _kerning_factor=mpl.rcParams['text.kerning_factor'],
1614 # also key on the thread ID to prevent segfaults with multi-threading
1615 thread_id=threading.get_ident()
1616 )
1619def _load_fontmanager(*, try_read_cache=True):
1620 fm_path = Path(
1621 mpl.get_cachedir(), f"fontlist-v{FontManager.__version__}.json")
1622 if try_read_cache:
1623 try:
1624 fm = json_load(fm_path)
1625 except Exception:
1626 pass
1627 else:
1628 if getattr(fm, "_version", object()) == FontManager.__version__:
1629 _log.debug("Using fontManager instance from %s", fm_path)
1630 return fm
1631 fm = FontManager()
1632 json_dump(fm, fm_path)
1633 _log.info("generated new fontManager")
1634 return fm
1637fontManager = _load_fontmanager()
1638findfont = fontManager.findfont
1639get_font_names = fontManager.get_font_names