Jump to content

Enumerating Fonts


 Share

Recommended Posts

In short, I'm trying to replicate some of the functionality of _ChooseFont().  A quick Google search suggests I'm not the first person to consider this, and at least one of the answers is to use _WinAPI_EnumFontFamilies().  Which seems reasonable.

I don't quite get one thing about it, though.

For example, I have a font installed on my system named DejaVu Serif.  There are actually 8 associated font faces:

  • DejaVu Serif
  • DejaVu Serif Bold
  • DejaVu Serif Bold Italic
  • DejaVu Serif Italic
  • DejaVu Serif Condensed
  • DejaVu Serif Condensed Bold
  • DejaVu Serif Condensed Bold Italic
  • DejaVu Serif Condensed Italic

Inspection of the C:\Windows\Fonts directory shows these are TrueType fonts, kept in 8 separate *.ttf files.  However, when I view them through Windows Explorer, it is clever enough to combine them into 8 variants of the same font, called DejaVu Serif.

Similarly, if I call _ChooseFont(), it displays DejaVu Serif only once in the Font list, with each of the 8 different font faces as a separate entry in the Font Style list.

On the other hand, if I call _WinAPI_EnumFontFamilies() with a null string as the second argument, it lists DejaVu Serif and DejaVu Serif Condensed as separate entries in the return array.  If I call

  • _WinAPI_EnumFontFamilies(0, "DejaVu Serif", ...)

it returns four items (the first four in the list above).  If I call

  • _WinAPI_EnumFontFamilies(0, "DejaVu Serif Condensed", ...)

I get the second four.

As (I think) I understand it, you should call _WinAPI_EnumFontFamilies() once with an empty string for the typeface name, and get one font in each available typeface.  Then you call it a second time with the specific typeface name to get all the fonts within the specified name.

Following that paradigm, I'd always get these fonts in two separate groups, which is not how they appear in either Windows Explorer or _ChooseFont().  I assume _ChooseFont() is using _WinAPI_EnumFontFamilies() (or more likely, I suppose, the corresponding underlying Windows function).  How does it know that these 8 fonts "go together"?  Simply by their similar names?

Appreciate any insight anyone might have.

Thanks!

/John

Link to comment
Share on other sites

50 minutes ago, DrJohn said:

I assume _ChooseFont() is using _WinAPI_EnumFontFamilies() (or more likely, I suppose, the corresponding underlying Windows function).

The _ChooseFont() function uses the ChooseFont Win32 API function in the comdlg32 DLL.  To see for yourself, just open the misc.au3 include file and find the _ChooseFont() function.  Or, If you have Scite4AutoIt installed, just place your cursor on the _ChooseFont() function in your script and hit CTRL+J, it will take you directly to the function in the misc.au3 include file.

Edited by TheXman
Link to comment
Share on other sites

21 minutes ago, TheXman said:

The _ChooseFont() function uses the ChooseFont Win32 API function in the comdlg32 DLL.  To see for yourself, just open the misc.au3 include file and find the _ChooseFont() function.

Yes.  Shortly after I posted, I did that.

So then I guess my question is:  Why (and how) does that Win32 API function decide that all 8 DejaVu Serif styles belong together, but _WinAPI_EnumFontFamilies() doesn't.

But I guess maybe that's more of a Windows question than an AutoIt question ...

/John

Link to comment
Share on other sites

23 minutes ago, DrJohn said:

So then I guess my question is:  Why (and how) does that Win32 API function decide that all 8 DejaVu Serif styles belong together, but _WinAPI_EnumFontFamilies() doesn't.

The answer to your question is in the question itself.  ;)  The EnumFontFamiles Win32 API lists/enumerates fonts by their Families.  Referring to your example, although they have the same base font name (Typeface), DejaVu Serif & DejaVu Serif Condensed are different families.  The ChooseFont Win32 API lists all fonts without taking the family into account,  Bold & italics are types within a given font family.

Maybe the article below will help:

https://docs.microsoft.com/en-us/windows/win32/gdi/enumerating-the-installed-fonts

 

Edited by TheXman
Link to comment
Share on other sites

23 minutes ago, TheXman said:

Referring to your example, although they have the same base font name (Typeface), DejaVu Serif & DejaVu Serif Condensed are different families.  The ChooseFont Win32 API lists all fonts without taking the family into account,  Bold & italics are types within a given font family.

Maybe the article below will help:

https://docs.microsoft.com/en-us/windows/win32/gdi/enumerating-the-installed-fonts

I had seen the article you mentioned.  And what you say makes sense.

But unless I'm being especially dense (a distinct possibility), it still doesn't make clear to me:  If DejaVu Serif and DejaVu Serif Condensed are in different families, by what criteria does ChooseFont determine that they should be grouped together under the single heading "DejaVu Serif"?  The Windows Fonts Settings page does the same.888473345_Image1.thumb.png.324e21f097c441d56f55fb13fcc859c4.png

 

Link to comment
Share on other sites

I think I misspoke.  In this particular case, your "DejaVu Serif" is the font family, which contains both Regular and Condensed styles.  I believe that when the fonts are installed in Windows, it puts all of the metadata related to the fonts in a database.  I think that database is what is queried by the different APIs.  ChooseFont and EnumFontFamilies just make different queries against that database.

I don't have DejaVu Serif installed on any of my PCs so I can't see exactly how that font/family is organized other than your screen shot.

Below is a good link to show how some Windows 10 fonts have similar names but are in different families.  It also shows what fonts and font names belong to the default Windows 10 font families.

https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list

 

Edited by TheXman
Link to comment
Share on other sites

11 hours ago, TheXman said:

In this particular case, your "DejaVu Serif" is the font family, which contains both Regular and Condensed styles.

I think that sums up my confusion.  If that's so, my expectation would be that if I call _WinAPI_EnumFontFamilies(""), it would only return one family ("DejaVu Serif"), instead of two ("DejaVu Serif" and "DejaVu Serif Condensed").  And then if I call _WinAPI_EnumFontFamilies("DejaVu Serif"), I'd get the Condensed styles too.  Instead, it seems the function regards the Condensed styles as a separate family.  It's not a huge deal, really.  It just means if I were to create a font selection GUI using this methodology, it would display two separate families instead of combining them together like Windows and _ChooseFont() do.

By way of example with a more standard font, the same thing happens on my system with Calibri.  There are 6 Calibri *.ttf files in the C:\Windows\Fonts directory, corresponding to Regular, Italic, Bold, Bold Italic, Light and Light Italic.  Windows font viewers (Settings, and navigating to the C:\Windows\Fonts folder in Windows Explorer) correctly group all 6 under the family "Calibri".

But _WinAPI_EnumFontFamilies() returns Calibri and Calibri Light as separate families.  Try though I might, I can't figure out how Windows (and _ChooseFont()) are programmatically determining that these all belong together.  There isn't anything in the .ttf files, nor the C:\Windows\Fonts directory, nor the registry that I can find that associates them.

Again, not really that big a deal.  It's more in the "Why can't I figure this out?  There must be a way ..." category.

/John

EDIT:  In fact, if I use a font viewer app (https://us.fontviewer.de) to examine the .ttf files, it displays "Calibri" as the Family for the first four files, and "Calibri Light" as the Family for the light files.  It seems these are different families.  Yet Windows and _ChooseFont() know to group them together somehow.  I suppose it could simply be going by their similar names, but that seems not reliable.

Edited by DrJohn
Additional information
Link to comment
Share on other sites

My confusion grows.

On my system, there is a single Bahnschrift .ttf file (bahnschrift.ttf).  It evidently contains 15 styles (Sub Families?  Font Faces?).  As usual, Windows Settings/Explorer and _ChooseFont() display them the way I'd expect -- a single Font entry with 15 different variants (_ChooseFont() calls them Styles; Windows Settings calls them font faces).  Here it makes sense to me how Windows and _ChooseFont() knows they are grouped together -- they're all in the same TrueType file.

Calling _WinAPI_EnumFontFamilies("") does not return them all as one family.  It mostly breaks them out into separate families.  It returns 12 separate families in all (Bahnschrift, Bahnschrift Condensed, Bahnschrift Light, Bahnschrift SemiBold, ...).  So here, even though I think _WinAPI_EnumFontFamilies() should understand that these are grouped together, it still doesn't.

There is obviously something here I just fail to understand.

Edited by DrJohn
Link to comment
Share on other sites

I understand and am starting to share some of your confusion.  :)

It looks like some of the confusion is coming from the many different terms used to describe fonts and how the Microsoft API functions are named and implemented.  If we assume that Calibri is a "Family" name and "Calibri (Regular)" and "Calibri Light (Regular)" are different "Styles" within the Calibri Family, then it appears that the EnumFontFamilies API function uses the Style name to differentiate between fonts and not the Family name.  That's a bit counter intuitive.  By the same token, it appears that the ChooseFont API function builds its dialog by pulling all of the Styles for a specified Family together and displays them as one -- which is a bit more intuitive.  I agree, it is confusing.  I guess it is probably obvious to a typographer :) but for people who don't deal with fonts and typography all the time, like us, something just doesn't seem right.  :)

Edited by TheXman
Link to comment
Share on other sites

3 hours ago, TheXman said:

I understand and am starting to share some of your confusion.

I'm glad it isn't just me.

3 hours ago, TheXman said:

it appears that the ChooseFont API function builds its dialog by pulling all of the Styles for a specified Family together and displays them as one -- which is a bit more intuitive.

Agreed, that is more intuitive.  The question is, how does it "pull all the Styles together"?  How does it know that they go together?

I'm hardly an expert on fonts.  I've just spent some time staring at the MS documentation for EnumFontFamiliesEx() and looking at the structure of the .ttf files in the Windows Fonts directory.  From what I can see, there isn't anything that ties "Calibri" and "Calibri Light" together except that they both begin with the string "Calibri".  Likewise "Bahnschrift", "Banschrift Condensed", "Banschrift Light", and so on -- they all start with the string "Bahnschrift".

I could imagine writing code to group the Styles together based on their common name stem.  But a quick perusal of some of the other font names suggests that won't be correct.  For example, there's Segoe UI, Segoe UI Emoji, Segoe UI Historic, and Segoe UI Symbol.  They all share the common stem "Segoe UI".  But the do not evidently belong together.  Both _ChooseFont() and the Windows Font Settings agree on this, and list them separately.  Same with Cambria and Cambria Math.

There is obviously some logic by which you can decide when to pull Styles together and display them as one and when not to, which ChooseFont and the Windows Font Settings evidently share.  In my probably too limited understanding, I have been unable to find what it is.  The practical upshot is that if I were to implement a choose-font GUI based on _WinAPI_EnumFontFamilies(), lacking any clear method for grouping entries together, I'd end up displaying twelve variants of Bahnschrift in the primary Font Family window.  Which doesn't seem very rewarding.  😕

Image 2.png

 

(BTW, I realize this is probably more of a Windows API issue than an AutoIt issue, since as best as I can tell, _WinAPI_EnumFontFamilies() just passes straight through to the gdi32.dll EnumFontFamiliesExW() function.  I was just hoping someone might have wrestled with this before and have some insight I clearly lack ...)

Edited by DrJohn
Link to comment
Share on other sites

1 hour ago, DrJohn said:

I've just spent some time staring at the MS documentation for EnumFontFamiliesEx() and looking at the structure of the .ttf files in the Windows Fonts directory.  From what I can see, there isn't anything that ties "Calibri" and "Calibri Light" together except that they both begin with the string "Calibri".

I did a little quick research and found that there is a hidden file in the \Windows\Fonts folder named fms_metadata.xml.  If you edit the file you will see that it lists font families.  I don't know if it is a complete list, but it looks like it.  Using a combination of the fms_metadata.xml to get a list of font families and the EnumFontFamilies function to get a list of all of the font styles, it wouldn't be too hard to generate a list that looks like the information on the ChooseFont dialog.

I also read that Windows 10 adds another layer of complexity because unlike previous versions of Windows, fonts downloaded & installed from the Windows Store are kept in a different location than \Windows\Fonts.  But that is a different issue altogether.  :)

Edited by TheXman
Link to comment
Share on other sites

Wow!  Dude, you rock.

I actually had a bash window open in that directory, nosing around the files.  (Windows Explorer won't show you anything but fonts in that directory).  But I somehow missed that one.  Will take a look and see how far I get ...

/John

Link to comment
Share on other sites

I might have spoken too soon.  (Which isn't to say that you don't still rock ...)

fms_metadata.xml doesn't appear to be a complete list.  There are quite a few fonts that enumerate with _WinAPI_EnumFontFamilies("") that aren't in the file.  Bahnschrift, for instance, isn't there (at least not on my system).

😐

/John

Link to comment
Share on other sites

FWIW, I just ended up grouping font styles based on their names (e.g., all styles that begin 'Bahnschrift' group under the family Bahnschrift, and so on).

It more or less works passably.  It doesn't produce the same groupings (quite) as _ChooseFont(), since I was never able to determine how _ChooseFont() determines those groupings.  I even posted to stackoverflow to see if any Windows internals guru might have some insight.  But so far, no takers.

/John

Link to comment
Share on other sites

Sure thing.  I don't think it's rocket science, but if it would be helpful ...

Here's a sample script that creates a Font Family/Style/Size chooser GUI.

EnumFontTree() uses _WinAPI_EnumFontFamilies() to gather a list of all installed font families (this particular version leaves out those beginning with "@"), and organizes it into a tree of families and associated sub-families.  The funny business happens on lines 130-131 -- any font families in the initial return list that start with the same 'stem' (prefix) get grouped together as associated 'sub-families'

Then when EnumFontStyles() enumerates the styles for a given family, it includes both those obtained by calling _WinAPI_EnumFontFamilies() on that family, plus any 'sub-families' that were collected in the initial pass.

(The rest of the code is just dedicated to creating and managing the three combo boxes).

The result is similar to _ChooseFont(), but not identical.  Note, for example, that if you select Cambria from the Family list, the Styles list populates with Cambria, Cambria Bold, Cambria Bold Italic, Cambria Italic ... and Cambria Math_ChooseFont() doesn't do this.  My code groups Cambria Math together with the others because the name begins with the same 'stem'.  _ChooseFont() doesn't because ... I don't know why.

 

#include <GuiConstantsEx.au3>
#include <WinAPIGdi.au3>
#include <FontConstants.au3>
#include <WindowsConstants.au3>
#include <GuiComboBox.au3>
#include <Array.au3>

Main()


; --------------------------------------------------------------------------
func Main()

    $gui = GUICreate("FontEnum", 800, 600)
    GUISetState(@SW_SHOW, $gui)

    $cur_font = "Arial Bold"
    $cur_size = 12

    $family_combo = GUICtrlCreateCombo _
    ( _
        "", _
        10, 10, _
        250, 300, _
        BitOR($CBS_SIMPLE, $WS_VSCROLL) _
    )
    $style_combo = GUICtrlCreateCombo _
    ( _
        "", _
        270, 10, _
        250, 200, _
        BitOR($CBS_SIMPLE, $WS_VSCROLL) _
    )
    $size_combo = GUICtrlCreateCombo("", 530, 10, 100, 20)
    GUICtrlSetData _
    ( _
        $size_combo, _
        "8|9|10|11|12|13|14|16|18|20|22|24|26|28|30|32|36|42|48", _
        $cur_size _
    )

    $sample_label = GUICtrlCreateLabel($cur_font, 10, 320, 780, 100)
    GUICtrlSetFont($sample_label, $cur_size, $FW_DONTCARE, $GUI_FONTNORMAL, $cur_font)

    $font_tree = EnumFontTree()
    PopulateComboBoxes($font_tree, $family_combo, $style_combo, $cur_font)

    while True
        $msg = GUIGetMsg()

        switch $msg

            case $family_combo, $style_combo, $size_combo
                
                if ($msg = $family_combo) then
                    PopulateStylesComboBox($font_tree, $family_combo, $style_combo, "")
                endif

                $cur_font = GUICtrlRead($style_combo)
                $cur_size = GUICtrlRead($size_combo)
                GUICtrlSetFont _
                ( _
                    $sample_label, _
                    $cur_size, _
                    $FW_DONTCARE, _
                    $GUI_FONTNORMAL, _
                    $cur_font _
                )
                GUICtrlSetData($sample_label, $cur_font)

            case $GUI_EVENT_CLOSE
                Exit
        endswitch
    wend

endfunc


; --------------------------------------------------------------------------
func PopulateComboBoxes($ft, $f, $st, $sel)

    for $i from 0 to ubound($ft) - 1
        $family = $ft[$i][0]
        _GUICtrlComboBox_AddString($f, $family)

        if (($sel <> "") and (StringLeft($sel, StringLen($family)) = $family)) then
            _GUICtrlComboBox_SetCurSel($f, $i)
            PopulateStylesComboBox($ft, $f, $st, $sel)
        endif
    next

endfunc


; --------------------------------------------------------------------------
func PopulateStylesComboBox($ft, $f, $st, $sel)

    _GUICtrlComboBox_ResetContent($st)
    $styles = EnumFontStyles($ft, GUICtrlRead($f))
    for $i from 0 to ubound($styles) - 1
        $style = $styles[$i]
        _GUICtrlComboBox_AddString($st, $style)
        if (($sel <> "") and ($style = $sel)) then
            _GUICtrlComboBox_SetCurSel($st, $i)
        else
            _GUICtrlComboBox_SetCurSel($st, 0)
        endif
    next

endfunc


; --------------------------------------------------------------------------
func EnumFontTree()

    local $font_list = _WinAPI_EnumFontFamilies(0, "", $ANSI_CHARSET, -1, "@*", True)
    _ArraySort($font_list, 0, 1, 0, 0)

    local $font_tree[0][2]
    $i = 1
    while ($i <= $font_list[0][0])
        $stem = $font_list[$i][0]
        _ArrayAdd($font_tree, $stem)

        local $sub[0]
        do
            $i += 1
            if ($i > $font_list[0][0]) then ExitLoop
            $st = $font_list[$i][0]
            if (StringLeft($st, StringLen($stem)) <> $stem) then ExitLoop
            _ArrayAdd($sub, $st)
        until False
        $font_tree[ubound($font_tree)-1][1] = $sub
    wend

    return $font_tree
endfunc


; --------------------------------------------------------------------------
func EnumFontStyles($font_tree, $family)

    local $styles[0]

    $idx = _ArrayBinarySearch($font_tree, $family)
    if ($idx >= 0) then

        ; Get base family styles
        $a = _WinAPI_EnumFontFamilies(0, $family, $ANSI_CHARSET, -1)
        for $i = 1 to $a[0][0]
            _ArrayAdd($styles, $a[$i][2])
        next

        ; Get subfamily styles
        $sub = $font_tree[$idx][1]
        for $i in $sub
            _ArrayAdd($styles, $i)
        next
    endif

    _ArraySort($styles)
    return _ArrayUnique($styles, Default, Default, Default, $ARRAYUNIQUE_NOCOUNT)
endfunc

/John

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...