213 lines
7.1 KiB
JavaScript
213 lines
7.1 KiB
JavaScript
|
|
'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};
|
||
|
|
}
|