vue基于pdf.js實現(xiàn)pdf文件預(yù)覽相關(guān)功能實例
PDF.js 簡介
PDF.js 是什么?
PDF.js 是 Mozilla 基于 JavaScript + HTML5 Canvas 構(gòu)建的 PDF 渲染引擎,完全運行于瀏覽器,不依賴插件。
核心目標(biāo):
- 在瀏覽器解析 PDF 二進(jìn)制
- 使用 Canvas / SVG 渲染頁面
- 提供可供二次開發(fā)的 API 和完整 Viewer
PDF.js 的主要模塊結(jié)構(gòu)
pdf.js(主庫) pdf.worker.js(Worker 后臺解析) PDFViewer(UI 查看器)
雙層架構(gòu)
- 主線程(UI線程)
- 負(fù)責(zé)用戶界面交互
- 處理頁面導(dǎo)航、縮放等操作
- 管理Canvas渲染
- Worker線程(核心處理線程)
- 真正解析PDF文檔
- 處理復(fù)雜的計算任務(wù)
- 文本提取和布局計算
- 圖像解碼和渲染
核心流程
getDocument()
↓
PDFDocumentLoadingTask
↓
PDFDocumentProxy
↓
getPage(n)
↓
PDFPageProxy
↓
Canvas/SVG 渲染
PDF.js 的三大關(guān)鍵對象
| 對象 | 描述 |
|---|---|
| PDFDocumentLoadingTask | PDF 加載任務(wù),支持中斷、進(jìn)度、promise |
| PDFDocumentProxy | PDF 文檔本體,包含頁數(shù)、metadata、加載頁面等 |
| PDFPageProxy | 頁對象,可渲染/提取文本/提取指令 |
安裝PDF.js
安裝依賴
npm install pdfjs-dist --save
安裝版本:5.4.449
引入并配置worker
在指定.vue文件中引入
import * as pdfjsLib from 'pdfjs-dist'
配置worker路徑
- PDF.js使用Web Worker來處理PDF文檔的解析和渲染,這樣可以避免阻塞主線程
GlobalWorkerOptions.workerSrc用于指定Web Worker腳本的URL路徑
對于Vite構(gòu)建工具,需要使用new URL()配合import.meta.url的資源引用方式,確保在生產(chǎn)環(huán)境和開發(fā)環(huán)境都能正確找到Worker文件
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/legacy/build/pdf.worker.min.mjs', import.meta.url, ).toString()
對于Webpack構(gòu)建工具,使用require語法
pdfjsLib.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/legacy/build/pdf.worker.min.mjs');
使用PDF.js
加載pdf
通過pdfjsLib.getDocument(pdfPath)加載pdf文件
const loadFile = () => {
const loadingTask = pdfjsLib.getDocument(pdfPath)
console.log(loadingTask, 'loadingTask')
}
獲取文件加載進(jìn)度
loadingTask.onProgress = (progressData) => {
console.log(progressData.loaded / progressData.total) // 0-1
state.percentage = (progressData.loaded / progressData.total) * 100
}
獲取pdf信息
let pdfDoc = null
loadingTask.promise.then((pdf) => {
console.log(pdf, 'pdf')
// 保存pdf文檔對象到全局,便于后續(xù)使用
pdfDoc = pdf
state.padPageNum = pdf.numPages //pdf的頁碼
})

渲染頁面
通過pdf.getPage(currentPage)獲取頁對象, 可渲染/提取文本/提取節(jié)點屬性等
const currentPage = 1
// 此處pdf為通過文件路徑加載獲得
pdf.getPage(currentPage).then((page) => {
showPdf(page)
})
通過page.render(renderContext)渲染頁面
// 渲染pdf
const showPdf = (page)=>{
const viewport = page.getViewport({ scale: 1.5 })
const canvas = document.getElementById('the-canvas') // 提前準(zhǔn)備canvas
const context = canvas.getContext('2d')
// 將畫布尺寸設(shè)為文檔原本尺寸
canvas.height = viewport.height
canvas.width = viewport.width
const renderContext = {
canvasContext: context,
viewport: viewport,
}
const renderTask = page.render(renderContext)
renderTask.promise.then(() => {
console.log('渲染完成')
})
}
pdf文字交互
PDF.js 將 PDF 渲染在 canvas 上,而 Canvas 只是位圖(圖片),瀏覽器是無法直接選中里面的文字的。要實現(xiàn)文字選中,必須啟用 Text Layer(文本層)。
核心原理:三明治結(jié)構(gòu)
PDF.js 的解決方案是在 Canvas 之上覆蓋一層透明的 HTML 層。
- 底層 (Canvas):負(fù)責(zé)展示 PDF 的視覺內(nèi)容(字體、圖片、排版)。
- 上層 (Text Layer):一個透明的
div,里面包含了很多 ,這些 span 的位置、大小、文字內(nèi)容與底層的 Canvas 完全重疊。
當(dāng)用戶“選中”文字時,實際上選中的是上層透明的 HTML DOM,而不是底層的 Canvas。
// <div id="text-layer" class="textLayer"></div>
// 頁對象獲取后,渲染文本交互層
const randerTextContent = async (viewport, scale, page) => {
const textLayerDiv = document.getElementById('text-layer')
// 設(shè)置文本層容器尺寸,必須與頁面渲染時的 Canvas 一致
textLayerDiv.style.height = viewport.height
textLayerDiv.style.width = viewport.width
// CSS 變量設(shè)置 (PDF.js v3+ 需要這個變量來計算字體縮放)
textLayerDiv.style.setProperty('--total-scale-factor', scale)
textLayerDiv.style.setProperty('--scale-round-x', 1)
textLayerDiv.style.setProperty('--scale-round-y', 1)
try {
// 獲取頁面文本內(nèi)容
const textContent = await page.getTextContent()
// 清除height為0的子項,避免無用的dom渲染
let obj = { ...textContent }
obj.items = textContent.items.filter((item) => item.height)
// 注意:在 v5.x 中使用 new pdfjsLib.TextLayer
const textLayer = new pdfjsLib.TextLayer({
textContentSource: obj,
container: textLayerDiv,
viewport: viewport,
})
// 注意:在 v5.x 中使用 new pdfjsLib.TextLayer
const textLayer = new pdfjsLib.TextLayer({
textContentSource: obj,
container: textLayerDiv,
viewport: viewport,
})
await textLayer.render()
console.log('文本層渲染完畢,現(xiàn)在可以選中文本了')
} catch (err) {
console.error('Text layer render error:', err)
}
}
官方有提供現(xiàn)成的 Text Layer CSS 樣式,樣式表包含了 PDF.js 官方的 Text Layer 字體和定位計算規(guī)則。
import 'pdfjs-dist/web/pdf_viewer.css'
渲染結(jié)果

搜索高亮
基本功能:搜索、切換匹配項、清空
搜索功能實現(xiàn)
1、遍歷所有頁面內(nèi)容,獲取與搜索內(nèi)容的匹配項。
// totalPageNum從加載文件時的pdf對象獲取
for (let pageNum = 1; pageNum <= state.totalPageNum; pageNum++) {
const page = await pdfDoc.getPage(pageNum)
const textContent = await page.getTextContent()
// 頁面內(nèi)容中會有一些空對象,并且height都為 0,篩選出有內(nèi)容的item
let arr = textContent.items.filter((item) => item.height)
arr.forEach((item, index) => {
// 獲取包含搜索文本的item,并記錄頁碼、文本內(nèi)容、item索引
if (item.str.toLowerCase().includes(state.searchText.toLowerCase())) {
state.searchResult.push({
pageNum,
str: item.str,
itemIndex: index,
})
}
})
}
2、展示第一個匹配項所在頁面內(nèi)容。
// 文檔內(nèi)容與搜索內(nèi)容匹配到的數(shù)量
state.searchCount = state.searchResult.length
if (state.searchCount > 0) {
// 根據(jù)當(dāng)前匹配的索引獲取第一個匹配項(便于設(shè)置深色的高亮,清晰的展示當(dāng)前匹配的位置)
const searchItem = state.searchResult[state.searchIndex]
if (state.currentPage === searchItem.pageNum) {
//
searchHeightLight(searchItem)
} else {
state.currentPage = searchItem.pageNum
// 跳轉(zhuǎn)頁面
await getPage()
}
}
3、獲取當(dāng)前頁面內(nèi)容中所有與搜索內(nèi)容匹配的子項
let textLayerDiv = document.getElementById('text-layer')
const searchHeightLight = (searchItem) => {
// 找到所有文本的span元素
const spans = textLayerDiv.querySelectorAll('span[role]')
spans.forEach((span, index) => {
if (
span.textContent.toLowerCase().includes(state.searchText.toLowerCase())
) {
// 找到包含搜索內(nèi)容的span元素,對搜索詞進(jìn)行高亮
spanSearchTextHandle(span, index, searchItem)
} else {
span.style.backgroundColor = ''
}
})
}
4、對匹配的子項中的搜索詞進(jìn)行高亮
首次匹配會將所有匹配內(nèi)容從 文本 -> span ,顏色為淺色高亮rgba(255, 255, 0, 0.5),第一個匹配項顏色為深色高亮rgba(255, 164, 0, 0.5)
// 替換span元素中的搜索文本為高亮span元素
const spanSearchTextHandle = (span, index, searchItem) => {
let target = span.innerHTML
// 找到搜索內(nèi)容的位置
let replaceIndex = target.toLowerCase()
.indexOf(state.searchText.toLowerCase())
// 找到需要高亮的原內(nèi)容【為了保證英文大小寫都能匹配到,不能直接使用搜索內(nèi)容進(jìn)行替換高亮】
let replaceText = target.substring(
replaceIndex,
replaceIndex + state.searchText.length,
)
let newHtml = target.replace(
replaceText,
`<span class="searchLight" style="background-color: rgba(255, 255, 0, 0.5);">${replaceText}</span>`,
)
// 對當(dāng)前匹配項進(jìn)行更深色的高亮,使用戶感知更清晰
if (index === searchItem.itemIndex) {
newHtml = target.replace(
replaceText,
`<span class="searchLight" style="background-color: rgba(255, 164, 0, 0.5);">${replaceText}</span>`,
)
span.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
}
span.innerHTML = newHtml
}
至此搜索功能基本實現(xiàn)。
切換匹配項功能實現(xiàn)
當(dāng)用戶切換匹配項時【點擊“上一個/下一個”按鈕】,對匹配索引進(jìn)行計算
// 上一個
const searchPrev = () => {
if (state.searchIndex > 0) {
state.searchIndex--
// 保證不在當(dāng)前頁的匹配項能夠絲滑跳轉(zhuǎn)進(jìn)入
jumpSearch()
}
}
// 下一個
const searchNext = () => {
if (state.searchIndex < state.searchCount) {
// 避免超出
state.searchIndex = (state.searchIndex + 1) % state.searchCount
jumpSearch()
}
}
切換匹配項時,更換深色高亮位置
// 替換span元素中的搜索文本為高亮span元素
const spanSearchTextHandle = (span, index, searchItem) => {
let target = span.innerHTML
// 此時所有匹配項已經(jīng)變?yōu)閟pan了
if (target.includes('span')) {
// 找到當(dāng)前匹配項
if (index === searchItem.itemIndex) {
// 替換深色高亮
let newHtml = target.replace(
'rgba(255, 255, 0, 0.5)',
'rgba(255, 164, 0, 0.5)',
)
span.innerHTML = newHtml
// 平滑移動到目標(biāo)位置
span.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
} else {
// 其他項保持淺色高亮
let newHtml = target.replace(
'rgba(255, 164, 0, 0.5)',
'rgba(255, 255, 0, 0.5)',
)
span.innerHTML = newHtml
}
}
}
清空搜索功能實現(xiàn)
清空搜索匹配項的數(shù)組內(nèi)容,恢復(fù)文本層dom
if (state.searchText === '') return
state.searchText = ''
state.searchResult = []
state.searchIndex = 0
state.searchCount = 0
if (!textLayerDiv) return
// 獲取文本層
const spans = textLayerDiv.querySelectorAll('.searchLight')
const parent = textLayerDiv.parentNode
// 先將文本層dom刪除,避免遍歷頻繁觸發(fā)回流
parent.removeChild(textLayerDiv)
spans.forEach((span) => {
// 將所有嵌套span替換為文本
span.parentNode.innerHTML = span.parentNode.textContent
})
// 恢復(fù)
parent.appendChild(textLayerDiv)
選中高亮
基礎(chǔ)功能:選中高亮、擦除高亮
選中高亮功能實現(xiàn)
功能設(shè)計:
- 選中文本,可以設(shè)置不同的顏色
- 對于已設(shè)置過顏色的文本,可再次選中更換其他顏色
關(guān)鍵:
- 保證dom結(jié)構(gòu)扁平化
- 跨標(biāo)簽選中高亮
分析:
選中高亮也嘗試過通過搜索高亮的方式實現(xiàn),但是考慮到文本層是通過絕對定位實現(xiàn),在文本層父節(jié)點實現(xiàn)dom扁平化獲取定位困難、在文本子項下實現(xiàn)也會導(dǎo)致高亮與后面的文本重疊,導(dǎo)致后面文本選中問題。
最終可行實現(xiàn):
再增加一層,保證與文本層重疊,置于文本層的下方,保證文本層可選中。
通過getClientRects()獲取選中文本相對于視口的矩形信息(含位置以及寬高),并且該方法能夠自動處理跨標(biāo)簽的選中內(nèi)容,返回多個矩形信息。
結(jié)合getBoundingClientRect()獲取整個文本層相對于視口的矩形信息,計算出選中文本的精確位置以及寬高,增加到數(shù)組中,再渲染到頁面,這樣的處理邏輯就非常簡單,干凈利落。
實現(xiàn):
設(shè)置鼠標(biāo)移動監(jiān)聽
const highLight = () => {
state.isLight = !state.isLight
if (state.isLight) {
textLayerDiv.addEventListener('pointerup', handleMouseUp)
} else {
// 不使用時需要移除,避免重復(fù)創(chuàng)建
textLayerDiv.removeEventListener('pointerup', handleMouseUp)
}
}
通過window.getSelection()獲取唯一選中
const handleMouseUp = () => {
const selection = window.getSelection()
// 無選中不處理
if (selection.toString().trim() === '' || selection.rangeCount === 0) return
const range = selection.getRangeAt(0)
// 確保文本層包含選中內(nèi)容
if (!textLayerDiv.contains(range.commonAncestorContainer)) return
// 高亮處理
hightLightHandle(range)
// 移除選中
selection.removeAllRanges()
}
直接計算選中內(nèi)容的矩形信息,添加到數(shù)組中
// 獲取選中內(nèi)容相對于視口的矩形信息
const reacts = Array.from(range.getClientRects())
// 獲取文本層相對于視口的矩形信息
const textLayerRect = textLayerDiv.getBoundingClientRect()
reacts.forEach((react) => {
state.hightLightList.push({
page: state.currentPage, // 記錄高亮內(nèi)容所在頁碼
style: { // 后期可直接在此設(shè)置不同的背景顏色
top: `${react.top - textLayerRect.top}px`,
left: `${react.left - textLayerRect.left}px`,
width: `${react.width}px`,
height: `${react.height}px`,
},
})
})
渲染模版
<template>
<div class="pdfShow">
<!-- PDF文件渲染層 -->
<canvas id="the-canvas"></canvas>
<!-- 文本層 -->
<div id="text-layer" class="textLayer"></div>
<!-- 高亮層 -->
<div
v-if="state.hightLightList.length !== 0"
:style="{
width: state.viewportWidth,
height: state.viewportHeight,
position: absolute,
}"
>
<div
class="highlight"
v-for="item in state.hightLightList"
:key="item"
:style="item.page === state.currentPage ? item.style : {}"
></div>
</div>
</div>
</template>
<style scoped>
.highlight {
position: absolute;
background: rgba(255, 255, 0, 0.4);
}
</style>
擦除高亮功能實現(xiàn)
功能設(shè)計: 可對高亮內(nèi)容部分擦除
實現(xiàn)思路:通過遍歷,對目標(biāo)高亮項進(jìn)行截斷/刪除,再通過計算將剩余的未選中高亮塊增加到數(shù)組
分析:
確保選中范圍與高亮塊有交集,那么選中范圍相對于高亮塊的位置有可能三種情況【左相交,包含,右相交】
情況一:選中的開始位置在高亮塊開始位置的右側(cè),選中的結(jié)束位置在高亮塊開始位置的右側(cè)

該情況的判斷前提條件為
reactLeft > itemLeft && reactLeft < itemLeft + itemWidth && reactLeft + react.width > itemLeft
選中的可能情況① 為包含狀態(tài),需要對高亮進(jìn)行截斷,設(shè)置當(dāng)前item的寬度,并向數(shù)組內(nèi)再push后半段高亮。
其他選中情況為相交,只需要修改當(dāng)前item的寬度。
// 選中范圍右側(cè)在高亮范圍內(nèi)
if (reactLeft + react.width < itemLeft + itemWidth) {
arr.push({
page: state.currentPage,
style: {
top: `${reactTop}px`,
left: `${reactLeft + react.width}px`,
width: `${itemLeft + itemWidth - (reactLeft + react.width)}px`,
height: `${react.height}px`,
},
})
}
item.style.width = `${reactLeft - itemLeft}px`
情況二:選中的開始位置與高亮塊的開始位置相同,選中的結(jié)束位置在高亮塊開始位置的右側(cè)

該情況的判斷前提條件為
reactLeft == itemLeft && reactLeft < itemLeft + itemWidth && reactLeft + react.width > itemLeft
由圖可已看出,選中的可能情況① 需要修改當(dāng)前item的left和width,在reactLeft + react.width >= itemLeft + itemWidth的情況下,需要刪除該高亮【注意此時處于循環(huán)中,無法對數(shù)組本身進(jìn)行刪除,所以收集索引】
// 選中范圍 完全包裹住 高亮范圍 的 先收集索引,遍歷結(jié)束后再刪除。避免刪除后索引錯亂
if (reactLeft + react.width >= itemLeft + itemWidth) {
deleteIndexArr.push(i)
} else {
item.style.width = `${itemLeft + itemWidth - (reactLeft + react.width)}px`
item.style.left = `${reactLeft + react.width}px`
}
情況三:選中的開始位置在高亮塊開始位置的左側(cè),選中的結(jié)束位置在高亮塊開始位置的右側(cè)

該情況的判斷前提條件為
reactLeft < itemLeft && reactLeft < itemLeft + itemWidth && reactLeft + react.width > itemLeft
由圖可已看出,本質(zhì)與情況二一致,所以處理邏輯可復(fù)用。
最終實現(xiàn):
在高亮處理邏輯中增加擦除邏輯
// 獲取 reacts、textLayerRect矩形信息
reacts.forEach((react) => {
if (state.isClear) {
clearHandle(react, textLayerRect)
return
}
//...
})
完整處理邏輯整合如下:
const clearHandle = (react, textLayerRect) => {
let deleteIndexArr = []
state.hightLightList.forEach((item, i, arr) => {
// 排除非當(dāng)前頁的高亮項
if (item.page !== state.currentPage) return
const itemTop = Number(item.style.top.replace('px', ''))
const reactTop = react.top - textLayerRect.top
// 排除 選中 與 高亮 不在同一高度 的項
if (itemTop !== reactTop) return
const itemWidth = Number(item.style.width.replace('px', ''))
const itemLeft = Number(item.style.left.replace('px', ''))
const reactLeft = react.left - textLayerRect.left
// 確保 選中范圍 與 高亮范圍 有交集
if (
reactLeft < itemLeft + itemWidth &&
reactLeft + react.width > itemLeft
) {
// 高亮范圍 包含 選中范圍
if (reactLeft > itemLeft) {
// 選中范圍右側(cè)在高亮范圍內(nèi)
if (reactLeft + react.width < itemLeft + itemWidth) {
arr.push({
page: state.currentPage,
style: {
top: `${reactTop}px`,
left: `${reactLeft + react.width}px`,
width: `${itemLeft + itemWidth - (reactLeft + react.width)}px`,
height: `${react.height}px`,
},
})
}
item.style.width = `${reactLeft - itemLeft}px`
} else {
// 選中范圍 完全包裹住 高亮范圍 的 先記錄索引,遍歷結(jié)束后再刪除。避免刪除后索引錯亂
if (reactLeft + react.width >= itemLeft + itemWidth) {
deleteIndexArr.push(i)
} else {
item.style.width = `${itemLeft + itemWidth - (reactLeft + react.width)}px`
item.style.left = `${reactLeft + react.width}px`
}
}
}
})
// 最后統(tǒng)一將需要刪除的項 以及 width<=6px(精度校準(zhǔn))的項 刪除
if (deleteIndexArr.length !== 0) {
state.hightLightList = state.hightLightList.filter(
(item, index) =>
!deleteIndexArr.includes(index) &&
Number(item.style.width.replace('px', '')) > 6,
)
}
console.log(state.hightLightList, 'state.hightLightList')
}
總結(jié)
到此這篇關(guān)于vue基于pdf.js實現(xiàn)pdf文件預(yù)覽的文章就介紹到這了,更多相關(guān)vue pdf.js實現(xiàn)pdf文件預(yù)覽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- vue實現(xiàn)在線預(yù)覽pdf文件和下載(pdf.js)
- vue使用pdf.js預(yù)覽pdf文件的方法
- vue插件開發(fā)之使用pdf.js實現(xiàn)手機端在線預(yù)覽pdf文檔的方法
- Vue實現(xiàn)在線預(yù)覽pdf文件功能(利用pdf.js/iframe/embed)
- vue3使用pdf.js來預(yù)覽文件的操作步驟(本地文件測試)
- Vue 集成 PDF.js 實現(xiàn) PDF 預(yù)覽和添加水印的步驟
- 使用Vue3+PDF.js實現(xiàn)PDF預(yù)覽功能
- Vue使用pdf.js和docx-preview實現(xiàn)docx和pdf的在線預(yù)覽
相關(guān)文章
Vue+ElementUI實現(xiàn)動態(tài)更換任意主題色(動態(tài)換膚)的全過程
眾所周知Element-UI有換膚功能,下面這篇文章主要給大家介紹了關(guān)于Vue+ElementUI實現(xiàn)動態(tài)更換任意主題色(動態(tài)換膚)的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02
關(guān)于IDEA中的.VUE文件報錯 Export declarations are not supported by cu
這篇文章主要介紹了關(guān)于IDEA中的.VUE文件報錯 Export declarations are not supported by current JavaScript version的問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10
vuejs使用$emit和$on進(jìn)行組件之間的傳值的示例
本篇文章主要介紹了vuejs使用$emit和$on進(jìn)行組件之間的傳值的示例,具有一定的參考價值,有興趣的可以了解一下2017-10-10
vue 界面刷新數(shù)據(jù)被清除 localStorage的使用詳解
今天小編就為大家分享一篇vue 界面刷新數(shù)據(jù)被清除 localStorage的使用詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09

