From 895c0b254e0ac9911ccf2a1d954bb00a76a4d961 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sat, 15 Jun 2024 21:11:16 +0100 Subject: [PATCH 1/7] Add: FontSearcher interface. --- src/fontcache.cpp | 16 ++++++++++++++++ src/fontdetection.h | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/fontcache.cpp b/src/fontcache.cpp index 93d56416ae..954f38d5ed 100644 --- a/src/fontcache.cpp +++ b/src/fontcache.cpp @@ -230,6 +230,22 @@ void UninitFontCache() #endif /* WITH_FREETYPE */ } +/** + * Register the FontSearcher instance. There can be only one font searcher, which depends on platform. + */ +FontSearcher::FontSearcher() +{ + FontSearcher::instance = this; +} + +/** + * Deregister this FontSearcher. + */ +FontSearcher::~FontSearcher() +{ + FontSearcher::instance = nullptr; +} + #if !defined(_WIN32) && !defined(__APPLE__) && !defined(WITH_FONTCONFIG) && !defined(WITH_COCOA) bool SetFallbackFont(FontCacheSettings *, const std::string &, int, MissingGlyphSearcher *) { return false; } diff --git a/src/fontdetection.h b/src/fontdetection.h index 2a316da505..43a5f4731d 100644 --- a/src/fontdetection.h +++ b/src/fontdetection.h @@ -39,4 +39,45 @@ FT_Error GetFontByFaceName(const char *font_name, FT_Face *face); */ bool SetFallbackFont(struct FontCacheSettings *settings, const std::string &language_isocode, int winlangid, class MissingGlyphSearcher *callback); +struct FontFamily { + std::string family; + std::string style; + int32_t slant; + int32_t weight; + + FontFamily(std::string_view family, std::string_view style, int32_t slant, int32_t weight) : family(family), style(style), slant(slant), weight(weight) {} +}; + +class FontSearcher { +public: + FontSearcher(); + virtual ~FontSearcher(); + + /** + * Get the active FontSearcher instance. + * @return FontSearcher instance, or nullptr if not present. + */ + static inline FontSearcher *GetFontSearcher() { return FontSearcher::instance; } + + /** + * List available fonts. + * @param language_isocode the language, e.g. en_GB. + * @param winlangid the language ID windows style. + * @return vector containing font family names. + */ + virtual std::vector ListFamilies(const std::string &language_isocode, int winlangid) = 0; + + /** + * List available styles for a font family. + * @param language_isocode the language, e.g. en_GB. + * @param winlangid the language ID windows style. + * @param font_family The font family to list. + * @return vector containing style information for the family. + */ + virtual std::vector ListStyles(const std::string &language_isocode, int winlangid, std::string_view font_family) = 0; + +private: + static inline FontSearcher *instance = nullptr; +}; + #endif From e31d6dba7da90ef89803b939122dc44f18f5c958 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sat, 15 Jun 2024 21:11:16 +0100 Subject: [PATCH 2/7] Add: FontConfig font search implementation. --- src/os/unix/font_unix.cpp | 124 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/src/os/unix/font_unix.cpp b/src/os/unix/font_unix.cpp index 6ab1d99823..f78dc80981 100644 --- a/src/os/unix/font_unix.cpp +++ b/src/os/unix/font_unix.cpp @@ -39,6 +39,19 @@ static std::tuple SplitFontFamilyAndStyle(std::string_ return { std::string(font_name.substr(0, separator)), std::string(font_name.substr(begin)) }; } +/** + * Get language string for FontConfig pattern matching. + * @param language_isocode Language's ISO code. + * @return Language code for FontConfig. + */ +static std::string GetFontConfigLanguage(const std::string &language_isocode) +{ + /* Fontconfig doesn't handle full language isocodes, only the part + * before the _ of e.g. en_GB is used, so "remove" everything after + * the _. */ + return fmt::format(":lang={}", language_isocode.substr(0, language_isocode.find('_'))); +} + FT_Error GetFontByFaceName(const char *font_name, FT_Face *face) { FT_Error err = FT_Err_Cannot_Open_Resource; @@ -107,10 +120,7 @@ bool SetFallbackFont(FontCacheSettings *settings, const std::string &language_is auto fc_instance = FcConfigReference(nullptr); assert(fc_instance != nullptr); - /* Fontconfig doesn't handle full language isocodes, only the part - * before the _ of e.g. en_GB is used, so "remove" everything after - * the _. */ - std::string lang = fmt::format(":lang={}", language_isocode.substr(0, language_isocode.find('_'))); + std::string lang = GetFontConfigLanguage(language_isocode); /* First create a pattern to match the wanted language. */ FcPattern *pat = FcNameParse((const FcChar8 *)lang.c_str()); @@ -180,3 +190,109 @@ bool SetFallbackFont(FontCacheSettings *settings, const std::string &language_is FcConfigDestroy(fc_instance); return ret; } + +/** + * FontConfig implementation of FontSearcher. + */ +class FontConfigFontSearcher : public FontSearcher { +public: + std::vector ListFamilies(const std::string &language_isocode, int winlangid) override; + std::vector ListStyles(const std::string &language_isocode, int winlangid, std::string_view font_family) override; +}; + +std::vector FontConfigFontSearcher::ListFamilies(const std::string &language_isocode, int) +{ + std::vector families; + + if (!FcInit()) return families; + + FcConfig *fc_instance = FcConfigReference(nullptr); + assert(fc_instance != nullptr); + + std::string lang = GetFontConfigLanguage(language_isocode); + + /* First create a pattern to match the wanted language. */ + FcPattern *pat = FcNameParse(reinterpret_cast(lang.c_str())); + /* We want to know this attributes. */ + FcObjectSet *os = FcObjectSetCreate(); + FcObjectSetAdd(os, FC_FAMILY); + /* Get the list of filenames matching the wanted language. */ + FcFontSet *fs = FcFontList(nullptr, pat, os); + + /* We don't need these anymore. */ + FcObjectSetDestroy(os); + FcPatternDestroy(pat); + + if (fs != nullptr) { + families.reserve(fs->nfont); + for (const FcPattern *font : std::span(fs->fonts, fs->nfont)) { + FcChar8 *family; + if (FcPatternGetString(font, FC_FAMILY, 0, &family) != FcResultMatch) continue; + + /* Check if the family already exists. */ + std::string_view sv_family = reinterpret_cast(family); + if (std::find(std::begin(families), std::end(families), sv_family) != std::end(families)) continue; + + families.emplace_back(sv_family); + } + + /* Clean up the list of filenames. */ + FcFontSetDestroy(fs); + } + + FcConfigDestroy(fc_instance); + return families; +} + +std::vector FontConfigFontSearcher::ListStyles(const std::string &language_isocode, int, std::string_view font_family) +{ + std::vector styles; + + if (!FcInit()) return styles; + + FcConfig *fc_instance = FcConfigReference(nullptr); + assert(fc_instance != nullptr); + + std::string lang = GetFontConfigLanguage(language_isocode); + + /* First create a pattern to match the wanted language. */ + FcPattern *pat = FcNameParse(reinterpret_cast(lang.c_str())); + FcPatternAddString(pat, FC_FAMILY, reinterpret_cast(std::string(font_family).c_str())); + /* We want to know these attributes. */ + FcObjectSet *os = FcObjectSetCreate(); + FcObjectSetAdd(os, FC_FAMILY); + FcObjectSetAdd(os, FC_STYLE); + FcObjectSetAdd(os, FC_SLANT); + FcObjectSetAdd(os, FC_WEIGHT); + /* Get the list of filenames matching the wanted language. */ + FcFontSet *fs = FcFontList(nullptr, pat, os); + + /* We don't need these anymore. */ + FcObjectSetDestroy(os); + FcPatternDestroy(pat); + + if (fs != nullptr) { + styles.reserve(fs->nfont); + for (const FcPattern *font : std::span(fs->fonts, fs->nfont)) { + FcChar8 *family; + FcChar8 *style; + int32_t slant; + int32_t weight; + + if (FcPatternGetString(font, FC_FAMILY, 0, &family) != FcResultMatch) continue; + if (FcPatternGetString(font, FC_STYLE, 0, &style) != FcResultMatch) continue; + if (FcPatternGetInteger(font, FC_SLANT, 0, &slant) != FcResultMatch) continue; + if (FcPatternGetInteger(font, FC_WEIGHT, 0, &weight) != FcResultMatch) continue; + + styles.emplace_back(reinterpret_cast(family), reinterpret_cast(style), slant, weight); + } + + /* Clean up the list of filenames. */ + FcFontSetDestroy(fs); + } + + FcConfigDestroy(fc_instance); + return styles; +} + +static FontConfigFontSearcher _fcfs_instance; From 512f2a2258601c215581385fe463d71aaa027f86 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sat, 15 Jun 2024 21:11:17 +0100 Subject: [PATCH 3/7] Add: Native Win32 font search implementation. --- src/os/windows/font_win32.cpp | 108 ++++++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/src/os/windows/font_win32.cpp b/src/os/windows/font_win32.cpp index b2eac91314..77d925a277 100644 --- a/src/os/windows/font_win32.cpp +++ b/src/os/windows/font_win32.cpp @@ -40,9 +40,8 @@ struct EFCParam { bool Add(const std::wstring_view &font) { - for (const auto &entry : this->fonts) { - if (font.compare(entry) == 0) return false; - } + if (font.starts_with('@')) return false; + if (std::find(std::begin(this->fonts), std::end(this->fonts), font) != std::end(this->fonts)) return false; this->fonts.emplace_back(font); @@ -99,7 +98,6 @@ bool SetFallbackFont(FontCacheSettings *settings, const std::string &, int winla return ret == 0; } - /** * Create a new Win32FontCache. * @param fs The font size that is going to be cached. @@ -380,3 +378,105 @@ void LoadWin32Font(FontSize fs) LoadWin32Font(fs, logfont, settings->size, font_name); } + +/** + * Win32 implementation of FontSearcher. + */ +class Win32FontSearcher : public FontSearcher { +public: + std::vector ListFamilies(const std::string &language_isocode, int winlangid) override; + std::vector ListStyles(const std::string &language_isocode, int winlangid, std::string_view font_family) override; +}; + +/** + * State passed between EnumFontFamiliesEx and our list families callback. + */ +struct EFCListFamiliesParam : EFCParam { + std::vector families; ///< List of families found. +}; + +static int CALLBACK ListFamiliesFontCallback(ENUMLOGFONTEX *lpelfe, NEWTEXTMETRICEX *metric, DWORD type, LPARAM lParam) +{ + EFCListFamiliesParam &info = *reinterpret_cast(lParam); + + /* Only use TrueType fonts */ + if (!(type & TRUETYPE_FONTTYPE)) return 1; + /* Skip duplicates */ + if (!info.Add(lpelfe->elfFullName)) return 1; + /* Don't use SYMBOL fonts */ + if (lpelfe->elfLogFont.lfCharSet == SYMBOL_CHARSET) return 1; + /* The font has to have at least one of the supported locales to be usable. */ + if ((metric->ntmFontSig.fsCsb[0] & info.locale.lsCsbSupported[0]) == 0 && (metric->ntmFontSig.fsCsb[1] & info.locale.lsCsbSupported[1]) == 0) return 1; + + LOGFONT &lf = lpelfe->elfLogFont; + info.families.emplace_back(FS2OTTD(lf.lfFaceName)); + + return 1; +} + +std::vector Win32FontSearcher::ListFamilies(const std::string &, int winlangid) +{ + EFCListFamiliesParam info; + if (GetLocaleInfo(MAKELCID(winlangid, SORT_DEFAULT), LOCALE_FONTSIGNATURE, reinterpret_cast(&info.locale), sizeof(info.locale) / sizeof(wchar_t)) == 0) { + /* Invalid langid or some other mysterious error, can't determine fallback font. */ + Debug(fontcache, 1, "Can't get locale info for fallback font (langid=0x{:x})", winlangid); + return info.families; + } + + LOGFONT lf{}; + lf.lfCharSet = DEFAULT_CHARSET; + + HDC dc = GetDC(nullptr); + EnumFontFamiliesEx(dc, &lf, (FONTENUMPROC)&ListFamiliesFontCallback, reinterpret_cast(&info), 0); + ReleaseDC(nullptr, dc); + + return info.families; +} + +/** + * State passed between EnumFontFamiliesEx and our list styles callback. + */ +struct EFCListStylesParam : EFCParam { + std::vector styles; ///< List of styles for the family. +}; + +static int CALLBACK ListStylesFontCallback(ENUMLOGFONTEX *lpelfe, NEWTEXTMETRICEX *metric, DWORD type, LPARAM lParam) +{ + EFCListStylesParam &info = *reinterpret_cast(lParam); + + /* Only use TrueType fonts */ + if (!(type & TRUETYPE_FONTTYPE)) return 1; + /* Skip duplicates */ + if (!info.Add(lpelfe->elfFullName)) return 1; + /* Don't use SYMBOL fonts */ + if (lpelfe->elfLogFont.lfCharSet == SYMBOL_CHARSET) return 1; + /* The font has to have at least one of the supported locales to be usable. */ + if ((metric->ntmFontSig.fsCsb[0] & info.locale.lsCsbSupported[0]) == 0 && (metric->ntmFontSig.fsCsb[1] & info.locale.lsCsbSupported[1]) == 0) return 1; + + LOGFONT &lf = lpelfe->elfLogFont; + info.styles.emplace_back(FS2OTTD(lf.lfFaceName), FS2OTTD(lpelfe->elfStyle), lf.lfItalic, lf.lfWeight); + + return 1; +} + +std::vector Win32FontSearcher::ListStyles(const std::string &, int winlangid, std::string_view font_family) +{ + EFCListStylesParam info; + if (GetLocaleInfo(MAKELCID(winlangid, SORT_DEFAULT), LOCALE_FONTSIGNATURE, reinterpret_cast(&info.locale), sizeof(info.locale) / sizeof(wchar_t)) == 0) { + /* Invalid langid or some other mysterious error, can't determine fallback font. */ + Debug(fontcache, 1, "Can't get locale info for fallback font (langid=0x{:x})", winlangid); + return info.styles; + } + + LOGFONT lf{}; + lf.lfCharSet = DEFAULT_CHARSET; + convert_to_fs(font_family, lf.lfFaceName, std::size(lf.lfFaceName)); + + HDC dc = GetDC(nullptr); + EnumFontFamiliesEx(dc, &lf, (FONTENUMPROC)&ListStylesFontCallback, reinterpret_cast(&info), 0); + ReleaseDC(nullptr, dc); + + return info.styles; +} + +static Win32FontSearcher _win32fs_instance; From 0378d5690971d525d5f241791f834cf5f2823303 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sat, 15 Jun 2024 21:11:17 +0100 Subject: [PATCH 4/7] Add: Mac OS font search implementation. --- src/os/macosx/font_osx.cpp | 134 +++++++++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/src/os/macosx/font_osx.cpp b/src/os/macosx/font_osx.cpp index db4d4c1f83..d2c3bba685 100644 --- a/src/os/macosx/font_osx.cpp +++ b/src/os/macosx/font_osx.cpp @@ -24,7 +24,7 @@ #include "safeguards.h" -bool SetFallbackFont(FontCacheSettings *settings, const std::string &language_isocode, int, MissingGlyphSearcher *callback) +static void EnumerateCoreFextFonts(const std::string &language_isocode, int ntries, std::function enum_func) { /* Determine fallback font using CoreText. This uses the language isocode * to find a suitable font. CoreText is available from 10.5 onwards. */ @@ -55,9 +55,12 @@ bool SetFallbackFont(FontCacheSettings *settings, const std::string &language_is CFAutoRelease mandatory_attribs(CFSetCreate(kCFAllocatorDefault, const_cast(reinterpret_cast(&kCTFontLanguagesAttribute)), 1, &kCFTypeSetCallBacks)); CFAutoRelease descs(CTFontDescriptorCreateMatchingFontDescriptors(lang_desc.get(), mandatory_attribs.get())); - bool result = false; - for (int tries = 0; tries < 2; tries++) { - for (CFIndex i = 0; descs.get() != nullptr && i < CFArrayGetCount(descs.get()); i++) { + /* Nothing to see here. */ + if (descs == nullptr) return; + + CFIndex count = CFArrayGetCount(descs.get()); + for (int tries = 0; tries < ntries; tries++) { + for (CFIndex i = 0; i < count; i++) { CTFontDescriptorRef font = (CTFontDescriptorRef)CFArrayGetValueAtIndex(descs.get(), i); /* Get font traits. */ @@ -67,34 +70,46 @@ bool SetFallbackFont(FontCacheSettings *settings, const std::string &language_is /* Skip symbol fonts and vertical fonts. */ if ((symbolic_traits & kCTFontClassMaskTrait) == (CTFontStylisticClass)kCTFontSymbolicClass || (symbolic_traits & kCTFontVerticalTrait)) continue; - /* Skip bold fonts (especially Arial Bold, which looks worse than regular Arial). */ - if (symbolic_traits & kCTFontBoldTrait) continue; - /* Select monospaced fonts if asked for. */ - if (((symbolic_traits & kCTFontMonoSpaceTrait) == kCTFontMonoSpaceTrait) != callback->Monospace()) continue; - /* Get font name. */ - char name[128]; - CFAutoRelease font_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontDisplayNameAttribute)); - CFStringGetCString(font_name.get(), name, lengthof(name), kCFStringEncodingUTF8); - - /* Serif fonts usually look worse on-screen with only small - * font sizes. As such, we try for a sans-serif font first. - * If we can't find one in the first try, try all fonts. */ - if (tries == 0 && (symbolic_traits & kCTFontClassMaskTrait) != (CTFontStylisticClass)kCTFontSansSerifClass) continue; - - /* There are some special fonts starting with an '.' and the last - * resort font that aren't usable. Skip them. */ - if (name[0] == '.' || strncmp(name, "LastResort", 10) == 0) continue; - - /* Save result. */ - callback->SetFontNames(settings, name); - if (!callback->FindMissingGlyphs()) { - Debug(fontcache, 2, "CT-Font for {}: {}", language_isocode, name); - result = true; - break; - } + bool continue_enumerating = enum_func(tries, font, symbolic_traits); + if (!continue_enumerating) return; } } +} + +bool SetFallbackFont(FontCacheSettings *settings, const std::string &language_isocode, int, MissingGlyphSearcher *callback) +{ + bool result = false; + EnumerateCoreFextFonts(language_isocode, 2, [&settings, &language_isocode, &callback, &result](int tries, CTFontDescriptorRef font, CTFontSymbolicTraits symbolic_traits) { + /* Skip bold fonts (especially Arial Bold, which looks worse than regular Arial). */ + if (symbolic_traits & kCTFontBoldTrait) return true; + /* Select monospaced fonts if asked for. */ + if (((symbolic_traits & kCTFontMonoSpaceTrait) == kCTFontMonoSpaceTrait) != callback->Monospace()) return true; + + /* Get font name. */ + char name[128]; + CFAutoRelease font_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontDisplayNameAttribute)); + CFStringGetCString(font_name.get(), name, lengthof(name), kCFStringEncodingUTF8); + + /* Serif fonts usually look worse on-screen with only small + * font sizes. As such, we try for a sans-serif font first. + * If we can't find one in the first try, try all fonts. */ + if (tries == 0 && (symbolic_traits & kCTFontClassMaskTrait) != (CTFontStylisticClass)kCTFontSansSerifClass) return true; + + /* There are some special fonts starting with an '.' and the last + * resort font that aren't usable. Skip them. */ + if (name[0] == '.' || strncmp(name, "LastResort", 10) == 0) return true; + + /* Save result. */ + callback->SetFontNames(settings, name); + if (!callback->FindMissingGlyphs()) { + Debug(fontcache, 2, "CT-Font for {}: {}", language_isocode, name); + result = true; + return false; + } + + return true; + }); if (!result) { /* For some OS versions, the font 'Arial Unicode MS' does not report all languages it @@ -371,3 +386,64 @@ void LoadCoreTextFont(FontSize fs) new CoreTextFontCache(fs, std::move(font_ref), settings->size); } + +class CoreTextFontSearcher : public FontSearcher { +public: + std::vector ListFamilies(const std::string &language_isocode, int winlangid) override; + std::vector ListStyles(const std::string &language_isocode, int winlangid, std::string_view font_family) override; +}; + +std::vector CoreTextFontSearcher::ListFamilies(const std::string &language_isocode, int) +{ + std::vector families; + + EnumerateCoreFextFonts(language_isocode, 1, [&families](int, CTFontDescriptorRef font, CTFontSymbolicTraits) { + /* Get font name. */ + char family[128]; + CFAutoRelease font_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontFamilyNameAttribute)); + CFStringGetCString(font_name.get(), family, std::size(family), kCFStringEncodingUTF8); + + /* There are some special fonts starting with an '.' and the last resort font that aren't usable. Skip them. */ + if (family[0] == '.' || strncmp(family, "LastResort", 10) == 0) return true; + + if (std::find(std::begin(families), std::end(families), family) == std::end(families)) { + families.push_back(family); + } + + return true; + }); + + return families; +} + +std::vector CoreTextFontSearcher::ListStyles(const std::string &language_isocode, int, std::string_view font_family) +{ + std::vector styles; + + EnumerateCoreFextFonts(language_isocode, 1, [&styles, &font_family](int, CTFontDescriptorRef font, CTFontSymbolicTraits) { + /* Get font name. */ + char family[128]; + CFAutoRelease family_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontFamilyNameAttribute)); + CFStringGetCString(family_name.get(), family, std::size(family), kCFStringEncodingUTF8); + + if (font_family != family) return true; + + char style[128]; + CFAutoRelease style_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontStyleNameAttribute)); + CFStringGetCString(style_name.get(), style, std::size(style), kCFStringEncodingUTF8); + + CFAutoRelease traits((CFDictionaryRef)CTFontDescriptorCopyAttribute(font, kCTFontTraitsAttribute)); + float weight = 0.0f; + CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(traits.get(), kCTFontWeightTrait), kCFNumberFloatType, &weight); + float slant = 0.0f; + CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(traits.get(), kCTFontSlantTrait), kCFNumberFloatType, &slant); + + styles.emplace_back(family, style, static_cast(slant * 100), static_cast(weight * 100)); + + return true; + }); + + return styles; +} + +CoreTextFontSearcher _coretextfs_instance; From 2f75614cfee2e128ebc9329ea1e50c430a403c94 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sat, 15 Jun 2024 21:11:18 +0100 Subject: [PATCH 5/7] Add: list_fonts console command to list available fonts. --- src/console_cmds.cpp | 52 ++++++++++++++++++++++++++++++++++++++++++++ src/fontcache.cpp | 9 ++++++++ src/fontdetection.h | 2 ++ 3 files changed, 63 insertions(+) diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 4656ecc52f..48f44a264a 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -24,6 +24,8 @@ #include "fios.h" #include "fileio_func.h" #include "fontcache.h" +#include "fontdetection.h" +#include "language.h" #include "screenshot.h" #include "genworld.h" #include "strings_func.h" @@ -46,6 +48,7 @@ #include "company_cmd.h" #include "misc_cmd.h" +#include #include #include "safeguards.h" @@ -2179,6 +2182,54 @@ DEF_CONSOLE_CMD(ConContent) } #endif /* defined(WITH_ZLIB) */ +/* List all the fonts available via console */ +DEF_CONSOLE_CMD(ConListFonts) +{ + if (argc == 0) { + IConsolePrint(CC_HELP, "List all fonts."); + return true; + } + + FontSearcher *fs = FontSearcher::GetFontSearcher(); + if (fs == nullptr) { + IConsolePrint(CC_ERROR, "No font searcher exists."); + return true; + } + + if (argc == 1) { + auto families = fs->ListFamilies(_current_language->isocode, _current_language->winlangid); + std::sort(std::begin(families), std::end(families)); + + int i = 0; + for (auto &family : families) { + IConsolePrint(CC_DEFAULT, "{}) {}", i, family); + ++i; + } + } else if (argc == 2) { + std::string family = argv[1]; + + /* If argv is a number treat it as an index into the list of fonts, which we need to get again... */ + int index; + auto [_, err] = std::from_chars(family.data(), family.data() + family.size(), index, 10); + if (err == std::errc()) { + auto families = fs->ListFamilies(_current_language->isocode, _current_language->winlangid); + std::sort(std::begin(families), std::end(families)); + if (IsInsideMM(index, 0, families.size())) family = families[index]; + } + + auto styles = fs->ListStyles(_current_language->isocode, _current_language->winlangid, family); + std::sort(std::begin(styles), std::end(styles), FontFamilySorter); + + int i = 0; + for (auto &font : styles) { + IConsolePrint(CC_DEFAULT, "{}) {}, {}", i, font.family, font.style); + i++; + } + } + + return true; +} + DEF_CONSOLE_CMD(ConFont) { if (argc == 0) { @@ -2751,6 +2802,7 @@ void IConsoleStdLibRegister() IConsole::CmdRegister("saveconfig", ConSaveConfig); IConsole::CmdRegister("ls", ConListFiles); IConsole::CmdRegister("list_saves", ConListFiles); + IConsole::CmdRegister("list_fonts", ConListFonts); IConsole::CmdRegister("list_scenarios", ConListScenarios); IConsole::CmdRegister("list_heightmaps", ConListHeightmaps); IConsole::CmdRegister("cd", ConChangeDirectory); diff --git a/src/fontcache.cpp b/src/fontcache.cpp index 954f38d5ed..88a7fdfb9f 100644 --- a/src/fontcache.cpp +++ b/src/fontcache.cpp @@ -230,6 +230,15 @@ void UninitFontCache() #endif /* WITH_FREETYPE */ } +bool FontFamilySorter(const FontFamily &a, const FontFamily &b) +{ + int r = StrNaturalCompare(a.family, b.family); + if (r == 0) r = (a.weight - b.weight); + if (r == 0) r = (a.slant - b.slant); + if (r == 0) r = StrNaturalCompare(a.style, b.style); + return r < 0; +} + /** * Register the FontSearcher instance. There can be only one font searcher, which depends on platform. */ diff --git a/src/fontdetection.h b/src/fontdetection.h index 43a5f4731d..fa71cd7728 100644 --- a/src/fontdetection.h +++ b/src/fontdetection.h @@ -48,6 +48,8 @@ struct FontFamily { FontFamily(std::string_view family, std::string_view style, int32_t slant, int32_t weight) : family(family), style(style), slant(slant), weight(weight) {} }; +bool FontFamilySorter(const FontFamily &a, const FontFamily &b); + class FontSearcher { public: FontSearcher(); From 233061aaa2517b185ceee2832f637036ea0e1c3b Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sat, 15 Jun 2024 21:11:18 +0100 Subject: [PATCH 6/7] Change: WIP Change interface to list all families and styles at once. --- src/fontcache.cpp | 36 +++++++++++++++ src/fontdetection.h | 16 ++++++- src/os/macosx/font_osx.cpp | 43 +++++------------- src/os/unix/font_unix.cpp | 66 ++++++--------------------- src/os/windows/font_win32.cpp | 86 +++++++++++------------------------ 5 files changed, 101 insertions(+), 146 deletions(-) diff --git a/src/fontcache.cpp b/src/fontcache.cpp index 88a7fdfb9f..93b4e69f04 100644 --- a/src/fontcache.cpp +++ b/src/fontcache.cpp @@ -255,6 +255,42 @@ FontSearcher::~FontSearcher() FontSearcher::instance = nullptr; } +std::vector FontSearcher::ListFamilies(const std::string &language_isocode, int winlangid) +{ + std::vector families; + + if (this->cached_language_isocode != language_isocode || this->cached_winlangid != winlangid) { + this->UpdateCachedFonts(language_isocode, winlangid); + this->cached_language_isocode = language_isocode; + this->cached_winlangid = winlangid; + } + + for (const FontFamily &ff : this->cached_fonts) { + if (std::find(std::begin(families), std::end(families), ff.family) != std::end(families)) continue; + families.push_back(ff.family); + } + + return families; +} + +std::vector> FontSearcher::ListStyles(const std::string &language_isocode, int winlangid, std::string_view family) +{ + std::vector> styles; + + if (this->cached_language_isocode != language_isocode || this->cached_winlangid != winlangid) { + this->UpdateCachedFonts(language_isocode, winlangid); + this->cached_language_isocode = language_isocode; + this->cached_winlangid = winlangid; + } + + for (const FontFamily &ff : this->cached_fonts) { + if (ff.family != family) continue; + styles.emplace_back(std::ref(ff)); + } + + return styles; +} + #if !defined(_WIN32) && !defined(__APPLE__) && !defined(WITH_FONTCONFIG) && !defined(WITH_COCOA) bool SetFallbackFont(FontCacheSettings *, const std::string &, int, MissingGlyphSearcher *) { return false; } diff --git a/src/fontdetection.h b/src/fontdetection.h index fa71cd7728..4556f18f5d 100644 --- a/src/fontdetection.h +++ b/src/fontdetection.h @@ -61,13 +61,20 @@ public: */ static inline FontSearcher *GetFontSearcher() { return FontSearcher::instance; } + /** + * Update cached font information. + * @param language_isocode the language, e.g. en_GB. + * @param winlangid the language ID windows style. + */ + virtual void UpdateCachedFonts(const std::string &language_isocode, int winlangid) = 0; + /** * List available fonts. * @param language_isocode the language, e.g. en_GB. * @param winlangid the language ID windows style. * @return vector containing font family names. */ - virtual std::vector ListFamilies(const std::string &language_isocode, int winlangid) = 0; + std::vector ListFamilies(const std::string &language_isocode, int winlangid); /** * List available styles for a font family. @@ -76,7 +83,12 @@ public: * @param font_family The font family to list. * @return vector containing style information for the family. */ - virtual std::vector ListStyles(const std::string &language_isocode, int winlangid, std::string_view font_family) = 0; + std::vector> ListStyles(const std::string &language_isocode, int winlangid, std::string_view family); + +protected: + std::vector cached_fonts; + std::string cached_language_isocode; + int cached_winlangid; private: static inline FontSearcher *instance = nullptr; diff --git a/src/os/macosx/font_osx.cpp b/src/os/macosx/font_osx.cpp index d2c3bba685..e6b1704c74 100644 --- a/src/os/macosx/font_osx.cpp +++ b/src/os/macosx/font_osx.cpp @@ -389,61 +389,40 @@ void LoadCoreTextFont(FontSize fs) class CoreTextFontSearcher : public FontSearcher { public: - std::vector ListFamilies(const std::string &language_isocode, int winlangid) override; - std::vector ListStyles(const std::string &language_isocode, int winlangid, std::string_view font_family) override; + void UpdateCachedFonts(const std::string &language_isocode, int winlangid) override; }; -std::vector CoreTextFontSearcher::ListFamilies(const std::string &language_isocode, int) +void CoreTextFontSearcher::UpdateCachedFonts(const std::string &language_isocode, int) { - std::vector families; + this->cached_fonts.clear(); - EnumerateCoreFextFonts(language_isocode, 1, [&families](int, CTFontDescriptorRef font, CTFontSymbolicTraits) { - /* Get font name. */ - char family[128]; - CFAutoRelease font_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontFamilyNameAttribute)); - CFStringGetCString(font_name.get(), family, std::size(family), kCFStringEncodingUTF8); - - /* There are some special fonts starting with an '.' and the last resort font that aren't usable. Skip them. */ - if (family[0] == '.' || strncmp(family, "LastResort", 10) == 0) return true; - - if (std::find(std::begin(families), std::end(families), family) == std::end(families)) { - families.push_back(family); - } - - return true; - }); - - return families; -} - -std::vector CoreTextFontSearcher::ListStyles(const std::string &language_isocode, int, std::string_view font_family) -{ - std::vector styles; - - EnumerateCoreFextFonts(language_isocode, 1, [&styles, &font_family](int, CTFontDescriptorRef font, CTFontSymbolicTraits) { + EnumerateCoreFextFonts(language_isocode, 1, [this](int, CTFontDescriptorRef font, CTFontSymbolicTraits) { /* Get font name. */ char family[128]; CFAutoRelease family_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontFamilyNameAttribute)); CFStringGetCString(family_name.get(), family, std::size(family), kCFStringEncodingUTF8); - if (font_family != family) return true; - char style[128]; CFAutoRelease style_name((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontStyleNameAttribute)); CFStringGetCString(style_name.get(), style, std::size(style), kCFStringEncodingUTF8); + /* Don't add duplicate fonts. */ + std::string_view sv_family = family; + std::string_view sv_style = style; + if (std::any_of(std::begin(this->cached_fonts), std::end(this->cached_fonts), [&sv_family, &sv_style](const FontFamily &ff) { return ff.family == sv_family && ff.style == sv_style; })) return true; + CFAutoRelease traits((CFDictionaryRef)CTFontDescriptorCopyAttribute(font, kCTFontTraitsAttribute)); float weight = 0.0f; CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(traits.get(), kCTFontWeightTrait), kCFNumberFloatType, &weight); float slant = 0.0f; CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(traits.get(), kCTFontSlantTrait), kCFNumberFloatType, &slant); - styles.emplace_back(family, style, static_cast(slant * 100), static_cast(weight * 100)); + this->cached_fonts.emplace_back(sv_family, sv_style, static_cast(slant * 100), static_cast(weight * 100)); return true; }); - return styles; + std::sort(std::begin(this->cached_fonts), std::end(this->cached_fonts), FontFamilySorter); } CoreTextFontSearcher _coretextfs_instance; diff --git a/src/os/unix/font_unix.cpp b/src/os/unix/font_unix.cpp index f78dc80981..085be73b06 100644 --- a/src/os/unix/font_unix.cpp +++ b/src/os/unix/font_unix.cpp @@ -196,15 +196,14 @@ bool SetFallbackFont(FontCacheSettings *settings, const std::string &language_is */ class FontConfigFontSearcher : public FontSearcher { public: - std::vector ListFamilies(const std::string &language_isocode, int winlangid) override; - std::vector ListStyles(const std::string &language_isocode, int winlangid, std::string_view font_family) override; + void UpdateCachedFonts(const std::string &language_isocode, int winlangid) override; }; -std::vector FontConfigFontSearcher::ListFamilies(const std::string &language_isocode, int) +void FontConfigFontSearcher::UpdateCachedFonts(const std::string &language_isocode, int) { - std::vector families; + this->cached_fonts.clear(); - if (!FcInit()) return families; + if (!FcInit()) return; FcConfig *fc_instance = FcConfigReference(nullptr); assert(fc_instance != nullptr); @@ -216,51 +215,6 @@ std::vector FontConfigFontSearcher::ListFamilies(const std::string /* We want to know this attributes. */ FcObjectSet *os = FcObjectSetCreate(); FcObjectSetAdd(os, FC_FAMILY); - /* Get the list of filenames matching the wanted language. */ - FcFontSet *fs = FcFontList(nullptr, pat, os); - - /* We don't need these anymore. */ - FcObjectSetDestroy(os); - FcPatternDestroy(pat); - - if (fs != nullptr) { - families.reserve(fs->nfont); - for (const FcPattern *font : std::span(fs->fonts, fs->nfont)) { - FcChar8 *family; - if (FcPatternGetString(font, FC_FAMILY, 0, &family) != FcResultMatch) continue; - - /* Check if the family already exists. */ - std::string_view sv_family = reinterpret_cast(family); - if (std::find(std::begin(families), std::end(families), sv_family) != std::end(families)) continue; - - families.emplace_back(sv_family); - } - - /* Clean up the list of filenames. */ - FcFontSetDestroy(fs); - } - - FcConfigDestroy(fc_instance); - return families; -} - -std::vector FontConfigFontSearcher::ListStyles(const std::string &language_isocode, int, std::string_view font_family) -{ - std::vector styles; - - if (!FcInit()) return styles; - - FcConfig *fc_instance = FcConfigReference(nullptr); - assert(fc_instance != nullptr); - - std::string lang = GetFontConfigLanguage(language_isocode); - - /* First create a pattern to match the wanted language. */ - FcPattern *pat = FcNameParse(reinterpret_cast(lang.c_str())); - FcPatternAddString(pat, FC_FAMILY, reinterpret_cast(std::string(font_family).c_str())); - /* We want to know these attributes. */ - FcObjectSet *os = FcObjectSetCreate(); - FcObjectSetAdd(os, FC_FAMILY); FcObjectSetAdd(os, FC_STYLE); FcObjectSetAdd(os, FC_SLANT); FcObjectSetAdd(os, FC_WEIGHT); @@ -272,7 +226,7 @@ std::vector FontConfigFontSearcher::ListStyles(const std::string &la FcPatternDestroy(pat); if (fs != nullptr) { - styles.reserve(fs->nfont); + this->cached_fonts.reserve(fs->nfont); for (const FcPattern *font : std::span(fs->fonts, fs->nfont)) { FcChar8 *family; FcChar8 *style; @@ -284,7 +238,12 @@ std::vector FontConfigFontSearcher::ListStyles(const std::string &la if (FcPatternGetInteger(font, FC_SLANT, 0, &slant) != FcResultMatch) continue; if (FcPatternGetInteger(font, FC_WEIGHT, 0, &weight) != FcResultMatch) continue; - styles.emplace_back(reinterpret_cast(family), reinterpret_cast(style), slant, weight); + /* Don't add duplicate fonts. */ + std::string_view sv_family = reinterpret_cast(family); + std::string_view sv_style = reinterpret_cast(style); + if (std::any_of(std::begin(this->cached_fonts), std::end(this->cached_fonts), [&sv_family, &sv_style](const FontFamily &ff) { return ff.family == sv_family && ff.style == sv_style; })) continue; + + this->cached_fonts.emplace_back(sv_family, sv_style, slant, weight); } /* Clean up the list of filenames. */ @@ -292,7 +251,8 @@ std::vector FontConfigFontSearcher::ListStyles(const std::string &la } FcConfigDestroy(fc_instance); - return styles; + + std::sort(std::begin(this->cached_fonts), std::end(this->cached_fonts), FontFamilySorter); } static FontConfigFontSearcher _fcfs_instance; diff --git a/src/os/windows/font_win32.cpp b/src/os/windows/font_win32.cpp index 77d925a277..7ee5993284 100644 --- a/src/os/windows/font_win32.cpp +++ b/src/os/windows/font_win32.cpp @@ -34,7 +34,7 @@ struct EFCParam { FontCacheSettings *settings; - LOCALESIGNATURE locale; + LOCALESIGNATURE locale; MissingGlyphSearcher *callback; std::vector fonts; @@ -384,20 +384,31 @@ void LoadWin32Font(FontSize fs) */ class Win32FontSearcher : public FontSearcher { public: - std::vector ListFamilies(const std::string &language_isocode, int winlangid) override; - std::vector ListStyles(const std::string &language_isocode, int winlangid, std::string_view font_family) override; + void UpdateCachedFonts(const std::string &language_isocode, int winlangid) override; }; /** - * State passed between EnumFontFamiliesEx and our list families callback. + * State passed between EnumFontFamiliesEx and our list fonts callbacks. */ -struct EFCListFamiliesParam : EFCParam { - std::vector families; ///< List of families found. +struct EFCListFontsParam : EFCParam { + std::vector &fonts; + + explicit EFCListFontsParam(std::vector &fonts) : fonts(fonts) {} }; +static int CALLBACK ListStylesFontCallback(ENUMLOGFONTEX *lpelfe, NEWTEXTMETRICEX *, DWORD, LPARAM lParam) +{ + EFCListFontsParam &info = *reinterpret_cast(lParam); + + LOGFONT &lf = lpelfe->elfLogFont; + info.fonts.emplace_back(FS2OTTD(lf.lfFaceName), FS2OTTD(lpelfe->elfStyle), lf.lfItalic, lf.lfWeight); + + return 1; +} + static int CALLBACK ListFamiliesFontCallback(ENUMLOGFONTEX *lpelfe, NEWTEXTMETRICEX *metric, DWORD type, LPARAM lParam) { - EFCListFamiliesParam &info = *reinterpret_cast(lParam); + EFCListFontsParam &info = *reinterpret_cast(lParam); /* Only use TrueType fonts */ if (!(type & TRUETYPE_FONTTYPE)) return 1; @@ -409,18 +420,23 @@ static int CALLBACK ListFamiliesFontCallback(ENUMLOGFONTEX *lpelfe, NEWTEXTMETRI if ((metric->ntmFontSig.fsCsb[0] & info.locale.lsCsbSupported[0]) == 0 && (metric->ntmFontSig.fsCsb[1] & info.locale.lsCsbSupported[1]) == 0) return 1; LOGFONT &lf = lpelfe->elfLogFont; - info.families.emplace_back(FS2OTTD(lf.lfFaceName)); + + HDC dc = GetDC(nullptr); + EnumFontFamiliesEx(dc, &lf, (FONTENUMPROC)&ListStylesFontCallback, reinterpret_cast(&info), 0); + ReleaseDC(nullptr, dc); return 1; } -std::vector Win32FontSearcher::ListFamilies(const std::string &, int winlangid) +void Win32FontSearcher::UpdateCachedFonts(const std::string &, int winlangid) { - EFCListFamiliesParam info; + EFCListFontsParam info(this->cached_fonts); + this->cached_fonts.clear(); + if (GetLocaleInfo(MAKELCID(winlangid, SORT_DEFAULT), LOCALE_FONTSIGNATURE, reinterpret_cast(&info.locale), sizeof(info.locale) / sizeof(wchar_t)) == 0) { /* Invalid langid or some other mysterious error, can't determine fallback font. */ Debug(fontcache, 1, "Can't get locale info for fallback font (langid=0x{:x})", winlangid); - return info.families; + return; } LOGFONT lf{}; @@ -429,54 +445,6 @@ std::vector Win32FontSearcher::ListFamilies(const std::string &, in HDC dc = GetDC(nullptr); EnumFontFamiliesEx(dc, &lf, (FONTENUMPROC)&ListFamiliesFontCallback, reinterpret_cast(&info), 0); ReleaseDC(nullptr, dc); - - return info.families; -} - -/** - * State passed between EnumFontFamiliesEx and our list styles callback. - */ -struct EFCListStylesParam : EFCParam { - std::vector styles; ///< List of styles for the family. -}; - -static int CALLBACK ListStylesFontCallback(ENUMLOGFONTEX *lpelfe, NEWTEXTMETRICEX *metric, DWORD type, LPARAM lParam) -{ - EFCListStylesParam &info = *reinterpret_cast(lParam); - - /* Only use TrueType fonts */ - if (!(type & TRUETYPE_FONTTYPE)) return 1; - /* Skip duplicates */ - if (!info.Add(lpelfe->elfFullName)) return 1; - /* Don't use SYMBOL fonts */ - if (lpelfe->elfLogFont.lfCharSet == SYMBOL_CHARSET) return 1; - /* The font has to have at least one of the supported locales to be usable. */ - if ((metric->ntmFontSig.fsCsb[0] & info.locale.lsCsbSupported[0]) == 0 && (metric->ntmFontSig.fsCsb[1] & info.locale.lsCsbSupported[1]) == 0) return 1; - - LOGFONT &lf = lpelfe->elfLogFont; - info.styles.emplace_back(FS2OTTD(lf.lfFaceName), FS2OTTD(lpelfe->elfStyle), lf.lfItalic, lf.lfWeight); - - return 1; -} - -std::vector Win32FontSearcher::ListStyles(const std::string &, int winlangid, std::string_view font_family) -{ - EFCListStylesParam info; - if (GetLocaleInfo(MAKELCID(winlangid, SORT_DEFAULT), LOCALE_FONTSIGNATURE, reinterpret_cast(&info.locale), sizeof(info.locale) / sizeof(wchar_t)) == 0) { - /* Invalid langid or some other mysterious error, can't determine fallback font. */ - Debug(fontcache, 1, "Can't get locale info for fallback font (langid=0x{:x})", winlangid); - return info.styles; - } - - LOGFONT lf{}; - lf.lfCharSet = DEFAULT_CHARSET; - convert_to_fs(font_family, lf.lfFaceName, std::size(lf.lfFaceName)); - - HDC dc = GetDC(nullptr); - EnumFontFamiliesEx(dc, &lf, (FONTENUMPROC)&ListStylesFontCallback, reinterpret_cast(&info), 0); - ReleaseDC(nullptr, dc); - - return info.styles; } static Win32FontSearcher _win32fs_instance; From f8bd97a848504eb5cf6c7586eb740c3787905680 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sat, 15 Jun 2024 21:11:19 +0100 Subject: [PATCH 7/7] Change: Use combined list api --- src/console_cmds.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 48f44a264a..dac319a75e 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -2198,10 +2198,9 @@ DEF_CONSOLE_CMD(ConListFonts) if (argc == 1) { auto families = fs->ListFamilies(_current_language->isocode, _current_language->winlangid); - std::sort(std::begin(families), std::end(families)); int i = 0; - for (auto &family : families) { + for (const std::string_view &family : families) { IConsolePrint(CC_DEFAULT, "{}) {}", i, family); ++i; } @@ -2218,10 +2217,9 @@ DEF_CONSOLE_CMD(ConListFonts) } auto styles = fs->ListStyles(_current_language->isocode, _current_language->winlangid, family); - std::sort(std::begin(styles), std::end(styles), FontFamilySorter); int i = 0; - for (auto &font : styles) { + for (const FontFamily &font : styles) { IConsolePrint(CC_DEFAULT, "{}) {}, {}", i, font.family, font.style); i++; }