canvas结合pdfjs-dist在vue项目中预览pdf文件
前言
公司项目,需要在app的内嵌H5项目中,实现预览pdf文件的需求
思考
这个需求拿到手上的一瞬间,我是懵的,因为这是一个我从来没有涉足过的领域,故有了一下的一些思考:
1.预览pdf应该是有插件的,先搜索一波能在vue中使用的插件:结果有vue-pdf,pdfjs
2.再明确一下需求:可能会有多个pdf文件,每个pdf文件可能不止一页,那么,至少在第一层肯定是循环了
实践
最开始,是打算用vue-pdf的,这个玩意儿其实我也根本不会使用。但是吧,有搜索这个方便的工具在,这个问题都不是问题。于是乎,开始一波疯狂的搜索行为,然而,理想很丰满,现实很骨感,搜索到的方法,我没一个成功实现了需求的。
既然这个不行,那就再试试pdfjs这个最原始的吧(vue-pdf似乎就是基于这个弄出来的)。按照教程方法,先下载一波文件,然后解压,放到项目中。一顿操作猛如虎,一看输入只有5,哭泣。
没办法,继续百度找方法,最后找到了另外一种pdfjs的引入方式,即pdfjs-dist,我也不知道这个和pdfjs有啥不一样的,貌似就是一个东西。
看了下这个,不用下载源代码放到项目中,似乎操作也比较简单,于是再次开始实践
操作
第一步:引入
npm i pdfjs-dist 复制代码
其实这里也有个坑,我开始下载了最新版本的,但是出现了蜜汁bug,后面看了前辈大佬们的文章,找了一个指定版本2.2.228
第二步:使用 看了很多文章,都是结合了canvas来使用的,所以我也不会开辟一个新的使用方法(我不会啊,想开也开不动)
// html部分 <template v-if="pdfList.length > 0"> <div v-for="(pdfPages, index) in pdfList" :key="index" class> <canvas v-for="page in pdfPages" :key="page" :id="'pdfCanvas' + page" ></canvas> </div> </template> 复制代码
// 1.引入pdfjs import PDFJS from "pdfjs-dist"; created() { PDFJS.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.min.js"); // 不要问我为啥在这里写,我表示不是很清楚,我最开始也是看文章中写在import下面的,但是我试了下不行,所以试了一下写在这里,结果试ok的 } 复制代码
最开始,我并没有封装canvas部分,全部写在了一个组件中,除了造成组件代码量庞大以外,此时还未出现其他问题
// 请求接口,获取pdf文件流(数据是第三方的,而且还是个明文的链接,数据敏感,考虑到安全性,我们这边是不存服务器的,后端只是转发而已) async getFileUrl() { try { const res = await this.$http.get(url); console.log(res, "getFileUrl"); if (xxx.code === 200) { let urlList = xxx.data; urlList.forEach(item => { this.handlerUrl(item); }); return; } } catch (error) { console.log(error, "getFileUrl"); } }, // 数据除了pdf,还有ofd的,先忽略ofd吧 handlerUrl(item) { if (item.fileFormat === "PDF") { this.getPdfFile(item.licenseId); } else if (item.fileFormat === "OFD") { this.getOfdFile(item.licenseId); } }, 复制代码
接下来就是渲染pdf文件了
async getPdfFile(licenseId) { const url = `xxx`; try { const res = await this.$http({ url, method: "get", params: { licenseId }, responseType: "blob" }); // 将文件流处理一下,变成url if (res.status == 200) { const content = res.data; let file = new Blob([content], { type: "application/pdf;charset-UTF-8" }); const objectURL = URL.createObjectURL(file); this.pdfList.push(objectURL); this._loadFile(objectURL); return; } } catch (error) { console.log(error, "getPdfFile"); } }, _loadFile(url) { PDFJS.getDocument(url).promise.then(pdf => { this.pdfDoc = pdf; const pdfPages = this.pdfDoc.numPages; this.pdfList = pdfPages; this.$nextTick(() => { this._renderPage(1); }); }); }, _renderPage(num) { this.pdfDoc.getPage(num).then(page => { let canvas = document.getElementById("pdfCanvas" + num + this.index); var vp = page.getViewport({ scale: 1 }); let ctx = canvas.getContext("2d"); 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({ scale: window.innerWidth / vp.width }); canvas.width = viewport.width * ratio; canvas.height = viewport.height * ratio; canvas.style.width = viewport.width + "px"; canvas.style.height = viewport.height + "px"; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); let renderContext = { canvasContext: ctx, viewport: viewport }; console.log(canvas, "------"); if (canvas) { page.render(renderContext); } if (this.pdfList > num) { this._renderPage(num + 1); } }); } 复制代码
好家伙,本以为这样OK的,因为当时测试数据返回只有一条,渲染出来一点毛病都没有,非常的nice。然而吧,其实数组里面是可能存在多条的,所以此时高兴的太早了。
当数据出现了两条的时候,此时,问题来了,先看下控制台报错是什么
这,这,这,这怎么就报错了呢?然后页面渲染的pdf也是错乱的,直接旋转了180度,而且显示也不全了,Σ(っ °Д °;)っ
遇事不决先百度,发现,难道就没有人遇到过这个问题吗?还是说我这个需求比较奇葩?不行我再找找,然而,我失败了,我没找到。/(ㄒoㄒ)/~~
没办法,自己来吧。先翻译一波这是啥意思:
错误:在多个render()操作期间不能使用相同的画布。使用不同的画布或确保先前的操作已被取消或完成。 复制代码
操作期间不能使用相同的画布,emmmm,啥意思?我不是循环了吗?怎么会是同一个画布?
难道是高度问题?于是我设置了固定高度,失败。
难道是我循环嵌套的问题?于是我把canvas进行了封装,失败
<div v-if="pdfList.length > 0"> <div v-for="(pdfPages, index) in pdfList" :key="index" class="pdf-preview" > <CanvasPreviewPdf :pdfData="pdfPages" :index="index" /> </div> </div> 复制代码
等等,我似乎忽略了什么重要的信息,多个render操作,相同的画布,我的画布为什么会相同?这是个好问题。于是我仔细对比了一下渲染出来的canvas标签,好家伙,设置的id,渲染出来的时候是一样的,就是在父组件中循环的时候,里面的canvas,每一组的id都是一样的,如下图(这是我修改过后的,修改前,pdfCanvas10和pdfCanvas11都叫做pdfCanvas1,这也就意味着重复了),于是乎,我把父组件中循环的index拼接在了后面,问题解决,封装组件代码如下
<template> <div> <div v-for="page in pdfList" :key="page"> <canvas :id="'pdfCanvas' + page + index"></canvas> </div> </div> </template> <script> import PDFJS from "pdfjs-dist"; export default { name: "PreviewPdf", components: {}, data() { return { pdfList: [] }; }, props: { pdfData: { type: Object }, index: { type: Number } }, created() { PDFJS.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.min.js"); this.getPdfFile(this.pdfData.licenseId); }, methods: { // pdf文件 async getPdfFile(licenseId) { const url = ``; try { const res = await this.$http({ url, method: "get", params: { licenseId }, responseType: "blob" }); if (res.status == 200) { const content = res.data; let file = new Blob([content], { type: "application/pdf;charset-UTF-8" }); const objectURL = URL.createObjectURL(file); this.pdfList.push(objectURL); this._loadFile(objectURL); return; } } catch (error) { console.log(error, "getPdfFile"); } }, _loadFile(url) { PDFJS.getDocument(url).promise.then(pdf => { this.pdfDoc = pdf; const pdfPages = this.pdfDoc.numPages; this.pdfList = pdfPages; this.$nextTick(() => { this._renderPage(1); }); }); }, _renderPage(num) { this.pdfDoc.getPage(num).then(page => { let canvas = document.getElementById("pdfCanvas" + num + this.index); var vp = page.getViewport({ scale: 1 }); let ctx = canvas.getContext("2d"); 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({ scale: window.innerWidth / vp.width }); canvas.width = viewport.width * ratio; canvas.height = viewport.height * ratio; canvas.style.width = viewport.width + "px"; canvas.style.height = viewport.height + "px"; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); let renderContext = { canvasContext: ctx, viewport: viewport }; console.log(canvas, "------"); if (canvas) { page.render(renderContext); } if (this.pdfList > num) { this._renderPage(num + 1); } }); } } }; </script> <style scoped></style> 复制代码
后记
讲道理,真没想到是这么个问题。记录一下,或许能帮到和我遇到同样问题的伙伴
作者:路过的前端小菜鸟
链接:https://juejin.cn/post/7025960857427247111