FreeType: Kerning API and font adjustments

This commit is contained in:
rtw1x1
2026-02-04 11:59:36 +00:00
parent 4280220819
commit ba514d2e9a
7 changed files with 134 additions and 55 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -4,23 +4,19 @@
#include "EterBase/Stl.h"
#include "Util.h"
#include <utf8.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H
#include <cmath>
// 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;

View File

@@ -6,7 +6,6 @@
#include <ft2build.h>
#include FT_FREETYPE_H
#include <string>
#include <vector>
#include <map>
@@ -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;
};

View File

@@ -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<wchar_t> s_currentSegment;
static std::vector<wchar_t> s_currentSegment;
s_currentSegment.clear();
SHyperlink currentHyperlink;
@@ -267,10 +281,12 @@ void CGraphicTextInstance::Update()
std::vector<wchar_t> 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<CGraphicFontTexture::TCharacterInfomation*> s_newCharInfos;
thread_local static std::vector<DWORD> s_newColors;
static std::vector<CGraphicFontTexture::TCharacterInfomation*> s_newCharInfos;
static std::vector<DWORD> s_newColors;
static std::vector<float> 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<wchar_t> s_visibleToRender;
static std::vector<wchar_t> 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<wchar_t> s_content;
static std::vector<wchar_t> 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();

View File

@@ -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<float> m_kernVector;
std::vector<DWORD> m_dwColorInfoVector;
std::vector<SHyperlink> m_hyperlinkVector;
std::vector<int> m_logicalToVisualPos; // Maps logical cursor pos (UTF-16 with tags) to visual pos (rendered chars)

View File

@@ -10,13 +10,13 @@
#include <cmath>
// 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();