From 2f1c74981f3a70dda055562cbd403ab19df59fcb Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 14 Oct 2025 20:13:18 +0100 Subject: [PATCH] FreeType: Make font fallback slightly more robust On Ubuntu 25.10, which includes Noto Color Emoji, I was seeing that the FontsDemo would assert when attempting to render non-emoji text using this font. It appears that FontConfig will tend to return Noto Color Emoji when this family name is passed, even though the font may not cover the required character set. The new strategy is to use FontConfig as before, but then to check the resolved font for coverage of the string. If the resolved font still can't render the string, we relax the font matching constraints by removing the family name from the pattern, then try matching again. --- .../native/juce_Fonts_freetype.cpp | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/modules/juce_graphics/native/juce_Fonts_freetype.cpp b/modules/juce_graphics/native/juce_Fonts_freetype.cpp index a6448d1e72..63bde137f9 100644 --- a/modules/juce_graphics/native/juce_Fonts_freetype.cpp +++ b/modules/juce_graphics/native/juce_Fonts_freetype.cpp @@ -429,37 +429,52 @@ public: if (cache == nullptr) return {}; - FcPatternPtr pattern { FcPatternCreate() }; - + const auto makeBasicPattern = [&] { + FcPatternPtr pattern { FcPatternCreate() }; + + { + FcValue value{}; + value.type = FcTypeString; + value.u.s = unalignedPointerCast (ftFace->face->style_name); + FcPatternAdd (pattern.get(), FC_STYLE, value, FcFalse); + } + + { + const FcCharSetPtr charset { FcCharSetCreate() }; + for (const auto& character : text) + FcCharSetAddChar (charset.get(), (FcChar32) character); + FcPatternAddCharSet (pattern.get(), FC_CHARSET, charset.get()); + } + + if (language.isNotEmpty()) + { + const FcLangSetPtr langset { FcLangSetCreate() }; + FcLangSetAdd (langset.get(), unalignedPointerCast (language.toRawUTF8())); + FcPatternAddLangSet (pattern.get(), FC_LANG, langset.get()); + } + + return pattern; + }; + + const auto fallbackWithFamily = std::invoke ([&] + { + auto pattern = makeBasicPattern(); + FcValue value{}; value.type = FcTypeString; value.u.s = unalignedPointerCast (ftFace->face->family_name); FcPatternAddWeak (pattern.get(), FC_FAMILY, value, FcFalse); - } - { - FcValue value{}; - value.type = FcTypeString; - value.u.s = unalignedPointerCast (ftFace->face->style_name); - FcPatternAddWeak (pattern.get(), FC_STYLE, value, FcFalse); - } + return fromPattern (pattern.get()); + }); - { - const FcCharSetPtr charset { FcCharSetCreate() }; - for (const auto& character : text) - FcCharSetAddChar (charset.get(), (FcChar32) character); - FcPatternAddCharSet (pattern.get(), FC_CHARSET, charset.get()); - } + if (text.isEmpty() || fallbackWithFamily->getNominalGlyphForCodepoint (*text.getCharPointer()).has_value()) + return fallbackWithFamily; - if (language.isNotEmpty()) - { - const FcLangSetPtr langset { FcLangSetCreate() }; - FcLangSetAdd (langset.get(), unalignedPointerCast (language.toRawUTF8())); - FcPatternAddLangSet (pattern.get(), FC_LANG, langset.get()); - } - - return fromPattern (pattern.get()); + const auto fallbackWithoutFamily = fromPattern (makeBasicPattern().get()); + jassert (fallbackWithoutFamily->getNominalGlyphForCodepoint (*text.getCharPointer()).has_value()); + return fallbackWithoutFamily; #else // Font substitution will not work unless fontconfig is enabled. jassertfalse;