IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    使用jsPDF导出PDF文件实践分享

    张 鑫旭发表于 2023-06-04 07:07:46
    love 0

    by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=10854 鑫空间-鑫生活
    本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可以联系授权。

    PDF封面图

    一、jsPDF项目简介

    最近遇到个需要将网页中特定内容转为PDF的需求,所以有机会试用了下jsPDF,也遇到了一些问题,这里分享给大家了。

    首先,项目地址:https://github.com/parallax/jsPDF

    目前2.6万的star数,可以说是Github上Top级别的项目了,也是Web导PDF的首选解决方案。

    Star数目示意

    官方的使用示意也很简单,构造,内容和保持。

    import { jsPDF } from "jspdf";
    
    // 默认是 a4 纸张尺寸,纵向,单位是mm
    const doc = new jsPDF();
    
    doc.text("Hello world!", 10, 10);
    doc.save("a4.pdf");

    好,接下来,就是把页面中的DOM内容作为PDF内容就可以了,理论上如此,但实际使用却令人深思。

    二、内置html()方法惊为天人

    jsPDF内置一个名为html()的方法,可以直接让HTML元素作为PDF的内容。

    var doc = new jsPDF();
    // element就是需要转变为pdf的DOM元素
    doc.html(element, {
       callback: function (doc) {
         doc.save();
       },
       x: 10,
       y: 10
    });

    结果我试了下,效果惊为天人。

    内置html方法的惊人效果

    研究一番,发现是要导入完整的中文字体,而一个中文字体,少说5-6M,在Web这种场景下就不太合适。

    于是改变策略,决定先用html2canvas将内容转成图片,再以图片的方式,一页一页地插入到PDF中,实现成本会低很多,不足就是文字无法选择。

    三、借助html2canvas

    html2canvas这个项目之前也有提过,接近3万 Star,也是Github上的顶级开源项目。

    项目地址:https://github.com/niklasvh/html2canvas

    极简使用示意

    这里快速演示下如何使用html2canvas和jsPDF生成PDF文件。

    假设页面上有个布局元素,id属性值是element,效果如下图所示:

    示意布局图

    则下面的这段代码就可以让这段布局内容变成PDF文件下载下来。

    <script src="./html2canvas.min.js"></script>
    <script src="./jspdf.umd.min.js"></script>
    <script>
    var pdf = new jspdf.jsPDF();
    html2canvas(target).then(function(canvas) {
        pdf.addImage(canvas.toDataURL('image/jpeg'), 10, 10);
        pdf.save('mybook.pdf');
    });
    </script>

    此时的PDF打开效果就是这样的:

    PDF效果示意

    眼见为实,您可以狠狠地点击这里:html2canvs与jspdf生成PDF简易demo

    然而,真实的项目开发要比demo页面麻烦的多。

    几乎大多数人一定会遇到的问题,那就是如果html2canvas生成的图片过长,该如何在PDF中分页。

    以及,图片跨域了,又当如何处理?

    四、图片跨域和分页的问题

    canvas与图片跨域的问题,我之前专门撰文讲解过,传送门地址:“解决canvas图片getImageData,toDataURL跨域问题”

    里面的方法虽多,根据我多年的实践,还是服务器设置Access-Control-Allow-Origin运行访问,前端fetch或XMLHttpRequest获取图片数据的策略最好用。

    以fetch举例,想要获得一个图片地址是 imgUrl 的图像数据,可以这么处理:

    fetch(imgUrl).then(res => res.blob()).then(blob => {
      var reader = new FileReader() ;
      reader.onload = function () {
        // this.result 就是图片的base64地址
      };
      reader.readAsDataURL(blob) ;
    })

    为何需要转成base64呢?因为根据我的实践,html2canvas内容中的图片地址,需要转换成base64地址,才能真正可用。

    分页的问题

    分页的问题可以使用代码搞定,设置好每一页PDF的高度,然后canvas的高度一页一页剪掉再分别添加即可。

    这里就有一些需要提前知道的,关于尺寸的知识。

    当我们构造一个 pdf 实例的时候,如果不设置任何参数,则其尺寸是 A4 纸的尺寸,210毫米×297毫米。

    根据我的实践,这个尺寸有些小了,生成的PDF内容模糊,阅读体验极为不佳。

    所以,有必要对PDF尺寸进行自定义,也就是设置得大一些,虽然大尺寸让PDF文件占据空间也大了,但现在是大屏高清时代,流量不值钱,此不足不值一提。

    具体而言,我是这么处理的。

    首先确定好页面中容器元素的宽度,假设是700px,则PDF的尺寸可以设置为2倍,也就是1400px,而竖版PDF的高宽比是根号二,也就是1.414,所以PDF的高度就是1400*1.414=1979.6像素。

    此时,再配合简单的数学计算,我们就可以将canvas图像分隔成一页一页的,分别塞在PDF中。

    具体代码参见下一节封装的代码示意。

    五、封装后的的方法

    为了方便遇到类似需求的同学可以快速完成对应的开发。

    我将上面的图像处理和分页封装成了可复用的方法,代码如下所示(支持jspdf和html2canvas src直连和 npm install 安装两种使用形式):

    // 导出 pdf 封装方法
    // by zhangxinxu(.com)
    // 访问 https://www.zhangxinxu.com/wordpress/?p=10854 了解更新信息
    export async function exportPdf (element, filename = '未命名', callback = () => {}) {
      if (!element) {
        callback();
        return;
      }
    
      // 尺寸的确定
      const originWidth = element.offsetWidth || 700;
    
      // 创建一个容器,用于克隆元素
      const container = document.createElement('div');
      // 16px是为了生成的PDF有安全边距
      container.style.cssText = `position:fixed;left: ${-2 * originWidth}px; top:0;padding:16px;width:${originWidth}px;box-sizing:content-box;`;
      // 插入到body中
      document.body.appendChild(container);
      // 克隆元素
      container.appendChild(element.cloneNode(true));
    
      // 依赖的库
      var jsPDF;
    
      if (typeof html2canvas == 'undefined') {
        html2canvas = await import('html2canvas').then(module => module.default);
      }
    
      if (typeof jspdf == 'undefined') {
        jsPDF = await import('jspdf').then(module => module.jsPDF);
      } else {
        jsPDF = jspdf.jsPDF;
      }
    
      // 为了保证显示质量,2倍PDF尺寸
      const scale = 2;
      const width = originWidth + 32;
    
      const PDF_WIDTH = width * scale;
      const PDF_HEIGHT = width * 1.414 * scale;
    
      // 渲染方法
      const render = function () {
        // 渲染为图片并下载
        html2canvas(container, {
          scale: scale
        }).then(function(canvas) {
          const contentWidth = canvas.width;
          const contentHeight = canvas.height;
    
          // 一页pdf显示html页面生成的canvas高度
          const pageHeight = contentWidth / PDF_WIDTH * PDF_HEIGHT;
    
          // canvas图像在画布上的尺寸
          const imgWidth = PDF_WIDTH;
          const imgHeight = PDF_WIDTH / contentWidth * contentHeight;
    
          let leftHeight = contentHeight;
          let position = 0;
    
          const doc = new jsPDF('p', 'px', [PDF_WIDTH, PDF_HEIGHT]);
    
          // 不足一页
          if (leftHeight < pageHeight) {
            doc.addImage(canvas, 'PNG', 0, 0, imgWidth, imgHeight);
          } else {
            // 多页
            while (leftHeight > 0) {
              doc.addImage(canvas, 'PNG', 0, position, imgWidth, imgHeight)
              leftHeight -= pageHeight;
              position -= PDF_HEIGHT;
              //避免添加空白页
              if (leftHeight > 0) {
                doc.addPage();
              }
            }
          }
    
          doc.save(filename + '.pdf');
    
          // 移除创建的元素
          container.remove();
    
          // 隐藏全局loading提示
          callback();
        });
      }
    
      // 图像地址替换成base64地址
      const eleImgs = container.querySelectorAll('img');
      const length = eleImgs.length;
      let start = 0;
      container.querySelectorAll('img').forEach(ele => {
        let src = ele.src;
    
        if (!src) {
          return;
        }
    
        // 事件处理,必须成功或失败
        ele.onload = function () {
          if (!/^http/.test(ele.src)) {
            start++;
            if (start == length) {
              render();
            }
          }
        };
    
        // 请求图片并转为base64地址
        fetch(src).then(res => res.blob()).then(blob => {
          var reader = new FileReader() ;
          reader.onload = function () {
            ele.src = this.result;
          };
          reader.readAsDataURL(blob) ;
        }).catch(() => {
          // 请求异常处理
          start++;
          if (start == length) {
            render();
          }
        });
      });
    }

    可以自动将容器元素中的图片Base64,同时内容分布在每一页的PDF上并下载。

    实践出真知

    为了验证封装的方法的效果,我特意做了个演示页面。

    //zxx: 演示页面采用的是直联调用

    您可以狠狠地点击这里:exportPdf封装方法与跨域图片PDF导出demo

    打击可以点击下图所示的“PDF生成”按钮,菊花转几圈之后,就可以看到PDF下载的提示了(看你浏览器设置,也可能是直接保持到本地)。

    导出PDF示意

    下图是生成的PDF文件的缩略图,可以看到图片和排版都是完全符合预期的。
    导出PDF示意图

    调用这块的JS代码参考:

    <script src="./html2canvas.min.js"></script>
    <script src="./jspdf.umd.min.js"></script>
    <script type="module">
    import { exportPdf } from './exportPdf.js';
    // 点击按钮执行PDF导出
    button.addEventListener('click', () => {
        const article = document.querySelector('article');
        // 显示loading
        button.loading = true;
        // 由于导出PDF是异步的,所以需要在导出完成后隐藏loading
        exportPdf(article, '最终章 极北大迷宫', () => {
            button.loading = false;
        });
    });
    </script>

    少了很多处理细节,是不是实现起来简单多了。

    六、跨行内联背景色的渲染问题

    实际使用中,还遇到了比较棘手的问题。

    就是内联元素,如果有背景色,且这个背景色换行了,则生成的PDF的这部分色块会覆盖部分内容,导致异常。

    例如页面渲染是这样的:
    原始布局效果示意

    PDF效果却是这样的:

    内联色块bug

    我查了下html2canvas的issues,有多个类似反馈,但是都没有进行处理。

    按照我对html2canvas底层实现方式的理解,这个问题确实不太好处理。

    但是,并不表示没有方法。

    可以将整块的内联元素,分隔成一个一个独立的内联元素,这样就可以正常渲染了。

    也就是将这个结构:

    <span class="bgcolor">CSS新世界</span>

    转换成这样子的(实际开发不能换行,这里是为了方便大家阅读刻意处理的):

    <span>
        <span class="bgcolor">C</span>
        <span class="bgcolor">S</span>
        <span class="bgcolor">S</span>
        <span class="bgcolor">新</span>
        <span class="bgcolor">世</span>
        <span class="bgcolor">届</span>
    </span>

    来看下最终的效果。

    您可以狠狠地点击这里:解决html2canvas span inline background色块问题demo

    点击下面这个按钮,JS会对原来的DOM结构进行处理(实际开发可以克隆该元素再处理,以避免DOM结构变化会带来潜在风险):

    修复按钮点击示意

    此时,生成的PDF效果就和原始布局样式效果保持一致了:

    生成效果保持一致了

    七、其他点点点

    本文示意页面所使用的JS文件都是script直连。

    实际开发,多是走前端框架。

    由于这两个JS都是体积比较大的JS,因此,可以使用动态加载的方式来实现。

    import('html2canvas').then(module => module.default).then(...)
    import('jspdf').then(module => module.jsPDF).then(...)

    盼星星盼月亮

    《CSS选择器世界 第2版》的签字版也已经可以购买啦,打开手机淘宝,扫下图左下角的码就可以了。

    书籍购买码

    //zxx: 包邮,另外,我这里还有三张极客时间14天畅学卡,先购买的优先赠送之。

    OK,其他就没什么好说的,希望本文的内容可以帮到遇到类似需求的小伙伴。

    ❤️ 🧡 💛 💚 💙 💜

    本文为原创文章,欢迎分享,勿全文转载,如果实在喜欢,可收藏,永不过期,且会及时更新知识点及修正错误,阅读体验也更好。
    本文地址:https://www.zhangxinxu.com/wordpress/?p=10854

    (本篇完)



沪ICP备19023445号-2号
友情链接