Spaces:
Runtime error
Runtime error
""" | |
Query system Windows fonts with pure Python. | |
Public domain work by anatoly techtonik <[email protected]> | |
Use MIT License if public domain doesn't make sense for you. | |
The task: Get monospace font for an application in the order of | |
preference. | |
A problem: Font ID in Windows is its name. Windows doesn't provide | |
any information about filenames they contained in. From two different | |
files with the same font name you can get only one. | |
Windows also doesn't have a clear concept of _generic font family_ | |
familiar from CSS specification. Here is how fontquery maps Windows | |
LOGFONT properties to generic CSS font families: | |
serif - (LOGFONT.lfPitchAndFamily >> 4) == FF_ROMAN | |
sans-serif - (LOGFONT.lfPitchAndFamily >> 4) == FF_SWISS | |
cursive - (LOGFONT.lfPitchAndFamily >> 4) == FF_SCRIPT | |
fantasy - (LOGFONT.lfPitchAndFamily >> 4) == FF_DECORATIVE | |
monospace - (lf.lfPitchAndFamily & 0b11) == FIXED_PITCH | |
NOTE: ATM, May 2015, the Microsoft documentation related to monospace | |
is misleading due to poor wording: | |
- FF_MODERN in the description of LOGFONT structure tells | |
"Fonts with constant stroke width (monospace), with or without serifs. | |
Monospace fonts are usually modern. | |
Pica, Elite, and CourierNew are examples. | |
" | |
Stroke width is the 'pen width', not glyph width. It should read | |
"Fonts with constant stroke width, with or without serifs. | |
Monospace fonts are usually modern, but not all modern are monospace | |
" | |
PYGLET NOTE: | |
Examination of all fonts in a windows xp machine shows that all fonts | |
with | |
fontentry.vector and fontentry.family != FF_DONTCARE | |
are rendered fine. | |
Use cases: | |
[x] get the list of all available system font names | |
[ ] get the list of all fonts for generic family | |
[ ] get the list of all fonts for specific charset | |
[ ] check if specific font is available | |
Considerations: | |
- performance of querying all system fonts is not measured | |
- Windows doesn't allow to get filenames of the fonts, so if there | |
are two fonts with the same name, one will be missing | |
MSDN: | |
If you request a font named Palatino, but no such font is available | |
on the system, the font mapper will substitute a font that has similar | |
attributes but a different name. | |
[ ] check if font chosen by the system has required family | |
To get the appropriate font, call EnumFontFamiliesEx with the | |
desired font characteristics in the LOGFONT structure, then retrieve the | |
appropriate typeface name and create the font using CreateFont or | |
CreateFontIndirect. | |
""" | |
DEBUG = False | |
__all__ = ['have_font', 'font_list'] | |
__version__ = '0.3' | |
__url__ = 'https://bitbucket.org/techtonik/fontquery' | |
# -- INTRO: MAINTAIN CACHED FONTS DB -- | |
# [ ] make it Django/NDB style model definition | |
class FontEntry: | |
""" | |
Font classification. | |
Level 0: | |
- name | |
- vector (True if font is vector, False for raster fonts) | |
- format: ttf | ... | |
""" | |
def __init__(self, name, vector, format, monospace, family): | |
self.name = name | |
self.vector = vector | |
self.format = format | |
self.monospace = monospace | |
self.family = family | |
# List of FontEntry objects | |
FONTDB = [] | |
# -- CHAPTER 1: GET ALL SYSTEM FONTS USING EnumFontFamiliesEx FROM GDI -- | |
""" | |
Q: Why GDI? Why not GDI+? | |
A: Wikipedia: | |
Because of the additional text processing and resolution independence | |
capabilities in GDI+, text rendering is performed by the CPU [2] and it | |
is nearly an order of magnitude slower than in hardware accelerated GDI.[3] | |
Chris Jackson published some tests indicating that a piece of text | |
rendering code he had written could render 99,000 glyphs per second in GDI, | |
but the same code using GDI+ rendered 16,600 glyphs per second. | |
""" | |
import ctypes | |
from ctypes import wintypes | |
from pyglet.libs.win32 import LOGFONT, LOGFONTW | |
user32 = ctypes.windll.user32 | |
gdi32 = ctypes.windll.gdi32 | |
# --- define necessary data structures from wingdi.h | |
# for calling ANSI functions of Windows API (end with A) TCHAR is | |
# defined as single char, for Unicode ones (end witn W) it is WCHAR | |
CHAR = ctypes.c_char # Python 2.7 compatibility | |
TCHAR = CHAR | |
BYTE = ctypes.c_ubyte # http://bugs.python.org/issue16376 | |
# charset codes for LOGFONT structure | |
ANSI_CHARSET = 0 | |
ARABIC_CHARSET = 178 | |
BALTIC_CHARSET = 186 | |
CHINESEBIG5_CHARSET = 136 | |
DEFAULT_CHARSET = 1 | |
# - charset for current system locale - | |
# means function can be called several times | |
# for the single font (for each charset) | |
EASTEUROPE_CHARSET = 238 | |
GB2312_CHARSET = 134 | |
GREEK_CHARSET = 161 | |
HANGUL_CHARSET = 129 | |
HEBREW_CHARSET = 177 | |
JOHAB_CHARSET = 130 | |
MAC_CHARSET = 77 | |
OEM_CHARSET = 255 # OS dependent system charset | |
RUSSIAN_CHARSET = 204 | |
SHIFTJIS_CHARSET = 128 | |
SYMBOL_CHARSET = 2 | |
THAI_CHARSET = 222 | |
TURKISH_CHARSET = 162 | |
VIETNAMESE_CHARSET = 163 | |
# build lookup dictionary to get charset name from its code | |
CHARSET_NAMES = {} | |
for (name, value) in locals().copy().items(): | |
if name.endswith('_CHARSET'): | |
CHARSET_NAMES[value] = name | |
# font pitch constants ('fixed pitch' means 'monospace') | |
DEFAULT_PITCH = 0 | |
FIXED_PITCH = 1 | |
VARIABLE_PITCH = 2 | |
# Windows font family constants | |
FF_DONTCARE = 0 # Don't care or don't know | |
FF_ROMAN = 1 # with serifs, proportional | |
FF_SWISS = 2 # w/out serifs, proportional | |
FF_MODERN = 3 # constant stroke width | |
FF_SCRIPT = 4 # handwritten | |
FF_DECORATIVE = 5 # novelty | |
class FONTSIGNATURE(ctypes.Structure): | |
# supported code pages and Unicode subranges for the font | |
# needed for NEWTEXTMETRICEX structure | |
_fields_ = [ | |
('sUsb', wintypes.DWORD * 4), # 128-bit Unicode subset bitfield (USB) | |
('sCsb', wintypes.DWORD * 2)] # 64-bit, code-page bitfield (CPB) | |
class NEWTEXTMETRIC(ctypes.Structure): | |
# physical font attributes for True Type fonts | |
# needed for NEWTEXTMETRICEX structure | |
_fields_ = [ | |
('tmHeight', wintypes.LONG), | |
('tmAscent', wintypes.LONG), | |
('tmDescent', wintypes.LONG), | |
('tmInternalLeading', wintypes.LONG), | |
('tmExternalLeading', wintypes.LONG), | |
('tmAveCharWidth', wintypes.LONG), | |
('tmMaxCharWidth', wintypes.LONG), | |
('tmWeight', wintypes.LONG), | |
('tmOverhang', wintypes.LONG), | |
('tmDigitizedAspectX', wintypes.LONG), | |
('tmDigitizedAspectY', wintypes.LONG), | |
('mFirstChar', TCHAR), | |
('mLastChar', TCHAR), | |
('mDefaultChar', TCHAR), | |
('mBreakChar', TCHAR), | |
('tmItalic', BYTE), | |
('tmUnderlined', BYTE), | |
('tmStruckOut', BYTE), | |
('tmPitchAndFamily', BYTE), | |
('tmCharSet', BYTE), | |
('tmFlags', wintypes.DWORD), | |
('ntmSizeEM', wintypes.UINT), | |
('ntmCellHeight', wintypes.UINT), | |
('ntmAvgWidth', wintypes.UINT)] | |
class NEWTEXTMETRICW(ctypes.Structure): | |
_fields_ = [ | |
('tmHeight', wintypes.LONG), | |
('tmAscent', wintypes.LONG), | |
('tmDescent', wintypes.LONG), | |
('tmInternalLeading', wintypes.LONG), | |
('tmExternalLeading', wintypes.LONG), | |
('tmAveCharWidth', wintypes.LONG), | |
('tmMaxCharWidth', wintypes.LONG), | |
('tmWeight', wintypes.LONG), | |
('tmOverhang', wintypes.LONG), | |
('tmDigitizedAspectX', wintypes.LONG), | |
('tmDigitizedAspectY', wintypes.LONG), | |
('mFirstChar', wintypes.WCHAR), | |
('mLastChar', wintypes.WCHAR), | |
('mDefaultChar', wintypes.WCHAR), | |
('mBreakChar', wintypes.WCHAR), | |
('tmItalic', BYTE), | |
('tmUnderlined', BYTE), | |
('tmStruckOut', BYTE), | |
('tmPitchAndFamily', BYTE), | |
('tmCharSet', BYTE), | |
('tmFlags', wintypes.DWORD), | |
('ntmSizeEM', wintypes.UINT), | |
('ntmCellHeight', wintypes.UINT), | |
('ntmAvgWidth', wintypes.UINT)] | |
class NEWTEXTMETRICEX(ctypes.Structure): | |
# physical font attributes for True Type fonts | |
# needed for FONTENUMPROC callback function | |
_fields_ = [ | |
('ntmTm', NEWTEXTMETRIC), | |
('ntmFontSig', FONTSIGNATURE)] | |
class NEWTEXTMETRICEXW(ctypes.Structure): | |
_fields_ = [ | |
('ntmTm', NEWTEXTMETRICW), | |
('ntmFontSig', FONTSIGNATURE)] | |
# type for a function that is called by the system for | |
# each font during execution of EnumFontFamiliesEx | |
FONTENUMPROC = ctypes.WINFUNCTYPE( | |
ctypes.c_int, # return non-0 to continue enumeration, 0 to stop | |
ctypes.POINTER(LOGFONT), | |
ctypes.POINTER(NEWTEXTMETRICEX), | |
wintypes.DWORD, # font type, a combination of | |
# DEVICE_FONTTYPE | |
# RASTER_FONTTYPE | |
# TRUETYPE_FONTTYPE | |
wintypes.LPARAM | |
) | |
FONTENUMPROCW = ctypes.WINFUNCTYPE( | |
ctypes.c_int, # return non-0 to continue enumeration, 0 to stop | |
ctypes.POINTER(LOGFONTW), | |
ctypes.POINTER(NEWTEXTMETRICEXW), | |
wintypes.DWORD, | |
wintypes.LPARAM | |
) | |
# When running 64 bit windows, some types are not 32 bit, so Python/ctypes guesses wrong | |
gdi32.EnumFontFamiliesExA.argtypes = [ | |
wintypes.HDC, | |
ctypes.POINTER(LOGFONT), | |
FONTENUMPROC, | |
wintypes.LPARAM, | |
wintypes.DWORD] | |
gdi32.EnumFontFamiliesExW.argtypes = [ | |
wintypes.HDC, | |
ctypes.POINTER(LOGFONTW), | |
FONTENUMPROCW, | |
wintypes.LPARAM, | |
wintypes.DWORD] | |
def _enum_font_names(logfont, textmetricex, fonttype, param): | |
"""callback function to be executed during EnumFontFamiliesEx | |
call for each font name. it stores names in global variable | |
""" | |
global FONTDB | |
lf = logfont.contents | |
name = lf.lfFaceName | |
# detect font type (vector|raster) and format (ttf) | |
# [ ] use Windows constant TRUETYPE_FONTTYPE | |
if fonttype & 4: | |
vector = True | |
fmt = 'ttf' | |
else: | |
vector = False | |
# [ ] research Windows raster format structure | |
fmt = 'unknown' | |
pitch = lf.lfPitchAndFamily & 0b11 | |
family = lf.lfPitchAndFamily >> 4 | |
# [ ] check FIXED_PITCH, VARIABLE_PITCH and FF_MODERN | |
# combination | |
# | |
# FP T NM 400 CHARSET: 0 DFKai-SB | |
# FP T NM 400 CHARSET: 136 DFKai-SB | |
# FP T NM 400 CHARSET: 0 @DFKai-SB | |
# FP T NM 400 CHARSET: 136 @DFKai-SB | |
# VP T M 400 CHARSET: 0 OCR A Extended | |
monospace = (pitch == FIXED_PITCH) | |
charset = lf.lfCharSet | |
FONTDB.append(FontEntry(name, vector, fmt, monospace, family)) | |
if DEBUG: | |
info = '' | |
if pitch == FIXED_PITCH: | |
info += 'FP ' | |
elif pitch == VARIABLE_PITCH: | |
info += 'VP ' | |
else: | |
info += ' ' | |
# [ ] check exact fonttype values meaning | |
info += '%s ' % {0: 'U', 1: 'R', 4: 'T'}[fonttype] | |
if monospace: | |
info += 'M ' | |
else: | |
info += 'NM ' | |
style = [' '] * 3 | |
if lf.lfItalic: | |
style[0] = 'I' | |
if lf.lfUnderline: | |
style[1] = 'U' | |
if lf.lfStrikeOut: | |
style[2] = 'S' | |
info += ''.join(style) | |
info += ' %s' % lf.lfWeight | |
# if pitch == FIXED_PITCH: | |
if 1: | |
# print('%s CHARSET: %3s %s' % (info, lf.lfCharSet, lf.lfFaceName)) | |
print(f'{info} CHARSET: {lf.lfCharSet} {lf.lfFaceName}') | |
return 1 # non-0 to continue enumeration | |
enum_font_names = FONTENUMPROCW(_enum_font_names) | |
# --- /define | |
# --- prepare and call EnumFontFamiliesEx | |
def query(charset=DEFAULT_CHARSET): | |
""" | |
Prepare and call EnumFontFamiliesEx. | |
query() | |
- return tuple with sorted list of all available system fonts | |
query(charset=ANSI_CHARSET) | |
- return tuple sorted list of system fonts supporting ANSI charset | |
""" | |
global FONTDB | |
# 1. Get device context of the entire screen | |
hdc = user32.GetDC(None) | |
# 2. Call EnumFontFamiliesExA (ANSI version) | |
# 2a. Call with empty font name to query all available fonts | |
# (or fonts for the specified charset) | |
# | |
# NOTES: | |
# | |
# * there are fonts that don't support ANSI charset | |
# * for DEFAULT_CHARSET font is passed to callback function as | |
# many times as charsets it supports | |
# [ ] font name should be less than 32 symbols with terminating \0 | |
# [ ] check double purpose - enumerate all available font names | |
# - enumerate all available charsets for a single font | |
# - other params? | |
logfont = LOGFONTW(0, 0, 0, 0, 0, 0, 0, 0, charset, 0, 0, 0, 0, '') | |
FONTDB = [] # clear cached FONTDB for enum_font_names callback | |
res = gdi32.EnumFontFamiliesExW( | |
hdc, # handle to device context | |
ctypes.byref(logfont), | |
enum_font_names, # pointer to callback function | |
0, # lParam - application-supplied data | |
0) # dwFlags - reserved = 0 | |
# res here is the last value returned by callback function | |
# 3. Release DC | |
user32.ReleaseDC(None, hdc) | |
return FONTDB | |
# --- Public API --- | |
def have_font(name, refresh=False): | |
""" | |
Return True if font with specified `name` is present. The result | |
of querying system font names is cached. Set `refresh` parameter | |
to True to purge cache and reload font information. | |
""" | |
if not FONTDB or refresh: | |
query() | |
if any(f.name == name for f in FONTDB): | |
return True | |
else: | |
return False | |
def font_list(vector_only=False, monospace_only=False): | |
"""Return list of system installed font names.""" | |
if not FONTDB: | |
query() | |
fonts = FONTDB | |
if vector_only: | |
fonts = [f for f in fonts if f.vector] | |
if monospace_only: | |
fonts = [f for f in fonts if f.monospace] | |
return sorted([f.name for f in fonts]) | |
# TODO: move this into tests/ | |
if __name__ == '__main__': | |
import sys | |
if sys.argv[1:] == ['debug']: | |
DEBUG = True | |
if sys.argv[1:] == ['test'] or DEBUG: | |
print('Running tests..') | |
# test have_font (Windows) | |
test_arial = have_font('Arial') | |
print('Have font "Arial"? %s' % test_arial) | |
print('Have font "missing-one"? %s' % have_font('missing-one')) | |
# test cache is not rebuilt | |
FONTDB = [FontEntry('stub', False, '', False, FF_MODERN)] | |
assert (have_font('Arial') != test_arial) | |
# test cache is rebiult | |
assert (have_font('Arial', refresh=True) == test_arial) | |
if not DEBUG: | |
sys.exit() | |
if sys.argv[1:] == ['vector']: | |
fonts = font_list(vector_only=True) | |
elif sys.argv[1:] == ['mono']: | |
fonts = font_list(monospace_only=True) | |
elif sys.argv[1:] == ['vector', 'mono']: | |
fonts = font_list(vector_only=True, monospace_only=True) | |
else: | |
fonts = font_list() | |
print('\n'.join(fonts)) | |
if DEBUG: | |
print(f"Total: {len(font_list())}") | |
# -- CHAPTER 2: WORK WITH FONT DIMENSIONS -- | |
# | |
# Essential info about font metrics http://support.microsoft.com/kb/32667 | |
# And about logical units at http://www.winprog.org/tutorial/fonts.html | |
# x. Convert desired font size from points into logical units (pixels) | |
# By default logical for the screen units are pixels. This is defined | |
# by default MM_TEXT mapping mode. | |
# Point is ancient unit of measurement for physical size of a font. | |
# 10pt is equal to 3.527mm. To make sure a char on screen has physical | |
# size equal to 3.527mm, we need to know display size to calculate how | |
# many pixels are in 3.527mm, and then fetch font that best matches | |
# this size. | |
# Essential info about conversion http://support.microsoft.com/kb/74299 | |
# x.1 Get pixels per inch using GetDeviceCaps() or ... | |
# -- CHAPTER 3: LAYERED FONT API -- | |
# | |
# y. Font object with several layers of info | |
# Font object should contains normalized font information. This | |
# information is split according to usage. For example, level 0 property | |
# is font id - its name. Level 1 can be information about loaded font | |
# characters - in pyglet it could be cached/used glyphs and video memory | |
# taken by those glyphs. | |
# [ ] (pyglet) investigate if it is possible to get video memory size | |
# occupied by the font glyphs | |
# [ ] (pyglet) investigate if it is possible to unload font from video | |
# memory if its unused | |