'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}; }