最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

vue基于pdf.js實現(xiàn)pdf文件預覽相關功能實例

 更新時間:2026年03月12日 09:48:07   作者:麻辣翅尖  
在Vue項目開發(fā)中,有時需要實現(xiàn)PDF預覽功能,下面這篇文章主要介紹了vue基于pdf.js實現(xiàn)pdf文件預覽相關功能的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下

PDF.js 簡介

PDF.js 是什么?

PDF.js 是 Mozilla 基于 JavaScript + HTML5 Canvas 構建的 PDF 渲染引擎,完全運行于瀏覽器,不依賴插件。

核心目標:

  • 在瀏覽器解析 PDF 二進制
  • 使用 Canvas / SVG 渲染頁面
  • 提供可供二次開發(fā)的 API 和完整 Viewer

PDF.js 的主要模塊結構

pdf.js(主庫)
pdf.worker.js(Worker 后臺解析)
PDFViewer(UI 查看器)

雙層架構

  • 主線程(UI線程)
    • 負責用戶界面交互
    • 處理頁面導航、縮放等操作
    • 管理Canvas渲染
  • Worker線程(核心處理線程)
    • 真正解析PDF文檔
    • 處理復雜的計算任務
    • 文本提取和布局計算
    • 圖像解碼和渲染

核心流程

getDocument()
    ↓
 PDFDocumentLoadingTask
    ↓
 PDFDocumentProxy
    ↓
 getPage(n)
    ↓
 PDFPageProxy
    ↓
 Canvas/SVG 渲染

PDF.js 的三大關鍵對象

對象描述
PDFDocumentLoadingTaskPDF 加載任務,支持中斷、進度、promise
PDFDocumentProxyPDF 文檔本體,包含頁數(shù)、metadata、加載頁面等
PDFPageProxy頁對象,可渲染/提取文本/提取指令

PDF.js官網(wǎng)示例

安裝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構建工具,需要使用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構建工具,使用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')
}

獲取文件加載進度

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') // 提前準備canvas
  const context = canvas.getContext('2d')
  // 將畫布尺寸設為文檔原本尺寸
  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(文本層)。

核心原理:三明治結構

PDF.js 的解決方案是在 Canvas 之上覆蓋一層透明的 HTML 層。

  • 底層 (Canvas):負責展示 PDF 的視覺內容(字體、圖片、排版)。
  • 上層 (Text Layer):一個透明的 div,里面包含了很多 ,這些 span 的位置、大小、文字內容與底層的 Canvas 完全重疊。

當用戶“選中”文字時,實際上選中的是上層透明的 HTML DOM,而不是底層的 Canvas。

// <div id="text-layer" class="textLayer"></div> 
// 頁對象獲取后,渲染文本交互層
const randerTextContent = async (viewport, scale, page) => {
  const textLayerDiv = document.getElementById('text-layer')  
  
  // 設置文本層容器尺寸,必須與頁面渲染時的 Canvas 一致
  textLayerDiv.style.height = viewport.height
  textLayerDiv.style.width = viewport.width
  
  // CSS 變量設置 (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 {
    // 獲取頁面文本內容
    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'

渲染結果

搜索高亮

基本功能:搜索、切換匹配項、清空

搜索功能實現(xiàn)

1、遍歷所有頁面內容,獲取與搜索內容的匹配項。

// totalPageNum從加載文件時的pdf對象獲取
for (let pageNum = 1; pageNum <= state.totalPageNum; pageNum++) {
  const page = await pdfDoc.getPage(pageNum)
  const textContent = await page.getTextContent()

  // 頁面內容中會有一些空對象,并且height都為 0,篩選出有內容的item
  let arr = textContent.items.filter((item) => item.height)
  arr.forEach((item, index) => {
    // 獲取包含搜索文本的item,并記錄頁碼、文本內容、item索引
    if (item.str.toLowerCase().includes(state.searchText.toLowerCase())) {
      state.searchResult.push({
        pageNum,
        str: item.str,
        itemIndex: index,
      })
    }
  })
}

2、展示第一個匹配項所在頁面內容。

// 文檔內容與搜索內容匹配到的數(shù)量
state.searchCount = state.searchResult.length
if (state.searchCount > 0) {
  // 根據(jù)當前匹配的索引獲取第一個匹配項(便于設置深色的高亮,清晰的展示當前匹配的位置)
  const searchItem = state.searchResult[state.searchIndex]
  if (state.currentPage === searchItem.pageNum) {
    // 
    searchHeightLight(searchItem)
  } else {
    state.currentPage = searchItem.pageNum
    // 跳轉頁面
    await getPage()
  }
}

3、獲取當前頁面內容中所有與搜索內容匹配的子項

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())
    ) {
      // 找到包含搜索內容的span元素,對搜索詞進行高亮
      spanSearchTextHandle(span, index, searchItem)
    } else {
      span.style.backgroundColor = ''
    }
  })
}

4、對匹配的子項中的搜索詞進行高亮

首次匹配會將所有匹配內容從 文本 -> span ,顏色為淺色高亮rgba(255, 255, 0, 0.5),第一個匹配項顏色為深色高亮rgba(255, 164, 0, 0.5)

// 替換span元素中的搜索文本為高亮span元素
const spanSearchTextHandle = (span, index, searchItem) => {
  let target = span.innerHTML
  
  // 找到搜索內容的位置
  let replaceIndex = target.toLowerCase()
    .indexOf(state.searchText.toLowerCase())
  
  // 找到需要高亮的原內容【為了保證英文大小寫都能匹配到,不能直接使用搜索內容進行替換高亮】
  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>`,
  )

  // 對當前匹配項進行更深色的高亮,使用戶感知更清晰
  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)

當用戶切換匹配項時【點擊“上一個/下一個”按鈕】,對匹配索引進行計算

// 上一個
const searchPrev = () => {
  if (state.searchIndex > 0) {
    state.searchIndex--
    // 保證不在當前頁的匹配項能夠絲滑跳轉進入
    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')) {
    // 找到當前匹配項
    if (index === searchItem.itemIndex) {
      // 替換深色高亮
      let newHtml = target.replace(
        'rgba(255, 255, 0, 0.5)',
        'rgba(255, 164, 0, 0.5)',
      )
      span.innerHTML = newHtml
      // 平滑移動到目標位置
      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ù)組內容,恢復文本層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
})
// 恢復
parent.appendChild(textLayerDiv)

選中高亮

基礎功能:選中高亮、擦除高亮

選中高亮功能實現(xiàn)

功能設計:

  • 選中文本,可以設置不同的顏色
  • 對于已設置過顏色的文本,可再次選中更換其他顏色

關鍵:

  • 保證dom結構扁平化
  • 跨標簽選中高亮

分析:

選中高亮也嘗試過通過搜索高亮的方式實現(xiàn),但是考慮到文本層是通過絕對定位實現(xiàn),在文本層父節(jié)點實現(xiàn)dom扁平化獲取定位困難、在文本子項下實現(xiàn)也會導致高亮與后面的文本重疊,導致后面文本選中問題。

最終可行實現(xiàn)

再增加一層,保證與文本層重疊,置于文本層的下方,保證文本層可選中。

通過getClientRects()獲取選中文本相對于視口的矩形信息(含位置以及寬高),并且該方法能夠自動處理跨標簽的選中內容,返回多個矩形信息。

結合getBoundingClientRect()獲取整個文本層相對于視口的矩形信息,計算出選中文本的精確位置以及寬高,增加到數(shù)組中,再渲染到頁面,這樣的處理邏輯就非常簡單,干凈利落。

實現(xiàn):

設置鼠標移動監(jiān)聽

const highLight = () => {
  state.isLight = !state.isLight
  if (state.isLight) {
    textLayerDiv.addEventListener('pointerup', handleMouseUp)
  } else {
    // 不使用時需要移除,避免重復創(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)
  // 確保文本層包含選中內容
  if (!textLayerDiv.contains(range.commonAncestorContainer)) return
  // 高亮處理
  hightLightHandle(range)
  // 移除選中
  selection.removeAllRanges()
}

直接計算選中內容的矩形信息,添加到數(shù)組中

// 獲取選中內容相對于視口的矩形信息
const reacts = Array.from(range.getClientRects())
// 獲取文本層相對于視口的矩形信息
const textLayerRect = textLayerDiv.getBoundingClientRect()

reacts.forEach((react) => {
  state.hightLightList.push({
    page: state.currentPage,  // 記錄高亮內容所在頁碼
    style: {   // 后期可直接在此設置不同的背景顏色
      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)

功能設計: 可對高亮內容部分擦除

實現(xiàn)思路:通過遍歷,對目標高亮項進行截斷/刪除,再通過計算將剩余的未選中高亮塊增加到數(shù)組

分析:

確保選中范圍與高亮塊有交集,那么選中范圍相對于高亮塊的位置有可能三種情況【左相交,包含,右相交】

情況一:選中的開始位置在高亮塊開始位置的右側,選中的結束位置在高亮塊開始位置的右側

該情況的判斷前提條件為

reactLeft > itemLeft && reactLeft < itemLeft + itemWidth && reactLeft + react.width > itemLeft

選中的可能情況① 為包含狀態(tài),需要對高亮進行截斷,設置當前item的寬度,并向數(shù)組內再push后半段高亮。

其他選中情況為相交,只需要修改當前item的寬度。

// 選中范圍右側在高亮范圍內
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`

情況二:選中的開始位置與高亮塊的開始位置相同,選中的結束位置在高亮塊開始位置的右側

該情況的判斷前提條件為

reactLeft == itemLeft && reactLeft < itemLeft + itemWidth && reactLeft + react.width > itemLeft

由圖可已看出,選中的可能情況① 需要修改當前item的left和width,在reactLeft + react.width >= itemLeft + itemWidth的情況下,需要刪除該高亮【注意此時處于循環(huán)中,無法對數(shù)組本身進行刪除,所以收集索引】

// 選中范圍 完全包裹住 高亮范圍 的 先收集索引,遍歷結束后再刪除。避免刪除后索引錯亂
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`
}

情況三:選中的開始位置在高亮塊開始位置的左側,選中的結束位置在高亮塊開始位置的右側

該情況的判斷前提條件為

reactLeft < itemLeft && reactLeft < itemLeft + itemWidth && reactLeft + react.width > itemLeft

由圖可已看出,本質與情況二一致,所以處理邏輯可復用。

最終實現(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) => {
    // 排除非當前頁的高亮項
    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) {
        // 選中范圍右側在高亮范圍內
        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 {
        // 選中范圍 完全包裹住 高亮范圍 的 先記錄索引,遍歷結束后再刪除。避免刪除后索引錯亂
        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(精度校準)的項 刪除
  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')
}

總結 

到此這篇關于vue基于pdf.js實現(xiàn)pdf文件預覽的文章就介紹到這了,更多相關vue pdf.js實現(xiàn)pdf文件預覽內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論

武宁县| 临清市| 乐昌市| 肇源县| 定兴县| 东宁县| 五台县| 综艺| 池州市| 马鞍山市| 德安县| 方山县| 景洪市| 岐山县| 仲巴县| 四会市| 浙江省| 洛浦县| 连山| 澄江县| 芜湖县| 手游| 汨罗市| 信阳市| 嘉兴市| 玉树县| 丹寨县| 鸡东县| 璧山县| 婺源县| 金川县| 页游| 讷河市| 台山市| 平乡县| 永仁县| 深泽县| 嘉荫县| 广丰县| 商水县| 合山市|