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

CSS columns

CSS中支持使用columns进行分栏显示。

但是wkhtmltopdf并不支持,详见官方issue#1872, issue#2266

wkhtmltopdf + javascript

wkhtmltopdf支持在页面加载后执行javascript (–run-script),类似于body的onload。

实现思路:

  1. 先把所有内容装在一个半页的div中,假设叫org_div

  2. 根据需要创建与每页等高的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;
    }

    如果是第一页,创建的高度可能有所不同,因为第一页会有标题等其他元素。

  3. 依次从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);
    }
  4. 若某子节点在左栏或右栏放不下事,对子节点进行切割,切割到高度满足为止

    切割算法思路:

    • 依次将最末尾的子节点移动到另外一个新创建的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

Reference