實(shí)現(xiàn)功能的時(shí)候采用了兩個(gè)方案,主要是第一個(gè)方案最后的實(shí)現(xiàn)效果并不太理想,但實(shí)現(xiàn)起來比較簡(jiǎn)單,要求不高時(shí)可以使用。
該 DEMO 會(huì)一次性加載并展示所有的 PDF 頁(yè)面,目的是方便在手機(jī)上觀看時(shí)上下滑動(dòng),如果要做成上一頁(yè)下一頁(yè)的效果,需要自行實(shí)現(xiàn)。
筆鋒簽名
我是用開源項(xiàng)目 smooth-signature 實(shí)現(xiàn)帶筆鋒簽名的功能。
Gitee 地址是 https://github.com/linjc/smooth-signature
npm install --save smooth-signature
使用起來也比較簡(jiǎn)單,首先獲取到需要操作的畫布 canvas ,然后生成一個(gè)筆鋒簽名對(duì)象 SmoothSignature,optionSign 是初始化的一些簡(jiǎn)單屬性。
const signature = new SmoothSignature(canvas, optionSign);
這樣一來,我們的 canvas 就可以畫線條了,同時(shí)我們可以通過 signature 去做一些操作,比如清空簽名、撤回一步的操作等。
方案一
實(shí)現(xiàn)要點(diǎn)
- 讀取 PDF 文件,并將 PDF 頁(yè)面渲染到 Canvas 畫布上,這里需要?jiǎng)討B(tài)生成 Canvas
- 將每一個(gè) Canvas 都包裝成 SmoothSignature
- 添加一個(gè)標(biāo)識(shí),判斷是否允許在 Canvas 上畫線(手機(jī)滑動(dòng)會(huì)和簽名畫線沖突,用按鈕來控制什么時(shí)候允許畫線)。
- 保存 PDF 時(shí),先將每一個(gè) Canvas 中的內(nèi)容轉(zhuǎn)化成圖片格式 image/JPEG ,或者 image/PNG ,PNG格式的文件可能會(huì)比較大。
- 最后用生成的圖片導(dǎo)出一個(gè)新的 PDF (實(shí)質(zhì)上 PDF 每一頁(yè)都是一張圖片)。
實(shí)現(xiàn)過程
組件引用
smooth-signature | 筆鋒簽名 |
pdfjs-dist | PDF展示等功能 |
jspdf | PDF導(dǎo)出相關(guān)功能 |
npm install --save smooth-signature
npm install --save pdfjs-dist@2.0.943
npm install --save jspdf
頁(yè)面元素
主要是讀取文件、切至簽名功能、切回預(yù)覽功能、撤回簽名、清除所有簽名以及下載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">切回預(yù)覽</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">
引用
......
實(shí)現(xiàn)代碼
......
</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;
實(shí)現(xiàn)代碼
代碼中添加了主要的注釋,可以查看下述代碼
export default defineComponent({
components: { Button, Input },
setup() {
const fielinput = ref(null);
const contentDiv = ref(null);
//簽名相關(guān)
const isSign = ref(false); //控制是否允許簽名
const canvass = ref([]); //保存所有畫布元素
const signatures = ref([]); //所有簽名對(duì)象
const historys = ref([]); //簽名歷史 用于回退或者清除,因?yàn)槭且淮涡哉故径鄠€(gè)頁(yè)面,會(huì)存在多個(gè)包裝好的簽名對(duì)象,存放歷史列表方便操作
//PDF展示相關(guān)
const pdfData = ref(null); // PDF 內(nèi)容
const scale = ref(2); //放大比例 ,有的時(shí)候展示可能會(huì)比較模糊,可以放大展示
//上傳控件選擇事件,加載選中的 PDF 文件
const uploadFile = (e: Event) => {
// 斷言為HTMLInputElement
const 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);
};
};
//加載PDF
function loadPdfData(data) {
//移除所有舊的 Canvas 畫布元素
removeChild();
//重置對(duì)象狀態(tài)
isSign.value = false;
canvass.value = [];
signatures.value = [];
// 引入pdf.js的字體,如果沒有引用的話字體可能會(huì)不顯示
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,
});
//渲染全部頁(yè)面
renderAllPages();
}
//移除頁(yè)面上舊的元素
function removeChild() {
var content = contentDiv.value;
var child = content.lastElementChild;
while (child) {
content.removeChild(child);
child = content.lastElementChild;
}
}
//渲染全部頁(yè)面
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);
//動(dòng)態(tài)生成 Canvas 畫布并設(shè)置寬高
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 頁(yè)面渲染到 Canvas 上
page.render(renderContext).then(() => {});
//將畫布包裝成 SmoothSignature
initSignatureCanvas(canvas);
//將畫布元素放入到 div 容器中展示
canvass.value.push(canvas);
contentDiv.value.appendChild(canvas);
});
}
});
}
//初始化簽名對(duì)象
const initSignatureCanvas = (canvas) => {
const optionSign = {
width: canvas.width,
height: canvas.height,
maxHistoryLength: 100, //最大歷史記錄
};
const signature = new SmoothSignature(canvas, optionSign);
//初始化時(shí) 先移除它內(nèi)部添加的監(jiān)聽事件,默認(rèn)不能簽名
signature.removeListener();
//簽名對(duì)象 addHistory 方法做一下修改,在原來邏輯的基礎(chǔ)上添加這一行
// 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);
};
/**
* 簽名預(yù)覽轉(zhuǎn)換
* 允許簽名:調(diào)用 signature 對(duì)象中的 addListener 方法,添加監(jiān)聽事件
* 不允許簽名:調(diào)用 signature 對(duì)象中的 removeListener 方法,移除監(jiān)聽事件
*/
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();
}
}
}
};
/**
* 后退操作
* 調(diào)用歷史簽名記錄中的 signature 對(duì)象中的 undo 方法會(huì)撤回當(dāng)前對(duì)象中的最后一次的畫線記錄
* 注意:后退后不要忘記將列表中最后一個(gè)元素移除
*/
const handleUndo = () => {
if (historys.value && historys.value.length > 0) {
const signatureList = historys.value;
let signature = signatureList.pop();
signature.undo();
historys.value = signatureList;
}
};
// 清除所有 循環(huán)把所有簽名歷史都處理了
const handleClear = async () => {
while (historys.value && historys.value.length > 0) {
handleUndo();
}
};
// 下載PDF
const savePDF = () => {
//生成新的 PDF
let pdf = new JsPDF('', 'pt', 'a4');
if (canvass.value.length > 0) {
//將 canvas 內(nèi)容轉(zhuǎn)化成 JPEG
for (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() {},
});
效果展示
缺點(diǎn)
1、生成的新的PDF每一頁(yè)都是一個(gè)圖片,這就表示 PDF 中的內(nèi)容無法被解析,文字再也無法被選中了。
2、因?yàn)樯傻氖菆D片,所以最終效果可能會(huì)變模糊,可以通過放大比例去優(yōu)化展示效果,但是始終不是一個(gè)最優(yōu)的解決方案。
方案二
方案二使用一個(gè)新的組件 pdf-lib 來處理最后生成的 PDF
方案二仍舊使用 pdfjs-dist 在 Canvas 上展示 PDF,并使用 smooth-signature 使得畫布擁有筆鋒簽名效果。
不同的是,這一次簽名畫布和 PDF 展示畫布并不再是同一個(gè)畫布,而是上下重疊的兩個(gè)分離的畫布
這樣一來,我們可以將簽名畫布上的內(nèi)容生成一個(gè)透明背景的 PNG 圖片,然后以水印的方式添加到原來的 PDF 文件中。
修改頁(yè)面元素
需要兩個(gè) Div 容器 ,父容器的滾動(dòng)條需要同步滾動(dòng),否則會(huì)出現(xiàn)簽名在滾動(dòng),但是 PDF 頁(yè)面不動(dòng)的情況
<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">點(diǎn)擊預(yù)覽</Button>
<Button :class="`button-common`" v-else @click="handleSign">點(diǎn)擊簽名</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數(shù)據(jù)
上傳文件的方法中添加一行保存PDF base64 ,生成新的 PDF 時(shí)使用
const uploadFile = (e: Event) => {
...
reader.onload = () => {
base64.value = reader.result;
...
};
};
加載 PDF 時(shí),我們要重置的對(duì)象增加了,而且加載完之后我們要讓兩個(gè)父容器滾動(dòng)同步
function loadPdfData(data) {
removeChild();
...
signCanvass.value = []; //重置
...
renderAllPages();
//兩個(gè)DIV協(xié)同滾動(dòng)
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;
});
}
移除頁(yè)面元素的時(shí)候,我們要將兩個(gè) 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 頁(yè)面的時(shí)候,每一個(gè)頁(yè)面都會(huì)生成兩個(gè)相同大小的畫布,一個(gè)用來展示,一個(gè)用來簽名,兩個(gè)畫布是重疊的。
function renderAllPages() {
pdfData.value.promise.then((pdf) => {
for (let i = 1; i <= pdf.numPages; i++) {
pdf.getPage(i).then((page) => {
// 獲取DOM中為預(yù)覽PDF準(zhǔn)備好的canvasDOM對(duì)象
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 的功能與原來完全不一樣。
因?yàn)槲覀兦懊嬲f的簽名畫布和 PDF 頁(yè)是同步生成的,所以頁(yè)碼(下標(biāo))也是相對(duì)應(yīng)的。
所以我們只要把簽名頁(yè)面轉(zhuǎn)成一個(gè)透明背景的 PNG ,然后添加到 PDF 對(duì)應(yīng)頁(yè)碼的頁(yè)面上,新的 PDF 文件就是我們需要的簽名文件 。
const savePDF = async () => {
const pdfDoc = await PDFDocument.load(base64.value);
const pages = pdfDoc.getPages();
for (let i = 0; i < pages.length; i++) {
//對(duì)應(yīng)下標(biāo)的 簽名畫布中的內(nèi)容生成 png圖片
const eleImgCover = await pdfDoc.embedPng(signCanvass.value[i].toDataURL('image/PNG'));
//頁(yè)面中添加水印
pages[i].drawImage(eleImgCover, {
x: 0,
y: 0,
width: eleImgCover.width / scale.value, //這里要進(jìn)行縮放,因?yàn)橹暗漠嫴嘉覀兪欠糯筮^的
height: eleImgCover.height / scale.value, //這里要進(jìn)行縮放,因?yàn)橹暗漠嫴嘉覀兪欠糯筮^的
});
}
//生成blob流
const pdfBytes = await pdfDoc.save();
saveBlob(pdfBytes, 'TestPdf');
};
//網(wǎng)上找的 保存 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'); //創(chuàng)建一個(gè)超鏈接元素
link.style.display = 'none'; //隱藏該元素
link.href = url; //創(chuàng)建下載的鏈接
link.setAttribute('download', fileName + '.pdf');
document.body.appendChild(link);
link.click(); //點(diǎn)擊下載
document.body.removeChild(link); //下載完成移除元素
window.URL.revokeObjectURL(url); //釋放掉blob對(duì)象
}
}
效果展示
文字內(nèi)容可以解析、能夠被選中文章來源:http://www.zghlxwxcb.cn/news/detail-756860.html
完整代碼地址
方案一
方案二文章來源地址http://www.zghlxwxcb.cn/news/detail-756860.html
到了這里,關(guān)于Vue3 -- PDF展示、添加簽名(帶筆鋒)、導(dǎo)出的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!