diff --git a/AK/Debug.h.in b/AK/Debug.h.in index ddb495e39fa..25c411b1f15 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -78,6 +78,10 @@ # cmakedefine01 FLAC_ENCODER_DEBUG #endif +#ifndef FONTCONFIG_DEBUG +# cmakedefine01 FONTCONFIG_DEBUG +#endif + #ifndef GENERATE_DEBUG # cmakedefine01 GENERATE_DEBUG #endif diff --git a/Libraries/LibGfx/CMakeLists.txt b/Libraries/LibGfx/CMakeLists.txt index 5a2ff475dd4..bccbcfe3307 100644 --- a/Libraries/LibGfx/CMakeLists.txt +++ b/Libraries/LibGfx/CMakeLists.txt @@ -1,5 +1,6 @@ include(skia) include(vulkan) +include(fontconfig) set(SOURCES AffineTransform.cpp @@ -125,6 +126,11 @@ else() target_link_libraries(LibGfx PRIVATE libjxl::libjxl hwy::hwy) endif() +if (HAS_FONTCONFIG) + target_sources(LibGfx PRIVATE Font/FontconfigFontProvider.cpp) + target_link_libraries(LibGfx PRIVATE Fontconfig::Fontconfig) +endif() + if (ENABLE_SWIFT) generate_clang_module_map(LibGfx GENERATED_FILES ${generated_headers} EXCLUDE_FILES ${SWIFT_EXCLUDE_HEADERS}) target_sources(LibGfx PRIVATE diff --git a/Libraries/LibGfx/Font/Font.h b/Libraries/LibGfx/Font/Font.h index 12155efe219..5acfa08a6fc 100644 --- a/Libraries/LibGfx/Font/Font.h +++ b/Libraries/LibGfx/Font/Font.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -46,10 +47,27 @@ enum FontWidth { UltraExpanded = 9 }; +enum class FontSlant : u8 { + Upright, + Italic, + Oblique +}; + constexpr float text_shaping_resolution = 64; class Typeface; +struct FontDescription { + FlyString family; + + // These fields correspond to SkFontStyle + u16 weight { 400 }; // Normal + FontWidth width { Normal }; + FontSlant slant { FontSlant::Upright }; + + Function()> load_typeface; +}; + class Font : public RefCounted { public: virtual ~Font(); diff --git a/Libraries/LibGfx/Font/FontDatabase.cpp b/Libraries/LibGfx/Font/FontDatabase.cpp index 9317b7eabc3..3d30d07f362 100644 --- a/Libraries/LibGfx/Font/FontDatabase.cpp +++ b/Libraries/LibGfx/Font/FontDatabase.cpp @@ -39,7 +39,7 @@ RefPtr FontDatabase::get(FlyString const& family, float point_size, u return m_system_font_provider->get_font(family, point_size, weight, width, slope); } -void FontDatabase::for_each_typeface_with_family_name(FlyString const& family_name, Function callback) +void FontDatabase::for_each_typeface_with_family_name(FlyString const& family_name, Function callback) { m_system_font_provider->for_each_typeface_with_family_name(family_name, move(callback)); } diff --git a/Libraries/LibGfx/Font/FontDatabase.h b/Libraries/LibGfx/Font/FontDatabase.h index 2787a2a1a9a..90bf86c3a45 100644 --- a/Libraries/LibGfx/Font/FontDatabase.h +++ b/Libraries/LibGfx/Font/FontDatabase.h @@ -20,7 +20,7 @@ public: virtual StringView name() const = 0; virtual RefPtr get_font(FlyString const& family, float point_size, unsigned weight, unsigned width, unsigned slope) = 0; - virtual void for_each_typeface_with_family_name(FlyString const& family_name, Function) = 0; + virtual void for_each_typeface_with_family_name(FlyString const& family_name, Function) = 0; }; class FontDatabase { @@ -29,7 +29,7 @@ public: SystemFontProvider& install_system_font_provider(NonnullOwnPtr); RefPtr get(FlyString const& family, float point_size, unsigned weight, unsigned width, unsigned slope); - void for_each_typeface_with_family_name(FlyString const& family_name, Function); + void for_each_typeface_with_family_name(FlyString const& family_name, Function); [[nodiscard]] StringView system_font_provider_name() const; private: diff --git a/Libraries/LibGfx/Font/FontconfigFontProvider.cpp b/Libraries/LibGfx/Font/FontconfigFontProvider.cpp new file mode 100644 index 00000000000..90442981385 --- /dev/null +++ b/Libraries/LibGfx/Font/FontconfigFontProvider.cpp @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#ifndef USE_FONTCONFIG +# error "FontconfigFontProvider requires USE_FONTCONFIG to be enabled" +#endif + +#include +#include +#include +#include +#include + +#include + +namespace Gfx { + +FontconfigFontProvider::FontconfigFontProvider() +{ + [[maybe_unused]] auto fontconfig_initialized = FcInit(); + VERIFY(fontconfig_initialized); +} + +FontconfigFontProvider::~FontconfigFontProvider() = default; + +void FontconfigFontProvider::add_uri_to_config(StringView uri) +{ + auto* config = FcConfigGetCurrent(); + VERIFY(config); + + auto path = MUST(Core::Resource::load_from_uri(uri)); + VERIFY(path->is_directory()); + ByteString const fs_path = path->filesystem_path().to_byte_string(); + + bool const success = FcConfigAppFontAddDir(config, reinterpret_cast(fs_path.characters())); + VERIFY(success); +} + +RefPtr FontconfigFontProvider::load_typeface_from_path(ByteString const& path, int index) +{ + dbgln_if(FONTCONFIG_DEBUG, "FontconfigFontProvider: Loading font {} from {}", index, path); + + auto resource = Core::Resource::load_from_filesystem(path); + if (resource.is_error()) + return nullptr; + auto typeface_or_error = Typeface::try_load_from_resource(resource.release_value(), index); + if (typeface_or_error.is_error()) { + typeface_or_error = WOFF::try_load_from_resource(resource.release_value(), index); + if (typeface_or_error.is_error()) + return nullptr; + } + auto typeface = typeface_or_error.release_value(); + + auto& family_list = m_typeface_by_family.ensure(typeface->family(), [] { + return Vector> {}; + }); + family_list.append(typeface); + return typeface; +} + +RefPtr FontconfigFontProvider::get_font(FlyString const& family, float point_size, unsigned int weight, unsigned int width, unsigned int slope) +{ + if (auto typefaces = m_typeface_by_family.get(family); typefaces.has_value()) { + for (auto& typeface : *typefaces) { + if (typeface->weight() == weight && typeface->width() == width && typeface->slope() == slope) + return typeface->scaled_font(point_size); + } + } + + // FIXME: We should be able to avoid this allocation with a null-terminated flystring + ByteString const nullterm_family = family.bytes_as_string_view(); + + auto* config = FcConfigGetCurrent(); + VERIFY(config); + + auto* pattern = FcPatternBuild(nullptr, FC_FAMILY, FcTypeString, reinterpret_cast(nullterm_family.characters()), nullptr); + VERIFY(pattern); + ScopeGuard const pattern_guard { [pattern] { FcPatternDestroy(pattern); } }; + + auto success = FcConfigSubstitute(config, pattern, FcMatchPattern); + VERIFY(success); + + FcDefaultSubstitute(pattern); + + FcResult result {}; + auto* matched_pattern = FcFontMatch(config, pattern, &result); + if (result != FcResultMatch || !matched_pattern) + return nullptr; + ScopeGuard const matched_pattern_guard { [matched_pattern] { FcPatternDestroy(matched_pattern); } }; + + FcChar8* file = nullptr; + if (FcPatternGetString(matched_pattern, FC_FILE, 0, &file) != FcResultMatch) + return nullptr; + auto filename = ByteString { reinterpret_cast(file) }; + + int index = 0; + if (FcPatternGetInteger(matched_pattern, FC_INDEX, 0, &index) != FcResultMatch) + return nullptr; + + auto typeface = load_typeface_from_path(filename, index); + return typeface ? typeface->scaled_font(point_size) : nullptr; +} + +Optional FontconfigFontProvider::description_for_fontconfig_parameters(FlyString const& family, ByteString const& path, int index, int weight, int width, int slant) +{ + // FIXME: Do better validation and normalization of fontconfig parameters + if (weight < 0 || weight > 1000) { + dbgln_if(FONTCONFIG_DEBUG, "FontconfigFontProvider: Invalid weight {} for font {} in {}@{}", weight, family, path, index); + return {}; + } + if (width < 0 || width > 9) { + dbgln_if(FONTCONFIG_DEBUG, "FontconfigFontProvider: Invalid width {} for font {} in {}@{}", width, family, path, index); + return {}; + } + FontSlant normalized_slant = FontSlant::Upright; + if (slant == FC_SLANT_ITALIC) + normalized_slant = FontSlant::Italic; + else if (slant == FC_SLANT_OBLIQUE) + normalized_slant = FontSlant::Oblique; + else if (slant != FC_SLANT_ROMAN) { + dbgln_if(FONTCONFIG_DEBUG, "FontconfigFontProvider: Invalid slant {} for font {} in {}@{}", slant, family, path, index); + return {}; + } + + return FontDescription { + .family = family, + .weight = static_cast(weight), + .width = static_cast(width), + .slant = normalized_slant, + .load_typeface = [this, family, path, index, weight, width, slant]() -> RefPtr { + // FIXME: Use more normalized values here to check cache + if (auto typefaces = m_typeface_by_family.get(family); typefaces.has_value()) { + for (auto& typeface : *typefaces) { + if (typeface->weight() == weight && typeface->width() == width && typeface->slope() == slant) + return typeface; + } + } + return load_typeface_from_path(path, index); + }, + }; +} + +void FontconfigFontProvider::for_each_typeface_with_family_name(FlyString const& family_name, Function callback) +{ + auto* config = FcConfigGetCurrent(); + VERIFY(config); + + auto* set = FcConfigGetFonts(config, FcSetSystem); + VERIFY(set); + + // FIXME: We should be able to avoid this allocation with a null-terminated flystring + ByteString const nullterm_family = family_name.bytes_as_string_view(); + + auto* pattern = FcPatternBuild(nullptr, FC_FAMILY, FcTypeString, reinterpret_cast(nullterm_family.characters()), nullptr); + VERIFY(pattern); + auto pattern_guard = ScopeGuard { [pattern] { FcPatternDestroy(pattern); } }; + + auto* object_set = FcObjectSetBuild(FC_FAMILY, FC_WEIGHT, FC_WIDTH, FC_SLANT, FC_FILE, FC_INDEX, nullptr); + VERIFY(object_set); + auto object_set_guard = ScopeGuard { [object_set] { FcObjectSetDestroy(object_set); } }; + + auto* matches = FcFontSetList(config, &set, 1, pattern, object_set); + if (!matches) + return; + ScopeGuard const matches_guard { [matches] { FcFontSetDestroy(matches); } }; + + FcResult result {}; + for (auto idx = 0; idx < matches->nfont; ++idx) { + auto* current_pattern = matches->fonts[idx]; + + FcChar8* path = nullptr; + result = FcPatternGetString(current_pattern, FC_FILE, 0, &path); + VERIFY(result == FcResultMatch); + auto pattern_path = ByteString { reinterpret_cast(path) }; + + int pattern_index = 0; + result = FcPatternGetInteger(current_pattern, FC_INDEX, 0, &pattern_index); + VERIFY(result == FcResultMatch); + + FcChar8* family = nullptr; + result = FcPatternGetString(current_pattern, FC_FAMILY, 0, &family); + VERIFY(result == FcResultMatch); + StringView const family_view = StringView { reinterpret_cast(family), strlen(reinterpret_cast(family)) }; + auto pattern_family_or_error = FlyString::from_utf8(family_view); + if (pattern_family_or_error.is_error()) { + dbgln("FontconfigFontProvider: Failed to read UTF-8 family name for font {} in {}", pattern_index, pattern_path); + continue; + } + auto pattern_family = pattern_family_or_error.release_value(); + + int weight = 0; + result = FcPatternGetInteger(current_pattern, FC_WEIGHT, 0, &weight); + if (result != FcResultMatch) { + dbgln_if(FONTCONFIG_DEBUG, "FontconfigFontProvider: Failed to read weight for font {} in {}@{}", pattern_family, pattern_path, pattern_index); + continue; + } + + int width = 0; + result = FcPatternGetInteger(current_pattern, FC_WIDTH, 0, &width); + if (result != FcResultMatch) { + dbgln_if(FONTCONFIG_DEBUG, "FontconfigFontProvider: Failed to read width for font {} in {}@{}", pattern_family, pattern_path, pattern_index); + continue; + } + + int slant = 0; + result = FcPatternGetInteger(current_pattern, FC_SLANT, 0, &slant); + if (result != FcResultMatch) { + dbgln_if(FONTCONFIG_DEBUG, "FontconfigFontProvider: Failed to read slant for font {} in {}@{}", pattern_family, pattern_path, pattern_index); + continue; + } + + if (auto descriptor = description_for_fontconfig_parameters(pattern_family, pattern_path, pattern_index, weight, width, slant); descriptor.has_value()) { + callback(descriptor.release_value()); + } + } +} + +} diff --git a/Libraries/LibGfx/Font/FontconfigFontProvider.h b/Libraries/LibGfx/Font/FontconfigFontProvider.h new file mode 100644 index 00000000000..02c4409d1cb --- /dev/null +++ b/Libraries/LibGfx/Font/FontconfigFontProvider.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Gfx { + +class FontconfigFontProvider final : public SystemFontProvider { + AK_MAKE_NONCOPYABLE(FontconfigFontProvider); + AK_MAKE_NONMOVABLE(FontconfigFontProvider); + +public: + FontconfigFontProvider(); + virtual ~FontconfigFontProvider() override; + + virtual RefPtr get_font(FlyString const& family, float point_size, unsigned weight, unsigned width, unsigned slope) override; + virtual void for_each_typeface_with_family_name(FlyString const& family_name, Function) override; + virtual StringView name() const override { return "FontConfig"sv; } + + void add_uri_to_config(StringView); + +private: + Optional description_for_fontconfig_parameters(FlyString const& family, ByteString const& path, int index, int weight, int width, int slant); + RefPtr load_typeface_from_path(ByteString const& path, int index); + + HashMap>, AK::ASCIICaseInsensitiveFlyStringTraits> m_typeface_by_family; +}; + +} diff --git a/Libraries/LibGfx/Font/PathFontProvider.cpp b/Libraries/LibGfx/Font/PathFontProvider.cpp index a0e31cd6a51..17161beca6b 100644 --- a/Libraries/LibGfx/Font/PathFontProvider.cpp +++ b/Libraries/LibGfx/Font/PathFontProvider.cpp @@ -65,13 +65,21 @@ RefPtr PathFontProvider::get_font(FlyString const& family, float poin return nullptr; } -void PathFontProvider::for_each_typeface_with_family_name(FlyString const& family_name, Function callback) +void PathFontProvider::for_each_typeface_with_family_name(FlyString const& family_name, Function callback) { auto it = m_typeface_by_family.find(family_name); if (it == m_typeface_by_family.end()) return; for (auto const& typeface : it->value) { - callback(*typeface); + callback(FontDescription { + .family = typeface->family(), + .weight = typeface->weight(), + .width = static_cast(typeface->width()), + .slant = static_cast(typeface->slope()), + .load_typeface = [typeface] { + return typeface; + }, + }); } } diff --git a/Libraries/LibGfx/Font/PathFontProvider.h b/Libraries/LibGfx/Font/PathFontProvider.h index d14bc1cc1d8..dc74a432a80 100644 --- a/Libraries/LibGfx/Font/PathFontProvider.h +++ b/Libraries/LibGfx/Font/PathFontProvider.h @@ -27,7 +27,7 @@ public: void load_all_fonts_from_uri(StringView); virtual RefPtr get_font(FlyString const& family, float point_size, unsigned weight, unsigned width, unsigned slope) override; - virtual void for_each_typeface_with_family_name(FlyString const& family_name, Function) override; + virtual void for_each_typeface_with_family_name(FlyString const& family_name, Function) override; virtual StringView name() const override { return m_name.bytes_as_string_view(); } private: diff --git a/Libraries/LibGfx/Font/TypefaceSkia.cpp b/Libraries/LibGfx/Font/TypefaceSkia.cpp index 17c6a502cdf..98cd1c93ab5 100644 --- a/Libraries/LibGfx/Font/TypefaceSkia.cpp +++ b/Libraries/LibGfx/Font/TypefaceSkia.cpp @@ -56,6 +56,8 @@ ErrorOr> TypefaceSkia::load_from_buffer(AK::Readonly return adopt_ref(*new TypefaceSkia { make(skia_typeface), buffer, ttc_index }); } +TypefaceSkia::~TypefaceSkia() = default; + SkTypeface const* TypefaceSkia::sk_typeface() const { return impl().skia_typeface.get(); diff --git a/Libraries/LibGfx/Font/TypefaceSkia.h b/Libraries/LibGfx/Font/TypefaceSkia.h index 7a04fe8da57..090ddb05fa0 100644 --- a/Libraries/LibGfx/Font/TypefaceSkia.h +++ b/Libraries/LibGfx/Font/TypefaceSkia.h @@ -15,6 +15,7 @@ class TypefaceSkia : public Gfx::Typeface { public: static ErrorOr> load_from_buffer(ReadonlyBytes, int index = 0); + ~TypefaceSkia() override; virtual u32 glyph_count() const override; virtual u16 units_per_em() const override; diff --git a/Libraries/LibGfx/Forward.h b/Libraries/LibGfx/Forward.h index 37e5998a7f6..284ee3c9f37 100644 --- a/Libraries/LibGfx/Forward.h +++ b/Libraries/LibGfx/Forward.h @@ -15,6 +15,7 @@ class Color; class Emoji; class Font; +struct FontDescription; class ImageDecoder; struct FontPixelMetrics; class ScaledFont; diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index e58c0dfa56f..e67359fd133 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -291,12 +291,12 @@ ErrorOr> FontLoader::try_load_font() struct StyleComputer::MatchingFontCandidate { FontFaceKey key; - Variant loader_or_typeface; + Variant loader_or_description; [[nodiscard]] RefPtr font_with_point_size(float point_size) const { RefPtr font_list = Gfx::FontCascadeList::create(); - if (auto* loader_list = loader_or_typeface.get_pointer(); loader_list) { + if (auto const* loader_list = loader_or_description.get_pointer(); loader_list) { for (auto const& loader : **loader_list) { if (auto font = loader->font_with_point_size(point_size); font) font_list->add(*font, loader->unicode_ranges()); @@ -304,7 +304,8 @@ struct StyleComputer::MatchingFontCandidate { return font_list; } - font_list->add(loader_or_typeface.get()->scaled_font(point_size)); + if (auto typeface = loader_or_description.get().load_typeface(); typeface) + font_list->add(typeface->scaled_font(point_size)); return font_list; } }; @@ -1790,14 +1791,14 @@ RefPtr StyleComputer::font_matching_algorithm(FlyStr 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) { + Gfx::FontDatabase::the().for_each_typeface_with_family_name(family_name, [&](Gfx::FontDescription description) { matching_family_fonts.empend( FontFaceKey { - .family_name = typeface.family(), - .weight = static_cast(typeface.weight()), - .slope = typeface.slope(), + .family_name = description.family, + .weight = description.weight, + .slope = static_cast(description.slant), }, - &typeface); + move(description)); }); quick_sort(matching_family_fonts, [](auto const& a, auto const& b) { return a.key.weight < b.key.weight; diff --git a/Libraries/LibWebView/Plugins/FontPlugin.cpp b/Libraries/LibWebView/Plugins/FontPlugin.cpp index efb5aaf1060..14fd9e6e15d 100644 --- a/Libraries/LibWebView/Plugins/FontPlugin.cpp +++ b/Libraries/LibWebView/Plugins/FontPlugin.cpp @@ -16,6 +16,7 @@ #include #ifdef USE_FONTCONFIG +# include # include #endif @@ -26,13 +27,13 @@ FontPlugin::FontPlugin(bool is_layout_test_mode, Gfx::SystemFontProvider* font_p { #ifdef USE_FONTCONFIG { - auto fontconfig_initialized = FcInit(); - VERIFY(fontconfig_initialized); + if (!font_provider) + font_provider = &Gfx::FontDatabase::the().install_system_font_provider(make()); } #endif if (!font_provider) - font_provider = &static_cast(Gfx::FontDatabase::the().install_system_font_provider(make())); + font_provider = &Gfx::FontDatabase::the().install_system_font_provider(make()); if (is(*font_provider)) { auto& path_font_provider = static_cast(*font_provider); // Load anything we can find in the system's font directories diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index 47d2db25da5..1573ff502dd 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -15,6 +15,7 @@ set(EDITOR_DEBUG ON) set(EMOJI_DEBUG ON) set(FILE_WATCHER_DEBUG ON) set(FLAC_ENCODER_DEBUG ON) +set(FONTCONFIG_DEBUG ON) set(GENERATE_DEBUG ON) set(GHASH_PROCESS_DEBUG ON) set(GIF_DEBUG ON) diff --git a/Services/WebContent/CMakeLists.txt b/Services/WebContent/CMakeLists.txt index ed3215395f0..fe382f1c25a 100644 --- a/Services/WebContent/CMakeLists.txt +++ b/Services/WebContent/CMakeLists.txt @@ -1,4 +1,5 @@ include(audio) +include(fontconfig) set(SOURCES BackingStoreManager.cpp diff --git a/Services/WebContent/main.cpp b/Services/WebContent/main.cpp index 6882fb8a881..4fb78a83a89 100644 --- a/Services/WebContent/main.cpp +++ b/Services/WebContent/main.cpp @@ -48,6 +48,10 @@ # include #endif +#if USE_FONTCONFIG +# include +#endif + static ErrorOr load_content_filters(StringView config_path); static ErrorOr initialize_resource_loader(GC::Heap&, int request_server_socket); static ErrorOr initialize_image_decoder(int image_decoder_socket); @@ -137,11 +141,16 @@ ErrorOr serenity_main(Main::Arguments arguments) Core::Process::wait_for_debugger_and_break(); } +#if USE_FONTCONFIG + auto& font_provider = static_cast(Gfx::FontDatabase::the().install_system_font_provider(make())); + font_provider.add_uri_to_config("resource://fonts"sv); +#else auto& font_provider = static_cast(Gfx::FontDatabase::the().install_system_font_provider(make())); if (force_fontconfig) { font_provider.set_name_but_fixme_should_create_custom_system_font_provider("FontConfig"_string); } font_provider.load_all_fonts_from_uri("resource://fonts"sv); +#endif // Layout test mode implies internals object is exposed and the Skia CPU backend is used if (is_layout_test_mode) {