"""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 )