manatee: Add support for pdfs in komga-reader
This commit is contained in:
@@ -834,6 +834,7 @@
|
||||
let fitMode = 'width';
|
||||
let naturalW = 0, naturalH = 0;
|
||||
let pageMode = 'single'; // 'single' or 'double'
|
||||
let isPdf = false; // current book is a PDF (rendered client-side via pdf.js)
|
||||
|
||||
const el = id => document.getElementById(id);
|
||||
const $loginScreen = el('login-screen');
|
||||
@@ -883,6 +884,56 @@
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// PDF SUPPORT (pdf.js, lazy-loaded)
|
||||
//
|
||||
// Komga serves PDF pages as raw application/pdf (one single-page
|
||||
// PDF per /pages/{n} request), which an <img> can't decode. For
|
||||
// PDF books we fetch that mini-PDF and rasterize it to a canvas
|
||||
// with pdf.js, then feed it through the same zoom/pan/double-page
|
||||
// machinery as image pages.
|
||||
// ══════════════════════════════════════════════
|
||||
const PDFJS_VERSION = '3.11.174';
|
||||
let pdfjsReady = null;
|
||||
|
||||
function ensurePdfjs() {
|
||||
if (window.pdfjsLib) return Promise.resolve();
|
||||
if (pdfjsReady) return pdfjsReady;
|
||||
pdfjsReady = new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS_VERSION}/pdf.min.js`;
|
||||
s.onload = () => {
|
||||
window.pdfjsLib.GlobalWorkerOptions.workerSrc =
|
||||
`https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS_VERSION}/pdf.worker.min.js`;
|
||||
resolve();
|
||||
};
|
||||
s.onerror = () => { pdfjsReady = null; reject(new Error('pdf.js failed to load')); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return pdfjsReady;
|
||||
}
|
||||
|
||||
// Fetch page `pageNum` (a single-page PDF) and render it to a fresh
|
||||
// offscreen canvas at a resolution that stays crisp when zoomed.
|
||||
async function renderPdfPage(pageNum) {
|
||||
const url = serverUrl.replace(/\/$/, '') + `/api/v1/books/${currentBook.id}/pages/${pageNum}`;
|
||||
const buf = await fetch(url, { headers: { 'Authorization': authHeader } }).then(r => r.arrayBuffer());
|
||||
const doc = await window.pdfjsLib.getDocument({ data: buf }).promise;
|
||||
try {
|
||||
const page = await doc.getPage(1);
|
||||
const base = page.getViewport({ scale: 1 });
|
||||
const scale = Math.min(4, Math.max(1, 2000 / base.width));
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.ceil(viewport.width);
|
||||
canvas.height = Math.ceil(viewport.height);
|
||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
|
||||
return canvas;
|
||||
} finally {
|
||||
doc.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// LOGIN
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -1139,6 +1190,7 @@
|
||||
async function openBook(book) {
|
||||
currentBook = book;
|
||||
currentPage = 1;
|
||||
isPdf = book.media?.mediaType === 'application/pdf';
|
||||
document.querySelectorAll('.list-item').forEach(e => e.classList.remove('active'));
|
||||
|
||||
try {
|
||||
@@ -1150,6 +1202,16 @@
|
||||
if (bd.readProgress && !bd.readProgress.completed) currentPage = bd.readProgress.page || 1;
|
||||
} catch(e) {}
|
||||
|
||||
if (isPdf) {
|
||||
try {
|
||||
await ensurePdfjs();
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
$readerPlaceholder.querySelector('p').textContent = 'Failed to load the PDF renderer (needs network for pdf.js).';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$readerPlaceholder.style.display = 'none';
|
||||
$imageContainer.style.display = 'block';
|
||||
$readerControls.style.display = 'flex';
|
||||
@@ -1190,6 +1252,8 @@
|
||||
currentPage = pageNum;
|
||||
$loadingOverlay.style.display = 'flex';
|
||||
|
||||
if (isPdf) { loadPdfPage(pageNum); return; }
|
||||
|
||||
if (pageMode === 'double') {
|
||||
const leftPage = currentPage;
|
||||
const rightPage = currentPage + 1 <= totalPages ? currentPage + 1 : null;
|
||||
@@ -1256,6 +1320,69 @@
|
||||
}
|
||||
}
|
||||
|
||||
function loadPdfPage(pageNum) {
|
||||
if (pageMode === 'double') {
|
||||
const leftPage = currentPage;
|
||||
const rightPage = currentPage + 1 <= totalPages ? currentPage + 1 : null;
|
||||
|
||||
$pageIndicator.textContent = rightPage
|
||||
? `${leftPage}-${rightPage} / ${totalPages}`
|
||||
: `${leftPage} / ${totalPages}`;
|
||||
|
||||
const jobs = [renderPdfPage(leftPage)];
|
||||
if (rightPage) jobs.push(renderPdfPage(rightPage));
|
||||
|
||||
Promise.all(jobs)
|
||||
.then(canvases => {
|
||||
const left = canvases[0];
|
||||
const right = canvases[1] || null;
|
||||
|
||||
const lw = left.width, lh = left.height;
|
||||
const rw = right ? right.width : 0;
|
||||
const rh = right ? right.height : 0;
|
||||
const canvasW = lw + (right ? rw : 0);
|
||||
const canvasH = Math.max(lh, rh || 0);
|
||||
|
||||
$comicCanvas.width = canvasW;
|
||||
$comicCanvas.height = canvasH;
|
||||
const ctx = $comicCanvas.getContext('2d');
|
||||
ctx.fillStyle = '#0a0a0c';
|
||||
ctx.fillRect(0, 0, canvasW, canvasH);
|
||||
ctx.drawImage(left, 0, (canvasH - lh) / 2);
|
||||
if (right) ctx.drawImage(right, lw, (canvasH - rh) / 2);
|
||||
|
||||
const prevW = naturalW;
|
||||
naturalW = canvasW;
|
||||
naturalH = canvasH;
|
||||
showElement($comicCanvas);
|
||||
|
||||
if (!prevW) applyFitMode(); else centerAtCurrentScale();
|
||||
$loadingOverlay.style.display = 'none';
|
||||
updateReadProgress(rightPage || leftPage);
|
||||
})
|
||||
.catch(() => { $loadingOverlay.style.display = 'none'; });
|
||||
} else {
|
||||
$pageIndicator.textContent = `${currentPage} / ${totalPages}`;
|
||||
|
||||
renderPdfPage(currentPage)
|
||||
.then(canvas => {
|
||||
$comicCanvas.width = canvas.width;
|
||||
$comicCanvas.height = canvas.height;
|
||||
$comicCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
||||
|
||||
const prevW = naturalW;
|
||||
naturalW = canvas.width;
|
||||
naturalH = canvas.height;
|
||||
showElement($comicCanvas);
|
||||
|
||||
if (!prevW) applyFitMode(); else centerAtCurrentScale();
|
||||
$loadingOverlay.style.display = 'none';
|
||||
updateReadProgress(currentPage);
|
||||
})
|
||||
.catch(() => { $loadingOverlay.style.display = 'none'; });
|
||||
}
|
||||
}
|
||||
|
||||
function pageStep() {
|
||||
return pageMode === 'double' ? 2 : 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user