移动端集成pdfjs预览pdf文件

584 阅读3分钟

背景

近来,在微信公众号集成的 H5 页面开发工作中,遇到了需要实现 PDF 文件预览功能的需求。为了找到合适的解决方案,我们先后尝试了 VueOfficePdfpdfjs-dist 这两种不同的实现方式,它们均具备在移动端进行 PDF 预览的基本能力。

项目伊始,我们选用了 VueOfficePdf 组件。该组件在功能实现上看似能够满足需求,但在实际应用时却暴露出明显问题。由于其在处理文件缩放比例(scale)时精度不足,使得 PDF 文件在 <canvas> 元素上渲染后呈现出模糊不清的效果,极大地影响了用户对文件内容的查看体验,显然无法达到项目预期的要求。 鉴于此,我们将目光转向了 pdfjs-dist。然而,使用该方案的过程并不顺利,途中遭遇了一系列棘手的问题和挑战。但凭借团队的不懈努力与反复调试,最终成功基于 pdfjs-dist 实现了稳定、清晰的 PDF 预览功能。

此外,我们还尝试了另一种同样基于 PDF.js 的实现方式,即通过访问 https://mozilla.github.io/pdf.js/web/viewer.html?url=xxxx 来预览指定 URL 的 PDF 文件。优先推荐这种方式原生组件预览方式。 最新的版本如果使用报错,尝试下载历史版本的兼容性版本:

image.png

集成pdfjs

使用的pdfjs版本:www.npmjs.com/package/pdf…

image.png

  1. 在项目根目录下运行以下命令

npm install pdfjs-dist

  1. 然后在项目代码中引入
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import pdfjsWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry';

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

注意这里的引入是 * as pdfjsLib 而不是 pdfjsLib from 'pdfjs-dist',这里我们使用legacy下的资源。

pdfjs-dist/legacy

  • 代码结构可能相对复杂,因为它需要兼容多种旧环境,可能会包含一些冗余的代码或者兼容性补丁。
  • 文件内容可能使用了一些旧的 JavaScript 语法和特性,例如 var 声明变量、function 关键字定义函数等。

pdfjs-dist/build

  • 代码结构更加简洁和现代化,采用了最新的 JavaScript 模块化规范,例如 ES6 模块(import 和 export)。
  • 文件内容可能使用了一些新的 JavaScript 特性,如箭头函数、const 和 let 声明变量等,以提高代码的可读性和性能。
  1. 自定义预览组件完整代码如下
<template>
  <div class="pdf-container">
    <canvas v-for="(page, index) in pages" :key="index" :ref="'pdfCanvas' + index"></canvas>
  </div>
</template>

<script>
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import pdfjsWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry';

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

export default {
  name: 'PdfViewer',
  props: {
    pdfUrl: {
      type: String,
      required: true,
    }
  },
  data() {
    return {
      numPages: 0,
      pages: []
    }
  },
  mounted() {
    this.renderPdf()
  },
  methods: {
    renderPdf() {
      const loadingTask = pdfjsLib.getDocument(this.pdfUrl)
      loadingTask.promise.then(pdf => {
        this.numPages = pdf.numPages
        this.pages = Array.from({ length: this.numPages }, (_, index) => index + 1)
        // 使用 nextTick 确保 DOM 渲染完成后再渲染页面
        this.$nextTick(() => {
          for (let pageNumber = 1; pageNumber <= this.numPages; pageNumber++) {
            this.renderPageHighRes(pdf, pageNumber)
          }
        })
      }).catch(error => {
        console.error('Error loading PDF:', error)
      })
    },
    renderPageHighRes(pdf, pageNumber) {
      const highScale = 4 // 设置精度
      pdf.getPage(pageNumber).then(page => {
        const viewport = page.getViewport({ scale: highScale })
        // 获取对应的 canvas 元素
        const canvasArray = this.$refs['pdfCanvas' + (pageNumber - 1)];
        // 线上返回的是数组,不知道什么原因,本地返回的是:canvas对象 TODO
        const canvas = Array.isArray(canvasArray) ? canvasArray[0] : canvasArray;
        if (canvas) {
          const context = canvas.getContext('2d')
          canvas.height = viewport.height
          canvas.width = viewport.width
          const renderContext = {
            canvasContext: context,
            viewport: viewport
          }
          page.render(renderContext).promise.catch(error => {
            console.error(`Error rendering high res PDF page ${pageNumber}:`, error)
          })
        } else {
          console.error(`Canvas element for page ${pageNumber} not found.`)
        }
      }).catch(error => {
        console.error(`Error getting PDF page ${pageNumber}:`, error)
      })
    }
  }
};
</script>

<style scoped>
.pdf-container {
  height: 100vh;
  overflow-y: auto;
}

canvas {
  width: 100%;
  height: auto;
  display: block;
  margin-bottom: 10px;
  transform: translate3d(0, 0, 0); /*触发硬件加速*/
}
</style>
  1. 引入与使用组件
import PdfViewer from '@/components/pdf/PdfViewer.vue';

export default {
  components:{
    PdfViewer
  }
}

// 使用组件
<PdfViewer pdfUrl="http://www.hnsdzjy.com/tpframe/AttachStorage/202412/PDF/bbb382ea-fe2d-
4303-8716-59c9bd6b47ce/18b42914-4eba-47e4-bcaa-f659e1474bef.pdf"/>
  1. 效果图

image.png

集成原生组件

image.png

image.png