pdf页面加载

This commit is contained in:
cwchen 2025-11-10 13:33:20 +08:00
parent dc5a57fdda
commit 1a75a8b025
1 changed files with 279 additions and 88 deletions

View File

@ -98,6 +98,11 @@ export default {
isDocumentReady: false,
renderQueue: [],
isProcessingQueue: false,
pageCache: new Map(),
isPrefetching: false,
prefetchHandle: null,
prefetchScheduled: false,
initialPreloadedCount: 0,
}
},
mounted() {
@ -167,12 +172,16 @@ export default {
this.totalPages = this.pdfDoc.numPages
this.renderQueue = []
this.isProcessingQueue = false
this.pageCache.clear()
this.isPrefetching = false
this.initialPreloadedCount = 0
const fragment = document.createDocumentFragment()
for (let pageNumber = 1; pageNumber <= this.totalPages; pageNumber += 1) {
const placeholder = document.createElement('div')
placeholder.className = 'pdf-page placeholder'
placeholder.dataset.page = pageNumber
placeholder.dataset.status = 'placeholder'
fragment.appendChild(placeholder)
this.pageContainers.push(placeholder)
}
@ -180,97 +189,149 @@ export default {
container.scrollTop = 0
this.setupIntersectionObserver()
await this.preloadInitialPages()
this.observeInitialPages()
this.schedulePrefetch()
this.isDocumentReady = true
},
async renderSinglePage(pageNumber) {
if (!this.pdfDoc) return
if (this.renderedPages.has(pageNumber)) return
ensureContainerDimensions(pageNumber, viewport) {
const index = pageNumber - 1
const container = this.pageContainers[index]
if (!container) return
container.style.width = `${viewport.width}px`
container.style.height = `${viewport.height}px`
if (!container.dataset.status || container.dataset.status === 'placeholder') {
container.dataset.status = 'prefetched'
}
container.classList.remove('placeholder')
if (container.dataset.status !== 'rendered') {
container.classList.add('prefetched')
}
if (this.$refs.pdfWrapper && !this.$refs.pdfWrapper.style.minWidth) {
this.$refs.pdfWrapper.style.minWidth = `${Math.ceil(viewport.width)}px`
}
},
async ensurePageCached(pageNumber) {
if (!this.pdfDoc) return null
if (this.pageCache.has(pageNumber)) {
return this.pageCache.get(pageNumber)
}
const page = await this.pdfDoc.getPage(pageNumber)
this.pageCache.set(pageNumber, page)
return page
},
async renderCanvas(pageNumber) {
const page = await this.ensurePageCached(pageNumber)
if (!page) return null
const index = pageNumber - 1
const container = this.pageContainers[index]
if (!container || container.classList.contains('rendering')) return
if (!container) return null
container.classList.add('rendering')
try {
const page = await this.pdfDoc.getPage(pageNumber)
const viewport = page.getViewport({ scale: this.scale })
container.classList.remove('placeholder')
container.style.width = `${viewport.width}px`
container.style.height = `${viewport.height}px`
const viewport = page.getViewport({ scale: this.scale })
this.ensureContainerDimensions(pageNumber, viewport)
const canvas = document.createElement('canvas')
canvas.className = 'pdf-canvas'
const deviceScale = window.devicePixelRatio || 1
const outputScale = Math.min(deviceScale, 1)
canvas.width = viewport.width * outputScale
canvas.height = viewport.height * outputScale
canvas.style.width = `${viewport.width}px`
canvas.style.height = `${viewport.height}px`
container.appendChild(canvas)
if (this.$refs.pdfWrapper && !this.$refs.pdfWrapper.style.minWidth) {
this.$refs.pdfWrapper.style.minWidth = `${Math.ceil(viewport.width)}px`
}
const canvasContext = canvas.getContext('2d')
const renderContext = {
canvasContext,
viewport,
}
if (outputScale !== 1) {
renderContext.transform = [outputScale, 0, 0, outputScale, 0, 0]
}
await page.render(renderContext).promise
this.pageTextDivs[index] = []
const scheduleTextLayer = () => {
const textLayerDiv = document.createElement('div')
textLayerDiv.className = 'textLayer'
textLayerDiv.style.width = `${viewport.width}px`
textLayerDiv.style.height = `${viewport.height}px`
container.appendChild(textLayerDiv)
const textLayer = new TextLayerBuilder({
textLayerDiv,
pageIndex: index,
viewport,
eventBus: this.eventBus,
})
page.getTextContent()
.then((textContent) => {
textLayer.setTextContent(textContent)
textLayer.render()
const textDivs = [...textLayer.textDivs]
textDivs.forEach((div) => {
div.dataset.originalText = div.textContent
})
this.pageTextDivs[index] = textDivs
})
.catch((err) => {
console.warn(`获取第 ${pageNumber} 页文本失败`, err)
})
}
if (typeof window.requestIdleCallback === 'function') {
window.requestIdleCallback(scheduleTextLayer, { timeout: 500 })
} else {
setTimeout(scheduleTextLayer, 40)
}
this.renderedPages.set(pageNumber, { container, viewport })
if (this.observer) {
this.observer.unobserve(container)
}
} catch (error) {
console.error(`渲染第 ${pageNumber} 页失败`, error)
} finally {
container.classList.remove('rendering')
const oldCanvas = container.querySelector('.pdf-canvas')
if (oldCanvas) {
oldCanvas.remove()
}
const canvas = document.createElement('canvas')
canvas.className = 'pdf-canvas'
const deviceScale = window.devicePixelRatio || 1
const outputScale = Math.min(deviceScale, 1.25)
canvas.width = viewport.width * outputScale
canvas.height = viewport.height * outputScale
canvas.style.width = `${viewport.width}px`
canvas.style.height = `${viewport.height}px`
container.appendChild(canvas)
const canvasContext = canvas.getContext('2d')
const renderContext = {
canvasContext,
viewport,
}
if (outputScale !== 1) {
renderContext.transform = [outputScale, 0, 0, outputScale, 0, 0]
}
await page.render(renderContext).promise
container.dataset.status = 'rendered'
container.classList.remove('prefetched')
const textLayerDiv = container.querySelector('.textLayer')
if (textLayerDiv) {
textLayerDiv.style.display = ''
}
this.renderedPages.set(pageNumber, { container, viewport })
return { page, viewport, container }
},
async renderTextLayer(pageNumber, { visible = true, force = false } = {}) {
const page = await this.ensurePageCached(pageNumber)
if (!page) return
const index = pageNumber - 1
const container = this.pageContainers[index]
if (!container) return
const viewport = page.getViewport({ scale: this.scale })
this.ensureContainerDimensions(pageNumber, viewport)
const existing = container.querySelector('.textLayer')
if (existing && !force && this.pageTextDivs[index]?.length) {
existing.style.display = visible ? '' : 'none'
return
}
if (existing) {
existing.remove()
}
const textLayerDiv = document.createElement('div')
textLayerDiv.className = 'textLayer'
textLayerDiv.style.width = `${viewport.width}px`
textLayerDiv.style.height = `${viewport.height}px`
textLayerDiv.style.display = visible ? '' : 'none'
container.appendChild(textLayerDiv)
const textLayer = new TextLayerBuilder({
textLayerDiv,
pageIndex: index,
viewport,
eventBus: this.eventBus,
})
try {
const textContent = await page.getTextContent()
textLayer.setTextContent(textContent)
textLayer.render()
const textDivs = [...textLayer.textDivs]
textDivs.forEach((div) => {
div.dataset.originalText = div.textContent
})
this.pageTextDivs[index] = textDivs
} catch (error) {
console.warn(`加载第 ${pageNumber} 页文本失败`, error)
}
if (container.dataset.status !== 'rendered') {
container.dataset.status = visible ? 'rendered' : 'text-ready'
if (!visible) {
container.classList.add('prefetched')
}
}
},
async renderSinglePage(pageNumber) {
await this.renderCanvas(pageNumber)
await this.renderTextLayer(pageNumber, { visible: true })
},
scheduleRender(pageNumber, { priority = false } = {}) {
@ -281,10 +342,6 @@ export default {
} else {
this.renderQueue.push(pageNumber)
}
const MAX_QUEUE_LENGTH = 4
if (this.renderQueue.length > MAX_QUEUE_LENGTH) {
this.renderQueue.splice(MAX_QUEUE_LENGTH)
}
this.processRenderQueue()
},
@ -302,6 +359,106 @@ export default {
}
},
schedulePrefetch() {
this.cancelPrefetch()
if (!this.totalPages) return
const startPage = Math.min(this.totalPages, Math.max(1, this.initialPreloadedCount + 1))
let pageNumber = startPage
const fetchNext = async () => {
this.prefetchScheduled = false
this.prefetchHandle = null
if (pageNumber > this.totalPages) {
this.isPrefetching = false
return
}
if (!this.pageCache.has(pageNumber)) {
try {
const page = await this.pdfDoc.getPage(pageNumber)
this.pageCache.set(pageNumber, page)
const viewport = page.getViewport({ scale: this.scale })
this.ensureContainerDimensions(pageNumber, viewport)
} catch (error) {
console.warn(`预取第 ${pageNumber} 页失败`, error)
}
}
pageNumber += 1
if (pageNumber <= this.totalPages) {
const schedule = () => {
this.prefetchScheduled = true
if (typeof window.requestIdleCallback === 'function') {
this.prefetchHandle = window.requestIdleCallback(fetchNext, { timeout: 1000 })
} else {
this.prefetchHandle = window.setTimeout(fetchNext, 80)
}
}
schedule()
} else {
this.isPrefetching = false
}
}
this.isPrefetching = true
if (typeof window.requestIdleCallback === 'function') {
this.prefetchScheduled = true
this.prefetchHandle = window.requestIdleCallback(fetchNext, { timeout: 800 })
} else {
this.prefetchScheduled = true
this.prefetchHandle = window.setTimeout(fetchNext, 120)
}
},
cancelPrefetch() {
if (this.prefetchHandle) {
if (typeof window.cancelIdleCallback === 'function' && this.prefetchScheduled) {
window.cancelIdleCallback(this.prefetchHandle)
} else {
clearTimeout(this.prefetchHandle)
}
}
this.prefetchHandle = null
this.prefetchScheduled = false
this.isPrefetching = false
},
async preloadInitialPages() {
const preloadCount = Math.min(3, this.totalPages)
if (!preloadCount) {
this.initialPreloadedCount = 0
return
}
for (let pageNumber = 1; pageNumber <= preloadCount; pageNumber += 1) {
await this.renderTextLayer(pageNumber, { visible: pageNumber === 1 })
}
if (preloadCount >= 1) {
await this.renderCanvas(1)
}
this.initialPreloadedCount = preloadCount
},
unloadPage(pageNumber) {
if (!pageNumber) return
const index = pageNumber - 1
const container = this.pageContainers[index]
if (!container || container.dataset.status !== 'rendered') return
const canvas = container.querySelector('.pdf-canvas')
if (canvas) {
canvas.remove()
}
const textLayer = container.querySelector('.textLayer')
if (textLayer) {
textLayer.style.display = 'none'
}
container.dataset.status = this.pageTextDivs[index]?.length ? 'text-ready' : 'prefetched'
container.classList.add('prefetched')
this.renderedPages.delete(pageNumber)
},
handleSearch: debounce(function () {
if (!this.keyword) {
this.resetSearch()
@ -432,6 +589,8 @@ export default {
this.totalPages = 0
this.isDocumentReady = false
this.disconnectObserver()
this.cancelPrefetch()
this.pageCache.clear()
const wrapper = this.$refs.pdfWrapper
if (wrapper) {
wrapper.innerHTML = ''
@ -459,7 +618,17 @@ export default {
if (entry.isIntersecting) {
const page = Number(entry.target.dataset.page)
if (page) {
this.scheduleRender(page)
if (page > this.initialPreloadedCount) {
this.renderTextLayer(page, { visible: true })
} else if (page !== 1) {
this.renderTextLayer(page, { visible: true })
}
this.scheduleRender(page, { priority: true })
}
} else {
const page = Number(entry.target.dataset.page)
if (page) {
this.unloadPage(page)
}
}
})
@ -470,11 +639,19 @@ export default {
observeInitialPages() {
if (!this.pageContainers.length) return
const initial = this.pageContainers.slice(0, 2)
const initial = this.pageContainers.slice(0, 3)
initial.forEach((container) => {
const page = Number(container.dataset.page)
if (page) {
this.scheduleRender(page, { priority: true })
if (page === 1) {
//
return
}
if (page <= this.initialPreloadedCount) {
this.scheduleRender(page, { priority: true })
} else {
this.scheduleRender(page)
}
}
})
},
@ -486,6 +663,7 @@ export default {
}
this.renderQueue = []
this.isProcessingQueue = false
this.cancelPrefetch()
},
},
}
@ -586,6 +764,19 @@ export default {
content: '正在准备页面...';
}
.pdf-page.prefetched {
background: #fdfdfd;
box-shadow: inset 0 0 0 1px rgba(112, 143, 255, 0.2);
position: relative;
}
.pdf-page.prefetched::after {
content: '滚动到此处以加载内容';
color: rgba(112, 143, 255, 0.7);
font-size: 12px;
letter-spacing: 0.4px;
}
.pdf-canvas {
width: 100%;
display: block;