123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584 |
- """
- A module for finding, managing, and using fonts across platforms.
- This module provides a single `FontManager` instance, ``fontManager``, that can
- be shared across backends and platforms. The `findfont`
- function returns the best TrueType (TTF) font file in the local or
- system font path that matches the specified `FontProperties`
- instance. The `FontManager` also handles Adobe Font Metrics
- (AFM) font files for use by the PostScript backend.
- The `FontManager.addfont` function adds a custom font from a file without
- installing it into your operating system.
- The design is based on the `W3C Cascading Style Sheet, Level 1 (CSS1)
- font specification <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_.
- Future versions may implement the Level 2 or 2.1 specifications.
- """
- # KNOWN ISSUES
- #
- # - documentation
- # - font variant is untested
- # - font stretch is incomplete
- # - font size is incomplete
- # - default font algorithm needs improvement and testing
- # - setWeights function needs improvement
- # - 'light' is an invalid weight value, remove it.
- from base64 import b64encode
- from collections import namedtuple
- import copy
- import dataclasses
- from functools import lru_cache
- from io import BytesIO
- import json
- import logging
- from numbers import Number
- import os
- from pathlib import Path
- import re
- import subprocess
- import sys
- import threading
- from typing import Union
- import matplotlib as mpl
- from matplotlib import _api, _afm, cbook, ft2font
- from matplotlib._fontconfig_pattern import (
- parse_fontconfig_pattern, generate_fontconfig_pattern)
- from matplotlib.rcsetup import _validators
- _log = logging.getLogger(__name__)
- font_scalings = {
- 'xx-small': 0.579,
- 'x-small': 0.694,
- 'small': 0.833,
- 'medium': 1.0,
- 'large': 1.200,
- 'x-large': 1.440,
- 'xx-large': 1.728,
- 'larger': 1.2,
- 'smaller': 0.833,
- None: 1.0,
- }
- stretch_dict = {
- 'ultra-condensed': 100,
- 'extra-condensed': 200,
- 'condensed': 300,
- 'semi-condensed': 400,
- 'normal': 500,
- 'semi-expanded': 600,
- 'semi-extended': 600,
- 'expanded': 700,
- 'extended': 700,
- 'extra-expanded': 800,
- 'extra-extended': 800,
- 'ultra-expanded': 900,
- 'ultra-extended': 900,
- }
- weight_dict = {
- 'ultralight': 100,
- 'light': 200,
- 'normal': 400,
- 'regular': 400,
- 'book': 400,
- 'medium': 500,
- 'roman': 500,
- 'semibold': 600,
- 'demibold': 600,
- 'demi': 600,
- 'bold': 700,
- 'heavy': 800,
- 'extra bold': 800,
- 'black': 900,
- }
- _weight_regexes = [
- # From fontconfig's FcFreeTypeQueryFaceInternal; not the same as
- # weight_dict!
- ("thin", 100),
- ("extralight", 200),
- ("ultralight", 200),
- ("demilight", 350),
- ("semilight", 350),
- ("light", 300), # Needs to come *after* demi/semilight!
- ("book", 380),
- ("regular", 400),
- ("normal", 400),
- ("medium", 500),
- ("demibold", 600),
- ("demi", 600),
- ("semibold", 600),
- ("extrabold", 800),
- ("superbold", 800),
- ("ultrabold", 800),
- ("bold", 700), # Needs to come *after* extra/super/ultrabold!
- ("ultrablack", 1000),
- ("superblack", 1000),
- ("extrablack", 1000),
- (r"\bultra", 1000),
- ("black", 900), # Needs to come *after* ultra/super/extrablack!
- ("heavy", 900),
- ]
- font_family_aliases = {
- 'serif',
- 'sans-serif',
- 'sans serif',
- 'cursive',
- 'fantasy',
- 'monospace',
- 'sans',
- }
- _ExceptionProxy = namedtuple('_ExceptionProxy', ['klass', 'message'])
- # OS Font paths
- try:
- _HOME = Path.home()
- except Exception: # Exceptions thrown by home() are not specified...
- _HOME = Path(os.devnull) # Just an arbitrary path with no children.
- MSFolders = \
- r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
- MSFontDirectories = [
- r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts',
- r'SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts']
- MSUserFontDirectories = [
- str(_HOME / 'AppData/Local/Microsoft/Windows/Fonts'),
- str(_HOME / 'AppData/Roaming/Microsoft/Windows/Fonts'),
- ]
- X11FontDirectories = [
- # an old standard installation point
- "/usr/X11R6/lib/X11/fonts/TTF/",
- "/usr/X11/lib/X11/fonts",
- # here is the new standard location for fonts
- "/usr/share/fonts/",
- # documented as a good place to install new fonts
- "/usr/local/share/fonts/",
- # common application, not really useful
- "/usr/lib/openoffice/share/fonts/truetype/",
- # user fonts
- str((Path(os.environ.get('XDG_DATA_HOME') or _HOME / ".local/share"))
- / "fonts"),
- str(_HOME / ".fonts"),
- ]
- OSXFontDirectories = [
- "/Library/Fonts/",
- "/Network/Library/Fonts/",
- "/System/Library/Fonts/",
- # fonts installed via MacPorts
- "/opt/local/share/fonts",
- # user fonts
- str(_HOME / "Library/Fonts"),
- ]
- def get_fontext_synonyms(fontext):
- """
- Return a list of file extensions that are synonyms for
- the given file extension *fileext*.
- """
- return {
- 'afm': ['afm'],
- 'otf': ['otf', 'ttc', 'ttf'],
- 'ttc': ['otf', 'ttc', 'ttf'],
- 'ttf': ['otf', 'ttc', 'ttf'],
- }[fontext]
- def list_fonts(directory, extensions):
- """
- Return a list of all fonts matching any of the extensions, found
- recursively under the directory.
- """
- extensions = ["." + ext for ext in extensions]
- return [os.path.join(dirpath, filename)
- # os.walk ignores access errors, unlike Path.glob.
- for dirpath, _, filenames in os.walk(directory)
- for filename in filenames
- if Path(filename).suffix.lower() in extensions]
- def win32FontDirectory():
- r"""
- Return the user-specified font directory for Win32. This is
- looked up from the registry key ::
- \\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\Fonts
- If the key is not found, ``%WINDIR%\Fonts`` will be returned.
- """
- import winreg
- try:
- with winreg.OpenKey(winreg.HKEY_CURRENT_USER, MSFolders) as user:
- return winreg.QueryValueEx(user, 'Fonts')[0]
- except OSError:
- return os.path.join(os.environ['WINDIR'], 'Fonts')
- def _get_win32_installed_fonts():
- """List the font paths known to the Windows registry."""
- import winreg
- items = set()
- # Search and resolve fonts listed in the registry.
- for domain, base_dirs in [
- (winreg.HKEY_LOCAL_MACHINE, [win32FontDirectory()]), # System.
- (winreg.HKEY_CURRENT_USER, MSUserFontDirectories), # User.
- ]:
- for base_dir in base_dirs:
- for reg_path in MSFontDirectories:
- try:
- with winreg.OpenKey(domain, reg_path) as local:
- for j in range(winreg.QueryInfoKey(local)[1]):
- # value may contain the filename of the font or its
- # absolute path.
- key, value, tp = winreg.EnumValue(local, j)
- if not isinstance(value, str):
- continue
- try:
- # If value contains already an absolute path,
- # then it is not changed further.
- path = Path(base_dir, value).resolve()
- except RuntimeError:
- # Don't fail with invalid entries.
- continue
- items.add(path)
- except (OSError, MemoryError):
- continue
- return items
- @lru_cache
- def _get_fontconfig_fonts():
- """Cache and list the font paths known to ``fc-list``."""
- try:
- if b'--format' not in subprocess.check_output(['fc-list', '--help']):
- _log.warning( # fontconfig 2.7 implemented --format.
- 'Matplotlib needs fontconfig>=2.7 to query system fonts.')
- return []
- out = subprocess.check_output(['fc-list', '--format=%{file}\\n'])
- except (OSError, subprocess.CalledProcessError):
- return []
- return [Path(os.fsdecode(fname)) for fname in out.split(b'\n')]
- def findSystemFonts(fontpaths=None, fontext='ttf'):
- """
- Search for fonts in the specified font paths. If no paths are
- given, will use a standard set of system paths, as well as the
- list of fonts tracked by fontconfig if fontconfig is installed and
- available. A list of TrueType fonts are returned by default with
- AFM fonts as an option.
- """
- fontfiles = set()
- fontexts = get_fontext_synonyms(fontext)
- if fontpaths is None:
- if sys.platform == 'win32':
- installed_fonts = _get_win32_installed_fonts()
- fontpaths = []
- else:
- installed_fonts = _get_fontconfig_fonts()
- if sys.platform == 'darwin':
- fontpaths = [*X11FontDirectories, *OSXFontDirectories]
- else:
- fontpaths = X11FontDirectories
- fontfiles.update(str(path) for path in installed_fonts
- if path.suffix.lower()[1:] in fontexts)
- elif isinstance(fontpaths, str):
- fontpaths = [fontpaths]
- for path in fontpaths:
- fontfiles.update(map(os.path.abspath, list_fonts(path, fontexts)))
- return [fname for fname in fontfiles if os.path.exists(fname)]
- def _fontentry_helper_repr_png(fontent):
- from matplotlib.figure import Figure # Circular import.
- fig = Figure()
- font_path = Path(fontent.fname) if fontent.fname != '' else None
- fig.text(0, 0, fontent.name, font=font_path)
- with BytesIO() as buf:
- fig.savefig(buf, bbox_inches='tight', transparent=True)
- return buf.getvalue()
- def _fontentry_helper_repr_html(fontent):
- png_stream = _fontentry_helper_repr_png(fontent)
- png_b64 = b64encode(png_stream).decode()
- return f"<img src=\"data:image/png;base64, {png_b64}\" />"
- FontEntry = dataclasses.make_dataclass(
- 'FontEntry', [
- ('fname', str, dataclasses.field(default='')),
- ('name', str, dataclasses.field(default='')),
- ('style', str, dataclasses.field(default='normal')),
- ('variant', str, dataclasses.field(default='normal')),
- ('weight', Union[str, int], dataclasses.field(default='normal')),
- ('stretch', str, dataclasses.field(default='normal')),
- ('size', str, dataclasses.field(default='medium')),
- ],
- namespace={
- '__doc__': """
- A class for storing Font properties.
- It is used when populating the font lookup dictionary.
- """,
- '_repr_html_': lambda self: _fontentry_helper_repr_html(self),
- '_repr_png_': lambda self: _fontentry_helper_repr_png(self),
- }
- )
- def ttfFontProperty(font):
- """
- Extract information from a TrueType font file.
- Parameters
- ----------
- font : `.FT2Font`
- The TrueType font file from which information will be extracted.
- Returns
- -------
- `FontEntry`
- The extracted font properties.
- """
- name = font.family_name
- # Styles are: italic, oblique, and normal (default)
- sfnt = font.get_sfnt()
- mac_key = (1, # platform: macintosh
- 0, # id: roman
- 0) # langid: english
- ms_key = (3, # platform: microsoft
- 1, # id: unicode_cs
- 0x0409) # langid: english_united_states
- # These tables are actually mac_roman-encoded, but mac_roman support may be
- # missing in some alternative Python implementations and we are only going
- # to look for ASCII substrings, where any ASCII-compatible encoding works
- # - or big-endian UTF-16, since important Microsoft fonts use that.
- sfnt2 = (sfnt.get((*mac_key, 2), b'').decode('latin-1').lower() or
- sfnt.get((*ms_key, 2), b'').decode('utf_16_be').lower())
- sfnt4 = (sfnt.get((*mac_key, 4), b'').decode('latin-1').lower() or
- sfnt.get((*ms_key, 4), b'').decode('utf_16_be').lower())
- if sfnt4.find('oblique') >= 0:
- style = 'oblique'
- elif sfnt4.find('italic') >= 0:
- style = 'italic'
- elif sfnt2.find('regular') >= 0:
- style = 'normal'
- elif font.style_flags & ft2font.ITALIC:
- style = 'italic'
- else:
- style = 'normal'
- # Variants are: small-caps and normal (default)
- # !!!! Untested
- if name.lower() in ['capitals', 'small-caps']:
- variant = 'small-caps'
- else:
- variant = 'normal'
- # The weight-guessing algorithm is directly translated from fontconfig
- # 2.13.1's FcFreeTypeQueryFaceInternal (fcfreetype.c).
- wws_subfamily = 22
- typographic_subfamily = 16
- font_subfamily = 2
- styles = [
- sfnt.get((*mac_key, wws_subfamily), b'').decode('latin-1'),
- sfnt.get((*mac_key, typographic_subfamily), b'').decode('latin-1'),
- sfnt.get((*mac_key, font_subfamily), b'').decode('latin-1'),
- sfnt.get((*ms_key, wws_subfamily), b'').decode('utf-16-be'),
- sfnt.get((*ms_key, typographic_subfamily), b'').decode('utf-16-be'),
- sfnt.get((*ms_key, font_subfamily), b'').decode('utf-16-be'),
- ]
- styles = [*filter(None, styles)] or [font.style_name]
- def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal.
- # OS/2 table weight.
- os2 = font.get_sfnt_table("OS/2")
- if os2 and os2["version"] != 0xffff:
- return os2["usWeightClass"]
- # PostScript font info weight.
- try:
- ps_font_info_weight = (
- font.get_ps_font_info()["weight"].replace(" ", "") or "")
- except ValueError:
- pass
- else:
- for regex, weight in _weight_regexes:
- if re.fullmatch(regex, ps_font_info_weight, re.I):
- return weight
- # Style name weight.
- for style in styles:
- style = style.replace(" ", "")
- for regex, weight in _weight_regexes:
- if re.search(regex, style, re.I):
- return weight
- if font.style_flags & ft2font.BOLD:
- return 700 # "bold"
- return 500 # "medium", not "regular"!
- weight = int(get_weight())
- # Stretch can be absolute and relative
- # Absolute stretches are: ultra-condensed, extra-condensed, condensed,
- # semi-condensed, normal, semi-expanded, expanded, extra-expanded,
- # and ultra-expanded.
- # Relative stretches are: wider, narrower
- # Child value is: inherit
- if any(word in sfnt4 for word in ['narrow', 'condensed', 'cond']):
- stretch = 'condensed'
- elif 'demi cond' in sfnt4:
- stretch = 'semi-condensed'
- elif any(word in sfnt4 for word in ['wide', 'expanded', 'extended']):
- stretch = 'expanded'
- else:
- stretch = 'normal'
- # Sizes can be absolute and relative.
- # Absolute sizes are: xx-small, x-small, small, medium, large, x-large,
- # and xx-large.
- # Relative sizes are: larger, smaller
- # Length value is an absolute font size, e.g., 12pt
- # Percentage values are in 'em's. Most robust specification.
- if not font.scalable:
- raise NotImplementedError("Non-scalable fonts are not supported")
- size = 'scalable'
- return FontEntry(font.fname, name, style, variant, weight, stretch, size)
- def afmFontProperty(fontpath, font):
- """
- Extract information from an AFM font file.
- Parameters
- ----------
- fontpath : str
- The filename corresponding to *font*.
- font : AFM
- The AFM font file from which information will be extracted.
- Returns
- -------
- `FontEntry`
- The extracted font properties.
- """
- name = font.get_familyname()
- fontname = font.get_fontname().lower()
- # Styles are: italic, oblique, and normal (default)
- if font.get_angle() != 0 or 'italic' in name.lower():
- style = 'italic'
- elif 'oblique' in name.lower():
- style = 'oblique'
- else:
- style = 'normal'
- # Variants are: small-caps and normal (default)
- # !!!! Untested
- if name.lower() in ['capitals', 'small-caps']:
- variant = 'small-caps'
- else:
- variant = 'normal'
- weight = font.get_weight().lower()
- if weight not in weight_dict:
- weight = 'normal'
- # Stretch can be absolute and relative
- # Absolute stretches are: ultra-condensed, extra-condensed, condensed,
- # semi-condensed, normal, semi-expanded, expanded, extra-expanded,
- # and ultra-expanded.
- # Relative stretches are: wider, narrower
- # Child value is: inherit
- if 'demi cond' in fontname:
- stretch = 'semi-condensed'
- elif any(word in fontname for word in ['narrow', 'cond']):
- stretch = 'condensed'
- elif any(word in fontname for word in ['wide', 'expanded', 'extended']):
- stretch = 'expanded'
- else:
- stretch = 'normal'
- # Sizes can be absolute and relative.
- # Absolute sizes are: xx-small, x-small, small, medium, large, x-large,
- # and xx-large.
- # Relative sizes are: larger, smaller
- # Length value is an absolute font size, e.g., 12pt
- # Percentage values are in 'em's. Most robust specification.
- # All AFM fonts are apparently scalable.
- size = 'scalable'
- return FontEntry(fontpath, name, style, variant, weight, stretch, size)
- class FontProperties:
- """
- A class for storing and manipulating font properties.
- The font properties are the six properties described in the
- `W3C Cascading Style Sheet, Level 1
- <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ font
- specification and *math_fontfamily* for math fonts:
- - family: A list of font names in decreasing order of priority.
- The items may include a generic font family name, either 'sans-serif',
- 'serif', 'cursive', 'fantasy', or 'monospace'. In that case, the actual
- font to be used will be looked up from the associated rcParam during the
- search process in `.findfont`. Default: :rc:`font.family`
- - style: Either 'normal', 'italic' or 'oblique'.
- Default: :rc:`font.style`
- - variant: Either 'normal' or 'small-caps'.
- Default: :rc:`font.variant`
- - stretch: A numeric value in the range 0-1000 or one of
- 'ultra-condensed', 'extra-condensed', 'condensed',
- 'semi-condensed', 'normal', 'semi-expanded', 'expanded',
- 'extra-expanded' or 'ultra-expanded'. Default: :rc:`font.stretch`
- - weight: A numeric value in the range 0-1000 or one of
- 'ultralight', 'light', 'normal', 'regular', 'book', 'medium',
- 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy',
- 'extra bold', 'black'. Default: :rc:`font.weight`
- - size: Either a relative value of 'xx-small', 'x-small',
- 'small', 'medium', 'large', 'x-large', 'xx-large' or an
- absolute font size, e.g., 10. Default: :rc:`font.size`
- - math_fontfamily: The family of fonts used to render math text.
- Supported values are: 'dejavusans', 'dejavuserif', 'cm',
- 'stix', 'stixsans' and 'custom'. Default: :rc:`mathtext.fontset`
- Alternatively, a font may be specified using the absolute path to a font
- file, by using the *fname* kwarg. However, in this case, it is typically
- simpler to just pass the path (as a `pathlib.Path`, not a `str`) to the
- *font* kwarg of the `.Text` object.
- The preferred usage of font sizes is to use the relative values,
- e.g., 'large', instead of absolute font sizes, e.g., 12. This
- approach allows all text sizes to be made larger or smaller based
- on the font manager's default font size.
- This class will also accept a fontconfig_ pattern_, if it is the only
- argument provided. This support does not depend on fontconfig; we are
- merely borrowing its pattern syntax for use here.
- .. _fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/
- .. _pattern:
- https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
- Note that Matplotlib's internal font manager and fontconfig use a
- different algorithm to lookup fonts, so the results of the same pattern
- may be different in Matplotlib than in other applications that use
- fontconfig.
- """
- def __init__(self, family=None, style=None, variant=None, weight=None,
- stretch=None, size=None,
- fname=None, # if set, it's a hardcoded filename to use
- math_fontfamily=None):
- self.set_family(family)
- self.set_style(style)
- self.set_variant(variant)
- self.set_weight(weight)
- self.set_stretch(stretch)
- self.set_file(fname)
- self.set_size(size)
- self.set_math_fontfamily(math_fontfamily)
- # Treat family as a fontconfig pattern if it is the only parameter
- # provided. Even in that case, call the other setters first to set
- # attributes not specified by the pattern to the rcParams defaults.
- if (isinstance(family, str)
- and style is None and variant is None and weight is None
- and stretch is None and size is None and fname is None):
- self.set_fontconfig_pattern(family)
- @classmethod
- def _from_any(cls, arg):
- """
- Generic constructor which can build a `.FontProperties` from any of the
- following:
- - a `.FontProperties`: it is passed through as is;
- - `None`: a `.FontProperties` using rc values is used;
- - an `os.PathLike`: it is used as path to the font file;
- - a `str`: it is parsed as a fontconfig pattern;
- - a `dict`: it is passed as ``**kwargs`` to `.FontProperties`.
- """
- if arg is None:
- return cls()
- elif isinstance(arg, cls):
- return arg
- elif isinstance(arg, os.PathLike):
- return cls(fname=arg)
- elif isinstance(arg, str):
- return cls(arg)
- else:
- return cls(**arg)
- def __hash__(self):
- l = (tuple(self.get_family()),
- self.get_slant(),
- self.get_variant(),
- self.get_weight(),
- self.get_stretch(),
- self.get_size(),
- self.get_file(),
- self.get_math_fontfamily())
- return hash(l)
- def __eq__(self, other):
- return hash(self) == hash(other)
- def __str__(self):
- return self.get_fontconfig_pattern()
- def get_family(self):
- """
- Return a list of individual font family names or generic family names.
- The font families or generic font families (which will be resolved
- from their respective rcParams when searching for a matching font) in
- the order of preference.
- """
- return self._family
- def get_name(self):
- """
- Return the name of the font that best matches the font properties.
- """
- return get_font(findfont(self)).family_name
- def get_style(self):
- """
- Return the font style. Values are: 'normal', 'italic' or 'oblique'.
- """
- return self._slant
- def get_variant(self):
- """
- Return the font variant. Values are: 'normal' or 'small-caps'.
- """
- return self._variant
- def get_weight(self):
- """
- Set the font weight. Options are: A numeric value in the
- range 0-1000 or one of 'light', 'normal', 'regular', 'book',
- 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold',
- 'heavy', 'extra bold', 'black'
- """
- return self._weight
- def get_stretch(self):
- """
- Return the font stretch or width. Options are: 'ultra-condensed',
- 'extra-condensed', 'condensed', 'semi-condensed', 'normal',
- 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'.
- """
- return self._stretch
- def get_size(self):
- """
- Return the font size.
- """
- return self._size
- def get_file(self):
- """
- Return the filename of the associated font.
- """
- return self._file
- def get_fontconfig_pattern(self):
- """
- Get a fontconfig_ pattern_ suitable for looking up the font as
- specified with fontconfig's ``fc-match`` utility.
- This support does not depend on fontconfig; we are merely borrowing its
- pattern syntax for use here.
- """
- return generate_fontconfig_pattern(self)
- def set_family(self, family):
- """
- Change the font family. Can be either an alias (generic name
- is CSS parlance), such as: 'serif', 'sans-serif', 'cursive',
- 'fantasy', or 'monospace', a real font name or a list of real
- font names. Real font names are not supported when
- :rc:`text.usetex` is `True`. Default: :rc:`font.family`
- """
- if family is None:
- family = mpl.rcParams['font.family']
- if isinstance(family, str):
- family = [family]
- self._family = family
- def set_style(self, style):
- """
- Set the font style.
- Parameters
- ----------
- style : {'normal', 'italic', 'oblique'}, default: :rc:`font.style`
- """
- if style is None:
- style = mpl.rcParams['font.style']
- _api.check_in_list(['normal', 'italic', 'oblique'], style=style)
- self._slant = style
- def set_variant(self, variant):
- """
- Set the font variant.
- Parameters
- ----------
- variant : {'normal', 'small-caps'}, default: :rc:`font.variant`
- """
- if variant is None:
- variant = mpl.rcParams['font.variant']
- _api.check_in_list(['normal', 'small-caps'], variant=variant)
- self._variant = variant
- def set_weight(self, weight):
- """
- Set the font weight.
- Parameters
- ----------
- weight : int or {'ultralight', 'light', 'normal', 'regular', 'book', \
- 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', \
- 'extra bold', 'black'}, default: :rc:`font.weight`
- If int, must be in the range 0-1000.
- """
- if weight is None:
- weight = mpl.rcParams['font.weight']
- if weight in weight_dict:
- self._weight = weight
- return
- try:
- weight = int(weight)
- except ValueError:
- pass
- else:
- if 0 <= weight <= 1000:
- self._weight = weight
- return
- raise ValueError(f"{weight=} is invalid")
- def set_stretch(self, stretch):
- """
- Set the font stretch or width.
- Parameters
- ----------
- stretch : int or {'ultra-condensed', 'extra-condensed', 'condensed', \
- 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', \
- 'ultra-expanded'}, default: :rc:`font.stretch`
- If int, must be in the range 0-1000.
- """
- if stretch is None:
- stretch = mpl.rcParams['font.stretch']
- if stretch in stretch_dict:
- self._stretch = stretch
- return
- try:
- stretch = int(stretch)
- except ValueError:
- pass
- else:
- if 0 <= stretch <= 1000:
- self._stretch = stretch
- return
- raise ValueError(f"{stretch=} is invalid")
- def set_size(self, size):
- """
- Set the font size.
- Parameters
- ----------
- size : float or {'xx-small', 'x-small', 'small', 'medium', \
- 'large', 'x-large', 'xx-large'}, default: :rc:`font.size`
- If a float, the font size in points. The string values denote
- sizes relative to the default font size.
- """
- if size is None:
- size = mpl.rcParams['font.size']
- try:
- size = float(size)
- except ValueError:
- try:
- scale = font_scalings[size]
- except KeyError as err:
- raise ValueError(
- "Size is invalid. Valid font size are "
- + ", ".join(map(str, font_scalings))) from err
- else:
- size = scale * FontManager.get_default_size()
- if size < 1.0:
- _log.info('Fontsize %1.2f < 1.0 pt not allowed by FreeType. '
- 'Setting fontsize = 1 pt', size)
- size = 1.0
- self._size = size
- def set_file(self, file):
- """
- Set the filename of the fontfile to use. In this case, all
- other properties will be ignored.
- """
- self._file = os.fspath(file) if file is not None else None
- def set_fontconfig_pattern(self, pattern):
- """
- Set the properties by parsing a fontconfig_ *pattern*.
- This support does not depend on fontconfig; we are merely borrowing its
- pattern syntax for use here.
- """
- for key, val in parse_fontconfig_pattern(pattern).items():
- if type(val) is list:
- getattr(self, "set_" + key)(val[0])
- else:
- getattr(self, "set_" + key)(val)
- def get_math_fontfamily(self):
- """
- Return the name of the font family used for math text.
- The default font is :rc:`mathtext.fontset`.
- """
- return self._math_fontfamily
- def set_math_fontfamily(self, fontfamily):
- """
- Set the font family for text in math mode.
- If not set explicitly, :rc:`mathtext.fontset` will be used.
- Parameters
- ----------
- fontfamily : str
- The name of the font family.
- Available font families are defined in the
- :ref:`default matplotlibrc file <customizing-with-matplotlibrc-files>`.
- See Also
- --------
- .text.Text.get_math_fontfamily
- """
- if fontfamily is None:
- fontfamily = mpl.rcParams['mathtext.fontset']
- else:
- valid_fonts = _validators['mathtext.fontset'].valid.values()
- # _check_in_list() Validates the parameter math_fontfamily as
- # if it were passed to rcParams['mathtext.fontset']
- _api.check_in_list(valid_fonts, math_fontfamily=fontfamily)
- self._math_fontfamily = fontfamily
- def copy(self):
- """Return a copy of self."""
- return copy.copy(self)
- # Aliases
- set_name = set_family
- get_slant = get_style
- set_slant = set_style
- get_size_in_points = get_size
- class _JSONEncoder(json.JSONEncoder):
- def default(self, o):
- if isinstance(o, FontManager):
- return dict(o.__dict__, __class__='FontManager')
- elif isinstance(o, FontEntry):
- d = dict(o.__dict__, __class__='FontEntry')
- try:
- # Cache paths of fonts shipped with Matplotlib relative to the
- # Matplotlib data path, which helps in the presence of venvs.
- d["fname"] = str(
- Path(d["fname"]).relative_to(mpl.get_data_path()))
- except ValueError:
- pass
- return d
- else:
- return super().default(o)
- def _json_decode(o):
- cls = o.pop('__class__', None)
- if cls is None:
- return o
- elif cls == 'FontManager':
- r = FontManager.__new__(FontManager)
- r.__dict__.update(o)
- return r
- elif cls == 'FontEntry':
- r = FontEntry.__new__(FontEntry)
- r.__dict__.update(o)
- if not os.path.isabs(r.fname):
- r.fname = os.path.join(mpl.get_data_path(), r.fname)
- return r
- else:
- raise ValueError("Don't know how to deserialize __class__=%s" % cls)
- def json_dump(data, filename):
- """
- Dump `FontManager` *data* as JSON to the file named *filename*.
- See Also
- --------
- json_load
- Notes
- -----
- File paths that are children of the Matplotlib data path (typically, fonts
- shipped with Matplotlib) are stored relative to that data path (to remain
- valid across virtualenvs).
- This function temporarily locks the output file to prevent multiple
- processes from overwriting one another's output.
- """
- with cbook._lock_path(filename), open(filename, 'w') as fh:
- try:
- json.dump(data, fh, cls=_JSONEncoder, indent=2)
- except OSError as e:
- _log.warning('Could not save font_manager cache %s', e)
- def json_load(filename):
- """
- Load a `FontManager` from the JSON file named *filename*.
- See Also
- --------
- json_dump
- """
- with open(filename) as fh:
- return json.load(fh, object_hook=_json_decode)
- class FontManager:
- """
- On import, the `FontManager` singleton instance creates a list of ttf and
- afm fonts and caches their `FontProperties`. The `FontManager.findfont`
- method does a nearest neighbor search to find the font that most closely
- matches the specification. If no good enough match is found, the default
- font is returned.
- Fonts added with the `FontManager.addfont` method will not persist in the
- cache; therefore, `addfont` will need to be called every time Matplotlib is
- imported. This method should only be used if and when a font cannot be
- installed on your operating system by other means.
- Notes
- -----
- The `FontManager.addfont` method must be called on the global `FontManager`
- instance.
- Example usage::
- import matplotlib.pyplot as plt
- from matplotlib import font_manager
- font_dirs = ["/resources/fonts"] # The path to the custom font file.
- font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
- for font_file in font_files:
- font_manager.fontManager.addfont(font_file)
- """
- # Increment this version number whenever the font cache data
- # format or behavior has changed and requires an existing font
- # cache files to be rebuilt.
- __version__ = 330
- def __init__(self, size=None, weight='normal'):
- self._version = self.__version__
- self.__default_weight = weight
- self.default_size = size
- # Create list of font paths.
- paths = [cbook._get_data_path('fonts', subdir)
- for subdir in ['ttf', 'afm', 'pdfcorefonts']]
- _log.debug('font search path %s', paths)
- self.defaultFamily = {
- 'ttf': 'DejaVu Sans',
- 'afm': 'Helvetica'}
- self.afmlist = []
- self.ttflist = []
- # Delay the warning by 5s.
- timer = threading.Timer(5, lambda: _log.warning(
- 'Matplotlib is building the font cache; this may take a moment.'))
- timer.start()
- try:
- for fontext in ["afm", "ttf"]:
- for path in [*findSystemFonts(paths, fontext=fontext),
- *findSystemFonts(fontext=fontext)]:
- try:
- self.addfont(path)
- except OSError as exc:
- _log.info("Failed to open font file %s: %s", path, exc)
- except Exception as exc:
- _log.info("Failed to extract font properties from %s: "
- "%s", path, exc)
- finally:
- timer.cancel()
- def addfont(self, path):
- """
- Cache the properties of the font at *path* to make it available to the
- `FontManager`. The type of font is inferred from the path suffix.
- Parameters
- ----------
- path : str or path-like
- Notes
- -----
- This method is useful for adding a custom font without installing it in
- your operating system. See the `FontManager` singleton instance for
- usage and caveats about this function.
- """
- # Convert to string in case of a path as
- # afmFontProperty and FT2Font expect this
- path = os.fsdecode(path)
- if Path(path).suffix.lower() == ".afm":
- with open(path, "rb") as fh:
- font = _afm.AFM(fh)
- prop = afmFontProperty(path, font)
- self.afmlist.append(prop)
- else:
- font = ft2font.FT2Font(path)
- prop = ttfFontProperty(font)
- self.ttflist.append(prop)
- self._findfont_cached.cache_clear()
- @property
- def defaultFont(self):
- # Lazily evaluated (findfont then caches the result) to avoid including
- # the venv path in the json serialization.
- return {ext: self.findfont(family, fontext=ext)
- for ext, family in self.defaultFamily.items()}
- def get_default_weight(self):
- """
- Return the default font weight.
- """
- return self.__default_weight
- @staticmethod
- def get_default_size():
- """
- Return the default font size.
- """
- return mpl.rcParams['font.size']
- def set_default_weight(self, weight):
- """
- Set the default font weight. The initial value is 'normal'.
- """
- self.__default_weight = weight
- @staticmethod
- def _expand_aliases(family):
- if family in ('sans', 'sans serif'):
- family = 'sans-serif'
- return mpl.rcParams['font.' + family]
- # Each of the scoring functions below should return a value between
- # 0.0 (perfect match) and 1.0 (terrible match)
- def score_family(self, families, family2):
- """
- Return a match score between the list of font families in
- *families* and the font family name *family2*.
- An exact match at the head of the list returns 0.0.
- A match further down the list will return between 0 and 1.
- No match will return 1.0.
- """
- if not isinstance(families, (list, tuple)):
- families = [families]
- elif len(families) == 0:
- return 1.0
- family2 = family2.lower()
- step = 1 / len(families)
- for i, family1 in enumerate(families):
- family1 = family1.lower()
- if family1 in font_family_aliases:
- options = [*map(str.lower, self._expand_aliases(family1))]
- if family2 in options:
- idx = options.index(family2)
- return (i + (idx / len(options))) * step
- elif family1 == family2:
- # The score should be weighted by where in the
- # list the font was found.
- return i * step
- return 1.0
- def score_style(self, style1, style2):
- """
- Return a match score between *style1* and *style2*.
- An exact match returns 0.0.
- A match between 'italic' and 'oblique' returns 0.1.
- No match returns 1.0.
- """
- if style1 == style2:
- return 0.0
- elif (style1 in ('italic', 'oblique')
- and style2 in ('italic', 'oblique')):
- return 0.1
- return 1.0
- def score_variant(self, variant1, variant2):
- """
- Return a match score between *variant1* and *variant2*.
- An exact match returns 0.0, otherwise 1.0.
- """
- if variant1 == variant2:
- return 0.0
- else:
- return 1.0
- def score_stretch(self, stretch1, stretch2):
- """
- Return a match score between *stretch1* and *stretch2*.
- The result is the absolute value of the difference between the
- CSS numeric values of *stretch1* and *stretch2*, normalized
- between 0.0 and 1.0.
- """
- try:
- stretchval1 = int(stretch1)
- except ValueError:
- stretchval1 = stretch_dict.get(stretch1, 500)
- try:
- stretchval2 = int(stretch2)
- except ValueError:
- stretchval2 = stretch_dict.get(stretch2, 500)
- return abs(stretchval1 - stretchval2) / 1000.0
- def score_weight(self, weight1, weight2):
- """
- Return a match score between *weight1* and *weight2*.
- The result is 0.0 if both weight1 and weight 2 are given as strings
- and have the same value.
- Otherwise, the result is the absolute value of the difference between
- the CSS numeric values of *weight1* and *weight2*, normalized between
- 0.05 and 1.0.
- """
- # exact match of the weight names, e.g. weight1 == weight2 == "regular"
- if cbook._str_equal(weight1, weight2):
- return 0.0
- w1 = weight1 if isinstance(weight1, Number) else weight_dict[weight1]
- w2 = weight2 if isinstance(weight2, Number) else weight_dict[weight2]
- return 0.95 * (abs(w1 - w2) / 1000) + 0.05
- def score_size(self, size1, size2):
- """
- Return a match score between *size1* and *size2*.
- If *size2* (the size specified in the font file) is 'scalable', this
- function always returns 0.0, since any font size can be generated.
- Otherwise, the result is the absolute distance between *size1* and
- *size2*, normalized so that the usual range of font sizes (6pt -
- 72pt) will lie between 0.0 and 1.0.
- """
- if size2 == 'scalable':
- return 0.0
- # Size value should have already been
- try:
- sizeval1 = float(size1)
- except ValueError:
- sizeval1 = self.default_size * font_scalings[size1]
- try:
- sizeval2 = float(size2)
- except ValueError:
- return 1.0
- return abs(sizeval1 - sizeval2) / 72
- def findfont(self, prop, fontext='ttf', directory=None,
- fallback_to_default=True, rebuild_if_missing=True):
- """
- Find a font that most closely matches the given font properties.
- Parameters
- ----------
- prop : str or `~matplotlib.font_manager.FontProperties`
- The font properties to search for. This can be either a
- `.FontProperties` object or a string defining a
- `fontconfig patterns`_.
- fontext : {'ttf', 'afm'}, default: 'ttf'
- The extension of the font file:
- - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf)
- - 'afm': Adobe Font Metrics (.afm)
- directory : str, optional
- If given, only search this directory and its subdirectories.
- fallback_to_default : bool
- If True, will fall back to the default font family (usually
- "DejaVu Sans" or "Helvetica") if the first lookup hard-fails.
- rebuild_if_missing : bool
- Whether to rebuild the font cache and search again if the first
- match appears to point to a nonexisting font (i.e., the font cache
- contains outdated entries).
- Returns
- -------
- str
- The filename of the best matching font.
- Notes
- -----
- This performs a nearest neighbor search. Each font is given a
- similarity score to the target font properties. The first font with
- the highest score is returned. If no matches below a certain
- threshold are found, the default font (usually DejaVu Sans) is
- returned.
- The result is cached, so subsequent lookups don't have to
- perform the O(n) nearest neighbor search.
- See the `W3C Cascading Style Sheet, Level 1
- <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ documentation
- for a description of the font finding algorithm.
- .. _fontconfig patterns:
- https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
- """
- # Pass the relevant rcParams (and the font manager, as `self`) to
- # _findfont_cached so to prevent using a stale cache entry after an
- # rcParam was changed.
- rc_params = tuple(tuple(mpl.rcParams[key]) for key in [
- "font.serif", "font.sans-serif", "font.cursive", "font.fantasy",
- "font.monospace"])
- ret = self._findfont_cached(
- prop, fontext, directory, fallback_to_default, rebuild_if_missing,
- rc_params)
- if isinstance(ret, _ExceptionProxy):
- raise ret.klass(ret.message)
- return ret
- def get_font_names(self):
- """Return the list of available fonts."""
- return list({font.name for font in self.ttflist})
- def _find_fonts_by_props(self, prop, fontext='ttf', directory=None,
- fallback_to_default=True, rebuild_if_missing=True):
- """
- Find font families that most closely match the given properties.
- Parameters
- ----------
- prop : str or `~matplotlib.font_manager.FontProperties`
- The font properties to search for. This can be either a
- `.FontProperties` object or a string defining a
- `fontconfig patterns`_.
- fontext : {'ttf', 'afm'}, default: 'ttf'
- The extension of the font file:
- - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf)
- - 'afm': Adobe Font Metrics (.afm)
- directory : str, optional
- If given, only search this directory and its subdirectories.
- fallback_to_default : bool
- If True, will fall back to the default font family (usually
- "DejaVu Sans" or "Helvetica") if none of the families were found.
- rebuild_if_missing : bool
- Whether to rebuild the font cache and search again if the first
- match appears to point to a nonexisting font (i.e., the font cache
- contains outdated entries).
- Returns
- -------
- list[str]
- The paths of the fonts found
- Notes
- -----
- This is an extension/wrapper of the original findfont API, which only
- returns a single font for given font properties. Instead, this API
- returns a dict containing multiple fonts and their filepaths
- which closely match the given font properties. Since this internally
- uses the original API, there's no change to the logic of performing the
- nearest neighbor search. See `findfont` for more details.
- """
- prop = FontProperties._from_any(prop)
- fpaths = []
- for family in prop.get_family():
- cprop = prop.copy()
- cprop.set_family(family) # set current prop's family
- try:
- fpaths.append(
- self.findfont(
- cprop, fontext, directory,
- fallback_to_default=False, # don't fallback to default
- rebuild_if_missing=rebuild_if_missing,
- )
- )
- except ValueError:
- if family in font_family_aliases:
- _log.warning(
- "findfont: Generic family %r not found because "
- "none of the following families were found: %s",
- family, ", ".join(self._expand_aliases(family))
- )
- else:
- _log.warning("findfont: Font family %r not found.", family)
- # only add default family if no other font was found and
- # fallback_to_default is enabled
- if not fpaths:
- if fallback_to_default:
- dfamily = self.defaultFamily[fontext]
- cprop = prop.copy()
- cprop.set_family(dfamily)
- fpaths.append(
- self.findfont(
- cprop, fontext, directory,
- fallback_to_default=True,
- rebuild_if_missing=rebuild_if_missing,
- )
- )
- else:
- raise ValueError("Failed to find any font, and fallback "
- "to the default font was disabled")
- return fpaths
- @lru_cache(1024)
- def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
- rebuild_if_missing, rc_params):
- prop = FontProperties._from_any(prop)
- fname = prop.get_file()
- if fname is not None:
- return fname
- if fontext == 'afm':
- fontlist = self.afmlist
- else:
- fontlist = self.ttflist
- best_score = 1e64
- best_font = None
- _log.debug('findfont: Matching %s.', prop)
- for font in fontlist:
- if (directory is not None and
- Path(directory) not in Path(font.fname).parents):
- continue
- # Matching family should have top priority, so multiply it by 10.
- score = (self.score_family(prop.get_family(), font.name) * 10
- + self.score_style(prop.get_style(), font.style)
- + self.score_variant(prop.get_variant(), font.variant)
- + self.score_weight(prop.get_weight(), font.weight)
- + self.score_stretch(prop.get_stretch(), font.stretch)
- + self.score_size(prop.get_size(), font.size))
- _log.debug('findfont: score(%s) = %s', font, score)
- if score < best_score:
- best_score = score
- best_font = font
- if score == 0:
- break
- if best_font is None or best_score >= 10.0:
- if fallback_to_default:
- _log.warning(
- 'findfont: Font family %s not found. Falling back to %s.',
- prop.get_family(), self.defaultFamily[fontext])
- for family in map(str.lower, prop.get_family()):
- if family in font_family_aliases:
- _log.warning(
- "findfont: Generic family %r not found because "
- "none of the following families were found: %s",
- family, ", ".join(self._expand_aliases(family)))
- default_prop = prop.copy()
- default_prop.set_family(self.defaultFamily[fontext])
- return self.findfont(default_prop, fontext, directory,
- fallback_to_default=False)
- else:
- # This return instead of raise is intentional, as we wish to
- # cache that it was not found, which will not occur if it was
- # actually raised.
- return _ExceptionProxy(
- ValueError,
- f"Failed to find font {prop}, and fallback to the default font was disabled"
- )
- else:
- _log.debug('findfont: Matching %s to %s (%r) with score of %f.',
- prop, best_font.name, best_font.fname, best_score)
- result = best_font.fname
- if not os.path.isfile(result):
- if rebuild_if_missing:
- _log.info(
- 'findfont: Found a missing font file. Rebuilding cache.')
- new_fm = _load_fontmanager(try_read_cache=False)
- # Replace self by the new fontmanager, because users may have
- # a reference to this specific instance.
- # TODO: _load_fontmanager should really be (used by) a method
- # modifying the instance in place.
- vars(self).update(vars(new_fm))
- return self.findfont(
- prop, fontext, directory, rebuild_if_missing=False)
- else:
- # This return instead of raise is intentional, as we wish to
- # cache that it was not found, which will not occur if it was
- # actually raised.
- return _ExceptionProxy(ValueError, "No valid font could be found")
- return _cached_realpath(result)
- @lru_cache
- def is_opentype_cff_font(filename):
- """
- Return whether the given font is a Postscript Compact Font Format Font
- embedded in an OpenType wrapper. Used by the PostScript and PDF backends
- that cannot subset these fonts.
- """
- if os.path.splitext(filename)[1].lower() == '.otf':
- with open(filename, 'rb') as fd:
- return fd.read(4) == b"OTTO"
- else:
- return False
- @lru_cache(64)
- def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id):
- first_fontpath, *rest = font_filepaths
- return ft2font.FT2Font(
- first_fontpath, hinting_factor,
- _fallback_list=[
- ft2font.FT2Font(
- fpath, hinting_factor,
- _kerning_factor=_kerning_factor
- )
- for fpath in rest
- ],
- _kerning_factor=_kerning_factor
- )
- # FT2Font objects cannot be used across fork()s because they reference the same
- # FT_Library object. While invalidating *all* existing FT2Fonts after a fork
- # would be too complicated to be worth it, the main way FT2Fonts get reused is
- # via the cache of _get_font, which we can empty upon forking (not on Windows,
- # which has no fork() or register_at_fork()).
- if hasattr(os, "register_at_fork"):
- os.register_at_fork(after_in_child=_get_font.cache_clear)
- @lru_cache(64)
- def _cached_realpath(path):
- # Resolving the path avoids embedding the font twice in pdf/ps output if a
- # single font is selected using two different relative paths.
- return os.path.realpath(path)
- def get_font(font_filepaths, hinting_factor=None):
- """
- Get an `.ft2font.FT2Font` object given a list of file paths.
- Parameters
- ----------
- font_filepaths : Iterable[str, Path, bytes], str, Path, bytes
- Relative or absolute paths to the font files to be used.
- If a single string, bytes, or `pathlib.Path`, then it will be treated
- as a list with that entry only.
- If more than one filepath is passed, then the returned FT2Font object
- will fall back through the fonts, in the order given, to find a needed
- glyph.
- Returns
- -------
- `.ft2font.FT2Font`
- """
- if isinstance(font_filepaths, (str, Path, bytes)):
- paths = (_cached_realpath(font_filepaths),)
- else:
- paths = tuple(_cached_realpath(fname) for fname in font_filepaths)
- if hinting_factor is None:
- hinting_factor = mpl.rcParams['text.hinting_factor']
- return _get_font(
- # must be a tuple to be cached
- paths,
- hinting_factor,
- _kerning_factor=mpl.rcParams['text.kerning_factor'],
- # also key on the thread ID to prevent segfaults with multi-threading
- thread_id=threading.get_ident()
- )
- def _load_fontmanager(*, try_read_cache=True):
- fm_path = Path(
- mpl.get_cachedir(), f"fontlist-v{FontManager.__version__}.json")
- if try_read_cache:
- try:
- fm = json_load(fm_path)
- except Exception:
- pass
- else:
- if getattr(fm, "_version", object()) == FontManager.__version__:
- _log.debug("Using fontManager instance from %s", fm_path)
- return fm
- fm = FontManager()
- json_dump(fm, fm_path)
- _log.info("generated new fontManager")
- return fm
- fontManager = _load_fontmanager()
- findfont = fontManager.findfont
- get_font_names = fontManager.get_font_names
|