diff --git a/.gitmodules b/.gitmodules index d2420043fd..4e6da7d89f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "YRpp"] path = YRpp url = https://github.com/Phobos-developers/YRpp.git +[submodule "hbftfr"] + path = hbftfr + url = https://github.com/G-LimeJuice/hbftfr.git diff --git a/Phobos.vcxproj b/Phobos.vcxproj index e1aa9e9c96..92f2831b85 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -32,6 +32,8 @@ + + @@ -225,6 +227,7 @@ + @@ -502,7 +505,7 @@ true Caret false - DEBUG;SYR_VER=2;HAS_EXCEPTIONS=0;NOMINMAX;_CRT_SECURE_NO_WARNINGS;_WIN32_WINNT=0x0601;NTDDI_VERSION=0x06010000;%(PreprocessorDefinitions);%(AdditionalDefinitions);PHOBOS_DLL="$(ProjectName).dll" + DEBUG;SYR_VER=2;HAS_EXCEPTIONS=0;NOMINMAX;_CRT_SECURE_NO_WARNINGS;_WIN32_WINNT=0x0601;NTDDI_VERSION=0x06010000;%(PreprocessorDefinitions);%(AdditionalDefinitions);PHOBOS_DLL="$(ProjectName).dll";HAV_HB_STATIC true false MultiThreaded @@ -521,15 +524,20 @@ true true true + $(ProjectDir)hbftfr\include;$(ProjectDir)hbftfr\lib;%(AdditionalIncludeDirectories) true true - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;dbghelp.lib;onecore.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;dbghelp.lib;onecore.lib;harfbuzz.lib; +fribidi.lib;freetype.lib; +bz2.lib; +zs.lib;libpng16.lib;brotlidec.lib;brotlienc.lib;brotlicommon.lib;%(AdditionalDependencies) Windows $(OutDir)$(TargetName).pdb $(IntDir)$(TargetName).pgd $(IntDir)$(TargetName).lib + $(ProjectDir)hbftfr\lib;$(ProjectDir)hbftfr\include;%(AdditionalLibraryDirectories) diff --git a/hbftfr b/hbftfr new file mode 160000 index 0000000000..f639a47423 --- /dev/null +++ b/hbftfr @@ -0,0 +1 @@ +Subproject commit f639a47423223f30ef9384a8637027329de50e38 diff --git a/src/TextRenderer/Hooks.cpp b/src/TextRenderer/Hooks.cpp new file mode 100644 index 0000000000..aafe3cccaf --- /dev/null +++ b/src/TextRenderer/Hooks.cpp @@ -0,0 +1,102 @@ + +#include +#include +#include "TextRenderer.h" +#include +#include +#include +#include +#include +#include + +static bool IsTTFEnabled() +{ + static CCINIClass ini_uimd; + ini_uimd.LoadFromFile(GameStrings::UIMD_INI); + return ini_uimd.ReadBool("Render", "EnableTTF", false); +} +// Best fix Unicode +DEFINE_HOOK(0x5D3BA0, sub_433F50, 7) +{ + + GET_STACK(const wchar_t*, pText, 0xC); + std::wstring wtext = TextRenderer::FixUtf8InWchar(pText); + R->Stack(0xC, wtext.c_str()); + + return 0; + +} + +// 43393A +DEFINE_HOOK(0x433880, BitFont_CTOR, 8) +{ + + if (IsTTFEnabled()) + return 0; + GET(BitFont*, pFont, ECX); + CCINIClass ini_uimd {}; + ini_uimd.LoadFromFile(GameStrings::UIMD_INI); + ini_uimd.ReadString("Font", "FileName", "default.ttf", Phobos::readBuffer); + std::string fontPath = std::string("Fonts\\") + Phobos::readBuffer; + pFont = TextRenderer::BitFont_CTOR_(pFont, fontPath.c_str()); + + return pFont ? 0x43393A : 0; +} + + +DEFINE_HOOK(0x433CF0, BitFont_GetTextDimension, 8) +{ + if (IsTTFEnabled()) + return 0; + GET(BitFont*, pFont, ECX); + GET_STACK(const wchar_t*, pText, 0x4); + GET_STACK(int*, pWidth, 0x8); + GET_STACK(int*, pHeight, 0xC); + GET_STACK(int, nMaxWidth, 0x10); + std::wstring arabicShaped; + + + R->EAX((DWORD)TextRenderer::BitFont_GetTextDimension_(pFont, pText, pWidth, pHeight, nMaxWidth)); + + return 0x433EA2; +} +DEFINE_HOOK(0x434CD0, BitText_DrawText, 10) +{ + if (IsTTFEnabled()) + return 0; + GET_STACK(BitFont*, pFont, 0x4); + GET_STACK(Surface*, pSurface, 0x8); + GET_STACK(const wchar_t*, pWideString, 0xC); + GET_STACK(int, X, 0x10); + GET_STACK(int, Y, 0x14); + GET_STACK(int, W, 0x18); + GET_STACK(int, H, 0x1C); + GET_STACK(int, a8, 0x20); + GET_STACK(int, a9, 0x24); + GET_STACK(int, nColorAdjust, 0x28); + + bool handled = TextRenderer::BitText_DrawText_(pFont, pSurface, pWideString, X, Y, W, H, a8, a9, nColorAdjust); + + return handled ? 0x435310 : 0; +} + +DEFINE_HOOK(0x434500, sub_434500, 7) +{ + if (IsTTFEnabled()) + return 0; + GET(BitFont*, pFont, ECX); + GET_STACK(wchar_t*, pText, 0x4); + GET_STACK(int, xLeft, 0x8); + GET_STACK(int, yTop, 0xC); + GET_STACK(int, charCount, 0x10); + GET_STACK(int, nColorAdjust, 0x14); + + if (!TextRenderer::GetFTFace(pFont)) + return 0; + + R->EAX(TextRenderer::BitFont_434500_(pFont, pText, xLeft, yTop, charCount, nColorAdjust)); + return 0x4346B4; +} + + + diff --git a/src/TextRenderer/TextRenderer.cpp b/src/TextRenderer/TextRenderer.cpp new file mode 100644 index 0000000000..eeef75cbdf --- /dev/null +++ b/src/TextRenderer/TextRenderer.cpp @@ -0,0 +1,1359 @@ +#include "TextRenderer.h" + +namespace TextRenderer +{ + static const wchar_t LATIN_ASC_TEST[] = L"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890$"; + static const wchar_t LATIN_DESC_TEST[] = L"gjpqyQ"; + static const wchar_t ARABIC_ASC_TEST[] = L"أبتثجحخدذرزسشصضطظعغفقكلمنهوي"; + static const wchar_t ARABIC_DESC_TEST[] = L"جحخصضطظعغ"; + static const wchar_t ARABIC_DIACRITICS[] = L"ًٌٍَُِّْ"; + + // FreeType faces + // Latin/default rendering face + std::unordered_map gFTFaceMap; + + // Arabic rendering face (different metrics/fallback) + std::unordered_map gFTFaceArabicMap; + + // HarfBuzz fonts + // Latin/default shaping font + std::unordered_map gHbFontMap; + + // Arabic shaping font (GSUB/GPOS for Arabic) + std::unordered_map gHbFontArabicMap; + + // Metrics (measured asc/desc) + // Latin ascender / descender + std::unordered_map gAscenderMap; + std::unordered_map gDescenderMap; + + // Arabic ascender / descender + std::unordered_map gAscenderArabicMap; + std::unordered_map gDescenderArabicMap; + + hb_font_t* GetHbFont(BitFont* pFont) + { + auto it = gHbFontMap.find(pFont); + return it != gHbFontMap.end() ? it->second : nullptr; + } + + std::wstring FixUtf8InWchar(const wchar_t* ws) + { + if (!ws) return L""; + + bool isRTL = IsRTLText(ws, (int)wcslen(ws)); + if (isRTL) + return std::wstring(ws); + + std::string utf8; + while (*ws) + { + utf8.push_back(static_cast(*ws)); + ++ws; + } + if (utf8.empty()) + return L""; + + int len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0); + if (len <= 0) + return L""; + + std::wstring result(len - 1, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, result.data(), len); + + return result; + } + + int MeasureRealAscender(FT_Face face, const wchar_t* testSet) + { + if (!face || !testSet) return 0; + + int maxTop = 0; + + for (int i = 0; testSet[i]; i++) + { + FT_UInt idx = FT_Get_Char_Index(face, testSet[i]); + if (!idx) continue; + + if (FT_Load_Glyph(face, idx, FT_LOAD_DEFAULT) != 0) + continue; + + int top = face->glyph->metrics.horiBearingY >> 6; + + if (top > maxTop) + maxTop = top; + } + + return maxTop > 0 ? maxTop : (int)(face->size->metrics.ascender >> 6); + } + + int MeasureRealDescender(FT_Face face, const wchar_t* testSet) + { + if (!face || !testSet) return 0; + + int maxBelow = 0; + + for (int i = 0; testSet[i]; i++) + { + FT_UInt idx = FT_Get_Char_Index(face, testSet[i]); + if (!idx) continue; + + if (FT_Load_Glyph(face, idx, FT_LOAD_DEFAULT) != 0) + continue; + + FT_GlyphSlot slot = face->glyph; + + int top = slot->metrics.horiBearingY >> 6; + int height = slot->metrics.height >> 6; + + int below = height - top; + + if (below > maxBelow) + maxBelow = below; + } + + return maxBelow > 0 ? -maxBelow : (int)(face->size->metrics.descender >> 6); + } + + // Select correct FreeType face for rendering (Arabic vs Latin) + FT_Face GetFTFaceForText(BitFont* pFont, bool isRTL) + { + if (!pFont) return nullptr; + + if (isRTL) + { + auto it = gFTFaceArabicMap.find(pFont); + if (it != gFTFaceArabicMap.end()) + return it->second; + } + + return GetFTFace(pFont); + } + + // Select correct HarfBuzz font for shaping (Arabic vs Latin) + hb_font_t* GetHbFontForText(BitFont* pFont, bool isRTL) + { + if (isRTL) + { + auto it = gHbFontArabicMap.find(pFont); + if (it != gHbFontArabicMap.end()) + return it->second; + } + + return GetHbFont(pFont); + } + + // Returns the primary FreeType face used for rendering Latin/default text. + // This is the base FT_Face (non‑Arabic). Arabic uses GetFTFaceForText() instead. + FT_Face GetFTFace(BitFont* pFont) + { + auto it = gFTFaceMap.find(pFont); + return it != gFTFaceMap.end() ? it->second : nullptr; + } + // RTL Arabic Text + bool IsRTLText(const wchar_t* text, int len) + { + for (int i = 0; i < len; i++) + { + wchar_t ch = text[i]; + if ((ch >= 0x0600 && ch <= 0x06FF) || // Arabic + (ch >= 0x0750 && ch <= 0x077F) || // Arabic Supplement + (ch >= 0x08A0 && ch <= 0x08FF) || // Arabic Extended-A + (ch >= 0xFB50 && ch <= 0xFDFF) || // Arabic Presentation Forms-A + (ch >= 0xFE70 && ch <= 0xFEFF)) // Arabic Presentation Forms-B + return true; + } + return false; + } + + const wchar_t* FindLineEnd(BitFont* pFont, const wchar_t* start, int nMaxWidth) + { + if (!pFont || !start || !*start) + return start; + + // No wrapping → stop only at newline + if (nMaxWidth <= 0) + { + const wchar_t* p = start; + while (*p && *p != L'\n' && *p != L'\r') + ++p; + return p; + } + + // Count logical characters until newline + int len = 0; + while (start[len] && start[len] != L'\n' && start[len] != L'\r') + ++len; + + if (len == 0) + return start; + + // Shape entire logical segment once (cluster‑safe) + auto shaped = ShapeText(pFont, start, len); + if (shaped.empty()) + return start + len; + + // Per‑codepoint width map + std::vector charWidth(len, 0); + std::vector isTab(len, false); + + const int tabSize = (pFont->Unknown_28 > 0) ? pFont->Unknown_28 : 64; + + // Accumulate cluster advances + for (const auto& g : shaped) + { + int idx = (int)g.cluster; + if (idx < 0 || idx >= len) + continue; + + wchar_t ch = start[idx]; + + if (ch == L'\t') + { + isTab[idx] = true; + } + else if (ch == 0x200C || ch == 0x200D || ch == 0x200B) // ZWNJ, ZWJ, ZWSP + { + charWidth[idx] = 0; + } + else + { + charWidth[idx] += g.x_advance; + } + } + + // Helpers + auto isHigh = [](wchar_t c) { return (c >= 0xD800 && c <= 0xDBFF); }; + auto isLow = [](wchar_t c) { return (c >= 0xDC00 && c <= 0xDFFF); }; + + auto isBreakable = [](wchar_t ch) + { + switch (ch) + { + case L' ': case L'\t': case L'-': case 0x00AD: case 0x200B: + return true; + case 0x00A0: case 0x2011: case 0x2060: case 0xFEFF: + return false; + } + return (ch <= 0x20 || (ch >= 0x2000 && ch <= 0x200A)); + }; + + auto script = [](wchar_t ch) + { + // Arabic family + if ((ch >= 0x0600 && ch <= 0x06FF) || (ch >= 0x0750 && ch <= 0x077F) || (ch >= 0x08A0 && ch <= 0x08FF) || + (ch >= 0xFB50 && ch <= 0xFDFF) || (ch >= 0xFE70 && ch <= 0xFEFF)) + return 1; + + // Hebrew + if (ch >= 0x0590 && ch <= 0x05FF) + return 2; + + // Latin + if ((ch >= L'A' && ch <= L'Z') || (ch >= L'a' && ch <= L'z') || + (ch >= 0x00C0 && ch <= 0x00FF) || (ch >= 0x0100 && ch <= 0x017F)) + return 3; + + // Cyrillic + if ((ch >= 0x0400 && ch <= 0x04FF) || (ch >= 0x0500 && ch <= 0x052F)) + return 4; + + // Greek + if ((ch >= 0x0370 && ch <= 0x03FF) || (ch >= 0x1F00 && ch <= 0x1FFF)) + return 5; + + // CJK + if ((ch >= 0x4E00 && ch <= 0x9FFF) || (ch >= 0x3400 && ch <= 0x4DBF) || + (ch >= 0x20000 && ch <= 0x2A6DF) || (ch >= 0xF900 && ch <= 0xFAFF) || + (ch >= 0x3040 && ch <= 0x309F) || (ch >= 0x30A0 && ch <= 0x30FF) || + (ch >= 0xAC00 && ch <= 0xD7AF)) + return 6; + + return 0; + }; + + // Main scan + int currentW = 0; + int lastBreakIdx = -1; + + for (int i = 0; i < len; i++) + { + wchar_t ch = start[i]; + + // Skip isolated low surrogate + if (isLow(ch)) + continue; + + // Resolve advance + int adv = charWidth[i]; + + if (isTab[i]) + { + adv = tabSize - (currentW % tabSize); + if (adv == 0) adv = tabSize; + } + + // Surrogate pair: merge width of low surrogate + if (isHigh(ch) && i + 1 < len && isLow(start[i + 1])) + adv += charWidth[i + 1]; + + // Wrap BEFORE adding this character + if (currentW + adv > nMaxWidth) + { + if (lastBreakIdx >= 0) + { + // Avoid splitting surrogate pair + if (isHigh(start[lastBreakIdx]) && + lastBreakIdx + 1 < len && + isLow(start[lastBreakIdx + 1])) + { + return start + lastBreakIdx + 2; + } + return start + lastBreakIdx + 1; + } + + // Hard break + if (i > 0) + { + if (isLow(ch) && isHigh(start[i - 1])) + return start + i - 1; + return start + i; + } + + // Single wide character + if (isHigh(ch) && i + 1 < len && isLow(start[i + 1])) + return start + i + 2; + return start + i + 1; + } + + currentW += adv; + + // Skip low surrogate (already merged) + if (isHigh(ch) && i + 1 < len && isLow(start[i + 1])) + continue; + + // Standard break opportunities + if (isBreakable(ch)) + lastBreakIdx = i; + + // Script boundary break (Arabic/Hebrew excluded) + int sPrev = (i > 0) ? script(start[i - 1]) : 0; + int sCurr = script(ch); + + if (i > 0 && + sPrev != 0 && sCurr != 0 && + sPrev != sCurr && + sPrev != 1 && sCurr != 1 && // Arabic + sPrev != 2 && sCurr != 2) // Hebrew + { + lastBreakIdx = i - 1; + } + + // CJK: break after any char except forbidden starts + if (sCurr == 6 && i > 0) + { + wchar_t next = (i + 1 < len) ? start[i + 1] : 0; + bool forbidden = (next == 0x3002 || // 。 Ideographic full stop + next == 0x3001 || // 、 Ideographic comma + next == 0x300B || // 》 Right angle bracket + next == 0x300D || // 」 Right corner bracket + next == 0x3011 || // 』 Right black lenticular bracket + next == 0x3009); // 〉 Right angle bracket + + if (!forbidden) + lastBreakIdx = i; + } + } + + // Entire line fits + return start + len; + } + // Measures the visual width of a shaped line [lineStart, lineEnd) + // Uses HarfBuzz clusters so multi‑glyph clusters (Arabic, ligatures) are handled correctly. + int MeasureLineWidth(BitFont* pFont, const wchar_t* lineStart, const wchar_t* lineEnd, int tabSize) + { + // Shape the logical substring into glyphs (HB handles bidi + clusters) + auto glyphs = ShapeText(pFont, lineStart, lineEnd - lineStart); + if (glyphs.empty()) + return 0; + + int width = 0; + + // Walk shaped glyphs in visual order + for (size_t i = 0; i < glyphs.size(); i++) + { + const auto& g = glyphs[i]; + + // Map glyph → original logical codepoint index + wchar_t ch = lineStart[g.cluster]; + + // Tabs: snap to next tab stop (same logic used in rendering) + if (ch == L'\t') + { + // advance = distance to next tab boundary + width += tabSize - (width % tabSize); + } + else + { + // Normal glyph: use HarfBuzz x_advance (already in 26.6 → pixels) + width += g.x_advance; + } + } + + return width; + } + + hb_codepoint_t GetFallbackGlyph(hb_font_t* hbFont, bool isRTL) + { + if (!hbFont) return 0; // .notdef + + hb_codepoint_t glyph = 0; + + // Universal: replacement character U+FFFD � + if (hb_font_get_nominal_glyph(hbFont, 0xFFFD, &glyph)) + return glyph; + + // White square — visible, script-neutral placeholder + if (hb_font_get_nominal_glyph(hbFont, 0x25A1, &glyph)) + return glyph; + + // Script-specific fallback + if (isRTL) + { + if (hb_font_get_nominal_glyph(hbFont, 0x061F, &glyph)) return glyph; // ؟ Arabic question mark + } + + // 4) Latin '?' — almost every font has this + if (hb_font_get_nominal_glyph(hbFont, L'?', &glyph)) + return glyph; + + // 5) .notdef — always exists at index 0 + return 0; + } + std::vector ShapeText(BitFont* pFont, const wchar_t* text, int len) + { + std::vector result; + if (!pFont || !text || len <= 0) + return result; + + // FriBidi: logical to visual, levels, maps + std::vector logical(len); + std::vector visual(len); + std::vector lToV(len); // logical to visual + std::vector levels(len); + + for (int i = 0; i < len; ++i) + logical[i] = (FriBidiChar)(unsigned short)text[i]; + + FriBidiParType baseDir = FRIBIDI_PAR_ON; + + FriBidiLevel maxLevel = fribidi_log2vis(logical.data(), (FriBidiStrIndex)len, &baseDir, visual.data(), lToV.data(), nullptr, levels.data()); + + // build visual → logical map + std::vector vToL(len); + for (int li = 0; li < len; ++li) + vToL[lToV[li]] = li; + + // levels in visual order + std::vector visualLevels(len); + for (int li = 0; li < len; ++li) + visualLevels[lToV[li]] = levels[li]; + + // Split into runs by contiguous visual level + + + std::vector runs; + + if (maxLevel == 0) + { + bool isArabic = IsRTLText(text, len); + runs.push_back({ 0, len, false, isArabic, 0, len }); + } + else + { + int vi = 0; + while (vi < len) + { + int vj = vi + 1; + while (vj < len && visualLevels[vj] == visualLevels[vi]) + ++vj; + + bool isRTL = (visualLevels[vi] & 1) != 0; + + int minL = vToL[vi]; + int maxL = vToL[vi]; + for (int v = vi + 1; v < vj; ++v) + { + int li = vToL[v]; + if (li < minL) minL = li; + if (li > maxL) maxL = li; + } + + int logStart = minL; + int logLen = maxL - minL + 1; + + bool isArabic = IsRTLText(text + logStart, logLen); + + runs.push_back({ logStart, logLen, isRTL, isArabic, vi, vj }); + vi = vj; + } + } + + // Minimal Fix Helper + auto AddRunToHB = [&](hb_buffer_t* buf, const Run& run) + { + for (int logical = run.start; logical < run.start + run.length; logical++) + { + bool belongs = false; + + for (int v = run.vi; v < run.vj; v++) + { + if (vToL[v] == logical) + { + belongs = true; + break; + } + } + if (!belongs) + continue; + + hb_buffer_add_utf16(buf, (const uint16_t*)text, len, logical, 1); + } + }; + + // Shape each run with HarfBuzz + hb_feature_t features[] = { + { HB_TAG('r','l','i','g'), 1, 0, (unsigned)-1 }, + { HB_TAG('c','c','m','p'), 1, 0, (unsigned)-1 }, + { HB_TAG('l','o','c','l'), 1, 0, (unsigned)-1 }, + { HB_TAG('m','a','r','k'), 1, 0, (unsigned)-1 }, + { HB_TAG('m','k','m','k'), 1, 0, (unsigned)-1 }, + { HB_TAG('c','u','r','s'), 1, 0, (unsigned)-1 }, + { HB_TAG('k','e','r','n'), 1, 0, (unsigned)-1 }, + { HB_TAG('l','i','g','a'), 1, 0, (unsigned)-1 }, + }; + const unsigned featureCount = sizeof(features) / sizeof(features[0]); + + for (const auto& run : runs) + { + hb_font_t* hbFont = GetHbFontForText(pFont, run.isArabic); + if (!hbFont) + continue; + + FT_Face face = GetFTFaceForText(pFont, run.isArabic); + if (face) + hb_ft_font_changed(hbFont); + + hb_buffer_t* buf = hb_buffer_create(); + if (!buf) + continue; + hb_buffer_set_unicode_funcs(buf, hb_unicode_funcs_get_default()); + hb_buffer_set_cluster_level(buf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); + + // Minimal Fix: add only characters that belong to this visual run + AddRunToHB(buf, run); + + if (run.isRTL) + { + hb_buffer_set_direction(buf, HB_DIRECTION_RTL); + hb_buffer_set_script(buf, HB_SCRIPT_ARABIC); + hb_buffer_set_language(buf, hb_language_from_string("ar", -1)); + } + else + { + hb_buffer_guess_segment_properties(buf); + } + + hb_shape(hbFont, buf, features, featureCount); + + unsigned int count = 0; + hb_glyph_info_t* info = hb_buffer_get_glyph_infos(buf, &count); + hb_glyph_position_t* pos = hb_buffer_get_glyph_positions(buf, &count); + hb_codepoint_t fallback = GetFallbackGlyph(hbFont, run.isArabic); + + for (unsigned i = 0; i < count; ++i) + { + ShapedGlyph g {}; + g.glyphId = info[i].codepoint; + g.x_advance = pos[i].x_advance >> 6; + g.x_offset = pos[i].x_offset >> 6; + g.y_offset = pos[i].y_offset >> 6; + g.cluster = info[i].cluster; + + wchar_t ch = L'?'; + if (g.cluster < (uint32_t)len) + ch = text[g.cluster]; + + g.ch = ch; + g.isSpace = (ch == L' ' || ch == 0x00A0 || ch == L'\t'); + g.isRTL = run.isRTL; + + if (g.glyphId == 0) + { + if (fallback != 0) + g.glyphId = fallback; + else + continue; + } + + result.push_back(g); + } + + hb_buffer_destroy(buf); + } + + return result; + } + void BitFont_434700(BitFont*) + { + JMP_STD(0x434700); + } + BitFont* BitFont_CTOR_(BitFont* pFont, const char* pFileName) + { + CCINIClass ini_uimd {}; + ini_uimd.LoadFromFile(GameStrings::UIMD_INI); + // Basic defaults + pFont->InternalPTR = nullptr; + pFont->Pointer_8 = nullptr; + pFont->pGraphBuffer = (short*)1; + pFont->field_1C = 1; + pFont->Unknown_14 = 0; + pFont->field_18 = nullptr; + pFont->field_20 = 0; + pFont->Color = 0x7FFF; + pFont->DefaultColor2 = 0x3555; + pFont->Unknown_28 = 64; + pFont->State_2C = 0; + pFont->Bounds = { 0, 0, 0, 0 }; + pFont->Bool_40 = true; + pFont->field_41 = true; + pFont->field_42 = false; + pFont->field_43 = false; + + // Load internal font data + + if (!pFont->InternalPTR) + pFont->InternalPTR = LoadTTFAsInternalData(pFileName); + + pFont->field_18 = (wchar_t*)(intptr_t)pFont->InternalPTR->Stride; + pFont->field_1C = pFont->InternalPTR->Lines; + + // Read config + int latinSize = ini_uimd.ReadInteger("FontSize", "LatinSize", 0); + int arabicSize = ini_uimd.ReadInteger("FontSize", "ArabicSize", 0); + int targetH = pFont->InternalPTR->FontHeight; + if (targetH <= 0) targetH = 14; + if (latinSize <= 0) latinSize = targetH; + if (arabicSize <= 0) arabicSize = latinSize; + // Init FreeType + FT_Library gFTLibrary = nullptr; + if (!gFTLibrary) + { + if (FT_Init_FreeType(&gFTLibrary) != 0) + { + pFont->State_2C = 1; + return pFont; + } + } + + // Create a single FT_Face + FT_Face ftFace = nullptr; + if (FT_New_Face(gFTLibrary, pFileName, 0, &ftFace) != 0 || !ftFace) + { + pFont->State_2C = 1; + return pFont; + } + + // Set FT pixel size ONCE + int basePixelSize = std::max(latinSize, arabicSize); + if (basePixelSize <= 0) basePixelSize = 14; + + if (FT_Set_Pixel_Sizes(ftFace, 0, basePixelSize) != 0) + { + FT_Done_Face(ftFace); + pFont->State_2C = 1; + return pFont; + } + + // Measure REAL Latin + Arabic metrics + int latinAsc = MeasureRealAscender(ftFace, LATIN_ASC_TEST) + 2; // fixed + int latinDesc = MeasureRealDescender(ftFace, LATIN_DESC_TEST); + + int arabicAsc = MeasureRealAscender(ftFace, ARABIC_ASC_TEST); + int arabicDesc = MeasureRealDescender(ftFace, ARABIC_DESC_TEST); + + // Store per-script metrics + gAscenderMap[pFont] = latinAsc; + gDescenderMap[pFont] = latinDesc; + + gAscenderArabicMap[pFont] = arabicAsc; + gDescenderArabicMap[pFont] = arabicDesc; + + gFTFaceMap[pFont] = ftFace; + gFTFaceArabicMap[pFont] = ftFace; + + int maxAdv = (int)(ftFace->size->metrics.max_advance >> 6); + + // Create HarfBuzz fonts + hb_font_t* hbLatin = hb_ft_font_create_referenced(ftFace); + hb_ft_font_set_load_flags(hbLatin, FT_LOAD_DEFAULT | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP | FT_LOAD_IGNORE_TRANSFORM); + hb_font_t* hbArabic = hb_ft_font_create_referenced(ftFace); + hb_ft_font_set_load_flags(hbArabic, FT_LOAD_DEFAULT | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP | FT_LOAD_IGNORE_TRANSFORM); + + int baseX = 0, baseY = 0; + hb_font_get_scale(hbLatin, &baseX, &baseY); + if (baseX == 0 || baseY == 0) + { + int upem = ftFace->units_per_EM ? ftFace->units_per_EM : 2048; + baseX = baseY = upem; + } + + float latinRatio = (float)latinSize / (float)basePixelSize; + float arabicRatio = (float)arabicSize / (float)basePixelSize; + + hb_font_set_scale(hbLatin, (int)(baseX * latinRatio), (int)(baseY * latinRatio)); + hb_font_set_scale(hbArabic, (int)(baseX * arabicRatio), (int)(baseY * arabicRatio)); + + gHbFontMap[pFont] = hbLatin; + gHbFontArabicMap[pFont] = hbArabic; + + // Unified line metrics + FontLineMetrics m; + m.ascender = std::max(latinAsc, arabicAsc); + m.descender = std::min(latinDesc, arabicDesc); + m.height = m.ascender - m.descender; + + pFont->InternalPTR->FontHeight = m.height; + pFont->InternalPTR->Lines = m.height; + pFont->InternalPTR->FontWidth = maxAdv; + pFont->InternalPTR->Stride = maxAdv; + + pFont->field_18 = (wchar_t*)(intptr_t)maxAdv; + pFont->field_1C = m.height; + + // Original post-init + BitFont_434700(pFont); + return pFont; + } + + // Measures total width/height of a multi-line UTF‑16 string using shaping + wrapping. + // Uses FindLineEnd() for wrapping and MeasureLineWidth() for per-line width. + bool BitFont_GetTextDimension_(BitFont* pFont, const wchar_t* pText, int* pWidth, int* pHeight, int nMaxWidth) + { + if (pWidth) *pWidth = 0; + if (pHeight) *pHeight = 0; + + // Validate input + if (!pFont || !pFont->InternalPTR || !pText || !*pText) + return false; + + const int lineH = pFont->InternalPTR->FontHeight; // fixed line height + const int tabSize = (pFont->Unknown_28 > 0) ? pFont->Unknown_28 : 64; + const int extraW = pFont->State_2C; // extra spacing after space-like chars + + int maxW = 0; + int totalH = 0; + + const wchar_t* lineStart = pText; + + // Iterate logical lines until NUL + while (*lineStart) + { + // Compute end of this line (wrap-aware) + const wchar_t* lineEnd = FindLineEnd(pFont, lineStart, nMaxWidth); + + // Advance to next line (skip CR/LF sequences) + const wchar_t* next = lineEnd; + if (*next == L'\r') + next = (*(next + 1) == L'\n') ? next + 2 : next + 1; + else if (*next == L'\n') + next = next + 1; + + const int lineLen = (int)(lineEnd - lineStart); + + if (lineLen > 0) + { + // Base width from shaped glyphs (tabs handled inside MeasureLineWidth) + int lineWidth = MeasureLineWidth(pFont, lineStart, lineEnd, tabSize); + + // Add extra spacing after each non-tab, non-last character + if (extraW > 0 && lineLen > 1) + { + for (int i = 0; i < lineLen - 1; i++) + { + wchar_t ch = lineStart[i]; + if (ch != L'\t') + lineWidth += extraW; + } + } + + // Track widest line + maxW = std::max(maxW, lineWidth); + + // Add one visual line height + totalH += lineH; + } + else if (lineStart == pText && lineLen == 0) + { + // First line is empty → still counts as one line + totalH = lineH; + } + + lineStart = next; + } + + if (pWidth) *pWidth = maxW; + if (pHeight) *pHeight = totalH; + return true; + } + + BitFont::InternalData* LoadTTFAsInternalData(const char* pFileName) + { + CCINIClass ini_uimd {}; + ini_uimd.LoadFromFile(GameStrings::UIMD_INI); + int LatinSize = ini_uimd.ReadInteger("FontSize", "LatinSize", 0); + //int ArabicSize = ini_uimd.ReadInteger("FontSize", "ArabicSize",0)); + FT_Library gFTLibrary = nullptr; + if (!gFTLibrary) + if (FT_Init_FreeType(&gFTLibrary) != 0) return nullptr; + + FT_Face ftFace; + if (FT_New_Face(gFTLibrary, pFileName, 0, &ftFace) != 0) + return nullptr; + + FT_Set_Pixel_Sizes(ftFace, 0, LatinSize); + + // scan metrics only — no rendering needed + int maxW = 0; + int maxAbove = 0; + int maxBelow = 0; + + for (int cp = 0x20; cp < 0x10000; cp++) + { + FT_UInt idx = FT_Get_Char_Index(ftFace, cp); + if (!idx) continue; + + if (FT_Load_Glyph(ftFace, idx, FT_LOAD_DEFAULT) != 0) // metrics only + continue; + + FT_GlyphSlot slot = ftFace->glyph; + + int w = (int)(slot->metrics.width >> 6); + int above = (int)(slot->metrics.horiBearingY >> 6); + int below = (int)((slot->metrics.height - slot->metrics.horiBearingY) >> 6); + + if (w > maxW) maxW = w; + if (above > maxAbove) maxAbove = above; + if (below > maxBelow) maxBelow = below; + } + + if (maxW == 0 || (maxAbove + maxBelow) == 0) + { + FT_Done_Face(ftFace); + return nullptr; + } + + int glyphH = maxAbove + maxBelow; + int bytesPerRow = (maxW + 7) / 8; + int dataSize = 1 + bytesPerRow * glyphH; + + // Allocate + BitFont::InternalData* data = new BitFont::InternalData(); + memset(data, 0, sizeof(BitFont::InternalData)); + + data->FontWidth = maxW; + data->Stride = bytesPerRow; + data->FontHeight = glyphH; + data->Lines = glyphH; + data->Count = 0x20000; + data->SymbolDataSize = dataSize; + data->SymbolTable = new short[0x20000](); + data->Bitmaps = new char[0x20000 * dataSize](); + + // render glyphs into atlas + for (int cp = 0x20; cp < 0x20000; cp++) + { + FT_UInt idx = FT_Get_Char_Index(ftFace, cp); + if (!idx) continue; + + if (FT_Load_Glyph(ftFace, idx, FT_LOAD_RENDER | FT_LOAD_TARGET_MONO) != 0) + continue; + + FT_GlyphSlot slot = ftFace->glyph; + FT_Bitmap& bmp = slot->bitmap; + + if (bmp.pixel_mode != FT_PIXEL_MODE_MONO) continue; + if (!bmp.buffer || bmp.rows == 0 || bmp.width == 0) continue; + + data->SymbolTable[cp] = (uint16_t)cp; + + char* dst = data->Bitmaps + cp * dataSize; + int advance = (int)(slot->advance.x >> 6); + dst[0] = (uint8_t)(advance < 255 ? advance : 255); + + // correct vertical placement in cell + int baseY = maxAbove - slot->bitmap_top; // was: slot->bitmap_top + int baseX = slot->bitmap_left; + int pitch = std::abs(bmp.pitch); // handle negative pitch + + for (int row = 0; row < (int)bmp.rows; row++) + { + int destRow = baseY + row; + if (destRow < 0 || destRow >= glyphH) continue; + + const uint8_t* srcRow = bmp.buffer + row * pitch; + uint8_t* dstRow = (uint8_t*)(dst + 1 + destRow * bytesPerRow); + + for (int col = 0; col < (int)bmp.width; col++) + { + int destBit = col + baseX; + if (destBit < 0 || destBit >= maxW) continue; + + int bit = (srcRow[col / 8] >> (7 - (col % 8))) & 1; + if (!bit) continue; + + dstRow[destBit / 8] |= (uint8_t)(1 << (7 - (destBit % 8))); + } + } + + data->ValidSymbolCount++; + } + + FT_Done_Face(ftFace); + return data; + } + + // Draws multi-line shaped text with wrapping, trimming, and RTL awareness. + // Uses FindLineEnd() for wrapping and DrawText_FlushLine() for per-line rendering. + bool BitText_DrawText_(BitFont* pFont, Surface* pSurface, const wchar_t* pWideString, int X, int Y, int W, int H, int a8, int a9, int nColorAdjust) + { + if (!pFont || !pWideString || !pSurface) + return false; + + FT_Face face = GetFTFace(pFont); + if (!face) + return false; + + WORD originalColor = pFont->Color; + pFont->Lock(pSurface); + pFont->SetField20(X); // internal left offset + + // Line height fallback (prevents infinite loop on bad fonts) + int fontH = (pFont->field_1C > 0) ? pFont->field_1C : 14; + + int surfaceW = pSurface->GetWidth(); + int surfaceH = pSurface->GetHeight(); + + // Wrap width and vertical clip region + int wrapW = (W > 0) ? W : (surfaceW - X); + int maxH = (H > 0) ? H : (surfaceH - Y); + + // Detect global RTL for trimming rules + int totalLen = (int)wcslen(pWideString); + bool textIsRTL = IsRTLText(pWideString, totalLen); + + int lineY = Y; + int shadowPos = 0; + + const wchar_t* currentPos = pWideString; + + // Process logical lines until NUL or vertical clip + while (*currentPos) + { + // Vertical clipping (single check per line) + if (lineY + fontH > Y + maxH) break; + if (lineY + fontH > surfaceH) break; + + const wchar_t* lineStart = currentPos; + const wchar_t* lineEnd = FindLineEnd(pFont, currentPos, wrapW); + + // Advance past newline sequences (\r, \n, \r\n) + if (*lineEnd == L'\r') + currentPos = (*(lineEnd + 1) == L'\n') ? lineEnd + 2 : lineEnd + 1; + else if (*lineEnd == L'\n') + currentPos = lineEnd + 1; + else + currentPos = lineEnd; + + // Trim leading/trailing spaces for LTR only + // (Arabic spacing is visually meaningful → preserved) + if (!textIsRTL) + { + while (lineStart < lineEnd && *lineStart == L' ') + ++lineStart; + while (lineEnd > lineStart && *(lineEnd - 1) == L' ') + --lineEnd; + } + + // Empty visual line → just advance Y + if (lineStart >= lineEnd) + { + lineY += fontH; + continue; + } + + // Render shaped + aligned line + DrawText_FlushLine(pFont, face, lineStart, lineEnd, X, lineY, wrapW, a8, a9, nColorAdjust, shadowPos); + + lineY += fontH; + } + + pFont->UnLock(pSurface); + pFont->Color = originalColor; + return true; + } + + // Computes a fade-to-white color based on glyph index. + WORD CalculateFade(uint16_t baseColor, int index, int shadowStart, int fadeLen) + { + if (!shadowStart) + return baseColor; // no fade active + + if (index >= shadowStart) + return 0xFFFF; // fully white + + int dist = shadowStart - index - 1; + if (dist >= fadeLen) + return baseColor; // outside fade zone + + int step = fadeLen - dist; + int factor = step * 255 / fadeLen; // 0..255 fade strength + + int r = ((baseColor >> 11) & 0x1F) * 255 / 31; + int g = ((baseColor >> 5) & 0x3F) * 255 / 63; + int b = (baseColor & 0x1F) * 255 / 31; + + // Linear blend toward white + r = r + (255 - r) * factor / 255; + g = g + (255 - g) * factor / 255; + b = b + (255 - b) * factor / 255; + + // Convert back to RGB565 + return (uint16_t)(((r * 31 / 255) << 11) | ((g * 63 / 255) << 5) | ((b * 31 / 255))); + } + + void RenderGlyphs(BitFont* pFont, FT_Face face, const std::vector& glyphs, int startX, int startY, WORD color, int shadowStart, int fadeLen, bool isRTL, bool isDrawline) + { + if (!face || glyphs.empty()) return; + + uint16_t* graphbuf = (uint16_t*)pFont->pGraphBuffer; + int pitch2 = pFont->PitchDiv2; + if (!graphbuf || !pitch2) return; + + + // Select FT face (Latin or Arabic) but DO NOT use its metrics + FT_Face renderFace = isRTL ? GetFTFaceForText(pFont, true) : face; + if (!renderFace) renderFace = face; + + color = pFont->Color; + + + // Unified metrics (correct asc/desc) + int ascender = isRTL ? gAscenderArabicMap[pFont] : gAscenderMap[pFont]; + + + int baselineY = startY + ascender; // baseline position + + + // Clipping + int clipL = pFont->Bounds.Left; + int clipR = pFont->Bounds.Right; + int clipT = pFont->Bounds.Top; + int clipB = pFont->Bounds.Bottom; + + if (clipR <= clipL || clipB <= clipT) + { + clipL = 0; + clipT = 0; + clipR = pitch2 - 1; + clipB = pitch2 - 1; + } + + clipL = std::max(0, clipL); + clipT = std::max(0, clipT); + clipR = std::min(pitch2 - 1, clipR); + + + // Compute bounding box + int totalWidth = 0; + for (const auto& g : glyphs) + totalWidth += std::abs(g.x_advance); + + int boxL = startX; + int boxR = startX + totalWidth; + if (boxR > pitch2 - 1) boxR = pitch2 - 1; + + int effL = std::max(clipL, boxL); // effective left + int effR = std::min(clipR, boxR); // effective right + + if (effL >= effR) return; + if (baselineY < clipT || baselineY > clipB) return; + + // Render loop + int curX = startX; + if (!isDrawline) + { + int len = (int)glyphs.size(); + fadeLen = len - shadowStart; // chars before fade zone + if (fadeLen < 0) + fadeLen = 0; + isDrawline = true; + } + for (int i = 0; i < (int)glyphs.size(); i++) + { + const auto& g = glyphs[i]; + + color = CalculateFade(color, i, shadowStart, fadeLen); + if (color == 0xFFFF) + break; + + int advance = std::abs(g.x_advance); + + if (g.glyphId == 0) + { + curX += advance; + continue; + } + + if (FT_Load_Glyph(renderFace, g.glyphId, FT_LOAD_RENDER) != 0) + { + FT_UInt fb = FT_Get_Char_Index(renderFace, L'?'); + if (fb == 0 || FT_Load_Glyph(renderFace, fb, FT_LOAD_RENDER) != 0) + { + curX += advance; + continue; + } + } + + FT_GlyphSlot slot = renderFace->glyph; + FT_Bitmap& bmp = slot->bitmap; + + if (!bmp.buffer || bmp.rows == 0 || bmp.width == 0) + { + curX += advance; + continue; + } + + // Correct baseline positioning + int drawX = curX + slot->bitmap_left + (g.x_offset >> 6); + int drawY = baselineY - slot->bitmap_top + (g.y_offset >> 6); + + // Draw bitmap + for (int row = 0; row < (int)bmp.rows; row++) + { + int py = drawY + row; + if (py < clipT || py > clipB) continue; + + uint8_t* srcRow = bmp.buffer + row * bmp.pitch; + uint16_t* dstRow = graphbuf + py * pitch2; + + for (int col = 0; col < (int)bmp.width; col++) + { + int px = drawX + col; + if (px < effL || px > effR) continue; + + uint8_t alpha = srcRow[col]; + if (!alpha) continue; + + if (alpha == 255) + { + dstRow[px] = color; + } + else + { + RGBClass fg((int)color, true); + RGBClass bg((int)dstRow[px], true); + RGBClass out(bg.Red + (fg.Red - bg.Red) * alpha / 255, bg.Green + (fg.Green - bg.Green) * alpha / 255, bg.Blue + (fg.Blue - bg.Blue) * alpha / 255); + dstRow[px] = (uint16_t)Drawing::RGB_To_Int(out.Red, out.Green, out.Blue); + } + } + } + + curX += advance; + pFont->Color = color; + if (curX > effR) + break; + } + } + + int MeasureGlyphs(BitFont* pFont, const std::vector& glyphs) + { + + int w = 0; + for (const auto& g : glyphs) + w += g.x_advance; + return w; + } + + int GetCharacterWidth(BitFont* pFont, wchar_t ch) + { + if (!pFont || !pFont->InternalPTR) return 0; + + FT_Face face = GetFTFace(pFont); + if (face) + { + // TTF path — HarfBuzz x_advance already includes all spacing + // State_2C must NOT be added here — it is a .FNT-only concept + FT_UInt idx = FT_Get_Char_Index(face, (FT_ULong)ch); + if (idx && FT_Load_Glyph(face, idx, FT_LOAD_DEFAULT) == 0) + return (int)(face->glyph->advance.x >> 6); + + // glyph not in font — return space width as fallback + if (ch == L' ') + { + // derive from em size — more accurate than field_1C fraction + if (face->units_per_EM > 0) + { + // space is typically ~25% of em + int pixelSize = (int)(face->size->metrics.x_ppem); + return pixelSize > 0 ? pixelSize / 4 : pFont->field_1C / 4; + } + } + + return 0; + } + uint8_t* bitmap = pFont->GetCharacterBitmap(ch); + if (!bitmap) bitmap = pFont->GetCharacterBitmap(L'?'); + if (!bitmap) return 0; + return bitmap[0]; + } + + // Draws one shaped line with alignment and bounds. + void DrawText_FlushLine(BitFont* pFont, FT_Face face, const wchar_t* lineStart, const wchar_t* lineEnd, int X, int Y, int W, int flag, int a9, int nColorAdjust, int& shadowPos) + { + int len = (int)(lineEnd - lineStart); + if (len <= 0 || !face) + return; + + WORD color = pFont->Color; + if (color == 0xFFFF) // invisible + return; + + bool isRTL = IsRTLText(lineStart, len); + + // Shape the logical substring [lineStart, lineEnd) + auto glyphs = ShapeText(pFont, lineStart, len); + if (glyphs.empty()) + return; + // Total shaped width (x_advance sum) + int lineW = MeasureGlyphs(pFont, glyphs); + + int boundL = X; + int boundR = (W > 0 ? X + W : pFont->PitchDiv2 - 1); + int boxW = boundR - boundL; + + if (lineW > boxW) + lineW = boxW; + + // Alignment: left / center / right + int startX = boundL; + if (flag & 1) + startX = boundL + (boxW - lineW) / 2; + else if (flag & 2) + startX = boundR - lineW; + + // Hard clamp to bounds + if (startX < boundL) + startX = boundL; + if (startX > boundR) + startX = boundR; + if (startX + lineW > boundR) + startX = boundR - lineW; + // Select correct FT_Face (Arabic/Latin) + FT_Face renderFace = GetFTFaceForText(pFont, isRTL); + + if (!renderFace) + renderFace = face; + + // Render shaped glyphs + RenderGlyphs(pFont, renderFace, glyphs, startX, Y, color, a9, nColorAdjust, isRTL, true); + + shadowPos += len; // consider using glyphs.size() + } + + // Used as a simple left‑padding operation for RTL + void MoveLastTwoToFront(wchar_t* str) + { + if (!str) return; + + int len = (int)wcslen(str); + if (len < 2) return; + + // Only rotate if last two chars are spaces + if (str[len - 2] != L' ' || str[len - 1] != L' ') + return; + + wchar_t a = str[len - 2]; + wchar_t b = str[len - 1]; + + // Shift right by 2 + for (int i = len - 3; i >= 0; --i) + str[i + 2] = str[i]; + + str[0] = a; + str[1] = b; + } + + // Draws a single line of text using HarfBuzz shaping. + int BitFont_434500_(BitFont* pFont, wchar_t* pText, int xLeft, int yTop, int charCount, int nColorAdjust) + { + if (!pText || !*pText) + return xLeft; + + int size = wcslen(pText); + if (IsRTLText(pText, size)) + MoveLastTwoToFront(pText); + + FT_Face face = GetFTFace(pFont); + if (!face) + return xLeft; + + WORD savedColor = pFont->Color; + + // visible length + int len = 0; + while (pText[len] && pText[len] != L'\r' && pText[len] != L'\n') + { + if (charCount > 0 && len >= charCount) + break; + len++; + } + + if (len <= 0) + { + pFont->Color = savedColor; + return xLeft; + } + + // HarfBuzz shaping + bool isRTL = IsRTLText(pText, len); + auto glyphs = ShapeText(pFont, pText, len); + + if (!glyphs.empty()) + { + int lineW = 0; + for (const auto& g : glyphs) + lineW += g.x_advance >> 6; + + int boundL = xLeft; + int boundR = pFont->PitchDiv2 - 1; + + int startX = boundL; + if (startX + lineW > boundR) + startX = boundR - lineW; + if (startX < boundL) + startX = boundL; + + FT_Face renderFace = GetFTFaceForText(pFont, isRTL); + if (!renderFace) + renderFace = face; + + RenderGlyphs(pFont, renderFace, glyphs, startX, yTop, savedColor, nColorAdjust, 0, isRTL, false); + + xLeft = startX + lineW; + } + + pFont->Color = savedColor; + return xLeft; + } +} diff --git a/src/TextRenderer/TextRenderer.h b/src/TextRenderer/TextRenderer.h new file mode 100644 index 0000000000..8d77fcb8e2 --- /dev/null +++ b/src/TextRenderer/TextRenderer.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using std::min; +using std::max; +#include FT_FREETYPE_H +#include FT_TRUETYPE_TABLES_H +namespace TextRenderer +{ + + struct ShapedGlyph + { + hb_codepoint_t glyphId; + int x_advance; + int y_advance; + int x_offset; + int y_offset; + unsigned int cluster; // index into original text + bool isSpace; + wchar_t ch; + bool isRTL; + }; + struct FontLineMetrics + { + int ascender; + int descender; + int height; + int baselineOffset; // extra: for alignment between fonts + float leading; + }; + struct Run + { + int start; + int length; + bool isRTL; + bool isArabic; + int vi, vj; // visual span (needed for minimal fix) + }; + FT_Face GetFTFace(BitFont* pFont); + FT_Face GetFTFaceForText(BitFont* pFont, bool isRTL); + hb_font_t* GetHbFontForText(BitFont* pFont, bool isRTL); + bool IsRTLText(const wchar_t* text, int len); + std::wstring FixUtf8InWchar(const wchar_t* ws); + int MeasureRealAscender(FT_Face face, const wchar_t* testSet); + int MeasureRealDescender(FT_Face face, const wchar_t* testSet); + int GetCharacterWidth(BitFont* pFont, wchar_t ch); + const wchar_t* FindLineEnd(BitFont* pFont, const wchar_t* start, int nMaxWidth); + BitFont::InternalData* LoadTTFAsInternalData(const char* pFileName); + int MeasureGlyphs(BitFont* pFont, const std::vector& glyphs); + std::vector ShapeText(BitFont* pFont, const wchar_t* text, int len); + void DrawText_FlushLine(BitFont* pFont, FT_Face face, const wchar_t* lineStart, const wchar_t* lineEnd, int X, int Y, int W, int a8, int a9, int nColorAdjust, int& shadowPos); + void RenderGlyphs(BitFont* pFont, FT_Face face, const std::vector& glyphs, int startX, int startY, WORD color, int shadowStart, int fadeLen, bool isRTL, bool isDrawLine); + BitFont* BitFont_CTOR_(BitFont* pFont, const char* pFileName); + bool BitFont_GetTextDimension_(BitFont* pFont, const wchar_t* pText, int* pWidth, int* pHeight, int nMaxWidth); + bool BitText_DrawText_(BitFont* pFont, Surface* pSurface, const wchar_t* pWideString, int X, int Y, int W, int H, int a8, int a9, int nColorAdjust); + int BitFont_434500_(BitFont* pFont, wchar_t* pText, int xLeft, int yTop, int charCount, int nColorAdjust); +};