1 /*
2 * PROJECT: ReactOS Console Server DLL
3 * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
4 * PURPOSE: Console GDI Fonts Management.
5 * COPYRIGHT: Copyright 2017-2022 Hermès Bélusca-Maïto
6 * Copyright 2017 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
7 */
8
9 /* INCLUDES *******************************************************************/
10
11 #include "precomp.h"
12 #include <winuser.h>
13
14 #include "settings.h"
15 #include "font.h"
16 // #include "concfg.h"
17
18 #define NDEBUG
19 #include <debug.h>
20
21 #define DBGFNT DPRINT
22 #define DBGFNT1 DPRINT1
23
24
25 /* GLOBALS ********************************************************************/
26
27 #define TERMINAL_FACENAME L"Terminal"
28 #define DEFAULT_NON_DBCS_FONTFACE L"Lucida Console" // L"Consolas"
29 #define DEFAULT_TT_FONT_FACENAME L"__DefaultTTFont__"
30
31 /* TrueType font list cache */
32 SINGLE_LIST_ENTRY TTFontCache = { NULL };
33
34 // NOTE: Used to tag code that makes sense only with a font cache.
35 // #define FONT_CACHE_PRESENT
36
37
38 /* FUNCTIONS ******************************************************************/
39
40 /**
41 * @brief
42 * Retrieves the character set associated with a given code page.
43 *
44 * @param[in] CodePage
45 * The code page to convert.
46 *
47 * @return
48 * The character set corresponding to the code page, or @b DEFAULT_CHARSET.
49 **/
50 BYTE
CodePageToCharSet(_In_ UINT CodePage)51 CodePageToCharSet(
52 _In_ UINT CodePage)
53 {
54 CHARSETINFO CharInfo;
55 if (TranslateCharsetInfo(UlongToPtr(CodePage), &CharInfo, TCI_SRCCODEPAGE))
56 return (BYTE)CharInfo.ciCharset;
57 else
58 return DEFAULT_CHARSET;
59 }
60
61 /*****************************************************************************/
62
63 typedef struct _FIND_SUITABLE_FONT_PROC_PARAM
64 {
65 /* Search criteria */
66 _In_reads_or_z_(LF_FACESIZE) PCWSTR AltFaceName;
67 FONT_DATA SearchFont;
68 UINT CodePage;
69 BOOL StrictSearch; // TRUE to do strict search; FALSE for relaxed criteria.
70
71 /* Candidate font data */
72 BOOL FontFound; // TRUE/FALSE if we have/haven't found a suitable font.
73 FONT_DATA CandidateFont;
74 WCHAR CandidateFaceName[LF_FACESIZE];
75 } FIND_SUITABLE_FONT_PROC_PARAM, *PFIND_SUITABLE_FONT_PROC_PARAM;
76
77 #define TM_IS_TT_FONT(x) (((x) & TMPF_TRUETYPE) == TMPF_TRUETYPE)
78 #define SIZE_EQUAL(s1, s2) (((s1).X == (s2).X) && ((s1).Y == (s2).Y))
79
80 /**
81 * @brief EnumFontFamiliesEx() callback helper for FindSuitableFont().
82 *
83 * @remark
84 * It implements a nearly-identical console-suitable font search
85 * algorithm based on the one from FindCreateFont()
86 * https://github.com/microsoft/terminal/blob/main/src/propsheet/fontdlg.cpp#L1113
87 * excepting that for now, it does not support an internal font cache.
88 **/
89 static BOOL CALLBACK
FindSuitableFontProc(_In_ PLOGFONTW lplf,_In_ PNEWTEXTMETRICW lpntm,_In_ DWORD FontType,_In_ LPARAM lParam)90 FindSuitableFontProc(
91 _In_ PLOGFONTW lplf,
92 _In_ PNEWTEXTMETRICW lpntm,
93 _In_ DWORD FontType,
94 _In_ LPARAM lParam)
95 {
96 PFIND_SUITABLE_FONT_PROC_PARAM Param = (PFIND_SUITABLE_FONT_PROC_PARAM)lParam;
97 PFONT_DATA SearchFont = &Param->SearchFont;
98
99 if (!IsValidConsoleFont2(lplf, lpntm, FontType, Param->CodePage))
100 {
101 /* This font does not suit us; continue enumeration */
102 return TRUE;
103 }
104
105 #ifndef FONT_CACHE_PRESENT
106 /*
107 * Since we don't cache all the possible font sizes for TrueType fonts,
108 * we cannot check our requested size (and weight) against the enumerated
109 * one; therefore reset the enumerated values to the requested ones.
110 * On the contrary, Raster fonts get their specific font sizes (and weights)
111 * enumerated separately, so for them we can keep the enumerated values.
112 */
113 if (FontType == TRUETYPE_FONTTYPE)
114 {
115 lplf->lfHeight = SearchFont->Size.Y;
116 lplf->lfWidth = 0; // SearchFont->Size.X;
117 lplf->lfWeight = FW_NORMAL;
118 }
119 #endif
120
121 if (Param->StrictSearch)
122 {
123 /*
124 * Find whether this is an exact match.
125 */
126
127 /* If looking for a particular family, skip non-matches */
128 if ((SearchFont->Family != 0) &&
129 ((BYTE)SearchFont->Family != (lplf->lfPitchAndFamily & 0xF0)))
130 {
131 /* Continue enumeration */
132 return TRUE;
133 }
134
135 /* Skip non-matching sizes */
136 #if 0
137 if ((FontInfo[i].SizeWant.Y != Size.Y) &&
138 !SIZE_EQUAL(FontInfo[i].Size, Size))
139 #endif
140 if ((lplf->lfHeight != SearchFont->Size.Y) &&
141 !(lplf->lfWidth == SearchFont->Size.X &&
142 lplf->lfHeight == SearchFont->Size.Y))
143 {
144 /* Continue enumeration */
145 return TRUE;
146 }
147
148 /* Skip non-matching weights */
149 if ((SearchFont->Weight != 0) &&
150 (SearchFont->Weight != lplf->lfWeight))
151 {
152 /* Continue enumeration */
153 return TRUE;
154 }
155
156 /* NOTE: We are making the font enumeration at fixed CharSet,
157 * with the one specified in the parameter block. */
158 ASSERT(lplf->lfCharSet == SearchFont->CharSet);
159
160 if ((FontType != TRUETYPE_FONTTYPE) && // !TM_IS_TT_FONT(lpntm->tmPitchAndFamily)
161 (lplf->lfCharSet != SearchFont->CharSet) &&
162 !(lplf->lfCharSet == OEM_CHARSET && IsCJKCodePage(Param->CodePage))) // g_fEastAsianSystem
163 {
164 /* Continue enumeration */
165 return TRUE;
166 }
167
168 /*
169 * Size (and maybe family) match. If we don't care about the name or
170 * if it matches, use this font. Otherwise, if name doesn't match and
171 * it is a raster font, consider it.
172 *
173 * NOTE: The font face names are case-sensitive.
174 */
175 if (!SearchFont->FaceName || !*(SearchFont->FaceName) ||
176 (wcscmp(lplf->lfFaceName, SearchFont->FaceName) == 0) ||
177 (wcscmp(lplf->lfFaceName, Param->AltFaceName) == 0))
178 {
179 // FontIndex = i;
180
181 PFONT_DATA CandidateFont = &Param->CandidateFont;
182
183 CandidateFont->FaceName = Param->CandidateFaceName;
184 StringCchCopyNW(Param->CandidateFaceName,
185 ARRAYSIZE(Param->CandidateFaceName),
186 lplf->lfFaceName, ARRAYSIZE(lplf->lfFaceName));
187
188 CandidateFont->Weight = lplf->lfWeight;
189 CandidateFont->Family = (lplf->lfPitchAndFamily & 0xF0);
190
191 CandidateFont->Size.X = lplf->lfWidth;
192 CandidateFont->Size.Y = lplf->lfHeight;
193
194 CandidateFont->CharSet = lplf->lfCharSet;
195
196 /* The font is found, stop enumeration */
197 Param->FontFound = TRUE;
198 return FALSE;
199 }
200 else if (FontType != TRUETYPE_FONTTYPE) // !TM_IS_TT_FONT(lpntm->tmPitchAndFamily)
201 {
202 // FontIndex = i;
203
204 PFONT_DATA CandidateFont = &Param->CandidateFont;
205
206 CandidateFont->FaceName = Param->CandidateFaceName;
207 StringCchCopyNW(Param->CandidateFaceName,
208 ARRAYSIZE(Param->CandidateFaceName),
209 lplf->lfFaceName, ARRAYSIZE(lplf->lfFaceName));
210
211 CandidateFont->Weight = lplf->lfWeight;
212 CandidateFont->Family = (lplf->lfPitchAndFamily & 0xF0);
213
214 CandidateFont->Size.X = lplf->lfWidth;
215 CandidateFont->Size.Y = lplf->lfHeight;
216
217 CandidateFont->CharSet = lplf->lfCharSet;
218
219 /* A close Raster Font fit was found; only the name doesn't match.
220 * Continue enumeration to see whether we can find better. */
221 Param->FontFound = TRUE;
222 }
223 }
224 else // !Param->StrictSearch
225 {
226 /*
227 * Failed to find exact match, even after enumeration, so now
228 * try to find a font of same family and same size or bigger.
229 */
230
231 if (IsCJKCodePage(Param->CodePage)) // g_fEastAsianSystem
232 {
233 if ((SearchFont->Family != 0) &&
234 ((BYTE)SearchFont->Family != (lplf->lfPitchAndFamily & 0xF0)))
235 {
236 /* Continue enumeration */
237 return TRUE;
238 }
239
240 if ((FontType != TRUETYPE_FONTTYPE) && // !TM_IS_TT_FONT(lpntm->tmPitchAndFamily)
241 (lplf->lfCharSet != SearchFont->CharSet))
242 {
243 /* Continue enumeration */
244 return TRUE;
245 }
246 }
247 else
248 {
249 if (// (SearchFont->Family != 0) &&
250 ((BYTE)SearchFont->Family != (lplf->lfPitchAndFamily & 0xF0)))
251 {
252 /* Continue enumeration */
253 return TRUE;
254 }
255 }
256
257 if ((lplf->lfHeight >= SearchFont->Size.Y) &&
258 (lplf->lfWidth >= SearchFont->Size.X))
259 {
260 /* Same family, size >= desired */
261 // FontIndex = i;
262
263 PFONT_DATA CandidateFont = &Param->CandidateFont;
264
265 CandidateFont->FaceName = Param->CandidateFaceName;
266 StringCchCopyNW(Param->CandidateFaceName,
267 ARRAYSIZE(Param->CandidateFaceName),
268 lplf->lfFaceName, ARRAYSIZE(lplf->lfFaceName));
269
270 CandidateFont->Weight = lplf->lfWeight;
271 CandidateFont->Family = (lplf->lfPitchAndFamily & 0xF0);
272
273 CandidateFont->Size.X = lplf->lfWidth;
274 CandidateFont->Size.Y = lplf->lfHeight;
275
276 CandidateFont->CharSet = lplf->lfCharSet;
277
278 /* The font is found, stop enumeration */
279 Param->FontFound = TRUE;
280 return FALSE;
281 }
282 }
283
284 /* Continue enumeration */
285 return TRUE;
286 }
287
288 /**
289 * @brief
290 * Finds a font suitable for the given code page, based on the current font
291 * and its characteristics provided in input.
292 *
293 * @param[in,out] FontData
294 * In input: The face name and characteristics of the font to search for,
295 * possibly getting a best match.
296 * In output: The face name and characteristics of the suitable font,
297 * in case of success.
298 *
299 * @param[in] CodePage
300 * The code page the font has to support.
301 *
302 * @return
303 * @b TRUE in case a suitable font has been found. Its name and characteristics
304 * are returned in @b FontData. @b FALSE if no suitable font has been found.
305 **/
306 static BOOL
FindSuitableFont(_Inout_ PFONT_DATA FontData,_In_ UINT CodePage)307 FindSuitableFont(
308 _Inout_ PFONT_DATA FontData,
309 _In_ UINT CodePage)
310 {
311 FIND_SUITABLE_FONT_PROC_PARAM Param;
312 _Inout_updates_z_(LF_FACESIZE) PWSTR FaceName;
313 HDC hDC;
314 LOGFONTW lf;
315 PTT_FONT_ENTRY FontEntry;
316
317 /* Save the original FaceName pointer */
318 FaceName = FontData->FaceName;
319
320 /* Save our current search criteria */
321 RtlZeroMemory(&Param, sizeof(Param));
322 Param.SearchFont = *FontData;
323
324 Param.SearchFont.CharSet = CodePageToCharSet(CodePage);
325 Param.CodePage = CodePage;
326
327 if (/* !FaceName || */ !*FaceName)
328 {
329 /* Find and use a default Raster font */
330
331 /* Use "Terminal" as the fallback */
332 StringCchCopyW(FaceName, LF_FACESIZE, TERMINAL_FACENAME);
333 #if 0
334 // FIXME: CJK font choose workaround: Don't choose Asian
335 // charset font if there is no preferred font for CJK.
336 if (IsCJKCodePage(CodePage))
337 FontData->CharSet = ANSI_CHARSET;
338 #endif
339 FontData->Family &= ~TMPF_TRUETYPE;
340 }
341 else if (wcscmp(FaceName, DEFAULT_TT_FONT_FACENAME) == 0)
342 {
343 /* Find and use a default TrueType font */
344 FontEntry = FindCachedTTFont(NULL, CodePage);
345 if (FontEntry)
346 {
347 StringCchCopyW(FaceName, LF_FACESIZE, FontEntry->FaceName);
348 }
349 else
350 {
351 StringCchCopyW(FaceName, LF_FACESIZE, DEFAULT_NON_DBCS_FONTFACE);
352 }
353 FontData->Family |= TMPF_TRUETYPE;
354 }
355
356 /* Search for a TrueType alternative face name */
357 FontEntry = FindCachedTTFont(FaceName, CodePage);
358 if (FontEntry)
359 {
360 /* NOTE: The font face names are case-sensitive */
361 if (wcscmp(FontEntry->FaceName, FaceName) == 0)
362 Param.AltFaceName = FontEntry->FaceNameAlt;
363 else if (wcscmp(FontEntry->FaceNameAlt, FaceName) == 0)
364 Param.AltFaceName = FontEntry->FaceName;
365 }
366 else
367 {
368 Param.AltFaceName = FaceName;
369 }
370
371 /* Initialize the search: start with a strict search, then a relaxed one */
372 Param.FontFound = FALSE;
373
374 Param.StrictSearch = TRUE;
375 SearchAgain:
376 /*
377 * Enumerate all fonts with the given character set.
378 * We will match them with the search criteria.
379 */
380 RtlZeroMemory(&lf, sizeof(lf));
381 lf.lfCharSet = Param.SearchFont.CharSet;
382 // lf.lfPitchAndFamily = FIXED_PITCH | FF_MODERN;
383
384 hDC = GetDC(NULL);
385 EnumFontFamiliesExW(hDC, &lf, (FONTENUMPROCW)FindSuitableFontProc, (LPARAM)&Param, 0);
386 ReleaseDC(NULL, hDC);
387
388 /* If we failed to find any font, search again with relaxed criteria */
389 if (Param.StrictSearch && !Param.FontFound)
390 {
391 Param.StrictSearch = FALSE;
392 goto SearchAgain;
393 }
394
395 /* If no font was found again, return failure */
396 if (!Param.FontFound)
397 return FALSE;
398
399 /* Return the font details */
400 *FontData = Param.CandidateFont;
401 FontData->FaceName = FaceName; // Restore the original FaceName pointer.
402 StringCchCopyNW(FaceName, LF_FACESIZE,
403 Param.CandidateFaceName,
404 ARRAYSIZE(Param.CandidateFaceName));
405
406 return TRUE;
407 }
408
409 /**
410 * @brief
411 * Validates and creates a suitable console font based on the font
412 * characteristics given in input.
413 *
414 * @param[in] FontData
415 * The face name and characteristics of the font to create.
416 *
417 * @param[in] CodePage
418 * The code page the font has to support.
419 *
420 * @return
421 * A GDI handle to the created font, or @b NULL in case of failure.
422 **/
423 static HFONT
CreateConsoleFontWorker(_In_ PFONT_DATA FontData,_In_ UINT CodePage)424 CreateConsoleFontWorker(
425 _In_ PFONT_DATA FontData,
426 _In_ UINT CodePage)
427 {
428 LOGFONTW lf;
429
430 RtlZeroMemory(&lf, sizeof(lf));
431
432 lf.lfHeight = (LONG)(ULONG)FontData->Size.Y;
433 lf.lfWidth = (LONG)(ULONG)FontData->Size.X;
434
435 lf.lfEscapement = 0;
436 lf.lfOrientation = 0; // TA_BASELINE; // TA_RTLREADING; when the console supports RTL?
437 // lf.lfItalic = lf.lfUnderline = lf.lfStrikeOut = FALSE;
438 lf.lfWeight = FontData->Weight;
439 lf.lfCharSet = CodePageToCharSet(CodePage);
440 lf.lfOutPrecision = OUT_DEFAULT_PRECIS;
441 lf.lfClipPrecision = CLIP_DEFAULT_PRECIS;
442 lf.lfQuality = DEFAULT_QUALITY;
443
444 /* Set the mandatory flags and remove those that we do not support */
445 lf.lfPitchAndFamily = (BYTE)( (FIXED_PITCH | FF_MODERN | FontData->Family) &
446 ~(VARIABLE_PITCH | FF_DECORATIVE | FF_ROMAN | FF_SCRIPT | FF_SWISS));
447
448 if (!IsValidConsoleFont(FontData->FaceName, CodePage))
449 return NULL;
450
451 StringCchCopyNW(lf.lfFaceName, ARRAYSIZE(lf.lfFaceName),
452 FontData->FaceName, LF_FACESIZE);
453
454 return CreateFontIndirectW(&lf);
455 }
456
457 /*****************************************************************************/
458
459 /**
460 * @brief
461 * Validates and creates a suitable console font based on the font
462 * characteristics given in input.
463 *
464 * @param[in] Height
465 * The font height in cell units (pixels).
466 *
467 * @param[in,opt] Width
468 * The font width in cell units (pixels).
469 *
470 * @param[in,out] FaceName
471 * A pointer to a maximally @b LF_FACESIZE-sized buffer.
472 * In input: The buffer contains the face name of the font to try to create.
473 * In output: The buffer receives the face name of the font that has been
474 * created, in case of success. It may, or may not be, identical to the face
475 * name provided in input, in case a substitute font has been chosen.
476 *
477 * @param[in] FontWeight
478 * The font weight.
479 *
480 * @param[in] FontFamily
481 * The font family.
482 *
483 * @param[in] CodePage
484 * The code page the font has to support.
485 *
486 * @param[in] UseDefaultFallback
487 * Whether (@b TRUE) or not (@b FALSE) to use a default fallback font in case
488 * neither the specified font nor any substitute font could be found and
489 * created for the specified code page.
490 *
491 * @param[out] FontData
492 * The face name and characteristics of the created font.
493 *
494 * @return
495 * A GDI handle to the created font, or @b NULL in case of failure.
496 *
497 * @remark
498 * Similar to FindCreateFont()
499 * https://github.com/microsoft/terminal/blob/main/src/propsheet/fontdlg.cpp#L1113
500 * but:
501 * - does not support an internal font cache for now;
502 * - returns a font handle (and not a font index to the cache).
503 **/
504 HFONT
CreateConsoleFontEx(_In_ LONG Height,_In_opt_ LONG Width,_Inout_updates_z_ (LF_FACESIZE)PWSTR FaceName,_In_ ULONG FontWeight,_In_ ULONG FontFamily,_In_ UINT CodePage,_In_ BOOL UseDefaultFallback,_Out_ PFONT_DATA FontData)505 CreateConsoleFontEx(
506 _In_ LONG Height,
507 _In_opt_ LONG Width,
508 _Inout_updates_z_(LF_FACESIZE)
509 PWSTR FaceName,
510 _In_ ULONG FontWeight,
511 _In_ ULONG FontFamily,
512 _In_ UINT CodePage,
513 _In_ BOOL UseDefaultFallback,
514 _Out_ PFONT_DATA FontData)
515 {
516 HFONT hFont;
517
518 FontData->FaceName = FaceName;
519 FontData->Weight = FontWeight;
520 FontData->Family = FontFamily;
521 /* NOTE: FontSize is always in cell height/width units (pixels) */
522 FontData->Size.X = Width;
523 FontData->Size.Y = Height;
524 FontData->CharSet = 0; // CodePageToCharSet(CodePage);
525
526 if (/* !FaceName || */ !*FaceName || wcscmp(FaceName, DEFAULT_TT_FONT_FACENAME) == 0)
527 {
528 /* We do not have an actual font face name yet and should find one.
529 * Call FindSuitableFont() to determine the default font to use. */
530 }
531 else
532 {
533 hFont = CreateConsoleFontWorker(FontData, CodePage);
534 if (hFont)
535 return hFont;
536
537 DBGFNT1("CreateConsoleFont('%S') failed - Try to find a suitable font...\n",
538 FaceName);
539 }
540
541 /*
542 * We could not create a font with the default settings.
543 * Try to find a suitable font and retry.
544 */
545 if (!FindSuitableFont(FontData, CodePage))
546 {
547 /* We could not find any suitable font, fall back
548 * to some default one if required to do so. */
549 DBGFNT1("FindSuitableFont could not find anything - %s\n",
550 UseDefaultFallback ? "Falling back to 'Terminal'"
551 : "Bailing out");
552
553 /* No fallback: no font! */
554 if (!UseDefaultFallback)
555 return NULL;
556
557 //
558 // FIXME: See also !*FaceName case in FindSuitableFont().
559 //
560 /* Use "Terminal" as the fallback */
561 StringCchCopyW(FaceName, LF_FACESIZE, TERMINAL_FACENAME);
562 #if 0
563 // FIXME: CJK font choose workaround: Don't choose Asian
564 // charset font if there is no preferred font for CJK.
565 if (IsCJKCodePage(CodePage))
566 FontData->CharSet = ANSI_CHARSET;
567 #endif
568 FontData->Family &= ~TMPF_TRUETYPE;
569 }
570 else
571 {
572 DBGFNT1("FindSuitableFont found: '%S', size (%d x %d)\n",
573 FaceName, FontData->Size.X, FontData->Size.Y);
574 }
575
576 /* Retry creating the font */
577 hFont = CreateConsoleFontWorker(FontData, CodePage);
578 if (!hFont)
579 DBGFNT1("CreateConsoleFont('%S') failed\n", FaceName);
580
581 return hFont;
582 }
583
584 /**
585 * @brief
586 * A wrapper for CreateConsoleFontEx().
587 *
588 * @param[in] Height
589 * The font height in cell units (pixels).
590 *
591 * @param[in,opt] Width
592 * The font width in cell units (pixels).
593 *
594 * @param[in,out] ConsoleInfo
595 * A pointer to console settings information, containing in particular
596 * (in input) the face name and characteristics of the font to create
597 * with the current console code page.
598 * In output, the font information gets updated.
599 * Note that a default fallback font is always being used in case neither
600 * the specified font nor any substitute font could be found and created
601 * for the specified code page.
602 *
603 * @return
604 * A GDI handle to the created font, or @b NULL in case of failure.
605 *
606 * @see CreateConsoleFontEx(), CreateConsoleFont()
607 **/
608 HFONT
CreateConsoleFont2(_In_ LONG Height,_In_opt_ LONG Width,_Inout_ PCONSOLE_STATE_INFO ConsoleInfo)609 CreateConsoleFont2(
610 _In_ LONG Height,
611 _In_opt_ LONG Width,
612 _Inout_ PCONSOLE_STATE_INFO ConsoleInfo)
613 {
614 FONT_DATA FontData;
615 HFONT hFont;
616
617 hFont = CreateConsoleFontEx(Height,
618 Width,
619 ConsoleInfo->FaceName,
620 ConsoleInfo->FontWeight,
621 ConsoleInfo->FontFamily,
622 ConsoleInfo->CodePage,
623 TRUE, // UseDefaultFallback
624 &FontData);
625 if (hFont)
626 {
627 ConsoleInfo->FontWeight = FontData.Weight;
628 ConsoleInfo->FontFamily = FontData.Family;
629 }
630
631 return hFont;
632 }
633
634 /**
635 * @brief
636 * A wrapper for CreateConsoleFontEx().
637 *
638 * @param[in,out] ConsoleInfo
639 * A pointer to console settings information, containing in particular
640 * (in input) the face name and characteristics of the font to create
641 * with the current console code page.
642 * In output, the font information gets updated.
643 * Note that a default fallback font is always being used in case neither
644 * the specified font nor any substitute font could be found and created
645 * for the specified code page.
646 *
647 * @return
648 * A GDI handle to the created font, or @b NULL in case of failure.
649 *
650 * @see CreateConsoleFontEx(), CreateConsoleFont2()
651 **/
652 HFONT
CreateConsoleFont(_Inout_ PCONSOLE_STATE_INFO ConsoleInfo)653 CreateConsoleFont(
654 _Inout_ PCONSOLE_STATE_INFO ConsoleInfo)
655 {
656 /*
657 * Format:
658 * Width = FontSize.X = LOWORD(FontSize);
659 * Height = FontSize.Y = HIWORD(FontSize);
660 */
661 /* NOTE: FontSize is always in cell height/width units (pixels) */
662 return CreateConsoleFont2((LONG)(ULONG)ConsoleInfo->FontSize.Y,
663 (LONG)(ULONG)ConsoleInfo->FontSize.X,
664 ConsoleInfo);
665 }
666
667 /**
668 * @brief
669 * Retrieves the cell size for a console font.
670 *
671 * @param[in,opt] hDC
672 * An optional GDI device context handle.
673 *
674 * @param[in] hFont
675 * The GDI handle to the font.
676 *
677 * @param[out] Height
678 * In case of success, receives the cell height size (in pixels).
679 *
680 * @param[out] Width
681 * In case of success, receives the cell height size (in pixels).
682 *
683 * @return
684 * @b TRUE if success, @b FALSE in case of failure.
685 **/
_Success_(return)686 _Success_(return)
687 BOOL
688 GetFontCellSize(
689 _In_opt_ HDC hDC,
690 _In_ HFONT hFont,
691 _Out_ PUINT Height,
692 _Out_ PUINT Width)
693 {
694 BOOL Success = FALSE;
695 HDC hOrgDC = hDC;
696 HFONT hOldFont;
697 // LONG LogSize, PointSize;
698 LONG CharWidth, CharHeight;
699 TEXTMETRICW tm;
700 // SIZE CharSize;
701
702 if (!hDC)
703 hDC = GetDC(NULL);
704
705 hOldFont = SelectObject(hDC, hFont);
706 if (hOldFont == NULL)
707 {
708 DBGFNT1("GetFontCellSize: SelectObject failed\n");
709 goto Quit;
710 }
711
712 /*
713 * See also: Display_SetTypeFace in applications/fontview/display.c
714 */
715
716 /*
717 * Note that the method with GetObjectW just returns
718 * the original parameters with which the font was created.
719 */
720 if (!GetTextMetricsW(hDC, &tm))
721 {
722 DBGFNT1("GetFontCellSize: GetTextMetrics failed\n");
723 goto Cleanup;
724 }
725
726 CharHeight = tm.tmHeight + tm.tmExternalLeading;
727
728 #if 0
729 /* Measure real char width more precisely if possible */
730 if (GetTextExtentPoint32W(hDC, L"R", 1, &CharSize))
731 CharWidth = CharSize.cx;
732 #else
733 CharWidth = tm.tmAveCharWidth; // tm.tmMaxCharWidth;
734 #endif
735
736 #if 0
737 /*** Logical to Point size ***/
738 LogSize = tm.tmHeight - tm.tmInternalLeading;
739 PointSize = MulDiv(LogSize, 72, GetDeviceCaps(hDC, LOGPIXELSY));
740 /*****************************/
741 #endif
742
743 *Height = (UINT)CharHeight;
744 *Width = (UINT)CharWidth;
745 Success = TRUE;
746
747 Cleanup:
748 SelectObject(hDC, hOldFont);
749 Quit:
750 if (!hOrgDC)
751 ReleaseDC(NULL, hDC);
752
753 return Success;
754 }
755
756 /**
757 * @brief
758 * Validates whether a given font can be supported in the console,
759 * under the specified code page.
760 *
761 * @param[in] lplf
762 * @param[in] lpntm
763 * @param[in] FontType
764 * The GDI font characteristics of the font to validate.
765 *
766 * @param[in] CodePage
767 * The code page the font has to support.
768 *
769 * @return
770 * @b TRUE if the font is valid and supported in the console,
771 * @b FALSE if not.
772 *
773 * @remark
774 * Equivalent of the font validation tests in FontEnumForV2Console()
775 * (or the more restrictive ones in FontEnum())
776 * https://github.com/microsoft/terminal/blob/main/src/propsheet/misc.cpp#L465
777 * https://github.com/microsoft/terminal/blob/main/src/propsheet/misc.cpp#L607
778 *
779 * @see IsValidConsoleFont()
780 **/
781 BOOL
IsValidConsoleFont2(_In_ PLOGFONTW lplf,_In_ PNEWTEXTMETRICW lpntm,_In_ DWORD FontType,_In_ UINT CodePage)782 IsValidConsoleFont2(
783 _In_ PLOGFONTW lplf,
784 _In_ PNEWTEXTMETRICW lpntm,
785 _In_ DWORD FontType,
786 _In_ UINT CodePage)
787 {
788 LPCWSTR FaceName = lplf->lfFaceName;
789
790 /*
791 * According to: https://web.archive.org/web/20140901124501/http://support.microsoft.com/kb/247815
792 * "Necessary criteria for fonts to be available in a command window",
793 * the criteria for console-eligible fonts are as follows:
794 * - The font must be a fixed-pitch font.
795 * - The font cannot be an italic font.
796 * - The font cannot have a negative A or C space.
797 * - If it is a TrueType font, it must be FF_MODERN.
798 * - If it is not a TrueType font, it must be OEM_CHARSET.
799 *
800 * Non documented: vertical fonts are forbidden (their name start with a '@').
801 *
802 * Additional criteria for Asian installations:
803 * - If it is not a TrueType font, the face name must be "Terminal".
804 * - If it is an Asian TrueType font, it must also be an Asian character set.
805 *
806 * See also Raymond Chen's blog: https://devblogs.microsoft.com/oldnewthing/?p=26843
807 * and MIT-licensed Microsoft Terminal source code: https://github.com/microsoft/terminal/blob/main/src/propsheet/misc.cpp
808 * for other details.
809 *
810 * To install additional TrueType fonts to be available for the console,
811 * add entries of type REG_SZ named "0", "00" etc... in:
812 * HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont
813 * The names of the fonts listed there should match those in:
814 * HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Fonts
815 */
816
817 /*
818 * In ReactOS we relax some of the criteria:
819 * - We allow fixed-pitch FF_MODERN (Monospace) TrueType fonts
820 * that can be italic or have negative A or C space.
821 * - If it is not a TrueType font, it can be from another character set
822 * than OEM_CHARSET. When an Asian codepage is active however, we require
823 * that this non-TrueType font has an Asian character set.
824 */
825
826 /* Reject variable-width fonts ... */
827 if ( ( ((lplf->lfPitchAndFamily & 0x03) != FIXED_PITCH)
828 #if 0 /* Reject italic and TrueType fonts with negative A or C space ... */
829 || (lplf->lfItalic)
830 || !(lpntm->ntmFlags & NTM_NONNEGATIVE_AC)
831 #endif
832 ) &&
833 /* ... if they are not in the list of additional TrueType fonts to include */
834 !IsAdditionalTTFont(FaceName) )
835 {
836 DBGFNT("Font '%S' rejected because it%s (lfPitchAndFamily = %d)\n",
837 FaceName,
838 !(lplf->lfPitchAndFamily & FIXED_PITCH) ? "'s not FIXED_PITCH"
839 : (!(lpntm->ntmFlags & NTM_NONNEGATIVE_AC) ? " has negative A or C space"
840 : " is broken"),
841 lplf->lfPitchAndFamily);
842 return FALSE;
843 }
844
845 /* Reject TrueType fonts that are not FF_MODERN */
846 if ((FontType == TRUETYPE_FONTTYPE) && ((lplf->lfPitchAndFamily & 0xF0) != FF_MODERN))
847 {
848 DBGFNT("TrueType font '%S' rejected because it's not FF_MODERN (lfPitchAndFamily = %d)\n",
849 FaceName, lplf->lfPitchAndFamily);
850 return FALSE;
851 }
852
853 /* Reject vertical fonts (tategaki) */
854 if (FaceName[0] == L'@')
855 {
856 DBGFNT("Font '%S' rejected because it's vertical\n", FaceName);
857 return FALSE;
858 }
859
860 /* Is the current code page Chinese, Japanese or Korean? */
861 if (IsCJKCodePage(CodePage))
862 {
863 /* It's CJK */
864
865 if (FontType == TRUETYPE_FONTTYPE)
866 {
867 /*
868 * Here we are inclusive and check for any CJK character set,
869 * instead of looking just at the current one via CodePageToCharSet().
870 */
871 if (!IsCJKCharSet(lplf->lfCharSet))
872 {
873 DBGFNT("TrueType font '%S' rejected because it's not Asian charset (lfCharSet = %d)\n",
874 FaceName, lplf->lfCharSet);
875 return FALSE;
876 }
877
878 /*
879 * If this is a cached TrueType font that is used only for certain
880 * code pages, verify that the charset it claims is the correct one.
881 *
882 * Since there may be multiple entries for a cached TrueType font,
883 * a general one (code page == 0) and one or more for explicit
884 * code pages, we need to perform two search queries instead of
885 * just one and retrieving the code page for this entry.
886 */
887 if (IsAdditionalTTFont(FaceName) && !IsAdditionalTTFontCP(FaceName, 0) &&
888 !IsCJKCharSet(lplf->lfCharSet))
889 {
890 DBGFNT("Cached TrueType font '%S' rejected because it claims a code page that is not Asian charset (lfCharSet = %d)\n",
891 FaceName, lplf->lfCharSet);
892 return FALSE;
893 }
894 }
895 else
896 {
897 /* Reject non-TrueType fonts that do not have an Asian character set */
898 if (!IsCJKCharSet(lplf->lfCharSet) && (lplf->lfCharSet != OEM_CHARSET))
899 {
900 DBGFNT("Non-TrueType font '%S' rejected because it's not Asian charset or OEM_CHARSET (lfCharSet = %d)\n",
901 FaceName, lplf->lfCharSet);
902 return FALSE;
903 }
904
905 /* Reject non-TrueType fonts that are not Terminal */
906 if (wcscmp(FaceName, TERMINAL_FACENAME) != 0)
907 {
908 DBGFNT("Non-TrueType font '%S' rejected because it's not 'Terminal'\n", FaceName);
909 return FALSE;
910 }
911 }
912 }
913 else
914 {
915 /* Not CJK */
916
917 /* Reject non-TrueType fonts that are not OEM or similar */
918 if ((FontType != TRUETYPE_FONTTYPE) &&
919 (lplf->lfCharSet != ANSI_CHARSET) &&
920 (lplf->lfCharSet != DEFAULT_CHARSET) &&
921 (lplf->lfCharSet != OEM_CHARSET))
922 {
923 DBGFNT("Non-TrueType font '%S' rejected because it's not ANSI_CHARSET or DEFAULT_CHARSET or OEM_CHARSET (lfCharSet = %d)\n",
924 FaceName, lplf->lfCharSet);
925 return FALSE;
926 }
927 }
928
929 /* All good */
930 return TRUE;
931 }
932
933 typedef struct _IS_VALID_CONSOLE_FONT_PARAM
934 {
935 BOOL IsValidFont;
936 UINT CodePage;
937 } IS_VALID_CONSOLE_FONT_PARAM, *PIS_VALID_CONSOLE_FONT_PARAM;
938
939 /**
940 * @brief EnumFontFamiliesEx() callback helper for IsValidConsoleFont().
941 **/
942 static BOOL CALLBACK
IsValidConsoleFontProc(_In_ PLOGFONTW lplf,_In_ PNEWTEXTMETRICW lpntm,_In_ DWORD FontType,_In_ LPARAM lParam)943 IsValidConsoleFontProc(
944 _In_ PLOGFONTW lplf,
945 _In_ PNEWTEXTMETRICW lpntm,
946 _In_ DWORD FontType,
947 _In_ LPARAM lParam)
948 {
949 PIS_VALID_CONSOLE_FONT_PARAM Param = (PIS_VALID_CONSOLE_FONT_PARAM)lParam;
950 Param->IsValidFont = IsValidConsoleFont2(lplf, lpntm, FontType, Param->CodePage);
951
952 /* Stop the enumeration now */
953 return FALSE;
954 }
955
956 /**
957 * @brief
958 * Validates whether a given font can be supported in the console,
959 * under the specified code page.
960 *
961 * @param[in] FaceName
962 * The face name of the font to validate.
963 *
964 * @param[in] CodePage
965 * The code page the font has to support.
966 *
967 * @return
968 * @b TRUE if the font is valid and supported in the console,
969 * @b FALSE if not.
970 *
971 * @see IsValidConsoleFont2()
972 **/
973 BOOL
IsValidConsoleFont(_In_ PCWSTR FaceName,_In_ UINT CodePage)974 IsValidConsoleFont(
975 // _In_reads_or_z_(LF_FACESIZE)
976 _In_ PCWSTR FaceName,
977 _In_ UINT CodePage)
978 {
979 IS_VALID_CONSOLE_FONT_PARAM Param;
980 HDC hDC;
981 LOGFONTW lf;
982
983 Param.IsValidFont = FALSE;
984 Param.CodePage = CodePage;
985
986 RtlZeroMemory(&lf, sizeof(lf));
987 lf.lfCharSet = CodePageToCharSet(CodePage);
988 // lf.lfPitchAndFamily = FIXED_PITCH | FF_MODERN;
989 StringCchCopyW(lf.lfFaceName, ARRAYSIZE(lf.lfFaceName), FaceName);
990
991 hDC = GetDC(NULL);
992 EnumFontFamiliesExW(hDC, &lf, (FONTENUMPROCW)IsValidConsoleFontProc, (LPARAM)&Param, 0);
993 ReleaseDC(NULL, hDC);
994
995 return Param.IsValidFont;
996 }
997
998
999 /**
1000 * @brief
1001 * Initializes the console TrueType font cache.
1002 *
1003 * @remark
1004 * To install additional TrueType fonts to be available for the console,
1005 * add entries of type REG_SZ named "0", "00" etc... in:
1006 * HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont
1007 * The names of the fonts listed there should match those in:
1008 * HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Fonts
1009 *
1010 * @return None.
1011 **/
1012 VOID
InitTTFontCache(VOID)1013 InitTTFontCache(VOID)
1014 {
1015 LRESULT lResult;
1016 HKEY hKey;
1017 DWORD dwIndex, dwType;
1018 WCHAR szValueName[MAX_PATH];
1019 DWORD cchValueName;
1020 WCHAR szValue[LF_FACESIZE] = L"";
1021 DWORD cbValue;
1022 UINT CodePage;
1023 PTT_FONT_ENTRY FontEntry;
1024 PWCHAR pszNext;
1025
1026 if (TTFontCache.Next != NULL)
1027 return;
1028 // TTFontCache.Next = NULL;
1029
1030 /* Open the Console\TrueTypeFont key */
1031 // "\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Console\\TrueTypeFont"
1032 if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
1033 L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Console\\TrueTypeFont",
1034 0,
1035 KEY_QUERY_VALUE,
1036 &hKey) != ERROR_SUCCESS)
1037 {
1038 return;
1039 }
1040
1041 /* Enumerate all the available TrueType console fonts */
1042 for (dwIndex = 0, cchValueName = ARRAYSIZE(szValueName),
1043 cbValue = sizeof(szValue);
1044 (lResult = RegEnumValueW(hKey, dwIndex,
1045 szValueName, &cchValueName,
1046 NULL, &dwType,
1047 (PBYTE)szValue, &cbValue)) != ERROR_NO_MORE_ITEMS;
1048 ++dwIndex, cchValueName = ARRAYSIZE(szValueName),
1049 cbValue = sizeof(szValue))
1050 {
1051 /* Ignore if we failed for another reason, e.g. because
1052 * the value name is too long (and thus, invalid). */
1053 if (lResult != ERROR_SUCCESS)
1054 continue;
1055
1056 /* Validate the value name (exclude the unnamed value) */
1057 if (!cchValueName || (*szValueName == UNICODE_NULL))
1058 continue;
1059 /* Too large value names have already been handled with ERROR_MORE_DATA */
1060 ASSERT((cchValueName < ARRAYSIZE(szValueName)) &&
1061 (szValueName[cchValueName] == UNICODE_NULL));
1062
1063 /* Only (multi-)string values are supported */
1064 if ((dwType != REG_SZ) && (dwType != REG_MULTI_SZ))
1065 continue;
1066
1067 /* The value name is a code page (in decimal), validate it */
1068 CodePage = wcstoul(szValueName, &pszNext, 10);
1069 if (*pszNext)
1070 continue; // Non-numerical garbage followed...
1071 // IsValidCodePage(CodePage);
1072
1073 FontEntry = RtlAllocateHeap(RtlGetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*FontEntry));
1074 if (!FontEntry)
1075 {
1076 DBGFNT1("InitTTFontCache: Failed to allocate memory, continuing...\n");
1077 continue;
1078 }
1079
1080 FontEntry->CodePage = CodePage;
1081
1082 pszNext = szValue;
1083
1084 /* Check whether bold is disabled for this font */
1085 if (*pszNext == BOLD_MARK)
1086 {
1087 FontEntry->DisableBold = TRUE;
1088 ++pszNext;
1089 }
1090 else
1091 {
1092 FontEntry->DisableBold = FALSE;
1093 }
1094
1095 /* Copy the font name */
1096 StringCchCopyNW(FontEntry->FaceName, ARRAYSIZE(FontEntry->FaceName),
1097 pszNext, wcslen(pszNext));
1098
1099 if (dwType == REG_MULTI_SZ)
1100 {
1101 /* There may be an alternate face name as the second string */
1102 pszNext += wcslen(pszNext) + 1;
1103
1104 /* Check whether bold is disabled for this font */
1105 if (*pszNext == BOLD_MARK)
1106 {
1107 FontEntry->DisableBold = TRUE;
1108 ++pszNext;
1109 }
1110 // else, keep the original setting.
1111
1112 /* Copy the alternate font name */
1113 StringCchCopyNW(FontEntry->FaceNameAlt, ARRAYSIZE(FontEntry->FaceNameAlt),
1114 pszNext, wcslen(pszNext));
1115 }
1116
1117 PushEntryList(&TTFontCache, &FontEntry->Entry);
1118 }
1119
1120 /* Close the key and quit */
1121 RegCloseKey(hKey);
1122 }
1123
1124 /**
1125 * @brief
1126 * Clears the console TrueType font cache.
1127 *
1128 * @return None.
1129 **/
1130 VOID
ClearTTFontCache(VOID)1131 ClearTTFontCache(VOID)
1132 {
1133 PSINGLE_LIST_ENTRY Entry;
1134 PTT_FONT_ENTRY FontEntry;
1135
1136 while (TTFontCache.Next != NULL)
1137 {
1138 Entry = PopEntryList(&TTFontCache);
1139 FontEntry = CONTAINING_RECORD(Entry, TT_FONT_ENTRY, Entry);
1140 RtlFreeHeap(RtlGetProcessHeap(), 0, FontEntry);
1141 }
1142 TTFontCache.Next = NULL;
1143 }
1144
1145 /**
1146 * @brief
1147 * Refreshes the console TrueType font cache,
1148 * by clearing and re-initializing it.
1149 *
1150 * @return None.
1151 **/
1152 VOID
RefreshTTFontCache(VOID)1153 RefreshTTFontCache(VOID)
1154 {
1155 ClearTTFontCache();
1156 InitTTFontCache();
1157 }
1158
1159 /**
1160 * @brief
1161 * Searches for a font in the console TrueType font cache,
1162 * with the specified code page.
1163 *
1164 * @param[in,opt] FaceName
1165 * An optional pointer to a maximally @b LF_FACESIZE-sized buffer.
1166 * The buffer contains the face name of the font to search for.
1167 *
1168 * - If FaceName != NULL, search for the named font that should
1169 * match the provided code page (when CodePage != INVALID_CP).
1170 *
1171 * - If FaceName == NULL, search for a font with the provided
1172 * code page. In this case, CodePage cannot be == INVALID_CP,
1173 * otherwise the search fails.
1174 *
1175 * @param[in] CodePage
1176 * The code page the font has to support, or @b INVALID_CP when
1177 * searching a font by face name only.
1178 *
1179 * @return
1180 * A pointer to the cache entry for the font, or @b NULL if not found.
1181 **/
1182 PTT_FONT_ENTRY
FindCachedTTFont(_In_reads_or_z_opt_ (LF_FACESIZE)PCWSTR FaceName,_In_ UINT CodePage)1183 FindCachedTTFont(
1184 _In_reads_or_z_opt_(LF_FACESIZE)
1185 PCWSTR FaceName,
1186 _In_ UINT CodePage)
1187 {
1188 PSINGLE_LIST_ENTRY Entry;
1189 PTT_FONT_ENTRY FontEntry;
1190
1191 if (FaceName)
1192 {
1193 /* Search for the named font */
1194 for (Entry = TTFontCache.Next;
1195 Entry != NULL;
1196 Entry = Entry->Next)
1197 {
1198 FontEntry = CONTAINING_RECORD(Entry, TT_FONT_ENTRY, Entry);
1199
1200 /* NOTE: The font face names are case-sensitive */
1201 if ((wcscmp(FontEntry->FaceName , FaceName) == 0) ||
1202 (wcscmp(FontEntry->FaceNameAlt, FaceName) == 0))
1203 {
1204 /* Return the font if we don't search by code page, or when they match */
1205 if ((CodePage == INVALID_CP) || (CodePage == FontEntry->CodePage))
1206 {
1207 return FontEntry;
1208 }
1209 }
1210 }
1211 }
1212 else if (CodePage != INVALID_CP)
1213 {
1214 /* Search for a font with the specified code page */
1215 for (Entry = TTFontCache.Next;
1216 Entry != NULL;
1217 Entry = Entry->Next)
1218 {
1219 FontEntry = CONTAINING_RECORD(Entry, TT_FONT_ENTRY, Entry);
1220
1221 /* Return the font if the code pages match */
1222 if (CodePage == FontEntry->CodePage)
1223 return FontEntry;
1224 }
1225 }
1226
1227 return NULL;
1228 }
1229
1230 /* EOF */
1231