'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 table of contents: all entries resolved at once (one after the other cost a round trip // to the worker each). A destination's page is a reference or, in some files, a plain index. async function _parsePdfOutline(pdf, items, depth) { depth = depth || 0; const parts = await Promise.all(items.map(async item => { let href = ''; if (item.dest) { try { const dest = typeof item.dest === 'string' ? await pdf.getDestination(item.dest) : item.dest; if (dest && dest[0] != null) { const index = typeof dest[0] === 'number' ? dest[0] : await pdf.getPageIndex(dest[0]); href = `#pdf-page-${index + 1}`; } } catch (e) {} } 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; 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. */ async function _pdfDraw(e, page, cssScale) { const token = ++e.token; 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; 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++; if (e.task) { try { e.task.cancel(); } catch (err) {} } 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) { _pdfLoadingTask = _pdfLoad(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 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}; _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 {title: pdfTitle, author: pdfAuthor, toc, numPages: pdf.numPages}; } 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 {title: pdfTitle, author: pdfAuthor, toc, numPages: pdf.numPages}; }