wkhtmltopdf实现分栏
最近项目中遇到的一个需求,由wkhtmltopdf转换生成的PDF需要分栏显示。就像下面这样。

CSS columns
CSS中支持使用columns进行分栏显示。
但是wkhtmltopdf并不支持,详见官方issue#1872, issue#2266。
wkhtmltopdf + javascript
wkhtmltopdf支持在页面加载后执行javascript (–run-script),类似于body的onload。
实现思路:
- 先把所有内容装在一个半页的div中,假设叫org_div 
- 根据需要创建与每页等高的div, 里面有两个子div - 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- function create_page(is_first) { 
 var page = document.createElement('div');
 var height = 0;
 if (is_first) {
 height = _A4_HEIGHT - _QUESTION_START_Y - _WK_BOT_MARGIN;
 } else {
 height = _PAGE_HEIGHT;
 }
 page.style.height = height + 'px';
 page.style.margin = '38px 46px 0px 48px';
 var left_part = document.createElement('div');
 left_part.style.width = '349px';
 left_part.style.height = height + 'px';
 left_part.style.borderRight = '1px dashed #808080';
 left_part.style.display = 'inline-block';
 left_part.style.verticalAlign = 'top';
 page.appendChild(left_part);
 var right_part = document.createElement('div');
 right_part.style.width = '334px';
 right_part.style.height = height + 'px';
 right_part.style.display = 'inline-block';
 right_part.style.verticalAlign = 'top';
 right_part.style.marginLeft = '15px';
 page.appendChild(right_part);
 return page;
 }- 如果是第一页,创建的高度可能有所不同,因为第一页会有标题等其他元素。 
- 依次从org_div中取出子节点,将其放入创建好的新div中,若左边放不下了,就放右边;若右边放不下了,就再create_page(),直到org_div被取空 - 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- function magic() { 
 var org_card = document.getElementById('org-analysis-card');
 var page = create_page(true);
 org_card.insertAdjacentElement('beforebegin', page);
 var remain = page.offsetHeight;
 var container = page.firstChild;
 var next_container = function () {
 if (container == page.firstChild) {
 container = page.lastChild;
 } else {
 page = create_page(false);
 org_card.insertAdjacentElement('beforebegin', page);
 container = page.firstChild;
 }
 }
 while (org_card.childNodes.length > 0) {
 var block = org_card.firstChild;
 if (block.offsetHeight <= (remain - 2)) {
 org_card.removeChild(block);
 container.appendChild(block);
 remain = remain - block.offsetHeight - 2 - _QUESTION_SPACING;
 } else {
 var new_div = cut_element(block, remain - 2);
 if (new_div) {
 org_card.removeChild(block);
 org_card.insertBefore(new_div, org_card.firstChild);
 container.appendChild(block);
 }
 next_container();
 remain = page.offsetHeight;
 }
 }
 document.body.removeChild(org_card);
 }
- 若某子节点在左栏或右栏放不下事,对子节点进行切割,切割到高度满足为止 - 切割算法思路: - 依次将最末尾的子节点移动到另外一个新创建的div中,直到offsetHeight满足条件为止。
- 若子节点还需要切割,递归此逻辑。
- 若子节点已没有子孙,若它是文本,则二分切割到满足为止。
 
其他替代方案
- 若使用的是Qt, 可使用QWebEnginePage的printToPdf,由于QWebEngine使用的是chromium,因此直接使用columns css就行。就像这样: - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16- QWebEnginePage page; 
 QEventLoop loop;
 loop.connect(&page, &QWebEnginePage::loadFinished, [&page, &loop, pdfFileName]() {
 page.printToPdf([&loop, &pdfFileName] (QByteArray ba) {
 QFile f(pdfFileName);
 if (f.open(QIODevice::WriteOnly)) {
 f.write(ba);
 f.close();
 } else {
 L2LOG_ERROR(QString("Error opening file for writing %1. %2").arg(pdfFileName, f.errorString()));
 }
 loop.exit();
 });
 });
 page.load(QUrl::fromLocalFile(htmlFileName));
 loop.exec();
- 使用其他第三方工具,例如ChromeHtmlToPdf, cef-pdf