Skip to content

Commit 6f8cf65

Browse files
committed
Add a last resort font for missing glyphs
1 parent a364dc5 commit 6f8cf65

File tree

15 files changed

+1034
-25
lines changed

15 files changed

+1034
-25
lines changed

.github/workflows/tests.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,10 @@ jobs:
196196
~/.cache/matplotlib
197197
!~/.cache/matplotlib/tex.cache
198198
!~/.cache/matplotlib/test_cache
199-
key: 5-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-${{ github.sha }}
199+
key: 6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-${{ github.sha }}
200200
restore-keys: |
201-
5-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-
202-
5-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-
201+
6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-
202+
6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-
203203
204204
- name: Install Python dependencies
205205
run: |

LICENSE/LICENSE_LAST_RESORT_FONT

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
Last Resort High-Efficiency Font License
2+
========================================
3+
4+
This Font Software is licensed under the SIL Open Font License,
5+
Version 1.1.
6+
7+
This license is copied below, and is also available with a FAQ at:
8+
http://scripts.sil.org/OFL
9+
10+
-----------------------------------------------------------
11+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
12+
-----------------------------------------------------------
13+
14+
PREAMBLE
15+
The goals of the Open Font License (OFL) are to stimulate worldwide
16+
development of collaborative font projects, to support the font
17+
creation efforts of academic and linguistic communities, and to
18+
provide a free and open framework in which fonts may be shared and
19+
improved in partnership with others.
20+
21+
The OFL allows the licensed fonts to be used, studied, modified and
22+
redistributed freely as long as they are not sold by themselves. The
23+
fonts, including any derivative works, can be bundled, embedded,
24+
redistributed and/or sold with any software provided that any reserved
25+
names are not used by derivative works. The fonts and derivatives,
26+
however, cannot be released under any other type of license. The
27+
requirement for fonts to remain under this license does not apply to
28+
any document created using the fonts or their derivatives.
29+
30+
DEFINITIONS
31+
"Font Software" refers to the set of files released by the Copyright
32+
Holder(s) under this license and clearly marked as such. This may
33+
include source files, build scripts and documentation.
34+
35+
"Reserved Font Name" refers to any names specified as such after the
36+
copyright statement(s).
37+
38+
"Original Version" refers to the collection of Font Software
39+
components as distributed by the Copyright Holder(s).
40+
41+
"Modified Version" refers to any derivative made by adding to,
42+
deleting, or substituting -- in part or in whole -- any of the
43+
components of the Original Version, by changing formats or by porting
44+
the Font Software to a new environment.
45+
46+
"Author" refers to any designer, engineer, programmer, technical
47+
writer or other person who contributed to the Font Software.
48+
49+
PERMISSION & CONDITIONS
50+
Permission is hereby granted, free of charge, to any person obtaining
51+
a copy of the Font Software, to use, study, copy, merge, embed,
52+
modify, redistribute, and sell modified and unmodified copies of the
53+
Font Software, subject to the following conditions:
54+
55+
1) Neither the Font Software nor any of its individual components, in
56+
Original or Modified Versions, may be sold by itself.
57+
58+
2) Original or Modified Versions of the Font Software may be bundled,
59+
redistributed and/or sold with any software, provided that each copy
60+
contains the above copyright notice and this license. These can be
61+
included either as stand-alone text files, human-readable headers or
62+
in the appropriate machine-readable metadata fields within text or
63+
binary files as long as those fields can be easily viewed by the user.
64+
65+
3) No Modified Version of the Font Software may use the Reserved Font
66+
Name(s) unless explicit written permission is granted by the
67+
corresponding Copyright Holder. This restriction only applies to the
68+
primary font name as presented to the users.
69+
70+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
71+
Software shall not be used to promote, endorse or advertise any
72+
Modified Version, except to acknowledge the contribution(s) of the
73+
Copyright Holder(s) and the Author(s) or with their explicit written
74+
permission.
75+
76+
5) The Font Software, modified or unmodified, in part or in whole,
77+
must be distributed entirely under this license, and must not be
78+
distributed under any other license. The requirement for fonts to
79+
remain under this license does not apply to any document created using
80+
the Font Software.
81+
82+
TERMINATION
83+
This license becomes null and void if any of the above conditions are
84+
not met.
85+
86+
DISCLAIMER
87+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
88+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
89+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
90+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
91+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
92+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
93+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
94+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
95+
OTHER DEALINGS IN THE FONT SOFTWARE.
96+
97+
SPDX-License-Identifier: OFL-1.1

doc/conf.py

+8
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ def _parse_skip_subdirs_file():
102102
# usage in the gallery.
103103
warnings.filterwarnings('error', append=True)
104104

105+
# Warnings for missing glyphs occur during `savefig`, and would cause any such plot to
106+
# not be created. Because the exception occurs in savefig, there is no way for the plot
107+
# itself ignore these warning locally, so we must do so globally.
108+
warnings.filterwarnings('default', category=UserWarning,
109+
message=r'Glyph \d+ \(.+\) missing from font\(s\)')
110+
warnings.filterwarnings('default', category=UserWarning,
111+
message=r'Matplotlib currently does not support .+ natively\.')
112+
105113
# Add any Sphinx extension module names here, as strings. They can be
106114
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
107115
extensions = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Missing glyphs use Last Resort font
2+
-----------------------------------
3+
4+
Most fonts do not have 100% character coverage, and will fall back to a "not found"
5+
glyph for characters that are not provided. Often, this glyph will be minimal (e.g., the
6+
default DejaVu Sans "not found" glyph is just a rectangle.) Such minimal glyphs provide
7+
no context as to the characters that are missing.
8+
9+
Now, missing glyphs will fall back to the `Last Resort font
10+
<https://github.com/unicode-org/last-resort-font>`__ produced by the Unicode Consortium.
11+
This special-purpose font provides glyphs that represent types of Unicode characters.
12+
These glyphs show a representative character from the missing Unicode block, and at
13+
larger sizes, more context to help determine which character and font are needed.
14+
15+
To disable this fallback behaviour, set :rc:`font.enable_last_resort` to ``False``.
16+
17+
.. plot::
18+
:alt: An example of missing glyph behaviour, the first glyph from Bengali script,
19+
second glyph from Hiragana, and the last glyph from the Unicode Private Use
20+
Area. Multiple lines repeat the text with increasing font size from top to
21+
bottom.
22+
23+
text_raw = r"'\N{Bengali Digit Zero}\N{Hiragana Letter A}\ufdd0'"
24+
text = eval(text_raw)
25+
sizes = [
26+
(0.85, 8),
27+
(0.80, 10),
28+
(0.75, 12),
29+
(0.70, 16),
30+
(0.63, 20),
31+
(0.55, 24),
32+
(0.45, 32),
33+
(0.30, 48),
34+
(0.10, 64),
35+
]
36+
37+
fig = plt.figure()
38+
fig.text(0.01, 0.90, f'Input: {text_raw}')
39+
for y, size in sizes:
40+
fig.text(0.01, y, f'{size}pt:{text}', fontsize=size)

lib/matplotlib/font_manager.py

+31-10
Original file line numberDiff line numberDiff line change
@@ -1546,19 +1546,39 @@ def is_opentype_cff_font(filename):
15461546

15471547

15481548
@lru_cache(64)
1549-
def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id):
1549+
def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id,
1550+
enable_last_resort):
15501551
first_fontpath, *rest = font_filepaths
1551-
return ft2font.FT2Font(
1552+
fallback_list = [
1553+
ft2font.FT2Font(fpath, hinting_factor, _kerning_factor=_kerning_factor)
1554+
for fpath in rest
1555+
]
1556+
last_resort_path = _cached_realpath(
1557+
cbook._get_data_path('fonts', 'ttf', 'LastResortHE-Regular.ttf'))
1558+
try:
1559+
last_resort_index = font_filepaths.index(last_resort_path)
1560+
except ValueError:
1561+
last_resort_index = -1
1562+
# Add Last Resort font so we always have glyphs regardless of font, unless we're
1563+
# already in the list.
1564+
if enable_last_resort:
1565+
fallback_list.append(
1566+
ft2font.FT2Font(last_resort_path, hinting_factor,
1567+
_kerning_factor=_kerning_factor,
1568+
_warn_if_used=True))
1569+
last_resort_index = len(fallback_list)
1570+
font = ft2font.FT2Font(
15521571
first_fontpath, hinting_factor,
1553-
_fallback_list=[
1554-
ft2font.FT2Font(
1555-
fpath, hinting_factor,
1556-
_kerning_factor=_kerning_factor
1557-
)
1558-
for fpath in rest
1559-
],
1572+
_fallback_list=fallback_list,
15601573
_kerning_factor=_kerning_factor
15611574
)
1575+
# Ensure we are using the right charmap for the Last Resort font; FreeType picks the
1576+
# Unicode one by default, but this exists only for Windows, and is empty.
1577+
if last_resort_index == 0:
1578+
font.set_charmap(0)
1579+
elif last_resort_index > 0:
1580+
fallback_list[last_resort_index - 1].set_charmap(0)
1581+
return font
15621582

15631583

15641584
# FT2Font objects cannot be used across fork()s because they reference the same
@@ -1611,7 +1631,8 @@ def get_font(font_filepaths, hinting_factor=None):
16111631
hinting_factor,
16121632
_kerning_factor=mpl.rcParams['text.kerning_factor'],
16131633
# also key on the thread ID to prevent segfaults with multi-threading
1614-
thread_id=threading.get_ident()
1634+
thread_id=threading.get_ident(),
1635+
enable_last_resort=mpl.rcParams['font.enable_last_resort'],
16151636
)
16161637

16171638

Binary file not shown.

lib/matplotlib/mpl-data/matplotlibrc

+5
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@
278278
#font.fantasy: Chicago, Charcoal, Impact, Western, xkcd script, fantasy
279279
#font.monospace: DejaVu Sans Mono, Bitstream Vera Sans Mono, Computer Modern Typewriter, Andale Mono, Nimbus Mono L, Courier New, Courier, Fixed, Terminal, monospace
280280

281+
## If font.enable_last_resort is True, then Unicode Consortium's Last Resort
282+
## font will be appended to all font selections. This ensures that there will
283+
## always be a glyph displayed.
284+
#font.enable_last_resort: true
285+
281286

282287
## ***************************************************************************
283288
## * TEXT *

lib/matplotlib/rcsetup.py

+1
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,7 @@ def _convert_validator_spec(key, conv):
10351035
"boxplot.meanprops.linewidth": validate_float,
10361036

10371037
## font props
1038+
"font.enable_last_resort": validate_bool,
10381039
"font.family": validate_stringlist, # used by text object
10391040
"font.style": validate_string,
10401041
"font.variant": validate_string,
Binary file not shown.
Loading

0 commit comments

Comments
 (0)