文章目录

  • 笔锋签名
  • 方案一
    • 实现要点
    • 实现过程
      • 组件引用
      • 页面元素
      • 添加引用
      • 实现代码
      • 效果展示
      • 缺点
  • 方案二
    • 修改页面元素
    • 替换引用
    • 修改代码
    • 效果展示
  • 完整代码地址

实现功能的时候采用了两个方案,主要是第一个方案最后的实现效果并不太理想,但实现起来比较简单,要求不高时可以使用。
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 去做一些操作,比如清空签名、撤回一步的操作等。

方案一

实现要点

  1. 读取 PDF 文件,并将 PDF 页面渲染到 Canvas 画布上,这里需要动态生成 Canvas
  2. 将每一个 Canvas 都包装成 SmoothSignature
  3. 添加一个标识,判断是否允许在 Canvas 上画线(手机滑动会和签名画线冲突,用按钮来控制什么时候允许画线)。
  4. 保存 PDF 时,先将每一个 Canvas 中的内容转化成图片格式 image/JPEG ,或者 image/PNG ,PNG格式的文件可能会比较大。
  5. 最后用生成的图片导出一个新的 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-distCanvas 上展示 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展示、添加签名(带笔锋)、导出相关推荐

  1. Android:自定义View实现签名带笔锋效果

    自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN).抬起(ACTION_UP).移动(ACTION_MOVE) 动作中处理触碰点的收集和绘 ...

  2. 使用PDF-XChange Editor为PDF文件添加签名(图片+签名)

    1.打开保护,签名文档 ,并在文档中要签名的位置点击绘制一个区域 2.创建签名证书,点击创建证书,来新建一个证书  3.点击管理,打开数字签名外观模板,先点击克隆,在点击编辑   选择签章图片以及设置 ...

  3. 使用Adobe Acrobat为PDF文件添加签名(图片+签名)

    1.使用Adobe Acrobat打开PDF文件,并切换到工具页,点击证书 2.选择数字签名,然后在文档中要签名的位置上单击并绘制一个区域 3.创建签名证书,点击"签名为:"下拉框 ...

  4. PDF文件如何添加签名

    PDF文件如何添加签名?在我们平常的工作中,许多文件都需要签名确认执行.如果有一份PDF文件需要签名,屏幕前的你知道应该如何操作吗?如果你并不了解PDF文件应该如何添加签名,那么没关系,继续往下阅读你 ...

  5. 网页上符号显示成方框_如何在word、PPT、Excel以及PDF中添加带√的方框

    办公的时候,我们接触到的是各种不同格式的文档,那么关于他们的编辑,大家又了解多少呢?比如说,在文档中添加带√的方框,应该如何添加? 今天这里就分享Word.PPT.Excel以及PDF的添加方法,希望 ...

  6. 小程序 uniapp 实现pdf 电子合同签名 并导出功能

    小程序 uniapp 实现pdf 电子合同签名 并导出功能 需求流程: 用户只允许上传pdf 后端将上传后的pdf以base64图片的形式返回 用户设置签名的位置 位置设置完成,将电子签名放到设定的位 ...

  7. java生成带书签的pdf,Java 添加、更新、获取、删除PDF中的书签

    Spire.Cloud.SDK for Java WebAPI提供了pdfBookmarkApi接口可用于添加书签addBookmark().更新书签updateBookmark().获取书签信息ge ...

  8. PDF图片格式中添加签名,文字操作

    经常会遇到一些pdf里需要添加文字以及签名的操作,通常不同的pdf 文件操作也有不同,但使用到几种方法可以参考 对于签名,一般首先将签名设置为透明形式的,参考文章:https://www.zhihu. ...

  9. mac自带邮箱导出邮件_如何将电子邮件从Mac Mail导出到Notes应用程序

    mac自带邮箱导出邮件 Khamosh Pathak Khamosh Pathak If you use the Mail app regularly, you're used to archivin ...

最新文章

  1. 聚类评价兰德系数讲明白的
  2. python做出来的东西_【python小白】 做了一个爬虫,但是爬出来的东西无法存储...
  3. Lodop客户端本地和集中打印 [是否安装][操作系统]
  4. 用java编写一个计算器_用java程序编写一个计算器
  5. 含根号的导数怎么求_数学分析Mathematical Analysis笔记整理 第四章 导数与微分
  6. 单体、分布式、微服务、Serverless软件架构一览
  7. ACM 杰出会员姬水旺:量子化学和物理的深度学习
  8. 异常检测算法分类及经典模型概览
  9. Python:***测试开源项目
  10. 简要解析红外摄像机技术与市场
  11. Linux下rpm安装jdk17
  12. 作为一名管理者,如何做好上传下达工作呢?
  13. 前端入门篇(五十三)JS应用6打地鼠小游戏
  14. Android 内存卡 / Micro SD 卡 / TF 卡 / 存储卡 剩余容量 / 剩余内存 / 可用空间、总容量的 2 种获取方式
  15. 智慧档案馆档案库房一体化平台建设
  16. 人民币升值,贬值,顺差,逆差,货币国际化
  17. python如何求解微分方程_用Python数值求解偏微分方程
  18. 跟同事杠上了,Apache Beanutils为什么被禁止使用?
  19. DirectSound开发指南
  20. charles+drony+android监听websocket

热门文章

  1. js实现第三方平台分享功能
  2. linux的cp的参数,Linux cp命令参数简介
  3. window11中QQ登录“无法访问个人文件夹”解决方案
  4. 由redux到react-redux再到rematch
  5. linux软中断通信的基本原理,实验三 软中断通信
  6. android源代码文本转语音api,Android 文本转语音TextToSpeech (TTS)
  7. 隐秘而伟大——纪念图灵诞辰110周年
  8. windows10@安装英语语言包异常_挂起中@设置搜索框的异常(总是搜不出任何结果)问题
  9. 深入谈谈String.intern()在JVM的实现
  10. 大话产品经理:真的 “人人都是产品经理” 吗?