/* * 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 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::FontMatchingAlgorithmCacheKey const& key) { auto hash = key.family_name.hash(); hash = pair_int_hash(hash, key.weight); hash = pair_int_hash(hash, key.slope); hash = pair_int_hash(hash, Traits::hash(key.font_size_in_pt)); 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, GC::Ptr parent_style_sheet, FlyString family_name, Vector unicode_ranges, Vector urls, Function)> on_load) : m_font_computer(font_computer) , m_parent_style_sheet(parent_style_sheet) , 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); visitor.visit(m_parent_style_sheet); visitor.visit(m_fetch_controller); } bool FontLoader::is_loading() const { return m_fetch_controller && !m_vector_font; } RefPtr FontLoader::font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations) { if (!m_vector_font) { if (!m_fetch_controller) start_loading_next_url(); return nullptr; } return m_vector_font->font(point_size, variations); } 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 stylesheet being rule’s parent // CSS style sheet, destination "font", CORS mode "cors", and processResponse being the following steps given // response res and null, failure or a byte stream stream: auto style_sheet_or_document = m_parent_style_sheet ? StyleSheetOrDocument { *m_parent_style_sheet } : StyleSheetOrDocument { m_font_computer->document() }; m_fetch_controller = fetch_a_style_resource(m_urls.take_first(), style_sheet_or_document, 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_vector_font = typeface.release_nonnull(); m_font_computer->did_load_font(m_family_name); if (m_on_load) m_on_load(m_vector_font); } else { if (m_on_load) m_on_load(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) const { 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); font) font_list->add(*font, loader->unicode_ranges()); } return font_list; } font_list->add(loader_or_typeface.get()->font(point_size, variations)); 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, 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)) 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, 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)) return found_font; } return {}; } RefPtr FontComputer::font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const { FontMatchingAlgorithmCacheKey key { family_name, weight, slope, font_size_in_pt }; return m_font_matching_algorithm_cache.ensure(key, [&] { return font_matching_algorithm_impl(family_name, weight, slope, font_size_in_pt); }); } // 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_impl(FlyString const& family_name, int weight, int slope, float font_size_in_pt) 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); }); 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. Gfx::FontVariationSettings variations; variations.set_weight(weight); 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)) return found_font; } if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, 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)) 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, true)) return found_font; if (auto found_font = find_matching_font_weight_ascending(matching_family_fonts, weight, font_size_in_pt, variations, 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, true)) return found_font; if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, false)) return found_font; } return {}; } NonnullRefPtr FontComputer::compute_font_for_style_values(StyleValue const& font_family, CSSPixels const& font_size, int slope, double font_weight, Percentage const& font_width, HashMap const& font_variation_settings) const { // FIXME: We round to int here as that is what is expected by our font infrastructure below auto width = round_to(font_width.value()); // FIXME: We round to int here as that is what is expected by our font infrastructure below auto weight = round_to(font_weight); // 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 * 0.75f; auto find_font = [&](FlyString const& family) -> RefPtr { FontFaceKey key { .family_name = family, .weight = weight, .slope = slope, }; auto result = Gfx::FontCascadeList::create(); if (auto it = m_loaded_fonts.find(key); it != m_loaded_fonts.end()) { auto const& loaders = it->value; Gfx::FontVariationSettings variation; variation.set_weight(font_weight); 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); } for (auto const& loader : loaders) { if (auto found_font = loader->font_with_point_size(font_size_in_pt, variation)) result->add(*found_font, loader->unicode_ranges()); } return result; } if (auto found_font = font_matching_algorithm(family, weight, slope, font_size_in_pt); found_font && !found_font->is_empty()) { return found_font; } if (auto found_font = Gfx::FontDatabase::the().get(family, font_size_in_pt, weight, width, slope)) { result->add(*found_font); return result; } 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); 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); 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; } void FontComputer::did_load_font(FlyString const&) { m_font_matching_algorithm_cache = {}; document().invalidate_style(DOM::StyleInvalidationReason::CSSFontLoaded); } GC::Ptr FontComputer::load_font_face(ParsedFontFace const& font_face, Function)> on_load) { if (font_face.sources().is_empty()) { if (on_load) on_load({}); 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({}); return {}; } auto loader = heap().allocate(*this, font_face.parent_style_sheet(), 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()) { if (!is(*rule)) continue; auto const& font_face_rule = static_cast(*rule); 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); } } } 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); }); } } 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; } }