110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
import hashlib
|
|
|
|
from fastapi import APIRouter, HTTPException, Response, status
|
|
|
|
from app.schemas.font import FontInfoResponse
|
|
from app.services import engine
|
|
from app.services.font import font_info_to_response
|
|
from app.services.store import document_store
|
|
|
|
router = APIRouter(tags=["documents"])
|
|
|
|
_SFNT_TTF_MAGIC = (b"\x00\x01\x00\x00", b"true", b"ttcf")
|
|
_SFNT_OTF_MAGIC = b"OTTO"
|
|
|
|
|
|
@router.get("/{document_id}/fonts", response_model=list[FontInfoResponse])
|
|
def get_document_fonts(
|
|
document_id: str, start_page: int = 0, end_page: int = -1
|
|
) -> list[FontInfoResponse]:
|
|
if not engine.is_available():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
detail="Engine bridge (bindings/python) not yet available.",
|
|
)
|
|
|
|
doc_info = document_store.get_document(document_id)
|
|
if not doc_info:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
|
|
|
try:
|
|
doc = doc_info["doc_instance"]
|
|
fonts = doc.get_fonts(start_page, end_page)
|
|
return [font_info_to_response(f) for f in fonts]
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except IndexError as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
|
|
@router.get("/{document_id}/font")
|
|
def get_font_bytes(document_id: str, internal_font_id: str) -> Response:
|
|
"""Raw embedded font bytes for an in-place-editing preview.
|
|
|
|
Returns the font only when it's a browser-loadable sfnt (TrueType / OpenType-CFF).
|
|
Type1/PFB, non-embedded, and unknown fonts return 404 so the frontend falls back to
|
|
a base-14 CSS font. The lookup is sandboxed to fonts inside the loaded document.
|
|
"""
|
|
if not engine.is_available():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
detail="Engine bridge (bindings/python) not yet available.",
|
|
)
|
|
|
|
if not internal_font_id or len(internal_font_id) > 256:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not found")
|
|
|
|
doc_info = document_store.get_document(document_id)
|
|
if not doc_info:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
|
|
|
try:
|
|
data = bytes(doc_info["doc_instance"].get_font_data(internal_font_id))
|
|
except Exception:
|
|
data = b""
|
|
if not data:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not found")
|
|
|
|
magic = data[:4]
|
|
if magic in _SFNT_TTF_MAGIC:
|
|
media_type = "font/ttf"
|
|
elif magic == _SFNT_OTF_MAGIC:
|
|
media_type = "font/otf"
|
|
else:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not found")
|
|
|
|
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
|
|
return Response(
|
|
content=data,
|
|
media_type=media_type,
|
|
headers={"Cache-Control": "public, max-age=31536000, immutable", "ETag": etag},
|
|
)
|
|
|
|
|
|
@router.get("/{document_id}/font-reconstructed")
|
|
def get_reconstructed_font_bytes(document_id: str, internal_font_id: str) -> Response:
|
|
"""Tier-2: a cmap-augmented copy of an embedded font (original glyph program + synthesized
|
|
Unicode cmap) so the WASM live preview can reuse the document's real glyphs and match the
|
|
saved result. 204 when reconstruction isn't possible -> frontend falls back to Tier-1."""
|
|
if not engine.is_available():
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
if not internal_font_id or len(internal_font_id) > 256:
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
doc_info = document_store.get_document(document_id)
|
|
if not doc_info:
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
try:
|
|
data = bytes(doc_info["doc_instance"].get_reconstructed_font_data(internal_font_id))
|
|
except Exception:
|
|
data = b""
|
|
if not data or data[:4] not in _SFNT_TTF_MAGIC:
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
|
|
return Response(
|
|
content=data,
|
|
media_type="font/ttf",
|
|
headers={"Cache-Control": "public, max-age=31536000, immutable", "ETag": etag},
|
|
)
|