diora-web/static/js/app/58-pdf.js
mrwnslz 429b875faf
Some checks failed
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Successful in 11m30s
Test / e2e (push) Failing after 1m30s
PDF: öffnet an der gespeicherten Seite, Leserfarben, Seitenzahlen wie im Buch (sw CACHE v166)
Der Lesestand wird gleich beim Öffnen erfragt (parallel zur Datei), ein PDF zeichnet zuerst
die Seite, an der es weitergeht, statt Seite 1; Titel, Inhaltsverzeichnis und Seitenzahlen
kommen nebenher und halten das Öffnen nicht mehr auf. Eine Zeichnung, die schon läuft, wird
abgewartet statt neu begonnen.

„Leserfarben“ (neben Originalfarben und Invertieren): die Seite in den Farben des Lesers
(Dunkel, Sepia) per SVG-Filter, Bilder darüber wie gedruckt (ihre Lage aus den
Zeichenbefehlen); ein Bild, das die halbe Seite füllt, ist ein Scan und wird mit umgefärbt.
pdf.js' eigene pageColors färben die Bilder mit um. Ein anderes Design ändert nur die Farben
des Filters.

Seitenzahlen des Buchs (PageLabels: „iv“, „A-3“) im Kopf mit der Seite der Datei daneben,
eingeben lässt sich beides; Lesezeichen und der Zurück-Knopf nennen sie. Das Test-PDF hat
dafür Seitenzahlen und ein farbiges Bild bekommen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 18:42:26 +01:00

669 lines
25 KiB
JavaScript

'use strict';
// ---------------------------------------------------------------------------
// The PDF reader: one document, shown either scrolling (every page has its
// place, only those near the view are drawn) or page by page, cropped to the
// page's text (66-pdf-paginated.js). Whichever, a page is drawn only through
// _pdfDraw: one drawing at a time per canvas, sharp at the device's pixel
// ratio but never larger than PDF_MAX_PIXELS.
// ---------------------------------------------------------------------------
// pdf.js's own viewer caps a canvas at this many pixels: beyond it phones run out of memory.
const PDF_MAX_PIXELS = 16777216;
// One pdf.js worker for every document (the reader, covers, uploads): starting one takes a
// moment — on an e-reader a long one.
let _pdfWorker = null;
function _pdfSharedWorker() {
if (!_pdfWorker && typeof pdfjsLib !== 'undefined' && pdfjsLib.PDFWorker) {
try { _pdfWorker = new pdfjsLib.PDFWorker(); } catch (e) { _pdfWorker = null; }
}
return _pdfWorker;
}
/** pdf.js's loading task for [bytes] (a Uint8Array), on the shared worker. */
function _pdfLoad(bytes) {
const worker = _pdfSharedWorker();
return pdfjsLib.getDocument(worker ? {data: bytes, worker} : {data: bytes});
}
/**
* The page (from 1) a destination leads to — a named one or an explicit array, whose page is
* a reference or, in some files, a plain index. 0 when it leads nowhere.
*/
async function _pdfDestPage(pdf, dest) {
try {
if (typeof dest === 'string') dest = await pdf.getDestination(dest);
if (!dest || dest[0] == null) return 0;
return (typeof dest[0] === 'number' ? dest[0] : await pdf.getPageIndex(dest[0])) + 1;
} catch (e) {
return 0;
}
}
// The table of contents: all entries resolved at once (one after the other cost a round trip
// to the worker each).
async function _parsePdfOutline(pdf, items, depth) {
depth = depth || 0;
const parts = await Promise.all(items.map(async item => {
let href = '';
if (item.dest) {
const n = await _pdfDestPage(pdf, item.dest);
if (n) href = `#pdf-page-${n}`;
}
const kids = item.items && item.items.length ? await _parsePdfOutline(pdf, item.items, depth + 1) : [];
return [{label: item.title || '(untitled)', href, depth}].concat(kids);
}));
return [].concat.apply([], parts);
}
// The document being loaded (pdf.js loading task), so opening can be cancelled.
let _pdfLoadingTask = null;
// The open document's pages and how they are drawn (renderPdf; stopped on the next one).
let _pdfView = null;
// Backwards name: the reader stops "the lazy drawing" when a book closes.
let _pdfLazy = null;
// The open document's text, page by page (pdf.js text content): for the text layer, the
// search and the crop to the text box. Kept for the document, not for one layout of it.
let _pdfTexts = null;
function _pdfPageText(pdf, num) {
if (!_pdfTexts || _pdfTexts.pdf !== pdf) _pdfTexts = {pdf, pages: new Map()};
let p = _pdfTexts.pages.get(num);
if (!p) {
p = pdf.getPage(num).then(page => page.getTextContent()).catch(() => ({items: [], styles: {}}));
_pdfTexts.pages.set(num, p);
}
return p;
}
/**
* A page's text as one string, lower case, for searching — with where each item (each span of
* the text layer, in order) starts in it. Lines ending inside it get a space.
*/
function _pdfPageString(tc) {
if (tc._diora) return tc._diora;
let str = '';
const starts = [];
for (const item of tc.items) {
starts.push(str.length);
str += item.str || '';
if (item.hasEOL) str += ' ';
}
tc._diora = {str: str.toLowerCase(), starts};
return tc._diora;
}
// Title, table of contents and page labels: asked for once per document, alongside the
// pages (opening doesn't wait for them; a layout anew reuses them).
let _pdfExtras = null;
function _pdfDocExtras(pdf) {
if (_pdfExtras && _pdfExtras.pdf === pdf) return _pdfExtras;
const meta = pdf.getMetadata().then(m => ({
title: (m && m.info && m.info.Title || '').trim(),
author: (m && m.info && m.info.Author || '').trim(),
}), () => ({title: '', author: ''}));
const toc = pdf.getOutline().then(o => (o && o.length ? _parsePdfOutline(pdf, o) : []), () => []);
const labels = pdf.getPageLabels().then(l => {
// Labels that only say 1, 2, 3 … are no labels.
if (!l || !l.length || l.every((s, i) => !s || s === String(i + 1))) return null;
return l.map((s, i) => s || String(i + 1));
}, () => null);
_pdfExtras = {pdf, meta, toc, labels};
return _pdfExtras;
}
// The page labels of the open document (front matter "iv", appendix "A-3" …), null without.
let _pdfLabels = null;
/** Page [n] (from 1) as the book numbers it. */
function pdfPageLabel(n) {
return (_pdfLabels && _pdfLabels[n - 1]) || String(n);
}
/** The page meant by [value] typed in: a label of the book first, else a page number. 0: none. */
function _pdfPageFromInput(value) {
const v = String(value || '').trim();
if (!v) return 0;
if (_pdfLabels) {
let i = _pdfLabels.indexOf(v);
if (i < 0) i = _pdfLabels.findIndex(l => l.toLowerCase() === v.toLowerCase());
if (i >= 0) return i + 1;
}
return parseInt(v, 10) || 0;
}
/**
* The pages in the reader's colours (Leserfarben): {background, foreground} as #rrggbb, the
* reader's (Dunkel, Sepia …). null: as printed (also when the reader is black on white).
*/
function _pdfPageColors() {
if (!readerSettings.pdfPageColors || readerSettings.pdfInverted) return null;
const contentEl = $('reader-content');
if (!contentEl) return null;
let bg = null;
for (let el = contentEl; el && !bg; el = el.parentElement) bg = _pdfCssHex(getComputedStyle(el).backgroundColor);
const fg = _pdfCssHex(getComputedStyle(contentEl).color);
// On white the page is as printed anyway (Hell).
if (!bg || !fg || bg === '#ffffff') return null;
return {background: bg, foreground: fg};
}
/** "rgb(…)" / "rgba(…)" as #rrggbb; null for transparent. */
function _pdfCssHex(css) {
const m = /rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?/.exec(css || '');
if (!m || (m[4] !== undefined && parseFloat(m[4]) === 0)) return null;
return '#' + [m[1], m[2], m[3]].map(x => (+x).toString(16).padStart(2, '0')).join('');
}
// Whether the pages are drawn for Leserfarben (with their pictures laid over them, below):
// switching that draws them anew; another theme only changes the filter's colours.
function _pdfColorsKey() {
return _pdfPageColors() ? 'reader' : '';
}
/**
* Leserfarben: the page is drawn as printed and recoloured by an SVG filter on its canvas —
* its brightness from the reader's text colour (black) to its background (white). (pdf.js's
* own pageColors recolours the pictures too, and lets nothing be left out.) Kept up to date
* with the colours; true when it applies.
*/
function _pdfColorFilter() {
const c = _pdfPageColors();
if (!c) return false;
const ns = 'http://www.w3.org/2000/svg';
let svg = document.getElementById('pdf-reader-colors-svg');
if (!svg) {
svg = document.createElementNS(ns, 'svg');
svg.id = 'pdf-reader-colors-svg';
svg.setAttribute('width', '0');
svg.setAttribute('height', '0');
svg.setAttribute('aria-hidden', 'true');
svg.style.position = 'absolute';
svg.innerHTML = '<filter id="pdf-reader-colors" color-interpolation-filters="sRGB">' +
'<feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 ' +
'0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/>' +
'<feComponentTransfer><feFuncR type="table"/><feFuncG type="table"/><feFuncB type="table"/>' +
'</feComponentTransfer></filter>';
document.body.appendChild(svg);
}
const ch = (hex, i) => (parseInt(hex.substr(1 + i * 2, 2), 16) / 255).toFixed(3);
['R', 'G', 'B'].forEach((name, i) => {
svg.querySelector('feFunc' + name).setAttribute('tableValues', `${ch(c.foreground, i)} ${ch(c.background, i)}`);
});
return true;
}
/**
* Where page [page]'s pictures are, in CSS pixels of [viewport]: from its drawing operations
* (the unit square of each image under the transform at that point). Image masks are left
* out (they're drawn in a colour, like text).
*/
async function _pdfImageRects(page, viewport) {
const OPS = pdfjsLib.OPS;
let list;
try {
list = await page.getOperatorList({annotationMode: pdfjsLib.AnnotationMode.ENABLE_FORMS});
} catch (e) {
return [];
}
const mul = (m, n) => [m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1],
m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3],
m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5]];
let ctm = viewport.transform.slice();
const stack = [];
const rects = [];
for (let i = 0; i < list.fnArray.length; i++) {
const fn = list.fnArray[i], args = list.argsArray[i];
if (fn === OPS.save) stack.push(ctm);
else if (fn === OPS.restore) ctm = stack.pop() || ctm;
else if (fn === OPS.transform) ctm = mul(ctm, args);
else if (fn === OPS.paintFormXObjectBegin) {
stack.push(ctm);
if (Array.isArray(args[0]) && args[0].length === 6) ctm = mul(ctm, args[0]);
} else if (fn === OPS.paintFormXObjectEnd) ctm = stack.pop() || ctm;
else if (fn === OPS.paintImageXObject || fn === OPS.paintInlineImageXObject) {
const xs = [ctm[4], ctm[0] + ctm[4], ctm[2] + ctm[4], ctm[0] + ctm[2] + ctm[4]];
const ys = [ctm[5], ctm[1] + ctm[5], ctm[3] + ctm[5], ctm[1] + ctm[3] + ctm[5]];
const x = Math.max(0, Math.min.apply(null, xs)), y = Math.max(0, Math.min.apply(null, ys));
const w = Math.min(viewport.width, Math.max.apply(null, xs)) - x;
const h = Math.min(viewport.height, Math.max.apply(null, ys)) - y;
if (w > 2 && h > 2) rects.push({x, y, w, h});
}
}
return rects;
}
// A picture covering this much of the page is the page itself (a scan): recoloured with it.
const PDF_SCAN_SHARE = 0.5;
/** Leserfarben: page [e]'s pictures laid over it as printed (copied from its canvas). */
async function _pdfImageLayer(e, page, viewport, ratio, token) {
const rects = await _pdfImageRects(page, viewport);
if (e.token !== token) return;
const area = viewport.width * viewport.height;
const shown = rects.filter(r => r.w * r.h < area * PDF_SCAN_SHARE);
if (!shown.length) return;
const layer = document.createElement('div');
layer.className = 'pdf-image-layer';
for (const r of shown) {
const c = document.createElement('canvas');
const sx = Math.floor(r.x * ratio), sy = Math.floor(r.y * ratio);
const sw = Math.min(e.canvas.width - sx, Math.ceil(r.w * ratio)), sh = Math.min(e.canvas.height - sy, Math.ceil(r.h * ratio));
if (sw <= 0 || sh <= 0) continue;
c.width = sw;
c.height = sh;
c.getContext('2d').drawImage(e.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
c.style.left = sx / ratio + 'px';
c.style.top = sy / ratio + 'px';
c.style.width = sw / ratio + 'px';
c.style.height = sh / ratio + 'px';
layer.appendChild(c);
}
e.inner.insertBefore(layer, e.canvas.nextSibling);
}
// Internal links followed, the pages they were followed from (the back button).
let _pdfBackStack = [];
/** The document is gone (another one, or the reader closed): what belonged to it goes too. */
function _pdfForgetDoc() {
_pdfTexts = null;
_pdfExtras = null;
_pdfLabels = null;
_pdfBackStack = [];
_pdfShowBack();
if (typeof _pdfSearchStop === 'function') _pdfSearchStop();
}
async function _pdfFollowDest(dest) {
const pdf = currentPdfDoc;
if (!pdf) return;
const n = await _pdfDestPage(pdf, dest);
if (!n || pdf !== currentPdfDoc) return;
const contentEl = $('reader-content');
const from = readerSettings.pdfPaginated || !contentEl ? pdfCurrentPage : _pdfTopPage(contentEl);
if (n === from) return;
_pdfBackStack.push(from);
pdfGoToPage(n);
_pdfShowBack();
}
/** The button back to where the last link was followed from (gone when there's none). */
function _pdfShowBack() {
let btn = document.getElementById('pdf-back-btn');
const from = _pdfBackStack[_pdfBackStack.length - 1];
if (!from) {
if (btn) btn.remove();
return;
}
if (!btn) {
const overlay = $('reader-overlay');
if (!overlay) return;
btn = document.createElement('button');
btn.id = 'pdf-back-btn';
btn.type = 'button';
btn.className = 'btn btn-sm pdf-back-btn';
btn.addEventListener('click', ev => {
ev.stopPropagation();
const page = _pdfBackStack.pop();
if (page) pdfGoToPage(page);
_pdfShowBack();
});
overlay.appendChild(btn);
}
btn.textContent = `← Zurück zu S. ${pdfPageLabel(from)}`;
}
/** A page's text layer and links taken off (another scale, or the page let go). */
function _pdfDropLayers(e) {
if (e.textTask) { try { e.textTask.cancel(); } catch (err) {} }
e.textTask = null;
e.textDivs = null;
e.textInfo = null;
e.marked = null;
e.inner.querySelectorAll('.pdf-text-layer, .pdf-link-layer, .pdf-image-layer').forEach(el => el.remove());
}
/**
* Over a drawn page: its text (transparent, to select and copy, and where the search marks
* go) and its links. After the pixels, so a turn isn't held up by them.
*/
async function _pdfLayers(e, page, viewport, token) {
const pdf = currentPdfDoc;
if (!pdf) return;
let annots = [];
try { annots = await page.getAnnotations({intent: 'display'}); } catch (err) {}
if (e.token !== token) return;
_pdfLinks(e, viewport, annots);
if (typeof pdfjsLib.renderTextLayer !== 'function') return;
const text = await _pdfPageText(pdf, e.num);
if (e.token !== token || !text.items.length) return;
const layer = document.createElement('div');
layer.className = 'pdf-text-layer';
layer.style.setProperty('--scale-factor', viewport.scale);
e.inner.appendChild(layer);
const divs = [];
const task = pdfjsLib.renderTextLayer({textContentSource: text, container: layer, viewport, textDivs: divs});
e.textTask = task;
try {
await task.promise;
} catch (err) {
return;
}
if (e.token !== token || e.textTask !== task) return;
e.textTask = null;
e.textDivs = divs;
e.textInfo = {items: text.items, string: _pdfPageString(text)};
if (typeof _pdfSearchMarkPage === 'function') _pdfSearchMarkPage(e);
}
// Links: ones into the document go there (and back, _pdfShowBack); web addresses open outside.
function _pdfLinks(e, viewport, annots) {
const links = annots.filter(a => a.subtype === 'Link' && (a.url || a.dest));
if (!links.length) return;
const layer = document.createElement('div');
layer.className = 'pdf-link-layer';
for (const a of links) {
const r = viewport.convertToViewportRectangle(a.rect);
const link = document.createElement('a');
link.className = 'pdf-link';
link.style.left = Math.min(r[0], r[2]) + 'px';
link.style.top = Math.min(r[1], r[3]) + 'px';
link.style.width = Math.abs(r[2] - r[0]) + 'px';
link.style.height = Math.abs(r[3] - r[1]) + 'px';
if (a.url) {
link.href = a.url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.title = a.url;
} else {
const dest = a.dest;
link.href = '#';
link.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
_pdfFollowDest(dest);
});
}
layer.appendChild(link);
}
e.inner.appendChild(layer);
}
function _pdfStopLazy() {
const v = _pdfView;
if (!v) return;
v.stopped = true;
if (v.observer) v.observer.disconnect();
if (v.resize) v.resize.disconnect();
clearTimeout(v.resizeTimer);
v.entries.forEach(e => { e.token++; if (e.task) { try { e.task.cancel(); } catch (err) {} } });
_pdfView = _pdfLazy = null;
}
/**
* Draw page [page] of entry [e] at [cssScale] (CSS pixels per PDF point). Cancels a drawing
* of the same canvas still under way (pdf.js refuses two at once) and waits for it to let go.
* Returns false when a newer drawing (or letting go) came first.
*/
function _pdfDraw(e, page, cssScale) {
// The same drawing asked for again while it's under way (the page shown, then laid out
// again as the bars go): that one, not a new one cancelling it.
if (e.drawing && e.drawing.scale === cssScale) return e.drawing.promise;
const drawing = {scale: cssScale, promise: null};
drawing.promise = _pdfDrawNow(e, page, cssScale).then(ok => {
if (e.drawing === drawing) e.drawing = null;
return ok;
}, err => {
if (e.drawing === drawing) e.drawing = null;
throw err;
});
e.drawing = drawing;
return drawing.promise;
}
async function _pdfDrawNow(e, page, cssScale) {
const token = ++e.token;
_pdfDropLayers(e);
const prev = e.task;
if (prev) {
try { prev.cancel(); } catch (err) {}
try { await prev.promise; } catch (err) {}
if (e.token !== token) return false;
}
const viewport = page.getViewport({scale: cssScale});
let ratio = window.devicePixelRatio || 1;
const area = viewport.width * viewport.height;
if (area * ratio * ratio > PDF_MAX_PIXELS) ratio = Math.max(0.5, Math.sqrt(PDF_MAX_PIXELS / area));
e.canvas.width = Math.round(viewport.width * ratio);
e.canvas.height = Math.round(viewport.height * ratio);
e.canvas.style.width = viewport.width + 'px';
e.canvas.style.height = viewport.height + 'px';
const task = page.render({
canvasContext: e.canvas.getContext('2d'),
viewport,
transform: ratio !== 1 ? [ratio, 0, 0, ratio, 0, 0] : null,
});
e.task = task;
try {
await task.promise;
} catch (err) {
if (e.task === task) e.task = null;
if (err && err.name !== 'RenderingCancelledException') console.warn('PDF page', e.num, err);
return false;
}
if (e.task === task) e.task = null;
if (e.token !== token) return false;
e.drawn = cssScale;
const recolor = _pdfColorFilter();
e.canvas.classList.toggle('pdf-recolored', recolor);
if (recolor) _pdfImageLayer(e, page, viewport, ratio, token).catch(err => console.warn('PDF page pictures', e.num, err));
_pdfLayers(e, page, viewport, token).catch(err => console.warn('PDF page layers', e.num, err));
return true;
}
/** Let a page's pixels go (its place stays) and pdf.js forget what it parsed of it. */
function _pdfLetGo(e, pdf) {
e.token++;
e.drawing = null;
if (e.task) { try { e.task.cancel(); } catch (err) {} }
_pdfDropLayers(e);
e.drawn = 0;
e.canvas.width = 0;
e.canvas.height = 0;
if (pdf) pdf.getPage(e.num).then(p => { if (!e.task) p.cleanup(); }, () => {});
}
/** The page shown at the top of the scrolling view (binary search, the pages are in order). */
function _pdfTopPage(contentEl) {
const v = _pdfView;
if (!v || !v.entries.length) return pdfCurrentPage || 1;
const top = contentEl.getBoundingClientRect().top + 20;
let lo = 0, hi = v.entries.length - 1;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (v.entries[mid].wrapper.getBoundingClientRect().bottom > top) hi = mid; else lo = mid + 1;
}
return v.entries[lo].num;
}
// 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. Page by page (pdfPaginated),
// 66-pdf-paginated.js shows and draws the pages itself.
async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
const myGen = ++_pdfRenderGen;
_pdfStopLazy();
let pdf = currentPdfDoc;
if (!pdf) {
_pdfForgetDoc();
_pdfLoadingTask = _pdfLoad(new Uint8Array(arrayBuffer.slice(0)));
try {
pdf = await _pdfLoadingTask.promise;
} finally {
_pdfLoadingTask = null;
}
}
if (_pdfRenderGen !== myGen) return null;
currentPdfDoc = pdf;
// [pivotPage] may be given as what of the document it is (the place saved, a fraction).
if (typeof pivotPage === 'function') pivotPage = pivotPage(pdf.numPages);
const extras = _pdfDocExtras(pdf);
extras.labels.then(labels => {
if (pdf !== currentPdfDoc) return;
_pdfLabels = labels;
if (labels) _pdfSetCurrentPage(pdfCurrentPage);
});
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 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 || readerSettings.pdfPaginated) {
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, inner, canvas, drawn: 0, near: false, task: null, token: 0});
}
pdfTotalPages = pdf.numPages;
const pivot = Math.max(1, Math.min(pdf.numPages, pivotPage || 1));
const view = {pdf, contentEl, entries, gen: myGen, stopped: false, observer: null, resize: null,
resizeTimer: 0, queue: new Set(), working: false, scaleFor, showToken: 0,
colorsKey: _pdfColorsKey()};
_pdfView = _pdfLazy = view;
const gone = () => view.stopped || _pdfRenderGen !== myGen;
async function draw(e) {
const page = await pdf.getPage(e.num);
if (gone() || !e.near || readerSettings.pdfPaginated) return;
await _pdfDraw(e, page, scaleFor(page.getViewport({scale: 1})));
}
// One page at a time, the nearest to the current one first.
async function work() {
if (view.working) return;
view.working = true;
try {
while (!gone() && view.queue.size && !readerSettings.pdfPaginated) {
const cur = _pdfTopPage(contentEl);
const next = Array.from(view.queue).sort((a, b) => Math.abs(a.num - cur) - Math.abs(b.num - cur))[0];
view.queue.delete(next);
if (next.near && !next.drawn) await draw(next);
}
} finally {
view.working = false;
}
}
// Turning the device, a narrower window, the bars coming or going: laid out again at the same page.
let lastW = contentEl.clientWidth, lastH = contentEl.clientHeight;
if (typeof ResizeObserver === 'function') {
view.resize = new ResizeObserver(() => {
const w = contentEl.clientWidth, h = contentEl.clientHeight;
if (!w || !h || (w === lastW && h === lastH)) return;
const widthChanged = Math.abs(w - lastW) > 2;
lastW = w;
lastH = h;
clearTimeout(view.resizeTimer);
view.resizeTimer = setTimeout(() => {
if (gone()) return;
if (readerSettings.pdfPaginated) _pdfShowPage(pdfCurrentPage);
else if (widthChanged) reRenderPdf(_pdfTopPage(contentEl));
}, readerSettings.pdfPaginated ? 150 : 300);
});
view.resize.observe(contentEl);
}
if (readerSettings.pdfPaginated) {
// Page by page: the pivot page cropped to its text, at once (66-pdf-paginated.js).
pdfCurrentPage = pivot;
await _pdfShowPage(pivot);
if (gone()) return null;
return _pdfResult(pdf, extras);
}
if (pivot > 1) entries[pivot - 1].wrapper.scrollIntoView({block: 'start'});
pdfCurrentPage = pivot;
const byEl = new Map(entries.map(e => [e.wrapper, e]));
if (typeof IntersectionObserver === 'function') {
view.observer = new IntersectionObserver(items => {
if (readerSettings.pdfPaginated) return;
for (const it of items) {
const e = byEl.get(it.target);
if (!e) continue;
e.near = it.isIntersecting;
if (e.near) { if (!e.drawn) view.queue.add(e); }
else { view.queue.delete(e); if (e.drawn || e.task) _pdfLetGo(e, pdf); }
}
work();
}, {root: contentEl, rootMargin: '150% 0px'});
entries.forEach(e => view.observer.observe(e.wrapper));
} else {
// Very old WebViews: every page, from the pivot on (as before).
entries.forEach(e => { e.near = true; view.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 _pdfResult(pdf, extras);
}
// What openBook needs of the document: the title (asked for while the page was drawn) and
// the table of contents to come (a long one resolves its pages after the book is shown).
async function _pdfResult(pdf, extras) {
const meta = await extras.meta;
return {title: meta.title, author: meta.author, toc: [], tocLater: extras.toc, numPages: pdf.numPages};
}