2025-11-06 18:27:22 +13:00
/*
* 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>
2026-02-11 07:33:58 +01:00
# include <LibWeb/CSS/CSSStyleSheet.h>
2025-11-06 18:27:22 +13:00
# include <LibWeb/CSS/ComputedProperties.h>
# include <LibWeb/CSS/Fetch.h>
2026-01-05 09:26:24 +00:00
# include <LibWeb/CSS/FontFace.h>
# include <LibWeb/CSS/FontFaceSet.h>
2025-11-06 18:27:22 +13:00
# 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>
2026-01-26 08:15:34 +01:00
# include <LibWeb/DOM/Element.h>
# include <LibWeb/DOM/ShadowRoot.h>
2025-11-06 18:27:22 +13:00
# 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 < >
2026-01-11 20:00:05 +13:00
struct Traits < Web : : CSS : : ComputedFontCacheKey > : public DefaultTraits < Web : : CSS : : ComputedFontCacheKey > {
static unsigned hash ( Web : : CSS : : ComputedFontCacheKey const & key )
2025-11-06 18:27:22 +13:00
{
2026-01-11 20:00:05 +13:00
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 ( ) ;
}
2026-01-29 11:42:34 +13:00
hash = pair_int_hash ( hash , to_underlying ( key . font_optical_sizing ) ) ;
2026-01-11 20:00:05 +13:00
hash = pair_int_hash ( hash , Traits < Web : : CSSPixels > : : hash ( key . font_size ) ) ;
hash = pair_int_hash ( hash , key . font_slope ) ;
hash = pair_int_hash ( hash , Traits < double > : : hash ( key . font_weight ) ) ;
hash = pair_int_hash ( hash , Traits < double > : : 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 < double > : : hash ( variation_value ) ) ) ;
2026-01-18 16:16:17 +13:00
hash = pair_int_hash ( hash , Traits < Web : : CSS : : FontFeatureData > : : hash ( key . font_feature_data ) ) ;
2026-01-11 20:00:05 +13:00
2025-11-06 18:27:22 +13:00
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 ;
}
2025-12-28 12:12:59 +01:00
FontLoader : : FontLoader ( FontComputer & font_computer , RuleOrDeclaration rule_or_declaration , FlyString family_name , Vector < Gfx : : UnicodeRange > unicode_ranges , Vector < URL > urls , GC : : Ptr < GC : : Function < void ( RefPtr < Gfx : : Typeface const > ) > > on_load )
2025-11-06 18:27:22 +13:00
: m_font_computer ( font_computer )
2025-12-28 12:12:59 +01:00
, m_rule_or_declaration ( rule_or_declaration )
2025-11-06 18:27:22 +13:00
, 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 ) ;
2025-12-28 12:12:59 +01:00
if ( auto * rule = m_rule_or_declaration . value . get_pointer < RuleOrDeclaration : : Rule > ( ) )
visitor . visit ( rule - > parent_style_sheet ) ;
else if ( auto * block = m_rule_or_declaration . value . get_pointer < RuleOrDeclaration : : StyleDeclaration > ( ) )
visitor . visit ( block - > parent_rule ) ;
2025-11-06 18:27:22 +13:00
visitor . visit ( m_fetch_controller ) ;
2025-12-26 07:04:32 +01:00
visitor . visit ( m_on_load ) ;
2025-11-06 18:27:22 +13:00
}
bool FontLoader : : is_loading ( ) const
{
2026-01-18 16:39:17 +13:00
return m_fetch_controller & & ! m_typeface ;
2025-11-06 18:27:22 +13:00
}
2026-01-18 16:16:17 +13:00
RefPtr < Gfx : : Font const > FontLoader : : font_with_point_size ( float point_size , Gfx : : FontVariationSettings const & variations , Gfx : : ShapeFeatures const & shape_features )
2025-11-06 18:27:22 +13:00
{
2026-01-18 16:39:17 +13:00
if ( ! m_typeface ) {
2025-11-06 18:27:22 +13:00
if ( ! m_fetch_controller )
start_loading_next_url ( ) ;
return nullptr ;
}
2026-01-18 16:16:17 +13:00
return m_typeface - > font ( point_size , variations , shape_features ) ;
2025-11-06 18:27:22 +13:00
}
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
2025-12-28 12:12:59 +01:00
// To fetch a font given a selected <url> 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 ,
2025-11-06 18:27:22 +13:00
[ 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 ) {
2026-01-18 16:39:17 +13:00
m_typeface = typeface . release_nonnull ( ) ;
2025-11-06 18:27:22 +13:00
m_font_computer - > did_load_font ( m_family_name ) ;
if ( m_on_load )
2026-01-18 16:39:17 +13:00
m_on_load - > function ( ) ( m_typeface ) ;
2025-11-06 18:27:22 +13:00
} else {
if ( m_on_load )
2025-12-26 07:04:32 +01:00
m_on_load - > function ( ) ( nullptr ) ;
2025-11-06 18:27:22 +13:00
}
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 ;
2026-01-18 16:16:17 +13:00
[[nodiscard]] RefPtr < Gfx : : FontCascadeList const > font_with_point_size ( float point_size , Gfx : : FontVariationSettings const & variations , FontFeatureData const & font_feature_data ) const
2025-11-06 18:27:22 +13:00
{
2026-01-18 16:16:17 +13:00
auto const & shape_features = font_feature_data . to_shape_features ( ) ;
2025-11-06 18:27:22 +13:00
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 ) {
2026-01-18 16:16:17 +13:00
if ( auto font = loader - > font_with_point_size ( point_size , variations , shape_features ) ; font )
2025-11-06 18:27:22 +13:00
font_list - > add ( * font , loader - > unicode_ranges ( ) ) ;
}
return font_list ;
}
2026-01-18 16:16:17 +13:00
font_list - > add ( loader_or_typeface . get < Gfx : : Typeface const * > ( ) - > font ( point_size , variations , shape_features ) ) ;
2025-11-06 18:27:22 +13:00
return font_list ;
}
} ;
void FontComputer : : visit_edges ( Visitor & visitor )
{
Base : : visit_edges ( visitor ) ;
visitor . visit ( m_document ) ;
visitor . visit ( m_loaded_fonts ) ;
}
2026-01-18 16:16:17 +13:00
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 , FontFeatureData const & font_feature_data , bool inclusive )
2025-11-06 18:27:22 +13:00
{
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 ) {
2026-01-18 16:16:17 +13:00
if ( auto found_font = it - > font_with_point_size ( font_size_in_pt , variations , font_feature_data ) )
2025-11-06 18:27:22 +13:00
return found_font ;
}
return { } ;
}
2026-01-18 16:16:17 +13:00
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 , FontFeatureData const & font_feature_data , bool inclusive )
2025-11-06 18:27:22 +13:00
{
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 ) {
2026-01-18 16:16:17 +13:00
if ( auto found_font = it - > font_with_point_size ( font_size_in_pt , variations , font_feature_data ) )
2025-11-06 18:27:22 +13:00
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.
2026-01-18 16:16:17 +13:00
RefPtr < Gfx : : FontCascadeList const > 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
2025-11-06 18:27:22 +13:00
{
// 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 ) ;
} ) ;
2026-01-18 14:41:07 +13:00
if ( matching_family_fonts . is_empty ( ) )
return { } ;
2025-11-06 18:27:22 +13:00
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 ) {
2026-01-18 16:16:17 +13:00
if ( auto found_font = it - > font_with_point_size ( font_size_in_pt , variations , font_feature_data ) )
2025-11-06 18:27:22 +13:00
return found_font ;
}
2026-01-18 16:16:17 +13:00
if ( auto found_font = find_matching_font_weight_descending ( matching_family_fonts , weight , font_size_in_pt , variations , font_feature_data , false ) )
2025-11-06 18:27:22 +13:00
return found_font ;
for ( ; it ! = matching_family_fonts . end ( ) ; + + it ) {
2026-01-18 16:16:17 +13:00
if ( auto found_font = it - > font_with_point_size ( font_size_in_pt , variations , font_feature_data ) )
2025-11-06 18:27:22 +13:00
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 ) {
2026-01-18 16:16:17 +13:00
if ( auto found_font = find_matching_font_weight_descending ( matching_family_fonts , weight , font_size_in_pt , variations , font_feature_data , true ) )
2025-11-06 18:27:22 +13:00
return found_font ;
2026-01-18 16:16:17 +13:00
if ( auto found_font = find_matching_font_weight_ascending ( matching_family_fonts , weight , font_size_in_pt , variations , font_feature_data , false ) )
2025-11-06 18:27:22 +13:00
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 ) {
2026-01-18 16:16:17 +13:00
if ( auto found_font = find_matching_font_weight_ascending ( matching_family_fonts , weight , font_size_in_pt , variations , font_feature_data , true ) )
2025-11-06 18:27:22 +13:00
return found_font ;
2026-01-18 16:16:17 +13:00
if ( auto found_font = find_matching_font_weight_descending ( matching_family_fonts , weight , font_size_in_pt , variations , font_feature_data , false ) )
2025-11-06 18:27:22 +13:00
return found_font ;
}
2026-01-18 14:41:07 +13:00
VERIFY_NOT_REACHED ( ) ;
2025-11-06 18:27:22 +13:00
}
2026-01-29 11:42:34 +13:00
NonnullRefPtr < Gfx : : FontCascadeList const > 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 < FlyString , double > const & font_variation_settings , FontFeatureData const & font_feature_data ) const
2026-01-11 20:00:05 +13:00
{
ComputedFontCacheKey cache_key {
. font_family = font_family ,
2026-01-29 11:42:34 +13:00
. font_optical_sizing = font_optical_sizing ,
2026-01-11 20:00:05 +13:00
. font_size = font_size ,
. font_slope = font_slope ,
. font_weight = font_weight ,
. font_width = font_width ,
. font_variation_settings = font_variation_settings ,
2026-01-18 16:16:17 +13:00
. font_feature_data = font_feature_data ,
2026-01-11 20:00:05 +13:00
} ;
return m_computed_font_cache . ensure ( cache_key , [ & ] ( ) {
2026-01-29 11:42:34 +13:00
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 ) ;
2026-01-11 20:00:05 +13:00
} ) ;
}
2026-01-29 11:42:34 +13:00
NonnullRefPtr < Gfx : : FontCascadeList const > 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 < FlyString , double > const & font_variation_settings , FontFeatureData const & font_feature_data ) const
2025-11-06 18:27:22 +13:00
{
// FIXME: We round to int here as that is what is expected by our font infrastructure below
auto weight = round_to < int > ( font_weight ) ;
2026-01-29 11:42:34 +13:00
// FIXME: We need to respect `font-size-adjust` once that is implemented.
auto font_size_used_value = font_size . to_float ( ) ;
2026-01-17 22:31:27 +13:00
Gfx : : FontVariationSettings variation ;
variation . set_weight ( font_weight ) ;
2026-01-18 14:17:39 +13:00
variation . set_width ( font_width . value ( ) ) ;
2026-01-17 22:31:27 +13:00
2026-01-29 11:42:34 +13:00
// 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 ) ;
2026-01-17 22:31:27 +13:00
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 ) ;
}
2025-11-06 18:27:22 +13:00
// FIXME: Implement the full font-matching algorithm: https://www.w3.org/TR/css-fonts-4/#font-matching-algorithm
2026-01-29 11:42:34 +13:00
float const font_size_in_pt = font_size_used_value * 0.75f ;
2025-11-06 18:27:22 +13:00
auto find_font = [ & ] ( FlyString const & family ) - > RefPtr < Gfx : : FontCascadeList const > {
2026-01-18 14:42:26 +13:00
// OPTIMIZATION: Look for an exact match in loaded fonts first.
// FIXME: Respect the other font-* descriptors
2025-11-06 18:27:22 +13:00
FontFaceKey key {
. family_name = family ,
. weight = weight ,
. slope = slope ,
} ;
if ( auto it = m_loaded_fonts . find ( key ) ; it ! = m_loaded_fonts . end ( ) ) {
2026-01-18 14:42:26 +13:00
auto result = Gfx : : FontCascadeList : : create ( ) ;
2025-11-06 18:27:22 +13:00
auto const & loaders = it - > value ;
for ( auto const & loader : loaders ) {
2026-01-18 16:16:17 +13:00
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 ) )
2025-11-06 18:27:22 +13:00
result - > add ( * found_font , loader - > unicode_ranges ( ) ) ;
}
return result ;
}
2026-01-18 16:16:17 +13:00
if ( auto found_font = font_matching_algorithm ( family , weight , slope , font_size_in_pt , variation , font_feature_data ) ; found_font & & ! found_font - > is_empty ( ) )
2025-11-06 18:27:22 +13:00
return found_font ;
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 ) ;
}
2026-01-18 16:16:17 +13:00
auto default_font = Platform : : FontPlugin : : the ( ) . default_font ( font_size_in_pt , variation , font_feature_data . to_shape_features ( ) ) ;
2025-11-06 18:27:22 +13:00
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 ) ;
2026-01-19 21:15:02 +00:00
if ( ! Platform : : FontPlugin : : the ( ) . is_layout_test_mode ( ) ) {
font_list - > set_system_font_fallback_callback ( [ ] ( u32 code_point , Gfx : : Font const & reference_font ) - > RefPtr < Gfx : : Font const > {
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 ( ) ) ;
} ) ;
}
2025-11-06 18:27:22 +13:00
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 ;
}
2026-01-26 08:15:34 +01:00
static bool style_value_references_font_family ( StyleValue const & font_family_value , FlyString const & family_name )
2025-11-06 18:27:22 +13:00
{
2026-01-26 08:15:34 +01:00
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 < PseudoElement > ( 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 < DOM : : Element > ( 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 ;
} ) ;
2025-11-06 18:27:22 +13:00
}
2025-12-26 07:04:32 +01:00
GC : : Ptr < FontLoader > FontComputer : : load_font_face ( ParsedFontFace const & font_face , GC : : Ptr < GC : : Function < void ( RefPtr < Gfx : : Typeface const > ) > > on_load )
2025-11-06 18:27:22 +13:00
{
if ( font_face . sources ( ) . is_empty ( ) ) {
if ( on_load )
2025-12-26 07:04:32 +01:00
on_load - > function ( ) ( { } ) ;
2025-11-06 18:27:22 +13:00
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 )
2025-12-26 07:04:32 +01:00
on_load - > function ( ) ( { } ) ;
2025-11-06 18:27:22 +13:00
return { } ;
}
2025-12-28 12:12:59 +01:00
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 < FontLoader > ( * this , rule_or_declaration , font_face . font_family ( ) , font_face . unicode_ranges ( ) , move ( urls ) , move ( on_load ) ) ;
2025-11-06 18:27:22 +13:00
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 ( ) ) {
2026-01-05 09:26:24 +00:00
auto * font_face_rule = as_if < CSSFontFaceRule > ( * rule ) ;
if ( ! font_face_rule )
2025-11-06 18:27:22 +13:00
continue ;
2026-01-05 09:26:24 +00:00
if ( ! font_face_rule - > is_valid ( ) )
2025-11-06 18:27:22 +13:00
continue ;
2026-01-05 09:26:24 +00:00
if ( auto font_loader = load_font_face ( font_face_rule - > font_face ( ) ) ) {
2025-11-06 18:27:22 +13:00
sheet . add_associated_font_loader ( * font_loader ) ;
}
2026-01-05 09:26:24 +00:00
auto font_face = FontFace : : create_css_connected ( document ( ) . realm ( ) , * font_face_rule ) ;
document ( ) . fonts ( ) - > add_css_connected_font ( font_face ) ;
2025-11-06 18:27:22 +13:00
}
}
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 ) ;
} ) ;
}
2026-01-05 10:08:52 +00:00
// 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 < CSSFontFaceRule > ( * rule ) )
font_face_rule - > disconnect_font_face ( ) ;
}
2025-11-06 18:27:22 +13:00
}
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 ;
}
}