From 8c49029bb76a9aee9e5d1aa659d789c00a36f972 Mon Sep 17 00:00:00 2001 From: Johan Dahlin Date: Wed, 22 Apr 2026 09:17:48 +0200 Subject: [PATCH] LibGfx+LibWeb: Only trigger @font-face loads from text shaping --- Libraries/LibGfx/FontCascadeList.cpp | 33 +++++++++++-------- Libraries/LibGfx/FontCascadeList.h | 10 +++++- Libraries/LibGfx/TextLayout.cpp | 4 +-- Libraries/LibWeb/Layout/TextNode.cpp | 6 ++-- .../font-face-probe-does-not-trigger-load.txt | 2 +- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/Libraries/LibGfx/FontCascadeList.cpp b/Libraries/LibGfx/FontCascadeList.cpp index 8b812c32f48..af18f8e7b55 100644 --- a/Libraries/LibGfx/FontCascadeList.cpp +++ b/Libraries/LibGfx/FontCascadeList.cpp @@ -58,20 +58,27 @@ 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) const +Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point, TriggerPendingLoads trigger_pending_loads) const { - // Walk pending entries first: if this codepoint falls in an unloaded face's - // unicode-range we kick off the fetch and drop the entry — a fallback font that - // happens to cover the codepoint shouldn't prevent the real face from loading. - // FontComputer::clear_computed_font_cache() rebuilds the cascade once the fetch - // completes, so later shapes pick up the loaded face. Run before the ASCII cache - // lookup so a previously-cached codepoint still triggers a newly-added face. - m_pending_faces.remove_all_matching([code_point](auto const& pending) { - if (!pending->covers(code_point)) - return false; - pending->start_load(); - return true; - }); + // 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- + // font metrics, for instance) skip this block so they can't initiate a download + // for a subset face that happens to cover the probe codepoint. + if (trigger_pending_loads == TriggerPendingLoads::Yes) { + // Walk pending entries first: if this codepoint falls in an unloaded face's + // unicode-range we kick off the fetch and drop the entry — a fallback font + // that happens to cover the codepoint shouldn't prevent the real face from + // loading. FontComputer::clear_computed_font_cache() rebuilds the cascade + // once the fetch completes, so later shapes pick up the loaded face. Run + // before the ASCII cache lookup so a previously-cached codepoint still + // triggers a newly-added face. + m_pending_faces.remove_all_matching([code_point](auto const& pending) { + if (!pending->covers(code_point)) + return false; + pending->start_load(); + return true; + }); + } if (code_point < m_ascii_cache.size()) { if (auto const* cached = m_ascii_cache[code_point]) diff --git a/Libraries/LibGfx/FontCascadeList.h b/Libraries/LibGfx/FontCascadeList.h index 82ae3597ddb..60b618eaa1d 100644 --- a/Libraries/LibGfx/FontCascadeList.h +++ b/Libraries/LibGfx/FontCascadeList.h @@ -43,7 +43,15 @@ public: void extend(FontCascadeList const& other); - Gfx::Font const& font_for_code_point(u32 code_point) const; + // 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 + // not kick off downloads for subset faces that happen to cover the probe point. + enum class TriggerPendingLoads : u8 { + No, + Yes, + }; + Gfx::Font const& font_for_code_point(u32 code_point, TriggerPendingLoads = TriggerPendingLoads::No) const; bool equals(FontCascadeList const& other) const; diff --git a/Libraries/LibGfx/TextLayout.cpp b/Libraries/LibGfx/TextLayout.cpp index 2931189067e..91249194a34 100644 --- a/Libraries/LibGfx/TextLayout.cpp +++ b/Libraries/LibGfx/TextLayout.cpp @@ -121,7 +121,7 @@ Vector> shape_text(FloatPoint baseline_start, Utf16View auto it = string.begin(); auto substring_begin_offset = string.iterator_offset(it); - Font const* last_font = &font_cascade_list.font_for_code_point(*it); + Font const* last_font = &font_cascade_list.font_for_code_point(*it, FontCascadeList::TriggerPendingLoads::Yes); FloatPoint last_position = baseline_start; auto add_run = [&runs, &last_position, letter_spacing](Utf16View const& string, Font const& font) { @@ -132,7 +132,7 @@ Vector> shape_text(FloatPoint baseline_start, Utf16View while (it != string.end()) { auto code_point = *it; - auto const* font = &font_cascade_list.font_for_code_point(code_point); + auto const* font = &font_cascade_list.font_for_code_point(code_point, FontCascadeList::TriggerPendingLoads::Yes); if (font != last_font) { auto substring = string.substring_view(substring_begin_offset, string.iterator_offset(it) - substring_begin_offset); add_run(substring, *last_font); diff --git a/Libraries/LibWeb/Layout/TextNode.cpp b/Libraries/LibWeb/Layout/TextNode.cpp index 4502abecba4..73345b7969e 100644 --- a/Libraries/LibWeb/Layout/TextNode.cpp +++ b/Libraries/LibWeb/Layout/TextNode.cpp @@ -643,7 +643,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); + auto const& font = m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes); if (!font.is_emoji_font() && has_glyph(font)) return font; // Text is coming from an emoji face; we'll fall back to (3). @@ -653,7 +653,7 @@ Gfx::Font const& TextNode::ChunkIterator::font_for_space(size_t at_index, u32 sp } // 3. No text around (leading/trailing/all spaces) — pick a font with the glyph from the cascade. - return m_font_cascade_list.font_for_code_point(space_code_point); + return m_font_cascade_list.font_for_code_point(space_code_point, Gfx::FontCascadeList::TriggerPendingLoads::Yes); } Optional TextNode::ChunkIterator::next_without_peek() @@ -680,7 +680,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); + : m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes); }; auto const& font = expected_font_for(current_code_point()); diff --git a/Tests/LibWeb/Text/expected/css/font-face-probe-does-not-trigger-load.txt b/Tests/LibWeb/Text/expected/css/font-face-probe-does-not-trigger-load.txt index 859cfa040f5..d48a290dc7c 100644 --- a/Tests/LibWeb/Text/expected/css/font-face-probe-does-not-trigger-load.txt +++ b/Tests/LibWeb/Text/expected/css/font-face-probe-does-not-trigger-load.txt @@ -1 +1 @@ -ProbeFont status: error +ProbeFont status: unloaded