Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Too many fonts when enumerating with EnumFontFamiliesEx function

I'm trying to create a list of fonts for the user to choose from. I'm doing this by using the EnumFontFamiliesEx function but unfortunately, the list of returned fonts is much too long. There are many extra fonts that seem frivolous, duplicate, for a different language, or otherwise undesirable to display to the user. My screenshot best illustrates the junk I am trying to filter out.

My code for calling EnumFontFamiliesEx looks like this:

LOGFONT lf;
memset(&lf, 0, sizeof(lf));
lf.lfCharSet = DEFAULT_CHARSET;
// screenDC is result of CreateCompatibleDC(NULL)
EnumFontFamiliesEx(screenDC, &lf, GetFontsCallback, NULL, 0);

The resulting list looks like this, after sorting alphabetically and removing fonts with duplicate face names:

enter image description here

As you can see, the ChooseFont font common dialog is displaying a very reasonable list of fonts that is user-friendly and makes sense. On the other hand, my code displays a long list of extra fonts: fonts that start with "@" (why? what are they even for?), 3 extra variants of Arial font, and several other fonts of unknown purpose like Aheroni, Andalus, Angsana New, AngsanaUPC, and so on. It's insane.

How do I filter the list of fonts returned by EnumFontFamiliesEx so that it exactly matches the list shown in the ChooseFont dialog?

like image 871
James Johnston Avatar asked Jun 28 '12 22:06

James Johnston


3 Answers

Thanks to Jesse Good, I have now learned about some insane unfortunate design decisions made by the Windows 7 team. I won't accept my own answer yet, because if someone else comes up with a way of using this hidden font feature in Windows 7 even when the registry key doesn't exist yet (e.g. perhaps by using the undocumented API, or some other trickery) and their answer works, I will accept it.

This filtering is done by actually "hiding" fonts in Windows 7 Control Panel. By default, fonts for other locales are hidden but they can be shown by the user. At least, this is the idea. Here is the MSDN page discussing this feature: International Font Management.

Here are some key excerpts from this page and other nearby pages in MSDN (also see http://msdn.microsoft.com/en-us/library/windows/desktop/dd371704(v=vs.85).aspx from the Windows 7 compatibility cookbook):

Starting with Windows 7, the font management infrastructure supports the hiding of fonts which are not appropriate for a user's font selection lists. ... This feature means users need no longer be faced with long lists of inappropriate fonts.

In Windows 7, there are no APIs for directly querying which fonts are hidden, or for setting fonts to be hidden. [emphasis mine] If you use the Windows ChooseFont API (Font common dialog) to enable font selection today, you will get the new behavior for free. The new Windows Scenic Ribbon (Font Controls) introduced in Windows 7 also supports this behavior and provides another reason to "Ribbonize" your applications.

When a font is selected into a device context, there is no effect on drawing due to the font being hidden. The EnumFontFamiliesEx function continues to enumerate fonts that are set to hidden. [emphasis mine; there is apparently no way to differentiate hidden and visible fonts with EnumFontFamiliesEx]

Note that charsets are a legacy notion corresponding to pre-Unicode character sets. [emphasis mine]

ChooseFont will only list the shown fonts and filter out the hidden fonts while displaying fonts in the list box. The additional flag (CF_INACTIVEFONTS) in the flags member of the CHOOSEFONT structure is added to allow you to display all the installed fonts in the font list, the same as ChooseFont behaved before Windows 7.

So in other words, unless you use the ChooseFont common dialog or the official Windows ribbon control (only available on Windows Vista/7), you have no supported way at all of filtering out hidden fonts. Is it any surprise or wonder that many users on the Internet are complaining that hiding fonts in the Windows 7 Control Panel seems to have no effect?!? (I previously false posted that MS Word 2010 filters out hidden fonts. It appears it does not, because they use their own custom ribbon control and not the ribbon built into Windows. It is amusing that the Windows 7 Font Control Panel, by design, is not compatible with one of Microsoft's flagship products and cannot be made compatible without dumping the more powerful ribbon in Office.)

Based on the the link that Jesse Good posted, I learned that the hidden fonts are stored in an undocumented registry key. Through this link, and also some experimentation and analysis with Process Monitor (looking at both stack traces and registry accesses), I learned the following:

  • The ribbon control calls an undocumented function called FmsGetFilteredFontList in FMS.DLL (Font Management Services). Its purpose appears quite obvious. It's a real shame they couldn't be bothered to publicly document and maintain it.
  • The settings are stored in an undocumented registry key, which is accessed by FMS.DLL.
  • If the registry key is deleted, it is recreated with default settings by FmsGetFilteredFontList, which are to hide fonts that are not related to the current input languages.
  • A brand new user profile created on a clean installation of Windows does NOT contain any registry keys related to what fonts should be hidden.

Therefore, the link posted by Jesse Good might work for many/most cases, but not 100% of the time. You need a way to reliably recreate these registry keys (or at least assume defaults) if they don't exist. The default behavior is still to hide some fonts, even if the registry keys are gone (e.g. on a new user profile).

like image 109
James Johnston Avatar answered Nov 05 '22 21:11

James Johnston


Given that FmsGetFilteredFontList is undocumented, your options for getting exactly the same list that a user sees in a Windows 7+ ChooseFont dialog may be limited. It may, however, be possible to get a good approximation to the default font list by using only documented APIs.

I've done something similar in order to cut down on the number of possibilities for an algorithm that automatically chooses an appropriate font.

My approach was to use the Unicode subrange bitmask in the FONTSIGNATURE, which can be inspected as you enumerate the fonts. If you know which Unicode subrange(s) you need, the font signature will tell you if the current font covers it. If it does, include it in the list. If it doesn't, then skip it. I suspect this is probably similar to how FmsGetFilteredFontList builds its default list.

The trick is to figure out what subrange(s) the user needs. In my case, it was relatively easy, because I knew exactly what text I was going to have to render. I built a mapping of subranges to FONTSIGNATURE-style bitmask values based on the documentation.

I scanned the code points in the text-to-be-rendered, looking them up in the mapping, and built up a target bitmask. I bitwise-anded this target bitmask with the one in the font signature for each enumerated font. Whenever the result matched the target bitmask, I knew the font could (most likely) support the text. For my application, I required all of the target bits to be present in the font. For your application, I think you'd want any of the target bits.

The font signature bitmask is a good first cut at what characters the font provides. You can use GetFontUnicodeRanges to be utterly certain, but I found that wasn't necessary and it was also slower than just checking the font signatures.

In your case, perhaps you'd have some representative text string available in the user's language. For example, from the document they are editing or from a UI resource that's been translated. You could scan that sample text to get the target font signature.

For example, if you scan some English text, you'll find that all of the necessary characters are in the Latin subrange. If you look at the fonts control panel applet in Windows 7 for an English user (and switch to the details view), you'll see that the Show/hide column is strongly correlated with whether Latin is listed in the Designed for column, which appears to be a textual representation of the font signature's Unicode subrange bitmask.

Update: I just tried enumerating fonts using DirectWrite, thinking that this newer API might handle the font-hiding feature. Alas, it returns everything and has not options (that I can find) for filtering out hidden fonts.

like image 32
Adrian McCarthy Avatar answered Nov 05 '22 20:11

Adrian McCarthy


It's disgraceful that Microsoft haven't documented this functionality, to be honest, but more and more this is what we've come to expect from them.

Another way to filter your own list of fonts is to leverage the shell by enumerating the fonts folder. If you look with Explorer you'll see that hidden fonts are displayed with a ghosted icon - we can use that attribute to tell if the font is hidden.

For example (not complete):

LPITEMIDLIST pidlFonts;
if (SUCCEEDED(SHGetKnownFolderIDList(FOLDERID_Fonts, 0, nullptr, &pidlFonts)))
{
    CComPtr<IShellFolder> psf;
    if (SUCCEEDED(SHBindToObject(nullptr, pidlFonts, nullptr, IID_IShellFolder, reinterpret_cast<void**>(&psf))))
    {
        CComPtr<IEnumIDList> pEnum;
        if (SUCCEEDED(psf->EnumObjects(hWnd, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN | SHCONTF_INIT_ON_FIRST_NEXT, &pEnum)))
        {
            LPITEMIDLIST pidl;
            ULONG celt = 0;
            while (pEnum->Next(1, &pidl, &celt) == S_OK)
            {
                SFGAOF hidden = SFGAO_GHOSTED;
                if (SUCCEEDED(psf->GetAttributesOf(1, const_cast<LPCITEMIDLIST*>(&pidl), &hidden)) && (hidden & SFGAO_GHOSTED) == SFGAO_GHOSTED)
                {
                    // this font should be hidden
                    // get its name via IShellFolder::GetDisplayNameOf
                }
                CoTaskMemFree(pidl);
            }
        }
    }
    CoTaskMemFree(pidlFonts);
}

You could use this method to build a set of hidden fonts and then use that to filter the results of EnumFontFamiliesEx.

like image 2
Jonathan Potter Avatar answered Nov 05 '22 20:11

Jonathan Potter