Skip to content

Commit 2a3d7a2

Browse files
committed
Add a last resort font for missing glyphs
1 parent c9402f3 commit 2a3d7a2

File tree

11 files changed

+189
-21
lines changed

11 files changed

+189
-21
lines changed

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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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
5+
found" glyph for character that are not provided. Often, this glyph will be minimal
6+
(e.g., the default DejaVu Sans "not found" glyph is just a rectangle.) Such minimal
7+
glyphs provide 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,
13+
and at larger sizes, more context to help determine which character and font
14+
are needed.
15+
16+
To disable this fallback behaviour, set :rc:`font.enable_last_resort` to ``False``.
17+
18+
.. plot::
19+
:alt: An example of missing glyph behaviour, with increasing font size from top to bottom.
20+
21+
text = '\N{Bengali Digit Zero}\N{Hiragana Letter A}\ufdd0'
22+
sizes = [
23+
(0.90, 6),
24+
(0.85, 8),
25+
(0.80, 10),
26+
(0.75, 12),
27+
(0.70, 16),
28+
(0.63, 20),
29+
(0.55, 24),
30+
(0.45, 32),
31+
(0.30, 48),
32+
(0.10, 64),
33+
]
34+
35+
fig = plt.figure()
36+
for y, size in sizes:
37+
fig.text(0.01, y, f'{size}pt: {text}', fontsize=size)

lib/matplotlib/font_manager.py

+20-9
Original file line numberDiff line numberDiff line change
@@ -1546,17 +1546,27 @@ 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
1552+
fallback_list = [
1553+
ft2font.FT2Font(fpath, hinting_factor, _kerning_factor=_kerning_factor)
1554+
for fpath in rest
1555+
]
1556+
# Add Last Resort font so we always have glyphs regardless of font.
1557+
if enable_last_resort:
1558+
path = _cached_realpath(
1559+
cbook._get_data_path('fonts', 'ttf', 'LastResortHE-Regular.ttf'))
1560+
last_resort = ft2font.FT2Font(path, hinting_factor,
1561+
_kerning_factor=_kerning_factor,
1562+
_warn_if_used=True)
1563+
# Ensure we are using the right charmap; FreeType picks the Unicode one
1564+
# by default, but this exists only for Windows, and is empty.
1565+
last_resort.set_charmap(0)
1566+
fallback_list.append(last_resort)
15511567
return ft2font.FT2Font(
15521568
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-
],
1569+
_fallback_list=fallback_list,
15601570
_kerning_factor=_kerning_factor
15611571
)
15621572

@@ -1611,7 +1621,8 @@ def get_font(font_filepaths, hinting_factor=None):
16111621
hinting_factor,
16121622
_kerning_factor=mpl.rcParams['text.kerning_factor'],
16131623
# also key on the thread ID to prevent segfaults with multi-threading
1614-
thread_id=threading.get_ident()
1624+
thread_id=threading.get_ident(),
1625+
enable_last_resort=mpl.rcParams['font.enable_last_resort'],
16151626
)
16161627

16171628

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
@@ -1016,6 +1016,7 @@ def _convert_validator_spec(key, conv):
10161016
"boxplot.meanprops.linewidth": validate_float,
10171017

10181018
## font props
1019+
"font.enable_last_resort": validate_bool,
10191020
"font.family": validate_stringlist, # used by text object
10201021
"font.style": validate_string,
10211022
"font.variant": validate_string,

lib/matplotlib/tests/test_ft2font.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,9 @@ def test__get_fontmap():
889889
)
890890
fontmap = ft._get_fontmap(test_str)
891891
for char, font in fontmap.items():
892-
if ord(char) > 127:
892+
if char == '\n': # Not actually a rendered glyph.
893+
assert Path(font.fname).name == 'LastResortHE-Regular.ttf'
894+
elif ord(char) > 127:
893895
assert Path(font.fname).name == 'DejaVuSans.ttf'
894896
else:
895897
assert Path(font.fname).name == 'cmr10.ttf'

lib/matplotlib/tests/test_text.py

+1
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ def test_pdf_kerning():
823823

824824

825825
def test_unsupported_script(recwarn):
826+
# plt.rcParams['font.enable_last_resort'] = False
826827
fig = plt.figure()
827828
t = fig.text(.5, .5, "\N{BENGALI DIGIT ZERO}")
828829
fig.canvas.draw()

src/ft2font.cpp

+12-8
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,11 @@ FT2Font::get_path(std::vector<double> &vertices, std::vector<unsigned char> &cod
258258
FT2Font::FT2Font(FT_Open_Args &open_args,
259259
long hinting_factor_,
260260
std::vector<FT2Font *> &fallback_list,
261-
FT2Font::WarnFunc warn)
262-
: ft_glyph_warn(warn), image(), face(nullptr)
261+
FT2Font::WarnFunc warn, bool warn_if_used)
262+
: ft_glyph_warn(warn), warn_if_used(warn_if_used), image(), face(nullptr),
263+
hinting_factor(hinting_factor_),
264+
// set default kerning factor to 0, i.e., no kerning manipulation
265+
kerning_factor(0)
263266
{
264267
clear();
265268

@@ -268,12 +271,7 @@ FT2Font::FT2Font(FT_Open_Args &open_args,
268271
throw_ft_error("Can not load face", error);
269272
}
270273

271-
// set default kerning factor to 0, i.e., no kerning manipulation
272-
kerning_factor = 0;
273-
274274
// set a default fontsize 12 pt at 72dpi
275-
hinting_factor = hinting_factor_;
276-
277275
error = FT_Set_Char_Size(face, 12 * 64, 0, 72 * (unsigned int)hinting_factor, 72);
278276
if (error) {
279277
FT_Done_Face(face);
@@ -441,6 +439,8 @@ void FT2Font::set_text(
441439
char_to_font[codepoint] = ft_object_with_glyph;
442440
glyph_to_font[glyph_index] = ft_object_with_glyph;
443441
ft_object_with_glyph->load_glyph(glyph_index, flags, ft_object_with_glyph, false);
442+
} else if (ft_object_with_glyph->warn_if_used) {
443+
ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts);
444444
}
445445

446446
// retrieve kerning distance and move pen position
@@ -510,6 +510,8 @@ void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool
510510
else if (glyph_error) {
511511
throw_ft_error("Could not load charcode", glyph_error);
512512
}
513+
} else if (ft_object_with_glyph->warn_if_used) {
514+
ft_glyph_warn(charcode, glyph_seen_fonts);
513515
}
514516
ft_object = ft_object_with_glyph;
515517
} else {
@@ -569,7 +571,9 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph,
569571
bool override = false)
570572
{
571573
FT_UInt glyph_index = FT_Get_Char_Index(face, charcode);
572-
glyph_seen_fonts.insert(face->family_name);
574+
if (!warn_if_used) {
575+
glyph_seen_fonts.insert(face->family_name);
576+
}
573577

574578
if (glyph_index || override) {
575579
charcode_error = FT_Load_Glyph(face, glyph_index, flags);

src/ft2font.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ class FT2Font
7171

7272
public:
7373
FT2Font(FT_Open_Args &open_args, long hinting_factor,
74-
std::vector<FT2Font *> &fallback_list, WarnFunc warn);
74+
std::vector<FT2Font *> &fallback_list,
75+
WarnFunc warn, bool warn_if_used);
7576
virtual ~FT2Font();
7677
void clear();
7778
void set_size(double ptsize, double dpi);
@@ -139,6 +140,7 @@ class FT2Font
139140

140141
private:
141142
WarnFunc ft_glyph_warn;
143+
bool warn_if_used;
142144
FT2Image image;
143145
FT_Face face;
144146
FT_Vector pen; /* untransformed origin */

src/ft2font_wrapper.cpp

+10-2
Original file line numberDiff line numberDiff line change
@@ -435,14 +435,20 @@ const char *PyFT2Font_init__doc__ = R"""(
435435
_kerning_factor : int, optional
436436
Used to adjust the degree of kerning.
437437
438+
.. warning::
439+
This API is private: do not use it directly.
440+
441+
_warn_if_used : bool, optional
442+
Used to trigger missing glyph warnings.
443+
438444
.. warning::
439445
This API is private: do not use it directly.
440446
)""";
441447

442448
static PyFT2Font *
443449
PyFT2Font_init(py::object filename, long hinting_factor = 8,
444450
std::optional<std::vector<PyFT2Font *>> fallback_list = std::nullopt,
445-
int kerning_factor = 0)
451+
int kerning_factor = 0, bool warn_if_used = false)
446452
{
447453
if (hinting_factor <= 0) {
448454
throw py::value_error("hinting_factor must be greater than 0");
@@ -491,7 +497,8 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8,
491497
self->stream.close = nullptr;
492498
}
493499

494-
self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn);
500+
self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn,
501+
warn_if_used);
495502

496503
self->x->set_kerning_factor(kerning_factor);
497504

@@ -1714,6 +1721,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
17141721
.def(py::init(&PyFT2Font_init),
17151722
"filename"_a, "hinting_factor"_a=8, py::kw_only(),
17161723
"_fallback_list"_a=py::none(), "_kerning_factor"_a=0,
1724+
"_warn_if_used"_a=false,
17171725
PyFT2Font_init__doc__)
17181726
.def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__)
17191727
.def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a,

0 commit comments

Comments
 (0)