Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

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

3 

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

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

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

7system font path that matches the specified `FontProperties` 

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

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

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 functools import lru_cache 

27import json 

28import logging 

29from numbers import Number 

30import os 

31from pathlib import Path 

32import subprocess 

33import sys 

34try: 

35 from threading import Timer 

36except ImportError: 

37 from dummy_threading import Timer 

38 

39import matplotlib as mpl 

40from matplotlib import afm, cbook, ft2font, rcParams 

41from matplotlib.fontconfig_pattern import ( 

42 parse_fontconfig_pattern, generate_fontconfig_pattern) 

43 

44_log = logging.getLogger(__name__) 

45 

46font_scalings = { 

47 'xx-small': 0.579, 

48 'x-small': 0.694, 

49 'small': 0.833, 

50 'medium': 1.0, 

51 'large': 1.200, 

52 'x-large': 1.440, 

53 'xx-large': 1.728, 

54 'larger': 1.2, 

55 'smaller': 0.833, 

56 None: 1.0, 

57} 

58stretch_dict = { 

59 'ultra-condensed': 100, 

60 'extra-condensed': 200, 

61 'condensed': 300, 

62 'semi-condensed': 400, 

63 'normal': 500, 

64 'semi-expanded': 600, 

65 'semi-extended': 600, 

66 'expanded': 700, 

67 'extended': 700, 

68 'extra-expanded': 800, 

69 'extra-extended': 800, 

70 'ultra-expanded': 900, 

71 'ultra-extended': 900, 

72} 

73weight_dict = { 

74 'ultralight': 100, 

75 'light': 200, 

76 'normal': 400, 

77 'regular': 400, 

78 'book': 400, 

79 'medium': 500, 

80 'roman': 500, 

81 'semibold': 600, 

82 'demibold': 600, 

83 'demi': 600, 

84 'bold': 700, 

85 'heavy': 800, 

86 'extra bold': 800, 

87 'black': 900, 

88} 

89font_family_aliases = { 

90 'serif', 

91 'sans-serif', 

92 'sans serif', 

93 'cursive', 

94 'fantasy', 

95 'monospace', 

96 'sans', 

97} 

98# OS Font paths 

99MSFolders = \ 

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

101MSFontDirectories = [ 

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

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

104MSUserFontDirectories = [ 

105 str(Path.home() / 'AppData/Local/Microsoft/Windows/Fonts'), 

106 str(Path.home() / 'AppData/Roaming/Microsoft/Windows/Fonts'), 

107] 

108X11FontDirectories = [ 

109 # an old standard installation point 

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

111 "/usr/X11/lib/X11/fonts", 

112 # here is the new standard location for fonts 

113 "/usr/share/fonts/", 

114 # documented as a good place to install new fonts 

115 "/usr/local/share/fonts/", 

116 # common application, not really useful 

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

118 # user fonts 

119 str(Path(os.environ.get('XDG_DATA_HOME', 

120 Path.home() / ".local/share")) / "fonts"), 

121 str(Path.home() / ".fonts"), 

122] 

123OSXFontDirectories = [ 

124 "/Library/Fonts/", 

125 "/Network/Library/Fonts/", 

126 "/System/Library/Fonts/", 

127 # fonts installed via MacPorts 

128 "/opt/local/share/fonts", 

129 # user fonts 

130 str(Path.home() / "Library/Fonts"), 

131] 

132 

133 

134def get_fontext_synonyms(fontext): 

135 """ 

136 Return a list of file extensions extensions that are synonyms for 

137 the given file extension *fileext*. 

138 """ 

139 return { 

140 'afm': ['afm'], 

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

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

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

144 }[fontext] 

145 

146 

147def list_fonts(directory, extensions): 

148 """ 

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

150 recursively under the directory. 

151 """ 

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

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

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

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

156 for filename in filenames 

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

158 

159 

160def win32FontDirectory(): 

161 r""" 

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

163 looked up from the registry key :: 

164 

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

166 

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

168 """ 

169 import winreg 

170 try: 

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

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

173 except OSError: 

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

175 

176 

177def _win32RegistryFonts(reg_domain, base_dir): 

178 r""" 

179 Searches for fonts in the Windows registry. 

180 

181 Parameters 

182 ---------- 

183 reg_domain : int 

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

185 

186 base_dir : str 

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

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

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

190 

191 Returns 

192 ------- 

193 `set` 

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

195 

196 """ 

197 import winreg 

198 items = set() 

199 

200 for reg_path in MSFontDirectories: 

201 try: 

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

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

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

205 # absolute path. 

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

207 if not isinstance(value, str): 

208 continue 

209 

210 # Work around for https://bugs.python.org/issue25778, which 

211 # is fixed in Py>=3.6.1. 

212 value = value.split("\0", 1)[0] 

213 

214 try: 

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

216 # is not changed further. 

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

218 except RuntimeError: 

219 # Don't fail with invalid entries. 

220 continue 

221 

222 items.add(path) 

223 except (OSError, MemoryError): 

224 continue 

225 

226 return items 

227 

228 

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

230 """ 

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

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

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

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

235 """ 

236 import winreg 

237 

238 if directory is None: 

239 directory = win32FontDirectory() 

240 

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

242 

243 items = set() 

244 

245 # System fonts 

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

247 

248 # User fonts 

249 for userdir in MSUserFontDirectories: 

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

251 

252 # Keep only paths with matching file extension. 

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

254 

255 

256@cbook.deprecated("3.1") 

257def OSXInstalledFonts(directories=None, fontext='ttf'): 

258 """Get list of font files on OS X.""" 

259 if directories is None: 

260 directories = OSXFontDirectories 

261 return [path 

262 for directory in directories 

263 for path in list_fonts(directory, get_fontext_synonyms(fontext))] 

264 

265 

266@lru_cache() 

267def _call_fc_list(): 

268 """Cache and list the font filenames known to `fc-list`. 

269 """ 

270 # Delay the warning by 5s. 

271 timer = Timer(5, lambda: _log.warning( 

272 'Matplotlib is building the font cache using fc-list. ' 

273 'This may take a moment.')) 

274 timer.start() 

275 try: 

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

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

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

279 return [] 

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

281 except (OSError, subprocess.CalledProcessError): 

282 return [] 

283 finally: 

284 timer.cancel() 

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

286 

287 

288def get_fontconfig_fonts(fontext='ttf'): 

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

290 """ 

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

292 return [fname for fname in _call_fc_list() 

293 if Path(fname).suffix.lower() in fontext] 

294 

295 

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

297 """ 

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

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

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

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

302 AFM fonts as an option. 

303 """ 

304 fontfiles = set() 

305 fontexts = get_fontext_synonyms(fontext) 

306 

307 if fontpaths is None: 

308 if sys.platform == 'win32': 

309 fontpaths = MSUserFontDirectories + [win32FontDirectory()] 

310 # now get all installed fonts directly... 

311 fontfiles.update(win32InstalledFonts(fontext=fontext)) 

312 else: 

313 fontpaths = X11FontDirectories 

314 if sys.platform == 'darwin': 

315 fontpaths = [*X11FontDirectories, *OSXFontDirectories] 

316 fontfiles.update(get_fontconfig_fonts(fontext)) 

317 

318 elif isinstance(fontpaths, str): 

319 fontpaths = [fontpaths] 

320 

321 for path in fontpaths: 

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

323 

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

325 

326 

327class FontEntry: 

328 """ 

329 A class for storing Font properties. It is used when populating 

330 the font lookup dictionary. 

331 """ 

332 def __init__(self, 

333 fname ='', 

334 name ='', 

335 style ='normal', 

336 variant='normal', 

337 weight ='normal', 

338 stretch='normal', 

339 size ='medium', 

340 ): 

341 self.fname = fname 

342 self.name = name 

343 self.style = style 

344 self.variant = variant 

345 self.weight = weight 

346 self.stretch = stretch 

347 try: 

348 self.size = str(float(size)) 

349 except ValueError: 

350 self.size = size 

351 

352 def __repr__(self): 

353 return "<Font '%s' (%s) %s %s %s %s>" % ( 

354 self.name, os.path.basename(self.fname), self.style, self.variant, 

355 self.weight, self.stretch) 

356 

357 

358def ttfFontProperty(font): 

359 """ 

360 Extract information from a TrueType font file. 

361 

362 Parameters 

363 ---------- 

364 font : `.FT2Font` 

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

366 

367 Returns 

368 ------- 

369 `FontEntry` 

370 The extracted font properties. 

371 

372 """ 

373 name = font.family_name 

374 

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

376 

377 sfnt = font.get_sfnt() 

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

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

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

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

382 sfnt2 = (sfnt.get((1, 0, 0, 2), b'').decode('latin-1').lower() or 

383 sfnt.get((3, 1, 0x0409, 2), b'').decode('utf_16_be').lower()) 

384 sfnt4 = (sfnt.get((1, 0, 0, 4), b'').decode('latin-1').lower() or 

385 sfnt.get((3, 1, 0x0409, 4), b'').decode('utf_16_be').lower()) 

386 

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

388 style = 'oblique' 

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

390 style = 'italic' 

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

392 style = 'normal' 

393 elif font.style_flags & ft2font.ITALIC: 

394 style = 'italic' 

395 else: 

396 style = 'normal' 

397 

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

399 

400 # !!!! Untested 

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

402 variant = 'small-caps' 

403 else: 

404 variant = 'normal' 

405 

406 if font.style_flags & ft2font.BOLD: 

407 weight = 700 

408 else: 

409 weight = next((w for w in weight_dict if w in sfnt4), 400) 

410 

411 # Stretch can be absolute and relative 

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

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

414 # and ultra-expanded. 

415 # Relative stretches are: wider, narrower 

416 # Child value is: inherit 

417 

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

419 stretch = 'condensed' 

420 elif 'demi cond' in sfnt4: 

421 stretch = 'semi-condensed' 

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

423 stretch = 'expanded' 

424 else: 

425 stretch = 'normal' 

426 

427 # Sizes can be absolute and relative. 

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

429 # and xx-large. 

430 # Relative sizes are: larger, smaller 

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

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

433 

434 if not font.scalable: 

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

436 size = 'scalable' 

437 

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

439 

440 

441def afmFontProperty(fontpath, font): 

442 """ 

443 Extract information from an AFM font file. 

444 

445 Parameters 

446 ---------- 

447 font : `.AFM` 

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

449 

450 Returns 

451 ------- 

452 `FontEntry` 

453 The extracted font properties. 

454 """ 

455 

456 name = font.get_familyname() 

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

458 

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

460 

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

462 style = 'italic' 

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

464 style = 'oblique' 

465 else: 

466 style = 'normal' 

467 

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

469 

470 # !!!! Untested 

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

472 variant = 'small-caps' 

473 else: 

474 variant = 'normal' 

475 

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

477 if weight not in weight_dict: 

478 weight = 'normal' 

479 

480 # Stretch can be absolute and relative 

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

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

483 # and ultra-expanded. 

484 # Relative stretches are: wider, narrower 

485 # Child value is: inherit 

486 if 'demi cond' in fontname: 

487 stretch = 'semi-condensed' 

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

489 stretch = 'condensed' 

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

491 stretch = 'expanded' 

492 else: 

493 stretch = 'normal' 

494 

495 # Sizes can be absolute and relative. 

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

497 # and xx-large. 

498 # Relative sizes are: larger, smaller 

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

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

501 

502 # All AFM fonts are apparently scalable. 

503 

504 size = 'scalable' 

505 

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

507 

508 

509@cbook.deprecated("3.2", alternative="FontManager.addfont") 

510def createFontList(fontfiles, fontext='ttf'): 

511 """ 

512 A function to create a font lookup list. The default is to create 

513 a list of TrueType fonts. An AFM font list can optionally be 

514 created. 

515 """ 

516 

517 fontlist = [] 

518 # Add fonts from list of known font files. 

519 seen = set() 

520 for fpath in fontfiles: 

521 _log.debug('createFontDict: %s', fpath) 

522 fname = os.path.split(fpath)[1] 

523 if fname in seen: 

524 continue 

525 if fontext == 'afm': 

526 try: 

527 with open(fpath, 'rb') as fh: 

528 font = afm.AFM(fh) 

529 except EnvironmentError: 

530 _log.info("Could not open font file %s", fpath) 

531 continue 

532 except RuntimeError: 

533 _log.info("Could not parse font file %s", fpath) 

534 continue 

535 try: 

536 prop = afmFontProperty(fpath, font) 

537 except KeyError as exc: 

538 _log.info("Could not extract properties for %s: %s", 

539 fpath, exc) 

540 continue 

541 else: 

542 try: 

543 font = ft2font.FT2Font(fpath) 

544 except (OSError, RuntimeError) as exc: 

545 _log.info("Could not open font file %s: %s", fpath, exc) 

546 continue 

547 except UnicodeError: 

548 _log.info("Cannot handle unicode filenames") 

549 continue 

550 try: 

551 prop = ttfFontProperty(font) 

552 except (KeyError, RuntimeError, ValueError, 

553 NotImplementedError) as exc: 

554 _log.info("Could not extract properties for %s: %s", 

555 fpath, exc) 

556 continue 

557 

558 fontlist.append(prop) 

559 seen.add(fname) 

560 return fontlist 

561 

562 

563class FontProperties: 

564 """ 

565 A class for storing and manipulating font properties. 

566 

567 The font properties are those described in the `W3C Cascading 

568 Style Sheet, Level 1 

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

570 specification. The six properties are: 

571 

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

573 The items may include a generic font family name, either 

574 'serif', 'sans-serif', 'cursive', 'fantasy', or 'monospace'. 

575 In that case, the actual font to be used will be looked up 

576 from the associated rcParam. 

577 

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

579 

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

581 

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

583 'ultra-condensed', 'extra-condensed', 'condensed', 

584 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 

585 'extra-expanded' or 'ultra-expanded'. 

586 

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

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

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

590 'extra bold', 'black'. 

591 

592 - size: Either an relative value of 'xx-small', 'x-small', 

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

594 absolute font size, e.g., 12. 

595 

596 The default font property for TrueType fonts (as specified in the 

597 default rcParams) is :: 

598 

599 sans-serif, normal, normal, normal, normal, scalable. 

600 

601 Alternatively, a font may be specified using an absolute path to a 

602 .ttf file, by using the *fname* kwarg. 

603 

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

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

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

607 on the font manager's default font size. 

608 

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

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

611 merely borrowing its pattern syntax for use here. 

612 

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

614 .. _pattern: 

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

616 

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

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

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

620 fontconfig. 

621 """ 

622 

623 def __init__(self, 

624 family = None, 

625 style = None, 

626 variant= None, 

627 weight = None, 

628 stretch= None, 

629 size = None, 

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

631 ): 

632 self._family = _normalize_font_family(rcParams['font.family']) 

633 self._slant = rcParams['font.style'] 

634 self._variant = rcParams['font.variant'] 

635 self._weight = rcParams['font.weight'] 

636 self._stretch = rcParams['font.stretch'] 

637 self._size = rcParams['font.size'] 

638 self._file = None 

639 

640 if isinstance(family, str): 

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

642 # parameter provided. 

643 if (style is None and 

644 variant is None and 

645 weight is None and 

646 stretch is None and 

647 size is None and 

648 fname is None): 

649 self.set_fontconfig_pattern(family) 

650 return 

651 

652 self.set_family(family) 

653 self.set_style(style) 

654 self.set_variant(variant) 

655 self.set_weight(weight) 

656 self.set_stretch(stretch) 

657 self.set_file(fname) 

658 self.set_size(size) 

659 

660 def _parse_fontconfig_pattern(self, pattern): 

661 return parse_fontconfig_pattern(pattern) 

662 

663 def __hash__(self): 

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

665 self.get_slant(), 

666 self.get_variant(), 

667 self.get_weight(), 

668 self.get_stretch(), 

669 self.get_size_in_points(), 

670 self.get_file()) 

671 return hash(l) 

672 

673 def __eq__(self, other): 

674 return hash(self) == hash(other) 

675 

676 def __str__(self): 

677 return self.get_fontconfig_pattern() 

678 

679 def get_family(self): 

680 """ 

681 Return a list of font names that comprise the font family. 

682 """ 

683 return self._family 

684 

685 def get_name(self): 

686 """ 

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

688 """ 

689 return get_font(findfont(self)).family_name 

690 

691 def get_style(self): 

692 """ 

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

694 """ 

695 return self._slant 

696 get_slant = get_style 

697 

698 def get_variant(self): 

699 """ 

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

701 """ 

702 return self._variant 

703 

704 def get_weight(self): 

705 """ 

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

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

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

709 'heavy', 'extra bold', 'black' 

710 """ 

711 return self._weight 

712 

713 def get_stretch(self): 

714 """ 

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

716 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 

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

718 """ 

719 return self._stretch 

720 

721 def get_size(self): 

722 """ 

723 Return the font size. 

724 """ 

725 return self._size 

726 

727 def get_size_in_points(self): 

728 return self._size 

729 

730 def get_file(self): 

731 """ 

732 Return the filename of the associated font. 

733 """ 

734 return self._file 

735 

736 def get_fontconfig_pattern(self): 

737 """ 

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

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

740 

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

742 pattern syntax for use here. 

743 """ 

744 return generate_fontconfig_pattern(self) 

745 

746 def set_family(self, family): 

747 """ 

748 Change the font family. May be either an alias (generic name 

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

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

751 font names. Real font names are not supported when 

752 `text.usetex` is `True`. 

753 """ 

754 if family is None: 

755 family = rcParams['font.family'] 

756 self._family = _normalize_font_family(family) 

757 set_name = set_family 

758 

759 def set_style(self, style): 

760 """ 

761 Set the font style. Values are: 'normal', 'italic' or 'oblique'. 

762 """ 

763 if style is None: 

764 style = rcParams['font.style'] 

765 cbook._check_in_list(['normal', 'italic', 'oblique'], style=style) 

766 self._slant = style 

767 set_slant = set_style 

768 

769 def set_variant(self, variant): 

770 """ 

771 Set the font variant. Values are: 'normal' or 'small-caps'. 

772 """ 

773 if variant is None: 

774 variant = rcParams['font.variant'] 

775 cbook._check_in_list(['normal', 'small-caps'], variant=variant) 

776 self._variant = variant 

777 

778 def set_weight(self, weight): 

779 """ 

780 Set the font weight. May be either a numeric value in the 

781 range 0-1000 or one of 'ultralight', 'light', 'normal', 

782 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', 

783 'demi', 'bold', 'heavy', 'extra bold', 'black' 

784 """ 

785 if weight is None: 

786 weight = rcParams['font.weight'] 

787 try: 

788 weight = int(weight) 

789 if weight < 0 or weight > 1000: 

790 raise ValueError() 

791 except ValueError: 

792 if weight not in weight_dict: 

793 raise ValueError("weight is invalid") 

794 self._weight = weight 

795 

796 def set_stretch(self, stretch): 

797 """ 

798 Set the font stretch or width. Options are: 'ultra-condensed', 

799 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 

800 'semi-expanded', 'expanded', 'extra-expanded' or 

801 'ultra-expanded', or a numeric value in the range 0-1000. 

802 """ 

803 if stretch is None: 

804 stretch = rcParams['font.stretch'] 

805 try: 

806 stretch = int(stretch) 

807 if stretch < 0 or stretch > 1000: 

808 raise ValueError() 

809 except ValueError: 

810 if stretch not in stretch_dict: 

811 raise ValueError("stretch is invalid") 

812 self._stretch = stretch 

813 

814 def set_size(self, size): 

815 """ 

816 Set the font size. Either an relative value of 'xx-small', 

817 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' 

818 or an absolute font size, e.g., 12. 

819 """ 

820 if size is None: 

821 size = rcParams['font.size'] 

822 try: 

823 size = float(size) 

824 except ValueError: 

825 try: 

826 scale = font_scalings[size] 

827 except KeyError: 

828 raise ValueError( 

829 "Size is invalid. Valid font size are " 

830 + ", ".join(map(str, font_scalings))) 

831 else: 

832 size = scale * FontManager.get_default_size() 

833 if size < 1.0: 

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

835 'Setting fontsize = 1 pt', size) 

836 size = 1.0 

837 self._size = size 

838 

839 def set_file(self, file): 

840 """ 

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

842 other properties will be ignored. 

843 """ 

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

845 

846 def set_fontconfig_pattern(self, pattern): 

847 """ 

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

849 

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

851 pattern syntax for use here. 

852 """ 

853 for key, val in self._parse_fontconfig_pattern(pattern).items(): 

854 if type(val) == list: 

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

856 else: 

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

858 

859 def copy(self): 

860 """Return a copy of self.""" 

861 new = type(self)() 

862 vars(new).update(vars(self)) 

863 return new 

864 

865 

866class _JSONEncoder(json.JSONEncoder): 

867 def default(self, o): 

868 if isinstance(o, FontManager): 

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

870 elif isinstance(o, FontEntry): 

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

872 try: 

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

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

875 d["fname"] = str( 

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

877 except ValueError: 

878 pass 

879 return d 

880 else: 

881 return super().default(o) 

882 

883 

884@cbook.deprecated("3.2", alternative="json_dump") 

885class JSONEncoder(_JSONEncoder): 

886 pass 

887 

888 

889def _json_decode(o): 

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

891 if cls is None: 

892 return o 

893 elif cls == 'FontManager': 

894 r = FontManager.__new__(FontManager) 

895 r.__dict__.update(o) 

896 return r 

897 elif cls == 'FontEntry': 

898 r = FontEntry.__new__(FontEntry) 

899 r.__dict__.update(o) 

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

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

902 return r 

903 else: 

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

905 

906 

907def json_dump(data, filename): 

908 """ 

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

910 

911 Notes 

912 ----- 

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

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

915 valid across virtualenvs). 

916 

917 See Also 

918 -------- 

919 json_load 

920 """ 

921 with open(filename, 'w') as fh: 

922 try: 

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

924 except OSError as e: 

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

926 

927 

928def json_load(filename): 

929 """ 

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

931 

932 See Also 

933 -------- 

934 json_dump 

935 """ 

936 with open(filename, 'r') as fh: 

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

938 

939 

940def _normalize_font_family(family): 

941 if isinstance(family, str): 

942 family = [family] 

943 return family 

944 

945 

946class FontManager: 

947 """ 

948 On import, the :class:`FontManager` singleton instance creates a 

949 list of TrueType fonts based on the font properties: name, style, 

950 variant, weight, stretch, and size. The :meth:`findfont` method 

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

952 matches the specification. If no good enough match is found, a 

953 default font is returned. 

954 """ 

955 # Increment this version number whenever the font cache data 

956 # format or behavior has changed and requires a existing font 

957 # cache files to be rebuilt. 

958 __version__ = 310 

959 

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

961 self._version = self.__version__ 

962 

963 self.__default_weight = weight 

964 self.default_size = size 

965 

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

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

968 # Create list of font paths 

969 for pathname in ['TTFPATH', 'AFMPATH']: 

970 if pathname in os.environ: 

971 ttfpath = os.environ[pathname] 

972 if ttfpath.find(';') >= 0: # win32 style 

973 paths.extend(ttfpath.split(';')) 

974 elif ttfpath.find(':') >= 0: # unix style 

975 paths.extend(ttfpath.split(':')) 

976 else: 

977 paths.append(ttfpath) 

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

979 # Load TrueType fonts and create font dictionary. 

980 

981 self.defaultFamily = { 

982 'ttf': 'DejaVu Sans', 

983 'afm': 'Helvetica'} 

984 

985 self.afmlist = [] 

986 self.ttflist = [] 

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

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

989 *findSystemFonts(fontext=fontext)]: 

990 try: 

991 self.addfont(path) 

992 except OSError as exc: 

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

994 except Exception as exc: 

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

996 path, exc) 

997 

998 def addfont(self, path): 

999 """ 

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

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

1002 

1003 Parameters 

1004 ---------- 

1005 path : str or path-like 

1006 """ 

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

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

1009 font = afm.AFM(fh) 

1010 prop = afmFontProperty(path, font) 

1011 self.afmlist.append(prop) 

1012 else: 

1013 font = ft2font.FT2Font(path) 

1014 prop = ttfFontProperty(font) 

1015 self.ttflist.append(prop) 

1016 

1017 @property 

1018 def defaultFont(self): 

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

1020 # the venv path in the json serialization. 

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

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

1023 

1024 def get_default_weight(self): 

1025 """ 

1026 Return the default font weight. 

1027 """ 

1028 return self.__default_weight 

1029 

1030 @staticmethod 

1031 def get_default_size(): 

1032 """ 

1033 Return the default font size. 

1034 """ 

1035 return rcParams['font.size'] 

1036 

1037 def set_default_weight(self, weight): 

1038 """ 

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

1040 """ 

1041 self.__default_weight = weight 

1042 

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

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

1045 def score_family(self, families, family2): 

1046 """ 

1047 Returns a match score between the list of font families in 

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

1049 

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

1051 

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

1053 

1054 No match will return 1.0. 

1055 """ 

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

1057 families = [families] 

1058 elif len(families) == 0: 

1059 return 1.0 

1060 family2 = family2.lower() 

1061 step = 1 / len(families) 

1062 for i, family1 in enumerate(families): 

1063 family1 = family1.lower() 

1064 if family1 in font_family_aliases: 

1065 if family1 in ('sans', 'sans serif'): 

1066 family1 = 'sans-serif' 

1067 options = rcParams['font.' + family1] 

1068 options = [x.lower() for x in options] 

1069 if family2 in options: 

1070 idx = options.index(family2) 

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

1072 elif family1 == family2: 

1073 # The score should be weighted by where in the 

1074 # list the font was found. 

1075 return i * step 

1076 return 1.0 

1077 

1078 def score_style(self, style1, style2): 

1079 """ 

1080 Returns a match score between *style1* and *style2*. 

1081 

1082 An exact match returns 0.0. 

1083 

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

1085 

1086 No match returns 1.0. 

1087 """ 

1088 if style1 == style2: 

1089 return 0.0 

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

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

1092 return 0.1 

1093 return 1.0 

1094 

1095 def score_variant(self, variant1, variant2): 

1096 """ 

1097 Returns a match score between *variant1* and *variant2*. 

1098 

1099 An exact match returns 0.0, otherwise 1.0. 

1100 """ 

1101 if variant1 == variant2: 

1102 return 0.0 

1103 else: 

1104 return 1.0 

1105 

1106 def score_stretch(self, stretch1, stretch2): 

1107 """ 

1108 Returns a match score between *stretch1* and *stretch2*. 

1109 

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

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

1112 between 0.0 and 1.0. 

1113 """ 

1114 try: 

1115 stretchval1 = int(stretch1) 

1116 except ValueError: 

1117 stretchval1 = stretch_dict.get(stretch1, 500) 

1118 try: 

1119 stretchval2 = int(stretch2) 

1120 except ValueError: 

1121 stretchval2 = stretch_dict.get(stretch2, 500) 

1122 return abs(stretchval1 - stretchval2) / 1000.0 

1123 

1124 def score_weight(self, weight1, weight2): 

1125 """ 

1126 Returns a match score between *weight1* and *weight2*. 

1127 

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

1129 and have the same value. 

1130 

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

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

1133 0.05 and 1.0. 

1134 """ 

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

1136 if cbook._str_equal(weight1, weight2): 

1137 return 0.0 

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

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

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

1141 

1142 def score_size(self, size1, size2): 

1143 """ 

1144 Returns a match score between *size1* and *size2*. 

1145 

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

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

1148 

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

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

1151 72pt) will lie between 0.0 and 1.0. 

1152 """ 

1153 if size2 == 'scalable': 

1154 return 0.0 

1155 # Size value should have already been 

1156 try: 

1157 sizeval1 = float(size1) 

1158 except ValueError: 

1159 sizeval1 = self.default_size * font_scalings[size1] 

1160 try: 

1161 sizeval2 = float(size2) 

1162 except ValueError: 

1163 return 1.0 

1164 return abs(sizeval1 - sizeval2) / 72 

1165 

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

1167 fallback_to_default=True, rebuild_if_missing=True): 

1168 """ 

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

1170 

1171 Parameters 

1172 ---------- 

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

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

1175 `.FontProperties` object or a string defining a 

1176 `fontconfig patterns`_. 

1177 

1178 fontext : {'ttf', 'afm'}, optional, default: 'ttf' 

1179 The extension of the font file: 

1180 

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

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

1183 

1184 directory : str, optional 

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

1186 fallback_to_default : bool 

1187 If True, will fallback to the default font family (usually 

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

1189 rebuild_if_missing : bool 

1190 Whether to rebuild the font cache and search again if no match 

1191 is found. 

1192 

1193 Returns 

1194 ------- 

1195 fontfile : str 

1196 The filename of the best matching font. 

1197 

1198 Notes 

1199 ----- 

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

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

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

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

1204 returned. 

1205 

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

1207 perform the O(n) nearest neighbor search. 

1208 

1209 See the `W3C Cascading Style Sheet, Level 1 

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

1211 for a description of the font finding algorithm. 

1212 

1213 .. _fontconfig patterns: 

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

1215 

1216 """ 

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

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

1219 # rcParam was changed. 

1220 rc_params = tuple(tuple(rcParams[key]) for key in [ 

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

1222 "font.monospace"]) 

1223 return self._findfont_cached( 

1224 prop, fontext, directory, fallback_to_default, rebuild_if_missing, 

1225 rc_params) 

1226 

1227 @lru_cache() 

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

1229 rebuild_if_missing, rc_params): 

1230 

1231 if not isinstance(prop, FontProperties): 

1232 prop = FontProperties(prop) 

1233 

1234 fname = prop.get_file() 

1235 if fname is not None: 

1236 return fname 

1237 

1238 if fontext == 'afm': 

1239 fontlist = self.afmlist 

1240 else: 

1241 fontlist = self.ttflist 

1242 

1243 best_score = 1e64 

1244 best_font = None 

1245 

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

1247 for font in fontlist: 

1248 if (directory is not None and 

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

1250 continue 

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

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

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

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

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

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

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

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

1259 if score < best_score: 

1260 best_score = score 

1261 best_font = font 

1262 if score == 0: 

1263 break 

1264 

1265 if best_font is None or best_score >= 10.0: 

1266 if fallback_to_default: 

1267 _log.warning( 

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

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

1270 default_prop = prop.copy() 

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

1272 return self.findfont(default_prop, fontext, directory, False) 

1273 else: 

1274 # This is a hard fail -- we can't find anything reasonable, 

1275 # so just return the DejaVuSans.ttf 

1276 _log.warning('findfont: Could not match %s. Returning %s.', 

1277 prop, self.defaultFont[fontext]) 

1278 result = self.defaultFont[fontext] 

1279 else: 

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

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

1282 result = best_font.fname 

1283 

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

1285 if rebuild_if_missing: 

1286 _log.info( 

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

1288 _rebuild() 

1289 return fontManager.findfont( 

1290 prop, fontext, directory, True, False) 

1291 else: 

1292 raise ValueError("No valid font could be found") 

1293 

1294 return result 

1295 

1296 

1297@lru_cache() 

1298def is_opentype_cff_font(filename): 

1299 """ 

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

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

1302 that can not subset these fonts. 

1303 """ 

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

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

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

1307 else: 

1308 return False 

1309 

1310 

1311_fmcache = os.path.join( 

1312 mpl.get_cachedir(), 'fontlist-v{}.json'.format(FontManager.__version__)) 

1313fontManager = None 

1314 

1315 

1316_get_font = lru_cache(64)(ft2font.FT2Font) 

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

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

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

1320# via the cache of _get_font, which we can empty upon forking (in Py3.7+). 

1321if hasattr(os, "register_at_fork"): 

1322 os.register_at_fork(after_in_child=_get_font.cache_clear) 

1323 

1324 

1325def get_font(filename, hinting_factor=None): 

1326 if hinting_factor is None: 

1327 hinting_factor = rcParams['text.hinting_factor'] 

1328 return _get_font(os.fspath(filename), hinting_factor, 

1329 _kerning_factor=rcParams['text.kerning_factor']) 

1330 

1331 

1332def _rebuild(): 

1333 global fontManager 

1334 fontManager = FontManager() 

1335 with cbook._lock_path(_fmcache): 

1336 json_dump(fontManager, _fmcache) 

1337 _log.info("generated new fontManager") 

1338 

1339 

1340try: 

1341 fontManager = json_load(_fmcache) 

1342except Exception: 

1343 _rebuild() 

1344else: 

1345 if getattr(fontManager, '_version', object()) != FontManager.__version__: 

1346 _rebuild() 

1347 else: 

1348 _log.debug("Using fontManager instance from %s", _fmcache) 

1349 

1350 

1351findfont = fontManager.findfont