2V0-g: port the .fnt font pipeline over a FreeType GDI emulation
CGraphicText, CGraphicFontTexture and CGraphicDib are the 40250 bodies verbatim; CGraphicTextInstance is verbatim except that its D3D draws become UI render commands (one image quad per glyph, Bar for cursor/underline). - platform/Win32Gdi.cpp: the GDI subset those files call, over vendored FreeType 2.13.3, following GDI's LOGFONT cell height, win metrics and gasp rules; system font lookup with substitute and CJK fallback faces - Win32Crt: MultiByteToWideChar / WideCharToMultiByte (UTF-8 and 1252 by hand, other code pages through iconv) - CGraphicImageTexture memory textures: glyph pages reach Godot as "mem:<id>@<revision>"; Metin2PythonHost.memory_texture + Canvas cache - Util.cpp code page / font face table, SetDefaultCodePage at boot, DefaultFont_* block, fnt resource type, GetMaxTextureWidth/Height - TextTag, StringCodec, Arabic, Vietnamese copied from 40250 - tests: port.text (Tahoma 12 metrics, alignment, CP949), port.app_loop and python_host_test.gd now check glyph quads instead of text commands Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
141206cb69
commit
afe63bfa61
@@ -0,0 +1,457 @@
|
||||
// The <wingdi.h> subset the 40250 font path calls (common/shim/win32/wingdi.h), over FreeType.
|
||||
// CGraphicDib / CGraphicFontTexture run unchanged on top of it: a memory DC owns a 32-bit DIB,
|
||||
// CreateFontIndirect maps the LOGFONT onto a TrueType face the way GDI sizes it, and TextOutW
|
||||
// rasterises into the DIB with the DC's text/background colours.
|
||||
//
|
||||
// GDI sizing that the glyph atlas depends on:
|
||||
// - lfHeight > 0 is the cell height (ascent + descent), lfHeight < 0 the em height; the em is
|
||||
// lfHeight * unitsPerEm / (usWinAscent + usWinDescent), rounded to whole pixels.
|
||||
// - tmAscent / tmDescent are usWinAscent / usWinDescent scaled to that em and rounded;
|
||||
// GetTextExtentPoint32W reports tmHeight = tmAscent + tmDescent as the height.
|
||||
// - ANTIALIASED_QUALITY follows the face's gasp table: grayscale only in the ppem ranges it
|
||||
// flags GASP_DO_GRAY, otherwise bilevel (Tahoma at 10/12 ppem is bilevel).
|
||||
// - The glyph origin is (x + A, y + tmAscent - top); advance = A + B + C.
|
||||
// Missing glyphs fall back to the first installed face that has them, like GDI font linking.
|
||||
#include "EterLib/StdAfx.h"
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include FT_TRUETYPE_TABLES_H
|
||||
#include FT_GASP_H
|
||||
#include FT_SYNTHESIS_H
|
||||
#include FT_DRIVER_H
|
||||
#include FT_MODULE_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <dirent.h>
|
||||
|
||||
namespace {
|
||||
|
||||
enum class GdiKind { Bitmap, Font };
|
||||
|
||||
struct GdiObject
|
||||
{
|
||||
GdiKind kind;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct HBITMAP__ : GdiObject
|
||||
{
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool top_down = true;
|
||||
std::vector<uint32_t> bits;
|
||||
};
|
||||
|
||||
struct HFONT__ : GdiObject
|
||||
{
|
||||
FT_Face face = nullptr;
|
||||
std::vector<FT_Face> fallbacks;
|
||||
int ppem = 0;
|
||||
int ascent = 0;
|
||||
int descent = 0;
|
||||
bool italic = false;
|
||||
bool antialiased = false;
|
||||
};
|
||||
|
||||
struct HDC__
|
||||
{
|
||||
HBITMAP bitmap = nullptr;
|
||||
HFONT font = nullptr;
|
||||
COLORREF text = 0;
|
||||
COLORREF bk = 0x00FFFFFF;
|
||||
int bk_mode = OPAQUE;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex g_mutex;
|
||||
FT_Library g_library = nullptr;
|
||||
std::unordered_map<std::string, FT_Face> g_faces; // loaded face per file path
|
||||
|
||||
FT_Library library()
|
||||
{
|
||||
if (!g_library) {
|
||||
FT_Init_FreeType(&g_library);
|
||||
// GDI runs the full TrueType bytecode, which is FreeType's v35 interpreter; v40 drops the
|
||||
// horizontal hints and gives different advances at small sizes.
|
||||
FT_UInt version = TT_INTERPRETER_VERSION_35;
|
||||
FT_Property_Set(g_library, "truetype", "interpreter-version", &version);
|
||||
}
|
||||
return g_library;
|
||||
}
|
||||
|
||||
std::string lower(std::string s)
|
||||
{
|
||||
for (char& c : s) c = char(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
}
|
||||
|
||||
const std::vector<std::string>& font_dirs()
|
||||
{
|
||||
static const std::vector<std::string> dirs = {
|
||||
#if defined(__ANDROID__)
|
||||
"/system/fonts",
|
||||
#else
|
||||
"/System/Library/Fonts/Supplemental",
|
||||
"/System/Library/Fonts",
|
||||
"/Library/Fonts",
|
||||
#endif
|
||||
};
|
||||
return dirs;
|
||||
}
|
||||
|
||||
// Face name -> font file: the file stem matched case-insensitively ("Tahoma" -> Tahoma.ttf).
|
||||
std::string find_font_file(const std::string& face)
|
||||
{
|
||||
const std::string want = lower(face);
|
||||
for (const std::string& dir : font_dirs()) {
|
||||
DIR* d = opendir(dir.c_str());
|
||||
if (!d) continue;
|
||||
std::string hit;
|
||||
while (dirent* e = readdir(d)) {
|
||||
std::string name = e->d_name;
|
||||
const size_t dot = name.rfind('.');
|
||||
if (dot == std::string::npos) continue;
|
||||
const std::string ext = lower(name.substr(dot + 1));
|
||||
if (ext != "ttf" && ext != "ttc" && ext != "otf") continue;
|
||||
if (lower(name.substr(0, dot)) == want) { hit = dir + "/" + name; break; }
|
||||
}
|
||||
closedir(d);
|
||||
if (!hit.empty()) return hit;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
FT_Face open_face(const std::string& path)
|
||||
{
|
||||
auto it = g_faces.find(path);
|
||||
if (it != g_faces.end()) return it->second;
|
||||
FT_Face face = nullptr;
|
||||
if (FT_New_Face(library(), path.c_str(), 0, &face) != 0) face = nullptr;
|
||||
if (face) FT_Select_Charmap(face, FT_ENCODING_UNICODE);
|
||||
g_faces[path] = face;
|
||||
return face;
|
||||
}
|
||||
|
||||
FT_Face open_named(const char* name)
|
||||
{
|
||||
const std::string path = find_font_file(name);
|
||||
return path.empty() ? nullptr : open_face(path);
|
||||
}
|
||||
|
||||
// PORT: the faces GDI would substitute when the requested one is not installed (the 40250 locale
|
||||
// asks for Tahoma / Arial on code page 1252), then the font-linking faces for CJK/Hangul glyphs.
|
||||
const char* const kSubstitutes[] = {"Arial", "Helvetica", "Roboto-Regular", "DroidSans", "NotoSans-Regular"};
|
||||
const char* const kLinked[] = {"Arial Unicode", "AppleSDGothicNeo", "NotoSansCJK-Regular", "NotoSansCJKkr-Regular",
|
||||
"DroidSansFallback"};
|
||||
|
||||
bool gray_at(FT_Face face, int ppem)
|
||||
{
|
||||
const FT_Int flags = FT_Get_Gasp(face, FT_UInt(ppem));
|
||||
if (flags == FT_GASP_NO_TABLE) return true;
|
||||
return (flags & FT_GASP_DO_GRAY) != 0;
|
||||
}
|
||||
|
||||
void set_size(FT_Face face, int ppem)
|
||||
{
|
||||
FT_Set_Pixel_Sizes(face, 0, FT_UInt(ppem));
|
||||
}
|
||||
|
||||
struct Glyph
|
||||
{
|
||||
int left = 0; // A
|
||||
int top = 0; // rows above the baseline
|
||||
int advance = 0; // A + B + C
|
||||
int width = 0; // B
|
||||
int rows = 0;
|
||||
int pitch = 0;
|
||||
bool mono = true;
|
||||
std::vector<unsigned char> bitmap;
|
||||
};
|
||||
|
||||
bool load_glyph(HFONT font, wchar_t ch, bool render, Glyph* out)
|
||||
{
|
||||
FT_Face face = font->face;
|
||||
FT_UInt index = FT_Get_Char_Index(face, FT_ULong(ch));
|
||||
if (index == 0) {
|
||||
for (FT_Face fallback : font->fallbacks) {
|
||||
FT_UInt i = FT_Get_Char_Index(fallback, FT_ULong(ch));
|
||||
if (i) { face = fallback; index = i; break; }
|
||||
}
|
||||
}
|
||||
set_size(face, font->ppem);
|
||||
const bool gray = font->antialiased && gray_at(face, font->ppem);
|
||||
const FT_Int32 target = gray ? FT_LOAD_TARGET_NORMAL : FT_LOAD_TARGET_MONO;
|
||||
if (FT_Load_Glyph(face, index, FT_LOAD_DEFAULT | target) != 0) return false;
|
||||
FT_GlyphSlot slot = face->glyph;
|
||||
if (font->italic) FT_GlyphSlot_Oblique(slot);
|
||||
out->advance = int((slot->advance.x + 32) >> 6);
|
||||
if (slot->format == FT_GLYPH_FORMAT_OUTLINE) {
|
||||
if (FT_Render_Glyph(slot, gray ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO) != 0) return false;
|
||||
}
|
||||
const FT_Bitmap& bm = slot->bitmap;
|
||||
out->left = slot->bitmap_left;
|
||||
out->top = slot->bitmap_top;
|
||||
out->width = int(bm.width);
|
||||
out->rows = int(bm.rows);
|
||||
out->mono = bm.pixel_mode == FT_PIXEL_MODE_MONO;
|
||||
if (render && bm.buffer && bm.rows) {
|
||||
out->pitch = std::abs(bm.pitch);
|
||||
out->bitmap.assign(bm.buffer, bm.buffer + size_t(out->pitch) * bm.rows);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
HFONT default_font()
|
||||
{
|
||||
static HFONT font = [] {
|
||||
LOGFONT lf{};
|
||||
lf.lfHeight = 12;
|
||||
std::strcpy(lf.lfFaceName, "Arial");
|
||||
return CreateFontIndirect(&lf);
|
||||
}();
|
||||
return font;
|
||||
}
|
||||
|
||||
HFONT dc_font(HDC hdc)
|
||||
{
|
||||
return hdc->font ? hdc->font : default_font();
|
||||
}
|
||||
|
||||
// COLORREF is 0x00BBGGRR; a 32-bit DIB pixel is 0x00RRGGBB.
|
||||
uint32_t dib_pixel(COLORREF c)
|
||||
{
|
||||
return ((c & 0xFF) << 16) | (c & 0xFF00) | ((c >> 16) & 0xFF);
|
||||
}
|
||||
|
||||
uint32_t* dib_row(HBITMAP bmp, int y)
|
||||
{
|
||||
const int row = bmp->top_down ? y : bmp->height - 1 - y;
|
||||
return bmp->bits.data() + size_t(row) * size_t(bmp->width);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HDC CreateCompatibleDC(HDC)
|
||||
{
|
||||
return new HDC__();
|
||||
}
|
||||
|
||||
BOOL DeleteDC(HDC hdc)
|
||||
{
|
||||
delete hdc;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
HBITMAP CreateDIBSection(HDC, const BITMAPINFO* pbmi, UINT, void** ppvBits, HANDLE, DWORD)
|
||||
{
|
||||
if (!pbmi || pbmi->bmiHeader.biBitCount != 32 || pbmi->bmiHeader.biCompression != BI_RGB) return nullptr;
|
||||
const LONG w = pbmi->bmiHeader.biWidth;
|
||||
const LONG h = pbmi->bmiHeader.biHeight;
|
||||
if (w <= 0 || h == 0) return nullptr;
|
||||
auto* bmp = new HBITMAP__();
|
||||
bmp->kind = GdiKind::Bitmap;
|
||||
bmp->width = int(w);
|
||||
bmp->height = int(h < 0 ? -h : h);
|
||||
bmp->top_down = h < 0;
|
||||
bmp->bits.assign(size_t(bmp->width) * size_t(bmp->height), 0);
|
||||
if (ppvBits) *ppvBits = bmp->bits.data();
|
||||
return bmp;
|
||||
}
|
||||
|
||||
// The only caller is CGraphicDib::Put onto a window DC, which this platform does not have.
|
||||
int SetDIBitsToDevice(HDC, int, int, DWORD, DWORD h, int, int, UINT, UINT, const void*, const BITMAPINFO*, UINT)
|
||||
{
|
||||
return int(h);
|
||||
}
|
||||
|
||||
HFONT CreateFontIndirect(const LOGFONT* lplf)
|
||||
{
|
||||
if (!lplf) return nullptr;
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
FT_Face face = open_named(lplf->lfFaceName);
|
||||
for (const char* name : kSubstitutes) {
|
||||
if (face) break;
|
||||
face = open_named(name);
|
||||
}
|
||||
if (!face) return nullptr;
|
||||
|
||||
auto* font = new HFONT__();
|
||||
font->kind = GdiKind::Font;
|
||||
font->face = face;
|
||||
for (const char* name : kLinked)
|
||||
if (FT_Face linked = open_named(name); linked && linked != face) font->fallbacks.push_back(linked);
|
||||
font->italic = lplf->lfItalic != 0;
|
||||
font->antialiased = lplf->lfQuality == ANTIALIASED_QUALITY;
|
||||
|
||||
const TT_OS2* os2 = static_cast<const TT_OS2*>(FT_Get_Sfnt_Table(face, FT_SFNT_OS2));
|
||||
const double upem = face->units_per_EM ? face->units_per_EM : 2048;
|
||||
const double win_ascent = os2 && os2->usWinAscent ? os2->usWinAscent : face->ascender;
|
||||
const double win_descent = os2 && os2->usWinAscent ? os2->usWinDescent : -face->descender;
|
||||
LONG height = lplf->lfHeight ? lplf->lfHeight : 12;
|
||||
int ppem = height > 0 ? int(std::lround(height * upem / (win_ascent + win_descent))) : int(-height);
|
||||
font->ppem = std::max(ppem, 1);
|
||||
font->ascent = int(std::lround(win_ascent * font->ppem / upem));
|
||||
font->descent = int(std::lround(win_descent * font->ppem / upem));
|
||||
return font;
|
||||
}
|
||||
|
||||
BOOL DeleteObject(HGDIOBJ ho)
|
||||
{
|
||||
if (!ho) return FALSE;
|
||||
auto* object = static_cast<GdiObject*>(ho);
|
||||
if (object->kind == GdiKind::Bitmap) delete static_cast<HBITMAP__*>(object);
|
||||
else delete static_cast<HFONT__*>(object); // faces stay cached for the process
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
HGDIOBJ SelectObject(HDC hdc, HGDIOBJ h)
|
||||
{
|
||||
if (!hdc || !h) return nullptr;
|
||||
auto* object = static_cast<GdiObject*>(h);
|
||||
if (object->kind == GdiKind::Bitmap) {
|
||||
HGDIOBJ old = hdc->bitmap;
|
||||
hdc->bitmap = static_cast<HBITMAP__*>(object);
|
||||
return old;
|
||||
}
|
||||
HGDIOBJ old = hdc->font;
|
||||
hdc->font = static_cast<HFONT__*>(object);
|
||||
return old;
|
||||
}
|
||||
|
||||
COLORREF SetTextColor(HDC hdc, COLORREF color)
|
||||
{
|
||||
const COLORREF old = hdc->text;
|
||||
hdc->text = color;
|
||||
return old;
|
||||
}
|
||||
|
||||
COLORREF SetBkColor(HDC hdc, COLORREF color)
|
||||
{
|
||||
const COLORREF old = hdc->bk;
|
||||
hdc->bk = color;
|
||||
return old;
|
||||
}
|
||||
|
||||
int SetBkMode(HDC hdc, int mode)
|
||||
{
|
||||
const int old = hdc->bk_mode;
|
||||
hdc->bk_mode = mode;
|
||||
return old;
|
||||
}
|
||||
|
||||
BOOL GetTextExtentPoint32W(HDC hdc, LPCWSTR lpString, int c, LPSIZE psizl)
|
||||
{
|
||||
if (!hdc || !psizl) return FALSE;
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
HFONT font = dc_font(hdc);
|
||||
if (!font) return FALSE;
|
||||
LONG cx = 0;
|
||||
for (int i = 0; i < c; ++i) {
|
||||
Glyph glyph;
|
||||
if (load_glyph(font, lpString[i], false, &glyph)) cx += glyph.advance;
|
||||
}
|
||||
psizl->cx = cx;
|
||||
psizl->cy = font->ascent + font->descent;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL GetCharABCWidthsFloatW(HDC hdc, UINT iFirst, UINT iLast, LPABCFLOAT lpABC)
|
||||
{
|
||||
if (!hdc || !lpABC || iLast < iFirst) return FALSE;
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
HFONT font = dc_font(hdc);
|
||||
if (!font) return FALSE;
|
||||
for (UINT ch = iFirst; ch <= iLast; ++ch) {
|
||||
Glyph glyph;
|
||||
if (!load_glyph(font, wchar_t(ch), false, &glyph)) return FALSE;
|
||||
ABCFLOAT& abc = lpABC[ch - iFirst];
|
||||
if (glyph.width == 0) {
|
||||
// A blank glyph (space) is all advance, like GDI reports it.
|
||||
abc.abcfA = 0;
|
||||
abc.abcfB = float(glyph.advance);
|
||||
abc.abcfC = 0;
|
||||
} else {
|
||||
abc.abcfA = float(glyph.left);
|
||||
abc.abcfB = float(glyph.width);
|
||||
abc.abcfC = float(glyph.advance - glyph.left - glyph.width);
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL TextOutW(HDC hdc, int x, int y, LPCWSTR lpString, int c)
|
||||
{
|
||||
if (!hdc || !hdc->bitmap) return FALSE;
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
HFONT font = dc_font(hdc);
|
||||
if (!font) return FALSE;
|
||||
HBITMAP bmp = hdc->bitmap;
|
||||
const int height = font->ascent + font->descent;
|
||||
|
||||
std::vector<Glyph> glyphs(size_t(std::max(c, 0)));
|
||||
int extent = 0;
|
||||
for (int i = 0; i < c; ++i) {
|
||||
load_glyph(font, lpString[i], true, &glyphs[size_t(i)]);
|
||||
extent += glyphs[size_t(i)].advance;
|
||||
}
|
||||
|
||||
if (hdc->bk_mode == OPAQUE) {
|
||||
const uint32_t bk = dib_pixel(hdc->bk);
|
||||
for (int py = std::max(y, 0); py < std::min(y + height, bmp->height); ++py) {
|
||||
uint32_t* row = dib_row(bmp, py);
|
||||
for (int px = std::max(x, 0); px < std::min(x + extent, bmp->width); ++px) row[px] = bk;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t fg = dib_pixel(hdc->text);
|
||||
const uint32_t bg = dib_pixel(hdc->bk);
|
||||
int pen = x;
|
||||
for (const Glyph& glyph : glyphs) {
|
||||
const int ox = pen + glyph.left;
|
||||
const int oy = y + font->ascent - glyph.top;
|
||||
for (int gy = 0; gy < glyph.rows && !glyph.bitmap.empty(); ++gy) {
|
||||
const int py = oy + gy;
|
||||
if (py < 0 || py >= bmp->height) continue;
|
||||
uint32_t* row = dib_row(bmp, py);
|
||||
const unsigned char* src = glyph.bitmap.data() + size_t(gy) * size_t(glyph.pitch);
|
||||
for (int gx = 0; gx < glyph.width; ++gx) {
|
||||
const int px = ox + gx;
|
||||
if (px < 0 || px >= bmp->width) continue;
|
||||
unsigned coverage = glyph.mono ? ((src[gx >> 3] >> (7 - (gx & 7))) & 1) * 255 : src[gx];
|
||||
if (!coverage) continue;
|
||||
if (coverage == 255) { row[px] = fg; continue; }
|
||||
// Grayscale GDI text blends the text colour over the background colour.
|
||||
const uint32_t under = hdc->bk_mode == OPAQUE ? bg : row[px];
|
||||
uint32_t out = 0;
|
||||
for (int shift = 0; shift < 24; shift += 8) {
|
||||
const unsigned a = (fg >> shift) & 0xFF, b = (under >> shift) & 0xFF;
|
||||
out |= ((a * coverage + b * (255 - coverage) + 127) / 255) << shift;
|
||||
}
|
||||
row[px] = out;
|
||||
}
|
||||
}
|
||||
pen += glyph.advance;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL TextOutA(HDC hdc, int x, int y, LPCSTR lpString, int c)
|
||||
{
|
||||
const int n = MultiByteToWideChar(CP_ACP, 0, lpString, c, nullptr, 0);
|
||||
if (n <= 0) return c == 0;
|
||||
std::vector<WCHAR> wide(static_cast<size_t>(n));
|
||||
MultiByteToWideChar(CP_ACP, 0, lpString, c, wide.data(), n);
|
||||
return TextOutW(hdc, x, y, wide.data(), n);
|
||||
}
|
||||
Reference in New Issue
Block a user