diff --git a/static/js/app.js b/static/js/app.js index 868dd26..3c8465f 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3010,7 +3010,7 @@ async function _parsePdfOutline(pdf, items, depth) { return result; } -async function renderPdf(arrayBuffer, contentEl, scaleOverride) { +async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) { const myGen = ++_pdfRenderGen; const pdf = currentPdfDoc || await pdfjsLib.getDocument({data: new Uint8Array(arrayBuffer.slice(0))}).promise; if (_pdfRenderGen !== myGen) return null; @@ -3031,7 +3031,6 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride) { contentEl.innerHTML = ''; - // Viewport wrapper: CSS zoom controls display scale without re-rendering const pdfVp = document.createElement('div'); pdfVp.id = 'pdf-viewport'; contentEl.appendChild(pdfVp); @@ -3039,15 +3038,17 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride) { 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; + // Pass 1: build DOM structure and size all canvases (no pixel rendering yet) + const pageEntries = []; let spreadContainer = null; for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { if (_pdfRenderGen !== myGen) { contentEl.innerHTML = ''; return null; } const page = await pdf.getPage(pageNum); const naturalVp = page.getViewport({scale: 1}); - const pageWidth = readerSettings.pdfSpread ? (containerWidth - 8) / 2 : containerWidth; - // Zoom baked into canvas resolution — no CSS zoom, stays sharp at any DPR const scale = scaleOverride != null ? scaleOverride : Math.max(0.5, pageWidth / naturalVp.width) * (readerSettings.pdfZoom / 100); const viewport = page.getViewport({scale}); @@ -3056,12 +3057,9 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride) { wrapper.className = 'pdf-page-wrapper'; wrapper.id = `pdf-page-${pageNum}`; - // Inner container gives canvas + text layer a shared position:relative origin, - // independent of the outer flex wrapper's centering. const inner = document.createElement('div'); inner.className = 'pdf-page-inner'; - const dpr = window.devicePixelRatio || 1; const canvas = document.createElement('canvas'); canvas.className = 'pdf-page'; canvas.width = Math.round(viewport.width * dpr); @@ -3087,14 +3085,29 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride) { else pdfVp.appendChild(wrapper); } - const ctx = canvas.getContext('2d'); - ctx.scale(dpr, dpr); - await page.render({canvasContext: ctx, viewport}).promise; - - // Text layer disabled — re-enable once overlay rendering is resolved + pageEntries.push({page, canvas, viewport}); } pdfTotalPages = pdf.numPages; + + // Scroll to pivot page immediately — DOM and canvas sizes are set + 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'}); + + // Pass 2: render pixels from pivot outward (pivot→end, then pivot-1→start) + const order = []; + for (let i = pivot - 1; i < pdf.numPages; i++) order.push(i); + for (let i = pivot - 2; i >= 0; i--) order.push(i); + + for (const idx of order) { + if (_pdfRenderGen !== myGen) { contentEl.innerHTML = ''; return null; } + const {page, canvas, viewport} = pageEntries[idx]; + const ctx = canvas.getContext('2d'); + ctx.scale(dpr, dpr); + await page.render({canvasContext: ctx, viewport}).promise; + } + return {title: pdfTitle, author: pdfAuthor, toc, numPages: pdf.numPages}; } @@ -3698,10 +3711,6 @@ function toggleSettingsPanel() { } async function applyPdfZoom(newZoom) { - const contentEl2 = $('reader-content'); - const fraction = contentEl2 - ? contentEl2.scrollTop / (contentEl2.scrollHeight - contentEl2.clientHeight || 1) - : 0; readerSettings.pdfZoom = newZoom; const zoomRange = document.getElementById('rs-zoom'); const zoomVal = document.getElementById('rs-zoom-val'); @@ -3712,25 +3721,32 @@ async function applyPdfZoom(newZoom) { pdfSmartZoomPage(pdfCurrentPage); } else { await reRenderPdf(); - // Wait for two animation frames so the browser finishes layout - // before reading scrollHeight for position restoration - await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); - if (contentEl2 && fraction > 0) { - contentEl2.scrollTop = fraction * (contentEl2.scrollHeight - contentEl2.clientHeight); + } +} + +function _currentScrollPage(contentEl) { + const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper'); + const viewTop = contentEl.scrollTop; + for (const w of wrappers) { + if (w.offsetTop + w.offsetHeight > viewTop) { + const n = parseInt(w.id.replace('pdf-page-', ''), 10); + if (n) return n; } } + return pdfCurrentPage || 1; } async function reRenderPdf() { if (!currentPdfBuffer) return; const contentEl = $('reader-content'); if (!contentEl) return; + const pivot = readerSettings.pdfPaginated ? pdfCurrentPage : _currentScrollPage(contentEl); const overlay = $('reader-overlay'); const loadingEl = document.createElement('div'); loadingEl.className = 'pdf-loading-overlay'; loadingEl.innerHTML = ''; if (overlay) overlay.appendChild(loadingEl); - await renderPdf(currentPdfBuffer, contentEl); + await renderPdf(currentPdfBuffer, contentEl, undefined, pivot); loadingEl.remove(); if (readerSettings.pdfPaginated) enterPdfPaginatedMode(); }