-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathwindows_fonts.py
More file actions
357 lines (267 loc) · 15.4 KB
/
Copy pathwindows_fonts.py
File metadata and controls
357 lines (267 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
from .advapi32 import Advapi32, RegistryDataType
from .dwrite import (
DWrite,
DWRITE_FACTORY_TYPE,
DWRITE_FONT_FILE_TYPE,
DWRITE_FONT_SIMULATIONS,
DWRITE_INFORMATIONAL_STRING_ID,
IDWriteFactory,
IDWriteFont,
IDWriteFontCollection,
IDWriteFontCollectionLoader,
IDWriteFontFace,
IDWriteFontFile,
IDWriteFontFileEnumerator,
IDWriteFontFileLoader,
IDWriteGdiInterop,
IDWriteLocalFontFileLoader,
IDWriteLocalizedStrings
)
from .gdi32 import GDI32, ENUMLOGFONTEXW, TEXTMETRICW
from .kernel32 import Kernel32
from .msvcrt import MSVCRT
from .user32 import User32
from .version_helpers import WindowsVersionHelpers
from comtypes import COMObject
from ctypes import addressof, byref, cast, create_unicode_buffer, POINTER, py_object, sizeof, wintypes
from pathlib import Path
from sys import getwindowsversion
from typing import List, Set
from ..exceptions import NotSupportedFontFile, OSNotSupported, SystemApiError
from ..system_fonts import SystemFonts
__all__ = ["WindowsFonts"]
def get_filepath_from_IDWriteFontFace(font_face) -> Set[str]:
fonts_filename: Set[str] = set()
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)) # type: ignore
loader = POINTER(IDWriteFontFileLoader)()
font_file.GetLoader(byref(loader)) # type: ignore
local_loader = loader.QueryInterface(IDWriteLocalFontFileLoader) # type: ignore
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)) # type: ignore
if DWRITE_FONT_FILE_TYPE(font_file_type.value) not in WindowsFonts.VALID_FONT_FORMATS:
continue
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))
fonts_filename.add(buffer.value)
return fonts_filename
def enum_fonts_2(logfont: POINTER(ENUMLOGFONTEXW), text_metric: POINTER(TEXTMETRICW), font_type: wintypes.DWORD, lparam: wintypes.LPARAM) -> wintypes.INT:
enum_data: EnumData = py_object.from_address(lparam).value # type: ignore
# It seems that font_type can be 0. In those case, the font format is .fon
# We also discard RASTER_FONTTYPE which are bitmap font
if not (font_type.value & enum_data.gdi.RASTER_FONTTYPE) and font_type.value:
# Replace the lfFaceName with the elfFullName.
# See why here: https://github.com/libass/libass/issues/744
lfFaceName = create_unicode_buffer(enum_data.gdi.LF_FACESIZE)
enum_data.msvcrt.wcsncpy_s(lfFaceName, enum_data.gdi.LF_FACESIZE, logfont.contents.elfFullName, enum_data.msvcrt.TRUNCATE)
logfont.contents.elfLogFont.lfFaceName = lfFaceName.value
hfont = enum_data.gdi.CreateFontIndirectW(byref(logfont.contents.elfLogFont))
enum_data.gdi.SelectObject(enum_data.dc, hfont)
font_face = POINTER(IDWriteFontFace)()
enum_data.gdi_interop.CreateFontFaceFromHdc(enum_data.dc, byref(font_face))
enum_data.fonts_filename.update(get_filepath_from_IDWriteFontFace(font_face))
enum_data.gdi.DeleteObject(hfont)
return wintypes.INT(1)
def enum_fonts_1(logfont: POINTER(ENUMLOGFONTEXW), text_metric: POINTER(TEXTMETRICW), font_type: wintypes.DWORD, lparam: wintypes.LPARAM) -> wintypes.INT:
enum_data: EnumData = py_object.from_address(lparam).value # type: ignore
enum_data.gdi.EnumFontFamiliesW(enum_data.dc, logfont.contents.elfLogFont.lfFaceName, enum_data.gdi.ENUMFONTFAMEXPROC(enum_fonts_2), lparam)
return wintypes.INT(1)
class EnumData:
def __init__(self, gdi: GDI32, msvcrt: MSVCRT, dc: wintypes.HDC, fonts_filename: Set[str], gdi_interop: POINTER(IDWriteGdiInterop)):
self.gdi = gdi
self.msvcrt = msvcrt
self.dc = dc
self.fonts_filename = fonts_filename
self.gdi_interop = gdi_interop
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,
]
@staticmethod
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")
dwrite = DWrite()
gdi = GDI32()
msvcrt = MSVCRT()
fonts_filename: Set[str] = set()
dwrite_factory = POINTER(IDWriteFactory)()
dwrite.DWriteCreateFactory(DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_ISOLATED, byref(dwrite_factory._iid_), byref(dwrite_factory)) # type: ignore[attr-defined]
gdi_interop = POINTER(IDWriteGdiInterop)()
dwrite_factory.GetGdiInterop(byref(gdi_interop)) # type: ignore
dc = gdi.CreateCompatibleDC(None)
enum_data = EnumData(gdi, msvcrt, dc, fonts_filename, gdi_interop)
object_enum_data = py_object(enum_data)
# See this link to understand why we do call EnumFontFamiliesW and then a EnumFontFamiliesW
# and not directly EnumFontFamiliesExW.
# https://stackoverflow.com/a/62405274/15835974
enum_data.gdi.EnumFontFamiliesW(enum_data.dc, None, enum_data.gdi.ENUMFONTFAMEXPROC(enum_fonts_1), addressof(object_enum_data))
gdi.DeleteDC(dc)
return fonts_filename
@staticmethod
def get_registry_font_name(font_filename: Path) -> str:
dwrite = DWrite()
kernel32 = Kernel32()
font_filename_buffer = create_unicode_buffer(str(font_filename))
dwrite_factory = POINTER(IDWriteFactory)()
dwrite.DWriteCreateFactory(DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_ISOLATED, IDWriteFactory._iid_, byref(dwrite_factory))
font_file = POINTER(IDWriteFontFile)()
dwrite_factory.CreateFontFileReference(font_filename_buffer, None, byref(font_file)) # type: ignore
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)) # type: ignore
if not is_supported_font_type:
raise NotSupportedFontFile(f"The font file \"{font_filename}\" isn't supported on Windows.")
font_collection_loader = CustomFontCollectionLoader([font_filename])
dwrite_factory.RegisterFontCollectionLoader(font_collection_loader) # type: ignore
custom_collection = POINTER(IDWriteFontCollection)()
font_loader_key = create_unicode_buffer("find_system_fonts_filename_collection_loader")
dwrite_factory.CreateCustomFontCollection(font_collection_loader, # type: ignore
cast(font_loader_key, wintypes.LPVOID),
sizeof(font_loader_key),
byref(custom_collection))
full_names: List[str] = []
for i in range(number_of_faces.value):
font_face = POINTER(IDWriteFontFace)()
dwrite_factory.CreateFontFace(font_face_type.value, 1, byref(font_file), i, DWRITE_FONT_SIMULATIONS.DWRITE_FONT_SIMULATIONS_NONE, byref(font_face)) # type: ignore
"""
Converting a IDWriteFontFace to a IDWriteFont isn't easy.
We can't use IDWriteGdiInterop and use ConvertFontFaceToLOGFONT and then CreateFontFromLOGFONT,
because it needs to have the font installed in the system and since we just called AddFontResourceW,
the font may not be available. See issue #14
"""
font = POINTER(IDWriteFont)()
custom_collection.GetFontFromFontFace(font_face, byref(font)) # type: ignore
full_name = POINTER(IDWriteLocalizedStrings)()
exists = wintypes.BOOL()
font.GetInformationalStrings( # type: ignore
DWRITE_INFORMATIONAL_STRING_ID.DWRITE_INFORMATIONAL_STRING_FULL_NAME,
byref(full_name),
byref(exists)
)
if not exists:
raise SystemApiError("Could not fetch the font name")
# Based on https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename#remarks
locale_name = create_unicode_buffer(kernel32.LOCALE_NAME_MAX_LENGTH)
kernel32.GetUserDefaultLocaleName(locale_name, kernel32.LOCALE_NAME_MAX_LENGTH)
index = wintypes.UINT()
exists = wintypes.BOOL()
full_name.FindLocaleName(locale_name, byref(index), byref(exists)) # type: ignore
if not exists.value:
full_name.FindLocaleName("en-us", byref(index), byref(exists)) # type: ignore
if not exists.value:
index = wintypes.UINT(0)
length = wintypes.UINT()
full_name.GetStringLength(index, byref(length)) # type: ignore
family_names_buffer = create_unicode_buffer(length.value + 1)
full_name.GetString(index, family_names_buffer, len(family_names_buffer)) # type: ignore
full_names.append(family_names_buffer.value)
registry_font_name = " & ".join(full_names) + " (FindSystemFontsFilename)"
dwrite_factory.UnregisterFontCollectionLoader(font_collection_loader) # type: ignore
return registry_font_name
@staticmethod
def install_font(font_filename: Path, add_font_to_registry: bool = False) -> None:
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")
# Font in the registry have been added in 10.0.17083.
# Source: https://superuser.com/a/1658749/1729132
is_build_17083_or_greater = WindowsVersionHelpers.is_windows_version_or_greater(windows_version, 10, 0, 17083)
gdi = GDI32()
user32 = User32()
font_filename_buffer = create_unicode_buffer(str(font_filename))
gdi.AddFontResourceW(font_filename_buffer)
if add_font_to_registry and is_build_17083_or_greater:
advapi32 = Advapi32()
hkey = wintypes.HKEY()
registry_font_name = WindowsFonts.get_registry_font_name(font_filename)
advapi32.RegOpenKeyExW(advapi32.HKEY_CURRENT_USER, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", 0, advapi32.KEY_SET_VALUE, byref(hkey))
advapi32.RegSetValueExW(hkey, registry_font_name, 0, RegistryDataType.REG_SZ.value, cast(font_filename_buffer, wintypes.LPBYTE), sizeof(font_filename_buffer))
advapi32.RegCloseKey(hkey)
user32.SendNotifyMessageW(user32.HWND_BROADCAST, user32.WM_FONTCHANGE, 0, 0)
@staticmethod
def uninstall_font(font_filename: Path, remove_font_in_registry: bool) -> None:
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")
# Font in the registry have been added in 10.0.17083.
# Source: https://superuser.com/a/1658749/1729132
is_build_17083_or_greater = WindowsVersionHelpers.is_windows_version_or_greater(windows_version, 10, 0, 17083)
gdi = GDI32()
user32 = User32()
if remove_font_in_registry and is_build_17083_or_greater:
advapi32 = Advapi32()
hkey = wintypes.HKEY()
registry_font_name = WindowsFonts.get_registry_font_name(font_filename)
advapi32.RegOpenKeyExW(advapi32.HKEY_CURRENT_USER, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", 0, advapi32.KEY_SET_VALUE, byref(hkey))
advapi32.RegDeleteValueW(hkey, registry_font_name)
advapi32.RegCloseKey(hkey)
# When a font have been installed multiple time,
# we need to call RemoveFontResourceW until it fails.
# Also, when the font have been added to the registry,
# RemoveFontResourceW fails, but in reality, it actually uninstalled
# the font, so let's always ignore any error it returns.
while True:
try:
gdi.RemoveFontResourceW(str(font_filename))
except SystemApiError:
break
user32.SendNotifyMessageW(user32.HWND_BROADCAST, user32.WM_FONTCHANGE, 0, 0)
class CustomFontFileEnumerator(COMObject):
_com_interfaces_ = [IDWriteFontFileEnumerator]
def __init__(self, dwrite_factory: POINTER(IDWriteFactory), font_files_path: List[Path]):
super(CustomFontFileEnumerator, self).__init__()
self.dwrite_factory = dwrite_factory
self.font_files_path = font_files_path
self.current_index = -1
self.current_font_file: POINTER(IDWriteFontFile) = None
def IDWriteFontFileEnumerator_MoveNext(self, this, has_current_file: POINTER(wintypes.BOOL)) -> int:
self.current_index += 1
if self.current_index < len(self.font_files_path):
font_filename_buffer = create_unicode_buffer(str(self.font_files_path[self.current_index]))
font_file = POINTER(IDWriteFontFile)()
self.dwrite_factory.CreateFontFileReference(font_filename_buffer, None, byref(font_file))
self.current_font_file = font_file
has_current_file.contents.value = True
else:
has_current_file.contents.value = False
return 0 # S_OK
def IDWriteFontFileEnumerator_GetCurrentFontFile(self, this, font_file: POINTER(POINTER(IDWriteFontFile))) -> int:
if self.current_font_file:
font_file[0] = self.current_font_file
return 0 # S_OK
return 1 # S_FALSE
class CustomFontCollectionLoader(COMObject):
_com_interfaces_ = [IDWriteFontCollectionLoader]
def __init__(self, font_files_path: List[Path]):
super(CustomFontCollectionLoader, self).__init__()
self.font_files_path = font_files_path
def IDWriteFontCollectionLoader_CreateEnumeratorFromKey(
self,
this,
factory: POINTER(IDWriteFactory),
collection_key: wintypes.LPVOID,
collection_key_size: wintypes.UINT,
font_file_enumerator: POINTER(POINTER(IDWriteFontFileEnumerator))
)-> int:
enum = CustomFontFileEnumerator(factory, self.font_files_path)
enumerator_ref = enum.QueryInterface(IDWriteFontFileEnumerator)
font_file_enumerator[0] = enumerator_ref
return 0 # S_OK