diff --git a/routes/document_routes.py b/routes/document_routes.py index 9ae2994..bae8bdb 100644 --- a/routes/document_routes.py +++ b/routes/document_routes.py @@ -30,6 +30,15 @@ def _locate_current_user_upload(request: Request, upload_dir: str, upload_id: st return _locate_upload(upload_dir, upload_id, owner=user, auth_manager=auth_manager) +def _load_pdf_viewer_fitz(): + from src.pdf_runtime import load_pymupdf_for_pdf_viewer + + try: + return load_pymupdf_for_pdf_viewer() + except RuntimeError as exc: + raise HTTPException(503, str(exc)) from exc + + def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: router = APIRouter(tags=["documents"]) @@ -972,7 +981,6 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: """ from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar from src.constants import UPLOAD_DIR - import fitz user = get_current_user(request) db = SessionLocal() @@ -988,6 +996,7 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: if not pdf_path: raise HTTPException(404, f"Source PDF {upload_id} not found") + fitz = _load_pdf_viewer_fitz() schema = load_field_sidecar(pdf_path) or [] values = parse_markdown_to_values(doc.current_content or "") @@ -1040,7 +1049,6 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: from fastapi.responses import Response from src.pdf_form_doc import find_source_upload_id from src.constants import UPLOAD_DIR - import fitz user = get_current_user(request) db = SessionLocal() @@ -1058,6 +1066,7 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: finally: db.close() + fitz = _load_pdf_viewer_fitz() pdf_doc = fitz.open(pdf_path) try: if page_no < 1 or page_no > pdf_doc.page_count: diff --git a/src/pdf_runtime.py b/src/pdf_runtime.py new file mode 100644 index 0000000..40d5016 --- /dev/null +++ b/src/pdf_runtime.py @@ -0,0 +1,15 @@ +"""Small helpers for optional PDF runtime dependencies.""" + +PDF_VIEWER_PYMUPDF_MISSING = ( + "PDF viewer requires PyMuPDF. Install optional PDF dependencies with " + "`pip install -r requirements-optional.txt` (PyMuPDF is AGPL-3.0)." +) + + +def load_pymupdf_for_pdf_viewer(): + """Return the PyMuPDF module, or raise a user-facing setup hint.""" + try: + import fitz # PyMuPDF, optional + except ImportError as exc: + raise RuntimeError(PDF_VIEWER_PYMUPDF_MISSING) from exc + return fitz diff --git a/static/js/document.js b/static/js/document.js index fe8084a..2d8b8e4 100644 --- a/static/js/document.js +++ b/static/js/document.js @@ -1106,6 +1106,16 @@ import * as Modals from './modalManager.js'; }); } + async function _pdfResponseErrorMessage(res) { + const text = await res.text().catch(() => ''); + try { + const data = JSON.parse(text); + if (typeof data?.detail === 'string') return data.detail; + if (data?.detail) return JSON.stringify(data.detail); + } catch (_) {} + return text || res.statusText || `HTTP ${res.status}`; + } + async function _renderPdfPane() { const pane = document.getElementById('doc-pdf-view'); if (!pane || !activeDocId) return; @@ -1118,7 +1128,7 @@ import * as Modals from './modalManager.js'; let data; try { const res = await fetch(`${API_BASE}/api/document/${docId}/render-pages`); - if (!res.ok) throw new Error(await res.text()); + if (!res.ok) throw new Error(await _pdfResponseErrorMessage(res)); data = await res.json(); } catch (e) { pane.innerHTML = `
Failed to load PDF view: ${_escHtml(e.message || String(e))}
`; diff --git a/tests/test_pdf_runtime.py b/tests/test_pdf_runtime.py new file mode 100644 index 0000000..cdeb6c7 --- /dev/null +++ b/tests/test_pdf_runtime.py @@ -0,0 +1,24 @@ +import builtins + +import pytest + +from src.pdf_runtime import PDF_VIEWER_PYMUPDF_MISSING, load_pymupdf_for_pdf_viewer + + +def test_pdf_viewer_dependency_error_is_user_actionable(monkeypatch): + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "fitz": + raise ImportError("No module named fitz") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", fake_import) + + with pytest.raises(RuntimeError) as exc: + load_pymupdf_for_pdf_viewer() + + message = str(exc.value) + assert message == PDF_VIEWER_PYMUPDF_MISSING + assert "requirements-optional.txt" in message + assert "PyMuPDF" in message