移动端pdf预览-水印&电子签章问题

10,622 阅读1分钟

一、背景

记录一下在开发pdf文件预览时遇到的问题,主要是水印和电子签章问题。由于网上也查了不了方案,但解决方案都不适用在生产环境,所以决定贴一下自己的方案。

二、方案介绍

目前主流的移动端pdf文件预览库都是基于mozilla的pdf.js,本文要介绍的两个库也不例外,分别是react的react-pdf和vue的vue-pdf

闲话少说,直接上代码

1.react-pdf方案,以下代码就完成一个pdf的预览组件了

import { Document, Page } from 'react-pdf';

export default function PdfPreview({ fileUrl }: Props) {
  const [numPages, setNumPages] = React.useState(null);
  function onDocumentLoadSuccess(document) {
    const { numPages } = document;
    setNumPages(numPages);
  }
  return (
    <Document
      file={fileUrl}
      onLoadSuccess={onDocumentLoadSuccess}
      loading="意向书加载中..."
    >
      {
        Array.from(
          new Array(numPages),
          (el, index) => (
            <Page
              key={`page_${index + 1}`}
              pageNumber={index + 1}
            />
          ),
        )
      }
    </Document>
  )
}

2.vue-pdf方案

<template>
  <div class='box' style="background: #fff">
      <pdf 
        v-for="i in numPages"
        :key="i"
        :src="src"
        :page="i"
      ></pdf>
    </div>
</template>
<script>
import pdf from 'vue-pdf'

export default {
    components: { pdf },
    data() {
    	return {
          src: pdfUrl,
          numPages: undefined,
    	}
    },
    mounted() {
      this.src.promise.then(pdf=>{
        this.numPages = pdf.numPages;
      }).catch(err=>{
        console.log('err',err);
      }
    },
}

</script>

上面两种方案就能完成基本的多页pdf文件在线预览了,美中不足的是当pdf文件中想添加水印和电子章时满足不了,特别是对于一些交易场景或者电子合同场景。如果你默认使用了水印和电子签章,在控制台会看到有这样的warning

Warning: Unimplemented widget field type "Sig", falling back to base field type. pdf.js:581 和 Warning: Error during font loading: The CMap "baseUrl" parameter must be specified, ensure that the "cMapUrl" and "cMapPacked" API parameters are provided.

第一个warning报的是关于电子签章的问题,第二个warning报的是水印的问题,带着这两个问题去搜索引擎一搜,会有很多的结果,循着这些提示,你翻到这两个库的源码及库依赖的pdfjs-dist源码,然后你会发现,签章的问题是因为下面这一段代码

if (data.fieldType === 'Sig') {
  data.fieldValue = null;

  _this3.setFlags(_util.AnnotationFlag.HIDDEN);
}

注释掉,电子章就正常出来了,这个方法对react-pdfvue-pdf库都有效,但隐隐感觉那里不对,注释掉的是本地的代码,生产环境怎么办?且看下文。

而水印的问题是因为这两个库中的字体文件(pdfjs-dist/cmaps)在webpack打包后并不会被打包进来(react-pdfvue-pdf的情况还不一样,下面会补充)。解决方案也很简单,一种是通过webpack的方式把字体文件打包到静态文件中,并引入;另一种是直接引入对应字体文件的cdn地址,或官方提供的https://unpkg.com/pdfjs-dist@2.0.943/cmaps/,代码如下(只截取部分,其余部分和上面的代码相同):

react-pdf
<Document
      file={fileUrl}
      onLoadSuccess={onDocumentLoadSuccess}
      loading="意向书加载中..."
      options={{
        cMapUrl: 'https://unpkg.com/pdfjs-dist@2.0.943/cmaps/',
        cMapPacked: true,
      }}
    >

如果你用的是vue,到这里你会发现vue-pdf没提供类似的方法,哭...不管怎样,薪水得拿,需求就要解决,于是你从githug上的readme文档中的api区域发现有这样的字眼,PDFJS.getDocument().带着好奇和编码的直觉,你觉得需要点进去看看,然后发现这个方法里有很多的参数,其中一个和字体相关,即:

/*
 * @property {Object} CMapReaderFactory - (optional) The factory that will be
 *   used when reading built-in CMap files. Providing a custom factory is useful
 *   for environments without `XMLHttpRequest` support, such as e.g. Node.js.
 *   The default value is {DOMCMapReaderFactory}.
 */

然后结合网上看到的蛛丝马迹,vue-pdf库下的水印问题也愉快解决了,上代码:

import CMapReaderFactory from 'vue-pdf/src/CMapReaderFactory.js'
data() {
    return {
      src: pdf.createLoadingTask(
        {
          url: pdfUrl,
          CMapReaderFactory,
        }
      ),
    }
},

如图(水印已经显示出来了,红色标注是打码):

到这里,水印是解决了,签章呢...革命尚未成功,同志仍需要努力,继续雄起coding...

三、终章

关于签章,注释库源码是行不通了,但是显然得通过注释掉对应的代码,去看mozillia/pdfjsissue,也说了,出于一些原因将签章功能屏蔽了,所以我们的方法也是注释代码,只是换一种方法,将库依赖的部分代码抽出来,注释后再行引用,比如上传cdn后再引用这部分代码。巧的是,react-pdf留有这样的口子,用以引用相应的代码,如下:

import { Document, Page, pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/build/pdf.worker.min.js';

react-pdf中多import一个pdfjs,然后换上自己注释掉以下代码的worker.js

if (data.fieldType === 'Sig') {
  data.fieldValue = null;

  _this3.setFlags(_util.AnnotationFlag.HIDDEN);
}

上面的cdn地址 worker.min.js是我贴的一个例子代码,是没有注释掉这部分代码的,有需要的同学自己去github上下载并自行去掉相应代码,有一个注意点就是用的pdfjs-distpdf.worker.min.js的版本最好一致,不然可能出现电子签章没出来的问题,到这react-pdfpdf文件预览水印和签章就完美解决了。等等,还有一个vue-pdf...

前方的坑虽然太凄迷,请在笑容里为我祝福...

vue-pdf默认只导出了pdf,也就是

import pdf from 'vue-pdf'

没有react-pdf那样的口子,可以通过workSrc来修改worker.js,从FJS.getDocument().方法里有看到PDFWorker这样的参数,可以像上面CMapReaderFactory那样传入,但是没搞定这部分,有搞定的同学欢迎分享一下

 * @property {PDFWorker}  worker - The worker that will be used for the loading
 *   and parsing of the PDF data.

只能另寻他路...还真给我找到了,话不多说,上代码:

<template>
  <div class='box' style="background: #fff">
    <div>
      <canvas v-for="page in pages" :id="'the-canvas'+page" :key="page"></canvas>
    </div>
  </div>
</template>
<script>

import PDFJS from 'pdfjs-dist';
PDFJS.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/build/pdf.worker.min.js';
//请注意,这里的cdn worker地址是贴的网上的,并且签章那部分代码是没有注释的,直接拿去用是达不到效果的,我也没有现成的资源可提供,只是贴一下方法

export default {
    data() {
    	return {
          pdfDoc: null,
          pages: 0,
    	}
    },
    methods: {
    _renderPage (num) {
      this.pdfDoc.getPage(num).then((page) => {
        let canvas = document.getElementById('the-canvas' + num)
        let ctx = canvas.getContext('2d');
        ctx.mozImageSmoothingEnabled = false;
        ctx.webkitImageSmoothingEnabled = false;
        ctx.msImageSmoothingEnabled = false;
        ctx.imageSmoothingEnabled = false;
        let dpr = window.devicePixelRatio || 1
        let bsr = ctx.webkitBackingStorePixelRatio ||
                  ctx.mozBackingStorePixelRatio ||
                  ctx.msBackingStorePixelRatio ||
                  ctx.oBackingStorePixelRatio ||
                  ctx.backingStorePixelRatio || 1
        let ratio = dpr / bsr
        let viewport = page.getViewport(screen.availWidth / page.getViewport(1).width);//这是让pdf文件的大小等于视口的大小
        canvas.width = viewport.width * ratio
        canvas.height = viewport.height * ratio//这里会进行压缩,解决模糊问题
        canvas.style.width = viewport.width + 'px'
        canvas.style.height = viewport.height + 'px'
        let renderContext = {
          canvasContext: ctx,
          viewport: viewport,
          transform: [ratio, 0, 0, ratio, 0, 0]//这里会进行放大,解决模糊问题
        }
        page.render(renderContext);
        if (this.pages > num) {
          this._renderPage(num + 1)
        }
      })
    },
    _loadFile (url) {
      PDFJS.getDocument({
        url,
        cMapUrl: 'http://cdn/fonts/cmaps/',//这里同样要引入字体解决水印问题,需自己提供
        cMapPacked: true
      }).promise.then((pdf) => {
        this.pdfDoc = pdf
        this.pages = this.pdfDoc.numPages
        this.$nextTick(() => {
          this._renderPage(1)
        })
      })
    }
},
  mounted() {
    this._loadFile(pdfUrl);
  }
}

</script>

四、总结

夜深了,想说的都在上面代码里了。需要留意的是,react-pdf或者vue-pdf,都是依赖于pdfjs-dist来完成各自的实现,所以如果两个库解决不了你的问题,不妨去看看上游依赖,有时候可能直接使用上游依赖就能达成你想要的效果,并且提供的能力更足。最后,总结来说这是一篇踩坑贴,由于自己在解决pdf预览问题时花了不少时间,因此总结梳理出来,希望有遇到相同问题的同学,能从此文得到一些帮助。