diff --git a/CMakeLists.txt b/CMakeLists.txt index f34361dd5265..34d8f63b84b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -259,6 +259,7 @@ endif() if(NOT LIBRETRO AND NOT IOS AND NOT MACOSX) find_package(SDL2) find_package(SDL2_TTF) + find_package(Fontconfig) endif() if(MACOSX AND NOT IOS) @@ -1322,6 +1323,11 @@ else() if(SDL2_TTF_FOUND) add_definitions(-DUSE_SDL2_TTF) set(nativeExtraLibs ${nativeExtraLibs} SDL2_ttf::SDL2_ttf) + + if(FONTCONFIG_FOUND) + add_definitions(-DUSE_SDL2_TTF_FONTCONFIG) + set(nativeExtraLibs ${nativeExtraLibs} Fontconfig::Fontconfig) + endif() endif() if(APPLE) set(nativeExtra ${nativeExtra} diff --git a/Common/Render/Text/draw_text_sdl.cpp b/Common/Render/Text/draw_text_sdl.cpp index 9e228ebef75d..ac4760e9f658 100644 --- a/Common/Render/Text/draw_text_sdl.cpp +++ b/Common/Render/Text/draw_text_sdl.cpp @@ -1,8 +1,5 @@ #include "ppsspp_config.h" -#include -#include - #include "Common/Log.h" #include "Common/System/Display.h" #include "Common/GPU/thin3d.h" @@ -10,6 +7,8 @@ #include "Common/Data/Text/WrapText.h" #include "Common/Data/Encoding/Utf8.h" #include "Common/File/VFS/VFS.h" +#include "Common/File/FileUtil.h" +#include "Common/File/Path.h" #include "Common/Render/Text/draw_text.h" #include "Common/Render/Text/draw_text_sdl.h" @@ -18,19 +17,187 @@ #include "SDL2/SDL.h" #include "SDL2/SDL_ttf.h" -const uint32_t MAX_WIDTH = 4096; - TextDrawerSDL::TextDrawerSDL(Draw::DrawContext *draw): TextDrawer(draw) { if (TTF_Init() < 0) { ERROR_LOG(G3D, "Unable to initialize SDL2_ttf"); } dpiScale_ = CalculateDPIScale(); + +#if defined(USE_SDL2_TTF_FONTCONFIG) + config = FcInitLoadConfigAndFonts(); +#endif + + PrepareFallbackFonts(); } TextDrawerSDL::~TextDrawerSDL() { ClearCache(); TTF_Quit(); + +#if defined(USE_SDL2_TTF_FONTCONFIG) + FcConfigDestroy(config); + FcFini(); +#endif +} + +// If a user complains about missing characters on SDL, re-visit this! +void TextDrawerSDL::PrepareFallbackFonts() { +#if defined(USE_SDL2_TTF_FONTCONFIG) + FcObjectSet *os = FcObjectSetBuild (FC_FILE, FC_INDEX, (char *) 0); + + FcPattern *names[] = { + FcNameParse((const FcChar8 *) "Source Han Sans Medium"), + FcNameParse((const FcChar8 *) "Droid Sans Bold"), + FcNameParse((const FcChar8 *) "DejaVu Sans Condensed"), + FcNameParse((const FcChar8 *) "Noto Sans CJK Medium"), + FcNameParse((const FcChar8 *) "Noto Sans Hebrew Medium"), + FcNameParse((const FcChar8 *) "Noto Sans Lao Medium"), + FcNameParse((const FcChar8 *) "Noto Sans Thai Medium") + }; + + for (int i = 0; i < ARRAY_SIZE(names); i++) { + FcFontSet *foundFonts = FcFontList(config, names[i], os); + + for (int j = 0; foundFonts && j < foundFonts->nfont; ++j) { + FcPattern* font = foundFonts->fonts[j]; + FcChar8 *path; + int fontIndex; + + if (FcPatternGetInteger(font, FC_INDEX, 0, &fontIndex) != FcResultMatch) { + fontIndex = 0; // The 0th face is guaranteed to exist + } + + if (FcPatternGetString(font, FC_FILE, 0, &path) == FcResultMatch) { + std::string path_str((const char*)path); + fallbackFontPaths_.push_back(std::make_pair(path_str, fontIndex)); + } + } + + if (foundFonts) { + FcFontSetDestroy(foundFonts); + } + + FcPatternDestroy(names[i]); + } +#elif PPSSPP_PLATFORM(MAC) + const char *fontDirs[] = { + "/System/Library/Fonts/", + "/System/Library/Fonts/Supplemental/", + "/Library/Fonts/" + }; + + const char *fallbackFonts[] = { + "Hiragino Sans GB.ttc", + "PingFang.ttc", + "PingFang SC.ttc", + "PingFang TC.ttc", + "ヒラギノ角ゴシック W4.ttc", + "AppleGothic.ttf", + "Arial Unicode.ttf", + }; + + for (int i = 0; i < ARRAY_SIZE(fontDirs); i++) { + for (int j = 0; j < ARRAY_SIZE(fallbackFonts); j++) { + Path fontPath = Path(fontDirs[i]) / fallbackFonts[j]; + + if (File::Exists(fontPath)) { + TTF_Font *openedFont = TTF_OpenFont(fontPath.ToString().c_str(), 24); + int64_t numFaces = TTF_FontFaces(openedFont); + + for (int k = 0; k < numFaces; k++) { + TTF_Font *fontFace = TTF_OpenFontIndex(fontPath.ToString().c_str(), 24, k); + std::string fontFaceName(TTF_FontFaceStyleName(fontFace)); + TTF_CloseFont(fontFace); + + if (strstr(fontFaceName.c_str(), "Medium") || + strstr(fontFaceName.c_str(), "Regular")) + { + fallbackFontPaths_.push_back(std::make_pair(fontPath.ToString(), k)); + break; + } + } + + TTF_CloseFont(openedFont); + } + } + } +#else + // We don't have a fallback font for this platform. + // Unsupported characters will be rendered as squares. +#endif +} + +// Count leading set bits. For use in UTF-8 decoding. +int LeadingOnes(uint8_t byte) { + for (int i = 7; i >= 0; i--) { + if (!((byte >> i) & 1)) return 7 - i; + } + + return 8; +} + +uint32_t TextDrawerSDL::CheckMissingGlyph(std::string& text) { + TTF_Font *font = fontMap_.find(fontHash_)->second; + + uint32_t missingGlyph = 0; + const int length = text.length(); + for (int i = 0; i < length; i++) { + uint32_t glyph; // UTF-32 char + + // Decoding UTF-8 string into UTF-32 codepoints + if (LeadingOnes(text[i]) == 2 && (i + 1 < length)) { + glyph = text[i] & 0b00011111; + glyph = (glyph << 6) | (text[++i] & 0b00111111); + } else if (LeadingOnes(text[i]) == 3 && (i + 2 < length)) { + glyph = text[i] & 0b00001111; + glyph = (glyph << 6) | (text[++i] & 0b00111111); + glyph = (glyph << 6) | (text[++i] & 0b00111111); + } else if (LeadingOnes(text[i]) == 4 && (i + 3 < length)) { + glyph = text[i] & 0b00000111; + glyph = (glyph << 6) | (text[++i] & 0b00111111); + glyph = (glyph << 6) | (text[++i] & 0b00111111); + glyph = (glyph << 6) | (text[++i] & 0b00111111); + } else { + glyph = text[i]; + } + + if (!TTF_GlyphIsProvided32(font, glyph)) { + missingGlyph = glyph; + break; + } + } + + return missingGlyph; +} + +// If this returns true, the first font in fallbackFonts_ can be used as a fallback. +bool TextDrawerSDL::FindFallbackFonts(uint32_t missingGlyph, int ptSize) { + // If we encounter a missing glyph, try to use one of the fallback fonts. + for (int i = 0; i < fallbackFonts_.size(); i++) { + TTF_Font *fallbackFont = fallbackFonts_[i]; + if (TTF_GlyphIsProvided32(fallbackFont, missingGlyph)) { + fallbackFonts_.erase(fallbackFonts_.begin() + i); + fallbackFonts_.insert(fallbackFonts_.begin(), fallbackFont); + return true; + } + } + + // If none of the loaded fonts can handle it, load more fonts. + for (int i = 0; i < fallbackFontPaths_.size(); i++) { + std::string& fontPath = fallbackFontPaths_[i].first; + int faceIndex = fallbackFontPaths_[i].second; + + TTF_Font *font = TTF_OpenFontIndex(fontPath.c_str(), ptSize, faceIndex); + + if (TTF_GlyphIsProvided32(font, missingGlyph)) { + fallbackFonts_.insert(fallbackFonts_.begin(), font); + fallbackFontPaths_.erase(fallbackFontPaths_.begin() + i); + return true; + } + } + + return false; } uint32_t TextDrawerSDL::SetFont(const char *fontName, int size, int flags) { @@ -47,7 +214,7 @@ uint32_t TextDrawerSDL::SetFont(const char *fontName, int size, int flags) { const char *useFont = fontName ? fontName : "Roboto-Condensed.ttf"; const int ptSize = (int)((size + 6) / dpiScale_); - TTF_Font *font = TTF_OpenFont(useFont, ptSize); + TTF_Font *font = TTF_OpenFont(useFont, ptSize); if (!font) { File::FileInfo fileInfo; @@ -79,6 +246,13 @@ void TextDrawerSDL::MeasureString(const char *str, size_t len, float *w, float * entry = iter->second.get(); } else { TTF_Font *font = fontMap_.find(fontHash_)->second; + int ptSize = TTF_FontHeight(font) / 1.35; + + uint32_t missingGlyph = CheckMissingGlyph(key.text); + + if (missingGlyph && FindFallbackFonts(missingGlyph, ptSize)) { + font = fallbackFonts_[0]; + } int width = 0; int height = 0; @@ -103,7 +277,14 @@ void TextDrawerSDL::MeasureStringRect(const char *str, size_t len, const Bounds WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w, wrap); } - TTF_Font* font = fontMap_.find(fontHash_)->second; + TTF_Font *font = fontMap_.find(fontHash_)->second; + int ptSize = TTF_FontHeight(font) / 1.35; + + uint32_t missingGlyph = CheckMissingGlyph(toMeasure); + + if (missingGlyph && FindFallbackFonts(missingGlyph, ptSize)) { + font = fallbackFonts_[0]; + } int width = 0; int height = 0; @@ -160,16 +341,35 @@ void TextDrawerSDL::DrawString(DrawBuffer &target, const char *str, float x, flo target.Flush(true); } } + void TextDrawerSDL::DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, int align) { if (!strlen(str)) { bitmapData.clear(); return; } - std::string processed_str(str); - processed_str = std::regex_replace(processed_str, std::regex("&&"), "&"); + // Replace "&&" with "&" + std::string processedStr(str); + int index = 0; + while (true) { + index = processedStr.find("&&"); + + if (index != std::string::npos) { + processedStr = processedStr.replace(index, 2, "&"); + index += 2; + } else { + break; + } + } TTF_Font *font = fontMap_.find(fontHash_)->second; + int ptSize = TTF_FontHeight(font) / 1.35; + + uint32_t missingGlyph = CheckMissingGlyph(processedStr); + + if (missingGlyph && FindFallbackFonts(missingGlyph, ptSize)) { + font = fallbackFonts_[0]; + } if (align & ALIGN_HCENTER) TTF_SetFontWrappedAlign(font, TTF_WRAPPED_ALIGN_CENTER); @@ -179,7 +379,7 @@ void TextDrawerSDL::DrawStringBitmap(std::vector &bitmapData, TextStrin TTF_SetFontWrappedAlign(font, TTF_WRAPPED_ALIGN_RIGHT); SDL_Color fgColor = { 0xFF, 0xFF, 0xFF, 0xFF }; - SDL_Surface *text = TTF_RenderUTF8_Blended(font, processed_str.c_str(), fgColor); + SDL_Surface *text = TTF_RenderUTF8_Blended_Wrapped(font, processedStr.c_str(), fgColor, 0); SDL_LockSurface(text); entry.texture = nullptr; @@ -187,7 +387,7 @@ void TextDrawerSDL::DrawStringBitmap(std::vector &bitmapData, TextStrin entry.bmHeight = entry.height = text->h; entry.lastUsedFrame = frameCount_; - uint32_t *imageData = (uint32_t *) text->pixels; + uint32_t *imageData = (uint32_t *)text->pixels; if (texFormat == Draw::DataFormat::B4G4R4A4_UNORM_PACK16 || texFormat == Draw::DataFormat::R4G4B4A4_UNORM_PACK16) { bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint16_t)); @@ -196,15 +396,15 @@ void TextDrawerSDL::DrawStringBitmap(std::vector &bitmapData, TextStrin for (int x = 0; x < entry.bmWidth; x++) { for (int y = 0; y < entry.bmHeight; y++) { uint64_t index = entry.bmWidth * y + x; - bitmapData16[entry.bmWidth * y + x] = 0xfff0 | (imageData[index] >> 28); + bitmapData16[index] = 0xfff0 | (imageData[index] >> 28); } } } else if (texFormat == Draw::DataFormat::R8_UNORM) { bitmapData.resize(entry.bmWidth * entry.bmHeight); for (int x = 0; x < entry.bmWidth; x++) { for (int y = 0; y < entry.bmHeight; y++) { - uint64_t index = text->pitch / sizeof(uint32_t) * y + x; - bitmapData[entry.bmWidth * y + x] = imageData[index] >> 24; + uint64_t index = entry.bmWidth * y + x; + bitmapData[index] = imageData[index] >> 24; } } } else { @@ -256,7 +456,11 @@ void TextDrawerSDL::ClearCache() { for (auto iter : fontMap_) { TTF_CloseFont(iter.second); } + for (auto iter : fallbackFonts_) { + TTF_CloseFont(iter); + } fontMap_.clear(); + fallbackFonts_.clear(); fontHash_ = 0; } diff --git a/Common/Render/Text/draw_text_sdl.h b/Common/Render/Text/draw_text_sdl.h index b86c96acb6df..7c047ee48c41 100644 --- a/Common/Render/Text/draw_text_sdl.h +++ b/Common/Render/Text/draw_text_sdl.h @@ -5,6 +5,10 @@ #include #include "Common/Render/Text/draw_text.h" +#if defined(USE_SDL2_TTF_FONTCONFIG) +#include +#endif + // SDL2_ttf's TTF_Font is a typedef of _TTF_Font. struct _TTF_Font; @@ -24,10 +28,20 @@ class TextDrawerSDL : public TextDrawer { protected: void ClearCache() override; + void PrepareFallbackFonts(); + uint32_t CheckMissingGlyph(std::string& text); + bool FindFallbackFonts(uint32_t missingGlyph, int ptSize); uint32_t fontHash_; std::map fontMap_; std::map> cache_; std::map> sizeCache_; + + std::vector<_TTF_Font *> fallbackFonts_; + std::vector> fallbackFontPaths_; // path and font face index + +#if defined(USE_SDL2_TTF_FONTCONFIG) + FcConfig *config; +#endif };