初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from fontTools.fontBuilder import FontBuilder
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
from PIL import Image
|
||||
|
||||
from app.fonts.store import MAX_FONT_UPLOAD_BYTES, FontCatalog
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
def build_test_font(family: str = "Codex Test Sans", style: str = "Regular") -> bytes:
|
||||
builder = FontBuilder(1000, isTTF=True)
|
||||
glyph_order = [".notdef", "space", "A", "B"]
|
||||
builder.setupGlyphOrder(glyph_order)
|
||||
|
||||
glyphs = {}
|
||||
empty = TTGlyphPen(None)
|
||||
glyphs[".notdef"] = empty.glyph()
|
||||
glyphs["space"] = TTGlyphPen(None).glyph()
|
||||
for name, inset in (("A", 100), ("B", 180)):
|
||||
pen = TTGlyphPen(None)
|
||||
pen.moveTo((inset, 0))
|
||||
pen.lineTo((800 - inset, 0))
|
||||
pen.lineTo((800 - inset, 800))
|
||||
pen.lineTo((inset, 800))
|
||||
pen.closePath()
|
||||
glyphs[name] = pen.glyph()
|
||||
|
||||
builder.setupGlyf(glyphs)
|
||||
builder.setupHorizontalMetrics({name: (900, 0) for name in glyph_order})
|
||||
builder.setupHorizontalHeader(ascent=800, descent=-200)
|
||||
builder.setupCharacterMap({32: "space", 65: "A", 66: "B"})
|
||||
builder.setupNameTable({
|
||||
"familyName": family,
|
||||
"styleName": style,
|
||||
"uniqueFontIdentifier": f"{family}-{style}",
|
||||
"fullName": f"{family} {style}",
|
||||
"psName": f"{family.replace(' ', '')}-{style.replace(' ', '')}",
|
||||
})
|
||||
builder.setupOS2(
|
||||
sTypoAscender=800,
|
||||
sTypoDescender=-200,
|
||||
usWinAscent=800,
|
||||
usWinDescent=200,
|
||||
)
|
||||
builder.setupPost()
|
||||
builder.setupMaxp()
|
||||
output = BytesIO()
|
||||
builder.save(output)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def test_font_catalog_import_duplicate_restart_and_render(tmp_path: Path) -> None:
|
||||
font_bytes = build_test_font()
|
||||
app = create_app(project_root=tmp_path, driver_kind="mock")
|
||||
client = TestClient(app)
|
||||
|
||||
initial = client.get("/api/fonts")
|
||||
assert initial.status_code == 200
|
||||
document = initial.json()
|
||||
assert document["items"][0] == {
|
||||
"id": "default",
|
||||
"label": "自动多语言字体",
|
||||
"family": "自动多语言字体",
|
||||
"style": "自动回退",
|
||||
"source": "automatic",
|
||||
}
|
||||
assert document["accepted_extensions"] == [".ttf", ".otf", ".ttc", ".otc"]
|
||||
assert document["max_upload_bytes"] == MAX_FONT_UPLOAD_BYTES
|
||||
assert str(tmp_path) not in initial.text
|
||||
|
||||
created = client.post(
|
||||
"/api/fonts/import?filename=Codex%20Test.ttf",
|
||||
content=font_bytes,
|
||||
headers={"Content-Type": "application/octet-stream"},
|
||||
)
|
||||
assert created.status_code == 201
|
||||
payload = created.json()
|
||||
assert payload["created"] is True
|
||||
assert len(payload["fonts"]) == 1
|
||||
imported = payload["fonts"][0]
|
||||
assert imported["id"].startswith("user:")
|
||||
assert imported["family"] == "Codex Test Sans"
|
||||
assert imported["source"] == "imported"
|
||||
assert str(tmp_path) not in created.text
|
||||
|
||||
stored = list((tmp_path / "data" / "fonts").glob("*.font"))
|
||||
assert len(stored) == 1
|
||||
assert stored[0].stem in imported["id"]
|
||||
|
||||
duplicate = client.post(
|
||||
"/api/fonts/import?filename=renamed.otf",
|
||||
content=font_bytes,
|
||||
headers={"Content-Type": "application/octet-stream"},
|
||||
)
|
||||
assert duplicate.status_code == 200
|
||||
assert duplicate.json() == {"created": False, "fonts": [imported]}
|
||||
assert len(list((tmp_path / "data" / "fonts").glob("*.font"))) == 1
|
||||
|
||||
preview = client.post("/api/preview/text-layer", json={
|
||||
"text": "AB",
|
||||
"font": imported["id"],
|
||||
"size": 18,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"align": "left",
|
||||
"color": "#FFFFFF",
|
||||
})
|
||||
assert preview.status_code == 200
|
||||
assert Image.open(BytesIO(preview.content)).getchannel("A").getbbox() is not None
|
||||
|
||||
restarted = TestClient(create_app(project_root=tmp_path, driver_kind="mock"))
|
||||
items = restarted.get("/api/fonts").json()["items"]
|
||||
assert imported in items
|
||||
restarted_preview = restarted.post("/api/preview/text-layer", json={
|
||||
"text": "A",
|
||||
"font": imported["id"],
|
||||
"size": 18,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"align": "left",
|
||||
"color": "#FFFFFF",
|
||||
})
|
||||
assert restarted_preview.status_code == 200
|
||||
|
||||
|
||||
def test_font_import_rejects_type_content_and_size_without_residue(tmp_path: Path) -> None:
|
||||
client = TestClient(create_app(project_root=tmp_path, driver_kind="mock"))
|
||||
root = tmp_path / "data" / "fonts"
|
||||
|
||||
wrong_content_type = client.post(
|
||||
"/api/fonts/import?filename=test.ttf",
|
||||
content=b"not-a-font",
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
assert wrong_content_type.status_code == 415
|
||||
|
||||
wrong_extension = client.post(
|
||||
"/api/fonts/import?filename=test.woff2",
|
||||
content=b"not-a-font",
|
||||
headers={"Content-Type": "application/octet-stream"},
|
||||
)
|
||||
assert wrong_extension.status_code == 415
|
||||
|
||||
damaged = client.post(
|
||||
"/api/fonts/import?filename=test.ttf",
|
||||
content=b"not-a-font",
|
||||
headers={"Content-Type": "application/octet-stream"},
|
||||
)
|
||||
assert damaged.status_code == 422
|
||||
|
||||
oversized = client.post(
|
||||
"/api/fonts/import?filename=test.ttf",
|
||||
content=b"x",
|
||||
headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": str(MAX_FONT_UPLOAD_BYTES + 1),
|
||||
},
|
||||
)
|
||||
assert oversized.status_code == 413
|
||||
assert not list(root.glob(".upload-*.tmp"))
|
||||
assert not list(root.glob("*.font"))
|
||||
|
||||
|
||||
def test_invalid_persisted_font_is_preserved_and_reported(tmp_path: Path) -> None:
|
||||
root = tmp_path / "data" / "fonts"
|
||||
root.mkdir(parents=True)
|
||||
damaged = root / f"{'a' * 64}.font"
|
||||
damaged.write_bytes(b"damaged")
|
||||
|
||||
catalog = FontCatalog(tmp_path / "data")
|
||||
document = catalog.document()
|
||||
assert damaged.read_bytes() == b"damaged"
|
||||
assert document["warnings"] == [f"已保留但忽略损坏字体:{damaged.name}"]
|
||||
assert all(item["source"] != "imported" for item in document["items"])
|
||||
Reference in New Issue
Block a user