Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/font_manager.py: 43%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

579 statements  

1""" 

2A module for finding, managing, and using fonts across platforms. 

3 

4This module provides a single `FontManager` instance, ``fontManager``, that can 

5be shared across backends and platforms. The `findfont` 

6function returns the best TrueType (TTF) font file in the local or 

7system font path that matches the specified `FontProperties` 

8instance. The `FontManager` also handles Adobe Font Metrics 

9(AFM) font files for use by the PostScript backend. 

10The `FontManager.addfont` function adds a custom font from a file without 

11installing it into your operating system. 

12 

13The design is based on the `W3C Cascading Style Sheet, Level 1 (CSS1) 

14font specification <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_. 

15Future versions may implement the Level 2 or 2.1 specifications. 

16""" 

17 

18# KNOWN ISSUES 

19# 

20# - documentation 

21# - font variant is untested 

22# - font stretch is incomplete 

23# - font size is incomplete 

24# - default font algorithm needs improvement and testing 

25# - setWeights function needs improvement 

26# - 'light' is an invalid weight value, remove it. 

27 

28from __future__ import annotations 

29 

30from base64 import b64encode 

31from collections import namedtuple 

32import copy 

33import dataclasses 

34from functools import lru_cache 

35from io import BytesIO 

36import json 

37import logging 

38from numbers import Number 

39import os 

40from pathlib import Path 

41import plistlib 

42import re 

43import subprocess 

44import sys 

45import threading 

46 

47import matplotlib as mpl 

48from matplotlib import _api, _afm, cbook, ft2font 

49from matplotlib._fontconfig_pattern import ( 

50 parse_fontconfig_pattern, generate_fontconfig_pattern) 

51from matplotlib.rcsetup import _validators 

52 

53_log = logging.getLogger(__name__) 

54 

55font_scalings = { 

56 'xx-small': 0.579, 

57 'x-small': 0.694, 

58 'small': 0.833, 

59 'medium': 1.0, 

60 'large': 1.200, 

61 'x-large': 1.440, 

62 'xx-large': 1.728, 

63 'larger': 1.2, 

64 'smaller': 0.833, 

65 None: 1.0, 

66} 

67stretch_dict = { 

68 'ultra-condensed': 100, 

69 'extra-condensed': 200, 

70 'condensed': 300, 

71 'semi-condensed': 400, 

72 'normal': 500, 

73 'semi-expanded': 600, 

74 'semi-extended': 600, 

75 'expanded': 700, 

76 'extended': 700, 

77 'extra-expanded': 800, 

78 'extra-extended': 800, 

79 'ultra-expanded': 900, 

80 'ultra-extended': 900, 

81} 

82weight_dict = { 

83 'ultralight': 100, 

84 'light': 200, 

85 'normal': 400, 

86 'regular': 400, 

87 'book': 400, 

88 'medium': 500, 

89 'roman': 500, 

90 'semibold': 600, 

91 'demibold': 600, 

92 'demi': 600, 

93 'bold': 700, 

94 'heavy': 800, 

95 'extra bold': 800, 

96 'black': 900, 

97} 

98_weight_regexes = [ 

99 # From fontconfig's FcFreeTypeQueryFaceInternal; not the same as 

100 # weight_dict! 

101 ("thin", 100), 

102 ("extralight", 200), 

103 ("ultralight", 200), 

104 ("demilight", 350), 

105 ("semilight", 350), 

106 ("light", 300), # Needs to come *after* demi/semilight! 

107 ("book", 380), 

108 ("regular", 400), 

109 ("normal", 400), 

110 ("medium", 500), 

111 ("demibold", 600), 

112 ("demi", 600), 

113 ("semibold", 600), 

114 ("extrabold", 800), 

115 ("superbold", 800), 

116 ("ultrabold", 800), 

117 ("bold", 700), # Needs to come *after* extra/super/ultrabold! 

118 ("ultrablack", 1000), 

119 ("superblack", 1000), 

120 ("extrablack", 1000), 

121 (r"\bultra", 1000), 

122 ("black", 900), # Needs to come *after* ultra/super/extrablack! 

123 ("heavy", 900), 

124] 

125font_family_aliases = { 

126 'serif', 

127 'sans-serif', 

128 'sans serif', 

129 'cursive', 

130 'fantasy', 

131 'monospace', 

132 'sans', 

133} 

134 

135_ExceptionProxy = namedtuple('_ExceptionProxy', ['klass', 'message']) 

136 

137# OS Font paths 

138try: 

139 _HOME = Path.home() 

140except Exception: # Exceptions thrown by home() are not specified... 

141 _HOME = Path(os.devnull) # Just an arbitrary path with no children. 

142MSFolders = \ 

143 r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' 

144MSFontDirectories = [ 

145 r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts', 

146 r'SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts'] 

147MSUserFontDirectories = [ 

148 str(_HOME / 'AppData/Local/Microsoft/Windows/Fonts'), 

149 str(_HOME / 'AppData/Roaming/Microsoft/Windows/Fonts'), 

150] 

151X11FontDirectories = [ 

152 # an old standard installation point 

153 "/usr/X11R6/lib/X11/fonts/TTF/", 

154 "/usr/X11/lib/X11/fonts", 

155 # here is the new standard location for fonts 

156 "/usr/share/fonts/", 

157 # documented as a good place to install new fonts 

158 "/usr/local/share/fonts/", 

159 # common application, not really useful 

160 "/usr/lib/openoffice/share/fonts/truetype/", 

161 # user fonts 

162 str((Path(os.environ.get('XDG_DATA_HOME') or _HOME / ".local/share")) 

163 / "fonts"), 

164 str(_HOME / ".fonts"), 

165] 

166OSXFontDirectories = [ 

167 "/Library/Fonts/", 

168 "/Network/Library/Fonts/", 

169 "/System/Library/Fonts/", 

170 # fonts installed via MacPorts 

171 "/opt/local/share/fonts", 

172 # user fonts 

173 str(_HOME / "Library/Fonts"), 

174] 

175 

176 

177def get_fontext_synonyms(fontext): 

178 """ 

179 Return a list of file extensions that are synonyms for 

180 the given file extension *fileext*. 

181 """ 

182 return { 

183 'afm': ['afm'], 

184 'otf': ['otf', 'ttc', 'ttf'], 

185 'ttc': ['otf', 'ttc', 'ttf'], 

186 'ttf': ['otf', 'ttc', 'ttf'], 

187 }[fontext] 

188 

189 

190def list_fonts(directory, extensions): 

191 """ 

192 Return a list of all fonts matching any of the extensions, found 

193 recursively under the directory. 

194 """ 

195 extensions = ["." + ext for ext in extensions] 

196 return [os.path.join(dirpath, filename) 

197 # os.walk ignores access errors, unlike Path.glob. 

198 for dirpath, _, filenames in os.walk(directory) 

199 for filename in filenames 

200 if Path(filename).suffix.lower() in extensions] 

201 

202 

203def win32FontDirectory(): 

204 r""" 

205 Return the user-specified font directory for Win32. This is 

206 looked up from the registry key :: 

207 

208 \\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\Fonts 

209 

210 If the key is not found, ``%WINDIR%\Fonts`` will be returned. 

211 """ # noqa: E501 

212 import winreg 

213 try: 

214 with winreg.OpenKey(winreg.HKEY_CURRENT_USER, MSFolders) as user: 

215 return winreg.QueryValueEx(user, 'Fonts')[0] 

216 except OSError: 

217 return os.path.join(os.environ['WINDIR'], 'Fonts') 

218 

219 

220def _get_win32_installed_fonts(): 

221 """List the font paths known to the Windows registry.""" 

222 import winreg 

223 items = set() 

224 # Search and resolve fonts listed in the registry. 

225 for domain, base_dirs in [ 

226 (winreg.HKEY_LOCAL_MACHINE, [win32FontDirectory()]), # System. 

227 (winreg.HKEY_CURRENT_USER, MSUserFontDirectories), # User. 

228 ]: 

229 for base_dir in base_dirs: 

230 for reg_path in MSFontDirectories: 

231 try: 

232 with winreg.OpenKey(domain, reg_path) as local: 

233 for j in range(winreg.QueryInfoKey(local)[1]): 

234 # value may contain the filename of the font or its 

235 # absolute path. 

236 key, value, tp = winreg.EnumValue(local, j) 

237 if not isinstance(value, str): 

238 continue 

239 try: 

240 # If value contains already an absolute path, 

241 # then it is not changed further. 

242 path = Path(base_dir, value).resolve() 

243 except RuntimeError: 

244 # Don't fail with invalid entries. 

245 continue 

246 items.add(path) 

247 except (OSError, MemoryError): 

248 continue 

249 return items 

250 

251 

252@lru_cache 

253def _get_fontconfig_fonts(): 

254 """Cache and list the font paths known to ``fc-list``.""" 

255 try: 

256 if b'--format' not in subprocess.check_output(['fc-list', '--help']): 

257 _log.warning( # fontconfig 2.7 implemented --format. 

258 'Matplotlib needs fontconfig>=2.7 to query system fonts.') 

259 return [] 

260 out = subprocess.check_output(['fc-list', '--format=%{file}\\n']) 

261 except (OSError, subprocess.CalledProcessError): 

262 return [] 

263 return [Path(os.fsdecode(fname)) for fname in out.split(b'\n')] 

264 

265 

266@lru_cache 

267def _get_macos_fonts(): 

268 """Cache and list the font paths known to ``system_profiler SPFontsDataType``.""" 

269 try: 

270 d, = plistlib.loads( 

271 subprocess.check_output(["system_profiler", "-xml", "SPFontsDataType"])) 

272 except (OSError, subprocess.CalledProcessError, plistlib.InvalidFileException): 

273 return [] 

274 return [Path(entry["path"]) for entry in d["_items"]] 

275 

276 

277def findSystemFonts(fontpaths=None, fontext='ttf'): 

278 """ 

279 Search for fonts in the specified font paths. If no paths are 

280 given, will use a standard set of system paths, as well as the 

281 list of fonts tracked by fontconfig if fontconfig is installed and 

282 available. A list of TrueType fonts are returned by default with 

283 AFM fonts as an option. 

284 """ 

285 fontfiles = set() 

286 fontexts = get_fontext_synonyms(fontext) 

287 

288 if fontpaths is None: 

289 if sys.platform == 'win32': 

290 installed_fonts = _get_win32_installed_fonts() 

291 fontpaths = [] 

292 else: 

293 installed_fonts = _get_fontconfig_fonts() 

294 if sys.platform == 'darwin': 

295 installed_fonts += _get_macos_fonts() 

296 fontpaths = [*X11FontDirectories, *OSXFontDirectories] 

297 else: 

298 fontpaths = X11FontDirectories 

299 fontfiles.update(str(path) for path in installed_fonts 

300 if path.suffix.lower()[1:] in fontexts) 

301 

302 elif isinstance(fontpaths, str): 

303 fontpaths = [fontpaths] 

304 

305 for path in fontpaths: 

306 fontfiles.update(map(os.path.abspath, list_fonts(path, fontexts))) 

307 

308 return [fname for fname in fontfiles if os.path.exists(fname)] 

309 

310 

311@dataclasses.dataclass(frozen=True) 

312class FontEntry: 

313 """ 

314 A class for storing Font properties. 

315 

316 It is used when populating the font lookup dictionary. 

317 """ 

318 

319 fname: str = '' 

320 name: str = '' 

321 style: str = 'normal' 

322 variant: str = 'normal' 

323 weight: str | int = 'normal' 

324 stretch: str = 'normal' 

325 size: str = 'medium' 

326 

327 def _repr_html_(self) -> str: 

328 png_stream = self._repr_png_() 

329 png_b64 = b64encode(png_stream).decode() 

330 return f"<img src=\"data:image/png;base64, {png_b64}\" />" 

331 

332 def _repr_png_(self) -> bytes: 

333 from matplotlib.figure import Figure # Circular import. 

334 fig = Figure() 

335 font_path = Path(self.fname) if self.fname != '' else None 

336 fig.text(0, 0, self.name, font=font_path) 

337 with BytesIO() as buf: 

338 fig.savefig(buf, bbox_inches='tight', transparent=True) 

339 return buf.getvalue() 

340 

341 

342def ttfFontProperty(font): 

343 """ 

344 Extract information from a TrueType font file. 

345 

346 Parameters 

347 ---------- 

348 font : `.FT2Font` 

349 The TrueType font file from which information will be extracted. 

350 

351 Returns 

352 ------- 

353 `FontEntry` 

354 The extracted font properties. 

355 

356 """ 

357 name = font.family_name 

358 

359 # Styles are: italic, oblique, and normal (default) 

360 

361 sfnt = font.get_sfnt() 

362 mac_key = (1, # platform: macintosh 

363 0, # id: roman 

364 0) # langid: english 

365 ms_key = (3, # platform: microsoft 

366 1, # id: unicode_cs 

367 0x0409) # langid: english_united_states 

368 

369 # These tables are actually mac_roman-encoded, but mac_roman support may be 

370 # missing in some alternative Python implementations and we are only going 

371 # to look for ASCII substrings, where any ASCII-compatible encoding works 

372 # - or big-endian UTF-16, since important Microsoft fonts use that. 

373 sfnt2 = (sfnt.get((*mac_key, 2), b'').decode('latin-1').lower() or 

374 sfnt.get((*ms_key, 2), b'').decode('utf_16_be').lower()) 

375 sfnt4 = (sfnt.get((*mac_key, 4), b'').decode('latin-1').lower() or 

376 sfnt.get((*ms_key, 4), b'').decode('utf_16_be').lower()) 

377 

378 if sfnt4.find('oblique') >= 0: 

379 style = 'oblique' 

380 elif sfnt4.find('italic') >= 0: 

381 style = 'italic' 

382 elif sfnt2.find('regular') >= 0: 

383 style = 'normal' 

384 elif font.style_flags & ft2font.ITALIC: 

385 style = 'italic' 

386 else: 

387 style = 'normal' 

388 

389 # Variants are: small-caps and normal (default) 

390 

391 # !!!! Untested 

392 if name.lower() in ['capitals', 'small-caps']: 

393 variant = 'small-caps' 

394 else: 

395 variant = 'normal' 

396 

397 # The weight-guessing algorithm is directly translated from fontconfig 

398 # 2.13.1's FcFreeTypeQueryFaceInternal (fcfreetype.c). 

399 wws_subfamily = 22 

400 typographic_subfamily = 16 

401 font_subfamily = 2 

402 styles = [ 

403 sfnt.get((*mac_key, wws_subfamily), b'').decode('latin-1'), 

404 sfnt.get((*mac_key, typographic_subfamily), b'').decode('latin-1'), 

405 sfnt.get((*mac_key, font_subfamily), b'').decode('latin-1'), 

406 sfnt.get((*ms_key, wws_subfamily), b'').decode('utf-16-be'), 

407 sfnt.get((*ms_key, typographic_subfamily), b'').decode('utf-16-be'), 

408 sfnt.get((*ms_key, font_subfamily), b'').decode('utf-16-be'), 

409 ] 

410 styles = [*filter(None, styles)] or [font.style_name] 

411 

412 def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal. 

413 # OS/2 table weight. 

414 os2 = font.get_sfnt_table("OS/2") 

415 if os2 and os2["version"] != 0xffff: 

416 return os2["usWeightClass"] 

417 # PostScript font info weight. 

418 try: 

419 ps_font_info_weight = ( 

420 font.get_ps_font_info()["weight"].replace(" ", "") or "") 

421 except ValueError: 

422 pass 

423 else: 

424 for regex, weight in _weight_regexes: 

425 if re.fullmatch(regex, ps_font_info_weight, re.I): 

426 return weight 

427 # Style name weight. 

428 for style in styles: 

429 style = style.replace(" ", "") 

430 for regex, weight in _weight_regexes: 

431 if re.search(regex, style, re.I): 

432 return weight 

433 if font.style_flags & ft2font.BOLD: 

434 return 700 # "bold" 

435 return 500 # "medium", not "regular"! 

436 

437 weight = int(get_weight()) 

438 

439 # Stretch can be absolute and relative 

440 # Absolute stretches are: ultra-condensed, extra-condensed, condensed, 

441 # semi-condensed, normal, semi-expanded, expanded, extra-expanded, 

442 # and ultra-expanded. 

443 # Relative stretches are: wider, narrower 

444 # Child value is: inherit 

445 

446 if any(word in sfnt4 for word in ['narrow', 'condensed', 'cond']): 

447 stretch = 'condensed' 

448 elif 'demi cond' in sfnt4: 

449 stretch = 'semi-condensed' 

450 elif any(word in sfnt4 for word in ['wide', 'expanded', 'extended']): 

451 stretch = 'expanded' 

452 else: 

453 stretch = 'normal' 

454 

455 # Sizes can be absolute and relative. 

456 # Absolute sizes are: xx-small, x-small, small, medium, large, x-large, 

457 # and xx-large. 

458 # Relative sizes are: larger, smaller 

459 # Length value is an absolute font size, e.g., 12pt 

460 # Percentage values are in 'em's. Most robust specification. 

461 

462 if not font.scalable: 

463 raise NotImplementedError("Non-scalable fonts are not supported") 

464 size = 'scalable' 

465 

466 return FontEntry(font.fname, name, style, variant, weight, stretch, size) 

467 

468 

469def afmFontProperty(fontpath, font): 

470 """ 

471 Extract information from an AFM font file. 

472 

473 Parameters 

474 ---------- 

475 fontpath : str 

476 The filename corresponding to *font*. 

477 font : AFM 

478 The AFM font file from which information will be extracted. 

479 

480 Returns 

481 ------- 

482 `FontEntry` 

483 The extracted font properties. 

484 """ 

485 

486 name = font.get_familyname() 

487 fontname = font.get_fontname().lower() 

488 

489 # Styles are: italic, oblique, and normal (default) 

490 

491 if font.get_angle() != 0 or 'italic' in name.lower(): 

492 style = 'italic' 

493 elif 'oblique' in name.lower(): 

494 style = 'oblique' 

495 else: 

496 style = 'normal' 

497 

498 # Variants are: small-caps and normal (default) 

499 

500 # !!!! Untested 

501 if name.lower() in ['capitals', 'small-caps']: 

502 variant = 'small-caps' 

503 else: 

504 variant = 'normal' 

505 

506 weight = font.get_weight().lower() 

507 if weight not in weight_dict: 

508 weight = 'normal' 

509 

510 # Stretch can be absolute and relative 

511 # Absolute stretches are: ultra-condensed, extra-condensed, condensed, 

512 # semi-condensed, normal, semi-expanded, expanded, extra-expanded, 

513 # and ultra-expanded. 

514 # Relative stretches are: wider, narrower 

515 # Child value is: inherit 

516 if 'demi cond' in fontname: 

517 stretch = 'semi-condensed' 

518 elif any(word in fontname for word in ['narrow', 'cond']): 

519 stretch = 'condensed' 

520 elif any(word in fontname for word in ['wide', 'expanded', 'extended']): 

521 stretch = 'expanded' 

522 else: 

523 stretch = 'normal' 

524 

525 # Sizes can be absolute and relative. 

526 # Absolute sizes are: xx-small, x-small, small, medium, large, x-large, 

527 # and xx-large. 

528 # Relative sizes are: larger, smaller 

529 # Length value is an absolute font size, e.g., 12pt 

530 # Percentage values are in 'em's. Most robust specification. 

531 

532 # All AFM fonts are apparently scalable. 

533 

534 size = 'scalable' 

535 

536 return FontEntry(fontpath, name, style, variant, weight, stretch, size) 

537 

538 

539class FontProperties: 

540 """ 

541 A class for storing and manipulating font properties. 

542 

543 The font properties are the six properties described in the 

544 `W3C Cascading Style Sheet, Level 1 

545 <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ font 

546 specification and *math_fontfamily* for math fonts: 

547 

548 - family: A list of font names in decreasing order of priority. 

549 The items may include a generic font family name, either 'sans-serif', 

550 'serif', 'cursive', 'fantasy', or 'monospace'. In that case, the actual 

551 font to be used will be looked up from the associated rcParam during the 

552 search process in `.findfont`. Default: :rc:`font.family` 

553 

554 - style: Either 'normal', 'italic' or 'oblique'. 

555 Default: :rc:`font.style` 

556 

557 - variant: Either 'normal' or 'small-caps'. 

558 Default: :rc:`font.variant` 

559 

560 - stretch: A numeric value in the range 0-1000 or one of 

561 'ultra-condensed', 'extra-condensed', 'condensed', 

562 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 

563 'extra-expanded' or 'ultra-expanded'. Default: :rc:`font.stretch` 

564 

565 - weight: A numeric value in the range 0-1000 or one of 

566 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 

567 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', 

568 'extra bold', 'black'. Default: :rc:`font.weight` 

569 

570 - size: Either a relative value of 'xx-small', 'x-small', 

571 'small', 'medium', 'large', 'x-large', 'xx-large' or an 

572 absolute font size, e.g., 10. Default: :rc:`font.size` 

573 

574 - math_fontfamily: The family of fonts used to render math text. 

575 Supported values are: 'dejavusans', 'dejavuserif', 'cm', 

576 'stix', 'stixsans' and 'custom'. Default: :rc:`mathtext.fontset` 

577 

578 Alternatively, a font may be specified using the absolute path to a font 

579 file, by using the *fname* kwarg. However, in this case, it is typically 

580 simpler to just pass the path (as a `pathlib.Path`, not a `str`) to the 

581 *font* kwarg of the `.Text` object. 

582 

583 The preferred usage of font sizes is to use the relative values, 

584 e.g., 'large', instead of absolute font sizes, e.g., 12. This 

585 approach allows all text sizes to be made larger or smaller based 

586 on the font manager's default font size. 

587 

588 This class will also accept a fontconfig_ pattern_, if it is the only 

589 argument provided. This support does not depend on fontconfig; we are 

590 merely borrowing its pattern syntax for use here. 

591 

592 .. _fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/ 

593 .. _pattern: 

594 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html 

595 

596 Note that Matplotlib's internal font manager and fontconfig use a 

597 different algorithm to lookup fonts, so the results of the same pattern 

598 may be different in Matplotlib than in other applications that use 

599 fontconfig. 

600 """ 

601 

602 def __init__(self, family=None, style=None, variant=None, weight=None, 

603 stretch=None, size=None, 

604 fname=None, # if set, it's a hardcoded filename to use 

605 math_fontfamily=None): 

606 self.set_family(family) 

607 self.set_style(style) 

608 self.set_variant(variant) 

609 self.set_weight(weight) 

610 self.set_stretch(stretch) 

611 self.set_file(fname) 

612 self.set_size(size) 

613 self.set_math_fontfamily(math_fontfamily) 

614 # Treat family as a fontconfig pattern if it is the only parameter 

615 # provided. Even in that case, call the other setters first to set 

616 # attributes not specified by the pattern to the rcParams defaults. 

617 if (isinstance(family, str) 

618 and style is None and variant is None and weight is None 

619 and stretch is None and size is None and fname is None): 

620 self.set_fontconfig_pattern(family) 

621 

622 @classmethod 

623 def _from_any(cls, arg): 

624 """ 

625 Generic constructor which can build a `.FontProperties` from any of the 

626 following: 

627 

628 - a `.FontProperties`: it is passed through as is; 

629 - `None`: a `.FontProperties` using rc values is used; 

630 - an `os.PathLike`: it is used as path to the font file; 

631 - a `str`: it is parsed as a fontconfig pattern; 

632 - a `dict`: it is passed as ``**kwargs`` to `.FontProperties`. 

633 """ 

634 if arg is None: 

635 return cls() 

636 elif isinstance(arg, cls): 

637 return arg 

638 elif isinstance(arg, os.PathLike): 

639 return cls(fname=arg) 

640 elif isinstance(arg, str): 

641 return cls(arg) 

642 else: 

643 return cls(**arg) 

644 

645 def __hash__(self): 

646 l = (tuple(self.get_family()), 

647 self.get_slant(), 

648 self.get_variant(), 

649 self.get_weight(), 

650 self.get_stretch(), 

651 self.get_size(), 

652 self.get_file(), 

653 self.get_math_fontfamily()) 

654 return hash(l) 

655 

656 def __eq__(self, other): 

657 return hash(self) == hash(other) 

658 

659 def __str__(self): 

660 return self.get_fontconfig_pattern() 

661 

662 def get_family(self): 

663 """ 

664 Return a list of individual font family names or generic family names. 

665 

666 The font families or generic font families (which will be resolved 

667 from their respective rcParams when searching for a matching font) in 

668 the order of preference. 

669 """ 

670 return self._family 

671 

672 def get_name(self): 

673 """ 

674 Return the name of the font that best matches the font properties. 

675 """ 

676 return get_font(findfont(self)).family_name 

677 

678 def get_style(self): 

679 """ 

680 Return the font style. Values are: 'normal', 'italic' or 'oblique'. 

681 """ 

682 return self._slant 

683 

684 def get_variant(self): 

685 """ 

686 Return the font variant. Values are: 'normal' or 'small-caps'. 

687 """ 

688 return self._variant 

689 

690 def get_weight(self): 

691 """ 

692 Set the font weight. Options are: A numeric value in the 

693 range 0-1000 or one of 'light', 'normal', 'regular', 'book', 

694 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 

695 'heavy', 'extra bold', 'black' 

696 """ 

697 return self._weight 

698 

699 def get_stretch(self): 

700 """ 

701 Return the font stretch or width. Options are: 'ultra-condensed', 

702 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 

703 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'. 

704 """ 

705 return self._stretch 

706 

707 def get_size(self): 

708 """ 

709 Return the font size. 

710 """ 

711 return self._size 

712 

713 def get_file(self): 

714 """ 

715 Return the filename of the associated font. 

716 """ 

717 return self._file 

718 

719 def get_fontconfig_pattern(self): 

720 """ 

721 Get a fontconfig_ pattern_ suitable for looking up the font as 

722 specified with fontconfig's ``fc-match`` utility. 

723 

724 This support does not depend on fontconfig; we are merely borrowing its 

725 pattern syntax for use here. 

726 """ 

727 return generate_fontconfig_pattern(self) 

728 

729 def set_family(self, family): 

730 """ 

731 Change the font family. Can be either an alias (generic name 

732 is CSS parlance), such as: 'serif', 'sans-serif', 'cursive', 

733 'fantasy', or 'monospace', a real font name or a list of real 

734 font names. Real font names are not supported when 

735 :rc:`text.usetex` is `True`. Default: :rc:`font.family` 

736 """ 

737 if family is None: 

738 family = mpl.rcParams['font.family'] 

739 if isinstance(family, str): 

740 family = [family] 

741 self._family = family 

742 

743 def set_style(self, style): 

744 """ 

745 Set the font style. 

746 

747 Parameters 

748 ---------- 

749 style : {'normal', 'italic', 'oblique'}, default: :rc:`font.style` 

750 """ 

751 if style is None: 

752 style = mpl.rcParams['font.style'] 

753 _api.check_in_list(['normal', 'italic', 'oblique'], style=style) 

754 self._slant = style 

755 

756 def set_variant(self, variant): 

757 """ 

758 Set the font variant. 

759 

760 Parameters 

761 ---------- 

762 variant : {'normal', 'small-caps'}, default: :rc:`font.variant` 

763 """ 

764 if variant is None: 

765 variant = mpl.rcParams['font.variant'] 

766 _api.check_in_list(['normal', 'small-caps'], variant=variant) 

767 self._variant = variant 

768 

769 def set_weight(self, weight): 

770 """ 

771 Set the font weight. 

772 

773 Parameters 

774 ---------- 

775 weight : int or {'ultralight', 'light', 'normal', 'regular', 'book', \ 

776'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', \ 

777'extra bold', 'black'}, default: :rc:`font.weight` 

778 If int, must be in the range 0-1000. 

779 """ 

780 if weight is None: 

781 weight = mpl.rcParams['font.weight'] 

782 if weight in weight_dict: 

783 self._weight = weight 

784 return 

785 try: 

786 weight = int(weight) 

787 except ValueError: 

788 pass 

789 else: 

790 if 0 <= weight <= 1000: 

791 self._weight = weight 

792 return 

793 raise ValueError(f"{weight=} is invalid") 

794 

795 def set_stretch(self, stretch): 

796 """ 

797 Set the font stretch or width. 

798 

799 Parameters 

800 ---------- 

801 stretch : int or {'ultra-condensed', 'extra-condensed', 'condensed', \ 

802'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', \ 

803'ultra-expanded'}, default: :rc:`font.stretch` 

804 If int, must be in the range 0-1000. 

805 """ 

806 if stretch is None: 

807 stretch = mpl.rcParams['font.stretch'] 

808 if stretch in stretch_dict: 

809 self._stretch = stretch 

810 return 

811 try: 

812 stretch = int(stretch) 

813 except ValueError: 

814 pass 

815 else: 

816 if 0 <= stretch <= 1000: 

817 self._stretch = stretch 

818 return 

819 raise ValueError(f"{stretch=} is invalid") 

820 

821 def set_size(self, size): 

822 """ 

823 Set the font size. 

824 

825 Parameters 

826 ---------- 

827 size : float or {'xx-small', 'x-small', 'small', 'medium', \ 

828'large', 'x-large', 'xx-large'}, default: :rc:`font.size` 

829 If a float, the font size in points. The string values denote 

830 sizes relative to the default font size. 

831 """ 

832 if size is None: 

833 size = mpl.rcParams['font.size'] 

834 try: 

835 size = float(size) 

836 except ValueError: 

837 try: 

838 scale = font_scalings[size] 

839 except KeyError as err: 

840 raise ValueError( 

841 "Size is invalid. Valid font size are " 

842 + ", ".join(map(str, font_scalings))) from err 

843 else: 

844 size = scale * FontManager.get_default_size() 

845 if size < 1.0: 

846 _log.info('Fontsize %1.2f < 1.0 pt not allowed by FreeType. ' 

847 'Setting fontsize = 1 pt', size) 

848 size = 1.0 

849 self._size = size 

850 

851 def set_file(self, file): 

852 """ 

853 Set the filename of the fontfile to use. In this case, all 

854 other properties will be ignored. 

855 """ 

856 self._file = os.fspath(file) if file is not None else None 

857 

858 def set_fontconfig_pattern(self, pattern): 

859 """ 

860 Set the properties by parsing a fontconfig_ *pattern*. 

861 

862 This support does not depend on fontconfig; we are merely borrowing its 

863 pattern syntax for use here. 

864 """ 

865 for key, val in parse_fontconfig_pattern(pattern).items(): 

866 if type(val) is list: 

867 getattr(self, "set_" + key)(val[0]) 

868 else: 

869 getattr(self, "set_" + key)(val) 

870 

871 def get_math_fontfamily(self): 

872 """ 

873 Return the name of the font family used for math text. 

874 

875 The default font is :rc:`mathtext.fontset`. 

876 """ 

877 return self._math_fontfamily 

878 

879 def set_math_fontfamily(self, fontfamily): 

880 """ 

881 Set the font family for text in math mode. 

882 

883 If not set explicitly, :rc:`mathtext.fontset` will be used. 

884 

885 Parameters 

886 ---------- 

887 fontfamily : str 

888 The name of the font family. 

889 

890 Available font families are defined in the 

891 :ref:`default matplotlibrc file <customizing-with-matplotlibrc-files>`. 

892 

893 See Also 

894 -------- 

895 .text.Text.get_math_fontfamily 

896 """ 

897 if fontfamily is None: 

898 fontfamily = mpl.rcParams['mathtext.fontset'] 

899 else: 

900 valid_fonts = _validators['mathtext.fontset'].valid.values() 

901 # _check_in_list() Validates the parameter math_fontfamily as 

902 # if it were passed to rcParams['mathtext.fontset'] 

903 _api.check_in_list(valid_fonts, math_fontfamily=fontfamily) 

904 self._math_fontfamily = fontfamily 

905 

906 def copy(self): 

907 """Return a copy of self.""" 

908 return copy.copy(self) 

909 

910 # Aliases 

911 set_name = set_family 

912 get_slant = get_style 

913 set_slant = set_style 

914 get_size_in_points = get_size 

915 

916 

917class _JSONEncoder(json.JSONEncoder): 

918 def default(self, o): 

919 if isinstance(o, FontManager): 

920 return dict(o.__dict__, __class__='FontManager') 

921 elif isinstance(o, FontEntry): 

922 d = dict(o.__dict__, __class__='FontEntry') 

923 try: 

924 # Cache paths of fonts shipped with Matplotlib relative to the 

925 # Matplotlib data path, which helps in the presence of venvs. 

926 d["fname"] = str(Path(d["fname"]).relative_to(mpl.get_data_path())) 

927 except ValueError: 

928 pass 

929 return d 

930 else: 

931 return super().default(o) 

932 

933 

934def _json_decode(o): 

935 cls = o.pop('__class__', None) 

936 if cls is None: 

937 return o 

938 elif cls == 'FontManager': 

939 r = FontManager.__new__(FontManager) 

940 r.__dict__.update(o) 

941 return r 

942 elif cls == 'FontEntry': 

943 if not os.path.isabs(o['fname']): 

944 o['fname'] = os.path.join(mpl.get_data_path(), o['fname']) 

945 r = FontEntry(**o) 

946 return r 

947 else: 

948 raise ValueError("Don't know how to deserialize __class__=%s" % cls) 

949 

950 

951def json_dump(data, filename): 

952 """ 

953 Dump `FontManager` *data* as JSON to the file named *filename*. 

954 

955 See Also 

956 -------- 

957 json_load 

958 

959 Notes 

960 ----- 

961 File paths that are children of the Matplotlib data path (typically, fonts 

962 shipped with Matplotlib) are stored relative to that data path (to remain 

963 valid across virtualenvs). 

964 

965 This function temporarily locks the output file to prevent multiple 

966 processes from overwriting one another's output. 

967 """ 

968 try: 

969 with cbook._lock_path(filename), open(filename, 'w') as fh: 

970 json.dump(data, fh, cls=_JSONEncoder, indent=2) 

971 except OSError as e: 

972 _log.warning('Could not save font_manager cache %s', e) 

973 

974 

975def json_load(filename): 

976 """ 

977 Load a `FontManager` from the JSON file named *filename*. 

978 

979 See Also 

980 -------- 

981 json_dump 

982 """ 

983 with open(filename) as fh: 

984 return json.load(fh, object_hook=_json_decode) 

985 

986 

987class FontManager: 

988 """ 

989 On import, the `FontManager` singleton instance creates a list of ttf and 

990 afm fonts and caches their `FontProperties`. The `FontManager.findfont` 

991 method does a nearest neighbor search to find the font that most closely 

992 matches the specification. If no good enough match is found, the default 

993 font is returned. 

994 

995 Fonts added with the `FontManager.addfont` method will not persist in the 

996 cache; therefore, `addfont` will need to be called every time Matplotlib is 

997 imported. This method should only be used if and when a font cannot be 

998 installed on your operating system by other means. 

999 

1000 Notes 

1001 ----- 

1002 The `FontManager.addfont` method must be called on the global `FontManager` 

1003 instance. 

1004 

1005 Example usage:: 

1006 

1007 import matplotlib.pyplot as plt 

1008 from matplotlib import font_manager 

1009 

1010 font_dirs = ["/resources/fonts"] # The path to the custom font file. 

1011 font_files = font_manager.findSystemFonts(fontpaths=font_dirs) 

1012 

1013 for font_file in font_files: 

1014 font_manager.fontManager.addfont(font_file) 

1015 """ 

1016 # Increment this version number whenever the font cache data 

1017 # format or behavior has changed and requires an existing font 

1018 # cache files to be rebuilt. 

1019 __version__ = 390 

1020 

1021 def __init__(self, size=None, weight='normal'): 

1022 self._version = self.__version__ 

1023 

1024 self.__default_weight = weight 

1025 self.default_size = size 

1026 

1027 # Create list of font paths. 

1028 paths = [cbook._get_data_path('fonts', subdir) 

1029 for subdir in ['ttf', 'afm', 'pdfcorefonts']] 

1030 _log.debug('font search path %s', paths) 

1031 

1032 self.defaultFamily = { 

1033 'ttf': 'DejaVu Sans', 

1034 'afm': 'Helvetica'} 

1035 

1036 self.afmlist = [] 

1037 self.ttflist = [] 

1038 

1039 # Delay the warning by 5s. 

1040 timer = threading.Timer(5, lambda: _log.warning( 

1041 'Matplotlib is building the font cache; this may take a moment.')) 

1042 timer.start() 

1043 try: 

1044 for fontext in ["afm", "ttf"]: 

1045 for path in [*findSystemFonts(paths, fontext=fontext), 

1046 *findSystemFonts(fontext=fontext)]: 

1047 try: 

1048 self.addfont(path) 

1049 except OSError as exc: 

1050 _log.info("Failed to open font file %s: %s", path, exc) 

1051 except Exception as exc: 

1052 _log.info("Failed to extract font properties from %s: " 

1053 "%s", path, exc) 

1054 finally: 

1055 timer.cancel() 

1056 

1057 def addfont(self, path): 

1058 """ 

1059 Cache the properties of the font at *path* to make it available to the 

1060 `FontManager`. The type of font is inferred from the path suffix. 

1061 

1062 Parameters 

1063 ---------- 

1064 path : str or path-like 

1065 

1066 Notes 

1067 ----- 

1068 This method is useful for adding a custom font without installing it in 

1069 your operating system. See the `FontManager` singleton instance for 

1070 usage and caveats about this function. 

1071 """ 

1072 # Convert to string in case of a path as 

1073 # afmFontProperty and FT2Font expect this 

1074 path = os.fsdecode(path) 

1075 if Path(path).suffix.lower() == ".afm": 

1076 with open(path, "rb") as fh: 

1077 font = _afm.AFM(fh) 

1078 prop = afmFontProperty(path, font) 

1079 self.afmlist.append(prop) 

1080 else: 

1081 font = ft2font.FT2Font(path) 

1082 prop = ttfFontProperty(font) 

1083 self.ttflist.append(prop) 

1084 self._findfont_cached.cache_clear() 

1085 

1086 @property 

1087 def defaultFont(self): 

1088 # Lazily evaluated (findfont then caches the result) to avoid including 

1089 # the venv path in the json serialization. 

1090 return {ext: self.findfont(family, fontext=ext) 

1091 for ext, family in self.defaultFamily.items()} 

1092 

1093 def get_default_weight(self): 

1094 """ 

1095 Return the default font weight. 

1096 """ 

1097 return self.__default_weight 

1098 

1099 @staticmethod 

1100 def get_default_size(): 

1101 """ 

1102 Return the default font size. 

1103 """ 

1104 return mpl.rcParams['font.size'] 

1105 

1106 def set_default_weight(self, weight): 

1107 """ 

1108 Set the default font weight. The initial value is 'normal'. 

1109 """ 

1110 self.__default_weight = weight 

1111 

1112 @staticmethod 

1113 def _expand_aliases(family): 

1114 if family in ('sans', 'sans serif'): 

1115 family = 'sans-serif' 

1116 return mpl.rcParams['font.' + family] 

1117 

1118 # Each of the scoring functions below should return a value between 

1119 # 0.0 (perfect match) and 1.0 (terrible match) 

1120 def score_family(self, families, family2): 

1121 """ 

1122 Return a match score between the list of font families in 

1123 *families* and the font family name *family2*. 

1124 

1125 An exact match at the head of the list returns 0.0. 

1126 

1127 A match further down the list will return between 0 and 1. 

1128 

1129 No match will return 1.0. 

1130 """ 

1131 if not isinstance(families, (list, tuple)): 

1132 families = [families] 

1133 elif len(families) == 0: 

1134 return 1.0 

1135 family2 = family2.lower() 

1136 step = 1 / len(families) 

1137 for i, family1 in enumerate(families): 

1138 family1 = family1.lower() 

1139 if family1 in font_family_aliases: 

1140 options = [*map(str.lower, self._expand_aliases(family1))] 

1141 if family2 in options: 

1142 idx = options.index(family2) 

1143 return (i + (idx / len(options))) * step 

1144 elif family1 == family2: 

1145 # The score should be weighted by where in the 

1146 # list the font was found. 

1147 return i * step 

1148 return 1.0 

1149 

1150 def score_style(self, style1, style2): 

1151 """ 

1152 Return a match score between *style1* and *style2*. 

1153 

1154 An exact match returns 0.0. 

1155 

1156 A match between 'italic' and 'oblique' returns 0.1. 

1157 

1158 No match returns 1.0. 

1159 """ 

1160 if style1 == style2: 

1161 return 0.0 

1162 elif (style1 in ('italic', 'oblique') 

1163 and style2 in ('italic', 'oblique')): 

1164 return 0.1 

1165 return 1.0 

1166 

1167 def score_variant(self, variant1, variant2): 

1168 """ 

1169 Return a match score between *variant1* and *variant2*. 

1170 

1171 An exact match returns 0.0, otherwise 1.0. 

1172 """ 

1173 if variant1 == variant2: 

1174 return 0.0 

1175 else: 

1176 return 1.0 

1177 

1178 def score_stretch(self, stretch1, stretch2): 

1179 """ 

1180 Return a match score between *stretch1* and *stretch2*. 

1181 

1182 The result is the absolute value of the difference between the 

1183 CSS numeric values of *stretch1* and *stretch2*, normalized 

1184 between 0.0 and 1.0. 

1185 """ 

1186 try: 

1187 stretchval1 = int(stretch1) 

1188 except ValueError: 

1189 stretchval1 = stretch_dict.get(stretch1, 500) 

1190 try: 

1191 stretchval2 = int(stretch2) 

1192 except ValueError: 

1193 stretchval2 = stretch_dict.get(stretch2, 500) 

1194 return abs(stretchval1 - stretchval2) / 1000.0 

1195 

1196 def score_weight(self, weight1, weight2): 

1197 """ 

1198 Return a match score between *weight1* and *weight2*. 

1199 

1200 The result is 0.0 if both weight1 and weight 2 are given as strings 

1201 and have the same value. 

1202 

1203 Otherwise, the result is the absolute value of the difference between 

1204 the CSS numeric values of *weight1* and *weight2*, normalized between 

1205 0.05 and 1.0. 

1206 """ 

1207 # exact match of the weight names, e.g. weight1 == weight2 == "regular" 

1208 if cbook._str_equal(weight1, weight2): 

1209 return 0.0 

1210 w1 = weight1 if isinstance(weight1, Number) else weight_dict[weight1] 

1211 w2 = weight2 if isinstance(weight2, Number) else weight_dict[weight2] 

1212 return 0.95 * (abs(w1 - w2) / 1000) + 0.05 

1213 

1214 def score_size(self, size1, size2): 

1215 """ 

1216 Return a match score between *size1* and *size2*. 

1217 

1218 If *size2* (the size specified in the font file) is 'scalable', this 

1219 function always returns 0.0, since any font size can be generated. 

1220 

1221 Otherwise, the result is the absolute distance between *size1* and 

1222 *size2*, normalized so that the usual range of font sizes (6pt - 

1223 72pt) will lie between 0.0 and 1.0. 

1224 """ 

1225 if size2 == 'scalable': 

1226 return 0.0 

1227 # Size value should have already been 

1228 try: 

1229 sizeval1 = float(size1) 

1230 except ValueError: 

1231 sizeval1 = self.default_size * font_scalings[size1] 

1232 try: 

1233 sizeval2 = float(size2) 

1234 except ValueError: 

1235 return 1.0 

1236 return abs(sizeval1 - sizeval2) / 72 

1237 

1238 def findfont(self, prop, fontext='ttf', directory=None, 

1239 fallback_to_default=True, rebuild_if_missing=True): 

1240 """ 

1241 Find the path to the font file most closely matching the given font properties. 

1242 

1243 Parameters 

1244 ---------- 

1245 prop : str or `~matplotlib.font_manager.FontProperties` 

1246 The font properties to search for. This can be either a 

1247 `.FontProperties` object or a string defining a 

1248 `fontconfig patterns`_. 

1249 

1250 fontext : {'ttf', 'afm'}, default: 'ttf' 

1251 The extension of the font file: 

1252 

1253 - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf) 

1254 - 'afm': Adobe Font Metrics (.afm) 

1255 

1256 directory : str, optional 

1257 If given, only search this directory and its subdirectories. 

1258 

1259 fallback_to_default : bool 

1260 If True, will fall back to the default font family (usually 

1261 "DejaVu Sans" or "Helvetica") if the first lookup hard-fails. 

1262 

1263 rebuild_if_missing : bool 

1264 Whether to rebuild the font cache and search again if the first 

1265 match appears to point to a nonexisting font (i.e., the font cache 

1266 contains outdated entries). 

1267 

1268 Returns 

1269 ------- 

1270 str 

1271 The filename of the best matching font. 

1272 

1273 Notes 

1274 ----- 

1275 This performs a nearest neighbor search. Each font is given a 

1276 similarity score to the target font properties. The first font with 

1277 the highest score is returned. If no matches below a certain 

1278 threshold are found, the default font (usually DejaVu Sans) is 

1279 returned. 

1280 

1281 The result is cached, so subsequent lookups don't have to 

1282 perform the O(n) nearest neighbor search. 

1283 

1284 See the `W3C Cascading Style Sheet, Level 1 

1285 <http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ documentation 

1286 for a description of the font finding algorithm. 

1287 

1288 .. _fontconfig patterns: 

1289 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html 

1290 """ 

1291 # Pass the relevant rcParams (and the font manager, as `self`) to 

1292 # _findfont_cached so to prevent using a stale cache entry after an 

1293 # rcParam was changed. 

1294 rc_params = tuple(tuple(mpl.rcParams[key]) for key in [ 

1295 "font.serif", "font.sans-serif", "font.cursive", "font.fantasy", 

1296 "font.monospace"]) 

1297 ret = self._findfont_cached( 

1298 prop, fontext, directory, fallback_to_default, rebuild_if_missing, 

1299 rc_params) 

1300 if isinstance(ret, _ExceptionProxy): 

1301 raise ret.klass(ret.message) 

1302 return ret 

1303 

1304 def get_font_names(self): 

1305 """Return the list of available fonts.""" 

1306 return list({font.name for font in self.ttflist}) 

1307 

1308 def _find_fonts_by_props(self, prop, fontext='ttf', directory=None, 

1309 fallback_to_default=True, rebuild_if_missing=True): 

1310 """ 

1311 Find the paths to the font files most closely matching the given properties. 

1312 

1313 Parameters 

1314 ---------- 

1315 prop : str or `~matplotlib.font_manager.FontProperties` 

1316 The font properties to search for. This can be either a 

1317 `.FontProperties` object or a string defining a 

1318 `fontconfig patterns`_. 

1319 

1320 fontext : {'ttf', 'afm'}, default: 'ttf' 

1321 The extension of the font file: 

1322 

1323 - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf) 

1324 - 'afm': Adobe Font Metrics (.afm) 

1325 

1326 directory : str, optional 

1327 If given, only search this directory and its subdirectories. 

1328 

1329 fallback_to_default : bool 

1330 If True, will fall back to the default font family (usually 

1331 "DejaVu Sans" or "Helvetica") if none of the families were found. 

1332 

1333 rebuild_if_missing : bool 

1334 Whether to rebuild the font cache and search again if the first 

1335 match appears to point to a nonexisting font (i.e., the font cache 

1336 contains outdated entries). 

1337 

1338 Returns 

1339 ------- 

1340 list[str] 

1341 The paths of the fonts found. 

1342 

1343 Notes 

1344 ----- 

1345 This is an extension/wrapper of the original findfont API, which only 

1346 returns a single font for given font properties. Instead, this API 

1347 returns a list of filepaths of multiple fonts which closely match the 

1348 given font properties. Since this internally uses the original API, 

1349 there's no change to the logic of performing the nearest neighbor 

1350 search. See `findfont` for more details. 

1351 """ 

1352 

1353 prop = FontProperties._from_any(prop) 

1354 

1355 fpaths = [] 

1356 for family in prop.get_family(): 

1357 cprop = prop.copy() 

1358 cprop.set_family(family) # set current prop's family 

1359 

1360 try: 

1361 fpaths.append( 

1362 self.findfont( 

1363 cprop, fontext, directory, 

1364 fallback_to_default=False, # don't fallback to default 

1365 rebuild_if_missing=rebuild_if_missing, 

1366 ) 

1367 ) 

1368 except ValueError: 

1369 if family in font_family_aliases: 

1370 _log.warning( 

1371 "findfont: Generic family %r not found because " 

1372 "none of the following families were found: %s", 

1373 family, ", ".join(self._expand_aliases(family)) 

1374 ) 

1375 else: 

1376 _log.warning("findfont: Font family %r not found.", family) 

1377 

1378 # only add default family if no other font was found and 

1379 # fallback_to_default is enabled 

1380 if not fpaths: 

1381 if fallback_to_default: 

1382 dfamily = self.defaultFamily[fontext] 

1383 cprop = prop.copy() 

1384 cprop.set_family(dfamily) 

1385 fpaths.append( 

1386 self.findfont( 

1387 cprop, fontext, directory, 

1388 fallback_to_default=True, 

1389 rebuild_if_missing=rebuild_if_missing, 

1390 ) 

1391 ) 

1392 else: 

1393 raise ValueError("Failed to find any font, and fallback " 

1394 "to the default font was disabled") 

1395 

1396 return fpaths 

1397 

1398 @lru_cache(1024) 

1399 def _findfont_cached(self, prop, fontext, directory, fallback_to_default, 

1400 rebuild_if_missing, rc_params): 

1401 

1402 prop = FontProperties._from_any(prop) 

1403 

1404 fname = prop.get_file() 

1405 if fname is not None: 

1406 return fname 

1407 

1408 if fontext == 'afm': 

1409 fontlist = self.afmlist 

1410 else: 

1411 fontlist = self.ttflist 

1412 

1413 best_score = 1e64 

1414 best_font = None 

1415 

1416 _log.debug('findfont: Matching %s.', prop) 

1417 for font in fontlist: 

1418 if (directory is not None and 

1419 Path(directory) not in Path(font.fname).parents): 

1420 continue 

1421 # Matching family should have top priority, so multiply it by 10. 

1422 score = (self.score_family(prop.get_family(), font.name) * 10 

1423 + self.score_style(prop.get_style(), font.style) 

1424 + self.score_variant(prop.get_variant(), font.variant) 

1425 + self.score_weight(prop.get_weight(), font.weight) 

1426 + self.score_stretch(prop.get_stretch(), font.stretch) 

1427 + self.score_size(prop.get_size(), font.size)) 

1428 _log.debug('findfont: score(%s) = %s', font, score) 

1429 if score < best_score: 

1430 best_score = score 

1431 best_font = font 

1432 if score == 0: 

1433 break 

1434 

1435 if best_font is None or best_score >= 10.0: 

1436 if fallback_to_default: 

1437 _log.warning( 

1438 'findfont: Font family %s not found. Falling back to %s.', 

1439 prop.get_family(), self.defaultFamily[fontext]) 

1440 for family in map(str.lower, prop.get_family()): 

1441 if family in font_family_aliases: 

1442 _log.warning( 

1443 "findfont: Generic family %r not found because " 

1444 "none of the following families were found: %s", 

1445 family, ", ".join(self._expand_aliases(family))) 

1446 default_prop = prop.copy() 

1447 default_prop.set_family(self.defaultFamily[fontext]) 

1448 return self.findfont(default_prop, fontext, directory, 

1449 fallback_to_default=False) 

1450 else: 

1451 # This return instead of raise is intentional, as we wish to 

1452 # cache that it was not found, which will not occur if it was 

1453 # actually raised. 

1454 return _ExceptionProxy( 

1455 ValueError, 

1456 f"Failed to find font {prop}, and fallback to the default font was " 

1457 f"disabled" 

1458 ) 

1459 else: 

1460 _log.debug('findfont: Matching %s to %s (%r) with score of %f.', 

1461 prop, best_font.name, best_font.fname, best_score) 

1462 result = best_font.fname 

1463 

1464 if not os.path.isfile(result): 

1465 if rebuild_if_missing: 

1466 _log.info( 

1467 'findfont: Found a missing font file. Rebuilding cache.') 

1468 new_fm = _load_fontmanager(try_read_cache=False) 

1469 # Replace self by the new fontmanager, because users may have 

1470 # a reference to this specific instance. 

1471 # TODO: _load_fontmanager should really be (used by) a method 

1472 # modifying the instance in place. 

1473 vars(self).update(vars(new_fm)) 

1474 return self.findfont( 

1475 prop, fontext, directory, rebuild_if_missing=False) 

1476 else: 

1477 # This return instead of raise is intentional, as we wish to 

1478 # cache that it was not found, which will not occur if it was 

1479 # actually raised. 

1480 return _ExceptionProxy(ValueError, "No valid font could be found") 

1481 

1482 return _cached_realpath(result) 

1483 

1484 

1485@lru_cache 

1486def is_opentype_cff_font(filename): 

1487 """ 

1488 Return whether the given font is a Postscript Compact Font Format Font 

1489 embedded in an OpenType wrapper. Used by the PostScript and PDF backends 

1490 that cannot subset these fonts. 

1491 """ 

1492 if os.path.splitext(filename)[1].lower() == '.otf': 

1493 with open(filename, 'rb') as fd: 

1494 return fd.read(4) == b"OTTO" 

1495 else: 

1496 return False 

1497 

1498 

1499@lru_cache(64) 

1500def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id): 

1501 first_fontpath, *rest = font_filepaths 

1502 return ft2font.FT2Font( 

1503 first_fontpath, hinting_factor, 

1504 _fallback_list=[ 

1505 ft2font.FT2Font( 

1506 fpath, hinting_factor, 

1507 _kerning_factor=_kerning_factor 

1508 ) 

1509 for fpath in rest 

1510 ], 

1511 _kerning_factor=_kerning_factor 

1512 ) 

1513 

1514 

1515# FT2Font objects cannot be used across fork()s because they reference the same 

1516# FT_Library object. While invalidating *all* existing FT2Fonts after a fork 

1517# would be too complicated to be worth it, the main way FT2Fonts get reused is 

1518# via the cache of _get_font, which we can empty upon forking (not on Windows, 

1519# which has no fork() or register_at_fork()). 

1520if hasattr(os, "register_at_fork"): 

1521 os.register_at_fork(after_in_child=_get_font.cache_clear) 

1522 

1523 

1524@lru_cache(64) 

1525def _cached_realpath(path): 

1526 # Resolving the path avoids embedding the font twice in pdf/ps output if a 

1527 # single font is selected using two different relative paths. 

1528 return os.path.realpath(path) 

1529 

1530 

1531def get_font(font_filepaths, hinting_factor=None): 

1532 """ 

1533 Get an `.ft2font.FT2Font` object given a list of file paths. 

1534 

1535 Parameters 

1536 ---------- 

1537 font_filepaths : Iterable[str, Path, bytes], str, Path, bytes 

1538 Relative or absolute paths to the font files to be used. 

1539 

1540 If a single string, bytes, or `pathlib.Path`, then it will be treated 

1541 as a list with that entry only. 

1542 

1543 If more than one filepath is passed, then the returned FT2Font object 

1544 will fall back through the fonts, in the order given, to find a needed 

1545 glyph. 

1546 

1547 Returns 

1548 ------- 

1549 `.ft2font.FT2Font` 

1550 

1551 """ 

1552 if isinstance(font_filepaths, (str, Path, bytes)): 

1553 paths = (_cached_realpath(font_filepaths),) 

1554 else: 

1555 paths = tuple(_cached_realpath(fname) for fname in font_filepaths) 

1556 

1557 if hinting_factor is None: 

1558 hinting_factor = mpl.rcParams['text.hinting_factor'] 

1559 

1560 return _get_font( 

1561 # must be a tuple to be cached 

1562 paths, 

1563 hinting_factor, 

1564 _kerning_factor=mpl.rcParams['text.kerning_factor'], 

1565 # also key on the thread ID to prevent segfaults with multi-threading 

1566 thread_id=threading.get_ident() 

1567 ) 

1568 

1569 

1570def _load_fontmanager(*, try_read_cache=True): 

1571 fm_path = Path( 

1572 mpl.get_cachedir(), f"fontlist-v{FontManager.__version__}.json") 

1573 if try_read_cache: 

1574 try: 

1575 fm = json_load(fm_path) 

1576 except Exception: 

1577 pass 

1578 else: 

1579 if getattr(fm, "_version", object()) == FontManager.__version__: 

1580 _log.debug("Using fontManager instance from %s", fm_path) 

1581 return fm 

1582 fm = FontManager() 

1583 json_dump(fm, fm_path) 

1584 _log.info("generated new fontManager") 

1585 return fm 

1586 

1587 

1588fontManager = _load_fontmanager() 

1589findfont = fontManager.findfont 

1590get_font_names = fontManager.get_font_names