diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 296bbd019b..7de567f033 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" @@ -2225,6 +2228,52 @@ 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); + + int i = 0; + for (const std::string_view &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); + + int i = 0; + for (const FontFamily &font : styles) { + IConsolePrint(CC_DEFAULT, "{}) {}, {}", i, font.family, font.style); + i++; + } + } + + return true; +} + DEF_CONSOLE_CMD(ConFont) { if (argc == 0) { @@ -2807,6 +2856,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 639b23e4e5..0b9af32aa9 100644 --- a/src/fontcache.cpp +++ b/src/fontcache.cpp @@ -250,6 +250,67 @@ 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. + */ +FontSearcher::FontSearcher() +{ + FontSearcher::instance = this; +} + +/** + * Deregister this FontSearcher. + */ +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 2a316da505..4556f18f5d 100644 --- a/src/fontdetection.h +++ b/src/fontdetection.h @@ -39,4 +39,59 @@ 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) {} +}; + +bool FontFamilySorter(const FontFamily &a, const FontFamily &b); + +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; } + + /** + * 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. + */ + std::vector ListFamilies(const std::string &language_isocode, int winlangid); + + /** + * 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. + */ + 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; +}; + #endif diff --git a/src/os/macosx/font_osx.cpp b/src/os/macosx/font_osx.cpp index 5d8b79e1be..e17821a093 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,43 @@ void LoadCoreTextFont(FontSize fs) new CoreTextFontCache(fs, std::move(font_ref), GetFontCacheFontSize(fs)); } + +class CoreTextFontSearcher : public FontSearcher { +public: + void UpdateCachedFonts(const std::string &language_isocode, int winlangid) override; +}; + +void CoreTextFontSearcher::UpdateCachedFonts(const std::string &language_isocode, int) +{ + this->cached_fonts.clear(); + + 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); + + 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); + + this->cached_fonts.emplace_back(sv_family, sv_style, static_cast(slant * 100), static_cast(weight * 100)); + + return true; + }); + + 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 6ab1d99823..085be73b06 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,69 @@ bool SetFallbackFont(FontCacheSettings *settings, const std::string &language_is FcConfigDestroy(fc_instance); return ret; } + +/** + * FontConfig implementation of FontSearcher. + */ +class FontConfigFontSearcher : public FontSearcher { +public: + void UpdateCachedFonts(const std::string &language_isocode, int winlangid) override; +}; + +void FontConfigFontSearcher::UpdateCachedFonts(const std::string &language_isocode, int) +{ + this->cached_fonts.clear(); + + if (!FcInit()) return; + + 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); + 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) { + this->cached_fonts.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; + + /* 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. */ + FcFontSetDestroy(fs); + } + + FcConfigDestroy(fc_instance); + + 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 6f021c270a..2457acddcb 100644 --- a/src/os/windows/font_win32.cpp +++ b/src/os/windows/font_win32.cpp @@ -34,15 +34,14 @@ struct EFCParam { FontCacheSettings *settings; - LOCALESIGNATURE locale; + LOCALESIGNATURE locale; MissingGlyphSearcher *callback; std::vector fonts; 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,73 @@ void LoadWin32Font(FontSize fs) LoadWin32Font(fs, logfont, GetFontCacheFontSize(fs), font_name); } + +/** + * Win32 implementation of FontSearcher. + */ +class Win32FontSearcher : public FontSearcher { +public: + void UpdateCachedFonts(const std::string &language_isocode, int winlangid) override; +}; + +/** + * State passed between EnumFontFamiliesEx and our list fonts callbacks. + */ +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) +{ + EFCListFontsParam &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; + + HDC dc = GetDC(nullptr); + EnumFontFamiliesEx(dc, &lf, (FONTENUMPROC)&ListStylesFontCallback, reinterpret_cast(&info), 0); + ReleaseDC(nullptr, dc); + + return 1; +} + +void Win32FontSearcher::UpdateCachedFonts(const std::string &, int winlangid) +{ + 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; + } + + LOGFONT lf{}; + lf.lfCharSet = DEFAULT_CHARSET; + + HDC dc = GetDC(nullptr); + EnumFontFamiliesEx(dc, &lf, (FONTENUMPROC)&ListFamiliesFontCallback, reinterpret_cast(&info), 0); + ReleaseDC(nullptr, dc); +} + +static Win32FontSearcher _win32fs_instance;