mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
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:
@@ -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}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user