From 57103da08d3fbdc68284fa456f69ab338d563b46 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Fri, 19 Jun 2026 12:33:20 +0100 Subject: [PATCH] LibGfx+LibWeb: Make emoji font fallback presentation-aware Previously, font selection ignored the Unicode emoji presentation of a code point. Emoji-capable code points were always resolved through pre-baked color emoji and symbol fonts. Text-default code points which lacked the `Emoji_Presentation` property, were therefore rendered as color emoji rather than text. We now classify each code point's default presentation from its `Emoji_Presentation` property and any trailing variation selector. --- Libraries/LibGfx/Font/FontDatabase.cpp | 6 +- Libraries/LibGfx/Font/FontDatabase.h | 5 +- Libraries/LibGfx/Font/TypefaceSkia.cpp | 7 +- Libraries/LibGfx/Font/TypefaceSkia.h | 2 +- Libraries/LibGfx/FontCascadeList.cpp | 83 +++++++++++++++++++----- Libraries/LibGfx/FontCascadeList.h | 24 ++++++- Libraries/LibWeb/CSS/FontComputer.cpp | 19 +++--- Libraries/LibWeb/Layout/TextNode.cpp | 14 +++- Libraries/LibWeb/Layout/TextNode.h | 1 + Libraries/LibWeb/Platform/FontPlugin.cpp | 9 +-- 10 files changed, 124 insertions(+), 46 deletions(-) diff --git a/Libraries/LibGfx/Font/FontDatabase.cpp b/Libraries/LibGfx/Font/FontDatabase.cpp index ef952adac2..1a1fd4e79d 100644 --- a/Libraries/LibGfx/Font/FontDatabase.cpp +++ b/Libraries/LibGfx/Font/FontDatabase.cpp @@ -50,11 +50,11 @@ RefPtr FontDatabase::get(FlyString const& family, float point_size, u return m_system_font_provider->get_font(family, point_size, weight, width, slope, font_variation_settings, shape_features); } -RefPtr FontDatabase::get_font_for_code_point(u32 code_point, float point_size, u16 weight, u16 width, u8 slope) +RefPtr FontDatabase::get_font_for_code_point(u32 code_point, float point_size, u16 weight, u16 width, u8 slope, bool prefer_color_emoji) { - CodePointFallbackKey key { code_point, weight, width, slope }; + CodePointFallbackKey key { code_point, weight, width, slope, prefer_color_emoji }; auto& entry = m_code_point_fallback_cache.ensure(key, [&]() -> CodePointFallbackEntry { - auto typeface_or_error = TypefaceSkia::find_typeface_for_code_point(code_point, weight, width, slope); + auto typeface_or_error = TypefaceSkia::find_typeface_for_code_point(code_point, weight, width, slope, prefer_color_emoji); if (typeface_or_error.is_error() || !typeface_or_error.value()) return { {}, nullptr }; diff --git a/Libraries/LibGfx/Font/FontDatabase.h b/Libraries/LibGfx/Font/FontDatabase.h index 8b0c094699..2d54fdc956 100644 --- a/Libraries/LibGfx/Font/FontDatabase.h +++ b/Libraries/LibGfx/Font/FontDatabase.h @@ -21,6 +21,7 @@ struct CodePointFallbackKey { u16 weight { 0 }; u16 width { 0 }; u8 slope { 0 }; + bool prefer_color_emoji { false }; bool operator==(CodePointFallbackKey const&) const = default; @@ -28,7 +29,7 @@ struct CodePointFallbackKey { { return pair_int_hash( pair_int_hash(code_point, weight), - pair_int_hash(width, slope)); + pair_int_hash(pair_int_hash(width, slope), prefer_color_emoji)); } }; @@ -47,7 +48,7 @@ public: SystemFontProvider& install_system_font_provider(NonnullOwnPtr); RefPtr get(FlyString const& family, float point_size, unsigned weight, unsigned width, unsigned slope, Optional const& font_variation_settings = {}, Optional const& shape_features = {}); - RefPtr get_font_for_code_point(u32 code_point, float point_size, u16 weight, u16 width, u8 slope); + RefPtr get_font_for_code_point(u32 code_point, float point_size, u16 weight, u16 width, u8 slope, bool prefer_color_emoji); void for_each_typeface_with_family_name(FlyString const& family_name, Function); [[nodiscard]] StringView system_font_provider_name() const; diff --git a/Libraries/LibGfx/Font/TypefaceSkia.cpp b/Libraries/LibGfx/Font/TypefaceSkia.cpp index d21b247ac8..b4482d68a5 100644 --- a/Libraries/LibGfx/Font/TypefaceSkia.cpp +++ b/Libraries/LibGfx/Font/TypefaceSkia.cpp @@ -305,12 +305,15 @@ ErrorOr> TypefaceSkia::match_family_style(StringView family return typeface_from_skia_typeface(move(skia_typeface)); } -ErrorOr> TypefaceSkia::find_typeface_for_code_point(u32 code_point, u16 weight, u16 width, u8 slope) +ErrorOr> TypefaceSkia::find_typeface_for_code_point(u32 code_point, u16 weight, u16 width, u8 slope, bool prefer_color_emoji) { SkFontStyle style(weight, width, slope_to_skia_slant(slope)); + // The "und-Zsye" language tag steers the font matcher towards a color emoji font. Without it, a text-presentation + // font is preferred for emoji-capable code points. + char const* emoji_locale[] = { "und-Zsye" }; auto skia_typeface = font_manager().matchFamilyStyleCharacter( - nullptr, style, nullptr, 0, code_point); + nullptr, style, prefer_color_emoji ? emoji_locale : nullptr, prefer_color_emoji ? 1 : 0, code_point); if (!skia_typeface) return RefPtr {}; diff --git a/Libraries/LibGfx/Font/TypefaceSkia.h b/Libraries/LibGfx/Font/TypefaceSkia.h index a0fff60cb8..8a031ffbd5 100644 --- a/Libraries/LibGfx/Font/TypefaceSkia.h +++ b/Libraries/LibGfx/Font/TypefaceSkia.h @@ -31,7 +31,7 @@ public: static ErrorOr> load_from_buffer(ReadonlyBytes, u32 ttc_index = 0); static ErrorOr> match_system_ui(SystemUIFontKind, float point_size, u16 weight, double width, u8 slope); static ErrorOr> match_family_style(StringView family_name, u16 weight, u16 width, u8 slope); - static ErrorOr> find_typeface_for_code_point(u32 code_point, u16 weight, u16 width, u8 slope); + static ErrorOr> find_typeface_for_code_point(u32 code_point, u16 weight, u16 width, u8 slope, bool prefer_color_emoji); static Optional resolve_generic_family(StringView family_name, u16 weight, u8 slope); RefPtr clone_with_variations(Vector const& axes) const; diff --git a/Libraries/LibGfx/FontCascadeList.cpp b/Libraries/LibGfx/FontCascadeList.cpp index af18f8e7b5..46ac77acfd 100644 --- a/Libraries/LibGfx/FontCascadeList.cpp +++ b/Libraries/LibGfx/FontCascadeList.cpp @@ -5,9 +5,24 @@ */ #include +#include namespace Gfx { +EmojiPresentationResult emoji_presentation_for_code_point(u32 code_point, Optional next_code_point) +{ + // VARIATION SELECTOR-16 (emoji) + if (next_code_point == 0xFE0Fu) + return { EmojiPresentation::Emoji, ForcedPresentation::Yes }; + // VARIATION SELECTOR-15 (text) + if (next_code_point == 0xFE0Eu) + return { EmojiPresentation::Text, ForcedPresentation::Yes }; + + if (Unicode::code_point_has_emoji_presentation_property(code_point)) + return { EmojiPresentation::Emoji, ForcedPresentation::No }; + return { EmojiPresentation::Text, ForcedPresentation::No }; +} + void FontCascadeList::add(NonnullRefPtr font) { m_fonts.append({ move(font), {} }); @@ -58,7 +73,12 @@ void FontCascadeList::extend(FontCascadeList const& other) m_pending_faces.extend(other.m_pending_faces); } -Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point, TriggerPendingLoads trigger_pending_loads) const +void FontCascadeList::extend_fallback(FontCascadeList const& other) +{ + m_fallback_fonts.extend(other.m_fonts); +} + +Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point, TriggerPendingLoads trigger_pending_loads, EmojiPresentationResult emoji_presentation) const { // Only the text-shaping paths pass TriggerPendingLoads::Yes. Probes that don't // lead to a glyph being drawn (the U+0020 check used to compute first-available- @@ -80,37 +100,68 @@ Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point, TriggerPen }); } - if (code_point < m_ascii_cache.size()) { + auto use_ascii_cache = code_point < m_ascii_cache.size() && emoji_presentation.presentation == EmojiPresentation::Text && emoji_presentation.forced == ForcedPresentation::No; + if (use_ascii_cache) { if (auto const* cached = m_ascii_cache[code_point]) return *cached; } auto cache_and_return = [&](Font const& font) -> Font const& { - if (code_point < m_ascii_cache.size()) + if (use_ascii_cache) m_ascii_cache[code_point] = &font; return font; }; - for (auto const& entry : m_fonts) { - if (entry.range_data.has_value()) { - if (!entry.range_data->enclosing_range.contains(code_point)) - continue; - for (auto const& range : entry.range_data->unicode_ranges) { - if (range.contains(code_point) && entry.font->contains_glyph(code_point)) - return cache_and_return(*entry.font); - } - } else if (entry.font->contains_glyph(code_point)) { - return cache_and_return(*entry.font); + auto presentation_matches = [wants_emoji = emoji_presentation.presentation == EmojiPresentation::Emoji](Font const& font) { + return font.is_emoji_font() == wants_emoji; + }; + + auto entry_contains_glyph = [code_point](Entry const& entry) { + if (!entry.range_data.has_value()) + return entry.font->contains_glyph(code_point); + if (!entry.range_data->enclosing_range.contains(code_point)) + return false; + for (auto const& range : entry.range_data->unicode_ranges) { + if (range.contains(code_point) && entry.font->contains_glyph(code_point)) + return true; } + return false; + }; + + Font const* author_glyph_match = nullptr; + for (auto const& entry : m_fonts) { + if (!entry_contains_glyph(entry)) + continue; + if (emoji_presentation.forced == ForcedPresentation::No || presentation_matches(*entry.font)) + return cache_and_return(*entry.font); + if (!author_glyph_match) + author_glyph_match = entry.font.ptr(); + } + + Font const* fallback_glyph_match = nullptr; + for (auto const& entry : m_fallback_fonts) { + if (!entry_contains_glyph(entry)) + continue; + if (presentation_matches(*entry.font)) + return cache_and_return(*entry.font); + if (!fallback_glyph_match) + fallback_glyph_match = entry.font.ptr(); } if (m_system_font_fallback_callback) { - if (auto fallback = m_system_font_fallback_callback(code_point, first())) { - m_fonts.append({ fallback.release_nonnull(), {} }); - return cache_and_return(*m_fonts.last().font); + if (auto fallback = m_system_font_fallback_callback(code_point, emoji_presentation.presentation, first())) { + if (presentation_matches(*fallback) || (!author_glyph_match && !fallback_glyph_match)) { + m_fallback_fonts.append({ fallback.release_nonnull(), {} }); + return cache_and_return(*m_fallback_fonts.last().font); + } } } + if (author_glyph_match) + return cache_and_return(*author_glyph_match); + if (fallback_glyph_match) + return cache_and_return(*fallback_glyph_match); + return cache_and_return(*m_last_resort_font); } diff --git a/Libraries/LibGfx/FontCascadeList.h b/Libraries/LibGfx/FontCascadeList.h index 56542c72da..8afcbc1119 100644 --- a/Libraries/LibGfx/FontCascadeList.h +++ b/Libraries/LibGfx/FontCascadeList.h @@ -14,9 +14,26 @@ namespace Gfx { +enum class EmojiPresentation : u8 { + Text, + Emoji, +}; + +enum class ForcedPresentation : u8 { + No, + Yes, +}; + +struct EmojiPresentationResult { + EmojiPresentation presentation { EmojiPresentation::Text }; + ForcedPresentation forced { ForcedPresentation::No }; +}; + +EmojiPresentationResult emoji_presentation_for_code_point(u32 code_point, Optional next_code_point); + class FontCascadeList : public RefCounted { public: - using SystemFontFallbackCallback = Function(u32, Font const&)>; + using SystemFontFallbackCallback = Function(u32, EmojiPresentation, Font const&)>; static NonnullRefPtr create() { @@ -42,6 +59,8 @@ public: void extend(FontCascadeList const& other); + void extend_fallback(FontCascadeList const& other); + // A pending-face fetch should only be initiated for codepoints that are actually // being shaped into glyph runs. Callers that merely probe the cascade (e.g. the // U+0020 check in "first available font" metrics) pass No so that probing does @@ -50,7 +69,7 @@ public: No, Yes, }; - Gfx::Font const& font_for_code_point(u32 code_point, TriggerPendingLoads = TriggerPendingLoads::No) const; + Gfx::Font const& font_for_code_point(u32 code_point, TriggerPendingLoads = TriggerPendingLoads::No, EmojiPresentationResult = {}) const; bool equals(FontCascadeList const& other) const; @@ -99,6 +118,7 @@ public: private: RefPtr m_last_resort_font; mutable Vector m_fonts; + mutable Vector m_fallback_fonts; mutable Vector> m_pending_faces; SystemFontFallbackCallback m_system_font_fallback_callback; diff --git a/Libraries/LibWeb/CSS/FontComputer.cpp b/Libraries/LibWeb/CSS/FontComputer.cpp index 30633a0e45..835f25054f 100644 --- a/Libraries/LibWeb/CSS/FontComputer.cpp +++ b/Libraries/LibWeb/CSS/FontComputer.cpp @@ -655,26 +655,25 @@ NonnullRefPtr FontComputer::compute_font_for_style_v font_list->add(*default_font); } - // Add emoji and symbol fonts - for (auto font_name : Platform::FontPlugin::the().symbol_font_names()) { - if (auto other_font_list = find_font(font_name)) { - font_list->extend(*other_font_list); - } - } - // The default font is already included in the font list, but we explicitly set it // as the last-resort font. This ensures that if none of the specified fonts contain // the requested code point, there is still a font available to provide a fallback glyph. font_list->set_last_resort_font(*default_font); - if (!Platform::FontPlugin::the().is_layout_test_mode()) { - font_list->set_system_font_fallback_callback([](u32 code_point, Gfx::Font const& reference_font) -> RefPtr { + if (Platform::FontPlugin::the().is_layout_test_mode()) { + for (auto font_name : Platform::FontPlugin::the().symbol_font_names()) { + if (auto other_font_list = find_font(font_name)) + font_list->extend_fallback(*other_font_list); + } + } else { + font_list->set_system_font_fallback_callback([](u32 code_point, Gfx::EmojiPresentation presentation, Gfx::Font const& reference_font) -> RefPtr { return Gfx::FontDatabase::the().get_font_for_code_point( code_point, reference_font.point_size(), reference_font.weight(), reference_font.typeface().width(), - reference_font.slope()); + reference_font.slope(), + presentation == Gfx::EmojiPresentation::Emoji); }); } diff --git a/Libraries/LibWeb/Layout/TextNode.cpp b/Libraries/LibWeb/Layout/TextNode.cpp index c3e2736e19..b92ce02bfb 100644 --- a/Libraries/LibWeb/Layout/TextNode.cpp +++ b/Libraries/LibWeb/Layout/TextNode.cpp @@ -758,7 +758,7 @@ Gfx::Font const& TextNode::ChunkIterator::font_for_space(size_t at_index, u32 sp for (size_t i = at_index; i < m_view.length_in_code_units();) { auto cp = m_view.code_point_at(i); if (!is_interword_space(cp) && cp != '\t' && cp != '\n') { - auto const& font = m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes); + auto const& font = m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes, emoji_presentation_at(i, cp)); if (!font.is_emoji_font() && has_glyph(font)) return font; // Text is coming from an emoji face; we'll fall back to (3). @@ -771,6 +771,16 @@ Gfx::Font const& TextNode::ChunkIterator::font_for_space(size_t at_index, u32 sp return m_font_cascade_list.font_for_code_point(space_code_point, Gfx::FontCascadeList::TriggerPendingLoads::Yes); } +Gfx::EmojiPresentationResult TextNode::ChunkIterator::emoji_presentation_at(size_t code_unit_offset, u32 code_point) const +{ + auto next_offset = code_unit_offset + AK::UnicodeUtils::code_unit_length_for_code_point(code_point); + Optional next_code_point; + if (next_offset < m_view.length_in_code_units()) + next_code_point = m_view.code_point_at(next_offset); + + return Gfx::emoji_presentation_for_code_point(code_point, next_code_point); +} + Optional TextNode::ChunkIterator::next_without_peek() { if (m_current_index >= m_view.length_in_code_units()) @@ -795,7 +805,7 @@ Optional TextNode::ChunkIterator::next_without_peek() auto const& expected_font_for = [&](u32 cp) -> Gfx::Font const& { return is_interword_space(cp) ? font_for_space(m_current_index, cp) - : m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes); + : m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes, emoji_presentation_at(m_current_index, cp)); }; auto const& font = expected_font_for(current_code_point()); diff --git a/Libraries/LibWeb/Layout/TextNode.h b/Libraries/LibWeb/Layout/TextNode.h index 5aa9e07746..6ec17839a5 100644 --- a/Libraries/LibWeb/Layout/TextNode.h +++ b/Libraries/LibWeb/Layout/TextNode.h @@ -72,6 +72,7 @@ public: [[nodiscard]] bool is_at_line_break_opportunity() const; [[nodiscard]] Gfx::Font const& font_for_space(size_t at_index, u32 space_code_point) const; + [[nodiscard]] Gfx::EmojiPresentationResult emoji_presentation_at(size_t code_unit_offset, u32 code_point) const; bool const m_should_wrap_lines; bool const m_should_respect_linebreaks; diff --git a/Libraries/LibWeb/Platform/FontPlugin.cpp b/Libraries/LibWeb/Platform/FontPlugin.cpp index 84e8d1b4e1..b4de572089 100644 --- a/Libraries/LibWeb/Platform/FontPlugin.cpp +++ b/Libraries/LibWeb/Platform/FontPlugin.cpp @@ -52,15 +52,8 @@ FontPlugin::FontPlugin(bool is_layout_test_mode, Gfx::SystemFontProvider* font_p m_default_fixed_width_font = Gfx::FontDatabase::the().get(default_fixed_width_font_name, 12.0, 400, Gfx::FontWidth::Normal, 0); VERIFY(m_default_fixed_width_font); - if (is_layout_test_mode) { + if (is_layout_test_mode) m_symbol_font_names = { "Noto Emoji"_fly_string }; - } else { -#ifdef AK_OS_MACOS - m_symbol_font_names = { "Apple Color Emoji"_fly_string, "Apple Symbols"_fly_string }; -#else - m_symbol_font_names = { "Noto Color Emoji"_fly_string, "Noto Sans Symbols"_fly_string }; -#endif - } } FontPlugin::~FontPlugin() = default;