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