Coverage for /usr/lib/python3/dist-packages/matplotlib/font_manager.py: 24%

596 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

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. 

10 

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

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

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

14""" 

15 

16# KNOWN ISSUES 

17# 

18# - documentation 

19# - font variant is untested 

20# - font stretch is incomplete 

21# - font size is incomplete 

22# - default font algorithm needs improvement and testing 

23# - setWeights function needs improvement 

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

25 

26from base64 import b64encode 

27import copy 

28import dataclasses 

29from functools import lru_cache 

30from io import BytesIO 

31import json 

32import logging 

33from numbers import Number 

34import os 

35from pathlib import Path 

36import re 

37import subprocess 

38import sys 

39import threading 

40 

41import matplotlib as mpl 

42from matplotlib import _api, _afm, cbook, ft2font 

43from matplotlib._fontconfig_pattern import ( 

44 parse_fontconfig_pattern, generate_fontconfig_pattern) 

45from matplotlib.rcsetup import _validators 

46 

47_log = logging.getLogger(__name__) 

48 

49font_scalings = { 

50 'xx-small': 0.579, 

51 'x-small': 0.694, 

52 'small': 0.833, 

53 'medium': 1.0, 

54 'large': 1.200, 

55 'x-large': 1.440, 

56 'xx-large': 1.728, 

57 'larger': 1.2, 

58 'smaller': 0.833, 

59 None: 1.0, 

60} 

61stretch_dict = { 

62 'ultra-condensed': 100, 

63 'extra-condensed': 200, 

64 'condensed': 300, 

65 'semi-condensed': 400, 

66 'normal': 500, 

67 'semi-expanded': 600, 

68 'semi-extended': 600, 

69 'expanded': 700, 

70 'extended': 700, 

71 'extra-expanded': 800, 

72 'extra-extended': 800, 

73 'ultra-expanded': 900, 

74 'ultra-extended': 900, 

75} 

76weight_dict = { 

77 'ultralight': 100, 

78 'light': 200, 

79 'normal': 400, 

80 'regular': 400, 

81 'book': 400, 

82 'medium': 500, 

83 'roman': 500, 

84 'semibold': 600, 

85 'demibold': 600, 

86 'demi': 600, 

87 'bold': 700, 

88 'heavy': 800, 

89 'extra bold': 800, 

90 'black': 900, 

91} 

92_weight_regexes = [ 

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

94 # weight_dict! 

95 ("thin", 100), 

96 ("extralight", 200), 

97 ("ultralight", 200), 

98 ("demilight", 350), 

99 ("semilight", 350), 

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

101 ("book", 380), 

102 ("regular", 400), 

103 ("normal", 400), 

104 ("medium", 500), 

105 ("demibold", 600), 

106 ("demi", 600), 

107 ("semibold", 600), 

108 ("extrabold", 800), 

109 ("superbold", 800), 

110 ("ultrabold", 800), 

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

112 ("ultrablack", 1000), 

113 ("superblack", 1000), 

114 ("extrablack", 1000), 

115 (r"\bultra", 1000), 

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

117 ("heavy", 900), 

118] 

119font_family_aliases = { 

120 'serif', 

121 'sans-serif', 

122 'sans serif', 

123 'cursive', 

124 'fantasy', 

125 'monospace', 

126 'sans', 

127} 

128 

129 

130# OS Font paths 

131try: 

132 _HOME = Path.home() 

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

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

135MSFolders = \ 

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

137MSFontDirectories = [ 

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

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

140MSUserFontDirectories = [ 

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

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

143] 

144X11FontDirectories = [ 

145 # an old standard installation point 

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

147 "/usr/X11/lib/X11/fonts", 

148 # here is the new standard location for fonts 

149 "/usr/share/fonts/", 

150 # documented as a good place to install new fonts 

151 "/usr/local/share/fonts/", 

152 # common application, not really useful 

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

154 # user fonts 

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

156 / "fonts"), 

157 str(_HOME / ".fonts"), 

158] 

159OSXFontDirectories = [ 

160 "/Library/Fonts/", 

161 "/Network/Library/Fonts/", 

162 "/System/Library/Fonts/", 

163 # fonts installed via MacPorts 

164 "/opt/local/share/fonts", 

165 # user fonts 

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

167] 

168 

169 

170def get_fontext_synonyms(fontext): 

171 """ 

172 Return a list of file extensions that are synonyms for 

173 the given file extension *fileext*. 

174 """ 

175 return { 

176 'afm': ['afm'], 

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

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

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

180 }[fontext] 

181 

182 

183def list_fonts(directory, extensions): 

184 """ 

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

186 recursively under the directory. 

187 """ 

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

189 if sys.platform == 'win32' and directory == win32FontDirectory(): 

190 return [os.path.join(directory, filename) 

191 for filename in os.listdir(directory) 

192 if os.path.isfile(filename)] 

193 else: 

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

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

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

197 for filename in filenames 

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

199 

200 

201def win32FontDirectory(): 

202 r""" 

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

204 looked up from the registry key :: 

205 

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

207 

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

209 """ 

210 import winreg 

211 try: 

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

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

214 except OSError: 

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

216 

217 

218def _win32RegistryFonts(reg_domain, base_dir): 

219 r""" 

220 Search for fonts in the Windows registry. 

221 

222 Parameters 

223 ---------- 

224 reg_domain : int 

225 The top level registry domain (e.g. HKEY_LOCAL_MACHINE). 

226 

227 base_dir : str 

228 The path to the folder where the font files are usually located (e.g. 

229 C:\Windows\Fonts). If only the filename of the font is stored in the 

230 registry, the absolute path is built relative to this base directory. 

231 

232 Returns 

233 ------- 

234 `set` 

235 `pathlib.Path` objects with the absolute path to the font files found. 

236 

237 """ 

238 import winreg 

239 items = set() 

240 

241 for reg_path in MSFontDirectories: 

242 try: 

243 with winreg.OpenKey(reg_domain, reg_path) as local: 

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

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

246 # absolute path. 

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

248 if not isinstance(value, str): 

249 continue 

250 try: 

251 # If value contains already an absolute path, then it 

252 # is not changed further. 

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

254 except RuntimeError: 

255 # Don't fail with invalid entries. 

256 continue 

257 

258 items.add(path) 

259 except (OSError, MemoryError): 

260 continue 

261 

262 return items 

263 

264 

265# Also remove _win32RegistryFonts when this is removed. 

266@_api.deprecated("3.5") 

267def win32InstalledFonts(directory=None, fontext='ttf'): 

268 """ 

269 Search for fonts in the specified font directory, or use the 

270 system directories if none given. Additionally, it is searched for user 

271 fonts installed. A list of TrueType font filenames are returned by default, 

272 or AFM fonts if *fontext* == 'afm'. 

273 """ 

274 import winreg 

275 

276 if directory is None: 

277 directory = win32FontDirectory() 

278 

279 fontext = ['.' + ext for ext in get_fontext_synonyms(fontext)] 

280 

281 items = set() 

282 

283 # System fonts 

284 items.update(_win32RegistryFonts(winreg.HKEY_LOCAL_MACHINE, directory)) 

285 

286 # User fonts 

287 for userdir in MSUserFontDirectories: 

288 items.update(_win32RegistryFonts(winreg.HKEY_CURRENT_USER, userdir)) 

289 

290 # Keep only paths with matching file extension. 

291 return [str(path) for path in items if path.suffix.lower() in fontext] 

292 

293 

294def _get_win32_installed_fonts(): 

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

296 import winreg 

297 items = set() 

298 # Search and resolve fonts listed in the registry. 

299 for domain, base_dirs in [ 

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

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

302 ]: 

303 for base_dir in base_dirs: 

304 for reg_path in MSFontDirectories: 

305 try: 

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

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

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

309 # absolute path. 

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

311 if not isinstance(value, str): 

312 continue 

313 try: 

314 # If value contains already an absolute path, 

315 # then it is not changed further. 

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

317 except RuntimeError: 

318 # Don't fail with invalid entries. 

319 continue 

320 items.add(path) 

321 except (OSError, MemoryError): 

322 continue 

323 return items 

324 

325 

326@lru_cache() 

327def _get_fontconfig_fonts(): 

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

329 try: 

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

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

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

333 return [] 

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

335 except (OSError, subprocess.CalledProcessError): 

336 return [] 

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

338 

339 

340@_api.deprecated("3.5") 

341def get_fontconfig_fonts(fontext='ttf'): 

342 """List font filenames known to ``fc-list`` having the given extension.""" 

343 fontext = ['.' + ext for ext in get_fontext_synonyms(fontext)] 

344 return [str(path) for path in _get_fontconfig_fonts() 

345 if path.suffix.lower() in fontext] 

346 

347 

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

349 """ 

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

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

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

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

354 AFM fonts as an option. 

355 """ 

356 fontfiles = set() 

357 fontexts = get_fontext_synonyms(fontext) 

358 

359 if fontpaths is None: 

360 if sys.platform == 'win32': 

361 installed_fonts = _get_win32_installed_fonts() 

362 fontpaths = MSUserFontDirectories + [win32FontDirectory()] 

363 else: 

364 installed_fonts = _get_fontconfig_fonts() 

365 if sys.platform == 'darwin': 

366 fontpaths = [*X11FontDirectories, *OSXFontDirectories] 

367 else: 

368 fontpaths = X11FontDirectories 

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

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

371 

372 elif isinstance(fontpaths, str): 

373 fontpaths = [fontpaths] 

374 

375 for path in fontpaths: 

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

377 

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

379 

380 

381def _fontentry_helper_repr_png(fontent): 

382 from matplotlib.figure import Figure # Circular import. 

383 fig = Figure() 

384 font_path = Path(fontent.fname) if fontent.fname != '' else None 

385 fig.text(0, 0, fontent.name, font=font_path) 

386 with BytesIO() as buf: 

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

388 return buf.getvalue() 

389 

390 

391def _fontentry_helper_repr_html(fontent): 

392 png_stream = _fontentry_helper_repr_png(fontent) 

393 png_b64 = b64encode(png_stream).decode() 

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

395 

396 

397FontEntry = dataclasses.make_dataclass( 

398 'FontEntry', [ 

399 ('fname', str, dataclasses.field(default='')), 

400 ('name', str, dataclasses.field(default='')), 

401 ('style', str, dataclasses.field(default='normal')), 

402 ('variant', str, dataclasses.field(default='normal')), 

403 ('weight', str, dataclasses.field(default='normal')), 

404 ('stretch', str, dataclasses.field(default='normal')), 

405 ('size', str, dataclasses.field(default='medium')), 

406 ], 

407 namespace={ 

408 '__doc__': """ 

409 A class for storing Font properties. 

410 

411 It is used when populating the font lookup dictionary. 

412 """, 

413 '_repr_html_': lambda self: _fontentry_helper_repr_html(self), 

414 '_repr_png_': lambda self: _fontentry_helper_repr_png(self), 

415 } 

416) 

417 

418 

419def ttfFontProperty(font): 

420 """ 

421 Extract information from a TrueType font file. 

422 

423 Parameters 

424 ---------- 

425 font : `.FT2Font` 

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

427 

428 Returns 

429 ------- 

430 `FontEntry` 

431 The extracted font properties. 

432 

433 """ 

434 name = font.family_name 

435 

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

437 

438 sfnt = font.get_sfnt() 

439 mac_key = (1, # platform: macintosh 

440 0, # id: roman 

441 0) # langid: english 

442 ms_key = (3, # platform: microsoft 

443 1, # id: unicode_cs 

444 0x0409) # langid: english_united_states 

445 

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

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

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

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

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

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

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

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

454 

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

456 style = 'oblique' 

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

458 style = 'italic' 

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

460 style = 'normal' 

461 elif font.style_flags & ft2font.ITALIC: 

462 style = 'italic' 

463 else: 

464 style = 'normal' 

465 

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

467 

468 # !!!! Untested 

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

470 variant = 'small-caps' 

471 else: 

472 variant = 'normal' 

473 

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

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

476 wws_subfamily = 22 

477 typographic_subfamily = 16 

478 font_subfamily = 2 

479 styles = [ 

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

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

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

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

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

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

486 ] 

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

488 

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

490 # OS/2 table weight. 

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

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

493 return os2["usWeightClass"] 

494 # PostScript font info weight. 

495 try: 

496 ps_font_info_weight = ( 

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

498 except ValueError: 

499 pass 

500 else: 

501 for regex, weight in _weight_regexes: 

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

503 return weight 

504 # Style name weight. 

505 for style in styles: 

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

507 for regex, weight in _weight_regexes: 

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

509 return weight 

510 if font.style_flags & ft2font.BOLD: 

511 return 700 # "bold" 

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

513 

514 weight = int(get_weight()) 

515 

516 # Stretch can be absolute and relative 

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

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

519 # and ultra-expanded. 

520 # Relative stretches are: wider, narrower 

521 # Child value is: inherit 

522 

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

524 stretch = 'condensed' 

525 elif 'demi cond' in sfnt4: 

526 stretch = 'semi-condensed' 

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

528 stretch = 'expanded' 

529 else: 

530 stretch = 'normal' 

531 

532 # Sizes can be absolute and relative. 

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

534 # and xx-large. 

535 # Relative sizes are: larger, smaller 

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

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

538 

539 if not font.scalable: 

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

541 size = 'scalable' 

542 

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

544 

545 

546def afmFontProperty(fontpath, font): 

547 """ 

548 Extract information from an AFM font file. 

549 

550 Parameters 

551 ---------- 

552 font : AFM 

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

554 

555 Returns 

556 ------- 

557 `FontEntry` 

558 The extracted font properties. 

559 """ 

560 

561 name = font.get_familyname() 

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

563 

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

565 

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

567 style = 'italic' 

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

569 style = 'oblique' 

570 else: 

571 style = 'normal' 

572 

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

574 

575 # !!!! Untested 

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

577 variant = 'small-caps' 

578 else: 

579 variant = 'normal' 

580 

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

582 if weight not in weight_dict: 

583 weight = 'normal' 

584 

585 # Stretch can be absolute and relative 

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

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

588 # and ultra-expanded. 

589 # Relative stretches are: wider, narrower 

590 # Child value is: inherit 

591 if 'demi cond' in fontname: 

592 stretch = 'semi-condensed' 

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

594 stretch = 'condensed' 

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

596 stretch = 'expanded' 

597 else: 

598 stretch = 'normal' 

599 

600 # Sizes can be absolute and relative. 

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

602 # and xx-large. 

603 # Relative sizes are: larger, smaller 

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

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

606 

607 # All AFM fonts are apparently scalable. 

608 

609 size = 'scalable' 

610 

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

612 

613 

614class FontProperties: 

615 """ 

616 A class for storing and manipulating font properties. 

617 

618 The font properties are the six properties described in the 

619 `W3C Cascading Style Sheet, Level 1 

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

621 specification and *math_fontfamily* for math fonts: 

622 

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

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

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

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

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

628 

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

630 Default: :rc:`font.style` 

631 

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

633 Default: :rc:`font.variant` 

634 

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

636 'ultra-condensed', 'extra-condensed', 'condensed', 

637 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 

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

639 

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

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

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

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

644 

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

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

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

648 

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

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

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

652 

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

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

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

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

657 

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

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

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

661 on the font manager's default font size. 

662 

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

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

665 merely borrowing its pattern syntax for use here. 

666 

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

668 .. _pattern: 

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

670 

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

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

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

674 fontconfig. 

675 """ 

676 

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

678 stretch=None, size=None, 

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

680 math_fontfamily=None): 

681 self.set_family(family) 

682 self.set_style(style) 

683 self.set_variant(variant) 

684 self.set_weight(weight) 

685 self.set_stretch(stretch) 

686 self.set_file(fname) 

687 self.set_size(size) 

688 self.set_math_fontfamily(math_fontfamily) 

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

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

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

692 if (isinstance(family, str) 

693 and style is None and variant is None and weight is None 

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

695 self.set_fontconfig_pattern(family) 

696 

697 @classmethod 

698 def _from_any(cls, arg): 

699 """ 

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

701 following: 

702 

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

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

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

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

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

708 """ 

709 if isinstance(arg, cls): 

710 return arg 

711 elif arg is None: 

712 return cls() 

713 elif isinstance(arg, os.PathLike): 

714 return cls(fname=arg) 

715 elif isinstance(arg, str): 

716 return cls(arg) 

717 else: 

718 return cls(**arg) 

719 

720 def __hash__(self): 

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

722 self.get_slant(), 

723 self.get_variant(), 

724 self.get_weight(), 

725 self.get_stretch(), 

726 self.get_size(), 

727 self.get_file(), 

728 self.get_math_fontfamily()) 

729 return hash(l) 

730 

731 def __eq__(self, other): 

732 return hash(self) == hash(other) 

733 

734 def __str__(self): 

735 return self.get_fontconfig_pattern() 

736 

737 def get_family(self): 

738 """ 

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

740 

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

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

743 the order of preference. 

744 """ 

745 return self._family 

746 

747 def get_name(self): 

748 """ 

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

750 """ 

751 return get_font(findfont(self)).family_name 

752 

753 def get_style(self): 

754 """ 

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

756 """ 

757 return self._slant 

758 

759 def get_variant(self): 

760 """ 

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

762 """ 

763 return self._variant 

764 

765 def get_weight(self): 

766 """ 

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

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

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

770 'heavy', 'extra bold', 'black' 

771 """ 

772 return self._weight 

773 

774 def get_stretch(self): 

775 """ 

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

777 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 

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

779 """ 

780 return self._stretch 

781 

782 def get_size(self): 

783 """ 

784 Return the font size. 

785 """ 

786 return self._size 

787 

788 def get_file(self): 

789 """ 

790 Return the filename of the associated font. 

791 """ 

792 return self._file 

793 

794 def get_fontconfig_pattern(self): 

795 """ 

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

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

798 

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

800 pattern syntax for use here. 

801 """ 

802 return generate_fontconfig_pattern(self) 

803 

804 def set_family(self, family): 

805 """ 

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

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

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

809 font names. Real font names are not supported when 

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

811 """ 

812 if family is None: 

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

814 if isinstance(family, str): 

815 family = [family] 

816 self._family = family 

817 

818 def set_style(self, style): 

819 """ 

820 Set the font style. 

821 

822 Parameters 

823 ---------- 

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

825 """ 

826 if style is None: 

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

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

829 self._slant = style 

830 

831 def set_variant(self, variant): 

832 """ 

833 Set the font variant. 

834 

835 Parameters 

836 ---------- 

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

838 """ 

839 if variant is None: 

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

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

842 self._variant = variant 

843 

844 def set_weight(self, weight): 

845 """ 

846 Set the font weight. 

847 

848 Parameters 

849 ---------- 

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

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

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

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

854 """ 

855 if weight is None: 

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

857 if weight in weight_dict: 

858 self._weight = weight 

859 return 

860 try: 

861 weight = int(weight) 

862 except ValueError: 

863 pass 

864 else: 

865 if 0 <= weight <= 1000: 

866 self._weight = weight 

867 return 

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

869 

870 def set_stretch(self, stretch): 

871 """ 

872 Set the font stretch or width. 

873 

874 Parameters 

875 ---------- 

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

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

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

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

880 """ 

881 if stretch is None: 

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

883 if stretch in stretch_dict: 

884 self._stretch = stretch 

885 return 

886 try: 

887 stretch = int(stretch) 

888 except ValueError: 

889 pass 

890 else: 

891 if 0 <= stretch <= 1000: 

892 self._stretch = stretch 

893 return 

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

895 

896 def set_size(self, size): 

897 """ 

898 Set the font size. 

899 

900 Parameters 

901 ---------- 

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

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

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

905 sizes relative to the default font size. 

906 """ 

907 if size is None: 

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

909 try: 

910 size = float(size) 

911 except ValueError: 

912 try: 

913 scale = font_scalings[size] 

914 except KeyError as err: 

915 raise ValueError( 

916 "Size is invalid. Valid font size are " 

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

918 else: 

919 size = scale * FontManager.get_default_size() 

920 if size < 1.0: 

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

922 'Setting fontsize = 1 pt', size) 

923 size = 1.0 

924 self._size = size 

925 

926 def set_file(self, file): 

927 """ 

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

929 other properties will be ignored. 

930 """ 

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

932 

933 def set_fontconfig_pattern(self, pattern): 

934 """ 

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

936 

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

938 pattern syntax for use here. 

939 """ 

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

941 if type(val) == list: 

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

943 else: 

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

945 

946 def get_math_fontfamily(self): 

947 """ 

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

949 

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

951 """ 

952 return self._math_fontfamily 

953 

954 def set_math_fontfamily(self, fontfamily): 

955 """ 

956 Set the font family for text in math mode. 

957 

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

959 

960 Parameters 

961 ---------- 

962 fontfamily : str 

963 The name of the font family. 

964 

965 Available font families are defined in the 

966 matplotlibrc.template file 

967 :ref:`here <customizing-with-matplotlibrc-files>` 

968 

969 See Also 

970 -------- 

971 .text.Text.get_math_fontfamily 

972 """ 

973 if fontfamily is None: 

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

975 else: 

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

977 # _check_in_list() Validates the parameter math_fontfamily as 

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

979 _api.check_in_list(valid_fonts, math_fontfamily=fontfamily) 

980 self._math_fontfamily = fontfamily 

981 

982 def copy(self): 

983 """Return a copy of self.""" 

984 return copy.copy(self) 

985 

986 # Aliases 

987 set_name = set_family 

988 get_slant = get_style 

989 set_slant = set_style 

990 get_size_in_points = get_size 

991 

992 

993class _JSONEncoder(json.JSONEncoder): 

994 def default(self, o): 

995 if isinstance(o, FontManager): 

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

997 elif isinstance(o, FontEntry): 

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

999 try: 

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

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

1002 d["fname"] = str( 

1003 Path(d["fname"]).relative_to(mpl.get_data_path())) 

1004 except ValueError: 

1005 pass 

1006 return d 

1007 else: 

1008 return super().default(o) 

1009 

1010 

1011def _json_decode(o): 

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

1013 if cls is None: 

1014 return o 

1015 elif cls == 'FontManager': 

1016 r = FontManager.__new__(FontManager) 

1017 r.__dict__.update(o) 

1018 return r 

1019 elif cls == 'FontEntry': 

1020 r = FontEntry.__new__(FontEntry) 

1021 r.__dict__.update(o) 

1022 if not os.path.isabs(r.fname): 

1023 r.fname = os.path.join(mpl.get_data_path(), r.fname) 

1024 return r 

1025 else: 

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

1027 

1028 

1029def json_dump(data, filename): 

1030 """ 

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

1032 

1033 See Also 

1034 -------- 

1035 json_load 

1036 

1037 Notes 

1038 ----- 

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

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

1041 valid across virtualenvs). 

1042 

1043 This function temporarily locks the output file to prevent multiple 

1044 processes from overwriting one another's output. 

1045 """ 

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

1047 try: 

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

1049 except OSError as e: 

1050 _log.warning('Could not save font_manager cache {}'.format(e)) 

1051 

1052 

1053def json_load(filename): 

1054 """ 

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

1056 

1057 See Also 

1058 -------- 

1059 json_dump 

1060 """ 

1061 with open(filename) as fh: 

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

1063 

1064 

1065class FontManager: 

1066 """ 

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

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

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

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

1071 font is returned. 

1072 """ 

1073 # Increment this version number whenever the font cache data 

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

1075 # cache files to be rebuilt. 

1076 __version__ = 330 

1077 

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

1079 self._version = self.__version__ 

1080 

1081 self.__default_weight = weight 

1082 self.default_size = size 

1083 

1084 # Create list of font paths. 

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

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

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

1088 

1089 self.defaultFamily = { 

1090 'ttf': 'DejaVu Sans', 

1091 'afm': 'Helvetica'} 

1092 

1093 self.afmlist = [] 

1094 self.ttflist = [] 

1095 

1096 # Delay the warning by 5s. 

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

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

1099 timer.start() 

1100 try: 

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

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

1103 *findSystemFonts(fontext=fontext)]: 

1104 try: 

1105 self.addfont(path) 

1106 except OSError as exc: 

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

1108 except Exception as exc: 

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

1110 "%s", path, exc) 

1111 finally: 

1112 timer.cancel() 

1113 

1114 def addfont(self, path): 

1115 """ 

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

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

1118 

1119 Parameters 

1120 ---------- 

1121 path : str or path-like 

1122 """ 

1123 # Convert to string in case of a path as 

1124 # afmFontProperty and FT2Font expect this 

1125 path = os.fsdecode(path) 

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

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

1128 font = _afm.AFM(fh) 

1129 prop = afmFontProperty(path, font) 

1130 self.afmlist.append(prop) 

1131 else: 

1132 font = ft2font.FT2Font(path) 

1133 prop = ttfFontProperty(font) 

1134 self.ttflist.append(prop) 

1135 self._findfont_cached.cache_clear() 

1136 

1137 @property 

1138 def defaultFont(self): 

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

1140 # the venv path in the json serialization. 

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

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

1143 

1144 def get_default_weight(self): 

1145 """ 

1146 Return the default font weight. 

1147 """ 

1148 return self.__default_weight 

1149 

1150 @staticmethod 

1151 def get_default_size(): 

1152 """ 

1153 Return the default font size. 

1154 """ 

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

1156 

1157 def set_default_weight(self, weight): 

1158 """ 

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

1160 """ 

1161 self.__default_weight = weight 

1162 

1163 @staticmethod 

1164 def _expand_aliases(family): 

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

1166 family = 'sans-serif' 

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

1168 

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

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

1171 def score_family(self, families, family2): 

1172 """ 

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

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

1175 

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

1177 

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

1179 

1180 No match will return 1.0. 

1181 """ 

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

1183 families = [families] 

1184 elif len(families) == 0: 

1185 return 1.0 

1186 family2 = family2.lower() 

1187 step = 1 / len(families) 

1188 for i, family1 in enumerate(families): 

1189 family1 = family1.lower() 

1190 if family1 in font_family_aliases: 

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

1192 if family2 in options: 

1193 idx = options.index(family2) 

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

1195 elif family1 == family2: 

1196 # The score should be weighted by where in the 

1197 # list the font was found. 

1198 return i * step 

1199 return 1.0 

1200 

1201 def score_style(self, style1, style2): 

1202 """ 

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

1204 

1205 An exact match returns 0.0. 

1206 

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

1208 

1209 No match returns 1.0. 

1210 """ 

1211 if style1 == style2: 

1212 return 0.0 

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

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

1215 return 0.1 

1216 return 1.0 

1217 

1218 def score_variant(self, variant1, variant2): 

1219 """ 

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

1221 

1222 An exact match returns 0.0, otherwise 1.0. 

1223 """ 

1224 if variant1 == variant2: 

1225 return 0.0 

1226 else: 

1227 return 1.0 

1228 

1229 def score_stretch(self, stretch1, stretch2): 

1230 """ 

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

1232 

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

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

1235 between 0.0 and 1.0. 

1236 """ 

1237 try: 

1238 stretchval1 = int(stretch1) 

1239 except ValueError: 

1240 stretchval1 = stretch_dict.get(stretch1, 500) 

1241 try: 

1242 stretchval2 = int(stretch2) 

1243 except ValueError: 

1244 stretchval2 = stretch_dict.get(stretch2, 500) 

1245 return abs(stretchval1 - stretchval2) / 1000.0 

1246 

1247 def score_weight(self, weight1, weight2): 

1248 """ 

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

1250 

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

1252 and have the same value. 

1253 

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

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

1256 0.05 and 1.0. 

1257 """ 

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

1259 if cbook._str_equal(weight1, weight2): 

1260 return 0.0 

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

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

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

1264 

1265 def score_size(self, size1, size2): 

1266 """ 

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

1268 

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

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

1271 

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

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

1274 72pt) will lie between 0.0 and 1.0. 

1275 """ 

1276 if size2 == 'scalable': 

1277 return 0.0 

1278 # Size value should have already been 

1279 try: 

1280 sizeval1 = float(size1) 

1281 except ValueError: 

1282 sizeval1 = self.default_size * font_scalings[size1] 

1283 try: 

1284 sizeval2 = float(size2) 

1285 except ValueError: 

1286 return 1.0 

1287 return abs(sizeval1 - sizeval2) / 72 

1288 

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

1290 fallback_to_default=True, rebuild_if_missing=True): 

1291 """ 

1292 Find a font that most closely matches the given font properties. 

1293 

1294 Parameters 

1295 ---------- 

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

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

1298 `.FontProperties` object or a string defining a 

1299 `fontconfig patterns`_. 

1300 

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

1302 The extension of the font file: 

1303 

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

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

1306 

1307 directory : str, optional 

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

1309 

1310 fallback_to_default : bool 

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

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

1313 

1314 rebuild_if_missing : bool 

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

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

1317 contains outdated entries). 

1318 

1319 Returns 

1320 ------- 

1321 str 

1322 The filename of the best matching font. 

1323 

1324 Notes 

1325 ----- 

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

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

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

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

1330 returned. 

1331 

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

1333 perform the O(n) nearest neighbor search. 

1334 

1335 See the `W3C Cascading Style Sheet, Level 1 

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

1337 for a description of the font finding algorithm. 

1338 

1339 .. _fontconfig patterns: 

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

1341 """ 

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

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

1344 # rcParam was changed. 

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

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

1347 "font.monospace"]) 

1348 ret = self._findfont_cached( 

1349 prop, fontext, directory, fallback_to_default, rebuild_if_missing, 

1350 rc_params) 

1351 if isinstance(ret, Exception): 

1352 raise ret 

1353 return ret 

1354 

1355 def get_font_names(self): 

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

1357 return list(set([font.name for font in self.ttflist])) 

1358 

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

1360 fallback_to_default=True, rebuild_if_missing=True): 

1361 """ 

1362 Find font families that most closely match the given properties. 

1363 

1364 Parameters 

1365 ---------- 

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

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

1368 `.FontProperties` object or a string defining a 

1369 `fontconfig patterns`_. 

1370 

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

1372 The extension of the font file: 

1373 

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

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

1376 

1377 directory : str, optional 

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

1379 

1380 fallback_to_default : bool 

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

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

1383 

1384 rebuild_if_missing : bool 

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

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

1387 contains outdated entries). 

1388 

1389 Returns 

1390 ------- 

1391 list[str] 

1392 The paths of the fonts found 

1393 

1394 Notes 

1395 ----- 

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

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

1398 returns a dict containing multiple fonts and their filepaths 

1399 which closely match the given font properties. Since this internally 

1400 uses the original API, there's no change to the logic of performing the 

1401 nearest neighbor search. See `findfont` for more details. 

1402 """ 

1403 

1404 prop = FontProperties._from_any(prop) 

1405 

1406 fpaths = [] 

1407 for family in prop.get_family(): 

1408 cprop = prop.copy() 

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

1410 

1411 try: 

1412 fpaths.append( 

1413 self.findfont( 

1414 cprop, fontext, directory, 

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

1416 rebuild_if_missing=rebuild_if_missing, 

1417 ) 

1418 ) 

1419 except ValueError: 

1420 if family in font_family_aliases: 

1421 _log.warning( 

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

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

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

1425 ) 

1426 else: 

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

1428 

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

1430 # fallback_to_default is enabled 

1431 if not fpaths: 

1432 if fallback_to_default: 

1433 dfamily = self.defaultFamily[fontext] 

1434 cprop = prop.copy() 

1435 cprop.set_family(dfamily) 

1436 fpaths.append( 

1437 self.findfont( 

1438 cprop, fontext, directory, 

1439 fallback_to_default=True, 

1440 rebuild_if_missing=rebuild_if_missing, 

1441 ) 

1442 ) 

1443 else: 

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

1445 "to the default font was disabled") 

1446 

1447 return fpaths 

1448 

1449 @lru_cache(1024) 

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

1451 rebuild_if_missing, rc_params): 

1452 

1453 prop = FontProperties._from_any(prop) 

1454 

1455 fname = prop.get_file() 

1456 if fname is not None: 

1457 return fname 

1458 

1459 if fontext == 'afm': 

1460 fontlist = self.afmlist 

1461 else: 

1462 fontlist = self.ttflist 

1463 

1464 best_score = 1e64 

1465 best_font = None 

1466 

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

1468 for font in fontlist: 

1469 if (directory is not None and 

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

1471 continue 

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

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

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

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

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

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

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

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

1480 if score < best_score: 

1481 best_score = score 

1482 best_font = font 

1483 if score == 0: 

1484 break 

1485 

1486 if best_font is None or best_score >= 10.0: 

1487 if fallback_to_default: 

1488 _log.warning( 

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

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

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

1492 if family in font_family_aliases: 

1493 _log.warning( 

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

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

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

1497 default_prop = prop.copy() 

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

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

1500 fallback_to_default=False) 

1501 else: 

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

1503 # cache the resulting exception, which will not occur if it was 

1504 # actually raised. 

1505 return ValueError(f"Failed to find font {prop}, and fallback " 

1506 f"to the default font was disabled") 

1507 else: 

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

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

1510 result = best_font.fname 

1511 

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

1513 if rebuild_if_missing: 

1514 _log.info( 

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

1516 new_fm = _load_fontmanager(try_read_cache=False) 

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

1518 # a reference to this specific instance. 

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

1520 # modifying the instance in place. 

1521 vars(self).update(vars(new_fm)) 

1522 return self.findfont( 

1523 prop, fontext, directory, rebuild_if_missing=False) 

1524 else: 

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

1526 # cache the resulting exception, which will not occur if it was 

1527 # actually raised. 

1528 return ValueError("No valid font could be found") 

1529 

1530 return _cached_realpath(result) 

1531 

1532 

1533@lru_cache() 

1534def is_opentype_cff_font(filename): 

1535 """ 

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

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

1538 that can not subset these fonts. 

1539 """ 

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

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

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

1543 else: 

1544 return False 

1545 

1546 

1547@lru_cache(64) 

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

1549 first_fontpath, *rest = font_filepaths 

1550 return ft2font.FT2Font( 

1551 first_fontpath, hinting_factor, 

1552 _fallback_list=[ 

1553 ft2font.FT2Font( 

1554 fpath, hinting_factor, 

1555 _kerning_factor=_kerning_factor 

1556 ) 

1557 for fpath in rest 

1558 ], 

1559 _kerning_factor=_kerning_factor 

1560 ) 

1561 

1562 

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

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

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

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

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

1568if hasattr(os, "register_at_fork"): 

1569 os.register_at_fork(after_in_child=_get_font.cache_clear) 

1570 

1571 

1572@lru_cache(64) 

1573def _cached_realpath(path): 

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

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

1576 return os.path.realpath(path) 

1577 

1578 

1579@_api.rename_parameter('3.6', "filepath", "font_filepaths") 

1580def get_font(font_filepaths, hinting_factor=None): 

1581 """ 

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

1583 

1584 Parameters 

1585 ---------- 

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

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

1588 

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

1590 as a list with that entry only. 

1591 

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

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

1594 glyph. 

1595 

1596 Returns 

1597 ------- 

1598 `.ft2font.FT2Font` 

1599 

1600 """ 

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

1602 paths = (_cached_realpath(font_filepaths),) 

1603 else: 

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

1605 

1606 if hinting_factor is None: 

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

1608 

1609 return _get_font( 

1610 # must be a tuple to be cached 

1611 paths, 

1612 hinting_factor, 

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

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

1615 thread_id=threading.get_ident() 

1616 ) 

1617 

1618 

1619def _load_fontmanager(*, try_read_cache=True): 

1620 fm_path = Path( 

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

1622 if try_read_cache: 

1623 try: 

1624 fm = json_load(fm_path) 

1625 except Exception: 

1626 pass 

1627 else: 

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

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

1630 return fm 

1631 fm = FontManager() 

1632 json_dump(fm, fm_path) 

1633 _log.info("generated new fontManager") 

1634 return fm 

1635 

1636 

1637fontManager = _load_fontmanager() 

1638findfont = fontManager.findfont 

1639get_font_names = fontManager.get_font_names