Skip to content

Commit

Permalink
[windows_fonts] Interop with GDI to get the system font filename
Browse files Browse the repository at this point in the history
This is a test. This may not resolve the issue #14
This is slower than the previous approch
  • Loading branch information
moi15moi committed Mar 28, 2024
1 parent d1b5b9a commit 75d337b
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 105 deletions.
124 changes: 123 additions & 1 deletion find_system_fonts_filename/windows/gdi.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,53 @@
from ctypes import Structure, c_ubyte, windll, wintypes
from ctypes import POINTER, c_ubyte, Structure, windll, WINFUNCTYPE, wintypes
from enum import IntEnum

__all__ = [
"LOGFONTW",
"TEXTMETRIC",
"GDI",
]

class Pitch(IntEnum):
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/22dbe377-aec4-4669-88e6-b8fdd9351d76
DEFAULT_PITCH = 0
FIXED_PITCH = 1
VARIABLE_PITCH = 2


class Family(IntEnum):
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/9a632766-1f1c-4e2b-b1a4-f5b1a45f99ad
FF_DONTCARE = 0 << 4
FF_ROMAN = 1 << 4
FF_SWISS = 2 << 4
FF_MODERN = 3 << 4
FF_SCRIPT = 4 << 4
FF_DECORATIVE = 5 << 4


class CharacterSet(IntEnum):
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/0d0b32ac-a836-4bd2-a112-b6000a1b4fc9
ANSI_CHARSET = 0x00000000
DEFAULT_CHARSET = 0x00000001
SYMBOL_CHARSET = 0x00000002
MAC_CHARSET = 0x0000004D
SHIFTJIS_CHARSET = 0x00000080
HANGUL_CHARSET = 0x00000081
JOHAB_CHARSET = 0x00000082
GB2312_CHARSET = 0x00000086
CHINESEBIG5_CHARSET = 0x00000088
GREEK_CHARSET = 0x000000A1
TURKISH_CHARSET = 0x000000A2
VIETNAMESE_CHARSET = 0x000000A3
HEBREW_CHARSET = 0x000000B1
ARABIC_CHARSET = 0x000000B2
BALTIC_CHARSET = 0x000000BA
RUSSIAN_CHARSET = 0x000000CC
THAI_CHARSET = 0x000000DE
EASTEUROPE_CHARSET = 0x000000EE
OEM_CHARSET = 0x000000FF
FEOEM_CHARSET = 254 # From https://github.com/tongzx/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/windows/core/ntgdi/fondrv/tt/ttfd/fdfon.c#L6718


class LOGFONTW(Structure):
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logfontw
_fields_ = [
Expand All @@ -25,6 +67,22 @@ class LOGFONTW(Structure):
("lfFaceName", wintypes.WCHAR * 32),
]

def __str__(self) -> str:
attributes = []
for field_name, _ in self._fields_:
value = getattr(self, field_name)
if field_name == "lfCharSet":
value = CharacterSet(value).name
elif field_name == "lfPitchAndFamily":
family = value & 0b11110000
pitch = value & 0b00001111
value = f"{Pitch(pitch).name}|{Family(family).name}"
elif field_name == "lfItalic":
value = bool(value)

attributes.append(f"{field_name}: {value}")
return "\n".join(attributes)


class TEXTMETRIC(Structure):
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-textmetricw
Expand Down Expand Up @@ -52,6 +110,16 @@ class TEXTMETRIC(Structure):
]


class ENUMLOGFONTEXW(Structure):
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-enumlogfontexw
_fields_ = [
("elfLogFont", LOGFONTW),
("elfFullName", wintypes.WCHAR * 64),
("elfStyle", wintypes.WCHAR * 32),
("elfScript", wintypes.WCHAR * 32),
]


class GDI:
def __init__(self) -> None:
gdi = windll.gdi32
Expand All @@ -68,8 +136,62 @@ def __init__(self) -> None:
self.DeleteDC.argtypes = [wintypes.HDC]
self.DeleteDC.errcheck = self.errcheck_is_result_0_or_null

# https://learn.microsoft.com/en-us/previous-versions/dd162618(v=vs.85)
self.ENUMFONTFAMEXPROC = WINFUNCTYPE(
wintypes.INT,
ENUMLOGFONTEXW,
TEXTMETRIC,
wintypes.DWORD,
wintypes.LPARAM,
)

# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-enumfontfamiliesw
self.EnumFontFamiliesW = gdi.EnumFontFamiliesW
self.EnumFontFamiliesW.restype = wintypes.INT
self.EnumFontFamiliesW.argtypes = [wintypes.HDC, wintypes.LPCWSTR, self.ENUMFONTFAMEXPROC, wintypes.LPARAM]

# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-enumfontfamiliesexw
self.EnumFontFamiliesExW = gdi.EnumFontFamiliesExW
self.EnumFontFamiliesExW.restype = wintypes.INT
self.EnumFontFamiliesExW.argtypes = [wintypes.HDC, POINTER(LOGFONTW), self.ENUMFONTFAMEXPROC, wintypes.LPARAM, wintypes.DWORD]

# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-enumfontsw
self.EnumFontsW = gdi.EnumFontsW
self.EnumFontsW.restype = wintypes.INT
self.EnumFontsW.argtypes = [wintypes.HDC, wintypes.LPCWSTR, self.ENUMFONTFAMEXPROC, wintypes.LPARAM]

self.RASTER_FONTTYPE = 0x0001
self.DEVICE_FONTTYPE = 0x0002
self.TRUETYPE_FONTTYPE = 0x0004

# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createfontindirectw
self.CreateFontIndirectW = gdi.CreateFontIndirectW
self.CreateFontIndirectW.restype = wintypes.HFONT
self.CreateFontIndirectW.argtypes = [POINTER(LOGFONTW)]
self.CreateFontIndirectW.errcheck = self.errcheck_is_result_0_or_null

# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-selectobject
self.SelectObject = gdi.SelectObject
self.SelectObject.restype = wintypes.HGDIOBJ
self.SelectObject.argtypes = [wintypes.HDC, wintypes.HGDIOBJ]
self.SelectObject.errcheck = self.is_SelectObject_failed

# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-deleteobject
self.DeleteObject = gdi.DeleteObject
self.DeleteObject.restype = wintypes.BOOL
self.DeleteObject.argtypes = [wintypes.HGDIOBJ]
self.DeleteObject.errcheck = self.errcheck_is_result_0_or_null


@staticmethod
def errcheck_is_result_0_or_null(result, func, args):
if not result:
raise OSError(f"{func.__name__} fails. The result is {result} which is invalid")
return result

@staticmethod
def is_SelectObject_failed(result, func, args):
HGDI_ERROR = wintypes.HGDIOBJ(0xFFFFFFFF)
if result == None or result == HGDI_ERROR:
raise OSError(f"{func.__name__} fails. The result is {result} which is invalid")
return result
168 changes: 64 additions & 104 deletions find_system_fonts_filename/windows/windows_fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
IDWriteFactory3,
DirectWrite,
)
from .gdi import GDI, LOGFONTW
from .gdi import GDI, LOGFONTW, ENUMLOGFONTEXW, TEXTMETRIC, CharacterSet
from .gdiplus import FontStyle, GDIPlus, GdiplusStartupInput, GdiplusStartupOutput
from .version_helpers import WindowsVersionHelpers
from comtypes import COMError, GUID, HRESULT, IUnknown, STDMETHOD
from ctypes import byref, create_unicode_buffer, POINTER, windll, wintypes
from ctypes import addressof, byref, create_unicode_buffer, POINTER, py_object, windll, wintypes
from os.path import isfile
from sys import getwindowsversion
from typing import List, Set
Expand All @@ -37,132 +37,92 @@
"WindowsFonts"
]

class WindowsFonts(SystemFonts):
VALID_FONT_FORMATS = [
DWRITE_FONT_FILE_TYPE.DWRITE_FONT_FILE_TYPE_CFF,
DWRITE_FONT_FILE_TYPE.DWRITE_FONT_FILE_TYPE_TRUETYPE,
DWRITE_FONT_FILE_TYPE.DWRITE_FONT_FILE_TYPE_OPENTYPE_COLLECTION,
DWRITE_FONT_FILE_TYPE.DWRITE_FONT_FILE_TYPE_TRUETYPE_COLLECTION,
]

def get_system_fonts_filename() -> Set[str]:
windows_version = getwindowsversion()
if not WindowsVersionHelpers.is_windows_vista_sp2_or_greater(windows_version):
raise OSNotSupported("FindSystemFontsFilename only works on Windows Vista SP2 or more")

gdi = GDI()
gdi_plus = GDIPlus()
dwrite = DirectWrite()
fonts_filename = set()

token = wintypes.ULONG()
startup_in = GdiplusStartupInput(1, None, False, False)
startup_out = GdiplusStartupOutput()
gdi_plus.GdiplusStartup(byref(token), byref(startup_in), byref(startup_out))

gdi_font_collection = wintypes.LPVOID()
gdi_plus.GdipNewInstalledFontCollection(byref(gdi_font_collection))

gdi_family_count = wintypes.INT()
gdi_plus.GdipGetFontCollectionFamilyCount(gdi_font_collection, byref(gdi_family_count))

gdi_families = (wintypes.LPVOID * gdi_family_count.value)()
num_found = wintypes.INT()
gdi_plus.GdipGetFontCollectionFamilyList(gdi_font_collection, gdi_family_count, gdi_families, byref(num_found))

dc = gdi.CreateCompatibleDC(None)
graphics = wintypes.LPVOID()
gdi_plus.GdipCreateFromHDC(dc, byref(graphics))

dwrite_factory = POINTER(IDWriteFactory)()
dwrite.DWriteCreateFactory(DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_ISOLATED, dwrite_factory._iid_, byref(dwrite_factory))

gdi_interop = POINTER(IDWriteGdiInterop)()
dwrite_factory.GetGdiInterop(byref(gdi_interop))
def enum_font_families_w(logfont: ENUMLOGFONTEXW, text_metric: TEXTMETRIC, font_type: wintypes.DWORD, lparam: wintypes.LPARAM):
enum_data: EnumData = py_object.from_address(lparam).value

font_styles_possibility = [
FontStyle.FontStyleRegular,
FontStyle.FontStyleBold,
FontStyle.FontStyleItalic,
FontStyle.FontStyleBoldItalic,
]
hfont = enum_data.gdi.CreateFontIndirectW(byref(logfont.elfLogFont))
enum_data.gdi.SelectObject(enum_data.dc, hfont)

for family in gdi_families:
for font_style in font_styles_possibility:
is_style_available = wintypes.BOOL()
gdi_plus.GdipIsStyleAvailable(family, font_style, byref(is_style_available))
error = False
try:
font_face = POINTER(IDWriteFontFace)()
enum_data.gdi_interop.CreateFontFaceFromHdc(enum_data.dc, byref(font_face))
except COMError:
error = True

if not error:
font_files = POINTER(IDWriteFontFile)()
font_face.GetFiles(byref(wintypes.UINT(1)), byref(font_files))

if not is_style_available:
continue
font_file_reference_key = wintypes.LPCVOID()
font_file_reference_key_size = wintypes.UINT()
font_files.GetReferenceKey(byref(font_file_reference_key), byref(font_file_reference_key_size))

gdi_font = wintypes.LPVOID()
# The emSize AND Unit doesn't mather
gdi_plus.GdipCreateFont(family, 16, font_style, gdi_plus.UnitPoint, byref(gdi_font))
loader = POINTER(IDWriteFontFileLoader)()
font_files.GetLoader(byref(loader))

logfont = LOGFONTW()
gdi_plus.GdipGetLogFontW(gdi_font, graphics, byref(logfont))
local_loader = loader.QueryInterface(IDWriteLocalFontFileLoader)

dwrite_font = POINTER(IDWriteFont)()
gdi_interop.CreateFontFromLOGFONT(logfont, byref(dwrite_font))
path_len = wintypes.UINT()
local_loader.GetFilePathLengthFromKey(font_file_reference_key, font_file_reference_key_size, byref(path_len))

gdi_plus.GdipDeleteFont(gdi_font)
buffer = create_unicode_buffer(path_len.value + 1)
local_loader.GetFilePathFromKey(font_file_reference_key, font_file_reference_key_size, buffer, len(buffer))

enum_data.fonts_filename.add(buffer.value)
enum_data.gdi.DeleteObject(hfont)

# big simulations = font_face.GetSimulations()
return True

# We need to convert the IDWriteFont back to IDWriteFontFamily
# because GDI can miss discard a font if 2 font file are identical.
dwrite_family = POINTER(IDWriteFontFamily)()
dwrite_font.GetFontFamily(byref(dwrite_family))

for j in range(dwrite_family.GetFontCount()):
try:
font = POINTER(IDWriteFont)()
dwrite_family.GetFont(j, byref(font))
except COMError:
# If the file doesn't exist, DirectWrite raise an exception
continue
def enum_fonts_w(logfont: ENUMLOGFONTEXW, text_metric: TEXTMETRIC, font_type: wintypes.DWORD, lparam: wintypes.LPARAM):
enum_data: EnumData = py_object.from_address(lparam).value

if font.GetSimulations() != DWRITE_FONT_SIMULATIONS.DWRITE_FONT_SIMULATIONS_NONE:
continue
if not (font_type & enum_data.gdi.RASTER_FONTTYPE):
enum_data.gdi.EnumFontFamiliesW(enum_data.dc, logfont.elfLogFont.lfFaceName, enum_data.gdi.ENUMFONTFAMEXPROC(enum_font_families_w), lparam)

font_face = POINTER(IDWriteFontFace)()
font.CreateFontFace(byref(font_face))
return True

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)
class EnumData:
def __init__(self, gdi, gdi_interop, fonts_filename, dc, already):
self.gdi = gdi
self.gdi_interop = gdi_interop
self.fonts_filename = fonts_filename
self.dc = dc
self.already = already

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))
class WindowsFonts(SystemFonts):
VALID_FONT_FORMATS = [
DWRITE_FONT_FILE_TYPE.DWRITE_FONT_FILE_TYPE_CFF,
DWRITE_FONT_FILE_TYPE.DWRITE_FONT_FILE_TYPE_TRUETYPE,
DWRITE_FONT_FILE_TYPE.DWRITE_FONT_FILE_TYPE_OPENTYPE_COLLECTION,
DWRITE_FONT_FILE_TYPE.DWRITE_FONT_FILE_TYPE_TRUETYPE_COLLECTION,
]

local_loader = loader.QueryInterface(IDWriteLocalFontFileLoader)
def get_system_fonts_filename() -> Set[str]:
windows_version = getwindowsversion()
if not WindowsVersionHelpers.is_windows_vista_sp2_or_greater(windows_version):
raise OSNotSupported("FindSystemFontsFilename only works on Windows Vista SP2 or more")

is_supported_font_type = wintypes.BOOLEAN()
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))
gdi = GDI()
dwrite = DirectWrite()
fonts_filename = set()

if DWRITE_FONT_FILE_TYPE(font_file_type.value) not in WindowsFonts.VALID_FONT_FORMATS:
continue
dwrite_factory = POINTER(IDWriteFactory)()
dwrite.DWriteCreateFactory(DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_ISOLATED, dwrite_factory._iid_, byref(dwrite_factory))

path_len = wintypes.UINT()
local_loader.GetFilePathLengthFromKey(font_file_reference_key, font_file_reference_key_size, byref(path_len))
dc = gdi.CreateCompatibleDC(None)

buffer = create_unicode_buffer(path_len.value + 1)
local_loader.GetFilePathFromKey(font_file_reference_key, font_file_reference_key_size, buffer, len(buffer))
gdi_interop = POINTER(IDWriteGdiInterop)()
dwrite_factory.GetGdiInterop(byref(gdi_interop))

fonts_filename.add(buffer.value)
enum_data = EnumData(gdi, gdi_interop, fonts_filename, dc, False)
object_enum_data = py_object(enum_data)

gdi_plus.GdipDeleteGraphics(graphics)
gdi.EnumFontsW(dc, None, gdi.ENUMFONTFAMEXPROC(enum_fonts_w), addressof(object_enum_data))
gdi.DeleteDC(dc)
gdi_plus.GdiplusShutdown(token)

return fonts_filename

0 comments on commit 75d337b

Please sign in to comment.