Show a clear message when PyMuPDF is missing
This commit is contained in:
@@ -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)
|
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:
|
def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||||
router = APIRouter(tags=["documents"])
|
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.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar
|
||||||
from src.constants import UPLOAD_DIR
|
from src.constants import UPLOAD_DIR
|
||||||
import fitz
|
|
||||||
|
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@@ -988,6 +996,7 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
|
|||||||
if not pdf_path:
|
if not pdf_path:
|
||||||
raise HTTPException(404, f"Source PDF {upload_id} not found")
|
raise HTTPException(404, f"Source PDF {upload_id} not found")
|
||||||
|
|
||||||
|
fitz = _load_pdf_viewer_fitz()
|
||||||
schema = load_field_sidecar(pdf_path) or []
|
schema = load_field_sidecar(pdf_path) or []
|
||||||
values = parse_markdown_to_values(doc.current_content 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 fastapi.responses import Response
|
||||||
from src.pdf_form_doc import find_source_upload_id
|
from src.pdf_form_doc import find_source_upload_id
|
||||||
from src.constants import UPLOAD_DIR
|
from src.constants import UPLOAD_DIR
|
||||||
import fitz
|
|
||||||
|
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@@ -1058,6 +1066,7 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter:
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
fitz = _load_pdf_viewer_fitz()
|
||||||
pdf_doc = fitz.open(pdf_path)
|
pdf_doc = fitz.open(pdf_path)
|
||||||
try:
|
try:
|
||||||
if page_no < 1 or page_no > pdf_doc.page_count:
|
if page_no < 1 or page_no > pdf_doc.page_count:
|
||||||
|
|||||||
15
src/pdf_runtime.py
Normal file
15
src/pdf_runtime.py
Normal file
@@ -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
|
||||||
@@ -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() {
|
async function _renderPdfPane() {
|
||||||
const pane = document.getElementById('doc-pdf-view');
|
const pane = document.getElementById('doc-pdf-view');
|
||||||
if (!pane || !activeDocId) return;
|
if (!pane || !activeDocId) return;
|
||||||
@@ -1118,7 +1128,7 @@ import * as Modals from './modalManager.js';
|
|||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/document/${docId}/render-pages`);
|
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();
|
data = await res.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
pane.innerHTML = `<div style="color:#fbb;padding:40px;text-align:center;">Failed to load PDF view: ${_escHtml(e.message || String(e))}</div>`;
|
pane.innerHTML = `<div style="color:#fbb;padding:40px;text-align:center;">Failed to load PDF view: ${_escHtml(e.message || String(e))}</div>`;
|
||||||
|
|||||||
24
tests/test_pdf_runtime.py
Normal file
24
tests/test_pdf_runtime.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user