/* * Copyright (c) 2018-2025, Andreas Kling * Copyright (c) 2021, the SerenityOS developers. * Copyright (c) 2021-2025, Sam Atkins * Copyright (c) 2024, Matthew Olsson * Copyright (c) 2025, Callum Law * * SPDX-License-Identifier: BSD-2-Clause */ #include "FontComputer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::CSS { GC_DEFINE_ALLOCATOR(FontComputer); GC_DEFINE_ALLOCATOR(FontLoader); struct FontFaceKey { NonnullRawPtr family_name; int weight { 0 }; int slope { 0 }; }; } namespace AK { namespace Detail { template<> inline constexpr bool IsHashCompatible = true; template<> inline constexpr bool IsHashCompatible = true; } template<> struct Traits : public DefaultTraits { static unsigned hash(Web::CSS::FontFaceKey const& key) { return pair_int_hash(key.family_name->hash(), pair_int_hash(key.weight, key.slope)); } }; template<> struct Traits : public DefaultTraits { static unsigned hash(Web::CSS::OwnFontFaceKey const& key) { return pair_int_hash(key.family_name.hash(), pair_int_hash(key.weight, key.slope)); } }; template<> struct Traits : public DefaultTraits { static unsigned hash(Web::CSS::ComputedFontCacheKey const& key) { unsigned hash = 0; for (auto const& family_value : key.font_family->as_value_list().values()) { if (family_value->is_keyword()) hash = pair_int_hash(hash, to_underlying(family_value->as_keyword().keyword())); else if (family_value->is_string()) hash = pair_int_hash(hash, family_value->as_string().string_value().hash()); else if (family_value->is_custom_ident()) hash = pair_int_hash(hash, family_value->as_custom_ident().custom_ident().hash()); else VERIFY_NOT_REACHED(); } hash = pair_int_hash(hash, to_underlying(key.font_optical_sizing)); hash = pair_int_hash(hash, Traits::hash(key.font_size)); hash = pair_int_hash(hash, key.font_slope); hash = pair_int_hash(hash, Traits::hash(key.font_weight)); hash = pair_int_hash(hash, Traits::hash(key.font_width.value())); for (auto const& [variation_name, variation_value] : key.font_variation_settings) hash = pair_int_hash(hash, pair_int_hash(variation_name.hash(), Traits::hash(variation_value))); hash = pair_int_hash(hash, Traits::hash(key.font_feature_data)); return hash; } }; } namespace Web::CSS { OwnFontFaceKey::OwnFontFaceKey(FontFaceKey const& other) : family_name(other.family_name) , weight(other.weight) , slope(other.slope) { } OwnFontFaceKey::operator FontFaceKey() const { return FontFaceKey { family_name, weight, slope }; } [[nodiscard]] bool OwnFontFaceKey::operator==(FontFaceKey const& other) const { return family_name == other.family_name && weight == other.weight && slope == other.slope; } FontLoader::FontLoader(FontComputer& font_computer, RuleOrDeclaration rule_or_declaration, FlyString family_name, Vector unicode_ranges, Vector urls, GC::Ptr)>> on_load) : m_font_computer(font_computer) , m_rule_or_declaration(rule_or_declaration) , m_family_name(move(family_name)) , m_unicode_ranges(move(unicode_ranges)) , m_urls(move(urls)) , m_on_load(move(on_load)) { } FontLoader::~FontLoader() = default; void FontLoader::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_font_computer); if (auto* rule = m_rule_or_declaration.value.get_pointer()) visitor.visit(rule->parent_style_sheet); else if (auto* block = m_rule_or_declaration.value.get_pointer()) visitor.visit(block->parent_rule); visitor.visit(m_fetch_controller); visitor.visit(m_on_load); } bool FontLoader::is_loading() const { return m_fetch_controller && !m_typeface; } RefPtr FontLoader::font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations, Gfx::ShapeFeatures const& shape_features) { if (!m_typeface) { if (!m_fetch_controller) start_loading_next_url(); return nullptr; } return m_typeface->font(point_size, variations, shape_features); } void FontLoader::start_loading_next_url() { // FIXME: Load local() fonts somehow. if (m_fetch_controller && m_fetch_controller->state() == Fetch::Infrastructure::FetchController::State::Ongoing) return; if (m_urls.is_empty()) return; // https://drafts.csswg.org/css-fonts-4/#fetch-a-font // To fetch a font given a selected url for @font-face rule, fetch url, with ruleOrDeclaration being rule, // destination "font", CORS mode "cors", and processResponse being the following steps given response res and null, // failure or a byte stream stream: m_fetch_controller = fetch_a_style_resource(m_urls.take_first(), m_rule_or_declaration, Fetch::Infrastructure::Request::Destination::Font, CorsMode::Cors, [loader = this](auto response, auto stream) { // 1. If stream is null, return. // 2. Load a font from stream according to its type. // NB: We need to fetch the next source if this one fails to fetch OR decode. So, first try to decode it. RefPtr typeface; if (auto* bytes = stream.template get_pointer()) { if (auto maybe_typeface = loader->try_load_font(response, *bytes); !maybe_typeface.is_error()) typeface = maybe_typeface.release_value(); } if (!typeface) { // NB: If we have other sources available, try the next one. if (loader->m_urls.is_empty()) { loader->font_did_load_or_fail(nullptr); } else { loader->m_fetch_controller = nullptr; loader->start_loading_next_url(); } } else { loader->font_did_load_or_fail(move(typeface)); } }); if (!m_fetch_controller) font_did_load_or_fail(nullptr); } void FontLoader::font_did_load_or_fail(RefPtr typeface) { if (typeface) { m_typeface = typeface.release_nonnull(); m_font_computer->did_load_font(m_family_name); if (m_on_load) m_on_load->function()(m_typeface); } else { if (m_on_load) m_on_load->function()(nullptr); } m_fetch_controller = nullptr; } ErrorOr> FontLoader::try_load_font(Fetch::Infrastructure::Response const& response, ByteBuffer const& bytes) { // FIXME: This could maybe use the format() provided in @font-face as well, since often the mime type is just application/octet-stream and we have to try every format auto mime_type = Fetch::Infrastructure::extract_mime_type(response.header_list()); if (!mime_type.has_value() || !mime_type->is_font()) { mime_type = MimeSniff::Resource::sniff(bytes, MimeSniff::SniffingConfiguration { .sniffing_context = MimeSniff::SniffingContext::Font }); } if (mime_type.has_value()) { if (mime_type->essence() == "font/ttf"sv || mime_type->essence() == "application/x-font-ttf"sv || mime_type->essence() == "font/otf"sv) { if (auto result = Gfx::Typeface::try_load_from_temporary_memory(bytes); !result.is_error()) { return result; } } if (mime_type->essence() == "font/woff"sv || mime_type->essence() == "application/font-woff"sv) { if (auto result = WOFF::try_load_from_bytes(bytes); !result.is_error()) { return result; } } if (mime_type->essence() == "font/woff2"sv || mime_type->essence() == "application/font-woff2"sv) { if (auto result = WOFF2::try_load_from_bytes(bytes); !result.is_error()) { return result; } } } return Error::from_string_literal("Automatic format detection failed"); } struct FontComputer::MatchingFontCandidate { FontFaceKey key; Variant loader_or_typeface; [[nodiscard]] RefPtr font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations, FontFeatureData const& font_feature_data) const { auto const& shape_features = font_feature_data.to_shape_features(); auto font_list = Gfx::FontCascadeList::create(); if (auto const* loader_list = loader_or_typeface.get_pointer(); loader_list) { for (auto const& loader : **loader_list) { if (auto font = loader->font_with_point_size(point_size, variations, shape_features); font) font_list->add(*font, loader->unicode_ranges()); } return font_list; } font_list->add(loader_or_typeface.get()->font(point_size, variations, shape_features)); return font_list; } }; void FontComputer::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_document); visitor.visit(m_loaded_fonts); } RefPtr FontComputer::find_matching_font_weight_ascending(Vector const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, FontFeatureData const& font_feature_data, bool inclusive) { using Fn = AK::Function; auto pred = inclusive ? Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight >= target_weight; }) : Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight > target_weight; }); auto it = find_if(candidates.begin(), candidates.end(), pred); for (; it != candidates.end(); ++it) { if (auto found_font = it->font_with_point_size(font_size_in_pt, variations, font_feature_data)) return found_font; } return {}; } RefPtr FontComputer::find_matching_font_weight_descending(Vector const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, FontFeatureData const& font_feature_data, bool inclusive) { using Fn = AK::Function; auto pred = inclusive ? Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight <= target_weight; }) : Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight < target_weight; }); auto it = find_if(candidates.rbegin(), candidates.rend(), pred); for (; it != candidates.rend(); ++it) { if (auto found_font = it->font_with_point_size(font_size_in_pt, variations, font_feature_data)) return found_font; } return {}; } // Partial implementation of the font-matching algorithm: https://www.w3.org/TR/css-fonts-4/#font-matching-algorithm // FIXME: This should be replaced by the full CSS font selection algorithm. RefPtr FontComputer::font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt, Gfx::FontVariationSettings const& variations, FontFeatureData const& font_feature_data) const { // If a font family match occurs, the user agent assembles the set of font faces in that family and then // narrows the set to a single face using other font properties in the order given below. Vector matching_family_fonts; for (auto const& font_key_and_loader : m_loaded_fonts) { if (font_key_and_loader.key.family_name.equals_ignoring_ascii_case(family_name)) matching_family_fonts.empend(font_key_and_loader.key, const_cast(&font_key_and_loader.value)); } Gfx::FontDatabase::the().for_each_typeface_with_family_name(family_name, [&](Gfx::Typeface const& typeface) { matching_family_fonts.empend( FontFaceKey { .family_name = typeface.family(), .weight = static_cast(typeface.weight()), .slope = typeface.slope(), }, &typeface); }); if (matching_family_fonts.is_empty()) return {}; quick_sort(matching_family_fonts, [](auto const& a, auto const& b) { return a.key.weight < b.key.weight; }); // FIXME: 1. font-stretch is tried first. // FIXME: 2. font-style is tried next. // We don't have complete support of italic and oblique fonts, so matching on font-style can be simplified to: // If a matching slope is found, all faces which don't have that matching slope are excluded from the matching set. auto style_it = find_if(matching_family_fonts.begin(), matching_family_fonts.end(), [&](auto const& matching_font_candidate) { return matching_font_candidate.key.slope == slope; }); if (style_it != matching_family_fonts.end()) { matching_family_fonts.remove_all_matching([&](auto const& matching_font_candidate) { return matching_font_candidate.key.slope != slope; }); } // 3. font-weight is matched next. // If the desired weight is inclusively between 400 and 500, weights greater than or equal to the target weight // are checked in ascending order until 500 is hit and checked, followed by weights less than the target weight // in descending order, followed by weights greater than 500, until a match is found. if (weight >= 400 && weight <= 500) { auto it = find_if(matching_family_fonts.begin(), matching_family_fonts.end(), [&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight >= weight; }); for (; it != matching_family_fonts.end() && it->key.weight <= 500; ++it) { if (auto found_font = it->font_with_point_size(font_size_in_pt, variations, font_feature_data)) return found_font; } if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, font_feature_data, false)) return found_font; for (; it != matching_family_fonts.end(); ++it) { if (auto found_font = it->font_with_point_size(font_size_in_pt, variations, font_feature_data)) return found_font; } } // If the desired weight is less than 400, weights less than or equal to the desired weight are checked in descending order // followed by weights above the desired weight in ascending order until a match is found. if (weight < 400) { if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, font_feature_data, true)) return found_font; if (auto found_font = find_matching_font_weight_ascending(matching_family_fonts, weight, font_size_in_pt, variations, font_feature_data, false)) return found_font; } // If the desired weight is greater than 500, weights greater than or equal to the desired weight are checked in ascending order // followed by weights below the desired weight in descending order until a match is found. if (weight > 500) { if (auto found_font = find_matching_font_weight_ascending(matching_family_fonts, weight, font_size_in_pt, variations, font_feature_data, true)) return found_font; if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, font_feature_data, false)) return found_font; } VERIFY_NOT_REACHED(); } NonnullRefPtr FontComputer::compute_font_for_style_values(StyleValue const& font_family, CSSPixels const& font_size, int font_slope, double font_weight, Percentage const& font_width, FontOpticalSizing font_optical_sizing, HashMap const& font_variation_settings, FontFeatureData const& font_feature_data) const { ComputedFontCacheKey cache_key { .font_family = font_family, .font_optical_sizing = font_optical_sizing, .font_size = font_size, .font_slope = font_slope, .font_weight = font_weight, .font_width = font_width, .font_variation_settings = font_variation_settings, .font_feature_data = font_feature_data, }; return m_computed_font_cache.ensure(cache_key, [&]() { return compute_font_for_style_values_impl(font_family, font_size, font_slope, font_weight, font_width, font_optical_sizing, font_variation_settings, font_feature_data); }); } NonnullRefPtr FontComputer::compute_font_for_style_values_impl(StyleValue const& font_family, CSSPixels const& font_size, int slope, double font_weight, Percentage const& font_width, FontOpticalSizing font_optical_sizing, HashMap const& font_variation_settings, FontFeatureData const& font_feature_data) const { // FIXME: We round to int here as that is what is expected by our font infrastructure below auto weight = round_to(font_weight); // FIXME: We need to respect `font-size-adjust` once that is implemented. auto font_size_used_value = font_size.to_float(); Gfx::FontVariationSettings variation; variation.set_weight(font_weight); variation.set_width(font_width.value()); // NB: The spec recommends that we use the 'used value' of font-size for 'opsz' when font-optical-sizing is 'auto'. // FIXME: User agents must not select a value for the "opsz" axis which is not supported by the font used for // rendering the text. This can be accomplished by clamping a chosen value to the range supported by the // font. https://drafts.csswg.org/css-fonts/#font-optical-sizing-def if (font_optical_sizing == FontOpticalSizing::Auto) variation.set_optical_sizing(font_size_used_value); for (auto const& [tag_string, value] : font_variation_settings) { auto string_view = tag_string.bytes_as_string_view(); if (string_view.length() != 4) continue; auto tag = Gfx::FourCC(string_view.characters_without_null_termination()); variation.axes.set(tag, value); } // FIXME: Implement the full font-matching algorithm: https://www.w3.org/TR/css-fonts-4/#font-matching-algorithm float const font_size_in_pt = font_size_used_value * 0.75f; auto find_font = [&](FlyString const& family) -> RefPtr { // OPTIMIZATION: Look for an exact match in loaded fonts first. // FIXME: Respect the other font-* descriptors FontFaceKey key { .family_name = family, .weight = weight, .slope = slope, }; if (auto it = m_loaded_fonts.find(key); it != m_loaded_fonts.end()) { auto result = Gfx::FontCascadeList::create(); auto const& loaders = it->value; for (auto const& loader : loaders) { auto shape_features = font_feature_data.to_shape_features(); if (auto found_font = loader->font_with_point_size(font_size_in_pt, variation, shape_features)) result->add(*found_font, loader->unicode_ranges()); } return result; } if (auto found_font = font_matching_algorithm(family, weight, slope, font_size_in_pt, variation, font_feature_data); found_font && !found_font->is_empty()) return found_font; return {}; }; auto find_generic_font = [&](Keyword font_id) -> RefPtr { Platform::GenericFont generic_font {}; switch (font_id) { case Keyword::Monospace: case Keyword::UiMonospace: generic_font = Platform::GenericFont::Monospace; break; case Keyword::Serif: generic_font = Platform::GenericFont::Serif; break; case Keyword::Fantasy: generic_font = Platform::GenericFont::Fantasy; break; case Keyword::SansSerif: generic_font = Platform::GenericFont::SansSerif; break; case Keyword::Cursive: generic_font = Platform::GenericFont::Cursive; break; case Keyword::UiSerif: generic_font = Platform::GenericFont::UiSerif; break; case Keyword::UiSansSerif: generic_font = Platform::GenericFont::UiSansSerif; break; case Keyword::UiRounded: generic_font = Platform::GenericFont::UiRounded; break; default: return {}; } return find_font(Platform::FontPlugin::the().generic_font_name(generic_font)); }; auto font_list = Gfx::FontCascadeList::create(); for (auto const& family : font_family.as_value_list().values()) { RefPtr other_font_list; if (family->is_keyword()) { other_font_list = find_generic_font(family->to_keyword()); } else if (family->is_string()) { other_font_list = find_font(family->as_string().string_value()); } else if (family->is_custom_ident()) { other_font_list = find_font(family->as_custom_ident().custom_ident()); } if (other_font_list) font_list->extend(*other_font_list); } auto default_font = Platform::FontPlugin::the().default_font(font_size_in_pt, variation, font_feature_data.to_shape_features()); if (font_list->is_empty()) { // This is needed to make sure we check default font before reaching to emojis. 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 { 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()); }); } return font_list; } Gfx::Font const& FontComputer::initial_font() const { // FIXME: This is not correct. static auto font = ComputedProperties::font_fallback(false, false, 12); return font; } static bool style_value_references_font_family(StyleValue const& font_family_value, FlyString const& family_name) { if (!font_family_value.is_value_list()) return false; for (auto const& item : font_family_value.as_value_list().values()) { FlyString item_family_name; if (item->is_string()) item_family_name = item->as_string().string_value(); else if (item->is_custom_ident()) item_family_name = item->as_custom_ident().custom_ident(); else continue; // Skip generic keywords (sans-serif, serif, etc.) if (item_family_name.equals_ignoring_ascii_case(family_name)) return true; } return false; } void FontComputer::did_load_font(FlyString const& family_name) { // Only clear cache entries that reference the loaded font family. m_computed_font_cache.remove_all_matching([&](auto const& key, auto const&) { return style_value_references_font_family(key.font_family, family_name); }); auto element_uses_font_family = [&](DOM::Element const& element) { // Check the element's own font-family. if (auto style = element.computed_properties()) { if (style_value_references_font_family(style->property(PropertyID::FontFamily), family_name)) return true; } // Check pseudo-elements, which may use a different font-family than the element itself. for (size_t i = 0; i < to_underlying(PseudoElement::KnownPseudoElementCount); ++i) { if (auto style = element.computed_properties(static_cast(i))) { if (style_value_references_font_family(style->property(PropertyID::FontFamily), family_name)) return true; } } return false; }; // Walk the DOM tree (including shadow trees) and invalidate elements that use this font family. document().for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) { auto* element = as_if(node); if (!element) return TraversalDecision::Continue; // If this element's subtree is already marked for style update, skip the entire subtree. if (element->entire_subtree_needs_style_update()) return TraversalDecision::SkipChildrenAndContinue; // If this element already needs a style update, check descendants but don't re-check this element. if (element->needs_style_update()) return TraversalDecision::Continue; if (element_uses_font_family(*element)) { element->invalidate_style(DOM::StyleInvalidationReason::CSSFontLoaded); // invalidate_style() marks the entire subtree, so skip descendants. return TraversalDecision::SkipChildrenAndContinue; } return TraversalDecision::Continue; }); } GC::Ptr FontComputer::load_font_face(ParsedFontFace const& font_face, GC::Ptr)>> on_load) { if (font_face.sources().is_empty()) { if (on_load) on_load->function()({}); return {}; } FontFaceKey key { .family_name = font_face.font_family(), .weight = font_face.weight().value_or(0), .slope = font_face.slope().value_or(0), }; // FIXME: Pass the sources directly, so the font loader can make use of the format information, or load local fonts. Vector urls; for (auto const& source : font_face.sources()) { if (source.local_or_url.has()) urls.append(source.local_or_url.get()); // FIXME: Handle local() } if (urls.is_empty()) { if (on_load) on_load->function()({}); return {}; } RuleOrDeclaration rule_or_declaration { .environment_settings_object = document().relevant_settings_object(), .value = RuleOrDeclaration::Rule { .parent_style_sheet = font_face.parent_rule()->parent_style_sheet(), } }; auto loader = heap().allocate(*this, rule_or_declaration, font_face.font_family(), font_face.unicode_ranges(), move(urls), move(on_load)); auto& loader_ref = *loader; auto maybe_font_loaders_list = m_loaded_fonts.get(key); if (maybe_font_loaders_list.has_value()) { maybe_font_loaders_list->append(move(loader)); } else { FontLoaderList loaders; loaders.append(loader); m_loaded_fonts.set(OwnFontFaceKey(key), move(loaders)); } // Actual object owned by font loader list inside m_loaded_fonts, this isn't use-after-move/free return loader_ref; } void FontComputer::load_fonts_from_sheet(CSSStyleSheet& sheet) { for (auto const& rule : sheet.rules()) { auto* font_face_rule = as_if(*rule); if (!font_face_rule) continue; if (!font_face_rule->is_valid()) continue; if (auto font_loader = load_font_face(font_face_rule->font_face())) { sheet.add_associated_font_loader(*font_loader); } auto font_face = FontFace::create_css_connected(document().realm(), *font_face_rule); document().fonts()->add_css_connected_font(font_face); } } void FontComputer::unload_fonts_from_sheet(CSSStyleSheet& sheet) { for (auto& [_, font_loader_list] : m_loaded_fonts) { font_loader_list.remove_all_matching([&](auto& font_loader) { return sheet.has_associated_font_loader(*font_loader); }); } // https://drafts.csswg.org/css-font-loading/#font-face-css-connection // If a @font-face rule is removed from the document, its connected FontFace object is no longer CSS-connected. for (auto const& rule : sheet.rules()) { if (auto* font_face_rule = as_if(*rule)) font_face_rule->disconnect_font_face(); } } size_t FontComputer::number_of_css_font_faces_with_loading_in_progress() const { size_t count = 0; for (auto const& [_, loaders] : m_loaded_fonts) { for (auto const& loader : loaders) { if (loader->is_loading()) ++count; } } return count; } }