前段时间接到公司一个需求,需要将用户选择的不定数量的文本内容(富文本格式,包含图片),生成自动分页,且每页都有相同页眉、页尾的 PDF 。考虑到如果是用后端语言实现排版,需要实现一套基于富文本格式的动态分页的逻辑,比较麻烦。于是想到利用 web 技术来排版,并通过 puppeteer 生成 PDF 。
实现的过程中遇到了一些坑点,在这里记录一下。
写在前面
puppeteer 是 google 官方维护的 headless Chrome 工具。可以理解为用 JavaScript 去操作 Chrome 完成一些任务(爬虫、截图等)的工具,且可以不打开 Chrome 的图形化界面。有关如何搭建环境、使用puppeteer ,掘金上已有很多文章谈及,这里不展开说。本文只讲如何生成自动分页的 PDF 。
如何实现分页
单纯分页其实很容易实现,在操作 puppeteer 的时候,传入每页的大小尺寸参数, puppeteer 在生成 PDF 截取内容时候就会按照设定的分页尺寸,将内容自动分页。效果类似于使用 Chrome 时按 ctrl+P 另存为 PDF 。
生成每页大小为 A4 的 PDF ,代码如下:
const browser = await puppeteer.launch({
headless: true,
});
const page = await browser.newPage();
await page.goto(url);
const pdfBuffer = await page.pdf({
format: 'A4',
scale: 1,
margin: {
top: '0',
bottom: '0',
left: '0',
right: '0',
},
landscape: false,
displayHeaderFooter: false,
});
await browser.close();
对掘金首页执行生成 PDF 的效果如图:
实现每页固定页头和页尾
1. 使用 fixed 布局
分页很容易实现,难点在于如何让每一页呈现相同的页头和页尾。我首先尝试了用 fixed 属性,确实可以实现每页固定出现页头和页尾的效果。但是由于使用的是 fixed 布局,出现了内容被页头遮挡的情况。还是以掘金首页为例:
如图,红框位置就被 fixed 布局的页头所遮挡。
2. table 布局
通过不断的 Google 和调试,最后发现 table 布局可以比较好地实现需求。实现上就是把页头放在 thead,页尾放在 tfoot 。代码如下:
<table class="table-container">
<!-- 每页固定头部 -->
<thead class="table-header">
<tr class="table-row">
<th class="table-row-item">
<div class="page-header-wrapper">
<header class="page-header">
<div class="left">
<div class="logo-wrapper">
<img class="logo" src="@/assets/images/logo.svg" />
<div class="user-name">页头</div>
</div>
</div>
</header>
</div>
</th>
</tr>
</thead>
<!-- 包裹段落容器 -->
<tbody class="table-body">
<tr class="table-row">
<td class="table-row-item">
<div class="container">
<!-- 此处放置页面内容 -->
</div>
</td>
</tr>
</tbody>
<!-- 每页固定尾部 -->
<tfoot class="table-footer">
<tr class="table-row">
<td class="table-row-item">
<div class="page-footer">页尾</div>
</td>
</tr>
</tfoot>
</table>
实现效果如下:
3. 使用 pdf-lib 生成页尾
实现了每页固定的页头和页尾,又出现了新问题。由于内容是动态的,最后一页的内容是不一定到底部的,使用上述的实现方法会出现最后一页样式不一致的问题。如图:
调试了很久也没有办法在 web 端解决这个问题,于是转换思路,每页只留下页尾的空白位置,生成PDF后再用工具画上页尾。 具体是使用 pdf-lib 这个 node.js 的库。代码如下:
const pdfDoc = await PDFDocument.load(pdfBuffer);
pdfDoc.registerFontkit(fontkit);
const customFont = await pdfDoc.embedFont(SimSun);
const pages = pdfDoc.getPages();
const firstPage = pages[0];
const { width: pageWidth } = firstPage.getSize();
pages.forEach((page, index) => {
const text = motto[this.getRandom(motto.length, 0)];
page.drawText(text, {
x: 41,
y: 23,
size: 11,
font: customFont,
color: rgb(0.302, 0.302, 0.302),
});
page.drawText(`第${index + 1}页/共${pages.length}页`, {
x: pageWidth - 100,
y: 23,
size: 11,
font: customFont,
color: rgb(0.302, 0.302, 0.302),
});
page.drawRectangle({
x: 41,
y: 45,
width: pageWidth - 82,
height: 0.6,
borderColor: rgb(0.941, 0.941, 0.941),
borderWidth: 0.6,
});
});
const editedPdfBuffer = await pdfDoc.save();
PS:pdf-lib 如果需要使用特定中文字体,会把字体打包到 PDF 文件中,导致文件大小激增。可以先用字体裁剪工具裁剪字体,只裁剪出会用到的少数字符。
实现效果如图:
这样就完美实现需求啦!
其他
1. 防止特定内容分页
有些内容可能不希望被自动分页,可以用css属性 page-break-inside:avoid;
控制。
2. 垂直方向的 margin 属性导致内容
垂直方向 margin 属性有时候可能会导致内容错位,因为 Chrome 自动分页的时候没有办法帮你自动分割 margin 。
于是使用了一点小技巧,使用空的占位 div 来代替 margin 。
<!-- vue组件 -->
<div class="place-holder">
<div
class="place-holder-item"
style="width: 100%; height: 1px;"
v-for="(item, index) in Array(height)"
:key="index"
></div>
</div>
<!-- 使用 -->
<placeholder :height="30" />
代码地址
最后附上 demo 代码地址,对你有帮助的话麻烦star一下。如果文章有说得不对的地方,烦请指正。
Demo GitHub地址
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!