Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/font_manager.py : 22%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2A module for finding, managing, and using fonts across platforms.
4This module provides a single `FontManager` instance 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 functools import lru_cache
27import json
28import logging
29from numbers import Number
30import os
31from pathlib import Path
32import subprocess
33import sys
34try:
35 from threading import Timer
36except ImportError:
37 from dummy_threading import Timer
39import matplotlib as mpl
40from matplotlib import afm, cbook, ft2font, rcParams
41from matplotlib.fontconfig_pattern import (
42 parse_fontconfig_pattern, generate_fontconfig_pattern)
44_log = logging.getLogger(__name__)
46font_scalings = {
47 'xx-small': 0.579,
48 'x-small': 0.694,
49 'small': 0.833,
50 'medium': 1.0,
51 'large': 1.200,
52 'x-large': 1.440,
53 'xx-large': 1.728,
54 'larger': 1.2,
55 'smaller': 0.833,
56 None: 1.0,
57}
58stretch_dict = {
59 'ultra-condensed': 100,
60 'extra-condensed': 200,
61 'condensed': 300,
62 'semi-condensed': 400,
63 'normal': 500,
64 'semi-expanded': 600,
65 'semi-extended': 600,
66 'expanded': 700,
67 'extended': 700,
68 'extra-expanded': 800,
69 'extra-extended': 800,
70 'ultra-expanded': 900,
71 'ultra-extended': 900,
72}
73weight_dict = {
74 'ultralight': 100,
75 'light': 200,
76 'normal': 400,
77 'regular': 400,
78 'book': 400,
79 'medium': 500,
80 'roman': 500,
81 'semibold': 600,
82 'demibold': 600,
83 'demi': 600,
84 'bold': 700,
85 'heavy': 800,
86 'extra bold': 800,
87 'black': 900,
88}
89font_family_aliases = {
90 'serif',
91 'sans-serif',
92 'sans serif',
93 'cursive',
94 'fantasy',
95 'monospace',
96 'sans',
97}
98# OS Font paths
99MSFolders = \
100 r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
101MSFontDirectories = [
102 r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts',
103 r'SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts']
104MSUserFontDirectories = [
105 str(Path.home() / 'AppData/Local/Microsoft/Windows/Fonts'),
106 str(Path.home() / 'AppData/Roaming/Microsoft/Windows/Fonts'),
107]
108X11FontDirectories = [
109 # an old standard installation point
110 "/usr/X11R6/lib/X11/fonts/TTF/",
111 "/usr/X11/lib/X11/fonts",
112 # here is the new standard location for fonts
113 "/usr/share/fonts/",
114 # documented as a good place to install new fonts
115 "/usr/local/share/fonts/",
116 # common application, not really useful
117 "/usr/lib/openoffice/share/fonts/truetype/",
118 # user fonts
119 str(Path(os.environ.get('XDG_DATA_HOME',
120 Path.home() / ".local/share")) / "fonts"),
121 str(Path.home() / ".fonts"),
122]
123OSXFontDirectories = [
124 "/Library/Fonts/",
125 "/Network/Library/Fonts/",
126 "/System/Library/Fonts/",
127 # fonts installed via MacPorts
128 "/opt/local/share/fonts",
129 # user fonts
130 str(Path.home() / "Library/Fonts"),
131]
134def get_fontext_synonyms(fontext):
135 """
136 Return a list of file extensions extensions that are synonyms for
137 the given file extension *fileext*.
138 """
139 return {
140 'afm': ['afm'],
141 'otf': ['otf', 'ttc', 'ttf'],
142 'ttc': ['otf', 'ttc', 'ttf'],
143 'ttf': ['otf', 'ttc', 'ttf'],
144 }[fontext]
147def list_fonts(directory, extensions):
148 """
149 Return a list of all fonts matching any of the extensions, found
150 recursively under the directory.
151 """
152 extensions = ["." + ext for ext in extensions]
153 return [os.path.join(dirpath, filename)
154 # os.walk ignores access errors, unlike Path.glob.
155 for dirpath, _, filenames in os.walk(directory)
156 for filename in filenames
157 if Path(filename).suffix.lower() in extensions]
160def win32FontDirectory():
161 r"""
162 Return the user-specified font directory for Win32. This is
163 looked up from the registry key ::
165 \\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\Fonts
167 If the key is not found, ``%WINDIR%\Fonts`` will be returned.
168 """
169 import winreg
170 try:
171 with winreg.OpenKey(winreg.HKEY_CURRENT_USER, MSFolders) as user:
172 return winreg.QueryValueEx(user, 'Fonts')[0]
173 except OSError:
174 return os.path.join(os.environ['WINDIR'], 'Fonts')
177def _win32RegistryFonts(reg_domain, base_dir):
178 r"""
179 Searches for fonts in the Windows registry.
181 Parameters
182 ----------
183 reg_domain : int
184 The top level registry domain (e.g. HKEY_LOCAL_MACHINE).
186 base_dir : str
187 The path to the folder where the font files are usually located (e.g.
188 C:\Windows\Fonts). If only the filename of the font is stored in the
189 registry, the absolute path is built relative to this base directory.
191 Returns
192 -------
193 `set`
194 `pathlib.Path` objects with the absolute path to the font files found.
196 """
197 import winreg
198 items = set()
200 for reg_path in MSFontDirectories:
201 try:
202 with winreg.OpenKey(reg_domain, reg_path) as local:
203 for j in range(winreg.QueryInfoKey(local)[1]):
204 # value may contain the filename of the font or its
205 # absolute path.
206 key, value, tp = winreg.EnumValue(local, j)
207 if not isinstance(value, str):
208 continue
210 # Work around for https://bugs.python.org/issue25778, which
211 # is fixed in Py>=3.6.1.
212 value = value.split("\0", 1)[0]
214 try:
215 # If value contains already an absolute path, then it
216 # is not changed further.
217 path = Path(base_dir, value).resolve()
218 except RuntimeError:
219 # Don't fail with invalid entries.
220 continue
222 items.add(path)
223 except (OSError, MemoryError):
224 continue
226 return items
229def win32InstalledFonts(directory=None, fontext='ttf'):
230 """
231 Search for fonts in the specified font directory, or use the
232 system directories if none given. Additionally, it is searched for user
233 fonts installed. A list of TrueType font filenames are returned by default,
234 or AFM fonts if *fontext* == 'afm'.
235 """
236 import winreg
238 if directory is None:
239 directory = win32FontDirectory()
241 fontext = ['.' + ext for ext in get_fontext_synonyms(fontext)]
243 items = set()
245 # System fonts
246 items.update(_win32RegistryFonts(winreg.HKEY_LOCAL_MACHINE, directory))
248 # User fonts
249 for userdir in MSUserFontDirectories:
250 items.update(_win32RegistryFonts(winreg.HKEY_CURRENT_USER, userdir))
252 # Keep only paths with matching file extension.
253 return [str(path) for path in items if path.suffix.lower() in fontext]
256@cbook.deprecated("3.1")
257def OSXInstalledFonts(directories=None, fontext='ttf'):
258 """Get list of font files on OS X."""
259 if directories is None:
260 directories = OSXFontDirectories
261 return [path
262 for directory in directories
263 for path in list_fonts(directory, get_fontext_synonyms(fontext))]
266@lru_cache()
267def _call_fc_list():
268 """Cache and list the font filenames known to `fc-list`.
269 """
270 # Delay the warning by 5s.
271 timer = Timer(5, lambda: _log.warning(
272 'Matplotlib is building the font cache using fc-list. '
273 'This may take a moment.'))
274 timer.start()
275 try:
276 if b'--format' not in subprocess.check_output(['fc-list', '--help']):
277 _log.warning( # fontconfig 2.7 implemented --format.
278 'Matplotlib needs fontconfig>=2.7 to query system fonts.')
279 return []
280 out = subprocess.check_output(['fc-list', '--format=%{file}\\n'])
281 except (OSError, subprocess.CalledProcessError):
282 return []
283 finally:
284 timer.cancel()
285 return [os.fsdecode(fname) for fname in out.split(b'\n')]
288def get_fontconfig_fonts(fontext='ttf'):
289 """List the font filenames known to `fc-list` having the given extension.
290 """
291 fontext = ['.' + ext for ext in get_fontext_synonyms(fontext)]
292 return [fname for fname in _call_fc_list()
293 if Path(fname).suffix.lower() in fontext]
296def findSystemFonts(fontpaths=None, fontext='ttf'):
297 """
298 Search for fonts in the specified font paths. If no paths are
299 given, will use a standard set of system paths, as well as the
300 list of fonts tracked by fontconfig if fontconfig is installed and
301 available. A list of TrueType fonts are returned by default with
302 AFM fonts as an option.
303 """
304 fontfiles = set()
305 fontexts = get_fontext_synonyms(fontext)
307 if fontpaths is None:
308 if sys.platform == 'win32':
309 fontpaths = MSUserFontDirectories + [win32FontDirectory()]
310 # now get all installed fonts directly...
311 fontfiles.update(win32InstalledFonts(fontext=fontext))
312 else:
313 fontpaths = X11FontDirectories
314 if sys.platform == 'darwin':
315 fontpaths = [*X11FontDirectories, *OSXFontDirectories]
316 fontfiles.update(get_fontconfig_fonts(fontext))
318 elif isinstance(fontpaths, str):
319 fontpaths = [fontpaths]
321 for path in fontpaths:
322 fontfiles.update(map(os.path.abspath, list_fonts(path, fontexts)))
324 return [fname for fname in fontfiles if os.path.exists(fname)]
327class FontEntry:
328 """
329 A class for storing Font properties. It is used when populating
330 the font lookup dictionary.
331 """
332 def __init__(self,
333 fname ='',
334 name ='',
335 style ='normal',
336 variant='normal',
337 weight ='normal',
338 stretch='normal',
339 size ='medium',
340 ):
341 self.fname = fname
342 self.name = name
343 self.style = style
344 self.variant = variant
345 self.weight = weight
346 self.stretch = stretch
347 try:
348 self.size = str(float(size))
349 except ValueError:
350 self.size = size
352 def __repr__(self):
353 return "<Font '%s' (%s) %s %s %s %s>" % (
354 self.name, os.path.basename(self.fname), self.style, self.variant,
355 self.weight, self.stretch)
358def ttfFontProperty(font):
359 """
360 Extract information from a TrueType font file.
362 Parameters
363 ----------
364 font : `.FT2Font`
365 The TrueType font file from which information will be extracted.
367 Returns
368 -------
369 `FontEntry`
370 The extracted font properties.
372 """
373 name = font.family_name
375 # Styles are: italic, oblique, and normal (default)
377 sfnt = font.get_sfnt()
378 # These tables are actually mac_roman-encoded, but mac_roman support may be
379 # missing in some alternative Python implementations and we are only going
380 # to look for ASCII substrings, where any ASCII-compatible encoding works
381 # - or big-endian UTF-16, since important Microsoft fonts use that.
382 sfnt2 = (sfnt.get((1, 0, 0, 2), b'').decode('latin-1').lower() or
383 sfnt.get((3, 1, 0x0409, 2), b'').decode('utf_16_be').lower())
384 sfnt4 = (sfnt.get((1, 0, 0, 4), b'').decode('latin-1').lower() or
385 sfnt.get((3, 1, 0x0409, 4), b'').decode('utf_16_be').lower())
387 if sfnt4.find('oblique') >= 0:
388 style = 'oblique'
389 elif sfnt4.find('italic') >= 0:
390 style = 'italic'
391 elif sfnt2.find('regular') >= 0:
392 style = 'normal'
393 elif font.style_flags & ft2font.ITALIC:
394 style = 'italic'
395 else:
396 style = 'normal'
398 # Variants are: small-caps and normal (default)
400 # !!!! Untested
401 if name.lower() in ['capitals', 'small-caps']:
402 variant = 'small-caps'
403 else:
404 variant = 'normal'
406 if font.style_flags & ft2font.BOLD:
407 weight = 700
408 else:
409 weight = next((w for w in weight_dict if w in sfnt4), 400)
411 # Stretch can be absolute and relative
412 # Absolute stretches are: ultra-condensed, extra-condensed, condensed,
413 # semi-condensed, normal, semi-expanded, expanded, extra-expanded,
414 # and ultra-expanded.
415 # Relative stretches are: wider, narrower
416 # Child value is: inherit
418 if any(word in sfnt4 for word in ['narrow', 'condensed', 'cond']):
419 stretch = 'condensed'
420 elif 'demi cond' in sfnt4:
421 stretch = 'semi-condensed'
422 elif any(word in sfnt4 for word in ['wide', 'expanded', 'extended']):
423 stretch = 'expanded'
424 else:
425 stretch = 'normal'
427 # Sizes can be absolute and relative.
428 # Absolute sizes are: xx-small, x-small, small, medium, large, x-large,
429 # and xx-large.
430 # Relative sizes are: larger, smaller
431 # Length value is an absolute font size, e.g., 12pt
432 # Percentage values are in 'em's. Most robust specification.
434 if not font.scalable:
435 raise NotImplementedError("Non-scalable fonts are not supported")
436 size = 'scalable'
438 return FontEntry(font.fname, name, style, variant, weight, stretch, size)
441def afmFontProperty(fontpath, font):
442 """
443 Extract information from an AFM font file.
445 Parameters
446 ----------
447 font : `.AFM`
448 The AFM font file from which information will be extracted.
450 Returns
451 -------
452 `FontEntry`
453 The extracted font properties.
454 """
456 name = font.get_familyname()
457 fontname = font.get_fontname().lower()
459 # Styles are: italic, oblique, and normal (default)
461 if font.get_angle() != 0 or 'italic' in name.lower():
462 style = 'italic'
463 elif 'oblique' in name.lower():
464 style = 'oblique'
465 else:
466 style = 'normal'
468 # Variants are: small-caps and normal (default)
470 # !!!! Untested
471 if name.lower() in ['capitals', 'small-caps']:
472 variant = 'small-caps'
473 else:
474 variant = 'normal'
476 weight = font.get_weight().lower()
477 if weight not in weight_dict:
478 weight = 'normal'
480 # Stretch can be absolute and relative
481 # Absolute stretches are: ultra-condensed, extra-condensed, condensed,
482 # semi-condensed, normal, semi-expanded, expanded, extra-expanded,
483 # and ultra-expanded.
484 # Relative stretches are: wider, narrower
485 # Child value is: inherit
486 if 'demi cond' in fontname:
487 stretch = 'semi-condensed'
488 elif any(word in fontname for word in ['narrow', 'cond']):
489 stretch = 'condensed'
490 elif any(word in fontname for word in ['wide', 'expanded', 'extended']):
491 stretch = 'expanded'
492 else:
493 stretch = 'normal'
495 # Sizes can be absolute and relative.
496 # Absolute sizes are: xx-small, x-small, small, medium, large, x-large,
497 # and xx-large.
498 # Relative sizes are: larger, smaller
499 # Length value is an absolute font size, e.g., 12pt
500 # Percentage values are in 'em's. Most robust specification.
502 # All AFM fonts are apparently scalable.
504 size = 'scalable'
506 return FontEntry(fontpath, name, style, variant, weight, stretch, size)
509@cbook.deprecated("3.2", alternative="FontManager.addfont")
510def createFontList(fontfiles, fontext='ttf'):
511 """
512 A function to create a font lookup list. The default is to create
513 a list of TrueType fonts. An AFM font list can optionally be
514 created.
515 """
517 fontlist = []
518 # Add fonts from list of known font files.
519 seen = set()
520 for fpath in fontfiles:
521 _log.debug('createFontDict: %s', fpath)
522 fname = os.path.split(fpath)[1]
523 if fname in seen:
524 continue
525 if fontext == 'afm':
526 try:
527 with open(fpath, 'rb') as fh:
528 font = afm.AFM(fh)
529 except EnvironmentError:
530 _log.info("Could not open font file %s", fpath)
531 continue
532 except RuntimeError:
533 _log.info("Could not parse font file %s", fpath)
534 continue
535 try:
536 prop = afmFontProperty(fpath, font)
537 except KeyError as exc:
538 _log.info("Could not extract properties for %s: %s",
539 fpath, exc)
540 continue
541 else:
542 try:
543 font = ft2font.FT2Font(fpath)
544 except (OSError, RuntimeError) as exc:
545 _log.info("Could not open font file %s: %s", fpath, exc)
546 continue
547 except UnicodeError:
548 _log.info("Cannot handle unicode filenames")
549 continue
550 try:
551 prop = ttfFontProperty(font)
552 except (KeyError, RuntimeError, ValueError,
553 NotImplementedError) as exc:
554 _log.info("Could not extract properties for %s: %s",
555 fpath, exc)
556 continue
558 fontlist.append(prop)
559 seen.add(fname)
560 return fontlist
563class FontProperties:
564 """
565 A class for storing and manipulating font properties.
567 The font properties are those described in the `W3C Cascading
568 Style Sheet, Level 1
569 <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ font
570 specification. The six properties are:
572 - family: A list of font names in decreasing order of priority.
573 The items may include a generic font family name, either
574 'serif', 'sans-serif', 'cursive', 'fantasy', or 'monospace'.
575 In that case, the actual font to be used will be looked up
576 from the associated rcParam.
578 - style: Either 'normal', 'italic' or 'oblique'.
580 - variant: Either 'normal' or 'small-caps'.
582 - stretch: A numeric value in the range 0-1000 or one of
583 'ultra-condensed', 'extra-condensed', 'condensed',
584 'semi-condensed', 'normal', 'semi-expanded', 'expanded',
585 'extra-expanded' or 'ultra-expanded'.
587 - weight: A numeric value in the range 0-1000 or one of
588 'ultralight', 'light', 'normal', 'regular', 'book', 'medium',
589 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy',
590 'extra bold', 'black'.
592 - size: Either an relative value of 'xx-small', 'x-small',
593 'small', 'medium', 'large', 'x-large', 'xx-large' or an
594 absolute font size, e.g., 12.
596 The default font property for TrueType fonts (as specified in the
597 default rcParams) is ::
599 sans-serif, normal, normal, normal, normal, scalable.
601 Alternatively, a font may be specified using an absolute path to a
602 .ttf file, by using the *fname* kwarg.
604 The preferred usage of font sizes is to use the relative values,
605 e.g., 'large', instead of absolute font sizes, e.g., 12. This
606 approach allows all text sizes to be made larger or smaller based
607 on the font manager's default font size.
609 This class will also accept a fontconfig_ pattern_, if it is the only
610 argument provided. This support does not depend on fontconfig; we are
611 merely borrowing its pattern syntax for use here.
613 .. _fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/
614 .. _pattern:
615 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
617 Note that Matplotlib's internal font manager and fontconfig use a
618 different algorithm to lookup fonts, so the results of the same pattern
619 may be different in Matplotlib than in other applications that use
620 fontconfig.
621 """
623 def __init__(self,
624 family = None,
625 style = None,
626 variant= None,
627 weight = None,
628 stretch= None,
629 size = None,
630 fname = None, # if set, it's a hardcoded filename to use
631 ):
632 self._family = _normalize_font_family(rcParams['font.family'])
633 self._slant = rcParams['font.style']
634 self._variant = rcParams['font.variant']
635 self._weight = rcParams['font.weight']
636 self._stretch = rcParams['font.stretch']
637 self._size = rcParams['font.size']
638 self._file = None
640 if isinstance(family, str):
641 # Treat family as a fontconfig pattern if it is the only
642 # parameter provided.
643 if (style is None and
644 variant is None and
645 weight is None and
646 stretch is None and
647 size is None and
648 fname is None):
649 self.set_fontconfig_pattern(family)
650 return
652 self.set_family(family)
653 self.set_style(style)
654 self.set_variant(variant)
655 self.set_weight(weight)
656 self.set_stretch(stretch)
657 self.set_file(fname)
658 self.set_size(size)
660 def _parse_fontconfig_pattern(self, pattern):
661 return parse_fontconfig_pattern(pattern)
663 def __hash__(self):
664 l = (tuple(self.get_family()),
665 self.get_slant(),
666 self.get_variant(),
667 self.get_weight(),
668 self.get_stretch(),
669 self.get_size_in_points(),
670 self.get_file())
671 return hash(l)
673 def __eq__(self, other):
674 return hash(self) == hash(other)
676 def __str__(self):
677 return self.get_fontconfig_pattern()
679 def get_family(self):
680 """
681 Return a list of font names that comprise the font family.
682 """
683 return self._family
685 def get_name(self):
686 """
687 Return the name of the font that best matches the font properties.
688 """
689 return get_font(findfont(self)).family_name
691 def get_style(self):
692 """
693 Return the font style. Values are: 'normal', 'italic' or 'oblique'.
694 """
695 return self._slant
696 get_slant = get_style
698 def get_variant(self):
699 """
700 Return the font variant. Values are: 'normal' or 'small-caps'.
701 """
702 return self._variant
704 def get_weight(self):
705 """
706 Set the font weight. Options are: A numeric value in the
707 range 0-1000 or one of 'light', 'normal', 'regular', 'book',
708 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold',
709 'heavy', 'extra bold', 'black'
710 """
711 return self._weight
713 def get_stretch(self):
714 """
715 Return the font stretch or width. Options are: 'ultra-condensed',
716 'extra-condensed', 'condensed', 'semi-condensed', 'normal',
717 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'.
718 """
719 return self._stretch
721 def get_size(self):
722 """
723 Return the font size.
724 """
725 return self._size
727 def get_size_in_points(self):
728 return self._size
730 def get_file(self):
731 """
732 Return the filename of the associated font.
733 """
734 return self._file
736 def get_fontconfig_pattern(self):
737 """
738 Get a fontconfig_ pattern_ suitable for looking up the font as
739 specified with fontconfig's ``fc-match`` utility.
741 This support does not depend on fontconfig; we are merely borrowing its
742 pattern syntax for use here.
743 """
744 return generate_fontconfig_pattern(self)
746 def set_family(self, family):
747 """
748 Change the font family. May be either an alias (generic name
749 is CSS parlance), such as: 'serif', 'sans-serif', 'cursive',
750 'fantasy', or 'monospace', a real font name or a list of real
751 font names. Real font names are not supported when
752 `text.usetex` is `True`.
753 """
754 if family is None:
755 family = rcParams['font.family']
756 self._family = _normalize_font_family(family)
757 set_name = set_family
759 def set_style(self, style):
760 """
761 Set the font style. Values are: 'normal', 'italic' or 'oblique'.
762 """
763 if style is None:
764 style = rcParams['font.style']
765 cbook._check_in_list(['normal', 'italic', 'oblique'], style=style)
766 self._slant = style
767 set_slant = set_style
769 def set_variant(self, variant):
770 """
771 Set the font variant. Values are: 'normal' or 'small-caps'.
772 """
773 if variant is None:
774 variant = rcParams['font.variant']
775 cbook._check_in_list(['normal', 'small-caps'], variant=variant)
776 self._variant = variant
778 def set_weight(self, weight):
779 """
780 Set the font weight. May be either a numeric value in the
781 range 0-1000 or one of 'ultralight', 'light', 'normal',
782 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold',
783 'demi', 'bold', 'heavy', 'extra bold', 'black'
784 """
785 if weight is None:
786 weight = rcParams['font.weight']
787 try:
788 weight = int(weight)
789 if weight < 0 or weight > 1000:
790 raise ValueError()
791 except ValueError:
792 if weight not in weight_dict:
793 raise ValueError("weight is invalid")
794 self._weight = weight
796 def set_stretch(self, stretch):
797 """
798 Set the font stretch or width. Options are: 'ultra-condensed',
799 'extra-condensed', 'condensed', 'semi-condensed', 'normal',
800 'semi-expanded', 'expanded', 'extra-expanded' or
801 'ultra-expanded', or a numeric value in the range 0-1000.
802 """
803 if stretch is None:
804 stretch = rcParams['font.stretch']
805 try:
806 stretch = int(stretch)
807 if stretch < 0 or stretch > 1000:
808 raise ValueError()
809 except ValueError:
810 if stretch not in stretch_dict:
811 raise ValueError("stretch is invalid")
812 self._stretch = stretch
814 def set_size(self, size):
815 """
816 Set the font size. Either an relative value of 'xx-small',
817 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'
818 or an absolute font size, e.g., 12.
819 """
820 if size is None:
821 size = rcParams['font.size']
822 try:
823 size = float(size)
824 except ValueError:
825 try:
826 scale = font_scalings[size]
827 except KeyError:
828 raise ValueError(
829 "Size is invalid. Valid font size are "
830 + ", ".join(map(str, font_scalings)))
831 else:
832 size = scale * FontManager.get_default_size()
833 if size < 1.0:
834 _log.info('Fontsize %1.2f < 1.0 pt not allowed by FreeType. '
835 'Setting fontsize = 1 pt', size)
836 size = 1.0
837 self._size = size
839 def set_file(self, file):
840 """
841 Set the filename of the fontfile to use. In this case, all
842 other properties will be ignored.
843 """
844 self._file = os.fspath(file) if file is not None else None
846 def set_fontconfig_pattern(self, pattern):
847 """
848 Set the properties by parsing a fontconfig_ *pattern*.
850 This support does not depend on fontconfig; we are merely borrowing its
851 pattern syntax for use here.
852 """
853 for key, val in self._parse_fontconfig_pattern(pattern).items():
854 if type(val) == list:
855 getattr(self, "set_" + key)(val[0])
856 else:
857 getattr(self, "set_" + key)(val)
859 def copy(self):
860 """Return a copy of self."""
861 new = type(self)()
862 vars(new).update(vars(self))
863 return new
866class _JSONEncoder(json.JSONEncoder):
867 def default(self, o):
868 if isinstance(o, FontManager):
869 return dict(o.__dict__, __class__='FontManager')
870 elif isinstance(o, FontEntry):
871 d = dict(o.__dict__, __class__='FontEntry')
872 try:
873 # Cache paths of fonts shipped with Matplotlib relative to the
874 # Matplotlib data path, which helps in the presence of venvs.
875 d["fname"] = str(
876 Path(d["fname"]).relative_to(mpl.get_data_path()))
877 except ValueError:
878 pass
879 return d
880 else:
881 return super().default(o)
884@cbook.deprecated("3.2", alternative="json_dump")
885class JSONEncoder(_JSONEncoder):
886 pass
889def _json_decode(o):
890 cls = o.pop('__class__', None)
891 if cls is None:
892 return o
893 elif cls == 'FontManager':
894 r = FontManager.__new__(FontManager)
895 r.__dict__.update(o)
896 return r
897 elif cls == 'FontEntry':
898 r = FontEntry.__new__(FontEntry)
899 r.__dict__.update(o)
900 if not os.path.isabs(r.fname):
901 r.fname = os.path.join(mpl.get_data_path(), r.fname)
902 return r
903 else:
904 raise ValueError("don't know how to deserialize __class__=%s" % cls)
907def json_dump(data, filename):
908 """
909 Dump `FontManager` *data* as JSON to the file named *filename*.
911 Notes
912 -----
913 File paths that are children of the Matplotlib data path (typically, fonts
914 shipped with Matplotlib) are stored relative to that data path (to remain
915 valid across virtualenvs).
917 See Also
918 --------
919 json_load
920 """
921 with open(filename, 'w') as fh:
922 try:
923 json.dump(data, fh, cls=_JSONEncoder, indent=2)
924 except OSError as e:
925 _log.warning('Could not save font_manager cache {}'.format(e))
928def json_load(filename):
929 """
930 Load a `FontManager` from the JSON file named *filename*.
932 See Also
933 --------
934 json_dump
935 """
936 with open(filename, 'r') as fh:
937 return json.load(fh, object_hook=_json_decode)
940def _normalize_font_family(family):
941 if isinstance(family, str):
942 family = [family]
943 return family
946class FontManager:
947 """
948 On import, the :class:`FontManager` singleton instance creates a
949 list of TrueType fonts based on the font properties: name, style,
950 variant, weight, stretch, and size. The :meth:`findfont` method
951 does a nearest neighbor search to find the font that most closely
952 matches the specification. If no good enough match is found, a
953 default font is returned.
954 """
955 # Increment this version number whenever the font cache data
956 # format or behavior has changed and requires a existing font
957 # cache files to be rebuilt.
958 __version__ = 310
960 def __init__(self, size=None, weight='normal'):
961 self._version = self.__version__
963 self.__default_weight = weight
964 self.default_size = size
966 paths = [cbook._get_data_path('fonts', subdir)
967 for subdir in ['ttf', 'afm', 'pdfcorefonts']]
968 # Create list of font paths
969 for pathname in ['TTFPATH', 'AFMPATH']:
970 if pathname in os.environ:
971 ttfpath = os.environ[pathname]
972 if ttfpath.find(';') >= 0: # win32 style
973 paths.extend(ttfpath.split(';'))
974 elif ttfpath.find(':') >= 0: # unix style
975 paths.extend(ttfpath.split(':'))
976 else:
977 paths.append(ttfpath)
978 _log.debug('font search path %s', str(paths))
979 # Load TrueType fonts and create font dictionary.
981 self.defaultFamily = {
982 'ttf': 'DejaVu Sans',
983 'afm': 'Helvetica'}
985 self.afmlist = []
986 self.ttflist = []
987 for fontext in ["afm", "ttf"]:
988 for path in [*findSystemFonts(paths, fontext=fontext),
989 *findSystemFonts(fontext=fontext)]:
990 try:
991 self.addfont(path)
992 except OSError as exc:
993 _log.info("Failed to open font file %s: %s", path, exc)
994 except Exception as exc:
995 _log.info("Failed to extract font properties from %s: %s",
996 path, exc)
998 def addfont(self, path):
999 """
1000 Cache the properties of the font at *path* to make it available to the
1001 `FontManager`. The type of font is inferred from the path suffix.
1003 Parameters
1004 ----------
1005 path : str or path-like
1006 """
1007 if Path(path).suffix.lower() == ".afm":
1008 with open(path, "rb") as fh:
1009 font = afm.AFM(fh)
1010 prop = afmFontProperty(path, font)
1011 self.afmlist.append(prop)
1012 else:
1013 font = ft2font.FT2Font(path)
1014 prop = ttfFontProperty(font)
1015 self.ttflist.append(prop)
1017 @property
1018 def defaultFont(self):
1019 # Lazily evaluated (findfont then caches the result) to avoid including
1020 # the venv path in the json serialization.
1021 return {ext: self.findfont(family, fontext=ext)
1022 for ext, family in self.defaultFamily.items()}
1024 def get_default_weight(self):
1025 """
1026 Return the default font weight.
1027 """
1028 return self.__default_weight
1030 @staticmethod
1031 def get_default_size():
1032 """
1033 Return the default font size.
1034 """
1035 return rcParams['font.size']
1037 def set_default_weight(self, weight):
1038 """
1039 Set the default font weight. The initial value is 'normal'.
1040 """
1041 self.__default_weight = weight
1043 # Each of the scoring functions below should return a value between
1044 # 0.0 (perfect match) and 1.0 (terrible match)
1045 def score_family(self, families, family2):
1046 """
1047 Returns a match score between the list of font families in
1048 *families* and the font family name *family2*.
1050 An exact match at the head of the list returns 0.0.
1052 A match further down the list will return between 0 and 1.
1054 No match will return 1.0.
1055 """
1056 if not isinstance(families, (list, tuple)):
1057 families = [families]
1058 elif len(families) == 0:
1059 return 1.0
1060 family2 = family2.lower()
1061 step = 1 / len(families)
1062 for i, family1 in enumerate(families):
1063 family1 = family1.lower()
1064 if family1 in font_family_aliases:
1065 if family1 in ('sans', 'sans serif'):
1066 family1 = 'sans-serif'
1067 options = rcParams['font.' + family1]
1068 options = [x.lower() for x in options]
1069 if family2 in options:
1070 idx = options.index(family2)
1071 return (i + (idx / len(options))) * step
1072 elif family1 == family2:
1073 # The score should be weighted by where in the
1074 # list the font was found.
1075 return i * step
1076 return 1.0
1078 def score_style(self, style1, style2):
1079 """
1080 Returns a match score between *style1* and *style2*.
1082 An exact match returns 0.0.
1084 A match between 'italic' and 'oblique' returns 0.1.
1086 No match returns 1.0.
1087 """
1088 if style1 == style2:
1089 return 0.0
1090 elif (style1 in ('italic', 'oblique')
1091 and style2 in ('italic', 'oblique')):
1092 return 0.1
1093 return 1.0
1095 def score_variant(self, variant1, variant2):
1096 """
1097 Returns a match score between *variant1* and *variant2*.
1099 An exact match returns 0.0, otherwise 1.0.
1100 """
1101 if variant1 == variant2:
1102 return 0.0
1103 else:
1104 return 1.0
1106 def score_stretch(self, stretch1, stretch2):
1107 """
1108 Returns a match score between *stretch1* and *stretch2*.
1110 The result is the absolute value of the difference between the
1111 CSS numeric values of *stretch1* and *stretch2*, normalized
1112 between 0.0 and 1.0.
1113 """
1114 try:
1115 stretchval1 = int(stretch1)
1116 except ValueError:
1117 stretchval1 = stretch_dict.get(stretch1, 500)
1118 try:
1119 stretchval2 = int(stretch2)
1120 except ValueError:
1121 stretchval2 = stretch_dict.get(stretch2, 500)
1122 return abs(stretchval1 - stretchval2) / 1000.0
1124 def score_weight(self, weight1, weight2):
1125 """
1126 Returns a match score between *weight1* and *weight2*.
1128 The result is 0.0 if both weight1 and weight 2 are given as strings
1129 and have the same value.
1131 Otherwise, the result is the absolute value of the difference between
1132 the CSS numeric values of *weight1* and *weight2*, normalized between
1133 0.05 and 1.0.
1134 """
1135 # exact match of the weight names, e.g. weight1 == weight2 == "regular"
1136 if cbook._str_equal(weight1, weight2):
1137 return 0.0
1138 w1 = weight1 if isinstance(weight1, Number) else weight_dict[weight1]
1139 w2 = weight2 if isinstance(weight2, Number) else weight_dict[weight2]
1140 return 0.95 * (abs(w1 - w2) / 1000) + 0.05
1142 def score_size(self, size1, size2):
1143 """
1144 Returns a match score between *size1* and *size2*.
1146 If *size2* (the size specified in the font file) is 'scalable', this
1147 function always returns 0.0, since any font size can be generated.
1149 Otherwise, the result is the absolute distance between *size1* and
1150 *size2*, normalized so that the usual range of font sizes (6pt -
1151 72pt) will lie between 0.0 and 1.0.
1152 """
1153 if size2 == 'scalable':
1154 return 0.0
1155 # Size value should have already been
1156 try:
1157 sizeval1 = float(size1)
1158 except ValueError:
1159 sizeval1 = self.default_size * font_scalings[size1]
1160 try:
1161 sizeval2 = float(size2)
1162 except ValueError:
1163 return 1.0
1164 return abs(sizeval1 - sizeval2) / 72
1166 def findfont(self, prop, fontext='ttf', directory=None,
1167 fallback_to_default=True, rebuild_if_missing=True):
1168 """
1169 Find a font that most closely matches the given font properties.
1171 Parameters
1172 ----------
1173 prop : str or `~matplotlib.font_manager.FontProperties`
1174 The font properties to search for. This can be either a
1175 `.FontProperties` object or a string defining a
1176 `fontconfig patterns`_.
1178 fontext : {'ttf', 'afm'}, optional, default: 'ttf'
1179 The extension of the font file:
1181 - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf)
1182 - 'afm': Adobe Font Metrics (.afm)
1184 directory : str, optional
1185 If given, only search this directory and its subdirectories.
1186 fallback_to_default : bool
1187 If True, will fallback to the default font family (usually
1188 "DejaVu Sans" or "Helvetica") if the first lookup hard-fails.
1189 rebuild_if_missing : bool
1190 Whether to rebuild the font cache and search again if no match
1191 is found.
1193 Returns
1194 -------
1195 fontfile : str
1196 The filename of the best matching font.
1198 Notes
1199 -----
1200 This performs a nearest neighbor search. Each font is given a
1201 similarity score to the target font properties. The first font with
1202 the highest score is returned. If no matches below a certain
1203 threshold are found, the default font (usually DejaVu Sans) is
1204 returned.
1206 The result is cached, so subsequent lookups don't have to
1207 perform the O(n) nearest neighbor search.
1209 See the `W3C Cascading Style Sheet, Level 1
1210 <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ documentation
1211 for a description of the font finding algorithm.
1213 .. _fontconfig patterns:
1214 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
1216 """
1217 # Pass the relevant rcParams (and the font manager, as `self`) to
1218 # _findfont_cached so to prevent using a stale cache entry after an
1219 # rcParam was changed.
1220 rc_params = tuple(tuple(rcParams[key]) for key in [
1221 "font.serif", "font.sans-serif", "font.cursive", "font.fantasy",
1222 "font.monospace"])
1223 return self._findfont_cached(
1224 prop, fontext, directory, fallback_to_default, rebuild_if_missing,
1225 rc_params)
1227 @lru_cache()
1228 def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
1229 rebuild_if_missing, rc_params):
1231 if not isinstance(prop, FontProperties):
1232 prop = FontProperties(prop)
1234 fname = prop.get_file()
1235 if fname is not None:
1236 return fname
1238 if fontext == 'afm':
1239 fontlist = self.afmlist
1240 else:
1241 fontlist = self.ttflist
1243 best_score = 1e64
1244 best_font = None
1246 _log.debug('findfont: Matching %s.', prop)
1247 for font in fontlist:
1248 if (directory is not None and
1249 Path(directory) not in Path(font.fname).parents):
1250 continue
1251 # Matching family should have top priority, so multiply it by 10.
1252 score = (self.score_family(prop.get_family(), font.name) * 10
1253 + self.score_style(prop.get_style(), font.style)
1254 + self.score_variant(prop.get_variant(), font.variant)
1255 + self.score_weight(prop.get_weight(), font.weight)
1256 + self.score_stretch(prop.get_stretch(), font.stretch)
1257 + self.score_size(prop.get_size(), font.size))
1258 _log.debug('findfont: score(%s) = %s', font, score)
1259 if score < best_score:
1260 best_score = score
1261 best_font = font
1262 if score == 0:
1263 break
1265 if best_font is None or best_score >= 10.0:
1266 if fallback_to_default:
1267 _log.warning(
1268 'findfont: Font family %s not found. Falling back to %s.',
1269 prop.get_family(), self.defaultFamily[fontext])
1270 default_prop = prop.copy()
1271 default_prop.set_family(self.defaultFamily[fontext])
1272 return self.findfont(default_prop, fontext, directory, False)
1273 else:
1274 # This is a hard fail -- we can't find anything reasonable,
1275 # so just return the DejaVuSans.ttf
1276 _log.warning('findfont: Could not match %s. Returning %s.',
1277 prop, self.defaultFont[fontext])
1278 result = self.defaultFont[fontext]
1279 else:
1280 _log.debug('findfont: Matching %s to %s (%r) with score of %f.',
1281 prop, best_font.name, best_font.fname, best_score)
1282 result = best_font.fname
1284 if not os.path.isfile(result):
1285 if rebuild_if_missing:
1286 _log.info(
1287 'findfont: Found a missing font file. Rebuilding cache.')
1288 _rebuild()
1289 return fontManager.findfont(
1290 prop, fontext, directory, True, False)
1291 else:
1292 raise ValueError("No valid font could be found")
1294 return result
1297@lru_cache()
1298def is_opentype_cff_font(filename):
1299 """
1300 Return whether the given font is a Postscript Compact Font Format Font
1301 embedded in an OpenType wrapper. Used by the PostScript and PDF backends
1302 that can not subset these fonts.
1303 """
1304 if os.path.splitext(filename)[1].lower() == '.otf':
1305 with open(filename, 'rb') as fd:
1306 return fd.read(4) == b"OTTO"
1307 else:
1308 return False
1311_fmcache = os.path.join(
1312 mpl.get_cachedir(), 'fontlist-v{}.json'.format(FontManager.__version__))
1313fontManager = None
1316_get_font = lru_cache(64)(ft2font.FT2Font)
1317# FT2Font objects cannot be used across fork()s because they reference the same
1318# FT_Library object. While invalidating *all* existing FT2Fonts after a fork
1319# would be too complicated to be worth it, the main way FT2Fonts get reused is
1320# via the cache of _get_font, which we can empty upon forking (in Py3.7+).
1321if hasattr(os, "register_at_fork"):
1322 os.register_at_fork(after_in_child=_get_font.cache_clear)
1325def get_font(filename, hinting_factor=None):
1326 if hinting_factor is None:
1327 hinting_factor = rcParams['text.hinting_factor']
1328 return _get_font(os.fspath(filename), hinting_factor,
1329 _kerning_factor=rcParams['text.kerning_factor'])
1332def _rebuild():
1333 global fontManager
1334 fontManager = FontManager()
1335 with cbook._lock_path(_fmcache):
1336 json_dump(fontManager, _fmcache)
1337 _log.info("generated new fontManager")
1340try:
1341 fontManager = json_load(_fmcache)
1342except Exception:
1343 _rebuild()
1344else:
1345 if getattr(fontManager, '_version', object()) != FontManager.__version__:
1346 _rebuild()
1347 else:
1348 _log.debug("Using fontManager instance from %s", _fmcache)
1351findfont = fontManager.findfont