Vue整合pdfjs实现前端拖拽盖章操作

4,514 阅读3分钟

vue整合pdfjs

浏览器是能直接接收pdf文件流的,pdf以iframe的格式嵌入到页面中。以防有需要的朋友,代码示例如下:

     /**
      sealPdfApi:接口,参数为data,返回值为pdf文件流;
      */
      sealPdfApi(data).then(res => {
        if (window.navigator && window.navigator.msSaveOrOpenBlob) {
          this.$notify.error({ title: "请使用google浏览器" });
        } else {
          var blob = new Blob([res], { type: "application/pdf" });
          this.pdfViewer = URL.createObjectURL(blob);//创建的pdf链接
          window.open(this.pdfViewer);//打开窗口,也可以页面使用iframe,src等于上面的pdf链接
        }
      })
     

使用以上方法需要特别注意的一点就是接口调用时,响应类型必须是blob类型,一般可以在项目的通用接口配置中修改。

    responseType: 'blob'

虽然这样的方法能正确显示pdf,但是对于公司的盖章需求有几个问题。

  • 拖拽盖章,一个pdf可能需要盖多个章
  • 盖章是需要调用后端接口的,接口参数需要每个印章在pdf中的位置 针对上面的需求,我一开始的想法是将想在iframe的外面包一层div,利用印章在div中的位置去获取对应的pdf坐标位置。但是这样另一个问题很难解决,如何在盖完章之后将章保留在页面上。想出来的方法相对来说都比较麻烦,感兴趣的大佬们可以尝试研究一下。

最终决定了一个方案,整合pdfjs,讲pdf用canvas的方式在页面上显示,在canvas的外层包裹一个div,获取印章在div块中的位置,在canvas中使用 ctx.drawImage()方法来讲印章绘制到pdf中。 以下是整合pdfjs的组件部分代码,项目地址我会放在文章的底部:

export default {
    name: 'Pdf',
    props: {
        url: {
            type: String,
            default: ''
        },
        type: {
            type: String,
            default: 'canvas'
        },
        pdfjsDistPath: {
            type: String,
            default: '.'
        }
    },
    data() {
        return {
            pdfViewer: null,
            pdfLinkService: null,
            currentScale: '1.0',//缩放比例
            loadingTask: null
        }
    },

    methods: {
        onPagesInit({ source }) {
            source.currentScaleValue = this.currentScale
        },
        async pdfLibInit() {
            let pdfjsLib = window.pdfjsLib;
            let pdfjsViewer = window.pdfjsViewer
            if (!pdfjsLib || !pdfjsViewer) {
                try {
                    await getPdfjsDist(this.pdfjsDistPath)
                    this.CMAP_URL = `${this.pdfjsDistPath}/pdf/cmaps/`;
                    // console.log( this.CMAP_URL)
                    pdfjsLib = window.pdfjsLib;
                    pdfjsLib.GlobalWorkerOptions.workerSrc = `${this.pdfjsDistPath}/pdf/build/pdf.worker.js`
                    pdfjsViewer = window.pdfjsViewer
                } catch (error) {
                    // console.log(error)
                    // pdfjs文件获取失败
                    return
                }
            }

            const container = this.$refs.container
            const eventBus = new pdfjsViewer.EventBus();

            // (Optionally) enable hyperlinks within PDF files.
            const pdfLinkService = new pdfjsViewer.PDFLinkService({
                eventBus: eventBus,
            });
            this.pdfLinkService = pdfLinkService
            const pdfViewer = new pdfjsViewer.PDFViewer({
                container: container,
                eventBus: eventBus,
                linkService: pdfLinkService,
                renderer: this.type,
                textLayerMode: 0,
                downloadManager: new pdfjsViewer.DownloadManager({}),
                enableWebGL: true
            });
            this.pdfViewer = pdfViewer
            pdfLinkService.setViewer(pdfViewer);

            eventBus.on("pagesinit", this.onPagesInit);
        },
        renderPdf() {
            if (!this.url) { return }
            // Loading document.
            if (this.pdfViewer === null || this.pdfLinkService === null) {
                return
            }
            if (this.loadingTask !== null) {
                this.loadingTask.destroy()
                this.loadingTask = null
            }
            this.loadingTask = window.pdfjsLib.getDocument({
                cMapUrl: this.CMAP_URL,
                cMapPacked: true,
                url: this.url,
            });
            return this.loadingTask.promise.then((pdfDocument) => {
                if (pdfDocument.loadingTask.destroyed || !this.url) { return }
                this.pdfViewer.setDocument(pdfDocument)
                this.pdfLinkService.setDocument(pdfDocument, null);
                this.loadingTask = null
            }).catch(error => {
                console.log(error)
            });
        }
    },
    mounted() {
        this.pdfLibInit().then(() => {
            this.renderPdf()
        })
    },
    watch: {
        url() {
            // 如果存在pdfViewer则取消渲染
            if (this.pdfViewer) {
                this.pdfViewer._cancelRendering()
            }
            this.renderPdf()
        }
    },
    render() {
        return (
            <div class="pdf-view">
                <div id="viewerContainer" ref="container">
                    <div id="viewer" class="pdfViewer"/>
                </div>
            </div>
        )
    }
}

以上代码会将pdf以canvas形式渲染到页面上。 组件调用代码如下:

<pdf :url="pdfUrl" :type="'canvas'" :pdfjsDistPath="'/static'" ref="pdf"/>

pdfUrl是pdf文件地址,如果后端返回的是文件流类型,就使用上述步骤整合出来的地址。pdfjsDistPath是pdfjs文件放置地址,这里我直接放到了vue项目的 static 中。(附带:该目录下的文件是不会被wabpack处理的,它们会被直接复制到最终的打包目录下面(默认是 dist/static ),且必须使用绝对路径来引用这些文件。任何放在 static 中的文件需要以绝对路径的形式引用:/static/[filename]。)

印章拖拽

实现拖拽的要素就是让图片脱离文档流。我将图片跟pdf组件放在同一个div下,给div的position设置relative,再给印章图片的position设置为absolute。

接下来要实现的是鼠标在移入pdf中时,印章出现,并且位置随着鼠标移动。给外层div加一个mouseMove事件。

    move(e) {
      if (this.signUrl !== "") {
        var pdfContainer = document.getElementById("pdf-container");
        var scrollY = pdfContainer.scrollTop;
        var moveX = e.clientX - pdfContainer.offsetLeft - 200;
        var moveY = e.clientY - pdfContainer.offsetTop + scrollY;
        var SignImg = document.getElementById("pdf-sign-img");
        if (moveX < 0) {
          SignImg.style.left = 0 + "px";
        } else if (
          moveX >
          document
            .getElementsByClassName("canvasWrapper")[0]
            .getElementsByTagName("canvas")[0].offsetWidth -
            SignImg.offsetWidth +
            11
        ) {
          SignImg.style.left =
            document
              .getElementsByClassName("canvasWrapper")[0]
              .getElementsByTagName("canvas")[0].offsetWidth -
            SignImg.offsetWidth +
            11 +
            "px";
        } else {
          SignImg.style.left = moveX + "px";
        }
        SignImg.style.top = moveY + "px";
      }
    },

以上代码中需要注意的是处理鼠标移动的边界问题,确保印章是在pdf所在的第一层div中移动。

盖章

盖章的操作主要在于找到正确的坐标点,然后调用canvas的drawImage方法。 这里有一个问题,就是渲染出来的页面,每一页对应一个canvas,首先我们需要计算出盖章位置对应的是页数,然后用查找到对应的dom元素下的canvas。

image.png 查找页数的方法就是目前图章所在的位置除以每一页的高度取整。

            var pageIndex = 0;
            var myCanvas = document
              .getElementsByClassName("canvasWrapper")[0]
              .getElementsByTagName("canvas")[0];
            if (CanvasTop > myCanvas.offsetHeight) {
              //获取当前印章所在页数
              pageIndex = Math.floor(CanvasTop / myCanvas.offsetHeight);
              myCanvas = document
                .getElementsByClassName("canvasWrapper")[pageIndex].getElementsByTagName("canvas")[0];
            }

找到对应的canvas之后,接下来需要计算每个印章在canvas中对应的坐标,x坐标比较容易计算,不管是在每一页都是不变的, e.clientX - odiv.offsetLeft;

y坐标需要用图章的纵坐标减去每一页的高度乘页数。

ctx.drawImage(img,CanvasLeft - 11,CanvasTop - myCanvas.offsetHeight * pageIndex);

这样的话我们就能将图片画在对应的canvas上面了。 后端需要盖章的坐标时也可以在盖章之后传递,需要注意的是,可能后端的pdf大小跟前端绘制的pdf大小不一致,这个时候就需要比例换算了。具体的话视情况而定,本文主要是提供一个前端盖章的思路。

撤回操作实现

项目需求中,还有一个撤回的操作。这里因为没有比较好的方案,我直接在每次盖章完成之后,使用getImageData方法,将盖章那一页的canvas像素数据存储到数组中。

      if (this.orignCanvas.length > 0) {
        var backCanvasInfo = this.orignCanvas[this.orignCanvas.length - 1];
        var canvas = document.getElementsByClassName("canvasWrapper")[backCanvasInfo.pageIndex].getElementsByTagName("canvas")[0];
        var ctx = canvas.getContext("2d");
        ctx.putImageData(backCanvasInfo.canvansImg, 0, 0);
        this.orignCanvas.pop();
        this.sealInfo.pop();
      } else {
        this.$notify.error({ title: "撤回到最后一步了" })
      }
    },

demo代码已经发布到我的github上,有兴趣的童靴可以看一下,还有许多可以优化的地方跟一些问题,比如浏览器比例或者是电脑显示比例不是百分之百的话,会出现盖章位置错乱的情况,欢迎大佬指点!

github.com/jingjiupro/…