refactor(astrolabe): replace client-side PDF.js with server-side PyMuPDF rendering

Replace the client-side PDF.js viewer with server-side rendering using PyMuPDF.
This avoids CSP worker restrictions and ES private field access issues that
affected Chromium browsers.

Changes:
- Add /api/v1/pdf-preview endpoint to MCP server (management.py)
- Add pdf-preview route and controller action in Astrolabe PHP backend
- Refactor PDFViewer.vue to display server-rendered PNG images
- Remove pdfjs-dist dependency and client-side PDF loading code
- Use @nextcloud/axios for CSRF token handling in PDFViewer

The server downloads the PDF via WebDAV, renders the requested page with
PyMuPDF at the specified scale, and returns a base64-encoded PNG image.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-01-26 20:04:57 +01:00
co-authored by Claude Opus 4.5
parent bc62f2a066
commit d5544a7731
9 changed files with 325 additions and 162 deletions
+57
View File
@@ -788,4 +788,61 @@ class ApiController extends Controller {
return new JSONResponse($result);
}
/**
* Get PDF page preview (server-side rendered).
*
* AJAX endpoint for PDF viewer in semantic search UI.
* Uses server-side PyMuPDF rendering to avoid CSP/worker issues.
*
* @param string $file_path WebDAV path to PDF file
* @param int $page Page number (1-indexed, default: 1)
* @param float $scale Zoom factor (default: 2.0)
* @return JSONResponse
*/
#[NoAdminRequired]
public function pdfPreview(
string $file_path,
int $page = 1,
float $scale = 2.0,
): JSONResponse {
$user = $this->userSession->getUser();
if (!$user) {
return new JSONResponse(['success' => false, 'error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED);
}
$userId = $user->getUID();
// Create refresh callback
$refreshCallback = function (string $refreshToken) {
$newTokenData = $this->tokenRefresher->refreshAccessToken($refreshToken);
if (!$newTokenData) {
return null;
}
return [
'access_token' => $newTokenData['access_token'],
'refresh_token' => $newTokenData['refresh_token'] ?? $refreshToken,
'expires_in' => $newTokenData['expires_in'] ?? 3600,
];
};
// Get user's OAuth token for MCP server with automatic refresh
$accessToken = $this->tokenStorage->getAccessToken($userId, $refreshCallback);
if (!$accessToken) {
return new JSONResponse([
'success' => false,
'error' => 'MCP server authorization required.'
], Http::STATUS_UNAUTHORIZED);
}
$result = $this->client->getPdfPreview($file_path, $page, $scale, $accessToken);
if (isset($result['error'])) {
return new JSONResponse(['success' => false, 'error' => $result['error']], Http::STATUS_INTERNAL_SERVER_ERROR);
}
return new JSONResponse($result);
}
}
+57
View File
@@ -605,4 +605,61 @@ class McpServerClient {
return ['error' => $e->getMessage()];
}
}
/**
* Get PDF page preview (server-side rendered).
*
* Renders a PDF page to PNG using PyMuPDF on the server.
* This avoids client-side PDF.js issues with CSP and ES private fields.
*
* Requires OAuth bearer token for authentication.
*
* @param string $filePath WebDAV path to PDF file
* @param int $page Page number (1-indexed)
* @param float $scale Zoom factor (default: 2.0)
* @param string $token OAuth bearer token
* @return array{
* success?: bool,
* image?: string,
* page_number?: int,
* total_pages?: int,
* error?: string
* }
*/
public function getPdfPreview(
string $filePath,
int $page,
float $scale,
string $token,
): array {
try {
$response = $this->httpClient->get(
$this->baseUrl . '/api/v1/pdf-preview',
[
'headers' => [
'Authorization' => 'Bearer ' . $token
],
'query' => [
'file_path' => $filePath,
'page' => $page,
'scale' => $scale,
]
]
);
$data = json_decode($response->getBody(), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Invalid JSON response from server');
}
return $data;
} catch (\Exception $e) {
$this->logger->error('Failed to get PDF preview', [
'error' => $e->getMessage(),
'file_path' => $filePath,
'page' => $page,
]);
return ['error' => $e->getMessage()];
}
}
}