首页详情

PDF文件预览

Vue
in
admin
28天前
43次浏览

PDF.js 是一个使用 HTML5 构建的可移植文档格式(PDF)库。是创建一个通用的、基于 Web 标准的平台,用于解析和渲染 PDF 文件。

首先安装pdfjs-dist插件

bash
npm install pdfjs-dist

然后引入插件和样式

js
import * as pdfjsLib from 'pdfjs-dist'
import 'pdfjs-dist/web/pdf_viewer.css'

pdf.js 在解析和渲染 PDF 时,计算量比较大。为了避免阻塞主线程(UI 卡顿),需要把PDF的解析任务放在Web Worker里执行。

所以我们需要手动配置pdfjsWorker。

js
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker

如果不设置在浏览器端运行时,pdf.js 会报错

error
Warning: Setting up fake worker.

这意味着它找不到 worker 文件,只能退回到“假 worker”模式(实际还是在主线程执行),性能会下降。

接下来我们写一下渲染pdf的核心方法

vue
<template>
  <div
    ref="pdfContainer"
    class="pdf-viewer"
  />
</template>
js
async function renderPdfFile() {
  const container = this.$refs.pdfContainer
  container.innerHTML = ''
  const loadingTask = pdfjsLib.getDocument(this.src)
  const pdf = await loadingTask.promise
  for (const pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
    const page = await pdf.getPage(pageNum)
    const viewport = page.getViewport({ scale: 1 })
    const canvas = document.createElement('canvas')
    canvas.style = 'margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);'
    const context = canvas.getContext('2d')
    const dpr = window.devicePixelRatio || 1
    canvas.width = viewport.width * dpr
    canvas.height = viewport.height * dpr
    canvas.style.width = `${viewport.width}px`
    canvas.style.height = `${viewport.height}px`
    const transform = [dpr, 0, 0, dpr, 0, 0]
    const renderContext = {
      canvasContext: context,
      transform,
      viewport
    }
    await page.render(renderContext).promise
    container.appendChild(canvas)
  }
}

上述方法主要在做以下几点

  1. 加载PDF:使用pdfjsLib异步加载指定URL的PDF文件
  2. 遍历页面:循环处理PDF中的每一页
  3. 创建画布:为每页创建canvas元素并设置样式
  4. 适配分辨率:根据设备像素比调整画布尺寸
  5. 渲染页面:将PDF页面渲染到canvas上
  6. 添加到容器:将渲染好的canvas添加到指定容器中显示

渲染PDF的工作基本完成,但有时候我们的PDF文件是加密的怎么办呢?

pdfjsLib.getDocument提供了第二种传参方式,接受一个对象,对象中传url和password属性,并提供了onPassword事件。

js
const loadingTask = pdfjsLib.getDocument({
  url,
  password
})
js
loadingTask.onPassword = (updatePassword, reason) => {
  this.passwordCallback = updatePassword
  this.message = reason === pdfjsLib.PasswordResponses.NEED_PASSWORD
    ? '本文档设置了密码保护,请输入密码。'
    : '密码错误,请重新输入。'
}

当pdfjsLib加载PDF文件时,文件需要输入密码或者密码错误时会触发onPassword回调

最后我们封装一个预览PDF的vue组件。组件接受一个PDF文件的url。组件中用一个modal弹框组件,用来输入密码。

vue
<template>
  <div>
    <div
      ref="pdfContainer"
      class="pdf-viewer"
    />
    <e-modal
      title="需要密码"
      :width="420"
      v-model="showPasswordModal"
      :closable="false"
    >
      <div style="line-height: 36px;">{{ message }}</div>
      <e-input
        type="password"
        password
        :clearable="false"
        v-model="password"
        @on-enter="handleConfirm"
      />
      <template slot="footer">
        <div style="display: flex;justify-content: flex-end;">
          <e-button type="primary" @click="handleConfirm">确认</e-button>
        </div>
      </template>
    </e-modal>
  </div>
</template>
<script>
import * as pdfjsLib from 'pdfjs-dist'
import 'pdfjs-dist/web/pdf_viewer.css'
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
export default {
  name: 'PdfViewer',
  props: {
    url: String
  },
  data() {
    return {
      showPasswordModal: false,
      password: '',
      message: '',
      passwordCallback: null
    }
  },
  watch: {
    url: {
      handler() {
        this.$nextTick(() => {
          this.renderPdfFile()
        })
      },
      immediate: true
    }
  },
  methods: {
    handleConfirm() {
      if (this.passwordCallback && this.password) {
        const cb = this.passwordCallback
        this.passwordCallback = null
        cb(this.password)
      }
    },
    async renderPdfFile(password = null) {
      const container = this.$refs.pdfContainer
      container.innerHTML = ''
      try {
        const loadingTask = pdfjsLib.getDocument({
          url: this.url,
          password
        })
        loadingTask.onPassword = (updatePassword, reason) => {
          this.password = ''
          this.passwordCallback = updatePassword
          this.message = reason === pdfjsLib.PasswordResponses.NEED_PASSWORD
            ? '本文档设置了密码保护,请输入密码。'
            : '密码错误,请重新输入。'
          if (!this.showPasswordModal) {
            this.showPasswordModal = true
          }
        }
        const pdf = await loadingTask.promise
        if (this.showPasswordModal) {
          this.showPasswordModal = false
        }
        for (const pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
          const page = await pdf.getPage(pageNum)
          const viewport = page.getViewport({ scale: 1 })
          const canvas = document.createElement('canvas')
          canvas.style = 'margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);'
          const context = canvas.getContext('2d')
          const dpr = window.devicePixelRatio || 1
          canvas.width = viewport.width * dpr
          canvas.height = viewport.height * dpr
          canvas.style.width = `${viewport.width}px`
          canvas.style.height = `${viewport.height}px`
          const transform = [dpr, 0, 0, dpr, 0, 0]
          const renderContext = {
            canvasContext: context,
            transform,
            viewport
          }
          await page.render(renderContext).promise
          container.appendChild(canvas)
        }
      } catch (err) {
        console.error('pdf加载失败', err)
      }
    }
  }
}
</script>
<style lang="less" scoped>
.pdf-viewer {
  width: 100%;
  max-height: 100vh;
  overflow-y: auto;
  background-color: #f5f5f5;
  padding: 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>

效果图