diff --git a/src/Graphics/Fonts/smart_font.cpp b/src/Graphics/Fonts/smart_font.cpp index 15fd477c28..76e0d2ce69 100644 --- a/src/Graphics/Fonts/smart_font.cpp +++ b/src/Graphics/Fonts/smart_font.cpp @@ -1151,6 +1151,16 @@ use_italic_greek (array a) { return count <= 1; } +// 单个 Unicode 字符的码点;多字符串、空串或 TeXmacs 专用标记( 等)返回 -1 +static int +unicode_code_of (string c) { + string uc= strict_cork_to_utf8 (c); + if (N (uc) == 0) return -1; + int pos = 0; + int code= decode_from_utf8 (uc, pos); + return (pos == N (uc)) ? code : -1; +} + int smart_font_rep::resolve (string c) { if (DEBUG_VERBOSE) { @@ -1163,6 +1173,11 @@ smart_font_rep::resolve (string c) { // Special handling for emoji characters - bypass font-family restrictions string range= get_unicode_range (c); + // U+20E3 COMBINING ENCLOSING KEYCAP 与 0-9、#、* 组成键帽序列(如 U+0030 U+20E3)。 + // lolly 把 U+2000-U+23FF 整体归为 mathsymbols,键帽因此走数学字体链;默认 + // 数学字体没有该字形,最终渲染为 <#20E3> 占位符(#4539)。它实际是 emoji + // 修饰符,应与 emoji 共用回退链。 + bool emoji_chain= (range == "emoji") || (unicode_code_of (c) == 0x20E3); // 如果设置了字体,就优先使用当前设置的字体 if (fn[SUBFONT_MAIN]->supports (c)) { bool prefer_italic_greek= false; @@ -1178,7 +1193,7 @@ smart_font_rep::resolve (string c) { } } - if (range == "emoji") { + if (emoji_chain) { for (int i= 0; i < N (a); i++) { array parts= trimmed_tokenize (a[i], "="); if (N (parts) >= 2 && parts[0] == "emoji") { @@ -1220,6 +1235,15 @@ smart_font_rep::resolve (string c) { maybe_initialize_font (nr); return sm->add_char (key, c); } + // 最后兜底自带的 Asana Math:其 cmap 含 U+20E3 等组合用符号 + font asana_fn= + closest_font ("Asana Math", "rm", "medium", "right", sz, dpi, 1); + if (!is_nil (asana_fn) && asana_fn->supports (c)) { + tree key= tuple ("emoji-font", "Asana Math"); + int nr = sm->add_font (key, REWRITE_NONE); + maybe_initialize_font (nr); + return sm->add_char (key, c); + } } // Fallback Cyrillic characters to default Chinese font diff --git a/tests/Graphics/Fonts/smart_font_test.cpp b/tests/Graphics/Fonts/smart_font_test.cpp index cedfb1167f..e93d91c241 100644 --- a/tests/Graphics/Fonts/smart_font_test.cpp +++ b/tests/Graphics/Fonts/smart_font_test.cpp @@ -49,6 +49,8 @@ private slots: void test_noto_sans_symbols2_font_support (); void test_misc_symbols_fallback (); void test_misc_symbols_fallback_to_symbols2 (); + void test_keycap_combining_font_support (); + void test_keycap_combining_fallback (); }; void @@ -326,5 +328,34 @@ TestSmartFont::test_misc_symbols_fallback_to_symbols2 () { QVERIFY (occurs ("NotoSansSymbols2", fn_rep->fn[nr]->res_name)); } +void +TestSmartFont::test_keycap_combining_font_support () { + // U+20E3 COMBINING ENCLOSING KEYCAP 必须至少被一款自带回退字体支持, + // 否则 emoji 回退链无处可去(4539 的前提:Asana Math cmap 含 U+20E3) + font asana_fn= + closest_font ("Asana Math", "rm", "medium", "right", 10, 600, 1); + font noto_fn= + closest_font ("Noto Sans Symbols", "rm", "medium", "right", 10, 600, 1); + font noto2_fn= + closest_font ("Noto Sans Symbols2", "rm", "medium", "right", 10, 600, 1); + bool supported= (!is_nil (asana_fn) && asana_fn->supports ("<#20E3>")) || + (!is_nil (noto_fn) && noto_fn->supports ("<#20E3>")) || + (!is_nil (noto2_fn) && noto2_fn->supports ("<#20E3>")); + QVERIFY (supported); +} + +void +TestSmartFont::test_keycap_combining_fallback () { + // U+20E3 落在 lolly 的 mathsymbols 区间(U+2000-U+23FF),此前走数学字体链, + // 默认数学字体无该字形,渲染为 <#20E3> 占位符(4539)。 + // 现应与 emoji 共用回退链,路由到某款符号字体而非 error 字体。 + font fn= smart_font ("sys-chinese", "rm", "medium", "right", 10, 600); + smart_font_rep* fn_rep= (smart_font_rep*) fn.rep; + int nr = fn_rep->resolve ("<#20E3>"); + QVERIFY (nr >= 0); + QVERIFY (!occurs ("error", fn_rep->fn[nr]->res_name)); + QVERIFY (fn_rep->fn[nr]->supports ("<#20E3>")); +} + QTEST_MAIN (TestSmartFont) #include "smart_font_test.moc"