From a1b23c0248a587bffdbb0bce4df540aa472d8caa Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 27 Feb 2024 19:11:21 +0000 Subject: [PATCH] Font: Add support for querying fallbacks --- modules/juce_graphics/fonts/juce_Font.cpp | 155 ++++++++++++++++------ modules/juce_graphics/fonts/juce_Font.h | 88 ++++++++---- 2 files changed, 177 insertions(+), 66 deletions(-) diff --git a/modules/juce_graphics/fonts/juce_Font.cpp b/modules/juce_graphics/fonts/juce_Font.cpp index 765885f5f0..5668e3448f 100644 --- a/modules/juce_graphics/fonts/juce_Font.cpp +++ b/modules/juce_graphics/fonts/juce_Font.cpp @@ -50,8 +50,6 @@ namespace FontValues const float defaultFontHeight = 14.0f; float minimumHorizontalScale = 0.7f; - String fallbackFont; - String fallbackFontStyle; } class HbScale @@ -455,12 +453,14 @@ public: instance referencing the shared state. */ + StringArray getFallbackFamilies() const { return fallbacks; } String getTypefaceName() const { return typefaceName; } String getTypefaceStyle() const { return typefaceStyle; } float getHeight() const { return height; } float getHorizontalScale() const { return horizontalScale; } float getKerning() const { return kerning; } bool getUnderline() const { return underline; } + bool getFallbackEnabled() const { return fallback; } /* This shared state may be shared between two or more Font instances that are being read/modified from multiple threads. @@ -469,10 +469,16 @@ public: during the modification. */ - void setTypeface (Typeface::Ptr x) + void setTypeface (Typeface::Ptr newTypeface) { jassert (getReferenceCount() == 1); - typeface = std::move (x); + typeface = newTypeface; + + if (newTypeface != nullptr) + { + typefaceName = typeface->getName(); + typefaceStyle = typeface->getStyle(); + } } void setTypefaceName (String x) @@ -517,6 +523,18 @@ public: underline = x; } + void setFallbackFamilies (const StringArray& x) + { + jassert (getReferenceCount() == 1); + fallbacks = x; + } + + void setFallback (bool x) + { + jassert (getReferenceCount() == 1); + fallback = x; + } + private: static float legacyHeightToPoints (Typeface::Ptr p, float h) { @@ -524,9 +542,11 @@ private: } Typeface::Ptr typeface; + StringArray fallbacks; String typefaceName, typefaceStyle; float height = 0.0f, horizontalScale = 1.0f, kerning = 0.0f, ascent = 0.0f; bool underline = false; + bool fallback = true; CriticalSection mutex; }; @@ -659,45 +679,39 @@ StringArray Font::getAvailableStyles() const return findAllTypefaceStyles (getTypefacePtr()->getName()); } +void Font::setPreferredFallbackFamilies (const StringArray& fallbacks) +{ + if (getPreferredFallbackFamilies() != fallbacks) + { + dupeInternalIfShared(); + font->setFallbackFamilies (fallbacks); + } +} + +StringArray Font::getPreferredFallbackFamilies() const +{ + return font->getFallbackFamilies(); +} + +void Font::setFallbackEnabled (bool enabled) +{ + if (getFallbackEnabled() != enabled) + { + dupeInternalIfShared(); + font->setFallback (enabled); + } +} + +bool Font::getFallbackEnabled() const +{ + return font->getFallbackEnabled(); +} + Typeface::Ptr Font::getTypefacePtr() const { return font->getTypefacePtr (*this); } -Typeface* Font::getTypeface() const -{ - return getTypefacePtr().get(); -} - -//============================================================================== -const String& Font::getFallbackFontName() -{ - return FontValues::fallbackFont; -} - -void Font::setFallbackFontName (const String& name) -{ - FontValues::fallbackFont = name; - - #if JUCE_MAC || JUCE_IOS - jassertfalse; // Note that use of a fallback font isn't currently implemented in OSX.. - #endif -} - -const String& Font::getFallbackFontStyle() -{ - return FontValues::fallbackFontStyle; -} - -void Font::setFallbackFontStyle (const String& style) -{ - FontValues::fallbackFontStyle = style; - - #if JUCE_MAC || JUCE_IOS - jassertfalse; // Note that use of a fallback font isn't currently implemented in OSX.. - #endif -} - //============================================================================== Font Font::withHeight (const float newHeight) const { @@ -944,6 +958,71 @@ void Font::findFonts (Array& destArray) } } +static bool characterNotRendered (uint32_t c) +{ + constexpr uint32_t points[] + { + // Control points + 0x0000, 0x0007, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x001A, 0x001B, 0x0085, + + // BIDI control points + 0x061C, 0x200E, 0x200F, 0x202A, 0x202B, 0x202C, 0x202D, 0x202E, 0x2066, 0x2067, 0x2068, 0x2069 + }; + + return std::find (std::begin (points), std::end (points), c) != std::end (points); +} + +static bool isFontSuitableForCodepoint (const Font& font, juce_wchar c) +{ + const auto& hbFont = font.getNativeDetails().font; + hb_codepoint_t glyph{}; + + return characterNotRendered ((uint32_t) c) + || hb_font_get_nominal_glyph (hbFont.get(), (hb_codepoint_t) c, &glyph); +} + +static bool isFontSuitableForText (const Font& font, const String& str) +{ + for (const auto c : str) + if (! isFontSuitableForCodepoint (font, c)) + return false; + + return true; +} + +Font Font::findSuitableFontForText (const String& text, const String& language) const +{ + if (! getFallbackEnabled() || isFontSuitableForText (*this, text)) + return *this; + + for (const auto& fallback : getPreferredFallbackFamilies()) + { + auto copy = *this; + copy.setTypefaceName (fallback); + + if (isFontSuitableForText (copy, text)) + return copy; + } + + if (auto current = getTypefacePtr()) + { + if (auto suggested = current->createSystemFallback (text, language)) + { + auto copy = *this; + + if (copy.getTypefacePtr() != suggested) + { + copy.dupeInternalIfShared(); + copy.font->setTypeface (suggested); + } + + return copy; + } + } + + return *this; +} + //============================================================================== String Font::toString() const { diff --git a/modules/juce_graphics/fonts/juce_Font.h b/modules/juce_graphics/fonts/juce_Font.h index f8d0f17f02..d2033c6ef8 100644 --- a/modules/juce_graphics/fonts/juce_Font.h +++ b/modules/juce_graphics/fonts/juce_Font.h @@ -168,6 +168,30 @@ public: /** Returns a list of the styles that this font can use. */ StringArray getAvailableStyles() const; + //============================================================================== + /** Sets the names of the fallback font families that should be tried, in order, + when searching for glyphs that are missing in the main typeface, specified via + setTypefaceName() or Font(const Typeface::Ptr&). + */ + void setPreferredFallbackFamilies (const StringArray& fallbacks); + + /** Returns the names of the fallback font families. + */ + StringArray getPreferredFallbackFamilies() const; + + /** When drawing text using this Font, specifies whether glyphs that are missing in the main + typeface should be replaced with glyphs from other fonts. + To find missing glyphs, the typefaces for the preferred fallback families will be checked + in order, followed by the system fallback fonts. The system fallback font is likely to be + different on each platform. + + Fallback is enabled by default. + */ + void setFallbackEnabled (bool enabled); + + /** Returns true if fallback is enabled, or false otherwise. */ + bool getFallbackEnabled() const; + //============================================================================== /** Returns a typeface font family that represents the default sans-serif font. @@ -208,7 +232,13 @@ public: */ static const String& getDefaultStyle(); - /** Returns the default system typeface for the given font. */ + /** Returns the default system typeface for the given font. + + Note: This will only ever return the typeface for the font's "main" family. + Before attempting to render glyphs from this typeface, it's a good idea to check + that those glyphs are present in the typeface, and to select a different + face if necessary. + */ static Typeface::Ptr getDefaultTypefaceForFont (const Font& font); //============================================================================== @@ -346,7 +376,8 @@ public: */ static void setDefaultMinimumHorizontalScaleFactor (float newMinimumScaleFactor) noexcept; - /** Returns the font's kerning. + /** Returns the font's tracking, i.e. spacing applied between characters in + addition to the kerning defined by the font. This is the extra space added between adjacent characters, as a proportion of the font's height. @@ -356,7 +387,7 @@ public: */ float getExtraKerningFactor() const noexcept; - /** Returns a copy of this font with a new kerning factor. + /** Returns a copy of this font with a new tracking factor. @param extraKerning a multiple of the font's height that will be added to space between the characters. So a value of zero is normal spacing, positive values spread the letters out, @@ -364,7 +395,7 @@ public: */ [[nodiscard]] Font withExtraKerningFactor (float extraKerning) const; - /** Changes the font's kerning. + /** Changes the font's tracking. @param extraKerning a multiple of the font's height that will be added to space between the characters. So a value of zero is normal spacing, positive values spread the letters out, @@ -404,17 +435,13 @@ public: void getGlyphPositions (const String& text, Array& glyphs, Array& xOffsets) const; //============================================================================== - #ifndef DOXYGEN - /** Returns the typeface used by this font. + /** Returns the main typeface used by this font. - Note that the object returned may go out of scope if this font is deleted - or has its style changed. + Note: This will only ever return the typeface for the "main" family. + Before attempting to render glyphs from this typeface, it's a good idea to check + that those glyphs are present in the typeface, and to select a different + face if necessary. */ - [[deprecated ("This method is unsafe, use getTypefacePtr() instead.")]] - Typeface* getTypeface() const; - #endif - - /** Returns the typeface used by this font. */ Typeface::Ptr getTypefacePtr() const; /** Creates an array of Font objects to represent all the fonts on the system. @@ -445,25 +472,30 @@ public: static StringArray findAllTypefaceStyles (const String& family); //============================================================================== - /** Returns the font family of the typeface to be used for rendering glyphs that aren't - found in the requested typeface. - */ - static const String& getFallbackFontName(); + /** Attempts to locate a visually similar font that is capable of rendering the + provided string. - /** Sets the (platform-specific) font family of the typeface to use to find glyphs that - aren't available in whatever font you're trying to use. - */ - static void setFallbackFontName (const String& name); + If fallback is disabled on this Font by setFallbackEnabled(), then this will + always return a copy of the current Font. - /** Returns the font style of the typeface to be used for rendering glyphs that aren't - found in the requested typeface. - */ - static const String& getFallbackFontStyle(); + Otherwise, the current font, then each of the fallback fonts specified by + setPreferredFallbackFamilies() will be checked, and the first Font that is + capable of rendering the string will be returned. If none of these fonts is + suitable, then the system font fallback mechanism will be used to locate a + font from the currently installed fonts. If the system also cannot find any + suitable font, then a copy of the original Font will be returned. - /** Sets the (platform-specific) font style of the typeface to use to find glyphs that - aren't available in whatever font you're trying to use. + Note that most fonts don't contain glyphs for all possible unicode codepoints, + and instead may contain e.g. just the glyphs required for a specific script. So, + if the provided text would be displayed using several scripts (multiple languages, + emoji, etc.) then there's a good chance that no single font will be able to + render the entire text. Shorter strings will generally produce better fallback + results than longer strings, with the caveat that the system may take control + characters such as combining marks and variation selectors into account when + selecting suitable fonts, so querying fallbacks character-by-character is likely + to produce poor results. */ - static void setFallbackFontStyle (const String& style); + Font findSuitableFontForText (const String& text, const String& language = {}) const; //============================================================================== /** Creates a string to describe this font.