From 91eed7db6cf559ece58261ce72110ca2d433c19d Mon Sep 17 00:00:00 2001 From: mirkobrombin Date: Thu, 29 Oct 2026 09:57:47 +0000 Subject: [PATCH] fix: enumerate font families per character set with their Win32 names --- native/desktop/gdi_api.cpp | 10 --- native/desktop/gdi_text.cpp | 173 +++++++++++++++++++++++++++--------- native/nt/font_service.cpp | 81 ++++++++++------- 3 files changed, 182 insertions(+), 82 deletions(-) diff --git a/native/desktop/gdi_api.cpp b/native/desktop/gdi_api.cpp index 27f19ee..9e5742c 100644 --- a/native/desktop/gdi_api.cpp +++ b/native/desktop/gdi_api.cpp @@ -1536,16 +1536,6 @@ void NativeDesktop::register_gdi_api(ApiRegistry ®istry) for (const auto *name : {"StartDocW", "StartDocA", "EndDoc", "StartPage", "EndPage", "AbortDoc"}) registry.add(name, 2, [](ApiCall &call) { call.fail(120, 0); }); registry.add("GdiGetCodePage", 1, [](ApiCall &call) { call.ret(1252); }); - registry.add("GetTextCharsetInfo", 3, [=, this](ApiCall &call) { - guarded(call, 0, [&] { - require_dc(call, 0); - if (call[1]) { - std::vector signature(24, 0); - call.write(call[1], signature); - } - call.ret(0); - }); - }); } diff --git a/native/desktop/gdi_text.cpp b/native/desktop/gdi_text.cpp index 0d44156..55f6c95 100644 --- a/native/desktop/gdi_text.cpp +++ b/native/desktop/gdi_text.cpp @@ -73,14 +73,20 @@ std::vector enumerable_families() // The LOGFONT of an enumerated family and the NEWTEXTMETRICEX that follows the ENUMLOGFONTEX at metrics: the ANSI forms keep characters // in bytes, so their fields after tmLastChar sit four bytes earlier. Sizes are those of a 12-pixel em. -void describe_family(std::vector &record, size_t metrics, const InstalledFamily &family, bool ansi) +void describe_family(std::vector &record, size_t metrics, const InstalledFamily &family, bool ansi, uint8_t charset) { const auto put = [&](size_t at, int32_t value) { std::memcpy(record.data() + at, &value, 4); }; const int32_t weight = int32_t(family.weight); - put(0, -12); + // The LOGFONT of a TrueType face as the enumeration gives it: the cell height and average width of the metrics below, stroke + // output and clip precision and proof quality. + put(0, 16); + put(4, 7); put(16, weight); record[20] = family.italic; - record[23] = 0; + record[23] = charset; + record[24] = 3; + record[25] = 2; + record[26] = 1; record[27] = family.fixed ? 0x31 : 0x22; put(metrics, 16); put(metrics + 4, 13); @@ -106,7 +112,7 @@ void describe_family(std::vector &record, size_t metrics, const Install record[at] = family.italic; // TMPF_TRUETYPE | TMPF_VECTOR, and the bit Windows sets for a variable pitch, under the family kind. record[at + 3] = uint8_t((family.fixed ? 0x06 : 0x07) | (family.fixed ? 0x30 : 0x20)); - record[at + 4] = 0; + record[at + 4] = charset; at += 8; put(at, (family.weight >= 600 ? 0x20 : 0) | (family.italic ? 0x01 : 0) | (family.weight < 600 && !family.italic ? 0x40 : 0)); put(at + 4, 2048); @@ -125,6 +131,46 @@ std::u16string_view style_of(const InstalledFamily &family) return family.italic ? u"Italic" : u"Regular"; } +// The character sets a face covers, from the code page bits of its OS/2 table in the order TranslateCharsetInfo gives them, each with +// the script name EnumFontFamiliesEx reports. A symbol font has the symbol set alone; a face that claims none is Western. +struct FontScript { + uint8_t charset; + const char16_t *script; +}; + +std::vector scripts_of(const InstalledFamily &family) +{ + static const struct { int bit; FontScript script; } bits[] = { + {0, {0, u"Western"}}, {1, {238, u"Central European"}}, {2, {204, u"Cyrillic"}}, {3, {161, u"Greek"}}, {4, {162, u"Turkish"}}, + {5, {177, u"Hebrew"}}, {6, {178, u"Arabic"}}, {7, {186, u"Baltic"}}, {8, {163, u"Vietnamese"}}, {16, {222, u"Thai"}}, + {17, {128, u"Japanese"}}, {18, {134, u"CHINESE_GB2312"}}, {19, {129, u"Hangul"}}, {20, {136, u"CHINESE_BIG5"}}, + {21, {130, u"Hangul(Johab)"}}}; + if (family.code_pages[0] & 0x80000000U) + return {{2, u"Symbol"}}; + std::vector scripts; + for (const auto &entry : bits) + if (family.code_pages[0] & (1U << entry.bit)) + scripts.push_back(entry.script); + if (scripts.empty()) + scripts.push_back({0, u"Western"}); + return scripts; +} + +// The reserved last argument of EnumFontFamiliesEx, which callers pass as zero, carries this when EnumFontFamilies forwards to it. +constexpr uint64_t once_marker = 0x52564e31; + +// The records EnumFontFamiliesEx hands out for a family: one per character set it covers when DEFAULT_CHARSET (or no LOGFONT) asks +// for any, the one for the set asked for otherwise, and the first alone for the older EnumFontFamilies, which lists a family once. +std::vector enumerated_scripts(const InstalledFamily &family, int charset, bool once) +{ + auto scripts = scripts_of(family); + if (once) + scripts.resize(1); + else if (charset != 1) + std::erase_if(scripts, [&](const FontScript &script) { return script.charset != charset; }); + return scripts; +} + } void NativeDesktop::register_text_api(ApiRegistry ®istry) @@ -401,7 +447,7 @@ void NativeDesktop::register_text_api(ApiRegistry ®istry) bytes[at + 1] = font.definition[21]; bytes[at + 2] = font.definition[22]; bytes[at + 3] = info.pitch; - bytes[at + 4] = font.definition[23]; + bytes[at + 4] = font.definition[23] == 1 ? 0 : font.definition[23]; call.write(call[1], bytes); call.ret(1); }); @@ -451,6 +497,36 @@ void NativeDesktop::register_text_api(ApiRegistry ®istry) call.ret(face.size() + 1); }); }); + // GetTextCharsetInfo(dc, FONTSIGNATURE, flags): the character set of the selected font, and the scripts and code pages its OS/2 table + // claims; a face the system does not enumerate stands in for the default face, which covers Latin. + registry.add("GetTextCharsetInfo", 3, [=, this](ApiCall &call) { + guarded(call, 1, [&] { + auto &dc = require_dc(call, 0); + const auto &font = selected_font(dc); + std::u16string face; + for (size_t at = 28; at + 1 < font.definition.size() && at < 28 + 64; at += 2) { + const char16_t character = char16_t(font.definition[at] | font.definition[at + 1] << 8); + if (!character) + break; + face.push_back(character); + } + InstalledFamily family{face}; + family.unicode_ranges[0] = 3; + family.code_pages[0] = 1; + const auto families = enumerable_families(); + if (const auto found = std::find_if(families.begin(), families.end(), [&](const InstalledFamily &item) { return equal_fold(item.name, face); }); + found != families.end()) + family = *found; + if (call[1]) { + std::vector signature(24, 0); + std::memcpy(signature.data(), family.unicode_ranges, 16); + std::memcpy(signature.data() + 16, family.code_pages, 8); + call.write(call[1], signature); + } + // DEFAULT_CHARSET is realized as the ANSI set; any other request comes back as asked. + call.ret(font.definition[23] == 1 ? 0 : font.definition[23]); + }); + }); // GetGlyphOutlineW(dc, character, format, GLYPHMETRICS, size, buffer, MAT2): the metrics of a character of the selected font and its // bitmap, rendered as text draws it: GGO_METRICS (0), GGO_BITMAP (1, a bit per pixel) and GGO_GRAY2, GRAY4 and GRAY8 (4, 5, 6: 4, 16 // and 64 levels a byte), rows padded to four bytes. Glyph indices, outlines and transforms other than the identity are not drawn: @@ -579,7 +655,7 @@ void NativeDesktop::register_text_api(ApiRegistry ®istry) const uint16_t characters[4] = {info.first, info.last, info.default_character, info.break_character}; std::memcpy(bytes.data() + 48, characters, 8); bytes[56] = font.definition[20]; bytes[57] = font.definition[21]; bytes[58] = font.definition[22]; bytes[59] = info.pitch; - bytes[60] = font.definition[23]; + bytes[60] = font.definition[23] == 1 ? 0 : font.definition[23]; const int32_t em = std::max(info.height - info.internal_leading, 1); put32(76, (weight >= 700 ? 0x20 : 0) | (font.definition[20] ? 1 : 0) | 0x40); const uint32_t design_em = objects.text().logical_units_per_em(font.definition); @@ -785,7 +861,9 @@ void NativeDesktop::register_text_api(ApiRegistry ®istry) require_dc(call, 0); const auto families = enumerable_families(); std::u16string wanted; + int charset = 1; if (call[1]) { + charset = call.read(call[1] + 23, 1)[0]; const auto bytes = call.read(call[1] + 28, 64); for (size_t i = 0; i < 32; ++i) { const char16_t c = char16_t(bytes[i * 2] | (bytes[i * 2 + 1] << 8)); @@ -816,26 +894,30 @@ void NativeDesktop::register_text_api(ApiRegistry ®istry) for (const auto &family : families) { if (!wanted.empty() && lower(family.name) != lower(wanted)) continue; - // ENUMLOGFONTEXW (LOGFONTW, full name, style, script) followed by NEWTEXTMETRICEXW. - std::vector record(512, 0); - describe_family(record, 348, family, false); - const auto put_text = [&](size_t at, std::u16string_view text, size_t limit) { - for (size_t i = 0; i < std::min(text.size(), limit); ++i) { - record[at + i * 2] = uint8_t(text[i]); - record[at + i * 2 + 1] = uint8_t(text[i] >> 8); - } - }; - put_text(28, family.name, 31); - put_text(92, family.name, 63); - put_text(92 + 128, style_of(family), 31); - put_text(92 + 192, u"Western", 31); - call.write(scratch, record); - const uint64_t arguments[] = {scratch, scratch + 348, 4, call[3]}; - const auto outcome = invoke(call[2], arguments); - if (outcome.stop != NativeStop::returned) - throw outcome; - result = outcome.value; - if (!uint32_t(outcome.value)) + for (const auto &script : enumerated_scripts(family, charset, call[4] == once_marker)) { + // ENUMLOGFONTEXW (LOGFONTW, full name, style, script) followed by NEWTEXTMETRICEXW. + std::vector record(512, 0); + describe_family(record, 348, family, false, script.charset); + const auto put_text = [&](size_t at, std::u16string_view text, size_t limit) { + for (size_t i = 0; i < std::min(text.size(), limit); ++i) { + record[at + i * 2] = uint8_t(text[i]); + record[at + i * 2 + 1] = uint8_t(text[i] >> 8); + } + }; + put_text(28, family.name, 31); + put_text(92, family.name, 63); + put_text(92 + 128, style_of(family), 31); + put_text(92 + 192, script.script, 31); + call.write(scratch, record); + const uint64_t arguments[] = {scratch, scratch + 348, 4, call[3]}; + const auto outcome = invoke(call[2], arguments); + if (outcome.stop != NativeStop::returned) + throw outcome; + result = outcome.value; + if (!uint32_t(outcome.value)) + break; + } + if (!uint32_t(result)) break; } } catch (...) { @@ -869,7 +951,7 @@ void NativeDesktop::register_text_api(ApiRegistry ®istry) const auto block = process.map_memory(4096, PROT_READ | PROT_WRITE); try { call.write(block, logical); - ApiCall inner(call.process, call.environment, {call[0], block, call[2], call[3], 0}); + ApiCall inner(call.process, call.environment, {call[0], block, call[2], call[3], once_marker}); extended->second(inner); process.unmap_memory(block); call.ret(inner.result); @@ -889,7 +971,9 @@ void NativeDesktop::register_text_api(ApiRegistry ®istry) require_dc(call, 0); const auto families = enumerable_families(); std::string wanted; + int charset = 1; if (call[1]) { + charset = call.read(call[1] + 23, 1)[0]; const auto bytes = call.read(call[1] + 28, 32); for (size_t i = 0; i < 32 && bytes[i]; ++i) wanted.push_back(char(bytes[i])); @@ -911,21 +995,26 @@ void NativeDesktop::register_text_api(ApiRegistry ®istry) const auto name = to_ansi(family.name, 1252); if (!wanted.empty() && lower(name) != lower(wanted)) continue; - // ENUMLOGFONTEXA (LOGFONTA, full name, style, script) followed by NEWTEXTMETRICEXA. - std::vector record(512, 0); - describe_family(record, 256, family, true); - const auto style = to_ansi(style_of(family), 1252); - std::memcpy(record.data() + 28, name.data(), std::min(name.size(), 31)); - std::memcpy(record.data() + 60, name.data(), std::min(name.size(), 63)); - std::memcpy(record.data() + 124, style.data(), std::min(style.size(), 31)); - std::memcpy(record.data() + 156, "Western", 7); - call.write(scratch, record); - const uint64_t arguments[] = {scratch, scratch + 256, 4, call[3]}; - const auto outcome = invoke(call[2], arguments); - if (outcome.stop != NativeStop::returned) - throw outcome; - result = outcome.value; - if (!uint32_t(outcome.value)) + for (const auto &script : enumerated_scripts(family, charset, call[4] == once_marker)) { + // ENUMLOGFONTEXA (LOGFONTA, full name, style, script) followed by NEWTEXTMETRICEXA. + std::vector record(512, 0); + describe_family(record, 256, family, true, script.charset); + const auto style = to_ansi(style_of(family), 1252); + const auto script_name = to_ansi(script.script, 1252); + std::memcpy(record.data() + 28, name.data(), std::min(name.size(), 31)); + std::memcpy(record.data() + 60, name.data(), std::min(name.size(), 63)); + std::memcpy(record.data() + 124, style.data(), std::min(style.size(), 31)); + std::memcpy(record.data() + 156, script_name.data(), std::min(script_name.size(), 31)); + call.write(scratch, record); + const uint64_t arguments[] = {scratch, scratch + 256, 4, call[3]}; + const auto outcome = invoke(call[2], arguments); + if (outcome.stop != NativeStop::returned) + throw outcome; + result = outcome.value; + if (!uint32_t(outcome.value)) + break; + } + if (!uint32_t(result)) break; } } catch (...) { diff --git a/native/nt/font_service.cpp b/native/nt/font_service.cpp index e74c56c..fbea8f8 100644 --- a/native/nt/font_service.cpp +++ b/native/nt/font_service.cpp @@ -62,7 +62,40 @@ void put_text(std::vector &bytes, const char *text) bytes.push_back(0); } -// The system fonts, as records the guest reads: sizes, weight, style, width, flags, then path, family, face, full and PostScript names. +// The English name a face's name table gives for an id, in UTF-8: Windows names are UTF-16 big endian, Macintosh names single byte. +std::string sfnt_name(FT_Face face, uint32_t id) +{ + std::string result; + const FT_UInt count = FT_Get_Sfnt_Name_Count(face); + for (FT_UInt index = 0; index < count && result.empty(); ++index) { + FT_SfntName name; + if (FT_Get_Sfnt_Name(face, index, &name) || name.name_id != id) + continue; + if (name.platform_id == 3 && (name.language_id & 0x3ff) == 9) { + for (FT_UInt at = 0; at + 1 < name.string_len; at += 2) { + const uint32_t unit = uint32_t(name.string[at]) << 8 | name.string[at + 1]; + if (unit < 0x80) + result.push_back(char(unit)); + else if (unit < 0x800) { + result.push_back(char(0xc0 | unit >> 6)); + result.push_back(char(0x80 | (unit & 0x3f))); + } else { + result.push_back(char(0xe0 | unit >> 12)); + result.push_back(char(0x80 | ((unit >> 6) & 0x3f))); + result.push_back(char(0x80 | (unit & 0x3f))); + } + } + } else if (name.platform_id == 1 && name.language_id == 0) { + result.assign(reinterpret_cast(name.string), name.string_len); + } + } + return result; +} + +// The system fonts, as records the guest reads: sizes, weight, style, width, flags, then path, family, face, full and PostScript names, +// then the Win32 family and the typographic family and face. The family and face are the weight-stretch-style ones DirectWrite groups +// by (name ids 21 and 22, else 1 and 2); GDI names a family by its Win32 name (id 1), which keeps Calibri Light or Arial Narrow apart +// from Calibri and Arial, where FreeType's family name is the typographic one (id 16). // They are the fonts installed in the Windows Fonts folder of the prefix, which links the host's fonts and keeps the ones programs // install; without a prefix, the host's own font folders. std::vector scan_fonts() @@ -112,7 +145,16 @@ std::vector scan_fonts() } const uint32_t style = (face->style_flags & FT_STYLE_FLAG_ITALIC) ? 2 : 0; const uint32_t flags = (FT_IS_FIXED_WIDTH(face) ? 2U : 0U); - InstalledFamily found{from_utf8(face->family_name), weight, style != 0, (flags & 2) != 0}; + const auto named = [&](uint32_t id, uint32_t fallback, const char *last) { + auto text = sfnt_name(face, id); + if (text.empty()) + text = sfnt_name(face, fallback); + return text.empty() ? std::string(last ? last : "") : text; + }; + const auto win32_family = named(1, 1, face->family_name), win32_face = named(2, 2, face->style_name); + const auto family = named(21, 1, face->family_name), style_name = named(22, 2, face->style_name); + const auto typographic_family = named(16, 1, face->family_name), typographic_face = named(17, 2, face->style_name); + InstalledFamily found{from_utf8(win32_family), weight, style != 0, (flags & 2) != 0}; if (const auto *os2 = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_OS2))) { found.unicode_ranges[0] = uint32_t(os2->ulUnicodeRange1); found.unicode_ranges[1] = uint32_t(os2->ulUnicodeRange2); @@ -135,11 +177,14 @@ std::vector scan_fonts() put32(record, stretch); put32(record, flags); put_text(record, walk->path().c_str()); - put_text(record, face->family_name); - put_text(record, face->style_name); - std::string full = std::string(face->family_name) + (face->style_name ? std::string(" ") + face->style_name : std::string()); + put_text(record, family.c_str()); + put_text(record, style_name.c_str()); + const auto full = named(4, 4, (win32_family + (win32_face.empty() ? "" : " " + win32_face)).c_str()); put_text(record, full.c_str()); put_text(record, FT_Get_Postscript_Name(face)); + put_text(record, win32_family.c_str()); + put_text(record, typographic_family.c_str()); + put_text(record, typographic_face.c_str()); const uint32_t size = uint32_t(record.size()); std::memcpy(record.data(), &size, 4); cache.insert(cache.end(), record.begin(), record.end()); @@ -448,31 +493,7 @@ void register_font_service(ApiRegistry ®istry, KernelContext &) call.ret(0); return; } - std::string result; - const FT_UInt count = FT_Get_Sfnt_Name_Count(font->face); - for (FT_UInt index = 0; index < count && result.empty(); ++index) { - FT_SfntName name; - if (FT_Get_Sfnt_Name(font->face, index, &name) || name.name_id != call.u32(1)) - continue; - // Windows names are UTF-16 big endian; Macintosh names are single byte. - if (name.platform_id == 3 && (name.language_id & 0x3ff) == 9) { - for (FT_UInt at = 0; at + 1 < name.string_len; at += 2) { - const uint32_t unit = uint32_t(name.string[at]) << 8 | name.string[at + 1]; - if (unit < 0x80) - result.push_back(char(unit)); - else if (unit < 0x800) { - result.push_back(char(0xc0 | unit >> 6)); - result.push_back(char(0x80 | (unit & 0x3f))); - } else { - result.push_back(char(0xe0 | unit >> 12)); - result.push_back(char(0x80 | ((unit >> 6) & 0x3f))); - result.push_back(char(0x80 | (unit & 0x3f))); - } - } - } else if (name.platform_id == 1 && name.language_id == 0) { - result.assign(reinterpret_cast(name.string), name.string_len); - } - } + std::string result = sfnt_name(font->face, call.u32(1)); if (call[2] && call.u32(3) > result.size()) { result.push_back('\0'); call.write(call[2], std::span(reinterpret_cast(result.data()), result.size()));