pdf页面加载
This commit is contained in:
parent
dc5a57fdda
commit
1a75a8b025
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue