'use strict'; // --------------------------------------------------------------------------- // PDF Paginated Mode // --------------------------------------------------------------------------- function enterPdfPaginatedMode() { const contentEl = $('reader-content'); 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) => { w.style.display = (i + 1 === pdfCurrentPage) ? '' : 'none'; }); pdfSmartZoomPage(pdfCurrentPage); // Tap left/right to navigate contentEl.addEventListener('click', _pdfPaginatedClick); } function exitPdfPaginatedMode() { const contentEl = $('reader-content'); if (!contentEl) return; contentEl.classList.remove('pdf-paginated'); contentEl.style.overflow = ''; contentEl.removeEventListener('click', _pdfPaginatedClick); reRenderPdf(); } function _pdfPaginatedClick(e) { const w = e.currentTarget.clientWidth; if (e.clientX < w * 0.4) pdfGoToPage(pdfCurrentPage - 1); else if (e.clientX > w * 0.6) pdfGoToPage(pdfCurrentPage + 1); } function _pdfTouchStart(e) { if (e.touches.length === 2) { const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; _pinchStartDist = Math.hypot(dx, dy); _pinchStartZoom = readerSettings.pdfZoom; _isPinching = true; const ce = $('reader-content'); if (ce) ce.classList.add('pinch-active'); } else { _touchStartX = e.touches[0].clientX; _isPinching = false; } } function _pdfTouchMove(e) { if (e.touches.length !== 2 || !_isPinching) return; e.preventDefault(); const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const dist = Math.hypot(dx, dy); if (_pinchStartDist === 0) return; const liveZoom = Math.max(50, Math.min(200, _pinchStartZoom * (dist / _pinchStartDist))); const vp = document.getElementById('pdf-viewport'); if (vp) vp.style.zoom = liveZoom / 100; } function _pdfTouchEnd(e) { if (_isPinching) { _isPinching = false; const ce = $('reader-content'); if (ce) ce.classList.remove('pinch-active'); const vp = document.getElementById('pdf-viewport'); const liveZoom = vp ? parseFloat(vp.style.zoom) * 100 : readerSettings.pdfZoom; const snapped = Math.max(50, Math.min(200, Math.round(liveZoom / 10) * 10)); applyPdfZoom(snapped); return; } if (!readerSettings.pdfPaginated) return; const delta = e.changedTouches[0].clientX - _touchStartX; if (delta > 50) pdfGoToPage(pdfCurrentPage - 1); else if (delta < -50) pdfGoToPage(pdfCurrentPage + 1); } function pdfGoToPage(n) { if (!currentPdfDoc) return; n = Math.max(1, Math.min(pdfTotalPages, n)); if (n === pdfCurrentPage) return; const contentEl = $('reader-content'); if (!contentEl) return; const oldWrapper = contentEl.querySelector(`#pdf-page-${pdfCurrentPage}`); if (oldWrapper) oldWrapper.style.display = 'none'; pdfCurrentPage = n; const newWrapper = contentEl.querySelector(`#pdf-page-${pdfCurrentPage}`); if (newWrapper) newWrapper.style.display = ''; pdfSmartZoomPage(pdfCurrentPage); const progressInput = $('reader-progress-input'); if (progressInput) progressInput.value = pdfCurrentPage; } async function pdfSmartZoomPage(pageNum) { if (!currentPdfDoc) return; const contentEl = $('reader-content'); if (!contentEl) return; const wrapper = contentEl.querySelector(`#pdf-page-${pageNum}`); if (!wrapper) return; const canvas = wrapper.querySelector('canvas'); if (!canvas) return; const page = await currentPdfDoc.getPage(pageNum); const naturalVp = page.getViewport({scale: 1}); const pageW = naturalVp.width; const pageH = naturalVp.height; let bbox = _pdfPageTextBoxCache[pageNum]; if (!bbox) { bbox = await _computePdfTextBox(page, pageW, pageH); _pdfPageTextBoxCache[pageNum] = bbox; } const containerW = contentEl.clientWidth; const containerH = contentEl.clientHeight; const contentW = bbox.x2 - bbox.x1; const contentH = bbox.y2 - bbox.y1; const pad = 12; const scale = Math.min( (containerW - pad * 2) / contentW, (containerH - pad * 2) / contentH ) * (readerSettings.pdfZoom / 100); // Re-render canvas at new scale if significantly different const currentScale = canvas.width / naturalVp.width; if (Math.abs(scale - currentScale) / currentScale > 0.05) { const vp = page.getViewport({scale}); canvas.width = vp.width; canvas.height = vp.height; await page.render({canvasContext: canvas.getContext('2d'), viewport: vp}).promise; } // Position canvas to center the text bounding box const renderedScale = canvas.width / naturalVp.width; const offsetX = -renderedScale * (bbox.x1 - pad) + (containerW - renderedScale * contentW - pad * 2) / 2; // PDF y-axis is bottom-up; canvas is top-down const offsetY = -renderedScale * (pageH - bbox.y2 - pad) + (containerH - renderedScale * contentH - pad * 2) / 2; canvas.style.transform = `translate(${offsetX}px, ${offsetY}px)`; wrapper.style.overflow = 'hidden'; wrapper.style.width = containerW + 'px'; wrapper.style.height = containerH + 'px'; } async function _computePdfTextBox(page, pageW, pageH) { // Tier 1: text-based try { const tc = await page.getTextContent(); if (tc.items && tc.items.length) { let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity; for (const item of tc.items) { if (!item.transform) continue; const tx = item.transform[4], ty = item.transform[5]; const iw = item.width || 0, ih = item.height || 0; if (tx < x1) x1 = tx; if (ty < y1) y1 = ty; if (tx + iw > x2) x2 = tx + iw; if (ty + ih > y2) y2 = ty + ih; } const area = (x2 - x1) * (y2 - y1); if (isFinite(x1) && area > pageW * pageH * 0.25) { return {x1, y1, x2, y2}; } } } catch (e) {} // Tier 2: pixel analysis at scale 0.3 try { const lowScale = 0.3; const vp = page.getViewport({scale: lowScale}); const offCanvas = document.createElement('canvas'); offCanvas.width = vp.width; offCanvas.height = vp.height; const ctx = offCanvas.getContext('2d'); await page.render({canvasContext: ctx, viewport: vp}).promise; const {data, width, height} = ctx.getImageData(0, 0, vp.width, vp.height); let rMin = height, rMax = 0, cMin = width, cMax = 0; for (let r = 0; r < height; r++) { for (let c = 0; c < width; c++) { const idx = (r * width + c) * 4; if (data[idx] + data[idx+1] + data[idx+2] < 720) { if (r < rMin) rMin = r; if (r > rMax) rMax = r; if (c < cMin) cMin = c; if (c > cMax) cMax = c; } } } if (rMin < rMax && cMin < cMax) { return { x1: cMin / lowScale, y1: (height - rMax) / lowScale, x2: cMax / lowScale, y2: (height - rMin) / lowScale, }; } } catch (e) {} // Fallback: full page return {x1: 0, y1: 0, x2: pageW, y2: pageH}; }