使用html2canvas和jspdf生成的pdf 防止文字被切割_jspdf+html2canvas 转pdf 分页时会把内容切割-程序员宅基地

技术标签: pdf  前端  javascript  

解决内容被切断问题
支持页眉页脚页码
计算一页高度,在高度位置扫描像素点判断是否纯色 是的话在此切割不是就往上一行

import jsPDF from "jspdf";
import html2canvas from "html2canvas";

/**
 * 生成pdf(处理多页pdf截断问题)
 * @param {Object} param
 * @param {HTMLElement} param.element - 需要转换的dom根节点
 * @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-595
 * @param {number} [param.contentHeight=800] - 一页pdf的内容高度,0-842
 * @param {string} [param.outputType='save'] - 生成pdf的数据类型,默认是save下载下来,添加了'file'类型,其他支持的类型见http://raw.githack.com/MrRio/jsPDF/master/docs/jsPDF.html#output
 * @param {number} [param.scale=window.devicePixelRatio * 2] - 清晰度控制,canvas放大倍数,默认像素比*2
 * @param {string} [param.direction='p'] - 纸张方向,l横向,p竖向,默认A4纸张
 * @param {string} [param.fileName='document.pdf'] - pdf文件名,当outputType='file'时候,需要加上.pdf后缀
 * @param {number} param.baseX - pdf页内容距页面左边的高度,默认居中显示,为(A4宽度 - contentWidth) / 2)
 * @param {number} param.baseY - pdf页内容距页面上边的高度,默认 15px
 * @param {HTMLElement} param.header - 页眉dom元素
 * @param {HTMLElement} param.footer - 页脚dom元素
 * @param {string} [param.isPageMessage=false] - 是否显示当前生成页数状态
 * @param {string} [param.isTransformBaseY=false] - 是否将baseY按照比例缩小(一般固定A4页边距时候可以用上)
 * @returns {Promise} 根据outputType返回不同的数据类型,是一个对象,包含pdf结果及需要计算的元素位置信息
 */

export class PdfLoader {
    
  constructor(element, param = {
     }) {
    
    if (!(element instanceof HTMLElement)) {
    
      throw new TypeError("element节点请传入dom节点");
    }
    this.element = element;
    this.contentWidth = param.contentWidth || 550;
    this.outputType = param.outputType || "save";
    this.fileName = param.fileName || "导出的pdf文件";
    this.scale = param.scale;
    this.baseX = param.baseX;
    this.baseY = param.baseY == null ? 15 : param.baseY;
    this.isTransformBaseY = param.isTransformBaseY || false;
    this.header = param.header;
    this.footer = param.footer;
    this.isPageMessage = param.isPageMessage;
    this.direction = param.direction || "p"; // 默认竖向,l横向
    this.A4_WIDTH = 595; // a4纸的尺寸[595,842],单位像素
    this.A4_HEIGHT = 842;
    if (this.direction === "l") {
    
      // 如果是横向,交换a4宽高参数
      [this.A4_HEIGHT, this.A4_WIDTH] = [this.A4_WIDTH, this.A4_HEIGHT];
    }
    // 页眉页脚高度
    this.pdfFooterHeight = 0;
    this.pdfHeaderHeight = 0;
    this.pdf = null;
    this.rate = 1; // 缩放比率
    this.pages = []; // 当前分页数据
  }
  /**
   * 将元素转化为canvas元素
   * @param {HTMLElement} element - 当前要转换的元素
   * @param {width} width - 内容宽度
   * @returns
   */
  async createAndDisplayCanvas() {
    
    let imgData = await this.toCanvas(this.element, this.contentWidth);
    let canvasEle = document.createElement("canvas");
    canvasEle.width = imgData.width;
    canvasEle.height = imgData.height;
    canvasEle.style.position = "fixed";
    canvasEle.style.top = "0";
    canvasEle.style.right = "0";
    this.canvasEle = canvasEle;
    document.body.appendChild(canvasEle);
    const ctx = canvasEle.getContext("2d");
    const img = await this.loadImage(imgData.data);
    ctx.drawImage(img, 0, 0, imgData.width, imgData.height);
    this.scan(ctx, imgData);
  }
  // url 图片路径
  /**
   * 加载图片资源
   *
   * @param url 图片资源链接
   * @returns 返回Promise对象,当图片加载成功时resolve,否则reject
   */
  loadImage(url) {
    
    return new Promise((resolve, reject) => {
    
      const img = new Image();
      img.setAttribute("crossOrigin", "anonymous");
      img.src = url;
      img.onload = () => {
    
        // 当图像加载完成后进行resolve
        resolve(img);
      };
      img.onerror = () => {
    
        reject(new Error("图像加载失败"));
      };
    });
  }

  /**
   * 扫描图像并确定每一页的起始高度
   *
   * @param ctx 绘制上下文
   * @param imgData 图像数据
   * @throws 当ctx或imgData为null/undefined时抛出错误
   */
  scan(ctx, imgData) {
    
    if (!ctx || !imgData) {
    
      throw new Error("Invalid arguments: ctx or imgData is null/undefined");
    }

    let originalPageHeight = parseInt(this.originalPageHeight, 10);
    let shouldContinueScanning = true;

    while (shouldContinueScanning) {
    
      // console.log(this.pages); // 调试代码,生产环境中应去除

      let imageData = ctx.getImageData(0, originalPageHeight, imgData.width, 1);
      const uniqueArr = Array.from(new Set(imageData.data));

      if (uniqueArr.length === 1) {
    
        this.pages.push(originalPageHeight);

        originalPageHeight += parseInt(this.originalPageHeight, 10);
        if (originalPageHeight > imgData.height) {
    
          shouldContinueScanning = false;
          if (this.canvasEle) {
    
            this.canvasEle.remove();
            this.canvasEle = null;
          }
        }
      } else {
    
        if (originalPageHeight == this.pages.at(-1)) {
    
          // 防止无限递减
          shouldContinueScanning = false;
        } else {
    
          originalPageHeight = Math.max(0, originalPageHeight - 1); // 防止originalPageHeight变为负数
        }
      }
    }
  }

  async toCanvas(element, width) {
    
    // canvas元素
    let canvas = await html2canvas(element, {
    
      allowTaint: true, // 允许渲染跨域图片
      scale: this.scale || window.devicePixelRatio * 2, // 增加清晰度
      useCORS: true, // 允许跨域
    });
    // 获取canvas转化后的宽度
    const canvasWidth = canvas.width;
    // 获取canvas转化后的高度
    const canvasHeight = canvas.height;
    // 高度转化为PDF的高度
    const height = (width / canvasWidth) * canvasHeight;
    // 转化成图片Data
    const canvasData = canvas.toDataURL("image/jpeg", 1.0);
    canvas = null;
    return {
     width, height, data: canvasData };
  }

  /**
   * 生成pdf方法,外面调用这个方法
   * @returns {Promise} 返回一个promise
   */
  getPdf() {
    
    // 滚动置顶,防止顶部空白
    window.pageYOffset = 0;
    document.documentElement.scrollTop = 0;
    document.body.scrollTop = 0;
    return new Promise(async (resolve, reject) => {
    
      // jsPDF实例
      const pdf = new jsPDF({
    
        unit: "pt", // mm,pt,in,cm
        format: "a4",
        orientation: this.direction,
      });

      this.pdf = pdf;
      let pdfFooterHeight = 0;
      let pdfHeaderHeight = 0;

      // 距离PDF左边的距离,/ 2 表示居中 ,,预留空间给左边,  右边,也就是左右页边距
      let baseX = (this.A4_WIDTH - this.contentWidth) / 2;

      // 距离PDF 页眉和页脚的间距, 留白留空
      let baseY = this.baseY;
      // 元素在网页页面的宽度
      const elementWidth = this.element.scrollWidth;

      // PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度  转化为 距离Canvas顶部的高度
      const rate = this.contentWidth / elementWidth;
      this.rate = rate;
      if (this.isTransformBaseY) {
    
        this.baseY = baseY = baseY * rate;
      }

      // 页脚元素 经过转换后在PDF页面的高度
      if (this.footer) {
    
        pdfFooterHeight = (await this.toCanvas(this.footer, this.A4_WIDTH))
          .height;
        this.pdfFooterHeight = pdfFooterHeight;
      }

      // 页眉元素 经过转换后在PDF的高度
      if (this.header) {
    
        pdfHeaderHeight = (await this.toCanvas(this.header, this.A4_WIDTH))
          .height;
        this.pdfHeaderHeight = pdfHeaderHeight;
      }

      // 除去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度
      const originalPageHeight =
        this.A4_HEIGHT - pdfFooterHeight - pdfHeaderHeight - 2 * baseY;
      this.originalPageHeight = originalPageHeight;
      this.pages = [0]; // 要从0开始
      // 计算分页
      await this.createAndDisplayCanvas();
      const pages = this.pages;
      const {
     width, height, data } = await this.toCanvas(
        this.element,
        this.contentWidth
      );
      // 可能会存在遍历到底部元素为深度节点,可能存在最后一页位置未截取到的情况
      if (pages[pages.length - 1] + originalPageHeight < height) {
    
        pages.push(pages[pages.length - 1] + originalPageHeight);
      }

      // 根据分页位置 开始分页生成pdf
      for (let i = 0; i < pages.length; ++i) {
    
        if (this.isPageMessage) {
    
          // Message.success(`共${pages.length}页, 生成第${i + 1}页`);
        }
        // 页眉高度
        let pdfHeaderH = pdfHeaderHeight;
        // 页脚高度
        let pdfFooterH = pdfFooterHeight;
        // 根据分页位置新增图片,要排除页眉和顶部留白
        this.addImage(
          baseX,
          baseY + pdfHeaderH - pages[i],
          pdf,
          data,
          width,
          height
        );

        // 将 内容 与 页眉之间留空留白的部分进行遮白处理
        this.addBlank(0, pdfHeaderH, this.A4_WIDTH, baseY, pdf);
        // 将 内容 与 页脚之间留空留白的部分进行遮白处理
        this.addBlank(
          0,
          this.A4_HEIGHT - baseY - pdfFooterH,
          this.A4_WIDTH,
          baseY,
          pdf
        );
        // 对于除最后一页外,对 内容 的多余部分进行遮白处理
        if (i < pages.length - 1) {
    
          // 获取当前页面需要的内容部分高度
          const imageHeight = pages[i + 1] - pages[i];
          // 对多余的内容部分进行遮白
          this.addBlank(
            0,
            baseY + imageHeight + pdfHeaderH,
            this.A4_WIDTH,
            this.A4_HEIGHT - imageHeight,
            pdf
          );
        }
        // 添加页眉
        await this.addHeader(i + 1, this.header, pdf, this.A4_WIDTH);
        // 添加页脚
        await this.addFooter(
          pages.length,
          i + 1,
          this.footer,
          pdf,
          this.A4_WIDTH
        );

        // 若不是最后一页,则分页
        if (i !== pages.length - 1) {
    
          // 增加分页
          pdf.addPage();
        }
      }
      try {
    
        const result = await this.getPdfByType(pdf);
        resolve({
    
          pdfResult: result,
        });
      } catch (error) {
    
        reject("生成pdf出错", error);
      }
    });
  }

  // 根据类型获取pdf
  getPdfByType(pdf) {
    
    let result = null;
    switch (this.outputType) {
    
      case "file":
        result = new File([pdf.output("blob")], this.fileName, {
    
          type: "application/pdf",
          lastModified: Date.now(),
        });
        break;
      case "save":
        result = pdf.save(this.fileName);
        break;
      default:
        result = pdf.output(this.outputType);
    }
    return result;
  }

  /**
   * 添加页眉
   * @param {HTMLElement} header -页眉元素
   * @param {Object} pdf - pdf实例
   * @param {Number} contentWidth -在pdf中占据的宽度(默认占满)
   * @returns
   */
  async addHeader(pageNo, header, pdf, contentWidth) {
    
    if (!header || !(header instanceof HTMLElement)) {
    
      return;
    }
    if (!this.__header) {
    
      // 其他页 页头都是一样的,不需要每次都生成
      this.__header = await this.toCanvas(header, contentWidth);
    }
    //  每页都从 0 0 开始?

    const {
     height, data } = this.__header;
    pdf.addImage(data, "JPEG", 0, 0, contentWidth, height);
  }

  /**
   * 添加页脚
   * @param {Number} pageSize -总页数
   * @param {Number} pageNo -当前第几页
   * @param {HTMLElement} footer -页脚元素
   * @param {Object} pdf - pdf实例
   * @param {Number} contentWidth - 在pdf中占据的宽度(默认占满)
   * @returns
   */
  async addFooter(pageSize, pageNo, footer, pdf, contentWidth) {
    
    if (!footer || !(footer instanceof HTMLElement)) {
    
      return;
    }

    // 页码元素,类名这里写死了
    let pageNoDom = footer.querySelector(".pdf-footer-page");
    let pageSizeDom = footer.querySelector(".pdf-footer-page-count");
    if (pageNoDom) {
    
      pageNoDom.innerText = pageNo;
    }
    if (pageSizeDom) {
    
      pageSizeDom.innerText = pageSize;
    }

    // 如果设置了页码的才需要每次重新生成cavan
    if (pageNoDom || !this.__footer) {
    
      this.__footer = await this.toCanvas(footer, contentWidth);
    }
    const {
     height, data } = this.__footer;
    // 高度位置计算:当前a4高度 - 页脚在pdf中的高度
    pdf.addImage(
      data,
      "JPEG",
      0,
      this.A4_HEIGHT - height,
      contentWidth,
      height
    );
  }

  // 截取图片
  addImage(_x, _y, pdf, data, width, height) {
    
    pdf.addImage(data, "JPEG", _x, _y, width, height);
  }

  /**
   * 添加空白遮挡
   * @param {Number} x - x 与页面左边缘的坐标(以 PDF 文档开始时声明的单位)
   * @param {Number} y - y 与页面上边缘的坐标(以 PDF 文档开始时声明的单位)
   * @param {Number} width - 填充宽度
   * @param {Number} height -填充高度
   * @param {Object} pdf - pdf实例
   * @returns
   */
  addBlank(x, y, width, height, pdf) {
    
    pdf.setFillColor(255, 255, 255);
    // rect(x, y, w, h, style) ->'F'填充方式,默认是描边方式
    pdf.rect(x, y, Math.ceil(width), Math.ceil(height), "F");
  }
}

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42689772/article/details/136501838

智能推荐

解决win10/win8/8.1 64位操作系统MT65xx preloader线刷驱动无法安装_mt65驱动-程序员宅基地

文章浏览阅读1.3w次。转载自 http://www.miui.com/thread-2003672-1-1.html 当手机在刷错包或者误修改删除系统文件后会出现无法开机或者是移动定制(联通合约机)版想刷标准版,这时就会用到线刷,首先就是安装线刷驱动。 在XP和win7上线刷是比较方便的,用那个驱动自动安装版,直接就可以安装好,完成线刷。不过现在也有好多机友换成了win8/8.1系统,再使用这个_mt65驱动

SonarQube简介及客户端集成_sonar的客户端区别-程序员宅基地

文章浏览阅读1k次。SonarQube是一个代码质量管理平台,可以扫描监测代码并给出质量评价及修改建议,通过插件机制支持25+中开发语言,可以很容易与gradle\maven\jenkins等工具进行集成,是非常流行的代码质量管控平台。通CheckStyle、findbugs等工具定位不同,SonarQube定位于平台,有完善的管理机制及强大的管理页面,并通过插件支持checkstyle及findbugs等既有的流..._sonar的客户端区别

元学习系列(六):神经图灵机详细分析_神经图灵机方法改进-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏27次。神经图灵机是LSTM、GRU的改进版本,本质上依然包含一个外部记忆结构、可对记忆进行读写操作,主要针对读写操作进行了改进,或者说提出了一种新的读写操作思路。神经图灵机之所以叫这个名字是因为它通过深度学习模型模拟了图灵机,但是我觉得如果先去介绍图灵机的概念,就会搞得很混乱,所以这里主要从神经图灵机改进了LSTM的哪些方面入手进行讲解,同时,由于模型的结构比较复杂,为了让思路更清晰,这次也会分开几..._神经图灵机方法改进

【机器学习】机器学习模型迭代方法(Python)-程序员宅基地

文章浏览阅读2.8k次。一、模型迭代方法机器学习模型在实际应用的场景,通常要根据新增的数据下进行模型的迭代,常见的模型迭代方法有以下几种:1、全量数据重新训练一个模型,直接合并历史训练数据与新增的数据,模型直接离线学习全量数据,学习得到一个全新的模型。优缺点:这也是实际最为常见的模型迭代方式,通常模型效果也是最好的,但这样模型迭代比较耗时,资源耗费比较多,实时性较差,特别是在大数据场景更为困难;2、模型融合的方法,将旧模..._模型迭代

base64图片打成Zip包上传,以及服务端解压的简单实现_base64可以装换zip吗-程序员宅基地

文章浏览阅读2.3k次。1、前言上传图片一般采用异步上传的方式,但是异步上传带来不好的地方,就如果图片有改变或者删除,图片服务器端就会造成浪费。所以有时候就会和参数同步提交。笔者喜欢base64图片一起上传,但是图片过多时就会出现数据丢失等异常。因为tomcat的post请求默认是2M的长度限制。2、解决办法有两种:① 修改tomcat的servel.xml的配置文件,设置 maxPostSize=..._base64可以装换zip吗

Opencv自然场景文本识别系统(源码&教程)_opencv自然场景实时识别文字-程序员宅基地

文章浏览阅读1k次,点赞17次,收藏22次。Opencv自然场景文本识别系统(源码&教程)_opencv自然场景实时识别文字

随便推点

ESXi 快速复制虚拟机脚本_exsi6.7快速克隆centos-程序员宅基地

文章浏览阅读1.3k次。拷贝虚拟机文件时间比较长,因为虚拟机 flat 文件很大,所以要等。脚本完成后,以复制虚拟机文件夹。将以下脚本内容写入文件。_exsi6.7快速克隆centos

好友推荐—基于关系的java和spark代码实现_本关任务:使用 spark core 知识完成 " 好友推荐 " 的程序。-程序员宅基地

文章浏览阅读2k次。本文主要实现基于二度好友的推荐。数学公式参考于:http://blog.csdn.net/qq_14950717/article/details/52197565测试数据为自己随手画的关系图把图片整理成文本信息如下:a b c d e f yb c a f gc a b dd c a e h q re f h d af e a b gg h f bh e g i di j m n ..._本关任务:使用 spark core 知识完成 " 好友推荐 " 的程序。

南京大学-高级程序设计复习总结_南京大学高级程序设计-程序员宅基地

文章浏览阅读367次。南京大学高级程序设计期末复习总结,c++面向对象编程_南京大学高级程序设计

4.朴素贝叶斯分类器实现-matlab_朴素贝叶斯 matlab训练和测试输出-程序员宅基地

文章浏览阅读3.1k次,点赞2次,收藏12次。实现朴素贝叶斯分类器,并且根据李航《统计机器学习》第四章提供的数据训练与测试,结果与书中一致分别实现了朴素贝叶斯以及带有laplace平滑的朴素贝叶斯%书中例题实现朴素贝叶斯%特征1的取值集合A1=[1;2;3];%特征2的取值集合A2=[4;5;6];%S M LAValues={A1;A2};%Y的取值集合YValue=[-1;1];%数据集和T=[ 1,4,-1;..._朴素贝叶斯 matlab训练和测试输出

Markdown 文本换行_markdowntext 换行-程序员宅基地

文章浏览阅读1.6k次。Markdown 文本换行_markdowntext 换行

错误:0xC0000022 在运行 Microsoft Windows 非核心版本的计算机上,运行”slui.exe 0x2a 0xC0000022″以显示错误文本_错误: 0xc0000022 在运行 microsoft windows 非核心版本的计算机上,运行-程序员宅基地

文章浏览阅读6.7w次,点赞2次,收藏37次。win10 2016长期服务版激活错误解决方法:打开“注册表编辑器”;(Windows + R然后输入Regedit)修改SkipRearm的值为1:(在HKEY_LOCAL_MACHINE–》SOFTWARE–》Microsoft–》Windows NT–》CurrentVersion–》SoftwareProtectionPlatform里面,将SkipRearm的值修改为1)重..._错误: 0xc0000022 在运行 microsoft windows 非核心版本的计算机上,运行“slui.ex