diff --git a/static/css/app.css b/static/css/app.css index f0af76c..556c98d 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1637,6 +1637,9 @@ body.dnd-mode .timer-display { } .reader-settings-panel input[type="range"] { width:80px; } +/* PDF viewport wrapper — CSS zoom applied here for smooth zoom without re-render */ +#pdf-viewport { transform-origin: top left; } + /* PDF inner container — shares origin with canvas so text layer aligns exactly */ .pdf-page-inner { position:relative; display:inline-block; line-height:0; } diff --git a/static/js/app.js b/static/js/app.js index 3a6e212..f36bce8 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2594,6 +2594,7 @@ let readerSearchOpen = false; let pdfCurrentPage = 1; let pdfTotalPages = 0; let _pdfPageTextBoxCache = {}; +let _pdfRenderGen = 0; let _touchStartX = 0; if (typeof pdfjsLib !== 'undefined') { @@ -2836,7 +2837,9 @@ async function _parsePdfOutline(pdf, items, depth) { } async function renderPdf(arrayBuffer, contentEl, scaleOverride) { + const myGen = ++_pdfRenderGen; const pdf = currentPdfDoc || await pdfjsLib.getDocument({data: new Uint8Array(arrayBuffer.slice(0))}).promise; + if (_pdfRenderGen !== myGen) return null; currentPdfDoc = pdf; let pdfTitle = '', pdfAuthor = ''; @@ -2854,13 +2857,20 @@ 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'; + if (scaleOverride == null) pdfVp.style.zoom = readerSettings.pdfZoom / 100; + contentEl.appendChild(pdfVp); + const containerWidth = Math.min(contentEl.clientWidth - 32, 900); 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 scale = scaleOverride != null ? scaleOverride - : Math.max(0.5, (containerWidth / naturalVp.width) * (readerSettings.pdfZoom / 100)); + : Math.max(0.5, containerWidth / naturalVp.width); const viewport = page.getViewport({scale}); const wrapper = document.createElement('div'); @@ -2878,7 +2888,7 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride) { canvas.height = viewport.height; inner.appendChild(canvas); wrapper.appendChild(inner); - contentEl.appendChild(wrapper); + pdfVp.appendChild(wrapper); await page.render({canvasContext: canvas.getContext('2d'), viewport}).promise; @@ -3424,11 +3434,16 @@ function toggleSettingsPanel() { } else { const zoomRange = panel.querySelector('#rs-zoom'); const zoomVal = panel.querySelector('#rs-zoom-val'); - zoomRange.addEventListener('change', () => { + zoomRange.addEventListener('input', () => { readerSettings.pdfZoom = parseInt(zoomRange.value, 10); zoomVal.textContent = readerSettings.pdfZoom + '%'; saveReaderSettings(); - reRenderPdf(); + if (readerSettings.pdfPaginated) { + pdfSmartZoomPage(pdfCurrentPage); + } else { + const vp = document.getElementById('pdf-viewport'); + if (vp) vp.style.zoom = readerSettings.pdfZoom / 100; + } }); panel.querySelector('#rs-invert').addEventListener('click', function () { @@ -3469,6 +3484,8 @@ function enterPdfPaginatedMode() { if (!contentEl) return; contentEl.classList.add('pdf-paginated'); contentEl.style.overflow = 'hidden'; + const pdfVp = document.getElementById('pdf-viewport'); + if (pdfVp) pdfVp.style.zoom = 1; const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper'); wrappers.forEach((w, i) => { @@ -3493,6 +3510,8 @@ function exitPdfPaginatedMode() { const canvas = w.querySelector('canvas'); if (canvas) canvas.style.transform = ''; }); + const pdfVp = document.getElementById('pdf-viewport'); + if (pdfVp) pdfVp.style.zoom = readerSettings.pdfZoom / 100; } function _pdfPaginatedClick(e) {