1 // Copyright (c) 2015 Ryan Prichard
3 // Permission is hereby granted, free of charge, to any person obtaining a copy
4 // of this software and associated documentation files (the "Software"), to
5 // deal in the Software without restriction, including without limitation the
6 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 // sell copies of the Software, and to permit persons to whom the Software is
8 // furnished to do so, subject to the following conditions:
10 // The above copyright notice and this permission notice shall be included in
11 // all copies or substantial portions of the Software.
13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 #include "ConsoleFont.h"
34 #include "../shared/DebugClient.h"
35 #include "../shared/OsModule.h"
36 #include "../shared/StringUtil.h"
37 #include "../shared/WindowsVersion.h"
38 #include "../shared/WinptyAssert.h"
39 #include "../shared/winpty_snprintf.h"
43 #define COUNT_OF(x) (sizeof(x) / sizeof((x)[0]))
45 // See https://en.wikipedia.org/wiki/List_of_CJK_fonts
46 const wchar_t kLucidaConsole[] = L"Lucida Console";
47 const wchar_t kMSGothic[] = { 0xff2d, 0xff33, 0x0020, 0x30b4, 0x30b7, 0x30c3, 0x30af, 0 }; // 932, Japanese
48 const wchar_t kNSimSun[] = { 0x65b0, 0x5b8b, 0x4f53, 0 }; // 936, Chinese Simplified
49 const wchar_t kGulimChe[] = { 0xad74, 0xb9bc, 0xccb4, 0 }; // 949, Korean
50 const wchar_t kMingLight[] = { 0x7d30, 0x660e, 0x9ad4, 0 }; // 950, Chinese Traditional
58 const wchar_t *faceName;
63 // Ideographs in East Asian languages take two columns rather than one.
64 // In the console screen buffer, a "full-width" character will occupy two
65 // cells of the buffer, the first with attribute 0x100 and the second with
68 // Windows does not correctly identify code points as double-width in all
69 // configurations. It depends heavily on the code page, the font facename,
70 // and (somehow) even the font size. In the 437 code page (MS-DOS), for
71 // example, no codepoints are interpreted as double-width. When the console
72 // is in an East Asian code page (932, 936, 949, or 950), then sometimes
73 // selecting a "Western" facename like "Lucida Console" or "Consolas" doesn't
74 // register, or if the font *can* be chosen, then the console doesn't handle
75 // double-width correctly. I tested the double-width handling by writing
76 // several code points with WriteConsole and checking whether one or two cells
79 // In the Japanese code page (932), Microsoft's default font is MS Gothic.
80 // MS Gothic double-width handling seems to be broken with console versions
81 // prior to Windows 10 (including Windows 10's legacy mode), and it's
82 // especially broken in Windows 8 and 8.1.
84 // Test by running: misc/Utf16Echo A2 A3 2014 3044 30FC 4000
86 // The first three codepoints are always rendered as half-width with the
87 // Windows Japanese fonts. (Of these, the first two must be half-width,
88 // but U+2014 could be either.) The last three are rendered as full-width,
89 // and they are East_Asian_Width=Wide.
91 // Windows 7 fails by modeling all codepoints as full-width with font
92 // sizes 22 and above.
94 // Windows 8 gets U+00A2, U+00A3, U+2014, U+30FC, and U+4000 wrong, but
95 // using a point size not listed in the console properties dialog
96 // (e.g. "9") is less wrong:
99 // font | 00A2 00A3 2014 3044 30FC 4000 | cell size
100 // ------------+---------------------------------+----------
101 // 8 | F F F F H H | 4x8
102 // 9 | F F F F F F | 5x9
103 // 16 | F F F F H H | 8x16
104 // raster 6x13 | H H H F F H(*) | 6x13
106 // (*) The Raster Font renders U+4000 as a white box (i.e. an unsupported
111 // - misc/Font-Report-June2016 directory for per-size details
112 // - misc/font-notes.txt
113 // - misc/Utf16Echo.cc, misc/FontSurvey.cc, misc/SetFont.cc, misc/GetFont.cc
115 const FontSize kLucidaFontSizes[] = {
131 // Japanese. Used on Vista and Windows 7.
132 const FontSize k932GothicVista[] = {
142 // All larger fonts are more broken w.r.t. full-size East Asian characters.
145 // Japanese. Used on Windows 8, 8.1, and the legacy 10 console.
146 const FontSize k932GothicWin8[] = {
147 // All of these characters are broken w.r.t. full-size East Asian
148 // characters, but they're equally broken.
159 // include extra-large fonts for small terminals
166 // Japanese. Used on the new Windows 10 console.
167 const FontSize k932GothicWin10[] = {
178 // include extra-large fonts for small terminals
185 // Chinese Simplified.
186 const FontSize k936SimSun[] = {
197 // include extra-large fonts for small terminals
205 const FontSize k949GulimChe[] = {
216 // include extra-large fonts for small terminals
223 // Chinese Traditional.
224 const FontSize k950MingLight[] = {
235 // include extra-large fonts for small terminals
242 // Some of these types and functions are missing from the MinGW headers.
243 // Others are undocumented.
245 struct AGENT_CONSOLE_FONT_INFO {
250 struct AGENT_CONSOLE_FONT_INFOEX {
256 WCHAR FaceName[LF_FACESIZE];
259 // undocumented XP API
260 typedef BOOL WINAPI SetConsoleFont_t(
264 // undocumented XP API
265 typedef DWORD WINAPI GetNumberOfConsoleFonts_t();
268 typedef BOOL WINAPI GetCurrentConsoleFont_t(
271 AGENT_CONSOLE_FONT_INFO *lpConsoleCurrentFont);
274 typedef COORD WINAPI GetConsoleFontSize_t(
275 HANDLE hConsoleOutput,
279 typedef BOOL WINAPI GetCurrentConsoleFontEx_t(
280 HANDLE hConsoleOutput,
282 AGENT_CONSOLE_FONT_INFOEX *lpConsoleCurrentFontEx);
285 typedef BOOL WINAPI SetCurrentConsoleFontEx_t(
286 HANDLE hConsoleOutput,
288 AGENT_CONSOLE_FONT_INFOEX *lpConsoleCurrentFontEx);
290 #define GET_MODULE_PROC(mod, funcName) \
291 m_##funcName = reinterpret_cast<funcName##_t*>((mod).proc(#funcName)); \
293 #define DEFINE_ACCESSOR(funcName) \
294 funcName##_t &funcName() const { \
296 return *m_##funcName; \
301 XPFontAPI() : m_kernel32(L"kernel32.dll") {
302 GET_MODULE_PROC(m_kernel32, GetCurrentConsoleFont);
303 GET_MODULE_PROC(m_kernel32, GetConsoleFontSize);
307 return m_GetCurrentConsoleFont != NULL &&
308 m_GetConsoleFontSize != NULL;
311 DEFINE_ACCESSOR(GetCurrentConsoleFont)
312 DEFINE_ACCESSOR(GetConsoleFontSize)
316 GetCurrentConsoleFont_t *m_GetCurrentConsoleFont;
317 GetConsoleFontSize_t *m_GetConsoleFontSize;
320 class UndocumentedXPFontAPI : public XPFontAPI {
322 UndocumentedXPFontAPI() : m_kernel32(L"kernel32.dll") {
323 GET_MODULE_PROC(m_kernel32, SetConsoleFont);
324 GET_MODULE_PROC(m_kernel32, GetNumberOfConsoleFonts);
328 return this->XPFontAPI::valid() &&
329 m_SetConsoleFont != NULL &&
330 m_GetNumberOfConsoleFonts != NULL;
333 DEFINE_ACCESSOR(SetConsoleFont)
334 DEFINE_ACCESSOR(GetNumberOfConsoleFonts)
338 SetConsoleFont_t *m_SetConsoleFont;
339 GetNumberOfConsoleFonts_t *m_GetNumberOfConsoleFonts;
342 class VistaFontAPI : public XPFontAPI {
344 VistaFontAPI() : m_kernel32(L"kernel32.dll") {
345 GET_MODULE_PROC(m_kernel32, GetCurrentConsoleFontEx);
346 GET_MODULE_PROC(m_kernel32, SetCurrentConsoleFontEx);
350 return this->XPFontAPI::valid() &&
351 m_GetCurrentConsoleFontEx != NULL &&
352 m_SetCurrentConsoleFontEx != NULL;
355 DEFINE_ACCESSOR(GetCurrentConsoleFontEx)
356 DEFINE_ACCESSOR(SetCurrentConsoleFontEx)
360 GetCurrentConsoleFontEx_t *m_GetCurrentConsoleFontEx;
361 SetCurrentConsoleFontEx_t *m_SetCurrentConsoleFontEx;
364 static std::vector<std::pair<DWORD, COORD> > readFontTable(
365 XPFontAPI &api, HANDLE conout, DWORD maxCount) {
366 std::vector<std::pair<DWORD, COORD> > ret;
367 for (DWORD i = 0; i < maxCount; ++i) {
368 COORD size = api.GetConsoleFontSize()(conout, i);
369 if (size.X == 0 && size.Y == 0) {
372 ret.push_back(std::make_pair(i, size));
377 static void dumpFontTable(HANDLE conout, const char *prefix) {
378 const int kMaxCount = 1000;
379 if (!isTracingEnabled()) {
384 trace("dumpFontTable: cannot dump font table -- missing APIs");
387 std::vector<std::pair<DWORD, COORD> > table =
388 readFontTable(api, conout, kMaxCount);
392 while (first < table.size()) {
393 size_t last = std::min(table.size() - 1, first + 10 - 1);
394 winpty_snprintf(tmp, "%sfonts %02u-%02u:",
395 prefix, static_cast<unsigned>(first), static_cast<unsigned>(last));
397 for (size_t i = first; i <= last; ++i) {
401 winpty_snprintf(tmp, " %2dx%-2d",
402 table[i].second.X, table[i].second.Y);
405 trace("%s", line.c_str());
408 if (table.size() == kMaxCount) {
409 trace("%sfonts: ... stopped reading at %d fonts ...",
414 static std::string stringToCodePoints(const std::wstring &str) {
415 std::string ret = "(";
416 for (size_t i = 0; i < str.size(); ++i) {
418 winpty_snprintf(tmp, "%X", str[i]);
419 if (ret.size() > 1) {
428 static void dumpFontInfoEx(
429 const AGENT_CONSOLE_FONT_INFOEX &infoex,
430 const char *prefix) {
431 if (!isTracingEnabled()) {
434 std::wstring faceName(infoex.FaceName,
435 winpty_wcsnlen(infoex.FaceName, COUNT_OF(infoex.FaceName)));
436 trace("%snFont=%u dwFontSize=(%d,%d) "
437 "FontFamily=0x%x FontWeight=%u FaceName=%s %s",
439 static_cast<unsigned>(infoex.nFont),
440 infoex.dwFontSize.X, infoex.dwFontSize.Y,
441 infoex.FontFamily, infoex.FontWeight, utf8FromWide(faceName).c_str(),
442 stringToCodePoints(faceName).c_str());
445 static void dumpVistaFont(VistaFontAPI &api, HANDLE conout, const char *prefix) {
446 if (!isTracingEnabled()) {
449 AGENT_CONSOLE_FONT_INFOEX infoex = {0};
450 infoex.cbSize = sizeof(infoex);
451 if (!api.GetCurrentConsoleFontEx()(conout, FALSE, &infoex)) {
452 trace("GetCurrentConsoleFontEx call failed");
455 dumpFontInfoEx(infoex, prefix);
458 static void dumpXPFont(XPFontAPI &api, HANDLE conout, const char *prefix) {
459 if (!isTracingEnabled()) {
462 AGENT_CONSOLE_FONT_INFO info = {0};
463 if (!api.GetCurrentConsoleFont()(conout, FALSE, &info)) {
464 trace("GetCurrentConsoleFont call failed");
467 trace("%snFont=%u dwFontSize=(%d,%d)",
469 static_cast<unsigned>(info.nFont),
470 info.dwFontSize.X, info.dwFontSize.Y);
473 static bool setFontVista(
477 AGENT_CONSOLE_FONT_INFOEX infoex = {};
478 infoex.cbSize = sizeof(AGENT_CONSOLE_FONT_INFOEX);
479 infoex.dwFontSize.Y = font.size;
480 infoex.FontFamily = font.family;
481 infoex.FontWeight = 400;
482 winpty_wcsncpy_nul(infoex.FaceName, font.faceName);
483 dumpFontInfoEx(infoex, "setFontVista: setting font to: ");
484 if (!api.SetCurrentConsoleFontEx()(conout, FALSE, &infoex)) {
485 trace("setFontVista: SetCurrentConsoleFontEx call failed");
488 memset(&infoex, 0, sizeof(infoex));
489 infoex.cbSize = sizeof(infoex);
490 if (!api.GetCurrentConsoleFontEx()(conout, FALSE, &infoex)) {
491 trace("setFontVista: GetCurrentConsoleFontEx call failed");
494 if (wcsncmp(infoex.FaceName, font.faceName,
495 COUNT_OF(infoex.FaceName)) != 0) {
496 trace("setFontVista: face name was not set");
497 dumpFontInfoEx(infoex, "setFontVista: post-call font: ");
500 // We'd like to verify that the new font size is correct, but we can't
501 // predict what it will be, even though we just set it to `pxSize` through
502 // an apprently symmetric interface. For the Chinese and Korean fonts, the
503 // new `infoex.dwFontSize.Y` value can be slightly larger than the height
508 static Font selectSmallFont(int codePage, int columns, bool isNewW10) {
509 // Iterate over a set of font sizes according to the code page, and select
512 const wchar_t *faceName = nullptr;
513 unsigned int fontFamily = 0;
514 const FontSize *table = nullptr;
515 size_t tableSize = 0;
518 case 932: // Japanese
519 faceName = kMSGothic;
522 table = k932GothicWin10;
523 tableSize = COUNT_OF(k932GothicWin10);
524 } else if (isAtLeastWindows8()) {
525 table = k932GothicWin8;
526 tableSize = COUNT_OF(k932GothicWin8);
528 table = k932GothicVista;
529 tableSize = COUNT_OF(k932GothicVista);
532 case 936: // Chinese Simplified
536 tableSize = COUNT_OF(k936SimSun);
539 faceName = kGulimChe;
541 table = k949GulimChe;
542 tableSize = COUNT_OF(k949GulimChe);
544 case 950: // Chinese Traditional
545 faceName = kMingLight;
547 table = k950MingLight;
548 tableSize = COUNT_OF(k950MingLight);
551 faceName = kLucidaConsole;
553 table = kLucidaFontSizes;
554 tableSize = COUNT_OF(kLucidaFontSizes);
558 size_t bestIndex = static_cast<size_t>(-1);
559 std::tuple<int, int> bestScore = std::make_tuple(-1, -1);
561 // We might want to pick the smallest possible font, because we don't know
562 // how large the monitor is (and the monitor size can change). We might
563 // want to pick a larger font to accommodate console programs that resize
564 // the console on their own, like DOS edit.com, which tends to resize the
565 // console to 80 columns.
567 for (size_t i = 0; i < tableSize; ++i) {
568 const int width = table[i].width * columns;
570 // In general, we'd like to pick a font size where cutting the number
571 // of columns in half doesn't immediately violate the minimum width
572 // constraint. (e.g. To run DOS edit.com, a user might resize their
573 // terminal to ~100 columns so it's big enough to show the 80 columns
574 // post-resize.) To achieve this, give priority to fonts that allow
575 // this halving. We don't want to encourage *very* large fonts,
576 // though, so disable the effect as the number of columns scales from
578 const int halfColumns = std::min(columns, std::max(40, columns / 2));
579 const int halfWidth = table[i].width * halfColumns;
581 std::tuple<int, int> thisScore = std::make_tuple(-1, -1);
582 if (width >= 160 && halfWidth >= 160) {
583 // Both sizes are good. Prefer the smaller fonts.
584 thisScore = std::make_tuple(2, -width);
585 } else if (width >= 160) {
586 // Prefer the smaller fonts.
587 thisScore = std::make_tuple(1, -width);
589 // Otherwise, prefer the largest font in our table.
590 thisScore = std::make_tuple(0, width);
592 if (thisScore > bestScore) {
594 bestScore = thisScore;
598 ASSERT(bestIndex != static_cast<size_t>(-1));
599 return Font { faceName, fontFamily, table[bestIndex].size };
602 static void setSmallFontVista(VistaFontAPI &api, HANDLE conout,
603 int columns, bool isNewW10) {
604 int codePage = GetConsoleOutputCP();
605 const auto font = selectSmallFont(codePage, columns, isNewW10);
606 if (setFontVista(api, conout, font)) {
607 trace("setSmallFontVista: success");
610 if (codePage == 932 || codePage == 936 ||
611 codePage == 949 || codePage == 950) {
612 trace("setSmallFontVista: falling back to default codepage font instead");
613 const auto fontFB = selectSmallFont(0, columns, isNewW10);
614 if (setFontVista(api, conout, fontFB)) {
615 trace("setSmallFontVista: fallback was successful");
619 trace("setSmallFontVista: failure");
622 struct FontSizeComparator {
623 bool operator()(const std::pair<DWORD, COORD> &obj1,
624 const std::pair<DWORD, COORD> &obj2) const {
625 int score1 = obj1.second.X + obj1.second.Y;
626 int score2 = obj2.second.X + obj2.second.Y;
627 return score1 < score2;
631 static void setSmallFontXP(UndocumentedXPFontAPI &api, HANDLE conout) {
632 // Read the console font table and sort it from smallest to largest.
633 const DWORD fontCount = api.GetNumberOfConsoleFonts()();
634 trace("setSmallFontXP: number of console fonts: %u",
635 static_cast<unsigned>(fontCount));
636 std::vector<std::pair<DWORD, COORD> > table =
637 readFontTable(api, conout, fontCount);
638 std::sort(table.begin(), table.end(), FontSizeComparator());
639 for (size_t i = 0; i < table.size(); ++i) {
640 // Skip especially narrow fonts to permit narrower terminals.
641 if (table[i].second.X < 4) {
644 trace("setSmallFontXP: setting font to %u",
645 static_cast<unsigned>(table[i].first));
646 if (!api.SetConsoleFont()(conout, table[i].first)) {
647 trace("setSmallFontXP: SetConsoleFont call failed");
650 AGENT_CONSOLE_FONT_INFO info;
651 if (!api.GetCurrentConsoleFont()(conout, FALSE, &info)) {
652 trace("setSmallFontXP: GetCurrentConsoleFont call failed");
655 if (info.nFont != table[i].first) {
656 trace("setSmallFontXP: font was not set");
657 dumpXPFont(api, conout, "setSmallFontXP: post-call font: ");
660 trace("setSmallFontXP: success");
663 trace("setSmallFontXP: failure");
666 } // anonymous namespace
668 // A Windows console window can never be larger than the desktop window. To
669 // maximize the possible size of the console in rows*cols, try to configure
670 // the console with a small font. Unfortunately, we cannot make the font *too*
671 // small, because there is also a minimum window size in pixels.
672 void setSmallFont(HANDLE conout, int columns, bool isNewW10) {
673 trace("setSmallFont: attempting to set a small font for %d columns "
674 "(CP=%u OutputCP=%u)",
676 static_cast<unsigned>(GetConsoleCP()),
677 static_cast<unsigned>(GetConsoleOutputCP()));
680 dumpVistaFont(vista, conout, "previous font: ");
681 dumpFontTable(conout, "previous font table: ");
682 setSmallFontVista(vista, conout, columns, isNewW10);
683 dumpVistaFont(vista, conout, "new font: ");
684 dumpFontTable(conout, "new font table: ");
687 UndocumentedXPFontAPI xp;
689 dumpXPFont(xp, conout, "previous font: ");
690 dumpFontTable(conout, "previous font table: ");
691 setSmallFontXP(xp, conout);
692 dumpXPFont(xp, conout, "new font: ");
693 dumpFontTable(conout, "new font table: ");
696 trace("setSmallFont: neither Vista nor XP APIs detected -- giving up");
697 dumpFontTable(conout, "font table: ");