119 lines
4.0 KiB
Python
119 lines
4.0 KiB
Python
"""Tests for the in-place-editing data path: per-run fill color + line baseline in the
|
|
page model, and the /font endpoint that serves browser-loadable font bytes.
|
|
|
|
These back the Adobe-style in-place text editor (real font + exact color + baseline).
|
|
"""
|
|
|
|
import contextlib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.services import engine
|
|
from app.services.store import document_store
|
|
|
|
has_pdfium = False
|
|
if engine.is_available():
|
|
with contextlib.suppress(Exception):
|
|
has_pdfium = engine.require().engine_has_pdfium()
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not engine.is_available() or not has_pdfium,
|
|
reason="pdfengine pybind11 module is not compiled/available, or was compiled without PDFium support.",
|
|
)
|
|
|
|
|
|
def _build_colored_pdf() -> bytes:
|
|
"""Minimal 1-page PDF: red 'Red Text' + blue 'Blue Text', Helvetica."""
|
|
content = (
|
|
b"1 0 0 rg\nBT /F1 24 Tf 72 700 Td (Red Text) Tj ET\n"
|
|
b"0 0 1 rg\nBT /F1 24 Tf 72 650 Td (Blue Text) Tj ET\n"
|
|
)
|
|
objs = [
|
|
b"<< /Type /Catalog /Pages 2 0 R >>",
|
|
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
|
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
|
|
b"/Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
|
|
b"<< /Length %d >>\nstream\n" % len(content) + content + b"endstream",
|
|
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
|
]
|
|
pdf = b"%PDF-1.7\n"
|
|
offsets = []
|
|
for i, o in enumerate(objs, 1):
|
|
offsets.append(len(pdf))
|
|
pdf += b"%d 0 obj\n" % i + o + b"\nendobj\n"
|
|
xref_pos = len(pdf)
|
|
pdf += b"xref\n0 %d\n" % (len(objs) + 1)
|
|
pdf += b"0000000000 65535 f \n"
|
|
for off in offsets:
|
|
pdf += b"%010d 00000 n \n" % off
|
|
pdf += b"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n" % (
|
|
len(objs) + 1,
|
|
xref_pos,
|
|
)
|
|
return pdf
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_store():
|
|
with document_store._lock:
|
|
document_store._documents.clear()
|
|
yield
|
|
|
|
|
|
@pytest.fixture()
|
|
def colored_doc_id(client: TestClient) -> str:
|
|
resp = client.post(
|
|
"/documents", files={"file": ("colored.pdf", _build_colored_pdf(), "application/pdf")}
|
|
)
|
|
assert resp.status_code == 201
|
|
return resp.json()["id"]
|
|
|
|
|
|
def _runs(model: dict) -> list[dict]:
|
|
return [r for p in model["paragraphs"] for ln in p["lines"] for r in ln["runs"]]
|
|
|
|
|
|
def test_model_has_fill_color_and_baseline(client: TestClient, colored_doc_id: str):
|
|
model = client.get(f"/documents/{colored_doc_id}/pages/0/model").json()
|
|
runs = _runs(model)
|
|
colors = {r["text"].strip(): r["color"] for r in runs}
|
|
assert colors.get("Red Text") == "#ff0000"
|
|
assert colors.get("Blue Text") == "#0000ff"
|
|
|
|
for p in model["paragraphs"]:
|
|
for line in p["lines"]:
|
|
if line["runs"]:
|
|
assert line["baseline_y"] > 0
|
|
|
|
|
|
def test_font_endpoint_serves_loadable_sfnt(client: TestClient, colored_doc_id: str):
|
|
model = client.get(f"/documents/{colored_doc_id}/pages/0/model").json()
|
|
fid = _runs(model)[0]["internal_font_id"]
|
|
|
|
resp = client.get(f"/documents/{colored_doc_id}/font", params={"internal_font_id": fid})
|
|
assert resp.status_code == 200
|
|
assert resp.headers["content-type"] in ("font/ttf", "font/otf")
|
|
assert resp.content[:4] in (b"\x00\x01\x00\x00", b"true", b"ttcf", b"OTTO")
|
|
assert "immutable" in resp.headers.get("cache-control", "")
|
|
assert resp.headers.get("etag")
|
|
|
|
|
|
def test_font_endpoint_rejects_unknown(client: TestClient, colored_doc_id: str):
|
|
assert (
|
|
client.get(
|
|
f"/documents/{colored_doc_id}/font", params={"internal_font_id": "NoSuchFont_XYZ_0"}
|
|
).status_code
|
|
== 404
|
|
)
|
|
long_resp = client.get(
|
|
f"/documents/{colored_doc_id}/font", params={"internal_font_id": "A" * 300}
|
|
)
|
|
assert long_resp.status_code == 404
|
|
assert "AAAA" not in long_resp.text
|
|
assert (
|
|
client.get("/documents/does-not-exist/font", params={"internal_font_id": "x"}).status_code
|
|
== 404
|
|
)
|