利用Html2canvas和JsPdf插件导出pdf文件及如何处理截断问题

134 阅读3分钟

如果需要打印,利用插件print-js, 自己install即可。 业务需求就是导出pdf文件,内容过多,分页,防止截断。大家一搜业务场景大差不差吧,这里不再赘述,如需理论分析,查阅一下即可。直接上代码

项目代码用react实现,我这里简单用html表示页面内容,重点在于js文件

<div class="button" id="print-btn" onclick="{() => this.generatePDF()}">导出pdf文件</div>
<div id="print-box"> 
  <div class="item"> 
  </div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>
generatePDF = async (isPrint) => {
    /**
     * A4 纸尺寸
     * */ 
    const A4_WIDTH = 595.28;
    // const A4_HEIGHT = 841.89;
    const A4_HEIGHT = 879;
    const intervalHeight = 0; // 不允许被截断元素之间的间隔高度

    // 获取需要截取节点的 dom
    // const domElement = document.getElementById('printJS-form');
    const domElement = document.getElementById('print-box'); 
    // console.log('domElement=====>', domElement);

    // 获取目标元素的高度(去除滚动条时高度)
    const domScrollHeight = domElement.scrollHeight;
    const domScrollWidth = domElement.scrollWidth;

    // 根据A4的宽高等比计算 dom页面对应的高度
    const pageWidth = domElement.offsetWidth;
    const pageHeight = (pageWidth / A4_WIDTH) * A4_HEIGHT; // 高度转化为PDF的高度

    // 将所有不允许被截断的子元素进行处理
    const wholeNodes = domElement.querySelectorAll('.item');

    // 插入空白块的总高度
    let allEmptyNodeHeight = 0;
    const videoNodes = document.querySelectorAll('.videoItem'); 
    const videoImages = document.querySelectorAll('.videoImage'); 

    for (let i = 0; i < wholeNodes.length; i++) {
      // 判断当前的不可分页元素是否在两页显示
      const topPageNum = Math.ceil(wholeNodes[i].offsetTop / pageHeight);
      const bottomPageNum = Math.ceil((wholeNodes[i].offsetTop + wholeNodes[i].offsetHeight) / pageHeight);

      if (videoNodes.length >= 0 && videoImages.length == 0) {
        const mVideoAreaBox = document.querySelector('.videoArea'); 
        if (mVideoAreaBox) {
          mVideoAreaBox.style.display = 'none';
        } 
      }

      if (videoNodes.length > 0 && videoImages.length != 0) { 
        for(let i = 0; i < videoNodes.length; i++) {
          videoNodes[i].style.display = 'none'  // 下载报告隐藏视频
        } 
      }

      // 说明该 dom 会被截断
      if (topPageNum !== bottomPageNum) {
        // 创建空白块
        const newBlock = document.createElement('div');
        newBlock.className = 'empty-node';
        newBlock.style.background = '#fff';
        // newBlock.style.background = 'red';
        newBlock.style.opacity = 0;

        // 计算空白块的高度,可以适当留出空间使得内容不会太靠边,根据自己需求而定
        const _H = topPageNum * pageHeight - wholeNodes[i].offsetTop;
        newBlock.style.height = _H + intervalHeight + 'px';

        // 插入空白块
        wholeNodes[i].parentNode.insertBefore(newBlock, wholeNodes[i]);

        // 更新插入空白块的总高度
        allEmptyNodeHeight = allEmptyNodeHeight + _H + intervalHeight;
      } 
    }

    // 设置打印区域的高度 (目标元素的高度 + 添加的空白块的高度)
    domElement.setAttribute('style', `height: ${domScrollHeight + allEmptyNodeHeight}px; width: ${domScrollWidth}px;`)

    /**
     * 以上完成 dom 层面的分页,可以转为图片进一步处理了
     * options
     *  - useCORS:是否尝试使用 CORS 从服务器加载图像
     *  - scale:用于渲染的比例,默认为浏览器设备像素比
     *  - ignoreElements:忽略的元素
     */
    await html2canvas(domElement, {
      width: domElement.offsetWidth,
      height: domElement.offsetHeight,
      useCORS: true,
      scale: 3,
    }).then(canvas => {
      // dom 已经转换为 canvas 对象,可以将插入的空白块删除了
      const emptyNodes = domElement.querySelectorAll('.empty-node');

      for (let i = 0; i < emptyNodes.length; i++) {
        emptyNodes[i].style.height = 0;
        emptyNodes[i].parentNode.removeChild(emptyNodes[i]);
      }

      const canvasWidth = canvas.width;
      const canvasHeight = canvas.height;

      // html 页面实际高度
      let htmlHeight = canvasHeight;

      // 页面偏移量
      let position = 0;

      // 根据 A4 的宽高等比计算 pdf 页面对应的高度
      const pageHeight = (canvasWidth / A4_WIDTH) * A4_HEIGHT;

      // html 页面生成的 canvas 在 pdf 中图片的宽高
      const imgWidth = A4_WIDTH;
      const imgHeight = (A4_WIDTH / canvasWidth) * canvasHeight;

      // 将图片转为 base64 格式
      const imageData = canvas.toDataURL('image/jpeg', 1.0);
      if (isPrint) {
        //如果是打印,可以拿着分号页的数据 直接使用
        const style = 'body {margin-top: 0; background-color: #fff}'
        printJS({ 
          printable: imageData, 
          type: 'image', 
          base64: true,
          documentTitle: '打印报告',
          // 解决出现多页打印时第一页空白问题
          // style: `@media print {@page {size: auto; margin: 0;} body: { margin: 0 5px }}`
          style
        })
        return
      }
      /**
       * 生成 pdf 实例
       *  - 方向: l 横向,p 纵向
       *  - 测量单位:"pt","mm","cm","m","in","px"
       *  - 格式:默认 a4
       */
      const PDF = new jsPDF('p', 'pt', 'a4');

      // html 页面的实际高度小于生成 pdf 的页面高度时,即内容未超过 pdf 一页显示的范围,无需分页
      if (htmlHeight <= pageHeight) {
        /**
         * 在 PDF 文档中添加图像
         *  - 图像数据
         *  - 图像格式
         *  - x 轴位置
         *  - y 轴位置
         *  - 图像宽度
         *  - 图像高度
         */
        PDF.addImage(imageData, 'JPEG', 0, 0, imgWidth, imgHeight)
      } else {
        while(htmlHeight > 0) {
          PDF.addImage(imageData, 'JPEG', 0, position, imgWidth, imgHeight); 

          // 更新高度和偏移量 
          htmlHeight -= pageHeight;
          position -= A4_HEIGHT;

          if (htmlHeight > 0) {
            // 在pdf文档中添加新页面
            PDF.addPage()
          }
        }
      }
 
      // 保存pdf文件
      PDF.save(`${this.state.type == 0 ? '下载xx报告' : this.state.type == 1 ? '下载xx报告' : '下载xx报告'}.pdf`)
    }) 
    
    // 还原页面展示布局
    this.setState({
      showDom: false
    }, () => {
      setTimeout(() => {
        this.setState({
          showDom: true
        })
      })
    }); 
  } 

js代码实现文件,这里我也参考了很多友友们的方案,结合自己的业务实现。

代码可以直接运行,注意修改一下外层包裹的容器id和item, 记得安装依赖html2Canvas、jspdf、print-js(需要打印机打印)。