Files
matrix-screen-controller/核桃派软件源代码/tests/test_bus_font.py
T

136 lines
7.1 KiB
Python

from __future__ import annotations
import hashlib
import base64
from io import BytesIO
import json
from pathlib import Path
import sys
from fastapi.testclient import TestClient
from fontTools.ttLib import TTFont
from PIL import Image
import pytest
from app.display.text_renderer import TextOptions, render_text_layer
from app.fonts.store import FontCatalog, FontValidationError
from app.main import create_app
from app.templates.store import TemplateStore, prepare_scene, render_scene_image
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from build_bus_font import build, gb2312_codepoints
ID = "builtin:walnut-bus-8x16"
BUNDLE = ROOT / "app/fonts/bundled"
def test_reproducible_font_and_coverage(tmp_path):
manifest = build(tmp_path / "build")
assert manifest == json.loads((BUNDLE / "manifest.json").read_text(encoding="utf-8"))
for item in manifest["fonts"]:
assert (tmp_path / "build" / item["file"]).read_bytes() == (BUNDLE / item["file"]).read_bytes()
with TTFont(BUNDLE / item["file"]) as font:
cmap = font.getBestCmap()
assert gb2312_codepoints() <= cmap.keys()
assert len([cp for cp in cmap if 0x4E00 <= cp <= 0x9FFF]) == 6763
assert all(font["hmtx"][name][0] == 512 for name in cmap.values())
assert font["name"].getBestFamilyName() == item["label"]
@pytest.mark.parametrize("width,height", [(8,16), (16,32)])
def test_all_glyphs_binary_pixels_and_cell_bounds(tmp_path, width, height):
catalog = FontCatalog(tmp_path)
source = ROOT / f"font_sources/walnut-bus-{width}x{height}"
masters = json.loads((source / "glyphs.json").read_text(encoding="utf-8"))
revisions = json.loads((source / "revisions.json").read_text(encoding="utf-8"))
for cp in sorted(gb2312_codepoints()):
image = render_text_layer(TextOptions(chr(cp), font=f"builtin:walnut-bus-{width}x{height}", size=height),
size=(width + 8, height + 4), font_resolver=catalog.resolve)
alpha = image.getchannel("A")
assert set(alpha.tobytes()) <= {0, 255}, f"U+{cp:04X} grey"
if cp not in (32, 0x3000):
assert alpha.getbbox(), f"U+{cp:04X} blank"
assert not alpha.crop((width - 1, 0, width + 8, height + 4)).getbbox(), f"U+{cp:04X} exceeds ink columns"
assert not alpha.crop((0, height, width + 8, height + 4)).getbbox()
key = f"{cp:04X}"
rows = revisions[key]["rows"] if key in revisions else masters[key]
expected = bytes(255 if int(row, 16) & (1 << (width - 1 - x)) else 0 for row in rows for x in range(width))
assert alpha.crop((0, 0, width, height)).tobytes() == expected, f"U+{cp:04X} differs from master"
@pytest.mark.parametrize("height,factor", [(16,2), (16,3), (16,4), (32,2)])
def test_integer_scaling(height, factor, tmp_path):
catalog = FontCatalog(tmp_path)
identifier = f"builtin:walnut-bus-{height // 2}x{height}"
for char in "高铁路鹤警藏0O1Il5S8B口日目田由甲申回":
small = render_text_layer(TextOptions(char, font=identifier, size=height), size=(height // 2, height), font_resolver=catalog.resolve)
large = render_text_layer(TextOptions(char, font=identifier, size=height * factor),
size=(height // 2 * factor, height * factor), font_resolver=catalog.resolve)
assert large.tobytes() == small.resize(large.size, Image.Resampling.NEAREST).tobytes()
def test_catalog_restart_legacy_digest_and_corrupt_preservation(tmp_path):
catalog = FontCatalog(tmp_path)
registration = json.loads((BUNDLE / "manifest.json").read_text(encoding="utf-8"))
manifest = registration["fonts"][0]
face = catalog.resolve(ID)[0]
original = Path(face.path).read_bytes()
entry = next(item for item in catalog.document()["items"] if item["id"] == ID)
assert entry["source"] == "system" and "16px" in entry["label"]
assert catalog.resolve(f"user:{manifest['sha256']}:0") == (face,)
assert catalog.resolve(next(iter(registration["aliases"]))) == (face,)
assert FontCatalog(tmp_path).resolve(ID) == (face,)
Path(face.path).write_bytes(b"corrupt")
with pytest.raises(FontValidationError, match="preserved"):
FontCatalog(tmp_path)
assert Path(face.path).read_bytes() == b"corrupt"
assert hashlib.sha256(original).hexdigest() == manifest["sha256"]
def test_api_preview_placement_alignment_and_import(tmp_path):
client = TestClient(create_app(project_root=tmp_path, driver_kind="mock"))
assert any(item["id"] == ID for item in client.get("/api/fonts").json()["items"])
request = {"text": "高铁北站21路", "font": ID, "size": 16, "x": 0, "y": 0, "color": "#FF0000"}
response = client.post("/api/preview/text-layer", json=request)
assert response.status_code == 200
image = Image.open(BytesIO(response.content)).convert("RGBA")
for align, x in [("left", 8), ("center", 36), ("right", 64)]:
result = client.post("/api/preview/text-layer", json={**request, "x": x, "y": 16, "align": align})
assert result.status_code == 200
assert Image.open(BytesIO(result.content)).getchannel("A").getbbox()
imported = client.post("/api/fonts/import?filename=bus.ttf", content=(BUNDLE / "SimilarBus8x16.ttf").read_bytes(),
headers={"Content-Type": "application/octet-stream"})
assert imported.status_code == 200
assert imported.json()["fonts"][0]["id"] == ID
masks = [render_text_layer(TextOptions(char, font=ID, size=16), size=(8, 16),
font_resolver=client.app.state.font_catalog.resolve).getchannel("A").tobytes()
for char in "0O1Il5S8B"]
assert len(set(masks)) == len(masks)
def test_bus_template_reload_layer_order_clip_and_missing_glyph_fallback(tmp_path):
catalog = FontCatalog(tmp_path / "data")
legacy = dict(version=1, width=64, height=64,
pixelRgb=base64.b64encode(bytes([10, 20, 30]) * 4096).decode("ascii"),
elements=[dict(id="bus", type="text", text="高铁北站21路", font=ID,
size=16, x=0, y=8, align="left", color="#FF0000")])
scene = prepare_scene(legacy)
store = TemplateStore(tmp_path / "templates", font_resolver=catalog.resolve)
saved = store.create("bus", scene)
reloaded = TemplateStore(tmp_path / "templates", font_resolver=FontCatalog(tmp_path / "data").resolve).get(saved["id"])
assert reloaded["scene"] == scene
full = render_scene_image(scene, font_resolver=catalog.resolve)
assert full.getpixel((63, 63)) == (10, 20, 30)
scene["layers"][1]["elements"][0]["clip"] = [0, 0, 1, 1]
clipped = render_scene_image(scene, font_resolver=catalog.resolve)
assert clipped.getpixel((32, 12)) == (10, 20, 30)
assert clipped.tobytes() != full.tobytes()
from app.display.text_renderer import resolve_font_face
try:
face = resolve_font_face(ID, "高あ", font_resolver=catalog.resolve)
except ValueError as error:
assert "Unicode" in str(error) # hosts without a covering fallback fail clearly
else:
assert face != catalog.resolve(ID)[0] # whole sequence, never mixed fragments