网页指定元素生成PDF下载

820 阅读2分钟

该实现方案是基于html2canvas和jspdf进行开发的,具体参考html2canvas.hertzen.com/configurati… 文档 www.npmjs.com/package/jsp… 文档 以下代码都是可以直接使用的,下载的PDF的效果如下

image.png

image.png

一、该方案解决了两个痛点:

1、下载页码收到限制
2、分页的时候会截断文字或者图片

二、使用DEMO

下面是原生的JavaScript实现,如果想要Vue实现,可以稍微改动一下即可,如果不会,在留言中告诉我,后续我加上在Vue中实现

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>下载PDF</title>
  </head>
  <style></style>
  <body>
    <div style="text-align: left">
      <button id="qubtn-download" style="margin: 20px 200px">下载</button>
      <div
        id="print"
        style="
          display: flex;
          justify-content: center;
          max-height: 600px;
          width: 595px;
          margin-top: 20px;
          text-align: center;
          overflow: auto;
        "
      >
        <div
          id="print-area"
          style="width: 595px; height: 100%; text-align: center"
        ></div>
      </div>
    </div>
  </body>
  // html2canvas.min.js 可以通过 https://github.com/niklasvh/html2canvas/releases下载
  <script src="./html2canvas.min.js"></script>
  <script src="https://unpkg.com/jspdf@latest/dist/jspdf.umd.min.js"></script>
  <script src="./downloadPDF.js"></script>
  <script>
    //下面是两个插件的文档
    // https://html2canvas.hertzen.com/configuration 文档
    // https://www.npmjs.com/package/jspdf 文档

    // 生成测试的节点数据
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 1000; i++) {
      const div = document.createElement("div");
      div.style.marginBottom = "20px";
      div.style.textAlign = "center";
      div.innerHTML = "我是用来测试打印的测试数据";
      fragment.append(div);
    }
    document.querySelector("#print-area").append(fragment);

    // 注册打印事件
    document
      .querySelector("#qubtn-download")
      .addEventListener("click", function () {
        // 调用PDF下载类,传入要下载元素的id即可,不限页码!
        // 主要看这里
        new DownloadPDF("print-area").init();
      });
  </script>
</html>

三、实现下载PDF代码

因为注释已经很详细了,如果觉得哪里看不懂有疑问可留言

/**
 * @description 该PDF下载类,基于jsPDF 跟 html2canvas两个插件开发,下载页码不受限制,并且分页的时候不会截断文字跟图片
 * @author 郭太东
 * @param id 需要下载的元素id
 */
class DownloadPDF {
  constructor(id) {
    this.id = id; // 需要下载的元素id
    this.devicePixelRatio = 2; // canvas高度 = 实际的元素高度*this.devicePixelRatio
    this.canvasHeight; // html转成canvas计算出的最大理论高度
    this.pageTotal; // 当前canvas可生成最多页数
    this.canvasW = 850; // 写死canvas的宽度,一定要跟实际元素宽度一致,才能计算准确
    this.a4Width = 595.28;
    this.a4Height = 841.89; //A4大小,210mm x 297mm,(对应的px:595.28*841.89)四边各保留10mm的边距,显示区域190x277
    this.a4HeightRef; //一页pdf显示html页面生成的canvas标准高度
    this.canvasCurH; // 当前生成canvas的总高度
    this.yPos = 0; // canvas开始绘制的y轴坐标点
    this.pageNumber = 0; // pdf底部页码显示
  }
  // 入口
  init() {
    this.canvasHeight =
      this.devicePixelRatio *
      document.querySelector("#" + this.id).clientHeight; // 计算canvas总高度(生成canvas的时候记得写死宽度,宽度跟id是print-area的元素一样)
    this.a4HeightRef = Math.floor(
      ((this.canvasW * this.devicePixelRatio) / this.a4Width) *
        (this.a4Height - 22)
    );
    this.pageTotal = Math.floor(this.canvasHeight / this.a4HeightRef) + 1;
    // 第一个canvas 高度, 一个canvas大概能容纳25页左右pdf高度的内容,多了会黑屏无法显示
    this.canvasCurH =
      this.pageTotal <= 25 ? this.canvasHeight : 25 * this.a4HeightRef;
    // jspdf 这是全局使用时候的使用方法
    const pdf = new jspdf.jsPDF("p", "pt", "a4"); //A4纸,纵向
    pdf.setDisplayMode("fullwidth", "continuous", "FullScreen");
    pdf.setFontSize(8); // 设置字体大小
    this.downloadPDF(pdf);
  }

  /**
   * @returns {void} 生成canvas 下载pdf
   */
  downloadPDF(pdf) {
    const _this = this;
    html2canvas(document.querySelector("#" + this.id), {
      allowTaint: true,
      useCORS: true,
      height: this.canvasCurH / 2,
      width: this.canvasW, // 写死canvas的宽度,一定要跟实际元素宽度一致,才能计算准确
      scale: this.devicePixelRatio,
      y: this.yPos / 2,
      background: "#FFFFFF", //如果指定的div没有设置背景色会默认成黑色,这里是个坑
    })
      .then((canvas) => {
        //未生成pdf的html页面高度
        let leftHeight = canvas.height;
        // console.log("canvas 宽度", canvas.width);
        // console.log("canvas 高度", leftHeight);
        let position = 0;
        let pageData = canvas.toDataURL("image/jpeg", 1.0);
        let index = 0,
          canvas1 = document.createElement("canvas"),
          height;
        function createImpl(canvas) {
          if (leftHeight > 0) {
            index++;
            _this.pageNumber++;
            let checkCount = 0;
            if (leftHeight > _this.a4HeightRef) {
              let i = position + _this.a4HeightRef;
              for (i; i >= position; i--) {
                let isWrite = true;
                for (let j = 0; j < canvas.width; j++) {
                  let c = canvas.getContext("2d").getImageData(j, i, 1, 1).data;
                  // 利用像素点颜色判断截断的时机
                  if (c[0] != 0xff || c[1] != 0xff || c[2] != 0xff) {
                    isWrite = false;
                    break;
                  }
                }
                if (isWrite) {
                  // 该像素行是纯白
                  checkCount++;
                  if (checkCount >= 2) {
                    break;
                  }
                } else {
                  // 该像素行不是纯白
                  checkCount = 0;
                }
              }
              height =
                Math.round(i - position) ||
                Math.min(leftHeight, _this.a4HeightRef);
              if (height <= 0) {
                height = _this.a4HeightRef;
              }
            } else {
              height = leftHeight;
            }
            // console.log(index, "截图高:", height, "偏移 pos", position);
            canvas1.width = canvas.width;
            canvas1.height = height;
            let ctx = canvas1.getContext("2d");
            ctx.drawImage(
              canvas,
              0,
              position,
              canvas.width,
              height,
              0,
              0,
              canvas.width,
              height
            );
            let y = 10; // 如果是第一页, 上边距 设为0,避免下移导致下边文字被截一半, 如果用在其他场景,酌情进行调整该值,或者设置为0则万事大吉!
            if (position != 0 || _this.yPos > 0) {
              // y = 10; // 同上调整,建议结合文字的间距调整值
              pdf.addPage();
            }
            pdf.addImage(
              canvas1.toDataURL("image/jpeg", 1.0),
              "JPEG",
              0,
              y,
              _this.a4Width,
              (_this.a4Width / canvas1.width) * height
            );
            pdf.setPage(_this.pageNumber);
            pdf.text(
              "Page " + _this.pageNumber,
              _this.a4Width / 2 - 20,
              _this.a4Height - 5
            );
            leftHeight -= height;
            position += height;
            if (leftHeight > 0) {
              if (leftHeight < _this.a4HeightRef && index > 24) {
                // 取倒数第二页的y轴坐标值,以便多个canvas拼接的时候不会出现页面填不满的情况
                _this.yPos += position;
                _this.pageTotal = Math.floor(
                  (_this.canvasHeight - _this.yPos) / _this.a4HeightRef
                );
                // 下一个 canvas 高度
                _this.canvasCurH =
                  _this.pageTotal <= 25
                    ? _this.canvasHeight - _this.yPos
                    : 25 * _this.a4HeightRef;
                // console.log("继续生成新的canvas画布!!", _this.yPos, _this.pageTotal);
                // 如果超出了25页,新建一个canvas画布绘制剩下的元素,直到绘制完所有的元素
                _this.downloadPDF(pdf);
              } else {
                setTimeout(createImpl, 100, canvas);
              }
            } else {
              pdf.save("pdf" + Date.now() + ".pdf");
              setTimeout(() => {
                alert("下载完成");
              }, 1000);
            }
          }
        }
        //当内容未超过pdf一页显示的范围,无需分页
        if (leftHeight < _this.a4HeightRef) {
          let y = 0;
          if (_this.yPos > 0) {
            y = 10;
            pdf.addPage();
          }
          pdf.addImage(
            pageData,
            "JPEG",
            0,
            y,
            _this.a4Width,
            (_this.a4Width / canvas.width) * leftHeight
          );
          pdf.save("pdf" + Date.now() + ".pdf");
          setTimeout(() => {
            alert("下载完成");
          }, 1000);
        } else {
          try {
            setTimeout(createImpl, 100, canvas);
          } catch (err) {
            alert("下载失败");
          }
        }
      })
      .catch((e) => {
        console.log(e);
        alert("下载失败");
      });
  }
}

欢迎留言交流~