使用pdfjs-dist库加载pdf文件及盖章原理

1,010 阅读2分钟

功能简述:在Drawer组件里的分步表单中,根据上一步上传的pdf文件,加载pdf文件,拖拽元素到pdf,拖拽完以后使用canvas画一个占位符(标记章的位置),并把元素坐标位置信息上传到上上签,在指定的位置生成印章和签名,使用的ui库是view-design,具体实现步骤如下

  1. 安装并引入第三方库pdfjs-dist
import * as pdfjsLib from 'pdfjs-dist';
  1. 加载PDF文件,这里使用了两个canvas,一个用于渲染pdf,一个用于在pdf上面蒙上一个画布,把拖拽元素生成的占位符在画布上显示。避免混在一起,造成一些渲染白屏的问题。
renderPDF() {
  // 加载PDF
  pdfjsLib.getDocument(this.getUploadPdfUrl).promise.then(pdf => {
    this.pdf = pdf;
    this.totalPages = pdf.numPages;
    // 初始化渲染第一页
    this.renderPage(this.currentPage);
  });
},

async renderPage(pageNumber) {
  if (pageNumber > this.totalPages) {
    return;
  }

  const pdfPage = await this.pdf.getPage(pageNumber);

  const viewport = pdfPage.getViewport({ scale: 1.5 });
  // 创建pdf-canvas
  const pdfCanvas = document.createElement('canvas');
  const canvasContext = pdfCanvas.getContext('2d');

  // 创建draw-canvas
  const drawCanvas = document.createElement('canvas');
  // const canvasContext = drawCanvas.getContext('2d');
  pdfCanvas.width = drawCanvas.width = viewport.width;
  pdfCanvas.height = drawCanvas.height = viewport.height;

  this.pdf.getPage(pageNumber).then(page => {
    const renderTask = page.render({
      canvasContext,
      viewport,
    });
    renderTask.promise.then(() => {
      // 渲染完成后销毁旧的Canvas元素
      if (this.$refs.pdfCanvas) {
        this.$refs.pdfCanvas.parentNode.removeChild(this.$refs.pdfCanvas);
      }

      if (this.$refs.drawCanvas) {
        this.$refs.drawCanvas.parentNode.removeChild(this.$refs.drawCanvas);
      }

      // 将新的Canvas元素添加到DOM中
      this.$refs.scrollContainer.appendChild(pdfCanvas);
      this.$refs.scrollContainer.appendChild(drawCanvas);
      this.$refs.pdfCanvas = pdfCanvas;
      this.$refs.drawCanvas = drawCanvas;
      this.$refs.pdfCanvas.style = `position: absolute; top: 0; left: 0`;
      this.$refs.drawCanvas.style = `position: absolute; top: 0; left: 0; z-index: 1;`;

      this.currentPage = pageNumber;
      this.drawRectangles();
    });
  });
},
  1. 调用drawRectangles()方法,绘制占位符,把每页pdf上面的拖拽元素坐标信息保存在rect对象中,用于后续的编辑时回显盖章位置坐标
async drawRectangles() {
  const canvas = this.$refs.drawCanvas;
  const context = canvas.getContext('2d');

  // 清空画布
  context.clearRect(0, 0, canvas.width, canvas.height);

  this.rect[this.currentPage]?.forEach(rectangle => {
    let width = 0;
    if (rectangle.type === 'seal' && rectangle.signType === 1) {
      // 签名
      context.fillStyle = 'yellow'; // 设置背景色
      context.fillRect(rectangle.x, rectangle.y, 134, 60);
      context.fillStyle = '#000';
      context.font = '14px Arial';
      context.textAlign = 'center';
      context.textBaseline = 'middle';
      const text = '签名处';
      context.fillText(text, rectangle.x + 67, rectangle.y + 30);
      width = 134;
    } else if (rectangle.type === 'seal' && rectangle.signType === 0) {
      // 签章
      context.fillStyle = 'red'; // 设置背景色
      context.fillRect(rectangle.x, rectangle.y, 200, 200);
      context.fillStyle = '#000';
      context.font = '14px Arial';
      context.textAlign = 'center';
      context.textBaseline = 'middle';
      const text = '盖章处';
      context.fillText(text, rectangle.x + 100, rectangle.y + 100);
      width = 200;
    } else if (rectangle.type === 'date' && rectangle.signType === 1) {
      context.fillStyle = 'yellow'; // 设置背景色
      context.fillRect(rectangle.x, rectangle.y, 140, 40);
      context.fillStyle = '#000';
      context.font = '14px Arial';
      context.textAlign = 'center';
      context.textBaseline = 'middle';
      const text = '签署日期';
      context.fillText(text, rectangle.x + 70, rectangle.y + 20);
      width = 140;
    } else {
      context.fillStyle = 'red'; // 设置背景色
      context.fillRect(rectangle.x, rectangle.y, 140, 40);
      context.fillStyle = '#000';
      context.font = '14px Arial';
      context.textAlign = 'center';
      context.textBaseline = 'middle';
      const text = '签署日期';
      context.fillText(text, rectangle.x + 70, rectangle.y + 20);
      width = 140;
    }

    // 绘制圆
    const centerX = rectangle.x + width;
    const centerY = rectangle.y;
    const radius = 10;

    context.beginPath();
    context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
    context.fillStyle = 'rgba(0, 0, 0, 0.5)';
    context.fill();

    // 绘制删除按钮
    context.fillStyle = 'white';
    context.font = '14px Arial';
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.fillText('X', centerX, centerY);
  });

  // 绘制矩形占位符

  canvas.addEventListener('click', this.onClickCanvas);
},
  1. 拖拽的占位符点击右上角删除按钮,支持删除对应的占位符
onClickCanvas(event) {
  const canvas = this.$refs.drawCanvas;
  const canvasRect = canvas.getBoundingClientRect();
  const clickX = event.clientX - canvasRect.left;
  const clickY = event.clientY - canvasRect.top;

  // 检查是否点击了删除按钮
  this.rect[this.currentPage]?.forEach((rectangle, index) => {
    let width = 0;
    if (rectangle.type === 'seal' && rectangle.signType === 1) {
      // 签名
      width = 134;
    } else if (rectangle.type === 'seal' && rectangle.signType === 0) {
      // 签章
      width = 200;
    } else {
      // 日期
      width = 140;
    }
    const deleteButtonX = rectangle.x + width - 10;
    const deleteButtonY = rectangle.y - 10;

    if (
      clickX >= deleteButtonX &&
      clickX <= deleteButtonX + 20 &&
      clickY >= deleteButtonY &&
      clickY <= deleteButtonY + 20
    ) {
      this.rect[this.currentPage].splice(index, 1);
      this.drawRectangles();
    }
  });
},
  1. 处理scrollContainer的拖拽事件,拖拽完成时计算出拖拽元素相对pdf的位置坐标,并根据坐标位置添加相应的占位符,在拖拽的过程中,还会判断越界情况
handleDragover(event) {
  event.preventDefault();
},
handleDrop(event) {
  event.preventDefault();
  const canvas = this.$refs.drawCanvas;
  const rect = canvas.getBoundingClientRect();

  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;
  this.addRectanglePlaceholder(x, y);
},
addRectanglePlaceholder(x, y) {
  const canvas = this.$refs.drawCanvas;
  const percentX = ((x - this.offsetX) / canvas.width).toFixed(2);
  const percentY = Math.abs((y - this.offsetY) / canvas.height).toFixed(2);
  const rectangle = {
    x: x - this.offsetX,
    y: y - this.offsetY,
    signId: this.signId,
    type: this.dragType,
    signType: this.signType,
    pageNum: this.currentPage,
    percentX,
    percentY,
  };

  if (this.dragType === 'seal' && this.signType === 1) {
    // 签名
    if (x - this.offsetX + 134 >= canvas.width || x - this.offsetX < 0) {
      // 越界
      this.$Message.error({
        content: '拖动位置已超出合同边界,请重新拖动',
        duration: 5,
      });
      return;
    }
  }

  if (this.dragType === 'seal' && this.signType === 0) {
    if (x - this.offsetX + 200 >= canvas.width || x - this.offsetX < 0) {
      this.$Message.error({
        content: '拖动位置已超出合同边界,请重新拖动',
        duration: 5,
      });
      return;
    }
  }

  if (this.dragType === 'date') {
    if (x - this.offsetX + 140 >= canvas.width || x - this.offsetX < 0) {
      this.$Message.error({
        content: '拖动位置已超出合同边界,请重新拖动',
        duration: 5,
      });
      return;
    }
  }

  if (!this.rect[this.currentPage]) {
    this.rect[this.currentPage] = [];
    this.rect[this.currentPage].push(rectangle);
  } else {
    this.rect[this.currentPage].push(rectangle);
  }

  this.drawRectangles();
},
handleDragStart(event) {
  this.offsetX = event.offsetX;
  this.offsetY = event.offsetY;
  const itemElement = event.target;
  const type = itemElement.getAttribute('data-type');
  this.dragType = type;
},
  1. 编辑进来回显盖章和签名的坐标位置信息
// 初始化
process() {
  const canvas = this.$refs.drawCanvas;
  const resultMap = {};
  this.sealCoordinateList?.forEach(it => {
    if (it.sealPageNum) {
      if (resultMap[it.sealPageNum]) {
        resultMap[it.sealPageNum].push({
          x: Math.floor(it.sealX * canvas.width),
          y: Math.floor(it.sealY * canvas.height),
          signId: it.signId,
          signType: it.signType,
          type: 'seal',
          pageNum: it.sealPageNum,
          percentX: it.sealX,
          percentY: it.sealY,
        });
      } else {
        resultMap[it.sealPageNum] = [          {            x: Math.floor(it.sealX * canvas.width),            y: Math.floor(it.sealY * canvas.height),            signId: it.signId,            signType: it.signType,            type: 'seal',            pageNum: it.sealPageNum,            percentX: it.sealX,            percentY: it.sealY,          },        ];
      }
    }
    if (it.datePageNum) {
      if (resultMap[it.datePageNum]) {
        resultMap[it.datePageNum].push({
          x: Math.floor(it.dateX * canvas.width),
          y: Math.floor(it.dateY * canvas.height),
          signId: it.signId,
          signType: it.signType,
          type: 'date',
          pageNum: it.datePageNum,
          percentX: it.dateX,
          percentY: it.dateY,
        });
      } else {
        resultMap[it.datePageNum] = [          {            x: Math.floor(it.dateX * canvas.width),            y: Math.floor(it.dateY * canvas.height),            signId: it.signId,            signType: it.signType,            type: 'date',            pageNum: it.datePageNum,            percentX: it.dateX,            percentY: it.dateY,          },        ];
      }
    }
  });
  this.rect = resultMap;
},
  1. template模板内容
<template>
  <div class="sign-position-box">
    <div class="drag-container">
      <div class="fixed">
        <div class="first-step">
          <div class="text">
            第一步:选择签约方
          </div>
          <RadioGroup v-model="sign" vertical @on-change="handleRadioChange">
            <Radio v-for="item in getSignaryInfo" :key="item.name" :label="item.name">
              {{ item.name }}
            </Radio>
          </RadioGroup>
        </div>
        <div class="second-step">
          <div class="text">
            第二步:拖动签约位置
          </div>
          <div class="sign-box">
            <div
              v-if="signType === 1"
              class="seal"
              draggable="true"
              data-type="seal"
              @dragstart="handleDragStart"
            >
              <div class="circle">
                签
              </div>
              <span>签名</span>
            </div>
            <div
              v-else
              class="seal"
              draggable="true"
              data-type="seal"
              @dragstart="handleDragStart"
            >
              <div class="circle">
                章
              </div>
              <span>盖章</span>
            </div>
            <div
              class="seal"
              draggable="true"
              data-type="date"
              @dragstart="handleDragStart"
            >
              <div class="circle">
                日
              </div>
              <span>签署日期</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- <div id="fileContent" class="file-content" /> -->
    <div ref="scrollContainer" class="file-content">
      <canvas ref="pdfCanvas" class="pdf-canvas" />
      <canvas ref="drawCanvas" class="draw-canvas" />
    </div>
  </div>
</template>