惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

腾讯CDC
N
Netflix TechBlog - Medium
aimingoo的专栏
aimingoo的专栏
P
Proofpoint News Feed
F
Fortinet All Blogs
大猫的无限游戏
大猫的无限游戏
I
InfoQ
V
V2EX
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
有赞技术团队
有赞技术团队
G
Google Developers Blog
L
LangChain Blog
博客园_首页
M
MIT News - Artificial intelligence
H
Hackread – Cybersecurity News, Data Breaches, AI and More
月光博客
月光博客
IT之家
IT之家
量子位
宝玉的分享
宝玉的分享
S
SegmentFault 最新的问题
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
雷峰网
雷峰网

张鑫旭-鑫空间-鑫生活

1分钟速度了解text-align match-parent声明 « 张鑫旭-鑫空间-鑫生活 光标的形状也能设置了,就是CSS caret-shape属性 « 张鑫旭-鑫空间-鑫生活 pretext与文字四面环绕效果的实现 « 张鑫旭-鑫空间-鑫生活 JSON.rawJSON方法的作用是什么? « 张鑫旭-鑫空间-鑫生活 如何使用CSS判断鼠标从哪个方向进入元素? « 张鑫旭-鑫空间-鑫生活 借助mediabunny纯JS实现视频水印、剪裁、合成等功能 « 张鑫旭-鑫空间-鑫生活 务必谨慎使用JS WeakRef弱引用 « 张鑫旭-鑫空间-鑫生活 CSS六边形头像的实现与蜂巢布局 « 张鑫旭-鑫空间-鑫生活 CSS corner-shape与背景底纹技术 « 张鑫旭-鑫空间-鑫生活 浅学WebTransport API:下一代Web双向通信技术 « 张鑫旭-鑫空间-鑫生活 HTML dialog元素新支持了closedBy属性 « 张鑫旭-鑫空间-鑫生活 HTML interestfor属性与悬停popover交互效果 « 张鑫旭-鑫空间-鑫生活 CSS3模拟window7炫酷界面效果展示 « 张鑫旭-鑫空间-鑫生活 图片旋转+剪裁js插件(兼容各浏览器) « 张鑫旭-鑫空间-鑫生活 纯js无图片无外链CSS图片幻灯片播放插件 « 张鑫旭-鑫空间-鑫生活 CSS border三角、圆角图形生成技术简介 « 张鑫旭-鑫空间-鑫生活 CSS “渐进增强”在web制作中常见应用举例 « 张鑫旭-鑫空间-鑫生活 让IE6/IE7/IE8浏览器支持CSS3属性 « 张鑫旭-鑫空间-鑫生活 jQuery – 高亮动态显示页面HTML代码插件 « 张鑫旭-鑫空间-鑫生活 翻译 – 逐渐消失的Flash网站 « 张鑫旭-鑫空间-鑫生活 CSS reset的重新审视 – 避免样式重置 « 张鑫旭-鑫空间-鑫生活 新浪微博插入话题后部分文字选中的js实现 « 张鑫旭-鑫空间-鑫生活 JavaScript实现http地址自动检测并添加URL链接 « 张鑫旭-鑫空间-鑫生活 CSS实现兼容性的渐变背景(gradient)效果 « 张鑫旭-鑫空间-鑫生活 CSS content内容生成技术以及应用 « 张鑫旭-鑫空间-鑫生活 CSS gradient渐变之webkit核心浏览器下的使用 « 张鑫旭-鑫空间-鑫生活 CSS渐变之CSS3 gradient在Firefox3.6下的使用 « 张鑫旭-鑫空间-鑫生活 区分IE8/IE7/IE6及其他浏览器-CSS “\9″hack « 张鑫旭-鑫空间-鑫生活 CSS实现跨浏览器兼容性的盒阴影效果 « 张鑫旭-鑫空间-鑫生活 CSS3&HTML5各浏览器支持情况一览表 « 张鑫旭-鑫空间-鑫生活
使用jsPDF导出PDF文件实践分享 « 张鑫旭-鑫空间-鑫生活
张鑫旭,zhangxinxu · 2023-06-04 · via 张鑫旭-鑫空间-鑫生活

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

(本篇完)