前言

之前业务上有用pdf文件进行合同协议签署的需求;即用户浏览完pdf合同文件,唤起签字面板进行手写签名;签完名字之后把名字和合同文件合并再一起,然后上传给后端,后端调用E签宝相关API进行盖章;

主要实现

经查找相关,确定了以下三个相关库:

  • PDF.JS 用于pdf文件的展示已经对pdf文件编辑完之后重新加载展示;
  • PDF-LIB 可以对pdf文件进行编辑,追加图片和 文字(追加中文时需要加载中文相关字体,比较麻烦);
  • Smooth-Signature 签字面板,用户手写签字之后,生成图片追加到合同pdf文件中;

缩放实现

通过设置pdf外容器的css变量--scale-factor,然后css中动态计算容器的宽和高,从而实现放的效果;

    <details  data-line="10" class="md-editor-code" open="">
      <summary class="md-editor-code-head">
        <div class="md-editor-code-flag"><span></span><span></span><span></span></div>
        <div class="md-editor-code-action">
          <span class="md-editor-code-lang">css</span>
          <span class="md-editor-copy-button" data-tips="复制代码">复制代码</span>
          
          <span class="md-editor-collapse-tips"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg></span>
        </div>
      </summary>
      <pre><code class="language-css" language=css><span class="md-editor-code-block"><span class="hljs-selector-class">.pdf-wrap</span> {
  <span class="hljs-attribute">min-height</span>: <span class="hljs-number">100vh</span>;
  <span class="hljs-attribute">width</span>: <span class="hljs-built_in">calc</span>(<span class="hljs-built_in">var</span>(--scale-factor) * <span class="hljs-number">612px</span>);
  <span class="hljs-attribute">height</span>: <span class="hljs-built_in">calc</span>(<span class="hljs-built_in">var</span>(--scale-factor) * <span class="hljs-number">792px</span>);
  <span class="hljs-attribute">transition</span>: all <span class="hljs-number">0.5s</span> ease-in;
}</span><span rn-wrapper aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre>

    </details>
  <h3 data-line="20" id="主要js代码">主要js代码</h3>

    <details  data-line="22" class="md-editor-code">
      <summary class="md-editor-code-head">
        <div class="md-editor-code-flag"><span></span><span></span><span></span></div>
        <div class="md-editor-code-action">
          <span class="md-editor-code-lang">javascript</span>
          <span class="md-editor-copy-button" data-tips="复制代码">复制代码</span>
          
          <span class="md-editor-collapse-tips"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg></span>
        </div>
      </summary>
      <pre><code class="language-javascript" language=javascript><span class="md-editor-code-block">  <span class="hljs-keyword">import</span> { ref, onMounted } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;vue&#x27;</span>

import SmoothSignature from 'smooth-signature' import dayjs from 'dayjs' import sealLogo from '@/assets/images/logo/person/jiang.png' const emits = defineEmits(['success']) const props = defineProps({ pdfSrc: { type: String, default: '', }, second: { type: Number, default: 10, }, info: { type: Object, default: () => ({}), }, }) const smoothSignatureCanvas = ref('') const signature = ref('') const signaturePng = ref('') const showSmoothSignatureWrap = ref(false) const localSecond = ref(props.second) const loading = ref(true) onMounted(() => { const timer = setInterval(() => { localSecond.value-- if (!localSecond.value) { clearInterval(timer) } }, 1000) const { width, height, } = scrollContainer.value.getBoundingClientRect() minScale.value = width / 612 // console.log(width, height) const options = { width: width - 24, height: height / 3 - 50, minWidth: 3, maxWidth: 10, color: '#333333', bgColor: 'transparent', // bgColor: '#f6f6f6', } scaleFactor.value = width / 612 signature.value = new SmoothSignature(smoothSignatureCanvas.value, options) document.querySelector('.scroll-container').addEventListener('scroll', (e) => { const scrollTop = e.target.scrollTop // console.log(scrollTop, pageHeight.value, Math.ceil((scrollTop + 306) / pageHeight.value)) currentPage.value = Math.ceil( ((scrollTop + 100) * window.devicePixelRatio) / pageHeight.value ) if (currentPage.value >= totalPageCount.value) { currentPage.value = totalPageCount } }) reloadPdf() }) const handleClear = () => { signature.value.clear() } const handleUndo = () => { signature.value.undo() } const handleFinish = () => { // 旋转 // const canvas = signature.value.getRotateCanvas(-90)// 不用反转 const canvas = signature.value const pngUrl = canvas.toDataURL() signaturePng.value = pngUrl editPdf() showSmoothSignatureWrap.value = false } const handleClose = () => { console.log('关闭') handleClear() showSmoothSignatureWrap.value = false } const showSmoothSignatureWrapHandle = () => { handleClear() showSmoothSignatureWrap.value = true } const scrollContainer = ref('') const pdfContainer = ref('') const totalPageCount = ref(1) // 页数和缩放相关 const currentPage = ref(1) const pageWidth = ref(0) const pageHeight = ref(0) const scaleFactor = ref(1) const minScale = ref(1) const changeScale = (t) => { let num = scaleFactor.value if (t === '-') { num = num - num * 0.1 if (num <= 1) { num = minScale.value } } else if (t === '+') { num = num + num * 0.1 if (num >= 4) { num = minScale.value * 4 } } scaleFactor.value = num } const reloadPdf = async (pdfData = props.pdfSrc) => { loading.value = true const pdfDocument = await pdfjsLib.getDocument(pdfData).promise // console.log(pdfDocument) pdfContainer.value.innerHTML = '' // 清空PDF容器 totalPageCount.value = pdfDocument.numPages for (let pageIndex = 1; pageIndex <= pdfDocument.numPages; pageIndex++) { const page = await pdfDocument.getPage(pageIndex) const viewport = page.getViewport({ scale: 2, }) pageWidth.value = viewport.width / 2 pageHeight.value = viewport.height / 2 // console.log(page) const canvas = document.createElement('canvas') pdfContainer.value.appendChild(canvas) const context = canvas.getContext('2d') canvas.width = viewport.width canvas.height = viewport.height

  <span class="hljs-keyword">const</span> renderContext = {
    <span class="hljs-attr">canvasContext</span>: context,
    viewport,
  }
  <span class="hljs-comment">// console.log(page)</span>
  <span class="hljs-keyword">await</span> page.<span class="hljs-title function_">render</span>(renderContext)
  loading.<span class="hljs-property">value</span> = <span class="hljs-literal">false</span>
}

} const editPdf = async () => { const PDFDocument = PDFLib.PDFDocument // showToast.loading('加载中') // const fontBytes = await fetch('https://jiang-xia.top/x-blog/api/v1/static/uploads/2023-12/ga0hqzh5lek2ntyxtzebx0-华文中宋.ttf').then((res) => res.arrayBuffer()) const pdfBuffer = await fetch(props.pdfSrc).then(res => res.arrayBuffer()) const pdfDoc = await PDFDocument.load(pdfBuffer) // pdfDoc.registerFontkit(fontkit) // const customFont = await pdfDoc.embedFont(fontBytes,{subset:true}) const pages = pdfDoc.getPages() console.log('签名设置————————开始') for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) { console.log('pageIndex', pageIndex) const width = pages[pageIndex].getWidth() const height = pages[pageIndex].getHeight() if (pageIndex === pages.length - 1) { const emblemImageBytes = await fetch(signaturePng.value).then(res => res.arrayBuffer()) const img = await pdfDoc.embedPng(emblemImageBytes) // pdf(0,0)再左上角 const x = width - 260 const y = height / 2 - 100

    <span class="hljs-comment">// 印章图片</span>
    <span class="hljs-keyword">const</span> sealImageBytes = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(sealLogo).<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">res</span> =&gt;</span> res.<span class="hljs-title function_">arrayBuffer</span>())
    <span class="hljs-keyword">const</span> sealImg = <span class="hljs-keyword">await</span> pdfDoc.<span class="hljs-title function_">embedPng</span>(sealImageBytes)
    pages[pageIndex].<span class="hljs-title function_">drawImage</span>(sealImg, {
      x,
      <span class="hljs-attr">y</span>: y - <span class="hljs-number">40</span>,
      <span class="hljs-attr">width</span>: <span class="hljs-number">140</span>,
      <span class="hljs-attr">height</span>: <span class="hljs-number">140</span>,
      <span class="hljs-attr">opacity</span>: <span class="hljs-number">1</span>, <span class="hljs-comment">// 设置图片透明度</span>
    })

    <span class="hljs-comment">// 签名图片</span>
    pages[pageIndex].<span class="hljs-title function_">drawImage</span>(img, {
      x,
      y,
      <span class="hljs-attr">width</span>: <span class="hljs-number">160</span>,
      <span class="hljs-attr">height</span>: <span class="hljs-number">60</span>,
    })
    <span class="hljs-comment">// 签名日期</span>
    <span class="hljs-keyword">const</span> dateText = <span class="hljs-title function_">dayjs</span>().<span class="hljs-title function_">format</span>(<span class="hljs-string">&#x27;YYYY MM DD&#x27;</span>)
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(dateText)
    pages[pageIndex].<span class="hljs-title function_">drawText</span>(dateText, {
      <span class="hljs-attr">x</span>: x + <span class="hljs-number">20</span>,
      <span class="hljs-attr">y</span>: y - <span class="hljs-number">20</span>,
      <span class="hljs-attr">size</span>: <span class="hljs-number">18</span>,
      <span class="hljs-comment">// font: customFont,</span>
    })
    <span class="hljs-keyword">const</span> pdfBytes = <span class="hljs-keyword">await</span> pdfDoc.<span class="hljs-title function_">save</span>()
    <span class="hljs-comment">// console.log(pdfBytes)</span>
    <span class="hljs-keyword">const</span> myFile = <span class="hljs-keyword">new</span> <span class="hljs-title class_">File</span>([pdfBytes], <span class="hljs-string">&#x27;generated.pdf&#x27;</span>)
    <span class="hljs-title function_">reloadPdf</span>(pdfBytes)
    <span class="hljs-title function_">mSignatureSuccess</span>(myFile)
    <span class="hljs-comment">// showToast.hide()</span>
  }
}

} // pdf // all canvas to pdf const pdfWrap = ref()

// 签名组件成功 const mSignatureSuccess = (res) => { emits('success', res)

    </details>
  <h3 data-line="223" id="链接">链接</h3>

Demo页面
完整代码
PDF.JS
PDF-LIB
Smooth-Signature