Add bundled bus fonts and shared font favorites
This commit is contained in:
+124
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user