Vue3 -- PDF展示、添加签名(带笔锋)、导出
文章目录
- 笔锋签名
- 方案一
- 实现要点
- 实现过程
- 组件引用
- 页面元素
- 添加引用
- 实现代码
- 效果展示
- 缺点
- 方案二
- 修改页面元素
- 替换引用
- 修改代码
- 效果展示
- 完整代码地址
实现功能的时候采用了两个方案,主要是第一个方案最后的实现效果并不太理想,但实现起来比较简单,要求不高时可以使用。
该 DEMO 会一次性加载并展示所有的 PDF 页面,目的是方便在手机上观看时上下滑动,如果要做成上一页下一页的效果,需要自行实现。
笔锋签名
我是用开源项目 smooth-signature 实现带笔锋签名的功能。
Gitee 地址是 https://github.com/linjc/smooth-signature
npm install --save smooth-signature
使用起来也比较简单,首先获取到需要操作的画布 canvas ,然后生成一个笔锋签名对象 SmoothSignature,optionSign 是初始化的一些简单属性。
const signature = new SmoothSignature(canvas, optionSign);
这样一来,我们的 canvas 就可以画线条了,同时我们可以通过 signature 去做一些操作,比如清空签名、撤回一步的操作等。
方案一
实现要点
- 读取 PDF 文件,并将 PDF 页面渲染到 Canvas 画布上,这里需要动态生成 Canvas
- 将每一个 Canvas 都包装成 SmoothSignature
- 添加一个标识,判断是否允许在 Canvas 上画线(手机滑动会和签名画线冲突,用按钮来控制什么时候允许画线)。
- 保存 PDF 时,先将每一个 Canvas 中的内容转化成图片格式 image/JPEG ,或者 image/PNG ,PNG格式的文件可能会比较大。
- 最后用生成的图片导出一个新的 PDF (实质上 PDF 每一页都是一张图片)。
实现过程
组件引用
smooth-signature | 笔锋签名 |
pdfjs-dist | PDF展示等功能 |
jspdf | PDF导出相关功能 |
npm install --save smooth-signature
npm install --save pdfjs-dist@2.0.943
npm install --save jspdf
页面元素
主要是读取文件、切至签名功能、切回预览功能、撤回签名、清除所有签名以及下载PDF的功能。
<template><div :class="`tab-header`"><div id="editor"><Input:class="`button-common`"type="file"ref="fielinput"accept=".pdf"id="fielinput"@change="uploadFile"/><Button :class="`button-common`" v-if="isSign" @click="handleSign">切回预览</Button><Button :class="`button-common`" v-else @click="handleSign">切至签名</Button><Button :class="`button-common`" @click="handleUndo">撤回</Button><Button :class="`button-common`" @click="handleClear">清除</Button><Button :class="`button-common`" @click="savePDF">下载PDF</Button></div><div><div id="parentDiv"><div ref="contentDiv" id="contentDiv"></div></div></div></div>
</template>
<script lang="ts">
引用
......
实现代码
......
</script>
<style lang="less" scoped>.tab-header {background: rgb(146, 175, 138);padding-left: 1%;padding-right: 1%;}.button-common {margin-right: 2px;max-width: 200px;}#contentDiv {// display: inline-block;}#parentDiv {position: absolute;overflow: auto;top: 5%;bottom: 1%;display: inline-block;}#signShower {position: absolute;left: 50%;top: 5%;bottom: 1%;display: inline-block;}
</style>
添加引用
这里要注意的是,需要给 pdfJS 指定工作路径
import { Button, Input } from 'ant-design-vue';import { defineComponent, ref } from 'vue';import SmoothSignature from 'smooth-signature';import * as pdfJS from 'pdfjs-dist';import * as pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';import JsPDF from 'jspdf';pdfJS.GlobalWorkerOptions.workerSrc = pdfjsWorker;
实现代码
代码中添加了主要的注释,可以查看下述代码
export default defineComponent({components: { Button, Input },setup() {const fielinput = ref(null);const contentDiv = ref(null);//签名相关const isSign = ref(false); //控制是否允许签名const canvass = ref([]); //保存所有画布元素const signatures = ref([]); //所有签名对象const historys = ref([]); //签名历史 用于回退或者清除,因为是一次性展示多个页面,会存在多个包装好的签名对象,存放历史列表方便操作//PDF展示相关const pdfData = ref(null); // PDF 内容const scale = ref(2); //放大比例 ,有的时候展示可能会比较模糊,可以放大展示//上传控件选择事件,加载选中的 PDF 文件const uploadFile = (e: Event) => {// 断言为HTMLInputElementconst target = e.target as HTMLInputElement;const files = target.files;let reader = new FileReader();reader.readAsDataURL(files[0]);reader.onload = () => {let data = atob(reader.result.substring(reader.result.indexOf(',') + 1));loadPdfData(data);};};//加载PDFfunction loadPdfData(data) {//移除所有旧的 Canvas 画布元素removeChild();//重置对象状态isSign.value = false;canvass.value = [];signatures.value = [];// 引入pdf.js的字体,如果没有引用的话字体可能会不显示let CMAP_URL = 'https://unpkg.com/pdfjs-dist@2.0.943/cmaps/';//读取base64的pdf流文件pdfData.value = pdfJS.getDocument({data: data, // PDF base64编码cMapUrl: CMAP_URL,cMapPacked: true,});//渲染全部页面renderAllPages();}//移除页面上旧的元素function removeChild() {var content = contentDiv.value;var child = content.lastElementChild;while (child) {content.removeChild(child);child = content.lastElementChild;}}//渲染全部页面function renderAllPages() {pdfData.value.promise.then((pdf) => {for (let i = 1; i <= pdf.numPages; i++) {pdf.getPage(i).then((page) => {let viewport = page.getViewport(scale.value);//动态生成 Canvas 画布并设置宽高var canvas = document.createElement('canvas');canvas.height = viewport.height;canvas.width = viewport.width;let ctx = canvas.getContext('2d');let renderContext = {canvasContext: ctx,viewport: viewport,};//将 PDF 页面渲染到 Canvas 上page.render(renderContext).then(() => {});//将画布包装成 SmoothSignatureinitSignatureCanvas(canvas);//将画布元素放入到 div 容器中展示canvass.value.push(canvas);contentDiv.value.appendChild(canvas);});}});}//初始化签名对象const initSignatureCanvas = (canvas) => {const optionSign = {width: canvas.width,height: canvas.height,maxHistoryLength: 100, //最大历史记录};const signature = new SmoothSignature(canvas, optionSign);//初始化时 先移除它内部添加的监听事件,默认不能签名signature.removeListener();//签名对象 addHistory 方法做一下修改,在原来逻辑的基础上添加这一行// historys.value.push(signature); 方便处理历史签名记录signature.addHistory = function () {if (!signature.maxHistoryLength || !signature.canAddHistory) return;signature.canAddHistory = false;signature.historyList.push(signature.canvas.toDataURL());signature.historyList = signature.historyList.slice(-signature.maxHistoryLength);historys.value.push(signature);};signatures.value.push(signature);};/*** 签名预览转换* 允许签名:调用 signature 对象中的 addListener 方法,添加监听事件* 不允许签名:调用 signature 对象中的 removeListener 方法,移除监听事件*/const handleSign = () => {isSign.value = !isSign.value;if (signatures.value && signatures.value.length > 0) {if (isSign.value) {for (let i = 0; i < signatures.value.length; i++) {signatures.value[i].addListener();}} else {for (let i = 0; i < signatures.value.length; i++) {signatures.value[i].removeListener();}}}};/*** 后退操作* 调用历史签名记录中的 signature 对象中的 undo 方法会撤回当前对象中的最后一次的画线记录* 注意:后退后不要忘记将列表中最后一个元素移除*/const handleUndo = () => {if (historys.value && historys.value.length > 0) {const signatureList = historys.value;let signature = signatureList.pop();signature.undo();historys.value = signatureList;}};// 清除所有 循环把所有签名历史都处理了const handleClear = async () => {while (historys.value && historys.value.length > 0) {handleUndo();}};// 下载PDFconst savePDF = () => {//生成新的 PDFlet pdf = new JsPDF('', 'pt', 'a4');if (canvass.value.length > 0) {//将 canvas 内容转化成 JPEGfor (let i = 0; i < canvass.value.length; i++) {const ccccc = canvass.value[i];let pageData = ccccc.toDataURL('image/JPEG');if (i > 0) {pdf.addPage();}pdf.addImage(pageData,'JPEG',0,0,ccccc.width / scale.value,ccccc.height / scale.value,);}//到处新的PDFreturn pdf.save('TestPdf.pdf');}};return {fielinput,uploadFile,contentDiv,isSign,handleSign,handleUndo,handleClear,savePDF,};},mounted() {},});
效果展示
缺点
1、生成的新的PDF每一页都是一个图片,这就表示 PDF 中的内容无法被解析,文字再也无法被选中了。
2、因为生成的是图片,所以最终效果可能会变模糊,可以通过放大比例去优化展示效果,但是始终不是一个最优的解决方案。
方案二
方案二使用一个新的组件 pdf-lib 来处理最后生成的 PDF
方案二仍旧使用 pdfjs-dist 在 Canvas 上展示 PDF,并使用 smooth-signature 使得画布拥有笔锋签名效果。
不同的是,这一次签名画布和 PDF 展示画布并不再是同一个画布,而是上下重叠的两个分离的画布
这样一来,我们可以将签名画布上的内容生成一个透明背景的 PNG 图片,然后以水印的方式添加到原来的 PDF 文件中。
修改页面元素
需要两个 Div 容器 ,父容器的滚动条需要同步滚动,否则会出现签名在滚动,但是 PDF 页面不动的情况
<template><div :class="`tab-header`"><div id="editor"><Input:class="`button-common`"type="file"ref="fielinput"accept=".pdf"id="fielinput"@change="uploadFile"/><Button :class="`button-common`" v-if="isSign" @click="handleSign">点击预览</Button><Button :class="`button-common`" v-else @click="handleSign">点击签名</Button><Button :class="`button-common`" @click="handleUndo">撤回</Button><Button :class="`button-common`" @click="handleClear">清除</Button><Button :class="`button-common`" @click="savePDF">下载PDF</Button></div><div><div id="parentDiv1"><div ref="contentDiv" id="contentDiv"></div></div><div id="parentDiv2"><div ref="signContentDiv" id="signContentDiv"></div></div></div></div>
</template>
替换引用
//import JsPDF from 'jspdf';import { PDFDocument } from 'pdf-lib';
修改代码
文章底部附完整代码
...
const signCanvass = ref([]); //保存所有签名画布
const base64 = ref(null); //读取的pdf的base64数据
上传文件的方法中添加一行保存PDF base64 ,生成新的 PDF 时使用
const uploadFile = (e: Event) => {...reader.onload = () => {base64.value = reader.result;...};
};
加载 PDF 时,我们要重置的对象增加了,而且加载完之后我们要让两个父容器滚动同步
function loadPdfData(data) {removeChild();...signCanvass.value = []; //重置...renderAllPages();//两个DIV协同滚动var div1 = document.getElementById('parentDiv1');var div2 = document.getElementById('parentDiv2');div1.addEventListener('scroll', function () {div2.scrollLeft = div1.scrollLeft;div2.scrollTop = div1.scrollTop;});div2.addEventListener('scroll', function () {div1.scrollLeft = div2.scrollLeft;div1.scrollTop = div2.scrollTop;});
}
移除页面元素的时候,我们要将两个 div 容器中的元素都移除掉
function removeChild() {var content = contentDiv.value;var child = content.lastElementChild;while (child) {content.removeChild(child);child = content.lastElementChild;}var signContent = signContentDiv.value;var child2 = signContent.lastElementChild;while (child2) {signContent.removeChild(child2);child2 = signContent.lastElementChild;}
}
渲染 PDF 页面的时候,每一个页面都会生成两个相同大小的画布,一个用来展示,一个用来签名,两个画布是重叠的。
function renderAllPages() {pdfData.value.promise.then((pdf) => {for (let i = 1; i <= pdf.numPages; i++) {pdf.getPage(i).then((page) => {// 获取DOM中为预览PDF准备好的canvasDOM对象let viewport = page.getViewport(scale.value);var canvas = document.createElement('canvas');//用来展示var sighCanvas = document.createElement('canvas');//用来签名canvas.height = viewport.height;canvas.width = viewport.width;sighCanvas.height = viewport.height;sighCanvas.width = viewport.width;let ctx = canvas.getContext('2d');let renderContext = {canvasContext: ctx,viewport: viewport,};page.render(renderContext).then(() => {});initSignatureCanvas(sighCanvas);canvass.value.push(canvas);signCanvass.value.push(sighCanvas);contentDiv.value.appendChild(canvas);signContentDiv.value.appendChild(sighCanvas);});}});
}
主要是保存 PDF 的功能与原来完全不一样。
因为我们前面说的签名画布和 PDF 页是同步生成的,所以页码(下标)也是相对应的。
所以我们只要把签名页面转成一个透明背景的 PNG ,然后添加到 PDF 对应页码的页面上,新的 PDF 文件就是我们需要的签名文件 。
const savePDF = async () => {const pdfDoc = await PDFDocument.load(base64.value);const pages = pdfDoc.getPages();for (let i = 0; i < pages.length; i++) {//对应下标的 签名画布中的内容生成 png图片const eleImgCover = await pdfDoc.embedPng(signCanvass.value[i].toDataURL('image/PNG'));//页面中添加水印pages[i].drawImage(eleImgCover, {x: 0,y: 0,width: eleImgCover.width / scale.value, //这里要进行缩放,因为之前的画布我们是放大过的height: eleImgCover.height / scale.value, //这里要进行缩放,因为之前的画布我们是放大过的});}//生成blob流const pdfBytes = await pdfDoc.save();saveBlob(pdfBytes, 'TestPdf');
};
//网上找的 保存 bolb流 的方法
function saveBlob(data, fileName) {if (typeof window.navigator.msSaveBlob !== 'undefined') {window.navigator.msSaveBlob(new Blob([data], { type: 'application/pdf' }),fileName + '.pdf',);} else {let url = window.URL.createObjectURL(new Blob([data], { type: 'application/pdf' })); //定义下载的链接let link = document.createElement('a'); //创建一个超链接元素link.style.display = 'none'; //隐藏该元素link.href = url; //创建下载的链接link.setAttribute('download', fileName + '.pdf');document.body.appendChild(link);link.click(); //点击下载document.body.removeChild(link); //下载完成移除元素window.URL.revokeObjectURL(url); //释放掉blob对象}
}
效果展示
文字内容可以解析、能够被选中
完整代码地址
方案一
方案二
Vue3 -- PDF展示、添加签名(带笔锋)、导出相关推荐
- Android:自定义View实现签名带笔锋效果
自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN).抬起(ACTION_UP).移动(ACTION_MOVE) 动作中处理触碰点的收集和绘 ...
- 使用PDF-XChange Editor为PDF文件添加签名(图片+签名)
1.打开保护,签名文档 ,并在文档中要签名的位置点击绘制一个区域 2.创建签名证书,点击创建证书,来新建一个证书 3.点击管理,打开数字签名外观模板,先点击克隆,在点击编辑 选择签章图片以及设置 ...
- 使用Adobe Acrobat为PDF文件添加签名(图片+签名)
1.使用Adobe Acrobat打开PDF文件,并切换到工具页,点击证书 2.选择数字签名,然后在文档中要签名的位置上单击并绘制一个区域 3.创建签名证书,点击"签名为:"下拉框 ...
- PDF文件如何添加签名
PDF文件如何添加签名?在我们平常的工作中,许多文件都需要签名确认执行.如果有一份PDF文件需要签名,屏幕前的你知道应该如何操作吗?如果你并不了解PDF文件应该如何添加签名,那么没关系,继续往下阅读你 ...
- 网页上符号显示成方框_如何在word、PPT、Excel以及PDF中添加带√的方框
办公的时候,我们接触到的是各种不同格式的文档,那么关于他们的编辑,大家又了解多少呢?比如说,在文档中添加带√的方框,应该如何添加? 今天这里就分享Word.PPT.Excel以及PDF的添加方法,希望 ...
- 小程序 uniapp 实现pdf 电子合同签名 并导出功能
小程序 uniapp 实现pdf 电子合同签名 并导出功能 需求流程: 用户只允许上传pdf 后端将上传后的pdf以base64图片的形式返回 用户设置签名的位置 位置设置完成,将电子签名放到设定的位 ...
- java生成带书签的pdf,Java 添加、更新、获取、删除PDF中的书签
Spire.Cloud.SDK for Java WebAPI提供了pdfBookmarkApi接口可用于添加书签addBookmark().更新书签updateBookmark().获取书签信息ge ...
- PDF图片格式中添加签名,文字操作
经常会遇到一些pdf里需要添加文字以及签名的操作,通常不同的pdf 文件操作也有不同,但使用到几种方法可以参考 对于签名,一般首先将签名设置为透明形式的,参考文章:https://www.zhihu. ...
- mac自带邮箱导出邮件_如何将电子邮件从Mac Mail导出到Notes应用程序
mac自带邮箱导出邮件 Khamosh Pathak Khamosh Pathak If you use the Mail app regularly, you're used to archivin ...
最新文章
- 聚类评价兰德系数讲明白的
- python做出来的东西_【python小白】 做了一个爬虫,但是爬出来的东西无法存储...
- Lodop客户端本地和集中打印 [是否安装][操作系统]
- 用java编写一个计算器_用java程序编写一个计算器
- 含根号的导数怎么求_数学分析Mathematical Analysis笔记整理 第四章 导数与微分
- 单体、分布式、微服务、Serverless软件架构一览
- ACM 杰出会员姬水旺:量子化学和物理的深度学习
- 异常检测算法分类及经典模型概览
- Python:***测试开源项目
- 简要解析红外摄像机技术与市场
- Linux下rpm安装jdk17
- 作为一名管理者,如何做好上传下达工作呢?
- 前端入门篇(五十三)JS应用6打地鼠小游戏
- Android 内存卡 / Micro SD 卡 / TF 卡 / 存储卡 剩余容量 / 剩余内存 / 可用空间、总容量的 2 种获取方式
- 智慧档案馆档案库房一体化平台建设
- 人民币升值,贬值,顺差,逆差,货币国际化
- python如何求解微分方程_用Python数值求解偏微分方程
- 跟同事杠上了,Apache Beanutils为什么被禁止使用?
- DirectSound开发指南
- charles+drony+android监听websocket
热门文章
- js实现第三方平台分享功能
- linux的cp的参数,Linux cp命令参数简介
- window11中QQ登录“无法访问个人文件夹”解决方案
- 由redux到react-redux再到rematch
- linux软中断通信的基本原理,实验三 软中断通信
- android源代码文本转语音api,Android 文本转语音TextToSpeech (TTS)
- 隐秘而伟大——纪念图灵诞辰110周年
- windows10@安装英语语言包异常_挂起中@设置搜索框的异常(总是搜不出任何结果)问题
- 深入谈谈String.intern()在JVM的实现
- 大话产品经理:真的 “人人都是产品经理” 吗?