Vue3 -- PDF展示、添加签名(带笔锋)、导出

这篇具有很好参考价值的文章主要介绍了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 去做一些操作,比如清空签名、撤回一步的操作等。

方案一

实现要点

  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) => {
        // 断言为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() {},
  });
效果展示

vue3加载pdf,Vue,pdf,javascript,前端
vue3加载pdf,Vue,pdf,javascript,前端

缺点

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,Vue,pdf,javascript,前端
vue3加载pdf,Vue,pdf,javascript,前端

完整代码地址

方案一
方案二文章来源地址https://www.toymoban.com/news/detail-756860.html

到了这里,关于Vue3 -- PDF展示、添加签名(带笔锋)、导出的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • vue3.2 导出pdf文件或表格数据

    要在Vue 3中导出PDF文件,你可以使用第三方库来处理PDF生成和导出。一个常用的库是jspdf,它允许你在客户端或服务器端生成PDF文档。 以下是在Vue 3中使用jspdf库导出PDF文件的基本步骤: 第一步首先,安装jspdf库。你可以使用npm或yarn来安装它: npm install jspdf --save  或  pnpm in

    2024年02月09日
    浏览(36)
  • 在uni中使用vue3写h5的pdf导出

    先安装依赖 把dom转canvas,屏幕截图基于 DOM,因此可能不是 100% 准确到真实表示,因为它不会制作实际的屏幕截图,而是根据页面上可用的信息构建屏幕截图。 components/pdf  创建pdf文件夹 在main文件中引入 在需要的页面绑定id  事件

    2024年02月16日
    浏览(40)
  • Vue3前端h5移动端页面预览PDF使用pdfjs-dist,添加自定义文本水印

    pnpm i pdfjs-dist@2.5.207

    2024年01月18日
    浏览(77)
  • Vue3PDF预览(vue3-pdf-app)

    vue3-pdf-app 插件 网站预览PDF最佳且最简单的方式: 如果需要自定义网页内预览,可以采用本PDF预览组件(PDFViewer.vue) 本组件基于  vue3-pdf-app@1.0.3  插件进行二次封装,更适合日常使用需要! 插件支持功能包括但不限于:缩放、旋转、全屏预览、打印、下载、内容检索、dar

    2023年04月21日
    浏览(40)
  • vue3 实现简单计数器示例——一个html文件展示vue3的效果

    目的 :作为一个新手开发,我想使用 Vue 3 将代码封装在 HTML 文件中时,进行界面打开展示。 学了一个简单计数器界面展示,代码如下: 在 上述HTML 文件里,包含了文件头标题 title , 接着定义了一个内容 div id=\\\"app\\\" 而后定义了一个内容,包含标题和数字,以及两个按钮的 templa

    2024年01月18日
    浏览(44)
  • vue3前端excel导出;组件表格,自定义表格导出;Vue3 + xlsx + xlsx-style

    当画面有自定义的表格或者样式过于复杂的表格时,导出功能可以由前端实现 1. 使用的插件 : sheet.js-xlsx 文档地址:https://docs.sheetjs.com/ 中文地址:https://geekdaxue.co/read/SheetJS-docs-zh/README.md xlsx-style:https://www.npmjs.com/package/xlsx-style 2. 安装引用 安装插件-vue3 引用插件 3. 组件表格

    2024年04月26日
    浏览(39)
  • Vue3 导出word

    🙂博主:锅盖哒 🙂文章核心: 导出word 目录 1.首先,你需要安装docxtemplater库。可以使用npm或yarn来安装: 2.在Vue组件中,你可以使用docxtemplater来生成Word文档并提供一个导出按钮供用户下载。            在Vue 3中,你可以使用 docxtemplater 库来生成Word文档并导出。下面是一个

    2024年02月15日
    浏览(33)
  • vue3项目利用iframe展示其他页面

    iframe是html内联框架元素,它能够将另一个 HTML 页面嵌入到当前页面中。 主要属性如下: src 被嵌套的页面的 URL 地址 name 框架名称 scrolling 否要在框架内显示滚动条。值; auto(仅当框架的内容超出框架的范围时显示滚动条)、yes、no width iframe的宽度 height iframe的高度 frameborde

    2024年02月12日
    浏览(47)
  • vue3 excel 导出功能

    1.安装 xlsx 库 2.创建导出函数 src/utils/excelUtils.js 3.在组件中调用导出函数 4.补充: 列名替换成中文

    2024年02月13日
    浏览(41)
  • Vue3 导入导出Excel

    提供一个 Excel 文件,将里面的内容导出成 JSON 数组 提供一个 JSON 数组,生成 Excel 文件并下载 表格 1. 安装步骤 2.使用 3.导出 导出之后发现数据结构不是我们想要的那种,此时就能通过以下方法转换。 更改列宽 4.导入 如果excel中含有日期,需要解析时间格式的内容。 XLSL.re

    2024年02月14日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包