pdf生成处理

# html2canvas/jspdf

# 使用

# 思路步骤

  1. 获取DOM
  2. 将DOM转换为canvas
  3. 获取canvas的宽度、高度(稍微大一点,预览)
  4. 将pdf的宽高设置为canvas的宽高(不适用A4纸大小)
  5. 将canvas转为图片
  6. 实例化jspdf,将内容图片放在pdf中(因为内容宽高和pdf宽高一样,就只需要一页,也防止内容截断问题)

# 加入图片调试

// const target = $(domId); target[0]
const target = document.querySelector(`${domId}`);
const opts = {
  // useCORS: true,
  // allowTaint: true,
  // dpi: 96, // option from 192 to 96
  scale: 2, // window.devicePixelRatio
  width: target.clientWidth,
  height: target.clientHeight,
  // logging: true,
};
return html2Canvas(target, opts).then(canvasN => {
  const pageData = canvasN.toDataURL('image/jpeg', 1.0);
  if (type === 1) {
    // 下载为图片
    const objectUrl = pageData.replace('image/jpeg', 'image/octet-stream');
    const a = document.createElement('a');
    document.body.appendChild(a);
    a.setAttribute('style', 'display:none');
    a.setAttribute('href', objectUrl);
    a.setAttribute('download', `${fileName}.jpeg`);
    a.click();
    URL.revokeObjectURL(objectUrl);
    return;
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 添加页眉页脚

const SIZE = [595.28,841.89];  //a4宽高
let node = document.getElementById("pdf_content");
let nodeH = node.clientHeight;
let nodeW = node.clientWidth;
let pageH = nodeW / SIZE[0] * SIZE[1];
let modules = node.childNodes;
let pageFooterH = 50;  //50为页尾的高度
this.addCover(node.childNodes[0],pageH);  //添加封面
this.addPageFooter(node.childNodes[1],1,0);  //添加页尾
this.addPageHeader(node.childNodes[2]);  //添加页头
for(let i = 0,len = modules.length;i < len;i++){
  let item = modules[i];
  //div距离body的高度是pageH的倍数x,但是加上自身高度之后是pageH的倍数x+1
  let beforeH = item.offsetTop + pageFooterH;
  let afterH = beforeH + item.clientHeight + pageFooterH;  
  let currentPage = parseInt(beforeH/pageH);
  if(currentPage != parseInt(afterH/pageH)){
    //上一个元素底部距离body的高度
    let lastItemAftarH = modules[i-1].offsetTop + modules[i-1].clientHeight; 
    let diff = pageH - lastItemAftarH%pageH - pageFooterH;  
    this.addPageFooter(item,currentPage+1,diff); //加页尾
    this.addPageHeader(item); //加页头
  }
  if(i == len-1){
    let diff = pageH - afterH%pageH + 20;  //50为页尾的高度
    this.addPageFooter(item,currentPage+1,diff,true);//加页尾
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在合理分页的基础上,我们可以添加封面,页头,页尾。

添加封面

addCover = (node,pageH) => {
  let cover = document.createElement("div");
  cover.className = "c-page-cover";
  cover.style.height = (pageH-50)+"px";
  cover.innerHTML = `<img src="./img/logo.png" />
<table>
<tbody>
<tr><td>pdf name</td></tr>   
<tr><td>pdf报告生成时间</td></tr>                                
</tbody>
</table>`;
  node.parentNode.insertBefore(cover,node);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

添加页头

addPageHeader = (item) => {
  let pageHeader = document.createElement("div");
  pageHeader.className = "c-page-head";
  pageHeader.innerHTML = "页头内容";
  item.parentNode.insertBefore(pageHeader,item);
}
1
2
3
4
5
6

添加页尾

addPageFooter = (item,currentPage,diff,isLastest) => {
  let pageFooter = document.createElement("div");
  pageFooter.className = "c-page-foot";
  pageFooter.innerHTML = "第 "+ currentPage +" 页 ";
  isLastest?item.parentNode.insertBefore(pageFooter,null):item.parentNode.insertBefore(pageFooter,item);
  pageFooter.style.marginTop = diff+"px";
  pageFooter.style.marginBottom = "10px";
}
1
2
3
4
5
6
7
8

# 碰到的问题

# 生产pdf文件非常大

确保转换后的base64是正确的编码;

const pageData = canvasN.toDataURL('image/jpeg', 1.0);// 将canvas转为base64图片
1

# 生产centos的pdf及img图片字体丢失

更新html2canvas最新版本后, 最后还原旧版本;

jspdf:1.5.3

# Supplied Data is not a valid base64-String jsPDF

可能是导出的内容特别大,可以折叠标签,一步步检查是否内存溢出;

# 内容截断问题

处理方式一: 不做分页;【常用方案】

如果不是有特别的需求,比如打印或者分页,还是建议能在一页显示pdf,这样就避免了文字被截断的风险。不做分页的处理;

px是像素单位,pt是印刷单位,两者之间的关系和转化

html2canvas(content, {
  allowTaint: true,
  scale: 2 // 提升画面质量,但是会增加文件大小
}).then(function (canvas) {
  // 得到canvas画布的单位是px 像素单位
  var contentWidth = canvas.width
  var contentHeight = canvas.height

  console.log('contentWidth', contentWidth)
  console.log('contentHeight', contentHeight)
  // 将canvas转为base64图片
  var pageData = canvas.toDataURL('image/jpeg', 1.0)

  // 设置pdf的尺寸,pdf要使用pt单位 已知 1pt/1px = 0.75   pt = (px/scale)* 0.75
  // 2为上面的scale 缩放了2倍
  var pdfX = (contentWidth + 10) / 2 * 0.75
  var pdfY = (contentHeight + 500) / 2 * 0.75 // 500为底部留白

  // 设置内容图片的尺寸,img是pt单位 
  var imgX = pdfX;
  var imgY = (contentHeight / 2 * 0.75); //内容图片这里不需要留白的距离

  // 初始化jspdf 第一个参数方向:默认''时为纵向,第二个参数设置pdf内容图片使用的长度单位为pt,第三个参数为PDF的大小,单位是pt
  var PDF = new jsPDF('', 'pt', [pdfX, pdfY])

  // 将内容图片添加到pdf中,因为内容宽高和pdf宽高一样,就只需要一页,位置就是 0,0
  PDF.addImage(pageData, 'jpeg', 0, 0, imgX, imgY)
  PDF.save('xxx.pdf')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

处理方式二: 设置间距

在接缝处插入空白 div,高度给了 10。然后循环处理,直到所有接缝处,都是插入的空白 div 未知。空白 div 给个特殊的 class 用来标识。

具体代码如下:

// 页面高度 A4纸高度: 1122px
// 相对高度 referenceTop 其实就是容器距离顶部的高度,
// 自身高度 item.offsetTop
// 最终计算自己的位置就是item.offsetTop-referenceTop 到 item.offsetTop+item.offsetHeight-referenceTop
// 这个高度,就是自己的位置,只要不在 1122的倍数之间就可以。
const calcDomPosition = index => {
  let falg = false;
  const referenceTop = $('form')[0].offsetTop;
  forEach($('form').children(), item => {
    const height1 = item.offsetTop - referenceTop;
    const height2 = item.offsetTop + item.offsetHeight - referenceTop;
    if (height1 < 1122 * index && height2 > 1122 * index) {
      if (!$(item).hasClass('placeholder')) {
        $('<div style="height: 10px" class="placeholder">&nbsp;</div>')
          .insertBefore(global.$(item));
        // 插入数据之后,再调用一次,保证在边界处是插入的占位元素
        falg = true;
      }
    }
  });
  return falg;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

处理方式三: 分页及控制间距;【最后方案】

实现思路

  • 每页按固定宽高布局
  • 以不被截断的最小固定高度布局
  • 不依赖布局,动态计算每页应放置的dom元素
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';

const A4_WIDTH = 592.28;
const A4_HEIGHT = 841.89;

jsPDF.API.output2 = function (outputType = 'save', filename = 'document.pdf') {
  let result = null;
  switch (outputType) {
    case 'file':
      result = new File([this.output('blob')], filename, {
        type: 'application/pdf',
        lastModified: Date.now(),
      });
      break;
    case 'save':
      result = this.save(filename);
      break;
    default:
      result = this.output(outputType);
  }
  return result;
};

jsPDF.API.addBlank = function (x, y, width, height) {
  this.setFillColor(255, 255, 255);
  this.rect(x, y, Math.ceil(width), Math.ceil(height), 'F');
};

jsPDF.API.toCanvas = async function (element, width) {
  const canvas = await html2canvas(element);
  const canvasWidth = canvas.width;
  const canvasHeight = canvas.height;
  const height = (width / canvasWidth) * canvasHeight;
  const canvasData = canvas.toDataURL('image/jpeg', 1.0);
  return { width, height, data: canvasData };
};

jsPDF.API.addHeader = async function (x, width, header) {
  if (!(header instanceof HTMLElement)) return;
  let __header;
  if (this.__header) {
    __header = this.__header;
  } else {
    __header = await this.toCanvas(header, width);
    this.__header = __header;
  }
  const { height, data } = __header;
  this.addImage(data, 'JPEG', x, 0, width, height);
};

jsPDF.API.addFooter = async function (x, width, footer) {
  if (!(footer instanceof HTMLElement)) return;
  let __footer;
  if (this.__footer) {
    __footer = this.__footer;
  } else {
    __footer = await this.toCanvas(footer, width);
    this.__footer = __footer;
  }
  const { height, data } = __footer;
  this.addImage(data, 'JPEG', x, A4_HEIGHT - height, width, height);
};

/**
 * 生成pdf(处理多页pdf截断问题)
 * @param {Object} param
 * @param {HTMLElement} param.element - 需要转换的dom根节点
 * @param {number} [param.contentWidth=550] - 一页pdf的内容宽度,0-592.28
 * @param {number} [param.contentHeight=800] - 一页pdf的内容高度,0-841.89
 * @param {string} [param.outputType='save'] - 生成pdf的数据类型,添加了'file'类型,其他支持的类型见http://raw.githack.com/MrRio/jsPDF/master/docs/jsPDF.html#output
 * @param {string} [param.filename='document.pdf'] - pdf文件名
 * @param {number} param.x - pdf页内容距页面左边的高度,默认居中显示,为(A4宽度 - contentWidth) / 2)
 * @param {number} param.y - pdf页内容距页面上边的高度,默认居中显示,为(A4高度 - contentHeight) / 2)
 * @param {HTMLElement} param.header - 页眉dom元素
 * @param {HTMLElement} param.footer - 页脚dom元素
 * @param {boolean} [param.headerOnlyFirst=true] - 是否只在第一页添加页眉
 * @param {boolean} [param.footerOnlyLast=true] - 是否只在最后一页添加页脚
 * @param {string} [param.mode='adaptive'] - 生成pdf的模式,支持'adaptive'、'fixed','adaptive'需给dom添加标识,'fixed'需固定布局。
 * @param {string} [param.itemName='item'] - 给dom添加元素标识的名字,'adaptive'模式需在dom中设置
 * @param {string} [param.groupName='group'] - 给dom添加组标识的名字,'adaptive'模式需在dom中设置
 * @returns {Promise} 根据outputType返回不同的数据类型
 */
async function outputPdf ({
  element, contentWidth = 550, contentHeight = 800,
  outputType = 'save', filename = 'document.pdf', x, y,
  header, footer, headerOnlyFirst = true, footerOnlyLast = true,
  mode = 'adaptive', itemName = 'item', groupName = 'group',
}) {
  if (!(element instanceof HTMLElement)) {
    throw new Error('The root element must be HTMLElement.');
  }

  const pdf = new jsPDF({
    unit: 'pt',
    format: 'a4',
    orientation: 'p',
  });
  const { width, height, data } = await pdf.toCanvas(element, contentWidth);
  const baseX = x == null ? (A4_WIDTH - contentWidth) / 2 : x;
  const baseY = y == null ? (A4_HEIGHT - contentHeight) / 2 : y;
  async function addHeader (isFirst) {
    if (isFirst || !headerOnlyFirst) {
      await pdf.addHeader(baseX, contentWidth, header);
    }
  }
  async function addFooter (isLast) {
    if (isLast || !footerOnlyLast) {
      await pdf.addFooter(baseX, contentWidth, footer);
    }
  }
  function addImage (_x, _y) {
    pdf.addImage(data, 'JPEG', _x, _y, width, height);
  }

  const params = {
    element, contentWidth, contentHeight, itemName, groupName,
    pdf, baseX, baseY, width, height, addImage, addHeader, addFooter,
  };
  switch (mode) {
    case 'adaptive':
      await outputWithAdaptive(params);
      break;
    case 'fixed':
    default:
      await outputWithFixedSize(params);
  }
  return pdf.output2(outputType, filename);
}

async function outputWithFixedSize ({
  pdf, baseX, baseY, height, addImage, addHeader, addFooter, contentHeight,
}) {
  const pageNum = Math.ceil(height / contentHeight); // 总页数
  const arr = Array.from({ length: pageNum }).map((_, i) => i);
  for await (const i of arr) {
    addImage(baseX, baseY - i * contentHeight);
    const isFirst = i === 0;
    const isLast = i === arr.length - 1;
    if (!isFirst) {
      // 用空白遮挡顶部需要隐藏的部分
      pdf.addBlank(0, 0, A4_WIDTH, baseY);
    }
    if (!isLast) {
      // 用空白遮挡底部需要隐藏的部分
      pdf.addBlank(0, baseY + contentHeight, A4_WIDTH, A4_HEIGHT - (baseY + contentHeight));
    }
    await addHeader(isFirst);
    await addFooter(isLast);
    if (!isLast) {
      pdf.addPage();
    }
  }
}

async function outputWithAdaptive ({
  element, contentWidth, itemName, groupName,
  pdf, baseX, baseY, addImage, addHeader, addFooter, contentHeight,
}) {
  // 从根节点遍历dom,计算出每页应放置的内容高度以保证不被截断
  const splitElement = () => {
    const res = [];
    let pos = 0;
    const elementWidth = element.offsetWidth;
    function updatePos (height) {
      if (pos + height <= contentHeight) {
        pos += height;
        return;
      }
      res.push(pos);
      pos = height;
    }
    function traversingNodes (nodes) {
      if (nodes.length === 0) return;
      nodes.forEach(one => {
        if (one.nodeType !== 1) return;
        const { [itemName]: item, [groupName]: group } = one.dataset;
                    if (item != null) {
        const { offsetHeight } = one;
        // dom高度转换成生成pdf的实际高度
        // 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
        updatePos(contentWidth / elementWidth * offsetHeight);
      } else if (group != null) {
        traversingNodes(one.childNodes);
      }
    });
  }
  traversingNodes(element.childNodes);
  res.push(pos);
  return res;
};

const elements = splitElement();
let accumulationHeight = 0;
let currentPage = 0;
for await (const elementHeight of elements) {
  addImage(baseX, baseY - accumulationHeight);
  accumulationHeight += elementHeight;
  const isFirst = currentPage === 0;
  const isLast = currentPage === elements.length - 1;
  if (!isFirst) {
    pdf.addBlank(0, 0, A4_WIDTH, baseY);
  }
  if (!isLast) {
    pdf.addBlank(0, baseY + elementHeight, A4_WIDTH, A4_HEIGHT - (baseY + elementHeight));
  }
  await addHeader(isFirst);
  await addFooter(isLast);
  if (!isLast) {
    pdf.addPage();
  }
  currentPage++;
}
}

export default outputPdf;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216

# 最后的方法封装

// import html2Canvas from 'html2canvas';
import { Html2Canvas as html2Canvas } from '@autorun/components';
import JsPDF from 'jspdf';

/**
 * @param {*} domId 操作的dom
 * @param {*} fileName 导出的文件名
 * @param {*} type 0 图片; 1 pdf a4大小分页导出  2 pdf 不A4分页;
 * @param {*} isA4Page
 */
export function htmlToPdf(domId, fileName, type) {
  // const target = $(domId); target[0]
  const target = document.querySelector(`${domId}`);
  const opts = {
    // useCORS: true,
    // allowTaint: true,
    // dpi: 96, // option from 192 to 96
    scale: 2, // window.devicePixelRatio
    width: target.clientWidth,
    height: target.clientHeight,
    // logging: true,
  };
  return html2Canvas(target, opts).then(canvasN => {
    const pageData = canvasN.toDataURL('image/jpeg', 1.0);

    if (type === 0) {
      const objectUrl = pageData.replace('image/jpeg', 'image/octet-stream');
      const a = document.createElement('a');
      document.body.appendChild(a);
      a.setAttribute('style', 'display:none');
      a.setAttribute('href', objectUrl);
      a.setAttribute('download', `${fileName}.jpeg`);
      a.click();
      URL.revokeObjectURL(objectUrl);
      return;
    }
    const contentWidth = canvasN.width;
    const contentHeight = canvasN.height;
    let pdf;
    if (type == 1) {
      const pageHeight = (contentWidth / 592.28) * 841.89;
      let leftHeight = contentHeight;
      let position = 0; // 页面偏移
      const imgWidth = 595.28; // a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
      const imgHeight = (592.28 / contentWidth) * contentHeight;
      pdf = new JsPDF('', 'pt', 'a4');
      // const pdf = new JsPDF('p', 'pt', 'a4', true);
      const leftPos = 16;
      const topPos = 15;

      // 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
      // 当内容未超过pdf一页显示的范围,无需分页
      if (leftHeight < pageHeight) {
        pdf.addImage(pageData, 'JPEG', leftPos, topPos, imgWidth, imgHeight);
      } else {
        while (leftHeight > 0) {
          pdf.addImage(pageData, 'JPEG', leftPos, position + topPos, imgWidth, imgHeight);
          leftHeight -= pageHeight;
          position -= 841.89;
          if (leftHeight > 0) {
            pdf.addPage();
          }
        }
      }
    } else {
      const pdfWidth = ((contentWidth + 10) / 2) * 0.75;
      const pdfHeight = ((contentHeight + 200) / 2) * 0.75; // 500为底部留白
      const imgWidth = pdfWidth;
      const imgHeight = (contentHeight / 2) * 0.75; // 内容图片这里不需要留白的距离
      pdf = new JsPDF('', 'pt', [pdfWidth, pdfHeight]);
      pdf.addImage(pageData, 'jpeg', 0, 0, imgWidth, imgHeight);
    }
    return pdf.save(`${fileName}.pdf`);
  });
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

# 其他导出方案

# freemarker

通过后端配合,把页面做离线的静态文件,再通过压缩成zip,导出到客户本地;详见[freemarker内部文章]

# 逻辑

FreeMarker数据模型的map的key只可以是String类型; 可以通过json中转;

  • 通过ftl把java中传递过来的数据;
  • 传递给js中的vue的data数据;【重要】
  • 再通过vue来渲染界面;

# 数据通信

  • 处理字符串;${exceptionSite}, //特殊字符不能用json处理;``包括起来;
  • 处理对象串;Object.freeze(JSON.parse('${report}') || {})
  • 处理数组串;Object.freeze(JSON.parse('${componentReport}') || [])

# 参考链接

  • http://raw.githack.com/MrRio/jsPDF/master/docs
  • https://github.com/linwalker/render-html-to-pdf
  • https://www.jianshu.com/p/257513ab0717
上次更新: 2022/04/15, 05:41:29
×