Add bundled bus fonts and shared font favorites

This commit is contained in:
2026-10-12 15:00:40 +08:00
parent c9dd03c23b
commit 4be74a34f9
181 changed files with 2078575 additions and 76 deletions
+124 -1
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import errno
import hashlib
import json
import logging
import os
import subprocess
@@ -15,6 +16,7 @@ from uuid import uuid4
from fontTools.ttLib import TTCollection, TTFont, TTLibError
from app.display.text_renderer import DEFAULT_FONT_CANDIDATES, FontFace
from app.persistence import atomic_write_bytes
logger = logging.getLogger(__name__)
@@ -186,10 +188,102 @@ class FontCatalog:
self.root.mkdir(parents=True, exist_ok=True)
if os.name != "nt":
self.root.chmod(0o750)
self._favorite_path = self.root / "favorites.json"
self._favorite_ids = self._load_favorites()
self._cleanup_upload_temporaries()
self._builtin = self._install_builtins()
self._system_catalog = self._system_faces()
self.refresh()
def _load_favorites(self) -> list[str]:
if not self._favorite_path.exists():
atomic_write_bytes(self._favorite_path, b'{"schema_version":1,"favorite_ids":[]}\n')
return []
try:
record = json.loads(self._favorite_path.read_text(encoding="utf-8"))
if (not isinstance(record, dict) or set(record) != {"schema_version", "favorite_ids"}
or type(record["schema_version"]) is not int or record["schema_version"] != 1):
raise ValueError("invalid favorites schema")
identifiers = record["favorite_ids"]
if (not isinstance(identifiers, list)
or any(not isinstance(value, str) or not value or len(value) > 512 for value in identifiers)
or len(set(identifiers)) != len(identifiers)):
raise ValueError("invalid favorites identifiers")
return identifiers
except (ValueError, UnicodeError) as exc:
raise FontValidationError("invalid font favorites; original file preserved") from exc
def set_favorite(self, identifier: str, favorite: bool) -> list[str]:
with self._lock:
if favorite and not any(item["id"] == identifier for item in self._items):
raise KeyError(identifier)
updated = [value for value in self._favorite_ids if value != identifier]
if favorite:
updated.append(identifier)
if set(updated) == set(self._favorite_ids):
return list(self._favorite_ids)
content = json.dumps({"schema_version": 1, "favorite_ids": updated}, ensure_ascii=False).encode("utf-8") + b"\n"
atomic_write_bytes(self._favorite_path, content)
self._favorite_ids = updated
return list(updated)
def _install_builtins(self) -> dict[str, tuple[dict, FontFace]]:
bundle = Path(__file__).resolve().parent / "bundled"
manifest = json.loads((bundle / "manifest.json").read_text(encoding="utf-8"))
expected = {"builtin:walnut-bus-8x16": "SimilarBus8x16.ttf", "builtin:walnut-bus-16x32": "SimilarBus16x32.ttf"}
entries = manifest.get("fonts", [])
if manifest.get("version") != 2 or len(entries) != 2 or {item["id"]: item["file"] for item in entries} != expected:
raise FontValidationError("invalid bundled font registration")
self._manifest = manifest
# Validate the entire bundle before modifying persisted resources.
for item in entries:
path = bundle / item["file"]
if hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"]:
raise FontValidationError("bundled font checksum mismatch")
_inspect_font_file(path, require_unicode=True)
installed = {item["id"]: (item, self._install_face(bundle, item)) for item in entries}
# Only retire the exact previous built-in asset; never glob user fonts.
for digest in manifest.get("obsolete", []):
if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest):
raise FontValidationError("invalid obsolete font registration")
old = self.root / _USER_FONT_NAME.format(digest=digest)
if old.exists():
if old.is_symlink() or hashlib.sha256(old.read_bytes()).hexdigest() != digest:
raise FontValidationError("obsolete font checksum mismatch; file preserved")
old.unlink()
_sync_directory(self.root)
return installed
def _install_face(self, bundle: Path, manifest: dict) -> FontFace:
payload = (bundle / manifest["file"]).read_bytes()
digest = hashlib.sha256(payload).hexdigest()
if digest != manifest["sha256"]:
raise FontValidationError("bundled font checksum mismatch")
target = self.root / _USER_FONT_NAME.format(digest=digest)
if target.exists():
if hashlib.sha256(target.read_bytes()).hexdigest() != digest:
raise FontValidationError("persisted bundled font checksum mismatch; file preserved")
else:
temporary = self.root / f".upload-{uuid4().hex}.tmp"
try:
with temporary.open("xb") as stream:
if os.name != "nt":
os.chmod(temporary, 0o640)
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
_inspect_font_file(temporary, require_unicode=True)
# Concurrent initialization must never replace an existing file.
try:
os.link(temporary, target)
except FileExistsError:
if hashlib.sha256(target.read_bytes()).hexdigest() != digest:
raise FontValidationError("persisted bundled font checksum mismatch; file preserved")
_sync_directory(self.root)
finally:
temporary.unlink(missing_ok=True)
return FontFace(str(target.resolve()), 0)
def _cleanup_upload_temporaries(self) -> None:
for path in self.root.glob(".upload-*.tmp"):
if path.is_file() and path.parent == self.root:
@@ -222,6 +316,12 @@ class FontCatalog:
system_items: list[dict[str, str]] = []
imported_items: list[dict[str, str]] = []
warnings: list[str] = []
builtin_digests = {item["sha256"] for item, face in self._builtin.values()}
for manifest, builtin_face in self._builtin.values():
faces[manifest["id"]] = (builtin_face,)
builtin_item = _entry(manifest["id"], manifest["family"], manifest["style"], "system")
builtin_item["label"] = manifest["label"]
system_items.append(builtin_item)
seen_system: set[tuple[str, int]] = set()
for item in self._system_catalog:
@@ -247,7 +347,11 @@ class FontCatalog:
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"))
if digest not in builtin_digests and digest not in self._manifest.get("obsolete", []):
imported_items.append(_entry(identifier, item.family, item.style, "imported"))
for alias, identifier in self._manifest.get("aliases", {}).items():
faces[alias] = faces[identifier]
sort_key = lambda value: (value["family"].casefold(), value["style"].casefold(), value["id"])
automatic = {
@@ -270,6 +374,7 @@ class FontCatalog:
with self._lock:
return {
"items": [dict(item) for item in self._items],
"favorite_ids": list(self._favorite_ids),
"accepted_extensions": list(ACCEPTED_FONT_EXTENSIONS),
"max_upload_bytes": MAX_FONT_UPLOAD_BYTES,
"warnings": list(self._warnings),
@@ -279,6 +384,16 @@ class FontCatalog:
with self._lock:
return self._faces.get(identifier, ())
def render_version(self, identifier: str) -> str | None:
identifier = self._manifest.get("aliases", {}).get(identifier, identifier)
item = self._builtin.get(identifier)
if item:
return item[0]["sha256"]
for manifest, face in self._builtin.values():
if identifier == _user_font_id(manifest["sha256"], 0):
return manifest["sha256"]
return None
async def import_font(
self,
filename: str,
@@ -316,6 +431,11 @@ class FontCatalog:
raise FontValidationError("font file is empty")
inspected = _inspect_font_file(temporary, require_unicode=True)
file_digest = digest.hexdigest()
old_alias = _user_font_id(file_digest, 0)
if file_digest in self._manifest.get("obsolete", []):
temporary.unlink(missing_ok=True)
destination = self._manifest["aliases"][old_alias]
return False, [dict(item) for item in self._items if item["id"] == destination]
target = self.root / _USER_FONT_NAME.format(digest=file_digest)
with self._lock:
created = not target.exists()
@@ -326,6 +446,9 @@ class FontCatalog:
temporary.unlink(missing_ok=True)
self.refresh()
identifiers = {_user_font_id(file_digest, item.face.index) for item in inspected}
for manifest, face in self._builtin.values():
if file_digest == manifest["sha256"]:
identifiers.add(manifest["id"])
imported = [dict(item) for item in self._items if item["id"] in identifiers]
return created, imported
except FontCatalogError: