2024年最全Vue3 -- PDF展示、添加签名(带笔锋)、导出_smooth-signature,腾讯 前端 面试

打开全栈工匠技能包-1小时轻松掌握SSR

两小时精通jq+bs插件开发

生产环境下如歌部署Node.js

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

网易内部VUE自定义插件库NPM集成

谁说前端不用懂安全,XSS跨站脚本的危害

webpack的loader到底是什么样的?两小时带你写一个自己loader

		* [实现代码](#_107)
		* [效果展示](#_293)
		* [缺点](#_296)
+ [方案二](#_299)
+ - [修改页面元素](#_304)
	- [替换引用](#_335)
	- [修改代码](#_340)
	- [效果展示](#_474)
+ [完整代码地址](#_478)

实现功能的时候采用了两个方案,主要是第一个方案最后的实现效果并不太理想,但实现起来比较简单,要求不高时可以使用。


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-distPDF展示等功能
jspdfPDF导出相关功能
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) => {
        // 断言为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();
        //重置对象状态
        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(() => {});
              //将画布包装成 SmoothSignature
              initSignatureCanvas(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();
        }
      };
      // 下载PDF
      const savePDF = () => {
        //生成新的 PDF
        let pdf = new JsPDF('', 'pt', 'a4');
        if (canvass.value.length > 0) {
          //将 canvas 内容转化成 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() {},
  });

效果展示

在这里插入图片描述
在这里插入图片描述

缺点

1、生成的新的PDF每一页都是一个图片,这就表示 PDF 中的内容无法被解析,文字再也无法被选中了。
2、因为生成的是图片,所以最终效果可能会变模糊,可以通过放大比例去优化展示效果,但是始终不是一个最优的解决方案。

方案二

方案二使用一个新的组件 pdf-lib 来处理最后生成的 PDF

最后

全网独播-价值千万金融项目前端架构实战

从两道网易面试题-分析JavaScript底层机制

RESTful架构在Nodejs下的最佳实践

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

一线互联网企业如何初始化项目-做一个自己的vue-cli

思维无价,看我用Nodejs实现MVC

代码优雅的秘诀-用观察者模式深度解耦模块

前端高级实战,如何封装属于自己的JS库

VUE组件库级组件封装-高复用弹窗组件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值