diora-web/static/js/app/58-pdf.js

202 lines
7 KiB
JavaScript
Raw Permalink Normal View History

'use strict';
async function _parsePdfOutline(pdf, items, depth) {
depth = depth || 0;
const result = [];
for (const item of items) {
let href = '';
if (item.dest) {
try {
const dest = typeof item.dest === 'string' ? await pdf.getDestination(item.dest) : item.dest;
if (dest) {
const pageIndex = await pdf.getPageIndex(dest[0]);
href = `#pdf-page-${pageIndex + 1}`;
}
} catch (e) {}
}
result.push({label: item.title || '(untitled)', href, depth});
if (item.items && item.items.length) {
result.push(...await _parsePdfOutline(pdf, item.items, depth + 1));
}
}
return result;
}
// The document being loaded (pdf.js loading task), so opening can be cancelled.
let _pdfLoadingTask = null;
// The pages' drawing of the last renderPdf: an observer and its queue (stopped on the next one).
let _pdfLazy = null;
function _pdfStopLazy() {
if (_pdfLazy) {
_pdfLazy.stopped = true;
if (_pdfLazy.observer) _pdfLazy.observer.disconnect();
_pdfLazy = null;
}
}
// Every page gets its place at once (sized like the first page until it is drawn), but only the
// pages near the view are drawn; ones far away are let go again. A book of 1000 scanned pages
// opens as fast as one of ten, and doesn't hold 1000 canvases.
async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
const myGen = ++_pdfRenderGen;
_pdfStopLazy();
let pdf = currentPdfDoc;
if (!pdf) {
_pdfLoadingTask = pdfjsLib.getDocument({data: new Uint8Array(arrayBuffer.slice(0))});
try {
pdf = await _pdfLoadingTask.promise;
} finally {
_pdfLoadingTask = null;
}
}
if (_pdfRenderGen !== myGen) return null;
currentPdfDoc = pdf;
let pdfTitle = '', pdfAuthor = '';
try {
const meta = await pdf.getMetadata();
pdfTitle = meta.info?.Title?.trim() || '';
pdfAuthor = meta.info?.Author?.trim() || '';
} catch (e) {}
let toc = [];
try {
const outline = await pdf.getOutline();
if (outline && outline.length) toc = await _parsePdfOutline(pdf, outline);
} catch (e) {}
if (_pdfRenderGen !== myGen) return null;
contentEl.innerHTML = '';
const pdfVp = document.createElement('div');
pdfVp.id = 'pdf-viewport';
contentEl.appendChild(pdfVp);
const containerWidth = readerSettings.pdfSpread
? contentEl.clientWidth - 32
: Math.min(contentEl.clientWidth - 32, 900);
const pageWidth = readerSettings.pdfSpread ? (containerWidth - 8) / 2 : containerWidth;
const dpr = window.devicePixelRatio || 1;
const scaleFor = vp => (scaleOverride != null ? scaleOverride
: Math.max(0.5, pageWidth / vp.width) * (readerSettings.pdfZoom / 100));
// The first page's size stands for all until each is drawn (a scan's pages are alike).
const first = await pdf.getPage(1);
if (_pdfRenderGen !== myGen) return null;
const firstVp = first.getViewport({scale: scaleFor(first.getViewport({scale: 1}))});
const entries = [];
let spreadContainer = null;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const wrapper = document.createElement('div');
wrapper.className = 'pdf-page-wrapper';
wrapper.id = `pdf-page-${pageNum}`;
const inner = document.createElement('div');
inner.className = 'pdf-page-inner';
const canvas = document.createElement('canvas');
canvas.className = 'pdf-page';
canvas.width = 0;
canvas.height = 0;
canvas.style.width = firstVp.width + 'px';
canvas.style.height = firstVp.height + 'px';
inner.appendChild(canvas);
wrapper.appendChild(inner);
if (!readerSettings.pdfSpread) {
pdfVp.appendChild(wrapper);
} else if (pageNum === 1) {
wrapper.classList.add('pdf-spread-cover');
pdfVp.appendChild(wrapper);
spreadContainer = null;
} else {
if (pageNum % 2 === 0) {
spreadContainer = document.createElement('div');
spreadContainer.className = 'pdf-spread-wrapper';
pdfVp.appendChild(spreadContainer);
}
if (spreadContainer) spreadContainer.appendChild(wrapper);
else pdfVp.appendChild(wrapper);
}
entries.push({num: pageNum, wrapper, canvas, drawn: false, near: false, task: null});
}
pdfTotalPages = pdf.numPages;
const pivot = Math.max(1, Math.min(pdf.numPages, pivotPage || 1));
const pivotEl = document.getElementById(`pdf-page-${pivot}`);
if (pivotEl && pivot > 1) pivotEl.scrollIntoView({block: 'start'});
const lazy = {stopped: false, observer: null, queue: new Set(), working: false};
_pdfLazy = lazy;
const gone = () => lazy.stopped || _pdfRenderGen !== myGen;
async function draw(e) {
const page = await pdf.getPage(e.num);
if (gone() || !e.near) return;
const viewport = page.getViewport({scale: scaleFor(page.getViewport({scale: 1}))});
e.canvas.style.width = viewport.width + 'px';
e.canvas.style.height = viewport.height + 'px';
e.canvas.width = Math.round(viewport.width * dpr);
e.canvas.height = Math.round(viewport.height * dpr);
const ctx = e.canvas.getContext('2d');
ctx.scale(dpr, dpr);
e.task = page.render({canvasContext: ctx, viewport});
try {
await e.task.promise;
e.drawn = true;
} catch (err) { /* cancelled: let go below */ }
e.task = null;
}
function letGo(e) {
if (e.task) { try { e.task.cancel(); } catch (err) {} }
if (!e.drawn) return;
e.drawn = false;
e.canvas.width = 0; // frees the pixels; the CSS size keeps the page's place
e.canvas.height = 0;
}
// One page at a time, the nearest to the current one first.
async function work() {
if (lazy.working) return;
lazy.working = true;
try {
while (!gone() && lazy.queue.size) {
const cur = typeof _currentScrollPage === 'function' && !readerSettings.pdfPaginated
? _currentScrollPage(contentEl) : (pdfCurrentPage || pivot);
const next = Array.from(lazy.queue).sort((a, b) => Math.abs(a.num - cur) - Math.abs(b.num - cur))[0];
lazy.queue.delete(next);
if (next.near && !next.drawn) await draw(next);
}
} finally {
lazy.working = false;
}
}
const byEl = new Map(entries.map(e => [e.wrapper, e]));
if (typeof IntersectionObserver === 'function') {
lazy.observer = new IntersectionObserver(items => {
for (const it of items) {
const e = byEl.get(it.target);
if (!e) continue;
e.near = it.isIntersecting;
if (e.near) { if (!e.drawn) lazy.queue.add(e); }
else { lazy.queue.delete(e); letGo(e); }
}
work();
}, {root: contentEl, rootMargin: '150% 0px'});
entries.forEach(e => lazy.observer.observe(e.wrapper));
} else {
// Very old WebViews: every page, from the pivot on (as before).
entries.forEach(e => { e.near = true; lazy.queue.add(e); });
work();
}
// The book shows as soon as the page it opens at is drawn.
const pe = entries[pivot - 1];
pe.near = true;
if (!pe.drawn) await draw(pe);
if (gone()) return null;
return {title: pdfTitle, author: pdfAuthor, toc, numPages: pdf.numPages};
}