{{format('1')}} {{format('492')}} {{format('4193')}}

【前端】jspdf+dom-to-image生成多页A4pdf加防截断处理 [ 前端 ]

晚安月亮 文章 正文

挣钱给咖喱买最好吃的罐罐
分享

椰奶冻

{{nature("2024-05-29 15:27:46")}}更新

首先,分析原始需求:点击导出为PDF文件时,弹出一个Modal框,预览生成的PDF文件,然后点击生成后下载PDF文件。

1. 预览时的文件是图片 or PDF or iFrame?

感觉预览图片会简单一点,还可以放大。那就用dom-to-image对DOM元素进行截图吧,然后上传图片url到oss,在预览窗口预览图片。

2. 下载时如何进行分页下载?

思路是将DOM图片按固定尺寸分批次调用 pdf.addPage() 和 pdf.addImage()方法生成pdf页面并插入剪切后的图片,如此循环操作,直至将整张图片遍历完毕后结束,最后调用pdf.save()方法进行保存

3. 分页截断怎么处理?

jspdf分页有个比较不好的地方就内容过长的时候虽然会虽然能做到分页,但是会把内容给截断,解决思路是给每个可能会被截断元素加上类,然后动态的计算该元素的位置是否在下一页和上一页之间,如果在的话就添加一个空白元素把这个元素给挤下去,这样就能实现

4.对于HTML生成PDF的前后端方案的分析

纯前端方案:

纯前端的方案,存在着浏览器环境依赖或一定的局限性, 一定程度上难以做到多端导出统一(也许可以,但是过于繁琐)。 以下是尝试过的方案:

1. printjs/window.print()

通过调取浏览器原生的打印功能进行打印,缺点: 对于自定义页眉页脚的自定义不友好。 需要用户手动打印/进行微调,对用户不够友好。

2. jsPDF + dom-to-image

实现是通过dom-to-image将HTML元素 转化 canvas 再转化成 JPED/PNG, 再通过jsPDF生成PDF文件。

优点: 生成符合完整符合样式的PDF,所见即所得,页头页尾自定义化高, 水印可以通过fixed布局元素生成。

缺点:需要手动计算分页点。 由于是通过转化成图片来生成PDF。 可以通过分割HTML元素来规避这个问题,但是操作会更加繁复。

最终选用jsPDF + dom-to-image的方案,和产品沟通后预览样式为dom(简单了很多)。html2Canvas也可以实现相同的效果,我这边用的是dom-to-image

实现原理:

动态计算每页dom元素的高度(元素margin会导致高度计算不准确,建议使用padding),将确保不被分割的dom 元素 加上特定的tag 标签(我这里用的是class='whole-node'),判断此dom 元素的最上面和最下面是否在同一页中,如果不在说明不处理就会被截断,怎么处理,不能被截断的元素上方插入对应的空白块占位,已达到将当前元素放到下一页的目的(当前页面高度 - dom元素最上方位置 = 需要插入空白块的高度)

import { jsPDF } from 'jspdf';
import { message } from 'antd';
import domtoimage from 'dom-to-image';
import moment from 'moment';

const convertPdf = (name: string, setLoading: any) => {
  try {
    const title = '文件名称';
    const A4_WIDTH = 592.28;
    const A4_HEIGHT = 880;
    // 要生成的dom
    const printDom: any = document.querySelector('#pdf_page');

    const pageHeight = (printDom.offsetWidth / A4_WIDTH) * A4_HEIGHT;
    const wholeNodes = document.querySelectorAll('.whole-node');

    // 在不能被截断的元素上方插入对应的空白块占位
    wholeNodes.forEach((node: any) => {
      const topPageNum = Math.ceil(node.offsetTop / pageHeight);
      const bottomPageNum = Math.ceil((node.offsetTop + node.offsetHeight) / pageHeight);

      if (topPageNum !== bottomPageNum) {
        // 说明dom会被截断
        const divParent = node.parentNode;
        const newBlock = document.createElement('div');
        newBlock.className = 'emptyDiv';
        const _H = topPageNum * pageHeight - node.offsetTop;
        // 空白块高度
        newBlock.style.height = _H + 60 + 'px';
        divParent.insertBefore(newBlock, node);
      }
    });

    domtoimage
      .toPng(printDom)
      .then((dataUrl) => {
        const emptyDivs = document.querySelectorAll('.emptyDiv');
        //dom 已经转换为canvas 对象,删除插入的空白块
        emptyDivs.forEach((div: any) => div.parentNode.removeChild(div));

        const img = new Image();
        img.src = dataUrl;
        img.onload = () => {
          const canvas = document.createElement('canvas');
          const ctx: any = canvas.getContext('2d');
          canvas.width = img.width;
          canvas.height = img.height;
          ctx.drawImage(img, 0, 0);

          const contentWidth = canvas.width;
          const contentHeight = canvas.height;
          const imgWidth = A4_WIDTH;
          const imgHeight = (A4_WIDTH / contentWidth) * contentHeight;
          const pageData = canvas.toDataURL('image/jpeg', 1.0);
          const PDF = new jsPDF('portrait', 'pt', 'a4');

          let position = 0;
          let leftHeight = contentHeight;

          while (leftHeight > 0) {
            PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
            leftHeight -= pageHeight;
            position -= A4_HEIGHT;

            if (leftHeight > 0) {
              PDF.addPage();
            }
          }

          PDF.save(`${title}.pdf`);
          message.success('保存成功');
          setLoading(false);
        };
      })
      .catch((error) => {
        console.error('Error generating PDF:', error);
        setLoading(false);
        message.error('保存失败');
      });
  } catch (error) {
    console.error('Error generating PDF:', error);
    setLoading(false);
    message.error('保存失败');
  }
};

export default convertPdf;

参考文档

jspdf+html2canvas生成多页pdf防截断处理

jsPDF+html2canvasA4分页截断完美解决方案

评论 1
1
{{userInfo.data?.nickname}}
{{userInfo.data?.email}}
TOP 2
【笔经】数字马力前端笔试-22应届

{{nature('2022-06-23 23:10:58')}} {{format('1317')}}人已阅读

TOP 3
【React】React组件卸载生命周期、路由跳转、页面关闭(刷新)拦截提示

{{nature('2023-02-03 16:12:08')}} {{format('1199')}}人已阅读

TOP 4
【前端】npm/yarn报错:getaddrinfo ENOTFOUND registry.nlark.com

{{nature('2024-05-29 15:14:38')}} {{format('571')}}人已阅读

TOP 5
【antd】DatePicker组件不可选中时间段

{{nature('2022-09-16 11:11:28')}} {{format('522')}}人已阅读

目录

标签云

React

一言

# {{hitokoto.data.from || '来自'}} #
{{hitokoto.data.hitokoto || '内容'}}
作者:{{hitokoto.data.from_who || '作者'}}
自定义UI
配色方案

侧边栏