167
167
]
168
168
169
169
170
- @lru_cache (64 )
171
- def _cached_realpath (path ):
172
- return os .path .realpath (path )
173
-
174
-
175
170
def get_fontext_synonyms (fontext ):
176
171
"""
177
172
Return a list of file extensions that are synonyms for
@@ -1354,7 +1349,110 @@ def get_font_names(self):
1354
1349
"""Return the list of available fonts."""
1355
1350
return list (set ([font .name for font in self .ttflist ]))
1356
1351
1357
- @lru_cache ()
1352
+ def _find_fonts_by_props (self , prop , fontext = 'ttf' , directory = None ,
1353
+ fallback_to_default = True , rebuild_if_missing = True ):
1354
+ """
1355
+ Find font families that most closely match the given properties.
1356
+
1357
+ Parameters
1358
+ ----------
1359
+ prop : str or `~matplotlib.font_manager.FontProperties`
1360
+ The font properties to search for. This can be either a
1361
+ `.FontProperties` object or a string defining a
1362
+ `fontconfig patterns`_.
1363
+
1364
+ fontext : {'ttf', 'afm'}, default: 'ttf'
1365
+ The extension of the font file:
1366
+
1367
+ - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf)
1368
+ - 'afm': Adobe Font Metrics (.afm)
1369
+
1370
+ directory : str, optional
1371
+ If given, only search this directory and its subdirectories.
1372
+
1373
+ fallback_to_default : bool
1374
+ If True, will fallback to the default font family (usually
1375
+ "DejaVu Sans" or "Helvetica") if none of the families were found.
1376
+
1377
+ rebuild_if_missing : bool
1378
+ Whether to rebuild the font cache and search again if the first
1379
+ match appears to point to a nonexisting font (i.e., the font cache
1380
+ contains outdated entries).
1381
+
1382
+ Returns
1383
+ -------
1384
+ list[str]
1385
+ The paths of the fonts found
1386
+
1387
+ Notes
1388
+ -----
1389
+ This is an extension/wrapper of the original findfont API, which only
1390
+ returns a single font for given font properties. Instead, this API
1391
+ returns an dict containing multiple fonts and their filepaths
1392
+ which closely match the given font properties. Since this internally
1393
+ uses the original API, there's no change to the logic of performing the
1394
+ nearest neighbor search. See `findfont` for more details.
1395
+
1396
+ """
1397
+
1398
+ rc_params = tuple (tuple (rcParams [key ]) for key in [
1399
+ "font.serif" , "font.sans-serif" , "font.cursive" , "font.fantasy" ,
1400
+ "font.monospace" ])
1401
+
1402
+ prop = FontProperties ._from_any (prop )
1403
+
1404
+ fpaths = []
1405
+ for family in prop .get_family ():
1406
+ cprop = prop .copy ()
1407
+
1408
+ # set current prop's family
1409
+ cprop .set_family (family )
1410
+
1411
+ # do not fall back to default font
1412
+ try :
1413
+ fpaths .append (
1414
+ self ._findfont_cached (
1415
+ cprop , fontext , directory ,
1416
+ fallback_to_default = False ,
1417
+ rebuild_if_missing = rebuild_if_missing ,
1418
+ rc_params = rc_params ,
1419
+ )
1420
+ )
1421
+ except ValueError :
1422
+ if family in font_family_aliases :
1423
+ _log .warning (
1424
+ "findfont: Generic family %r not found because "
1425
+ "none of the following families were found: %s" ,
1426
+ family ,
1427
+ ", " .join (self ._expand_aliases (family ))
1428
+ )
1429
+ else :
1430
+ _log .warning (
1431
+ 'findfont: Font family \' %s\' not found.' , family
1432
+ )
1433
+
1434
+ # only add default family if no other font was found and
1435
+ # fallback_to_default is enabled
1436
+ if not fpaths :
1437
+ if fallback_to_default :
1438
+ dfamily = self .defaultFamily [fontext ]
1439
+ cprop = prop .copy ()
1440
+ cprop .set_family (dfamily )
1441
+ fpaths .append (
1442
+ self ._findfont_cached (
1443
+ cprop , fontext , directory ,
1444
+ fallback_to_default = True ,
1445
+ rebuild_if_missing = rebuild_if_missing ,
1446
+ rc_params = rc_params ,
1447
+ )
1448
+ )
1449
+ else :
1450
+ raise ValueError ("Failed to find any font, and fallback "
1451
+ "to the default font was disabled." )
1452
+
1453
+ return fpaths
1454
+
1455
+ @lru_cache (1024 )
1358
1456
def _findfont_cached (self , prop , fontext , directory , fallback_to_default ,
1359
1457
rebuild_if_missing , rc_params ):
1360
1458
@@ -1447,9 +1545,19 @@ def is_opentype_cff_font(filename):
1447
1545
1448
1546
1449
1547
@lru_cache (64 )
1450
- def _get_font (filename , hinting_factor , * , _kerning_factor , thread_id ):
1548
+ def _get_font (font_filepaths , hinting_factor , * , _kerning_factor , thread_id ):
1549
+ first_fontpath , * rest = font_filepaths
1451
1550
return ft2font .FT2Font (
1452
- filename , hinting_factor , _kerning_factor = _kerning_factor )
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
+ )
1453
1561
1454
1562
1455
1563
# FT2Font objects cannot be used across fork()s because they reference the same
@@ -1461,16 +1569,51 @@ def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id):
1461
1569
os .register_at_fork (after_in_child = _get_font .cache_clear )
1462
1570
1463
1571
1464
- def get_font (filename , hinting_factor = None ):
1572
+ @lru_cache (64 )
1573
+ def _cached_realpath (path ):
1465
1574
# Resolving the path avoids embedding the font twice in pdf/ps output if a
1466
1575
# single font is selected using two different relative paths.
1467
- filename = _cached_realpath (filename )
1576
+ return os .path .realpath (path )
1577
+
1578
+
1579
+ @_api .rename_parameter ('3.6' , "filepath" , "font_filepaths" )
1580
+ def 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
+
1468
1606
if hinting_factor is None :
1469
1607
hinting_factor = rcParams ['text.hinting_factor' ]
1470
- # also key on the thread ID to prevent segfaults with multi-threading
1471
- return _get_font (filename , hinting_factor ,
1472
- _kerning_factor = rcParams ['text.kerning_factor' ],
1473
- thread_id = threading .get_ident ())
1608
+
1609
+ return _get_font (
1610
+ # must be a tuple to be cached
1611
+ paths ,
1612
+ hinting_factor ,
1613
+ _kerning_factor = rcParams ['text.kerning_factor' ],
1614
+ # also key on the thread ID to prevent segfaults with multi-threading
1615
+ thread_id = threading .get_ident ()
1616
+ )
1474
1617
1475
1618
1476
1619
def _load_fontmanager (* , try_read_cache = True ):
0 commit comments