网页 html 生成A4大小分页的 pdf ,翻遍了整个互联网发现没有很系统的整理与分析,甚至对 jsPDF 的解析也没有几篇。遇到过几次,用的比较多,完成代码编写后特此整理分析,自我记录。

  1. 存在图片/组件/文字被分割的现象,即分页处理

  1. 包括页头、页脚、上下安全间隔的情况

  1. 富文本分页情况

通过 深度搜索优先遍历 ,从顶部遍历需要转换的 HTML 节点, 并将节点分为三种情况进行处理( 1. 普通节点。2. 需要进行分页处理并且内部可能包含也需要分页处理子节点的节点。3. 需要进行分页内部不包含需要分页处理的节点,即深度搜索的终点节点 ),通过 从高到低 遍历维护一个 分页数组pages ,该数组记录每一页的起始位置,如: pages[0] 对应 第一页起始位置 pages[1] 对应 第二页起始位置

图解如下:

通过深度遍历后得出每页起始位置的数组,遍历数组,通过 jspdf addImage 接口对 canvas 进行画面截取,由于 addImage 只能 固定位置的左上角起始点 ,不能进行 非常精确的上下定位截取 (下一节会详解addImage),会造成 截取多余的内容 (如上图 页面1 pages[1] 下方的内容会和 页面2 pages[1] 下方的内容会一样(除长度外),而 页面1 pages[1] 下方的内容是多余的(是属于 页面2 的内容))因此需要对页面不需要的内容 使用jspdf的addBlank进行空白遮挡处理。

jsPDF.addImage详解

官方文档链接 addImage - Documentation (artskydj.github.io)

通过深度优先遍历操作,可以从高到低去遍历需要进行跨页判断的元素,检测是否跨页,并记录分页点,从而避免跨页问题。

1. 普通节点

当遍历到普通节点,即不需要进行分页判断的节点时,只需要进行 2步操作

  1. 当前节点距离顶部的高度 - pages最后一位元素的值(即上一页的分界点)得出的差值是否 大于 页面的高度 , 如果大于,则证明当前节点已经跨页,进行操作 pages.push(pages[pages.length - 1] + 一页PDF的高度)

  1. 对子节点进行深度遍历

2. 需要进行跨页判断,且内部也含有 可能跨页/需要进行跨页判断 的节点

当元素进行到该类型的节点时, 需要进行 3步操作

  1. 需要进行与 普通节点第一步 相同的判断

  1. (检测当前节点距离顶部的距离 + 节点自身的高度) 是否大于 (pages 最后一位元素(即当前页 顶部位置) + 一页PDF的高度(当前指A4的高度))

如果条件为真,则证明该节点属于跨页元素, 距离页面顶部距离的值top 是分页点,往pages中 push top

  1. 且由于内部还存在需要进行跨页检测的节点,因此需要对子节点进行深度遍历

3. 需要进行跨页判断,但内部不含有可能跨页/需要进行跨页判断 的节点, 即深度终点

该节点只需要进行 内部含有可能跨页/需要进行跨页判断 的节点 的第一第二步操作, 由于内部不再含有,因此不需要遍历子节点,为搜索的叶子节点。

html2Canvas生成图片模糊导致导出的PDF也模糊的问题

通过 scale 参数, 对canvas进行等比放大,可以使canvas生成的图片更清晰。

// 将元素转化为canvas元素// 通过 放大 提高清晰度// width为内容宽度asyncfunctiontoCanvas(element, width) {
  // canvas元素const canvas = awaithtml2canvas(element, {
   // allowTaint: true, // 允许渲染跨域图片scale: window.devicePixelRatio * 3// 增加清晰度
  // 获取canavs转化后的宽度const canvasWidth = canvas.width;
  // 获取canvas转化后的高度const canvasHeight = canvas.height;
  // 高度转化为PDF的高度const height = (width / canvasWidth) * canvasHeight;
  // 转化成图片Dataconst canvasData = canvas.toDataURL('image/jpeg', 1.0);
  //console.log(canvasData)return { width, height, data: canvasData };
 

样例及代码

gitee仓库: output_pdf_demo: jsPDF + html2canvas , 网页HTML导出A4格式PDF 处理分页切割问题 (gitee.com)

npm install & npm run serve 即可运行

需要做的额外处理:

  1. 图片摆放的Y坐标由原来的-pages[i] 变成了 baseY + 页头元素高度 - pages[i]

  1. 中间实际内容部分与页眉/页脚之间的边距也需要进行遮白处理

  1. 内容的高度才为PDF页面的实际高度,判断分页的依据应该以内容高度为准

  1. 富文本文字的分页处理

import jsPDF from'jspdf';
import html2canvas from'html2canvas';
import { message } from'ant-design-vue';
constA4_WIDTH = 592.28;
constA4_HEIGHT = 841.89;
// 将元素转化为canvas元素// 通过 放大 提高清晰度// width为内容宽度asyncfunctiontoCanvas(element, width) {
  // canvas元素const canvas = awaithtml2canvas(element, {
   // allowTaint: true, // 允许渲染跨域图片scale: window.devicePixelRatio * 2// 增加清晰度
  // 获取canavs转化后的宽度const canvasWidth = canvas.width;
  // 获取canvas转化后的高度const canvasHeight = canvas.height;
  // 高度转化为PDF的高度const height = (width / canvasWidth) * canvasHeight;
  // 转化成图片Dataconst canvasData = canvas.toDataURL('image/jpeg', 1.0);
  //console.log(canvasData)return { width, height, data: canvasData };
 * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)
 * @param {Object} param
 * @param {HTMLElement} param.element - 需要转换的dom根节点
 * @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-592.28
 * @param {string} [param.filename='document.pdf'] - pdf文件名
 * @param {HTMLElement} param.header - 页眉dom元素
 * @param {HTMLElement} param.footer - 页脚dom元素
 */exportasyncfunctionoutputPDF({ element, contentWidth = 550,
  footer, header, filename = "测试A4分页.pdf" }) {
  if (!(element instanceofHTMLElement)) {
    return;
  // jsPDFs实例const pdf = newjsPDF({
    unit: 'pt',
    format: 'a4',
    orientation: 'p',
  // 一页的高度, 转换宽度为一页元素的宽度const { width, height, data } = awaittoCanvas(element, contentWidth);
  // 添加页脚asyncfunctionaddHeader(header, pdf, contentWidth) {
    const { height: headerHeight, data: headerData, width: hWidth } = awaittoCanvas(header, contentWidth);
    pdf.addImage(headerData, 'JPEG', 0, 0, contentWidth, headerHeight);
  // 添加页眉asyncfunctionaddFooter(pageNum, now, footer, pdf, contentWidth) {
    const newFooter = footer.cloneNode(true);
    newFooter.querySelector('.pdf-footer-page').innerText = now;
    newFooter.querySelector('.pdf-footer-page-count').innerText = pageNum;
    document.documentElement.append(newFooter);
    const { height: footerHeight, data: footerData, width: fWidth } = awaittoCanvas(newFooter, contentWidth);
    pdf.addImage(footerData, 'JPEG', 0, A4_HEIGHT - footerHeight, contentWidth, footerHeight)
  // 添加functionaddImage(_x, _y, pdf, data, width, height) {
    pdf.addImage(data, 'JPEG', _x, _y, width, height);
  // 增加空白遮挡functionaddBlank(x, y, width, height, pdf) {
    pdf.setFillColor(255, 255, 255);
    pdf.rect(x, y, Math.ceil(width), Math.ceil(height), 'F');
  // 页脚元素 经过转换后在PDF页面的高度const { height: tfooterHeight } = awaittoCanvas(footer, contentWidth)
  // 页眉元素 经过转换后在PDF的高度const { height: theaderHeight } = awaittoCanvas(header, contentWidth);
  // 距离PDF左边的距离,/ 2 表示居中 const baseX = (A4_WIDTH - contentWidth) / 2;        // 预留空间给左边// 距离PDF 页眉和页脚的间距, 留白留空const baseY = 15;
  // 出去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度const originalPageHeight = (A4_HEIGHT - tfooterHeight - theaderHeight - 2 * baseY);
  // 元素在网页页面的宽度const elementWidth = element.offsetWidth;
  // PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度  转化为 距离Canvas顶部的高度const rate = contentWidth / elementWidth
  // 每一页的分页坐标, PDF高度, 初始值为根元素距离顶部的距离const pages = [rate * getElementTop(element)];
  // 获取元素距离网页顶部的距离// 通过遍历offsetParant获取距离顶端元素的高度值functiongetElementTop(element) {
    let actualTop = element.offsetTop;
    let current = element.offsetParent;
    while (current && current !== null) {
      actualTop += current.offsetTop;
      current = current.offsetParent;
    return actualTop;
  // 遍历正常的元素节点functiontraversingNodes(nodes) {
    for (let i = 0; i < nodes.length; ++i) {
      const one = nodes[i];
      // 需要判断跨页且内部存在跨页的元素const isDivideInside = one.classList && one.classList.contains('divide-inside');
      // 图片元素不需要继续深入,作为深度终点const isIMG = one.tagName === 'IMG';
      // table的每一行元素也是深度终点const isTableCol = one.classList && ((one.classList.contains('ant-table-row')));
      // 特殊的富文本元素const isEditor = one.classList && (one.classList.contains('editor'));
      // 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断let { offsetHeight } = one;
      // 计算出最终高度let offsetTop = getElementTop(one);
      // dom转换后距离顶部的高度// 转换成canvas高度const top = rate * (offsetTop)
      // 对于需要进行分页且内部存在需要分页(即不属于深度终点)的元素进行处理if (isDivideInside) {
        // 执行位置更新操作updatePos(rate * offsetHeight, top, one);
        // 执行深度遍历操作traversingNodes(one.childNodes);
      // 对于深度终点元素进行处理elseif (isTableCol || isIMG) {
        // dom高度转换成生成pdf的实际高度// 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-boxupdatePos(rate * offsetHeight, top, one);
      elseif (isEditor) {
        // 执行位置更新操作updatePos(rate * offsetHeight, top, one);
        // 遍历富文本节点traversingEditor(one.childNodes)
      // 对于普通元素,则判断是否高度超过分页值,并且深入else {
        // 执行位置更新操作updateNomalElPos(top)
        // 遍历子节点traversingNodes(one.childNodes);
    return;
  // 对于富文本元素,观察所得段落之间都是以<p> / <img> 元素相隔,因此不需要进行深度遍历 (仅针对个人遇到的情况)functiontraversingEditor(nodes) {
    // 遍历子节点for (let i = 0; i < nodes.length; ++i) {
      const one = nodes[i];
      let { offsetHeight } = one;
      let offsetTop = getElementTop(one);
      const top = contentWidth / elementWidth * (offsetTop)
      updatePos(contentWidth / elementWidth * offsetHeight, top, one);
  // 普通元素更新位置的方法// 普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度 - 上一个分页点的高度 大于 正常一页的高度,则需要载入分页点 functionupdateNomalElPos(top) {
    if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) {
      pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight);
  // 可能跨页元素位置更新的方法// 需要考虑分页元素,则需要考虑两种情况// 1. 普通达顶情况,如上// 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点functionupdatePos(eheight, top) {
    // 如果高度已经超过当前页,则证明可以分页了if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) >= originalPageHeight) {
      pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight);
    // 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置elseif ((top + eheight - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) && (top != (pages.length > 0 ? pages[pages.length - 1] : 0))) {
      pages.push(top);
  // 深度遍历节点的方法traversingNodes(element.childNodes);
  // 可能会存在遍历到底部元素为深度节点,可能存在最后一页位置未截取到的情况if (pages[pages.length - 1] + originalPageHeight < height) {
    pages.push(pages[pages.length - 1] + originalPageHeight);
  //console.log({ pages, contentWidth, width,height })// 根据分页位置 开始分页for (let i = 0; i < pages.length; ++i) {
    message.success(`共${pages.length}页, 生成第${i + 1}页`)
    // 根据分页位置新增图片addImage(baseX, baseY + theaderHeight - pages[i], pdf, data, width, height);
    // 将 内容 与 页眉之间留空留白的部分进行遮白处理addBlank(0, theaderHeight, A4_WIDTH, baseY, pdf);
    // 将 内容 与 页脚之间留空留白的部分进行遮白处理addBlank(0, A4_HEIGHT - baseY - tfooterHeight, A4_WIDTH, baseY, pdf);
    // 对于除最后一页外,对 内容 的多余部分进行遮白处理if (i < pages.length - 1) {
      // 获取当前页面需要的内容部分高度const imageHeight = pages[i + 1] - pages[i];
      // 对多余的内容部分进行遮白addBlank(0, baseY + imageHeight + theaderHeight, A4_WIDTH, A4_HEIGHT - (imageHeight), pdf);
    // 添加页眉awaitaddHeader(header, pdf, A4_WIDTH)
    // 添加页脚awaitaddFooter(pages.length, i + 1, footer, pdf, A4_WIDTH);
    // 若不是最后一页,则分页if (i !== pages.length - 1) {
      // 增加分页
      pdf.addPage();
  return pdf.save(filename)
 

参考文档及博文

转载自:https://juejin.cn/post/7138370283739545613

jsPDF - Documentation (artskydj.github.io)

配置型 | HTML2CANVAS 中文文档 (allenchinese.github.io)

【原创】jspdf+html2canvas生成多页pdf防截断处理 - 简书 (jianshu.com)

首先说说遇到了什么问题。首先有这么一个需求。需要前端根据后端传过来数据,动态的生成图片。图片中的文案、背景图片、用户头像全部都是通过后端的接口获取。但是使用 html2canvas 生成的canvas有些图片成功的在canvas里生成了。但是有些图片无论如何都显示不出来。 在项目里面操作了半天未果,google了半天未果。此时有些许绝望。突然想到了,为什么不去它的 官网 看看呢。于是乎我在官网上看到了下面的内容。 Limitations<br> All the images that the script uses need to reside under the same 声明:此篇文章并不是最优解决办法,下载pdf这一步主要参考睡衣大佬提供的思路和代码,个人在此基础上进行细微修改处理段落文字截断,勉强实现不截断文字效果,但也有诸多限制和不足。 jsPDF-html2canvasjsPDFhtml2canvas结合使用,可将html内容转换为PDF文件。 html2PDF函数将自动将目标dom宽度调整为PDF大小。 因此,无需担心溢出部分。 如果内容高度超过1 pdf,它将自动将其分隔到另一个pdf页面。 npm i jspdf-html2canvas import html2PDF from 'jspdf-html2canvas' ; html2PDF ( node , options ) ; 由于此插件是umd模块,因此您也可以将CDN与/dist/jspdf-html2canvas.min.js结合使用,只需记住在此插件之前同时包jspdfhtml2canvas cdn即可。 < script src = "https://unpkg.com/jspdf@latest/dist/jspdf.um 文中内容是在 Vue 项目中使用 html2canvasjspdf 插件实现将页面的内容生成 PDF,并记录在使用过程中遇到的一些问题和解决方案。文中将会贴出很多参考文章,如果各位有需要,可以前往原文章查阅。 html2canvas + jspdf介绍使用方法分页分割问题html2canvas生成图片只有一半其他参考文章 1、html2canvas 该插件允许我们直接在浏览器上对网页或其部分进行“截图”操作,但是屏幕截图基于 DOM,这一点很关键。这就会导致截图的结果,很可能不是自己想要的。. 前言生成PDF有不少种方案,如今只讨论用jspdfhtml2canvas生成多页PDF时,相信用过的人也遇到若是文字或图片卡在分页位置处被无情裁断的问题。再次以前先简单介绍下咱们用于解决问题的属性。phpColumns 属性介绍columns:100px 3; //每列多少宽度 分多少列也就是:column-count:3 // 分多少列column-width: 100px //每列多少宽度c... 由于项目需要前端导出文档,一开始是准备导出word,后来发现导出word比较麻烦,并且前端基本都是由echarts和dataTable组成的,不好导出,最后改为导出为pdf,采用的是jsPDFhtml2canvas 一、先引入html2canvas和jsPD <script type="text/javascript" src="/resource/plugins/jsPDF/jspdf.debug.js"></script> <script type="text/ 基于PDFTK在Node.js中进行PDF操作! 分割,加入,修剪,阅读,提取,煮沸,捣碎,粘在炖锅中。 该项目不再得到积极维护,我们无法响应问题。 考虑其他替代方法,例如 始终欢迎漏洞修复。 var scissors = require ( 'scissors' ) ; // Use and chain any of these commands... var pdf = scissors ( 'in.pdf' ) . pages ( 4 , 5 , 6 , 1 , 12 ) // select or reorder individual pages . range ( 1 , 10 ) // pages 1-10 . even ( ) // select even pages, . odd ( ) // or odd, . rotat 欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdow...