/** * SegmentManager.js * 长画布分段管理模块 * 负责PDF连续模式的分段渲染和懒加载 */ class SegmentManager { constructor(pdfDoc, options = {}) { this.pdfDoc = pdfDoc; this.totalPages = pdfDoc.numPages; this.scale = 1.0; this.dpr = window.devicePixelRatio || 1; // 配置选项 this.options = Object.assign({ maxSegmentPixels: null, // 自动根据DPR选择 bufferRatio: 0.5, scrollDebounceMs: 80, bboxNormalizedRange: 1000 }, options); // 段数据 this.segments = []; this.pageInfos = []; this.mode = 'continuous'; // 容器引用 this.originalSegmentsContainer = null; this.translationSegmentsContainer = null; this.originalScroll = null; this.translationScroll = null; // 懒加载状态 this._lazyScrollTimer = null; this._lazyInitialized = false; this._renderingVisible = false; this._pendingVisibleRender = false; // 保存事件处理函数引用,用于清理 this._originalScrollHandler = null; this._translationScrollHandler = null; // 依赖的渲染函数(由外部注入) this.renderPageBboxesToCtx = null; this.renderPageTranslationToCtx = null; this.clearTextInBbox = null; this.clearFormulaElementsForPageInWrapper = null; this.onOverlayClick = null; this.contentListJson = null; } /** * 设置依赖的渲染函数 */ setDependencies(deps) { Object.assign(this, deps); } /** * 设置容器元素 */ setContainers(originalSegments, translationSegments, originalScroll, translationScroll) { this.originalSegmentsContainer = originalSegments; this.translationSegmentsContainer = translationSegments; this.originalScroll = originalScroll; this.translationScroll = translationScroll; } /** * 渲染所有页面(连续模式) */ async renderAllPagesContinuous() { this.mode = 'continuous'; // 计算自适应缩放 const firstPage = await this.pdfDoc.getPage(1); const originalViewport = firstPage.getViewport({ scale: 1.0 }); const containerWidth = this.originalScroll.clientWidth - 40; this.scale = Math.min(containerWidth / originalViewport.width, 1.5); const dpr = this.dpr; console.log(`[SegmentManager] DPR=${dpr}, scale=${this.scale}`); // 计算所有页面尺寸(物理像素) this.pageInfos = []; let totalHeight = 0; for (let i = 1; i <= this.totalPages; i++) { const page = await this.pdfDoc.getPage(i); const viewport = page.getViewport({ scale: this.scale * dpr }); const info = { pageNum: i, page, viewport, yOffset: totalHeight, width: viewport.width, height: viewport.height }; this.pageInfos.push(info); totalHeight += viewport.height; } // 清空旧段容器 this.originalSegmentsContainer.innerHTML = ''; this.translationSegmentsContainer.innerHTML = ''; this.segments = []; // 分段策略 const MAX_SEG_PX = this.options.maxSegmentPixels || (dpr >= 2 ? 4096 : 8192); const canvasWidth = this.pageInfos.length > 0 ? this.pageInfos[0].width : Math.round(originalViewport.width * this.scale * dpr); let currentSeg = null; let currentSegHeight = 0; let currentSegTop = 0; const startNewSegment = () => { const segIndex = this.segments.length; if (currentSeg) currentSegTop += currentSegHeight; currentSegHeight = 0; const seg = { index: segIndex, topPx: currentSegTop, heightPx: 0, widthPx: canvasWidth, pages: [], left: null, right: null, rendered: false, rendering: false, textCleared: false, }; this.segments.push(seg); currentSeg = seg; }; startNewSegment(); for (const p of this.pageInfos) { if (currentSegHeight > 0 && (currentSegHeight + p.height) > MAX_SEG_PX) { currentSeg.heightPx = currentSegHeight; startNewSegment(); } const yInSeg = currentSegHeight; currentSeg.pages.push({ pageNum: p.pageNum, page: p.page, viewport: p.viewport, yInSegPx: yInSeg, width: p.width, height: p.height }); currentSegHeight += p.height; } if (currentSeg) currentSeg.heightPx = currentSegHeight; // 创建段 DOM for (const seg of this.segments) { this.createSegmentDom(seg, dpr); } // 初始化懒加载 this.initLazyLoadingSegments(); console.log(`[SegmentManager] 已创建 ${this.segments.length} 个段`); } /** * 创建一个段的左右 DOM 和上下文 */ createSegmentDom(seg, dpr) { const cssWidth = seg.widthPx / dpr; const cssHeight = seg.heightPx / dpr; const buildSide = (container, side) => { const wrapper = document.createElement('div'); wrapper.className = 'pdf-segment-wrapper'; wrapper.style.position = 'relative'; wrapper.style.display = 'block'; wrapper.style.width = cssWidth + 'px'; wrapper.style.height = cssHeight + 'px'; wrapper.style.margin = '0'; const canvas = document.createElement('canvas'); canvas.width = seg.widthPx; canvas.height = seg.heightPx; canvas.style.width = cssWidth + 'px'; canvas.style.height = cssHeight + 'px'; const ctx = canvas.getContext('2d', { willReadFrequently: true, alpha: false }); const overlay = document.createElement('canvas'); overlay.width = seg.widthPx; overlay.height = seg.heightPx; overlay.style.width = cssWidth + 'px'; overlay.style.height = cssHeight + 'px'; overlay.style.position = 'absolute'; overlay.style.left = '0'; overlay.style.top = '0'; const overlayCtx = overlay.getContext('2d', { willReadFrequently: true }); wrapper.appendChild(canvas); wrapper.appendChild(overlay); container.appendChild(wrapper); const sideObj = { wrapper, canvas, ctx, overlay, overlayCtx }; if (side === 'left') seg.left = sideObj; else seg.right = sideObj; // 绑定点击事件 if (side === 'left' && this.onOverlayClick) { overlay.addEventListener('click', (e) => this.onOverlayClick(e, seg)); } }; buildSide(this.originalSegmentsContainer, 'left'); buildSide(this.translationSegmentsContainer, 'right'); } /** * 初始化懒加载:监听滚动,渲染可见区域 */ initLazyLoadingSegments() { if (!this.originalScroll || !this.translationScroll) return; // 初始渲染可见段 this.renderVisibleSegments(this.originalScroll); const onScroll = (scroller) => { clearTimeout(this._lazyScrollTimer); this._lazyScrollTimer = setTimeout(() => { this.renderVisibleSegments(scroller); }, this.options.scrollDebounceMs); }; if (!this._lazyInitialized) { // 保存事件处理函数引用 this._originalScrollHandler = () => onScroll(this.originalScroll); this._translationScrollHandler = () => onScroll(this.translationScroll); this.originalScroll.addEventListener('scroll', this._originalScrollHandler); this.translationScroll.addEventListener('scroll', this._translationScrollHandler); this._lazyInitialized = true; } } /** * 渲染当前可见的页面(视口+缓冲区) */ async renderVisibleSegments(container) { if (!this.segments || this.segments.length === 0 || !container) return; if (this._renderingVisible) { this._pendingVisibleRender = true; return; } this._renderingVisible = true; const dpr = this.dpr; const scrollTopCss = container.scrollTop; const viewportHeightCss = container.clientHeight; const bufferCss = viewportHeightCss * this.options.bufferRatio; const visibleStartPx = Math.max(0, (scrollTopCss - bufferCss) * dpr); const visibleEndPx = (scrollTopCss + viewportHeightCss + bufferCss) * dpr; for (const seg of this.segments) { const segStart = seg.topPx; const segEnd = seg.topPx + seg.heightPx; const isVisible = segEnd >= visibleStartPx && segStart <= visibleEndPx; if (isVisible && !seg.rendering) { try { seg.rendering = true; await this.renderSegment(seg); seg.rendered = true; } finally { seg.rendering = false; } } } this._renderingVisible = false; if (this._pendingVisibleRender) { this._pendingVisibleRender = false; this.renderVisibleSegments(container); } } /** * 渲染单个段 */ async renderSegment(seg) { // 使用离屏画布避免 PDF.js 清除问题 const off = document.createElement('canvas'); const offCtx = off.getContext('2d', { willReadFrequently: true, alpha: false }); for (const p of seg.pages) { if (off.width !== p.width) off.width = p.width; if (off.height !== p.height) off.height = p.height; offCtx.clearRect(0, 0, off.width, off.height); await p.page.render({ canvasContext: offCtx, viewport: p.viewport }).promise; // 绘制到左右段画布 seg.left.ctx.drawImage(off, 0, p.yInSegPx); seg.right.ctx.drawImage(off, 0, p.yInSegPx); } // 绘制 overlays await this.renderSegmentOverlays(seg); } /** * 渲染段的覆盖层(bbox和翻译) */ async renderSegmentOverlays(seg) { if (!this.renderPageBboxesToCtx || !this.renderPageTranslationToCtx) { console.warn('[SegmentManager] 缺少渲染函数依赖'); return; } const leftCtx = seg.left.overlayCtx; const rightCtx = seg.right.overlayCtx; // 清理段 overlay leftCtx.clearRect(0, 0, seg.widthPx, seg.heightPx); rightCtx.clearRect(0, 0, seg.widthPx, seg.heightPx); // 清理公式 DOM if (this.clearFormulaElementsForPageInWrapper) { for (const p of seg.pages) { this.clearFormulaElementsForPageInWrapper(p.pageNum, seg.right.wrapper); } } // 绘制 overlays(串行执行) for (const p of seg.pages) { this.renderPageBboxesToCtx(leftCtx, p.pageNum, p.yInSegPx, p.width, p.height); await this.renderPageTranslationToCtx(rightCtx, seg.right.wrapper, p.pageNum, p.yInSegPx, p.width, p.height); } } /** * 清除段内所有 bbox 的原始文字 */ async clearTextInSegment(seg) { if (!this.contentListJson || !this.clearTextInBbox) { console.warn('[SegmentManager] 缺少清除文字依赖'); return; } const pageItems = this.contentListJson.filter(item => item.type === 'text'); const BBOX_NORMALIZED_RANGE = this.options.bboxNormalizedRange; for (const p of seg.pages) { const pageNum = p.pageNum; const scaleX = p.width / BBOX_NORMALIZED_RANGE; const scaleY = p.height / BBOX_NORMALIZED_RANGE; // 获取当前页的所有 bbox const currentPageItems = pageItems.filter(item => item.page_idx === pageNum - 1); for (const item of currentPageItems) { if (!item.bbox) continue; const bb = item.bbox; const x = bb[0] * scaleX; const y = bb[1] * scaleY + p.yInSegPx; const w = (bb[2] - bb[0]) * scaleX; const h = (bb[3] - bb[1]) * scaleY; // 精确清除文字 await this.clearTextInBbox(seg.right.ctx, pageNum, { x, y, w, h }, p.yInSegPx); } } } /** * 获取当前可见的页码 */ getCurrentVisiblePageNum(container) { if (!container || !this.pageInfos || this.pageInfos.length === 0) return 1; const scrollTopCss = container.scrollTop; const scrollTopPx = scrollTopCss * this.dpr; for (const info of this.pageInfos) { if (scrollTopPx >= info.yOffset && scrollTopPx < info.yOffset + info.height) { return info.pageNum; } } return 1; } /** * 滚动到指定页面 */ scrollToPage(pageNum, container) { if (!container || pageNum < 1 || pageNum > this.pageInfos.length) return; const info = this.pageInfos[pageNum - 1]; const scrollTopCss = info.yOffset / this.dpr; container.scrollTop = scrollTopCss; } /** * 清理资源 */ destroy() { // 清理定时器 if (this._lazyScrollTimer) { clearTimeout(this._lazyScrollTimer); this._lazyScrollTimer = null; } // 移除事件监听 if (this._lazyInitialized && this.originalScroll && this.translationScroll) { if (this._originalScrollHandler) { this.originalScroll.removeEventListener('scroll', this._originalScrollHandler); this._originalScrollHandler = null; } if (this._translationScrollHandler) { this.translationScroll.removeEventListener('scroll', this._translationScrollHandler); this._translationScrollHandler = null; } this._lazyInitialized = false; } // 清空数据 this.segments = []; this.pageInfos = []; // 清空 DOM if (this.originalSegmentsContainer) { this.originalSegmentsContainer.innerHTML = ''; } if (this.translationSegmentsContainer) { this.translationSegmentsContainer.innerHTML = ''; } } } // 导出模块 if (typeof module !== 'undefined' && module.exports) { module.exports = SegmentManager; }