Search code examples
pythonpython-3.xttx-fonttools

Get metadata from ttf font files in windows with python and fontTools library


To be able to quickly find the font I want (windows only for now) I want to create a list I can query to find the font name, family and styles available for the font.

Using fontTools and ttlib , I am able to create the list but, when the font styles are not a part of the file name, I am not able to get them.

Example, I get the styles for the Arial font if they have a different file for them: Arial styles

But for Cascadia Code, one ttf, contains multiple styles: Cascadia ttf file

I can see the styles on the windows app: cascadia styles

I found a code that does work for some fonts, but not all:

from fontTools import ttLib

path = "C:\\Windows\\Fonts\\CascadiaCode.ttf"
font = ttLib.TTFont(path)

for instance in font["fvar"].instances:
    style = font["name"].getName(instance.subfamilyNameID, 3, 1, 0x409)
    print(f"{family} {style}")

Which returns the right styles:

Cascadia Code ExtraLight
Cascadia Code Light
Cascadia Code SemiLight
Cascadia Code Regular
Cascadia Code SemiBold
Cascadia Code Bold

But if I use it with other fonts, ex. arial, it returns an error. Not sure why.

Here is my current code:

import os
import pandas as pd
from fontTools import ttLib

paths = []
for file in os.listdir(r'C:\Windows\fonts'):
    if file.endswith('.ttf'):
        font_path = os.path.join("C:\\Windows\\Fonts\\", file)
        paths.append(font_path)
        table = []
        for p in paths:
            font = ttLib.TTFont(str(p))        
            fontFamilyName = font['name'].getDebugName(1)
            style = font['name'].getDebugName(2)
            fullName= font['name'].getDebugName(3)
            table.append({'family':fontFamilyName, 'name':fullName, 'style':style, 'path': p})

df=pd.DataFrame(table, columns = ['family', 'name', 'style', 'path'])
df['name'] =  df['name'].str.split(':').str[-1]
df = df.drop_duplicates()
df.head()

Any ideas?Thanks.


Solution

  • You have a problem with "fvar" because the fvar table is only used in variable font (Cascadia is a variable font).

    I recommand you to use DirectWrite instead of parsing the font manually.

    This answer is based on List all system fonts as dictionary. | python

    import pandas as pd
    from pathlib import Path
    from comtypes import COMError, GUID, IUnknown, STDMETHOD
    from ctypes import byref, create_unicode_buffer, HRESULT, POINTER, windll, wintypes
    from dataclasses import dataclass
    from enum import IntEnum, IntFlag
    from sys import getwindowsversion
    from typing import List, Set
    
    
    @dataclass
    class FontFace:
        file_path: List[str] # See this link to know why we need a list: https://stackoverflow.com/questions/41161152/when-can-an-idwritefontface-have-more-than-one-file
        face_name: str
        font_name: str
    
    
    @dataclass
    class FontFamily:
        family_name: str
        fonts: List[FontFace]
    
    
    class DWRITE_FACTORY_TYPE(IntEnum):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_factory_type
        DWRITE_FACTORY_TYPE_SHARED = 0
        DWRITE_FACTORY_TYPE_ISOLATED = 1
    
    
    class DWRITE_FONT_SIMULATIONS(IntFlag):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_font_simulations
        DWRITE_FONT_SIMULATIONS_NONE = 0x0000
        DWRITE_FONT_SIMULATIONS_BOLD = 0x0001
        DWRITE_FONT_SIMULATIONS_OBLIQUE = 0x0002
    
    
    class DWRITE_INFORMATIONAL_STRING_ID(IntEnum):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_informational_string_id
        DWRITE_INFORMATIONAL_STRING_NONE = 0
        DWRITE_INFORMATIONAL_STRING_COPYRIGHT_NOTICE = 1
        DWRITE_INFORMATIONAL_STRING_VERSION_STRINGS = 2
        DWRITE_INFORMATIONAL_STRING_TRADEMARK = 3
        DWRITE_INFORMATIONAL_STRING_MANUFACTURER = 4
        DWRITE_INFORMATIONAL_STRING_DESIGNER = 5
        DWRITE_INFORMATIONAL_STRING_DESIGNER_URL = 6
        DWRITE_INFORMATIONAL_STRING_DESCRIPTION = 7
        DWRITE_INFORMATIONAL_STRING_FONT_VENDOR_URL = 8
        DWRITE_INFORMATIONAL_STRING_LICENSE_DESCRIPTION = 9
        DWRITE_INFORMATIONAL_STRING_LICENSE_INFO_URL = 10
        DWRITE_INFORMATIONAL_STRING_WIN32_FAMILY_NAMES = 11
        DWRITE_INFORMATIONAL_STRING_WIN32_SUBFAMILY_NAMES = 12
        DWRITE_INFORMATIONAL_STRING_TYPOGRAPHIC_FAMILY_NAMES = 13
        DWRITE_INFORMATIONAL_STRING_TYPOGRAPHIC_SUBFAMILY_NAMES = 14
        DWRITE_INFORMATIONAL_STRING_SAMPLE_TEXT = 15
        DWRITE_INFORMATIONAL_STRING_FULL_NAME = 16
        DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME = 17
        DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_CID_NAME = 18
        DWRITE_INFORMATIONAL_STRING_WEIGHT_STRETCH_STYLE_FAMILY_NAME = 19
        DWRITE_INFORMATIONAL_STRING_DESIGN_SCRIPT_LANGUAGE_TAG = 20
        DWRITE_INFORMATIONAL_STRING_SUPPORTED_SCRIPT_LANGUAGE_TAG = 21
        DWRITE_INFORMATIONAL_STRING_PREFERRED_FAMILY_NAMES = DWRITE_INFORMATIONAL_STRING_TYPOGRAPHIC_FAMILY_NAMES
        DWRITE_INFORMATIONAL_STRING_PREFERRED_SUBFAMILY_NAMES = DWRITE_INFORMATIONAL_STRING_TYPOGRAPHIC_SUBFAMILY_NAMES
        DWRITE_INFORMATIONAL_STRING_WWS_FAMILY_NAME = DWRITE_INFORMATIONAL_STRING_WEIGHT_STRETCH_STYLE_FAMILY_NAME
    
    
    class IDWriteFontFileLoader(IUnknown):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontfileloader
        _iid_ = GUID("{727cad4e-d6af-4c9e-8a08-d695b11caa49}")
        _methods_ = [
            STDMETHOD(None, "CreateStreamFromKey"),  # Need to be implemented
        ]
    
    
    class IDWriteLocalFontFileLoader(IDWriteFontFileLoader):
        # https://learn.microsoft.com/en-us/windows/win32/directwrite/idwritelocalfontfileloader
        _iid_ = GUID("{b2d9f3ec-c9fe-4a11-a2ec-d86208f7c0a2}")
        _methods_ = [
            STDMETHOD(HRESULT, "GetFilePathLengthFromKey", [wintypes.LPCVOID, wintypes.UINT, POINTER(wintypes.UINT)]),
            STDMETHOD(HRESULT, "GetFilePathFromKey", [wintypes.LPCVOID, wintypes.UINT, POINTER(wintypes.WCHAR), wintypes.UINT]),
            STDMETHOD(None, "GetLastWriteTimeFromKey"),  # Need to be implemented
        ]
    
    
    class IDWriteFontFile(IUnknown):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontfile
        _iid_ = GUID("{739d886a-cef5-47dc-8769-1a8b41bebbb0}")
        _methods_ = [
            STDMETHOD(HRESULT, "GetReferenceKey", [POINTER(wintypes.LPCVOID), POINTER(wintypes.UINT)]),
            STDMETHOD(HRESULT, "GetLoader", [POINTER(POINTER(IDWriteFontFileLoader))]),
            STDMETHOD(HRESULT, "Analyze", [POINTER(wintypes.BOOL), POINTER(wintypes.UINT), POINTER(wintypes.UINT), POINTER(wintypes.UINT)]),
        ]
    
    
    class IDWriteLocalizedStrings(IUnknown):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritelocalizedstrings
        _iid_ = GUID("{08256209-099a-4b34-b86d-c22b110e7771}")
        _methods_ = [
            STDMETHOD(wintypes.UINT, "GetCount"),
            STDMETHOD(HRESULT, "FindLocaleName", [POINTER(wintypes.WCHAR), POINTER(wintypes.UINT), POINTER(wintypes.BOOL)]),
            STDMETHOD(HRESULT, "GetLocaleNameLength", [wintypes.UINT, POINTER(wintypes.UINT)]),
            STDMETHOD(HRESULT, "GetLocaleName", [wintypes.UINT, POINTER(wintypes.WCHAR), wintypes.UINT]),
            STDMETHOD(HRESULT, "GetStringLength", [wintypes.UINT, POINTER(wintypes.UINT)]),
            STDMETHOD(HRESULT, "GetString", [wintypes.UINT, POINTER(wintypes.WCHAR), wintypes.UINT]),
        ]
    
    
    class IDWriteFontFace(IUnknown):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontface
        _iid_ = GUID("{5f49804d-7024-4d43-bfa9-d25984f53849}")
        _methods_ = [
            STDMETHOD(None, "GetType"),  # Need to be implemented
            STDMETHOD(HRESULT, "GetFiles", [POINTER(wintypes.UINT), POINTER(POINTER(IDWriteFontFile))]),
            STDMETHOD(None, "GetIndex"),  # Need to be implemented
            STDMETHOD(None, "GetSimulations"),  # Need to be implemented
            STDMETHOD(None, "IsSymbolFont"),  # Need to be implemented
            STDMETHOD(None, "GetMetrics"),  # Need to be implemented
            STDMETHOD(None, "GetGlyphCount"),  # Need to be implemented
            STDMETHOD(None, "GetDesignGlyphMetrics"),  # Need to be implemented
            STDMETHOD(None, "GetGlyphIndices"),  # Need to be implemented
            STDMETHOD(None, "TryGetFontTable"),  # Need to be implemented
            STDMETHOD(None, "ReleaseFontTable"),  # Need to be implemented
            STDMETHOD(None, "GetGlyphRunOutline"),  # Need to be implemented
            STDMETHOD(None, "GetRecommendedRenderingMode"),  # Need to be implemented
            STDMETHOD(None, "GetGdiCompatibleMetrics"),  # Need to be implemented
            STDMETHOD(None, "GetGdiCompatibleGlyphMetrics"),  # Need to be implemented
        ]
    
    
    class IDWriteFont(IUnknown):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefont
        _iid_ = GUID("{acd16696-8c14-4f5d-877e-fe3fc1d32737}")
        _methods_ = [
            STDMETHOD(None, "GetFontFamily"),  # Need to be implemented
            STDMETHOD(None, "GetWeight"),  # Need to be implemented
            STDMETHOD(None, "GetStretch"),  # Need to be implemented
            STDMETHOD(None, "GetStyle"),  # Need to be implemented
            STDMETHOD(None, "IsSymbolFont"),  # Need to be implemented
            STDMETHOD(HRESULT, "GetFaceNames", [POINTER(POINTER(IDWriteLocalizedStrings))]),
            STDMETHOD(HRESULT, "GetInformationalStrings", [wintypes.UINT, POINTER(POINTER(IDWriteLocalizedStrings)), POINTER(wintypes.BOOL)]),
            STDMETHOD(wintypes.UINT, "GetSimulations"),
            STDMETHOD(None, "GetMetrics"),  # Need to be implemented
            STDMETHOD(None, "HasCharacter"),  # Need to be implemented
            STDMETHOD(HRESULT, "CreateFontFace", [POINTER(POINTER(IDWriteFontFace))]),
        ]
    
    
    class IDWriteFontList(IUnknown):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontlist
        _iid_ = GUID("{1a0d8438-1d97-4ec1-aef9-a2fb86ed6acb}")
        _methods_ = [
            STDMETHOD(None, "GetFontCollection"),  # Need to be implemented
            STDMETHOD(wintypes.UINT, "GetFontCount"),
            STDMETHOD(HRESULT, "GetFont", [wintypes.UINT, POINTER(POINTER(IDWriteFont))]),
        ]
    
    
    class IDWriteFontFamily(IDWriteFontList):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontfamily
        _iid_ = GUID("{da20d8ef-812a-4c43-9802-62ec4abd7add}")
        _methods_ = [
            STDMETHOD(HRESULT, "GetFamilyNames", [POINTER(POINTER(IDWriteLocalizedStrings))]),
            STDMETHOD(None, "GetFirstMatchingFont"),  # Need to be implemented
            STDMETHOD(None, "GetMatchingFonts"),  # Need to be implemented
        ]
    
    
    class IDWriteFontCollection(IUnknown):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection
        _iid_ = GUID("{a84cee02-3eea-4eee-a827-87c1a02a0fcc}")
        _methods_ = [
            STDMETHOD(wintypes.UINT, "GetFontFamilyCount"),
            STDMETHOD(HRESULT, "GetFontFamily", [wintypes.UINT, POINTER(POINTER(IDWriteFontFamily))]),
            STDMETHOD(None, "FindFamilyName"),  # Need to be implemented
            STDMETHOD(None, "GetFontFromFontFace"),  # Need to be implemented
        ]
    
    
    class IDWriteFactory(IUnknown):
        # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefactory
        _iid_ = GUID("{b859ee5a-d838-4b5b-a2e8-1adc7d93db48}")
        _methods_ = [
            STDMETHOD(HRESULT, "GetSystemFontCollection", [POINTER(POINTER(IDWriteFontCollection)), wintypes.BOOLEAN]),
            STDMETHOD(None, "CreateCustomFontCollection"),  # Need to be implemented
            STDMETHOD(None, "RegisterFontCollectionLoader"),  # Need to be implemented
            STDMETHOD(None, "UnregisterFontCollectionLoader"),  # Need to be implemented
            STDMETHOD(None, "CreateFontFileReference"),  # Need to be implemented
            STDMETHOD(None, "CreateCustomFontFileReference"),  # Need to be implemented
            STDMETHOD(None, "CreateFontFace"),  # Need to be implemented
            STDMETHOD(None, "CreateRenderingParams"),  # Need to be implemented
            STDMETHOD(None, "CreateMonitorRenderingParams"),  # Need to be implemented
            STDMETHOD(None, "CreateCustomRenderingParams"),  # Need to be implemented
            STDMETHOD(None, "RegisterFontFileLoader"),  # Need to be implemented
            STDMETHOD(None, "UnregisterFontFileLoader"),  # Need to be implemented
            STDMETHOD(None, "CreateTextFormat"),  # Need to be implemented
            STDMETHOD(None, "CreateTypography"),  # Need to be implemented
            STDMETHOD(None, "GetGdiInterop"),  # Need to be implemented
            STDMETHOD(None, "CreateTextLayout"),  # Need to be implemented
            STDMETHOD(None, "CreateGdiCompatibleTextLayout"),  # Need to be implemented
            STDMETHOD(None, "CreateEllipsisTrimmingSign"),  # Need to be implemented
            STDMETHOD(None, "CreateTextAnalyzer"),  # Need to be implemented
            STDMETHOD(None, "CreateNumberSubstitution"),  # Need to be implemented
            STDMETHOD(None, "CreateGlyphRunAnalysis"),  # Need to be implemented
        ]
    
    
    class Kernel32:
        def __init__(self):
            kernel32 = windll.kernel32
    
            # https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultlocalename
            self.GetUserDefaultLocaleName = kernel32.GetUserDefaultLocaleName
            self.GetUserDefaultLocaleName.restype = wintypes.INT
            self.GetUserDefaultLocaleName.argtypes = [wintypes.LPWSTR, wintypes.INT]
            self.GetUserDefaultLocaleName.errcheck = self.errcheck_is_result_0
    
            self.LOCALE_NAME_MAX_LENGTH = 85
    
    
        def GetUserDefaultLocaleNameFunc(self) -> str:
            locale_buffer = create_unicode_buffer(self.LOCALE_NAME_MAX_LENGTH)
            try:
                self.GetUserDefaultLocaleName(locale_buffer, self.LOCALE_NAME_MAX_LENGTH)
            except ValueError:
                return "en-us"
    
            return locale_buffer.value
    
    
        @staticmethod
        def errcheck_is_result_0(result, func, args):
            if result == 0:
                raise ValueError(f"Error encountered with {func.__name__}.")
            return result
    
    
    class DirectWrite:
        def __init__(self):
            dwrite = windll.dwrite
    
            # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-dwritecreatefactory
            self.DWriteCreateFactory = dwrite.DWriteCreateFactory
            self.DWriteCreateFactory.restype = HRESULT
            self.DWriteCreateFactory.argtypes = [wintypes.UINT, GUID, POINTER(POINTER(IUnknown))]
    
    
        def get_str_from_localized_strings(self, localized_strings) -> str:
            # From https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritelocalizedstrings#remarks
    
            kernel32 = Kernel32()
            locale_name = kernel32.GetUserDefaultLocaleNameFunc()
    
            index = wintypes.UINT()
            exists = wintypes.BOOL()
            localized_strings.FindLocaleName(locale_name, byref(index), byref(exists))
    
            if not exists:
                localized_strings.FindLocaleName("en-us", byref(index), byref(exists))
    
            if not exists:
                index = 0
    
            length = wintypes.UINT()
            localized_strings.GetStringLength(index, byref(length))
    
            localized_strings_buffer = create_unicode_buffer(length.value + 1)
            localized_strings.GetString(index, localized_strings_buffer, len(localized_strings_buffer))
    
            return localized_strings_buffer.value
    
    
    class WindowsVersionHelpers:
        @staticmethod
        def is_windows_version_or_greater(windows_version, major: int, minor: int, build: int) -> bool:
            """
            Parameters:
                windows_version: An object from getwindowsversion.
                major (int): The minimum major OS version number.
                minor (int): The minimum minor OS version number.
                build (int): The minimum build version number.
            Returns:
                True if the specified version matches or if it is greater than the version of the current Windows OS. Otherwise, False.
            """
    
            if windows_version.major > major:
                return True
            elif windows_version.major == major and windows_version.minor > minor:
                return True
            else:
                return (
                    windows_version.major == major
                    and windows_version.minor == minor
                    and windows_version.build >= build
                )
    
        @staticmethod
        def is_windows_vista_sp2_or_greater(windows_version) -> bool:
            # From https://www.lifewire.com/windows-version-numbers-2625171
            return WindowsVersionHelpers.is_windows_version_or_greater(windows_version, 6, 0, 6002)
    
    
    def get_system_fonts_families() -> List[FontFamily]:
        windows_version = getwindowsversion()
    
        if not WindowsVersionHelpers.is_windows_vista_sp2_or_greater(windows_version):
            raise OSError("This program only works on Windows Vista SP2 or more")
    
        dwrite = DirectWrite()
        fonts_families: List[FontFamily] = []
    
        dwrite_factory = POINTER(IDWriteFactory)()
        dwrite.DWriteCreateFactory(DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_ISOLATED, IDWriteFactory._iid_, byref(dwrite_factory))
    
        sys_collection = POINTER(IDWriteFontCollection)()
        dwrite_factory.GetSystemFontCollection(byref(sys_collection), False)
    
        for i in range(sys_collection.GetFontFamilyCount()):
            family = POINTER(IDWriteFontFamily)()
            sys_collection.GetFontFamily(i, byref(family))
    
            family_names = POINTER(IDWriteLocalizedStrings)()
            family.GetFamilyNames(byref(family_names))
            family_names_str = dwrite.get_str_from_localized_strings(family_names)
    
            faces: List[FontFace] = []
            for j in range(family.GetFontCount()):
                try:
                    font = POINTER(IDWriteFont)()
                    family.GetFont(j, byref(font))
                except COMError:
                    # If the file doesn't exist, DirectWrite raise an exception
                    continue
    
                simulations = font.GetSimulations()
                if simulations != DWRITE_FONT_SIMULATIONS.DWRITE_FONT_SIMULATIONS_NONE:
                    continue
    
                face_file_path: List[Path] = []
    
                face_name = POINTER(IDWriteLocalizedStrings)()
                font.GetFaceNames(byref(face_name))
                face_name_str = dwrite.get_str_from_localized_strings(face_name)
    
                font_names = POINTER(IDWriteLocalizedStrings)()
                exists = wintypes.BOOL()
                font.GetInformationalStrings(
                    DWRITE_INFORMATIONAL_STRING_ID.DWRITE_INFORMATIONAL_STRING_WIN32_FAMILY_NAMES,
                    byref(font_names),
                    byref(exists)
                )
    
                if exists.value:
                    font_names_str = dwrite.get_str_from_localized_strings(font_names)
                else:
                    font_names_str = ""
    
                font_face = POINTER(IDWriteFontFace)()
                font.CreateFontFace(byref(font_face))
    
                file_count = wintypes.UINT()
                font_face.GetFiles(byref(file_count), None)
    
                font_files = (POINTER(IDWriteFontFile) * file_count.value)()
                font_face.GetFiles(byref(file_count), font_files)
    
                for font_file in font_files:
                    font_file_reference_key = wintypes.LPCVOID()
                    font_file_reference_key_size = wintypes.UINT()
                    font_file.GetReferenceKey(byref(font_file_reference_key), byref(font_file_reference_key_size))
    
                    loader = POINTER(IDWriteFontFileLoader)()
                    font_file.GetLoader(byref(loader))
    
                    local_loader = loader.QueryInterface(IDWriteLocalFontFileLoader)
    
                    is_supported_font_type = wintypes.BOOL()
                    font_file_type = wintypes.UINT()
                    font_face_type = wintypes.UINT()
                    number_of_faces = wintypes.UINT()
                    font_file.Analyze(byref(is_supported_font_type), byref(font_file_type), byref(font_face_type), byref(number_of_faces))
    
                    path_len = wintypes.UINT()
                    local_loader.GetFilePathLengthFromKey(font_file_reference_key, font_file_reference_key_size, byref(path_len))
    
                    buffer = create_unicode_buffer(path_len.value + 1)
                    local_loader.GetFilePathFromKey(font_file_reference_key, font_file_reference_key_size, buffer, len(buffer))
    
                    face_file_path.append(str(Path(buffer.value).resolve()))
                faces.append(FontFace(face_file_path, face_name_str, font_names_str))
            fonts_families.append(FontFamily(family_names_str, faces))
        return fonts_families
    
    
    def main():
        font_families = get_system_fonts_families()
    
        table = []
        for font_family in font_families:
            family = font_family.family_name
            for font in font_family.fonts:
                name = font.font_name
                style = font.face_name
                path = font.file_path[0] # for 99.99% of the case, their is only 1 file
                table.append({'family':family, 'name':name, 'style':style, 'path': path})
    
        df=pd.DataFrame(table, columns = ['family', 'name', 'style', 'path'])
        df['name'] =  df['name'].str.split(':').str[-1]
        df = df.drop_duplicates()
    
        df_sorted = df.sort_values(by='family')
    
        with open('font_data.md', 'w', encoding="utf-8") as f:
            f.write(df_sorted.to_markdown(index=True))
    
    if __name__ == "__main__":
        main()