manatee: Add support for pdfs in komga-reader
This commit is contained in:
@@ -834,6 +834,7 @@
|
|||||||
let fitMode = 'width';
|
let fitMode = 'width';
|
||||||
let naturalW = 0, naturalH = 0;
|
let naturalW = 0, naturalH = 0;
|
||||||
let pageMode = 'single'; // 'single' or 'double'
|
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 el = id => document.getElementById(id);
|
||||||
const $loginScreen = el('login-screen');
|
const $loginScreen = el('login-screen');
|
||||||
@@ -883,6 +884,56 @@
|
|||||||
img.src = url;
|
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
|
// LOGIN
|
||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
@@ -1139,6 +1190,7 @@
|
|||||||
async function openBook(book) {
|
async function openBook(book) {
|
||||||
currentBook = book;
|
currentBook = book;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
isPdf = book.media?.mediaType === 'application/pdf';
|
||||||
document.querySelectorAll('.list-item').forEach(e => e.classList.remove('active'));
|
document.querySelectorAll('.list-item').forEach(e => e.classList.remove('active'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1150,6 +1202,16 @@
|
|||||||
if (bd.readProgress && !bd.readProgress.completed) currentPage = bd.readProgress.page || 1;
|
if (bd.readProgress && !bd.readProgress.completed) currentPage = bd.readProgress.page || 1;
|
||||||
} catch(e) {}
|
} 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';
|
$readerPlaceholder.style.display = 'none';
|
||||||
$imageContainer.style.display = 'block';
|
$imageContainer.style.display = 'block';
|
||||||
$readerControls.style.display = 'flex';
|
$readerControls.style.display = 'flex';
|
||||||
@@ -1190,6 +1252,8 @@
|
|||||||
currentPage = pageNum;
|
currentPage = pageNum;
|
||||||
$loadingOverlay.style.display = 'flex';
|
$loadingOverlay.style.display = 'flex';
|
||||||
|
|
||||||
|
if (isPdf) { loadPdfPage(pageNum); return; }
|
||||||
|
|
||||||
if (pageMode === 'double') {
|
if (pageMode === 'double') {
|
||||||
const leftPage = currentPage;
|
const leftPage = currentPage;
|
||||||
const rightPage = currentPage + 1 <= totalPages ? currentPage + 1 : null;
|
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() {
|
function pageStep() {
|
||||||
return pageMode === 'double' ? 2 : 1;
|
return pageMode === 'double' ? 2 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user