前言

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

主要实现

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

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

缩放实现

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

.pdf-wrap {
      min-height: 100vh;
      width: calc(var(--scale-factor) * 612px);
      height: calc(var(--scale-factor) * 792px);
      transition: all 0.5s ease-in;
    }

主要js代码

  import { ref, onMounted } from 'vue'
  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> <span class="hljs-variable constant_">renderContext</span> = {
    canvasContext: context,
    viewport,
  }
  <span class="hljs-comment">// console.log(page)</span>
  await page.<span class="hljs-title function_ invoke__">render</span>(renderContext)
  loading.value = <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> <span class="hljs-variable constant_">sealImageBytes</span> = await <span class="hljs-title function_ invoke__">fetch</span>(sealLogo).<span class="hljs-title function_ invoke__">then</span>(res =&gt; res.<span class="hljs-title function_ invoke__">arrayBuffer</span>())
    <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">sealImg</span> = await pdfDoc.<span class="hljs-title function_ invoke__">embedPng</span>(sealImageBytes)
    pages[pageIndex].<span class="hljs-title function_ invoke__">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>
    pages[pageIndex].<span class="hljs-title function_ invoke__">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> <span class="hljs-variable constant_">dateText</span> = <span class="hljs-title function_ invoke__">dayjs</span>().<span class="hljs-title function_ invoke__">format</span>(<span class="hljs-string">&#x27;YYYY MM DD&#x27;</span>)
    console.<span class="hljs-title function_ invoke__">log</span>(dateText)
    pages[pageIndex].<span class="hljs-title function_ invoke__">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-attr">font</span>: customFont,
    })
    <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">pdfBytes</span> = await pdfDoc.<span class="hljs-title function_ invoke__">save</span>()
    <span class="hljs-comment">// console.log(pdfBytes)</span>
    <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">myFile</span> = <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_ invoke__">reloadPdf</span>(pdfBytes)
    <span class="hljs-title function_ invoke__">mSignatureSuccess</span>(myFile)
    <span class="hljs-comment">// showToast.hide()</span>
  }
}

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

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

链接

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