文章目录
实现功能的时候采用了两个方案,主要是第一个方案最后的实现效果并不太理想,但实现起来比较简单,要求不高时可以使用。
该
DEMO 会一次性加载并展示所有的
PDF 页面,目的是方便在手机上观看时上下滑动,如果要做成上一页下一页的效果,需要自行实现。
笔锋签名
我是用开源项目 smooth-signature 实现带笔锋签名的功能。
Gitee 地址是 https://github.com/linjc/smooth-signature
npm install --save smooth-signature
使用起来也比较简单,首先获取到需要操作的画布 canvas ,然后生成一个笔锋签名对象 SmoothSignature,optionSign 是初始化的一些简单属性。
const signature =newSmoothSignature(canvas, optionSign);
这样一来,我们的 canvas 就可以画线条了,同时我们可以通过 signature 去做一些操作,比如清空签名、撤回一步的操作等。
方案一
实现要点
- 读取 PDF 文件,并将 PDF 页面渲染到 Canvas 画布上,这里需要动态生成 Canvas
- 将每一个 Canvas 都包装成 SmoothSignature
- 添加一个标识,判断是否允许在 Canvas 上画线(手机滑动会和签名画线冲突,用按钮来控制什么时候允许画线)。
- 保存 PDF 时,先将每一个 Canvas 中的内容转化成图片格式 image/JPEG ,或者 image/PNG ,PNG格式的文件可能会比较大。
- 最后用生成的图片导出一个新的 PDF (实质上 PDF 每一页都是一张图片)。
实现过程
组件引用
smooth-signature笔锋签名pdfjs-distPDF展示等功能jspdfPDF导出相关功能
npm install --save smooth-signature
npm install --save [email protected]
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;
实现代码
代码中添加了主要的注释,可以查看下述代码
exportdefaultdefineComponent({
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 文件constuploadFile=(e: Event)=>{// 断言为HTMLInputElementconst target = e.target as HTMLInputElement;const files = target.files;let reader =newFileReader();
reader.readAsDataURL(files[0]);
reader.onload=()=>{let data =atob(reader.result.substring(reader.result.indexOf(',')+1));loadPdfData(data);};};//加载PDFfunctionloadPdfData(data){//移除所有旧的 Canvas 画布元素removeChild();//重置对象状态
isSign.value =false;
canvass.value =[];
signatures.value =[];// 引入pdf.js的字体,如果没有引用的话字体可能会不显示letCMAP_URL='https://unpkg.com/[email protected]/cmaps/';//读取base64的pdf流文件
pdfData.value = pdfJS.getDocument({
data: data,// PDF base64编码
cMapUrl:CMAP_URL,
cMapPacked:true,});//渲染全部页面renderAllPages();}//移除页面上旧的元素functionremoveChild(){var content = contentDiv.value;var child = content.lastElementChild;while(child){
content.removeChild(child);
child = content.lastElementChild;}}//渲染全部页面functionrenderAllPages(){
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);});}});}//初始化签名对象constinitSignatureCanvas=(canvas)=>{const optionSign ={
width: canvas.width,
height: canvas.height,
maxHistoryLength:100,//最大历史记录};const signature =newSmoothSignature(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 方法,移除监听事件
*/consthandleSign=()=>{
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 方法会撤回当前对象中的最后一次的画线记录
* 注意:后退后不要忘记将列表中最后一个元素移除
*/consthandleUndo=()=>{if(historys.value && historys.value.length >0){const signatureList = historys.value;let signature = signatureList.pop();
signature.undo();
historys.value = signatureList;}};// 清除所有 循环把所有签名历史都处理了consthandleClear=async()=>{while(historys.value && historys.value.length >0){handleUndo();}};// 下载PDFconstsavePDF=()=>{//生成新的 PDFlet pdf =newJsPDF('','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,);}//到处新的PDF return 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 时使用
constuploadFile=(e: Event)=>{...
reader.onload=()=>{
base64.value = reader.result;...};};
加载 PDF 时,我们要重置的对象增加了,而且加载完之后我们要让两个父容器滚动同步
functionloadPdfData(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 容器中的元素都移除掉
functionremoveChild(){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 页面的时候,每一个页面都会生成两个相同大小的画布,一个用来展示,一个用来签名,两个画布是重叠的。
functionrenderAllPages(){
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 文件就是我们需要的签名文件 。
constsavePDF=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流 的方法functionsaveBlob(data, fileName){if(typeof window.navigator.msSaveBlob !=='undefined'){
window.navigator.msSaveBlob(newBlob([data],{ type:'application/pdf'}),
fileName +'.pdf',);}else{let url = window.URL.createObjectURL(newBlob([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对象}}
效果展示
文字内容可以解析、能够被选中
完整代码地址
方案一
方案二
版权归原作者 Mingvvv 所有, 如有侵权,请联系我们删除。