fix(fonts): find per-user Windows fonts by their family name

Read the per-user font registry as well as the machine one, and list each font under the family name in its file. Skip missing files, bitmap fonts and values that are not text.
This commit is contained in:
imSp4rky
2026-11-11 10:46:47 -07:00
parent f85d7f5fa8
commit a28c99b811
2 changed files with 118 additions and 9 deletions
+92
View File
@@ -0,0 +1,92 @@
"""get_windows_fonts reads the fonts installed for all users and for the current user."""
from __future__ import annotations
import sys
import types
from pathlib import Path, PureWindowsPath
import pytest
from unshackle.core import utilities
HKLM, HKCU = "HKLM", "HKCU"
class Key(list):
def __enter__(self) -> Key:
return self
def __exit__(self, *exc: object) -> None:
return None
@pytest.fixture
def registry(monkeypatch, tmp_path):
"""Install a fake winreg whose hives are the returned dict, with the machine font folder in tmp_path."""
hives: dict[str, list[tuple[str, object]]] = {}
def open_key(hive: str, *_: object) -> Key:
if hive not in hives:
raise FileNotFoundError(hive)
return Key(hives[hive])
winreg = types.SimpleNamespace(
HKEY_LOCAL_MACHINE=HKLM,
HKEY_CURRENT_USER=HKCU,
KEY_READ=0,
OpenKey=open_key,
QueryInfoKey=lambda key: (0, len(key), 0),
EnumValue=lambda key, n: (*key[n], 1),
)
monkeypatch.setitem(sys.modules, "winreg", winreg)
monkeypatch.setattr(sys, "platform", "win32")
(tmp_path / "machine").mkdir()
monkeypatch.setattr(utilities, "WINDOWS_FONTS_DIR", tmp_path / "machine")
monkeypatch.setattr(utilities, "extract_font_family", lambda path: None)
return hives
def font(path: Path) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"")
return path
def test_per_user_fonts_are_found(registry, tmp_path):
machine = font(tmp_path / "machine/arial.ttf")
user = font(tmp_path / "user/MyFont-Bold.ttf")
registry[HKLM] = [("Arial (TrueType)", "arial.ttf")]
registry[HKCU] = [("My Font Bold (TrueType)", str(user)), ("Arial (TrueType)", str(user))]
fonts = utilities.get_windows_fonts()
assert fonts == {"Arial": machine, "My Font Bold": user}
assert utilities.find_font_with_fallbacks("My Font", fonts) == user
def test_a_font_is_found_by_the_family_name_in_its_file(registry, tmp_path, monkeypatch):
user = font(tmp_path / "user/My Font - Bold.ttf")
registry[HKCU] = [("MyFont-Bold (TrueType)", str(user))]
monkeypatch.setattr(utilities, "extract_font_family", lambda path: "My Font")
fonts = utilities.get_windows_fonts()
assert fonts == {"MyFont-Bold": user, "My Font": user}
assert utilities.find_font_with_fallbacks("My Font", fonts) == user
def test_an_absolute_registry_value_replaces_the_font_folder():
user = r"C:\Users\x\AppData\Local\Microsoft\Windows\Fonts\MyFont-Bold.ttf"
assert PureWindowsPath(r"C:\Windows\Fonts", user) == PureWindowsPath(user)
def test_missing_files_bitmap_fonts_and_odd_values_are_skipped(registry, tmp_path):
font(tmp_path / "machine/modern.fon")
collection = font(tmp_path / "machine/msgothic.ttc")
registry[HKLM] = [
("Modern (All res)", "modern.fon"),
("MS Gothic & MS UI Gothic (TrueType)", "msgothic.ttc"),
("Gone (TrueType)", "gone.ttf"),
("Binary (TrueType)", b"x.ttf"),
]
registry[HKCU] = [("Deleted (TrueType)", str(tmp_path / "user/Deleted.ttf"))]
assert utilities.get_windows_fonts() == {"MS Gothic & MS UI Gothic": collection}
+26 -9
View File
@@ -838,9 +838,16 @@ def extract_font_family(font_path: Path) -> Optional[str]:
return None
WINDOWS_FONTS_DIR = Path(r"C:\Windows\Fonts")
def get_windows_fonts() -> dict[str, Path]:
"""
Get fonts from Windows registry.
Get the fonts installed for all users and for the current user from the Windows registry.
Only TrueType and OpenType files that exist are kept: a bitmap font cannot be attached, and a per-user entry
can outlive its file. Each font is also listed under the family name in its file, because a registry name
can differ from it (``GandhiSansPL-Bold`` for the family ``Gandhi Sans PL``).
Returns:
Dictionary mapping font family names to their file paths
@@ -849,14 +856,24 @@ def get_windows_fonts() -> dict[str, Path]:
raise OSError("Windows font registry is only available on Windows")
import winreg
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as reg:
key = winreg.OpenKey(reg, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", 0, winreg.KEY_READ)
total_fonts = winreg.QueryInfoKey(key)[1]
return {
name.replace(" (TrueType)", ""): Path(r"C:\Windows\Fonts", filename)
for n in range(0, total_fonts)
for name, filename, _ in [winreg.EnumValue(key, n)]
}
fonts: dict[str, Path] = {}
for hive in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
try:
key = winreg.OpenKey(hive, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", 0, winreg.KEY_READ)
except OSError:
continue
with key:
for n in range(winreg.QueryInfoKey(key)[1]):
name, filename, _ = winreg.EnumValue(key, n)
if not isinstance(filename, str):
continue
path = Path(WINDOWS_FONTS_DIR, filename)
if path.suffix.lower() in {".ttf", ".otf", ".ttc"} and path.is_file():
fonts.setdefault(name.replace(" (TrueType)", ""), path)
for path in dict.fromkeys(fonts.values()):
if family := extract_font_family(path):
fonts.setdefault(family, path)
return fonts
def scan_font_directory(font_dir: Path, fonts: dict[str, Path], log: logging.Logger) -> None: