// The 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 #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 #include #include #include #include #include #include #include #include #include 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 bits; }; struct HFONT__ : GdiObject { FT_Face face = nullptr; std::vector 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 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(c))); return s; } const std::vector& font_dirs() { static const std::vector 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 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 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(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(ho); if (object->kind == GdiKind::Bitmap) delete static_cast(object); else delete static_cast(object); // faces stay cached for the process return TRUE; } HGDIOBJ SelectObject(HDC hdc, HGDIOBJ h) { if (!hdc || !h) return nullptr; auto* object = static_cast(h); if (object->kind == GdiKind::Bitmap) { HGDIOBJ old = hdc->bitmap; hdc->bitmap = static_cast(object); return old; } HGDIOBJ old = hdc->font; hdc->font = static_cast(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 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 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 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 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 wide(static_cast(n)); MultiByteToWideChar(CP_ACP, 0, lpString, c, wide.data(), n); return TextOutW(hdc, x, y, wide.data(), n); }