diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index 18bfb550c90b..8625186ba8ec 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -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 diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 68ce02cc366c..ae17c7a4a4e3 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -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") diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 0139bdf41526..2e2ce673f4b8 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -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"]: @@ -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): @@ -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): @@ -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: diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 7222d5b00cf7..3c14357e45f3 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -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.",)]) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index fbc00dc69696..b20f224715bf 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -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 family_names) { PyObject *text_helpers = NULL, *tmp = NULL; + std::set::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: @@ -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 @@ -510,13 +507,13 @@ void FT2Font::set_text( FT_Pos last_advance; FT_Error charcode_error, glyph_error; + std::set 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; @@ -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 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 @@ -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); } @@ -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); } @@ -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 &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) { @@ -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; } @@ -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) diff --git a/src/ft2font.h b/src/ft2font.h index d566c3f9bd9d..69dafb66bdca 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -5,6 +5,7 @@ #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H #include +#include #include #include @@ -91,6 +92,7 @@ class FT2Font FT_Int32 flags, FT_Error &charcode_error, FT_Error &glyph_error, + std::set &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);