205 lines
7.1 KiB
Python
205 lines
7.1 KiB
Python
from typing import List, Annotated
|
|
from fastapi import APIRouter, HTTPException, status, File, UploadFile, Query
|
|
from pydantic import BaseModel
|
|
|
|
from app.services import engine
|
|
from app.services.store import document_store
|
|
|
|
router = APIRouter(prefix="/documents", tags=["documents"])
|
|
|
|
class DocumentInfoResponse(BaseModel):
|
|
id: str
|
|
filename: str
|
|
sizeBytes: int
|
|
totalPages: int
|
|
uploadedAt: str
|
|
status: str
|
|
|
|
@router.post("", response_model=DocumentInfoResponse, status_code=status.HTTP_201_CREATED)
|
|
async def upload_document(file: UploadFile = File(...), password: str = "") -> DocumentInfoResponse:
|
|
if not engine.is_available():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
detail="Engine bridge (bindings/python) not yet available."
|
|
)
|
|
|
|
bytes_data = await file.read()
|
|
try:
|
|
pdfengine = engine.require()
|
|
doc = pdfengine.PdfDocument.load_from_memory(bytes_data, password)
|
|
info = document_store.add_document(file.filename, bytes_data, doc)
|
|
return DocumentInfoResponse(
|
|
id=info["id"],
|
|
filename=info["filename"],
|
|
sizeBytes=info["sizeBytes"],
|
|
totalPages=info["totalPages"],
|
|
uploadedAt=info["uploadedAt"],
|
|
status=info["status"]
|
|
)
|
|
except ValueError as e:
|
|
detail = str(e)
|
|
if "Password required" in detail:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Password required")
|
|
elif "Invalid password" in detail:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password")
|
|
else:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to load PDF: {str(e)}")
|
|
|
|
@router.get("", response_model=List[DocumentInfoResponse])
|
|
def list_documents() -> List[DocumentInfoResponse]:
|
|
if not engine.is_available():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
detail="Engine bridge (bindings/python) not yet available."
|
|
)
|
|
|
|
docs = document_store.list_documents()
|
|
return [
|
|
DocumentInfoResponse(
|
|
id=d["id"],
|
|
filename=d["filename"],
|
|
sizeBytes=d["sizeBytes"],
|
|
totalPages=d["totalPages"],
|
|
uploadedAt=d["uploadedAt"],
|
|
status=d["status"]
|
|
)
|
|
for d in docs
|
|
]
|
|
|
|
@router.get("/{document_id}", response_model=DocumentInfoResponse)
|
|
def get_document(document_id: str) -> DocumentInfoResponse:
|
|
if not engine.is_available():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
detail="Engine bridge (bindings/python) not yet available."
|
|
)
|
|
|
|
d = document_store.get_document(document_id)
|
|
if not d:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
|
|
|
return DocumentInfoResponse(
|
|
id=d["id"],
|
|
filename=d["filename"],
|
|
sizeBytes=d["sizeBytes"],
|
|
totalPages=d["totalPages"],
|
|
uploadedAt=d["uploadedAt"],
|
|
status=d["status"]
|
|
)
|
|
|
|
@router.delete("/{document_id}")
|
|
def delete_document(document_id: str):
|
|
if not engine.is_available():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
detail="Engine bridge (bindings/python) not yet available."
|
|
)
|
|
|
|
deleted = document_store.delete_document(document_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
|
|
|
return {"success": True}
|
|
|
|
class DocumentMetadataResponse(BaseModel):
|
|
title: str
|
|
author: str
|
|
creator: str
|
|
producer: str
|
|
creation_date: str
|
|
modification_date: str
|
|
|
|
@router.get("/{document_id}/metadata", response_model=DocumentMetadataResponse)
|
|
def get_document_metadata(document_id: str) -> DocumentMetadataResponse:
|
|
if not engine.is_available():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
detail="Engine bridge (bindings/python) not yet available."
|
|
)
|
|
|
|
d = document_store.get_document(document_id)
|
|
if not d:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
|
|
|
try:
|
|
doc = d["doc_instance"]
|
|
meta = doc.metadata
|
|
return DocumentMetadataResponse(
|
|
title=meta.title,
|
|
author=meta.author,
|
|
creator=meta.creator,
|
|
producer=meta.producer,
|
|
creation_date=meta.creation_date,
|
|
modification_date=meta.modification_date
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
|
|
class FontInfoResponse(BaseModel):
|
|
fontName: str
|
|
type: str
|
|
isEmbedded: bool
|
|
isSubset: bool
|
|
isVertical: bool
|
|
encoding: str
|
|
hasToUnicode: bool
|
|
cmapName: str
|
|
cidSystemInfo: str
|
|
subsetTag: str
|
|
sourceType: str
|
|
substitutedFrom: str
|
|
substitutedTo: str
|
|
normalizedFamily: str
|
|
internalFontId: str
|
|
flags: int
|
|
ascent: float
|
|
descent: float
|
|
capHeight: float
|
|
|
|
@router.get("/{document_id}/fonts", response_model=List[FontInfoResponse])
|
|
def get_document_fonts(document_id: str, start_page: Annotated[int, Query(ge=0)] = 0, end_page: Annotated[int, Query(ge=-1)] = -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 [
|
|
FontInfoResponse(
|
|
fontName=f.font_name,
|
|
type=f.type,
|
|
isEmbedded=f.is_embedded,
|
|
isSubset=f.is_subset,
|
|
isVertical=f.is_vertical,
|
|
encoding=f.encoding,
|
|
hasToUnicode=f.has_to_unicode,
|
|
cmapName=f.cmap_name,
|
|
cidSystemInfo=f.cid_system_info,
|
|
subsetTag=f.subset_tag,
|
|
sourceType=f.source_type,
|
|
substitutedFrom=f.substituted_from,
|
|
substitutedTo=f.substituted_to,
|
|
normalizedFamily=f.normalized_family,
|
|
internalFontId=f.internal_font_id,
|
|
flags=f.flags,
|
|
ascent=f.ascent,
|
|
descent=f.descent,
|
|
capHeight=f.cap_height
|
|
)
|
|
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)) |