LibGfx: Load with fontconfig the fonts we use

This commit is contained in:
Andrew Kaster 2025-04-08 19:41:18 -06:00
parent f070264800
commit a15aae5fa5
17 changed files with 325 additions and 17 deletions

View file

@ -78,6 +78,10 @@
# cmakedefine01 FLAC_ENCODER_DEBUG
#endif
#ifndef FONTCONFIG_DEBUG
# cmakedefine01 FONTCONFIG_DEBUG
#endif
#ifndef GENERATE_DEBUG
# cmakedefine01 GENERATE_DEBUG
#endif

View file

@ -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

View file

@ -7,6 +7,7 @@
#pragma once
#include <AK/FlyString.h>
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
@ -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<RefPtr<Typeface>()> load_typeface;
};
class Font : public RefCounted<Font> {
public:
virtual ~Font();

View file

@ -39,7 +39,7 @@ RefPtr<Gfx::Font> 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<void(Typeface const&)> callback)
void FontDatabase::for_each_typeface_with_family_name(FlyString const& family_name, Function<void(FontDescription)> callback)
{
m_system_font_provider->for_each_typeface_with_family_name(family_name, move(callback));
}

View file

@ -20,7 +20,7 @@ public:
virtual StringView name() const = 0;
virtual RefPtr<Gfx::Font> 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<void(Typeface const&)>) = 0;
virtual void for_each_typeface_with_family_name(FlyString const& family_name, Function<void(FontDescription)>) = 0;
};
class FontDatabase {
@ -29,7 +29,7 @@ public:
SystemFontProvider& install_system_font_provider(NonnullOwnPtr<SystemFontProvider>);
RefPtr<Gfx::Font> 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(Typeface const&)>);
void for_each_typeface_with_family_name(FlyString const& family_name, Function<void(FontDescription)>);
[[nodiscard]] StringView system_font_provider_name() const;
private:

View file

@ -0,0 +1,221 @@
/*
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#ifndef USE_FONTCONFIG
# error "FontconfigFontProvider requires USE_FONTCONFIG to be enabled"
#endif
#include <AK/RefPtr.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/Font/FontconfigFontProvider.h>
#include <LibGfx/Font/ScaledFont.h>
#include <LibGfx/Font/WOFF/Loader.h>
#include <fontconfig/fontconfig.h>
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<FcChar8 const*>(fs_path.characters()));
VERIFY(success);
}
RefPtr<Typeface> 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<NonnullRefPtr<Typeface>> {};
});
family_list.append(typeface);
return typeface;
}
RefPtr<Gfx::Font> 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<FcChar8 const*>(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<char const*>(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<FontDescription> 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<u16>(weight),
.width = static_cast<FontWidth>(width),
.slant = normalized_slant,
.load_typeface = [this, family, path, index, weight, width, slant]() -> RefPtr<Typeface> {
// 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<void(FontDescription)> 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<FcChar8 const*>(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<char const*>(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<char const*>(family), strlen(reinterpret_cast<char const*>(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());
}
}
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGfx/Font/FontDatabase.h>
namespace Gfx {
class FontconfigFontProvider final : public SystemFontProvider {
AK_MAKE_NONCOPYABLE(FontconfigFontProvider);
AK_MAKE_NONMOVABLE(FontconfigFontProvider);
public:
FontconfigFontProvider();
virtual ~FontconfigFontProvider() override;
virtual RefPtr<Gfx::Font> 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<void(FontDescription)>) override;
virtual StringView name() const override { return "FontConfig"sv; }
void add_uri_to_config(StringView);
private:
Optional<FontDescription> description_for_fontconfig_parameters(FlyString const& family, ByteString const& path, int index, int weight, int width, int slant);
RefPtr<Typeface> load_typeface_from_path(ByteString const& path, int index);
HashMap<FlyString, Vector<NonnullRefPtr<Typeface>>, AK::ASCIICaseInsensitiveFlyStringTraits> m_typeface_by_family;
};
}

View file

@ -65,13 +65,21 @@ RefPtr<Gfx::Font> PathFontProvider::get_font(FlyString const& family, float poin
return nullptr;
}
void PathFontProvider::for_each_typeface_with_family_name(FlyString const& family_name, Function<void(Typeface const&)> callback)
void PathFontProvider::for_each_typeface_with_family_name(FlyString const& family_name, Function<void(FontDescription)> 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<FontWidth>(typeface->width()),
.slant = static_cast<FontSlant>(typeface->slope()),
.load_typeface = [typeface] {
return typeface;
},
});
}
}

View file

@ -27,7 +27,7 @@ public:
void load_all_fonts_from_uri(StringView);
virtual RefPtr<Gfx::Font> 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<void(Typeface const&)>) override;
virtual void for_each_typeface_with_family_name(FlyString const& family_name, Function<void(FontDescription)>) override;
virtual StringView name() const override { return m_name.bytes_as_string_view(); }
private:

View file

@ -56,6 +56,8 @@ ErrorOr<NonnullRefPtr<TypefaceSkia>> TypefaceSkia::load_from_buffer(AK::Readonly
return adopt_ref(*new TypefaceSkia { make<TypefaceSkia::Impl>(skia_typeface), buffer, ttc_index });
}
TypefaceSkia::~TypefaceSkia() = default;
SkTypeface const* TypefaceSkia::sk_typeface() const
{
return impl().skia_typeface.get();

View file

@ -15,6 +15,7 @@ class TypefaceSkia : public Gfx::Typeface {
public:
static ErrorOr<NonnullRefPtr<TypefaceSkia>> load_from_buffer(ReadonlyBytes, int index = 0);
~TypefaceSkia() override;
virtual u32 glyph_count() const override;
virtual u16 units_per_em() const override;

View file

@ -15,6 +15,7 @@ class Color;
class Emoji;
class Font;
struct FontDescription;
class ImageDecoder;
struct FontPixelMetrics;
class ScaledFont;

View file

@ -291,12 +291,12 @@ ErrorOr<NonnullRefPtr<Gfx::Typeface>> FontLoader::try_load_font()
struct StyleComputer::MatchingFontCandidate {
FontFaceKey key;
Variant<FontLoaderList*, Gfx::Typeface const*> loader_or_typeface;
Variant<FontLoaderList*, Gfx::FontDescription> loader_or_description;
[[nodiscard]] RefPtr<Gfx::FontCascadeList const> font_with_point_size(float point_size) const
{
RefPtr<Gfx::FontCascadeList> font_list = Gfx::FontCascadeList::create();
if (auto* loader_list = loader_or_typeface.get_pointer<FontLoaderList*>(); loader_list) {
if (auto const* loader_list = loader_or_description.get_pointer<FontLoaderList*>(); 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<Gfx::Typeface const*>()->scaled_font(point_size));
if (auto typeface = loader_or_description.get<Gfx::FontDescription>().load_typeface(); typeface)
font_list->add(typeface->scaled_font(point_size));
return font_list;
}
};
@ -1790,14 +1791,14 @@ RefPtr<Gfx::FontCascadeList const> 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<FontLoaderList*>(&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<int>(typeface.weight()),
.slope = typeface.slope(),
.family_name = description.family,
.weight = description.weight,
.slope = static_cast<int>(description.slant),
},
&typeface);
move(description));
});
quick_sort(matching_family_fonts, [](auto const& a, auto const& b) {
return a.key.weight < b.key.weight;

View file

@ -16,6 +16,7 @@
#include <LibWebView/Plugins/FontPlugin.h>
#ifdef USE_FONTCONFIG
# include <LibGfx/Font/FontconfigFontProvider.h>
# include <fontconfig/fontconfig.h>
#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<Gfx::FontconfigFontProvider>());
}
#endif
if (!font_provider)
font_provider = &static_cast<Gfx::PathFontProvider&>(Gfx::FontDatabase::the().install_system_font_provider(make<Gfx::PathFontProvider>()));
font_provider = &Gfx::FontDatabase::the().install_system_font_provider(make<Gfx::PathFontProvider>());
if (is<Gfx::PathFontProvider>(*font_provider)) {
auto& path_font_provider = static_cast<Gfx::PathFontProvider&>(*font_provider);
// Load anything we can find in the system's font directories

View file

@ -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)

View file

@ -1,4 +1,5 @@
include(audio)
include(fontconfig)
set(SOURCES
BackingStoreManager.cpp

View file

@ -48,6 +48,10 @@
# include <LibCore/Platform/ProcessStatisticsMach.h>
#endif
#if USE_FONTCONFIG
# include <LibGfx/Font/FontconfigFontProvider.h>
#endif
static ErrorOr<void> load_content_filters(StringView config_path);
static ErrorOr<void> initialize_resource_loader(GC::Heap&, int request_server_socket);
static ErrorOr<void> initialize_image_decoder(int image_decoder_socket);
@ -137,11 +141,16 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
Core::Process::wait_for_debugger_and_break();
}
#if USE_FONTCONFIG
auto& font_provider = static_cast<Gfx::FontconfigFontProvider&>(Gfx::FontDatabase::the().install_system_font_provider(make<Gfx::FontconfigFontProvider>()));
font_provider.add_uri_to_config("resource://fonts"sv);
#else
auto& font_provider = static_cast<Gfx::PathFontProvider&>(Gfx::FontDatabase::the().install_system_font_provider(make<Gfx::PathFontProvider>()));
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) {