feat: load system fonts automatically
Bringing fonts into the binary creates more distribution burdens and legal risks. I suppose exposing an interface on the system interface to make the process of searching and loading system fonts automatic. The CSS font-family fallback order can be used.
I currently achieved this on Windows with this code.
std::vector<std::filesystem::path> getSystemFonts() {
std::vector<std::filesystem::path> fonts;
std::string windowsPath(MAX_PATH, '\0');
GetWindowsDirectoryA(windowsPath.data(), windowsPath.size());
windowsPath.resize(strlen(windowsPath.c_str()));
HKEY hKey;
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", 0,
KEY_READ, &hKey) == ERROR_SUCCESS) {
DWORD index = 0;
char valueName[16383];
DWORD valueNameSize = 16383;
DWORD valueType;
BYTE valueData[16383];
DWORD valueDataSize = 16383;
while (RegEnumValue(hKey, index, valueName, &valueNameSize, NULL,
&valueType, valueData,
&valueDataSize) == ERROR_SUCCESS) {
fonts.push_back(std::filesystem::path(windowsPath) / "Fonts" /
std::string(valueData, valueData + valueDataSize));
index++;
valueNameSize = 16383;
valueDataSize = 16383;
}
RegCloseKey(hKey);
}
if (RegOpenKeyEx(HKEY_CURRENT_USER,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", 0,
KEY_READ, &hKey) == ERROR_SUCCESS) {
DWORD index = 0;
char valueName[16383];
DWORD valueNameSize = 16383;
DWORD valueType;
BYTE valueData[16383];
DWORD valueDataSize = 16383;
while (RegEnumValue(hKey, index, valueName, &valueNameSize, NULL,
&valueType, valueData,
&valueDataSize) == ERROR_SUCCESS) {
fonts.push_back(std::filesystem::path(
std::string(valueData, valueData + valueDataSize)));
index++;
valueNameSize = 16383;
valueDataSize = 16383;
}
RegCloseKey(hKey);
}
return fonts;
}
struct FontFace {
std::string name;
std::string style;
int weight;
std::string file;
};
std::vector<FontFace> getFontFaces() {
auto fonts = getSystemFonts();
std::vector<FontFace> fontFaces;
FT_Library library;
FT_Init_FreeType(&library);
for (auto &font : fonts) {
FT_Face face;
if (!FT_New_Face(library, font.string().c_str(), 0, &face)) {
TT_OS2 *font_table = (TT_OS2 *)FT_Get_Sfnt_Table(face, FT_SFNT_OS2);
int weight;
if (font_table && font_table->usWeightClass != 0)
weight = font_table->usWeightClass;
else
weight = (face->style_flags & FT_STYLE_FLAG_BOLD) == FT_STYLE_FLAG_BOLD
? 700
: 400;
fontFaces.push_back({.name = face->family_name,
.style = face->style_name,
.weight = weight,
.file = font.string()});
FT_Done_Face(face);
}
}
return fontFaces;
}
// In main()
auto fonts = getFontFaces();
auto loadFonts = [&](std::initializer_list<std::string> preferedFonts,
bool fallback, bool onlyOne = true) {
for (auto &preferedFont : preferedFonts) {
auto font = std::find_if(fonts.begin(), fonts.end(), [&](auto &font) {
return font.name == preferedFont && font.weight == 400;
});
if (font != fonts.end()) {
Rml::LoadFontFace(font->file, fallback);
if (onlyOne)
return;
}
}
};
loadFonts(
{
"Arial",
},
false);
loadFonts(
{
"Noto Sans SC",
"Microsoft YaHei",
"Segoe UI",
},
true);
This can also involve a load-when-used system for fonts and glyphs. Currently the memory consumption is significant for loading a font.
We do have one example for loading system fonts: https://github.com/mikke89/RmlUi/blob/master/Samples/basic/ime/src/SystemFontWin32.cpp
I don't really think it's the right thing for our library to include this functionality in our core library. We want to give users full control over things like font resources. So any such platform-specific behavior should be located on the user side. We might later consider adding it to the platform layer,
Some kind of system for loading fonts as needed based on provided families and glyphs would indeed be nice. See in particular #500, I believe that issue should cover this feature already.
The things is that it's extremely awful for users to implement this. Consider the following three points:
- CSS font family fallback cannot be used. This prevents softwares from providing compatibility.
- All the fonts have to be in memory to be used in html. This increases memory consumption greatly. If only one glyph is used, then we should only store one glyph in the memory, instead of everything about the font + the glyph. This is how real-world browsers works.
- Users have to implement their own system as there's no default one to use. This makes cross-platform more difficult, also highering the confusion of beginners.
The thing can be optional, but we really should have something like that in the platform abstraction. if the user is not satisfied with the default implementation, they can use theit own's
I am closing this one, as I think #500 already cover parts of it (the fallback fonts), and we already have a sample to show how loading fonts from the system can be done. I think a sample is the best place for this right now, as most users provide their own fonts and stick to only those. We might consider integrating it into the platform layer at a later stage.