初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -0,0 +1,21 @@
from .store import (
ACCEPTED_FONT_EXTENSIONS,
MAX_FONT_UPLOAD_BYTES,
FontCatalog,
FontCatalogError,
FontStorageFullError,
FontTooLargeError,
FontTypeError,
FontValidationError,
)
__all__ = [
"ACCEPTED_FONT_EXTENSIONS",
"MAX_FONT_UPLOAD_BYTES",
"FontCatalog",
"FontCatalogError",
"FontStorageFullError",
"FontTooLargeError",
"FontTypeError",
"FontValidationError",
]
+341
View File
@@ -0,0 +1,341 @@
from __future__ import annotations
import errno
import hashlib
import logging
import os
import subprocess
import threading
from dataclasses import dataclass
from io import BytesIO
from pathlib import Path
from typing import AsyncIterable, Iterable
from uuid import uuid4
from fontTools.ttLib import TTCollection, TTFont, TTLibError
from app.display.text_renderer import DEFAULT_FONT_CANDIDATES, FontFace
logger = logging.getLogger(__name__)
ACCEPTED_FONT_EXTENSIONS = (".ttf", ".otf", ".ttc", ".otc")
MAX_FONT_UPLOAD_BYTES = 32 * 1024 * 1024
_FONTCONFIG_TIMEOUT_SECONDS = 8
_USER_FONT_NAME = "{digest}.font"
class FontCatalogError(RuntimeError):
"""Base error for font catalog operations."""
class FontTooLargeError(FontCatalogError):
pass
class FontTypeError(FontCatalogError):
pass
class FontValidationError(FontCatalogError):
pass
class FontStorageFullError(FontCatalogError):
pass
@dataclass(frozen=True)
class _DiscoveredFace:
face: FontFace
family: str
style: str
def _friendly_name(font: TTFont, fallback: str) -> tuple[str, str]:
names = font.get("name")
if names is None:
return fallback, "Regular"
family = names.getBestFamilyName() or names.getDebugName(1) or fallback
style = names.getBestSubFamilyName() or names.getDebugName(2) or "Regular"
return str(family).strip() or fallback, str(style).strip() or "Regular"
def _has_unicode_cmap(font: TTFont) -> bool:
cmap = font.get("cmap")
return bool(cmap and any(table.isUnicode() and table.cmap for table in cmap.tables))
def _inspect_font_file(path: Path, *, require_unicode: bool) -> tuple[_DiscoveredFace, ...]:
path = Path(path)
fallback = path.stem or "Unnamed font"
collection: TTCollection | None = None
fonts: list[TTFont] = []
try:
content = path.read_bytes()
try:
collection = TTCollection(BytesIO(content), lazy=False)
fonts = list(collection.fonts)
except TTLibError:
fonts = [TTFont(BytesIO(content), lazy=False)]
discovered: list[_DiscoveredFace] = []
for index, font in enumerate(fonts):
if require_unicode and not _has_unicode_cmap(font):
continue
family, style = _friendly_name(font, fallback)
discovered.append(
_DiscoveredFace(FontFace(str(path.resolve()), index), family, style)
)
if not discovered:
raise FontValidationError("font file does not contain a usable Unicode font face")
return tuple(discovered)
except FontValidationError:
raise
except (OSError, TTLibError, KeyError, ValueError) as exc:
raise FontValidationError("font file is invalid or unsupported") from exc
finally:
if collection is not None:
collection.close()
else:
for font in fonts:
font.close()
def _system_font_id(face: FontFace) -> str:
raw = f"{Path(face.path).resolve()}\0{face.index}".encode("utf-8")
return f"system:{hashlib.sha256(raw).hexdigest()}"
def _user_font_id(digest: str, index: int) -> str:
return f"user:{digest}:{index}"
def _entry(identifier: str, family: str, style: str, source: str) -> dict[str, str]:
family = family.strip() or "未命名字体"
style = style.strip() or "Regular"
label = family if style.casefold() in {"regular", "normal", "book"} else f"{family} — {style}"
return {
"id": identifier,
"label": label,
"family": family,
"style": style,
"source": source,
}
def _fontconfig_faces() -> tuple[_DiscoveredFace, ...]:
try:
result = subprocess.run(
["fc-list", "--format=%{file}\t%{index}\t%{family[0]}\t%{style[0]}\n"],
check=True,
capture_output=True,
text=True,
encoding="utf-8",
timeout=_FONTCONFIG_TIMEOUT_SECONDS,
)
except (FileNotFoundError, OSError, subprocess.SubprocessError):
return ()
discovered: list[_DiscoveredFace] = []
for line in result.stdout.splitlines():
fields = line.split("\t", 3)
if len(fields) != 4:
continue
raw_path, raw_index, family, style = fields
path = Path(raw_path.strip())
if path.suffix.casefold() not in ACCEPTED_FONT_EXTENSIONS or not path.is_file():
continue
try:
index = int(raw_index.strip() or "0")
except ValueError:
continue
discovered.append(
_DiscoveredFace(
FontFace(str(path.resolve()), index),
family.strip() or path.stem,
style.strip() or "Regular",
)
)
return tuple(discovered)
def _configured_paths() -> Iterable[Path]:
raw = os.environ.get("MATRIX_FONT_PATHS", "")
for value in raw.split(os.pathsep):
if value.strip():
yield Path(value.strip()).expanduser()
def _sync_directory(path: Path) -> None:
if os.name == "nt":
return
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
descriptor = os.open(path, flags)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
class FontCatalog:
def __init__(self, data_dir: Path) -> None:
self.root = Path(data_dir) / "fonts"
self._lock = threading.RLock()
self._items: tuple[dict[str, str], ...] = ()
self._faces: dict[str, tuple[FontFace, ...]] = {}
self._warnings: tuple[str, ...] = ()
self.root.mkdir(parents=True, exist_ok=True)
if os.name != "nt":
self.root.chmod(0o750)
self._cleanup_upload_temporaries()
self._system_catalog = self._system_faces()
self.refresh()
def _cleanup_upload_temporaries(self) -> None:
for path in self.root.glob(".upload-*.tmp"):
if path.is_file() and path.parent == self.root:
path.unlink(missing_ok=True)
def _system_faces(self) -> tuple[_DiscoveredFace, ...]:
discovered = list(_fontconfig_faces())
known = {(str(Path(item.face.path).resolve()), item.face.index) for item in discovered}
known_paths = {path for path, _index in known}
for candidate in (*_configured_paths(), *(Path(value) for value in DEFAULT_FONT_CANDIDATES)):
if candidate.suffix.casefold() not in ACCEPTED_FONT_EXTENSIONS or not candidate.is_file():
continue
resolved_candidate = str(candidate.resolve())
if resolved_candidate in known_paths:
continue
try:
inspected = _inspect_font_file(candidate, require_unicode=True)
except FontValidationError:
continue
for item in inspected:
key = (str(Path(item.face.path).resolve()), item.face.index)
if key not in known:
known.add(key)
known_paths.add(key[0])
discovered.append(item)
return tuple(discovered)
def refresh(self) -> None:
faces: dict[str, tuple[FontFace, ...]] = {}
system_items: list[dict[str, str]] = []
imported_items: list[dict[str, str]] = []
warnings: list[str] = []
seen_system: set[tuple[str, int]] = set()
for item in self._system_catalog:
key = (str(Path(item.face.path).resolve()), item.face.index)
if key in seen_system:
continue
seen_system.add(key)
identifier = _system_font_id(item.face)
faces[identifier] = (item.face,)
system_items.append(_entry(identifier, item.family, item.style, "system"))
for path in sorted(self.root.glob("*.font"), key=lambda item: item.name):
digest = path.stem.casefold()
if len(digest) != 64 or any(character not in "0123456789abcdef" for character in digest):
warnings.append(f"已忽略未登记字体文件:{path.name}")
continue
try:
inspected = _inspect_font_file(path, require_unicode=True)
except FontValidationError:
warnings.append(f"已保留但忽略损坏字体:{path.name}")
logger.warning("Ignoring invalid persisted font %s", path)
continue
for item in inspected:
identifier = _user_font_id(digest, item.face.index)
faces[identifier] = (item.face,)
imported_items.append(_entry(identifier, item.family, item.style, "imported"))
sort_key = lambda value: (value["family"].casefold(), value["style"].casefold(), value["id"])
automatic = {
"id": "default",
"label": "自动多语言字体",
"family": "自动多语言字体",
"style": "自动回退",
"source": "automatic",
}
with self._lock:
self._items = (
automatic,
*sorted(imported_items, key=sort_key),
*sorted(system_items, key=sort_key),
)
self._faces = faces
self._warnings = tuple(warnings)
def document(self) -> dict:
with self._lock:
return {
"items": [dict(item) for item in self._items],
"accepted_extensions": list(ACCEPTED_FONT_EXTENSIONS),
"max_upload_bytes": MAX_FONT_UPLOAD_BYTES,
"warnings": list(self._warnings),
}
def resolve(self, identifier: str) -> tuple[FontFace, ...]:
with self._lock:
return self._faces.get(identifier, ())
async def import_font(
self,
filename: str,
chunks: AsyncIterable[bytes],
) -> tuple[bool, list[dict[str, str]]]:
if (
not filename
or len(filename) > 255
or "\0" in filename
or "/" in filename
or "\\" in filename
):
raise FontTypeError("font filename is missing or invalid")
if Path(filename).suffix.casefold() not in ACCEPTED_FONT_EXTENSIONS:
raise FontTypeError("font type must be TTF, OTF, TTC, or OTC")
temporary = self.root / f".upload-{uuid4().hex}.tmp"
digest = hashlib.sha256()
total = 0
try:
with temporary.open("xb") as handle:
if os.name != "nt":
os.chmod(temporary, 0o640)
async for chunk in chunks:
if not chunk:
continue
total += len(chunk)
if total > MAX_FONT_UPLOAD_BYTES:
raise FontTooLargeError("font file exceeds the 32 MiB limit")
digest.update(chunk)
handle.write(chunk)
handle.flush()
os.fsync(handle.fileno())
if total == 0:
raise FontValidationError("font file is empty")
inspected = _inspect_font_file(temporary, require_unicode=True)
file_digest = digest.hexdigest()
target = self.root / _USER_FONT_NAME.format(digest=file_digest)
with self._lock:
created = not target.exists()
if created:
os.replace(temporary, target)
_sync_directory(self.root)
else:
temporary.unlink(missing_ok=True)
self.refresh()
identifiers = {_user_font_id(file_digest, item.face.index) for item in inspected}
imported = [dict(item) for item in self._items if item["id"] in identifiers]
return created, imported
except FontCatalogError:
temporary.unlink(missing_ok=True)
raise
except OSError as exc:
temporary.unlink(missing_ok=True)
if exc.errno in {errno.ENOSPC, errno.EDQUOT}:
raise FontStorageFullError("not enough disk space to import font") from exc
raise FontCatalogError(str(exc)) from exc
except BaseException:
temporary.unlink(missing_ok=True)
raise