xref: /reactos/win32ss/user/winsrv/concfg/font.c (revision 2ae45e09)
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