From ba514d2e9aa77cc95d22cc5e8b22a884e65f45d8 Mon Sep 17 00:00:00 2001 From: rtw1x1 Date: Wed, 4 Feb 2026 11:59:36 +0000 Subject: [PATCH] FreeType: Kerning API and font adjustments --- src/EterLib/BlockTexture.cpp | 6 +-- src/EterLib/FontManager.cpp | 17 +++++--- src/EterLib/GrpFontTexture.cpp | 46 ++++++++++++-------- src/EterLib/GrpFontTexture.h | 6 +-- src/EterLib/GrpTextInstance.cpp | 77 ++++++++++++++++++++++++--------- src/EterLib/GrpTextInstance.h | 4 +- src/EterLib/TextBar.cpp | 33 ++++++++++++-- 7 files changed, 134 insertions(+), 55 deletions(-) diff --git a/src/EterLib/BlockTexture.cpp b/src/EterLib/BlockTexture.cpp index d2b3e31..ae21b9d 100644 --- a/src/EterLib/BlockTexture.cpp +++ b/src/EterLib/BlockTexture.cpp @@ -146,11 +146,11 @@ void CBlockTexture::InvalidateRect(const RECT & c_rsrcRect) DWORD * pdwDst = (DWORD *)lockedRect.pBits; DWORD dwDstWidth = lockedRect.Pitch>>2; DWORD dwSrcWidth = m_pDIB->GetWidth(); - for (int i = 0; i < iclipHeight; ++i) + for (int y = 0; y < iclipHeight; ++y) { - for (int i = 0; i < iclipWidth; ++i) + for (int x = 0; x < iclipWidth; ++x) { - pdwDst[i] = pdwSrc[i]; + pdwDst[x] = pdwSrc[x]; } pdwDst += dwDstWidth; pdwSrc += dwSrcWidth; diff --git a/src/EterLib/FontManager.cpp b/src/EterLib/FontManager.cpp index 3181de3..d0ea800 100644 --- a/src/EterLib/FontManager.cpp +++ b/src/EterLib/FontManager.cpp @@ -110,10 +110,17 @@ std::string CFontManager::ResolveFontPath(const char* faceName) // 3. Fall back to C:\Windows\Fonts #ifdef _WIN32 - char winDir[MAX_PATH]; - if (GetWindowsDirectoryA(winDir, MAX_PATH)) + static std::string s_fontsDir; + if (s_fontsDir.empty()) { - std::string systemPath = std::string(winDir) + "\\Fonts\\" + fileName; + char winDir[MAX_PATH]; + if (GetWindowsDirectoryA(winDir, MAX_PATH)) + s_fontsDir = std::string(winDir) + "\\Fonts\\"; + } + + if (!s_fontsDir.empty()) + { + std::string systemPath = s_fontsDir + fileName; if (FileExists(systemPath)) return systemPath; } @@ -127,9 +134,9 @@ std::string CFontManager::ResolveFontPath(const char* faceName) if (FileExists(localPath)) return localPath; - if (GetWindowsDirectoryA(winDir, MAX_PATH)) + if (!s_fontsDir.empty()) { - std::string systemPath = std::string(winDir) + "\\Fonts\\" + ttcName; + std::string systemPath = s_fontsDir + ttcName; if (FileExists(systemPath)) return systemPath; } diff --git a/src/EterLib/GrpFontTexture.cpp b/src/EterLib/GrpFontTexture.cpp index ff51cbb..de18020 100644 --- a/src/EterLib/GrpFontTexture.cpp +++ b/src/EterLib/GrpFontTexture.cpp @@ -4,23 +4,19 @@ #include "EterBase/Stl.h" #include "Util.h" -#include #include #include FT_FREETYPE_H -#include FT_GLYPH_H #include -// Precomputed gamma LUT to sharpen FreeType's grayscale anti-aliasing. -// GDI ClearType has high-contrast edges; FreeType grayscale is softer. -// Gamma < 1.0 boosts mid-range alpha, making edges crisper. +// Gamma LUT to sharpen grayscale anti-aliasing edges. static struct SAlphaGammaLUT { unsigned char table[256]; SAlphaGammaLUT() { table[0] = 0; for (int i = 1; i < 256; ++i) - table[i] = (unsigned char)(pow(i / 255.0, 0.80) * 255.0 + 0.5); + table[i] = (unsigned char)(pow(i / 255.0, 0.85) * 255.0 + 0.5); } } s_alphaGammaLUT; @@ -45,11 +41,11 @@ void CGraphicFontTexture::Initialize() m_bItalic = false; m_ascender = 0; m_lineHeight = 0; + m_hasKerning = false; m_x = 0; m_y = 0; m_step = 0; m_fontSize = 0; - memset(m_fontName, 0, sizeof(m_fontName)); } bool CGraphicFontTexture::IsEmpty() const @@ -122,10 +118,6 @@ bool CGraphicFontTexture::Create(const char* c_szFontName, int fontSize, bool bI { Destroy(); - // UTF-8 -> UTF-16 for font name storage - std::wstring wFontName = Utf8ToWide(c_szFontName ? c_szFontName : ""); - wcsncpy_s(m_fontName, wFontName.c_str(), _TRUNCATE); - m_fontSize = fontSize; m_bItalic = bItalic; @@ -147,9 +139,6 @@ bool CGraphicFontTexture::Create(const char* c_szFontName, int fontSize, bool bI m_pAtlasBuffer = new DWORD[width * height]; memset(m_pAtlasBuffer, 0, width * height * sizeof(DWORD)); - // Store UTF-8 name for device reset re-creation - m_fontNameUTF8 = c_szFontName ? c_szFontName : ""; - // Create a per-instance FT_Face (this instance owns it) m_ftFace = CFontManager::Instance().CreateFace(c_szFontName); if (!m_ftFace) @@ -158,12 +147,14 @@ bool CGraphicFontTexture::Create(const char* c_szFontName, int fontSize, bool bI return false; } - // Set pixel size int pixelSize = (fontSize < 0) ? -fontSize : fontSize; if (pixelSize == 0) pixelSize = 12; + FT_Set_Pixel_Sizes(m_ftFace, 0, pixelSize); + m_hasKerning = FT_HAS_KERNING(m_ftFace) != 0; + // Apply italic via shear matrix if needed if (bItalic) { @@ -234,6 +225,24 @@ bool CGraphicFontTexture::UpdateTexture() return true; } +float CGraphicFontTexture::GetKerning(wchar_t prev, wchar_t cur) +{ + if (!m_hasKerning || !m_ftFace || prev == 0) + return 0.0f; + + FT_UInt prevIndex = FT_Get_Char_Index(m_ftFace, prev); + FT_UInt curIndex = FT_Get_Char_Index(m_ftFace, cur); + + if (prevIndex == 0 || curIndex == 0) + return 0.0f; + + FT_Vector delta; + if (FT_Get_Kerning(m_ftFace, prevIndex, curIndex, FT_KERNING_DEFAULT, &delta) != 0) + return 0.0f; + + return (float)(delta.x) / 64.0f; +} + CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::GetCharacterInfomation(wchar_t keyValue) { TCharacterKey code = keyValue; @@ -268,7 +277,10 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI return NULL; } - if (FT_Load_Glyph(m_ftFace, glyphIndex, FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL) != 0) + if (FT_Load_Glyph(m_ftFace, glyphIndex, FT_LOAD_DEFAULT) != 0) + return NULL; + + if (FT_Render_Glyph(m_ftFace->glyph, FT_RENDER_MODE_NORMAL) != 0) return NULL; FT_GlyphSlot slot = m_ftFace->glyph; @@ -335,7 +347,7 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI } } - // Copy FreeType bitmap into atlas buffer at baseline-normalized position + // Copy grayscale FreeType bitmap into atlas buffer with gamma correction for (int row = 0; row < glyphBitmapHeight; ++row) { int atlasY = m_y + yOffset + row; diff --git a/src/EterLib/GrpFontTexture.h b/src/EterLib/GrpFontTexture.h index d4cf282..0d173ea 100644 --- a/src/EterLib/GrpFontTexture.h +++ b/src/EterLib/GrpFontTexture.h @@ -6,7 +6,6 @@ #include #include FT_FREETYPE_H -#include #include #include @@ -48,6 +47,8 @@ class CGraphicFontTexture : public CGraphicTexture TCharacterInfomation* GetCharacterInfomation(wchar_t keyValue); TCharacterInfomation* UpdateCharacterInfomation(TCharacterKey keyValue); + float GetKerning(wchar_t prev, wchar_t cur); + bool IsEmpty() const; protected: @@ -76,12 +77,11 @@ class CGraphicFontTexture : public CGraphicTexture int m_step; bool m_isDirty; - TCHAR m_fontName[LF_FACESIZE]; - std::string m_fontNameUTF8; // stored for device reset re-creation LONG m_fontSize; bool m_bItalic; // FreeType metrics cached per-font int m_ascender; int m_lineHeight; + bool m_hasKerning; }; diff --git a/src/EterLib/GrpTextInstance.cpp b/src/EterLib/GrpTextInstance.cpp index 8211e3c..0633852 100644 --- a/src/EterLib/GrpTextInstance.cpp +++ b/src/EterLib/GrpTextInstance.cpp @@ -38,18 +38,23 @@ int CGraphicTextInstance::Hyperlink_GetText(char* buf, int len) return (written > 0) ? written : 0; } -int CGraphicTextInstance::__DrawCharacter(CGraphicFontTexture * pFontTexture, wchar_t text, DWORD dwColor) +int CGraphicTextInstance::__DrawCharacter(CGraphicFontTexture * pFontTexture, wchar_t text, DWORD dwColor, wchar_t prevChar) { CGraphicFontTexture::TCharacterInfomation* pInsCharInfo = pFontTexture->GetCharacterInfomation(text); if (pInsCharInfo) { + // Round kerning to nearest pixel to keep glyphs on the pixel grid. + // Fractional offsets cause bilinear interpolation blur in D3D9. + float kern = floorf(pFontTexture->GetKerning(prevChar, text) + 0.5f); + m_dwColorInfoVector.push_back(dwColor); m_pCharInfoVector.push_back(pInsCharInfo); + m_kernVector.push_back(kern); - m_textWidth += pInsCharInfo->advance; + m_textWidth += (int)(pInsCharInfo->advance + kern); m_textHeight = std::max((WORD)pInsCharInfo->height, m_textHeight); - return pInsCharInfo->advance; + return (int)(pInsCharInfo->advance + kern); } return 0; @@ -99,6 +104,7 @@ void CGraphicTextInstance::Update() auto ResetState = [&, spaceHeight]() { m_pCharInfoVector.clear(); + m_kernVector.clear(); m_dwColorInfoVector.clear(); m_hyperlinkVector.clear(); m_textWidth = 0; @@ -121,6 +127,7 @@ void CGraphicTextInstance::Update() } m_pCharInfoVector.clear(); + m_kernVector.clear(); m_dwColorInfoVector.clear(); m_hyperlinkVector.clear(); @@ -154,8 +161,12 @@ void CGraphicTextInstance::Update() // Secret mode: draw '*' instead of actual characters if (m_isSecret) { + wchar_t prevCh = 0; for (int i = 0; i < wTextLen; ++i) - __DrawCharacter(pFontTexture, L'*', dwColor); + { + __DrawCharacter(pFontTexture, L'*', dwColor, prevCh); + prevCh = L'*'; + } pFontTexture->UpdateTexture(); m_isUpdate = true; @@ -183,8 +194,12 @@ void CGraphicTextInstance::Update() wMsg.data(), (int)wMsg.size(), m_computedRTL); + wchar_t prevCh = 0; for (size_t i = 0; i < visual.size(); ++i) - __DrawCharacter(pFontTexture, visual[i], dwColor); + { + __DrawCharacter(pFontTexture, visual[i], dwColor, prevCh); + prevCh = visual[i]; + } pFontTexture->UpdateTexture(); m_isUpdate = true; @@ -245,8 +260,7 @@ void CGraphicTextInstance::Update() int hyperlinkStep = 0; // 0=normal, 1=collecting metadata, 2=visible hyperlink std::wstring hyperlinkMetadata; - // Use thread-local buffer to avoid per-call allocation - thread_local static std::vector s_currentSegment; + static std::vector s_currentSegment; s_currentSegment.clear(); SHyperlink currentHyperlink; @@ -267,10 +281,12 @@ void CGraphicTextInstance::Update() std::vector visual = BuildVisualBidiText_Tagless( s_currentSegment.data(), (int)s_currentSegment.size(), forceRTLForBidi); + wchar_t prevCh = m_pCharInfoVector.empty() ? 0 : 0; // no prev across segments for (size_t j = 0; j < visual.size(); ++j) { - int w = __DrawCharacter(pFontTexture, visual[j], segColor); + int w = __DrawCharacter(pFontTexture, visual[j], segColor, prevCh); totalWidth += w; + prevCh = visual[j]; } s_currentSegment.clear(); @@ -285,13 +301,15 @@ void CGraphicTextInstance::Update() { outWidth = 0; - // Use thread-local buffers to avoid allocation - thread_local static std::vector s_newCharInfos; - thread_local static std::vector s_newColors; + static std::vector s_newCharInfos; + static std::vector s_newColors; + static std::vector s_newKerns; s_newCharInfos.clear(); s_newColors.clear(); + s_newKerns.clear(); s_newCharInfos.reserve(chars.size()); s_newColors.reserve(chars.size()); + s_newKerns.reserve(chars.size()); for (size_t k = 0; k < chars.size(); ++k) { @@ -301,16 +319,16 @@ void CGraphicTextInstance::Update() s_newCharInfos.push_back(pInfo); s_newColors.push_back(color); + s_newKerns.push_back(0.0f); outWidth += pInfo->advance; m_textHeight = std::max((WORD)pInfo->height, m_textHeight); } - // Insert at the beginning of the draw list. m_pCharInfoVector.insert(m_pCharInfoVector.begin(), s_newCharInfos.begin(), s_newCharInfos.end()); m_dwColorInfoVector.insert(m_dwColorInfoVector.begin(), s_newColors.begin(), s_newColors.end()); + m_kernVector.insert(m_kernVector.begin(), s_newKerns.begin(), s_newKerns.end()); - // Shift any already-recorded hyperlinks to the right. for (auto& link : m_hyperlinkVector) { link.sx += outWidth; @@ -366,7 +384,7 @@ void CGraphicTextInstance::Update() if (!s_currentSegment.empty()) { // OPTIMIZED: Use thread-local buffer for visible rendering - thread_local static std::vector s_visibleToRender; + static std::vector s_visibleToRender; s_visibleToRender.clear(); // Find bracket positions: [ ... ] @@ -385,7 +403,7 @@ void CGraphicTextInstance::Update() s_visibleToRender.push_back(L'['); // Extract inside content and apply BiDi - thread_local static std::vector s_content; + static std::vector s_content; s_content.assign( s_currentSegment.begin() + openBracket + 1, s_currentSegment.begin() + closeBracket); @@ -430,10 +448,12 @@ void CGraphicTextInstance::Update() { // LTR or non-chat: keep original "append" behavior currentHyperlink.sx = currentHyperlink.ex; + wchar_t prevCh = 0; for (size_t j = 0; j < s_visibleToRender.size(); ++j) { - int w = __DrawCharacter(pFontTexture, s_visibleToRender[j], currentColor); + int w = __DrawCharacter(pFontTexture, s_visibleToRender[j], currentColor, prevCh); currentHyperlink.ex += w; + prevCh = s_visibleToRender[j]; } m_hyperlinkVector.push_back(currentHyperlink); } @@ -471,8 +491,14 @@ void CGraphicTextInstance::Update() // Simple LTR rendering for plain text (no tags, no RTL) // Just draw characters in logical order - for (int i = 0; i < wTextLen; ++i) - __DrawCharacter(pFontTexture, wTextBuf[i], dwColor); + { + wchar_t prevCh = 0; + for (int i = 0; i < wTextLen; ++i) + { + __DrawCharacter(pFontTexture, wTextBuf[i], dwColor, prevCh); + prevCh = wTextBuf[i]; + } + } pFontTexture->UpdateTexture(); m_isUpdate = true; @@ -580,13 +606,18 @@ void CGraphicTextInstance::Render(RECT * pClipRect) fCurY=fStanY; fFontMaxHeight=0.0f; + int charIdx = 0; CGraphicFontTexture::TPCharacterInfomationVector::iterator i; - for (i=m_pCharInfoVector.begin(); i!=m_pCharInfoVector.end(); ++i) + for (i=m_pCharInfoVector.begin(); i!=m_pCharInfoVector.end(); ++i, ++charIdx) { pCurCharInfo = *i; + float fKern = (charIdx < (int)m_kernVector.size()) ? m_kernVector[charIdx] : 0.0f; + fCurX += fKern; + fFontWidth=float(pCurCharInfo->width); fFontHeight=float(pCurCharInfo->height); + fFontMaxHeight=(std::max)(fFontMaxHeight, fFontHeight); fFontAdvance=float(pCurCharInfo->advance); if ((fCurX+fFontWidth)-m_v3Position.x > m_fLimitWidth) [[unlikely]] { @@ -685,13 +716,16 @@ void CGraphicTextInstance::Render(RECT * pClipRect) fCurY=fStanY; fFontMaxHeight=0.0f; - for (int i = 0; i < m_pCharInfoVector.size(); ++i) + for (int i = 0; i < (int)m_pCharInfoVector.size(); ++i) { pCurCharInfo = m_pCharInfoVector[i]; + float fKern = (i < (int)m_kernVector.size()) ? m_kernVector[i] : 0.0f; + fCurX += fKern; + fFontWidth=float(pCurCharInfo->width); fFontHeight=float(pCurCharInfo->height); - fFontMaxHeight=(std::max)(fFontHeight, (float)pCurCharInfo->height); + fFontMaxHeight=(std::max)(fFontMaxHeight, fFontHeight); fFontAdvance=float(pCurCharInfo->advance); if ((fCurX + fFontWidth) - m_v3Position.x > m_fLimitWidth) [[unlikely]] { @@ -1267,6 +1301,7 @@ void CGraphicTextInstance::Destroy() { m_stText=""; m_pCharInfoVector.clear(); + m_kernVector.clear(); m_dwColorInfoVector.clear(); m_hyperlinkVector.clear(); m_logicalToVisualPos.clear(); diff --git a/src/EterLib/GrpTextInstance.h b/src/EterLib/GrpTextInstance.h index ec7d1aa..4bb03f9 100644 --- a/src/EterLib/GrpTextInstance.h +++ b/src/EterLib/GrpTextInstance.h @@ -90,7 +90,7 @@ class CGraphicTextInstance protected: void __Initialize(); - int __DrawCharacter(CGraphicFontTexture * pFontTexture, wchar_t text, DWORD dwColor); + int __DrawCharacter(CGraphicFontTexture * pFontTexture, wchar_t text, DWORD dwColor, wchar_t prevChar = 0); void __GetTextPos(DWORD index, float* x, float* y); protected: @@ -130,7 +130,6 @@ class CGraphicTextInstance private: bool m_isUpdate; - bool m_isUpdateFontTexture; bool m_computedRTL; // Result of BiDi analysis (used when m_direction == Auto) bool m_isChatMessage; // True if this text was set via SetChatValue (has separated name/message) std::string m_chatName; // Chat sender name (only used when m_isChatMessage is true) @@ -138,6 +137,7 @@ class CGraphicTextInstance CGraphicText::TRef m_roText; CGraphicFontTexture::TPCharacterInfomationVector m_pCharInfoVector; + std::vector m_kernVector; std::vector m_dwColorInfoVector; std::vector m_hyperlinkVector; std::vector m_logicalToVisualPos; // Maps logical cursor pos (UTF-16 with tags) to visual pos (rendered chars) diff --git a/src/EterLib/TextBar.cpp b/src/EterLib/TextBar.cpp index 07d9843..977d929 100644 --- a/src/EterLib/TextBar.cpp +++ b/src/EterLib/TextBar.cpp @@ -10,13 +10,13 @@ #include -// Same gamma LUT as GrpFontTexture for consistent text sharpness +// Gamma LUT matching GrpFontTexture for consistent text sharpness static struct STextBarGammaLUT { unsigned char table[256]; STextBarGammaLUT() { table[0] = 0; for (int i = 1; i < 256; ++i) - table[i] = (unsigned char)(pow(i / 255.0, 0.80) * 255.0 + 0.5); + table[i] = (unsigned char)(pow(i / 255.0, 0.85) * 255.0 + 0.5); } } s_textBarGammaLUT; @@ -73,15 +73,27 @@ void CTextBar::GetTextExtent(const char* c_szText, SIZE* p_size) std::wstring wText = Utf8ToWide(c_szText); + bool hasKerning = FT_HAS_KERNING(m_ftFace) != 0; + FT_UInt prevIndex = 0; int totalAdvance = 0; + for (size_t i = 0; i < wText.size(); ++i) { FT_UInt glyphIndex = FT_Get_Char_Index(m_ftFace, wText[i]); if (glyphIndex == 0) glyphIndex = FT_Get_Char_Index(m_ftFace, L' '); + if (hasKerning && prevIndex && glyphIndex) + { + FT_Vector delta; + if (FT_Get_Kerning(m_ftFace, prevIndex, glyphIndex, FT_KERNING_DEFAULT, &delta) == 0) + totalAdvance += (int)(delta.x / 64); + } + if (FT_Load_Glyph(m_ftFace, glyphIndex, FT_LOAD_DEFAULT) == 0) totalAdvance += (int)ceilf((float)(m_ftFace->glyph->advance.x) / 64.0f); + + prevIndex = glyphIndex; } p_size->cx = totalAdvance; @@ -107,14 +119,26 @@ void CTextBar::TextOut(int ix, int iy, const char * c_szText) DWORD colorRGB = m_textColor; // 0x00BBGGRR in memory + bool hasKerning = FT_HAS_KERNING(m_ftFace) != 0; + FT_UInt prevIndex = 0; + for (size_t i = 0; i < wText.size(); ++i) { FT_UInt glyphIndex = FT_Get_Char_Index(m_ftFace, wText[i]); if (glyphIndex == 0) glyphIndex = FT_Get_Char_Index(m_ftFace, L' '); - FT_Int32 loadFlags = FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL; - if (FT_Load_Glyph(m_ftFace, glyphIndex, loadFlags) != 0) + if (hasKerning && prevIndex && glyphIndex) + { + FT_Vector delta; + if (FT_Get_Kerning(m_ftFace, prevIndex, glyphIndex, FT_KERNING_DEFAULT, &delta) == 0) + penX += (int)(delta.x / 64); + } + + if (FT_Load_Glyph(m_ftFace, glyphIndex, FT_LOAD_DEFAULT) != 0) + continue; + + if (FT_Render_Glyph(m_ftFace->glyph, FT_RENDER_MODE_NORMAL) != 0) continue; FT_GlyphSlot slot = m_ftFace->glyph; @@ -151,6 +175,7 @@ void CTextBar::TextOut(int ix, int iy, const char * c_szText) } penX += (int)ceilf((float)(slot->advance.x) / 64.0f); + prevIndex = glyphIndex; } Invalidate();