ladybird/Libraries/LibWeb/CSS/FontComputer.cpp

574 lines
23 KiB
C++
Raw Normal View History

/*
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, the SerenityOS developers.
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "FontComputer.h"
#include <AK/NonnullRawPtr.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Font/WOFF/Loader.h>
#include <LibGfx/Font/WOFF2/Loader.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Fetch.h>
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/MIME.h>
#include <LibWeb/Fetch/Response.h>
#include <LibWeb/MimeSniff/Resource.h>
#include <LibWeb/Platform/FontPlugin.h>
namespace Web::CSS {
GC_DEFINE_ALLOCATOR(FontComputer);
GC_DEFINE_ALLOCATOR(FontLoader);
struct FontFaceKey {
NonnullRawPtr<FlyString const> family_name;
int weight { 0 };
int slope { 0 };
};
}
namespace AK {
namespace Detail {
template<>
inline constexpr bool IsHashCompatible<Web::CSS::FontFaceKey, Web::CSS::OwnFontFaceKey> = true;
template<>
inline constexpr bool IsHashCompatible<Web::CSS::OwnFontFaceKey, Web::CSS::FontFaceKey> = true;
}
template<>
struct Traits<Web::CSS::FontFaceKey> : public DefaultTraits<Web::CSS::FontFaceKey> {
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<Web::CSS::OwnFontFaceKey> : public DefaultTraits<Web::CSS::OwnFontFaceKey> {
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<Web::CSS::FontMatchingAlgorithmCacheKey> : public DefaultTraits<Web::CSS::FontMatchingAlgorithmCacheKey> {
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<float>::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<CSSStyleSheet> parent_style_sheet, FlyString family_name, Vector<Gfx::UnicodeRange> unicode_ranges, Vector<URL> urls, Function<void(RefPtr<Gfx::Typeface const>)> 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<Gfx::Font const> 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> 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<Gfx::Typeface const> typeface;
if (auto* bytes = stream.template get_pointer<ByteBuffer>()) {
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<Gfx::Typeface const> 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<NonnullRefPtr<Gfx::Typeface const>> 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<FontLoaderList*, Gfx::Typeface const*> loader_or_typeface;
[[nodiscard]] RefPtr<Gfx::FontCascadeList const> 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<FontLoaderList*>(); 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<Gfx::Typeface const*>()->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<Gfx::FontCascadeList const> FontComputer::find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive)
{
using Fn = AK::Function<bool(MatchingFontCandidate const&)>;
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<Gfx::FontCascadeList const> FontComputer::find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive)
{
using Fn = AK::Function<bool(MatchingFontCandidate const&)>;
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<Gfx::FontCascadeList const> 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<Gfx::FontCascadeList const> 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<MatchingFontCandidate> 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<FontLoaderList*>(&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<int>(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 {};
}
RefPtr<Gfx::FontCascadeList const> FontComputer::compute_font_for_style_values(StyleValue const& font_family, CSSPixels const& font_size, int slope, double font_weight, Percentage const& font_width, HashMap<FlyString, double> 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<int>(font_width.value());
// FIXME: We round to int here as that is what is expected by our font infrastructure below
auto weight = round_to<int>(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<Gfx::FontCascadeList const> {
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<Gfx::FontCascadeList const> {
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<Gfx::FontCascadeList const> 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<FontLoader> FontComputer::load_font_face(ParsedFontFace const& font_face, Function<void(RefPtr<Gfx::Typeface const>)> 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<URL> urls;
for (auto const& source : font_face.sources()) {
if (source.local_or_url.has<URL>())
urls.append(source.local_or_url.get<URL>());
// FIXME: Handle local()
}
if (urls.is_empty()) {
if (on_load)
on_load({});
return {};
}
auto loader = heap().allocate<FontLoader>(*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<CSSFontFaceRule>(*rule))
continue;
auto const& font_face_rule = static_cast<CSSFontFaceRule const&>(*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;
}
}