673 lines
26 KiB
JavaScript
673 lines
26 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);
|
|
}
|
|
|
|
// Opening at the saved page is no "scrolled far" (44-books.js).
|
|
if (pivot > 1) {
|
|
if (typeof _suppressScrollJumpDetect === 'function') _suppressScrollJumpDetect();
|
|
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};
|
|
}
|