Skip to content

MNT: print fontname in missing glyph warning #26989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions lib/matplotlib/_text_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
"LayoutItem", ["ft_object", "char", "glyph_idx", "x", "prev_kern"])


def warn_on_missing_glyph(codepoint):
def warn_on_missing_glyph(codepoint, fontnames):
_api.warn_external(
"Glyph {} ({}) missing from current font.".format(
codepoint,
chr(codepoint).encode("ascii", "namereplace").decode("ascii")))
f"Glyph {codepoint} "
f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) "
f"missing from font(s) {fontnames}.")

block = ("Hebrew" if 0x0590 <= codepoint <= 0x05ff else
"Arabic" if 0x0600 <= codepoint <= 0x06ff else
"Devanagari" if 0x0900 <= codepoint <= 0x097f else
Expand Down
16 changes: 16 additions & 0 deletions lib/matplotlib/tests/test_backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,19 @@ def test_multi_font_type42():

fig = plt.figure()
fig.text(0.15, 0.475, "There are 几个汉字 in between!")


@pytest.mark.parametrize('family_name, file_name',
[("Noto Sans", "NotoSans-Regular.otf"),
("FreeMono", "FreeMono.otf")])
def test_otf_font_smoke(family_name, file_name):
# checks that there's no segfault
fp = fm.FontProperties(family=[family_name])
if Path(fm.findfont(fp)).name != file_name:
pytest.skip(f"Font {family_name} may be missing")

plt.rc('font', family=[family_name], size=27)

fig = plt.figure()
fig.text(0.15, 0.475, "Привет мир!")
fig.savefig(io.BytesIO(), format="pdf")
46 changes: 29 additions & 17 deletions lib/matplotlib/tests/test_ft2font.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,21 @@ def test_ft2font_positive_hinting_factor():
ft2font.FT2Font(file_name, 0)


def test_fallback_smoke():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font wqy-zenhei.ttc may be missing")

fp = fm.FontProperties(family=["Noto Sans CJK JP"])
if Path(fm.findfont(fp)).name != "NotoSansCJK-Regular.ttc":
pytest.skip("Noto Sans CJK JP font may be missing.")

@pytest.mark.parametrize('family_name, file_name',
[("WenQuanYi Zen Hei", "wqy-zenhei.ttc"),
("Noto Sans CJK JP", "NotoSansCJK.ttc"),
("Noto Sans TC", "NotoSansTC-Regular.otf")]
)
def test_fallback_smoke(family_name, file_name):
fp = fm.FontProperties(family=[family_name])
if Path(fm.findfont(fp)).name != file_name:
pytest.skip(f"Font {family_name} ({file_name}) is missing")
plt.rcParams['font.size'] = 20
fig = plt.figure(figsize=(4.75, 1.85))
fig.text(0.05, 0.45, "There are 几个汉字 in between!",
family=['DejaVu Sans', "Noto Sans CJK JP"])
fig.text(0.05, 0.25, "There are 几个汉字 in between!",
family=['DejaVu Sans', "WenQuanYi Zen Hei"])
fig.text(0.05, 0.65, "There are 几个汉字 in between!",
family=["Noto Sans CJK JP"])
family=['DejaVu Sans', family_name])
fig.text(0.05, 0.85, "There are 几个汉字 in between!",
family=["WenQuanYi Zen Hei"])
family=[family_name])

# TODO enable fallback for other backends!
for fmt in ['png', 'raw']: # ["svg", "pdf", "ps"]:
Expand All @@ -57,7 +53,8 @@ def test_fallback_smoke():

@pytest.mark.parametrize('family_name, file_name',
[("WenQuanYi Zen Hei", "wqy-zenhei"),
("Noto Sans CJK JP", "NotoSansCJK")]
("Noto Sans CJK JP", "NotoSansCJK"),
("Noto Sans TC", "NotoSansTC-Regular.otf")]
)
@check_figures_equal(extensions=["png", "pdf", "eps", "svg"])
def test_font_fallback_chinese(fig_test, fig_ref, family_name, file_name):
Expand All @@ -78,11 +75,27 @@ def test_font_fallback_chinese(fig_test, fig_ref, family_name, file_name):
fig_test.text(0.05, .85 - 0.15*j, txt, family=test_font)


@pytest.mark.parametrize("font_list",
[['DejaVu Serif', 'DejaVu Sans'],
['DejaVu Sans Mono']],
ids=["two fonts", "one font"])
def test_fallback_missing(recwarn, font_list):
fig = plt.figure()
fig.text(.5, .5, "Hello 🙃 World!", family=font_list)
fig.canvas.draw()
assert all(isinstance(warn.message, UserWarning) for warn in recwarn)
# not sure order is guaranteed on the font listing so
assert recwarn[0].message.args[0].startswith(
"Glyph 128579 (\\N{UPSIDE-DOWN FACE}) missing from font(s)")
assert all([font in recwarn[0].message.args[0] for font in font_list])


@pytest.mark.parametrize(
"family_name, file_name",
[
("WenQuanYi Zen Hei", "wqy-zenhei"),
("Noto Sans CJK JP", "NotoSansCJK"),
("Noto Sans TC", "NotoSansTC-Regular.otf")
],
)
def test__get_fontmap(family_name, file_name):
Expand All @@ -97,7 +110,6 @@ def test__get_fontmap(family_name, file_name):
fm.FontProperties(family=["DejaVu Sans", family_name])
)
)

fontmap = ft._get_fontmap(text)
for char, font in fontmap.items():
if ord(char) > 127:
Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/tests/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,12 +817,13 @@ def test_pdf_kerning():

def test_unsupported_script(recwarn):
fig = plt.figure()
fig.text(.5, .5, "\N{BENGALI DIGIT ZERO}")
t = fig.text(.5, .5, "\N{BENGALI DIGIT ZERO}")
fig.canvas.draw()
assert all(isinstance(warn.message, UserWarning) for warn in recwarn)
assert (
[warn.message.args for warn in recwarn] ==
[(r"Glyph 2534 (\N{BENGALI DIGIT ZERO}) missing from current font.",),
[(r"Glyph 2534 (\N{BENGALI DIGIT ZERO}) missing from font(s) "
+ f"{t.get_fontname()}.",),
(r"Matplotlib currently does not support Bengali natively.",)])


Expand Down
62 changes: 33 additions & 29 deletions src/ft2font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <algorithm>
#include <iterator>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
Expand Down Expand Up @@ -184,11 +185,20 @@ FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1,
m_dirty = true;
}

static void ft_glyph_warn(FT_ULong charcode)
static void ft_glyph_warn(FT_ULong charcode, std::set<FT_String*> family_names)
{
PyObject *text_helpers = NULL, *tmp = NULL;
std::set<FT_String*>::iterator it = family_names.begin();
std::stringstream ss;
ss<<*it;
while(++it != family_names.end()){
ss<<", "<<*it;
}

if (!(text_helpers = PyImport_ImportModule("matplotlib._text_helpers")) ||
!(tmp = PyObject_CallMethod(text_helpers, "warn_on_missing_glyph", "k", charcode))) {
!(tmp = PyObject_CallMethod(text_helpers,
"warn_on_missing_glyph", "(k, s)",
charcode, ss.str().c_str()))) {
goto exit;
}
exit:
Expand All @@ -199,19 +209,6 @@ static void ft_glyph_warn(FT_ULong charcode)
}
}

static FT_UInt
ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode, bool warn = true)
{
FT_UInt glyph_index = FT_Get_Char_Index(face, charcode);
if (glyph_index) {
return glyph_index;
}
if (warn) {
ft_glyph_warn(charcode);
}
return 0;
}

// ft_outline_decomposer should be passed to FT_Outline_Decompose. On the
// first pass, vertices and codes are set to NULL, and index is simply
// incremented for each vertex that should be inserted, so that it is set, at
Expand Down Expand Up @@ -510,13 +507,13 @@ void FT2Font::set_text(
FT_Pos last_advance;

FT_Error charcode_error, glyph_error;
std::set<FT_String*> glyph_seen_fonts;
FT2Font *ft_object_with_glyph = this;
bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs,
char_to_font, glyph_to_font, codepoints[n], flags,
charcode_error, glyph_error, false);
charcode_error, glyph_error, glyph_seen_fonts, false);
if (!was_found) {
ft_glyph_warn((FT_ULong)codepoints[n]);

ft_glyph_warn((FT_ULong)codepoints[n], glyph_seen_fonts);
// render missing glyph tofu
// come back to top-most font
ft_object_with_glyph = this;
Expand Down Expand Up @@ -570,6 +567,7 @@ void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool
// if this is parent FT2Font, cache will be filled in 2 ways:
// 1. set_text was previously called
// 2. set_text was not called and fallback was enabled
std::set <FT_String *> glyph_seen_fonts;
if (fallback && char_to_font.find(charcode) != char_to_font.end()) {
ft_object = char_to_font[charcode];
// since it will be assigned to ft_object anyway
Expand All @@ -579,10 +577,12 @@ void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool
FT_UInt final_glyph_index;
FT_Error charcode_error, glyph_error;
FT2Font *ft_object_with_glyph = this;
bool was_found = load_char_with_fallback(ft_object_with_glyph, final_glyph_index, glyphs, char_to_font,
glyph_to_font, charcode, flags, charcode_error, glyph_error, true);
bool was_found = load_char_with_fallback(ft_object_with_glyph, final_glyph_index,
glyphs, char_to_font, glyph_to_font,
charcode, flags, charcode_error, glyph_error,
glyph_seen_fonts, true);
if (!was_found) {
ft_glyph_warn(charcode);
ft_glyph_warn(charcode, glyph_seen_fonts);
if (charcode_error) {
throw_ft_error("Could not load charcode", charcode_error);
}
Expand All @@ -592,9 +592,13 @@ void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool
}
ft_object = ft_object_with_glyph;
} else {
//no fallback case
ft_object = this;
FT_UInt glyph_index = ft_get_char_index_or_warn(face, (FT_ULong)charcode);

FT_UInt glyph_index = FT_Get_Char_Index(face, (FT_ULong) charcode);
if (!glyph_index){
glyph_seen_fonts.insert((face != NULL)?face->family_name: NULL);
ft_glyph_warn((FT_ULong)charcode, glyph_seen_fonts);
}
if (FT_Error error = FT_Load_Glyph(face, glyph_index, flags)) {
throw_ft_error("Could not load charcode", error);
}
Expand Down Expand Up @@ -640,16 +644,17 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph,
FT_Int32 flags,
FT_Error &charcode_error,
FT_Error &glyph_error,
std::set<FT_String*> &glyph_seen_fonts,
bool override = false)
{
FT_UInt glyph_index = FT_Get_Char_Index(face, charcode);
glyph_seen_fonts.insert(face->family_name);

if (glyph_index || override) {
charcode_error = FT_Load_Glyph(face, glyph_index, flags);
if (charcode_error) {
return false;
}

FT_Glyph thisGlyph;
glyph_error = FT_Get_Glyph(face->glyph, &thisGlyph);
if (glyph_error) {
Expand All @@ -667,12 +672,12 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph,
parent_glyphs.push_back(thisGlyph);
return true;
}

else {
for (size_t i = 0; i < fallbacks.size(); ++i) {
bool was_found = fallbacks[i]->load_char_with_fallback(
ft_object_with_glyph, final_glyph_index, parent_glyphs, parent_char_to_font,
parent_glyph_to_font, charcode, flags, charcode_error, glyph_error, override);
ft_object_with_glyph, final_glyph_index, parent_glyphs,
parent_char_to_font, parent_glyph_to_font, charcode, flags,
charcode_error, glyph_error, glyph_seen_fonts, override);
if (was_found) {
return true;
}
Expand Down Expand Up @@ -721,8 +726,7 @@ FT_UInt FT2Font::get_char_index(FT_ULong charcode, bool fallback = false)
ft_object = this;
}

// historically, get_char_index never raises a warning
return ft_get_char_index_or_warn(ft_object->get_face(), charcode, false);
return FT_Get_Char_Index(ft_object->get_face(), charcode);
}

void FT2Font::get_width_height(long *width, long *height)
Expand Down
2 changes: 2 additions & 0 deletions src/ft2font.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#ifndef MPL_FT2FONT_H
#define MPL_FT2FONT_H
#include <vector>
#include<set>
#include <stdint.h>
#include <unordered_map>

Expand Down Expand Up @@ -91,6 +92,7 @@ class FT2Font
FT_Int32 flags,
FT_Error &charcode_error,
FT_Error &glyph_error,
std::set<FT_String*> &glyph_seen_fonts,
bool override);
void load_glyph(FT_UInt glyph_index, FT_Int32 flags, FT2Font *&ft_object, bool fallback);
void load_glyph(FT_UInt glyph_index, FT_Int32 flags);
Expand Down