pdf-lib 实测:不截图、不转 HTML,用代码一行行写出 PDF——中文嵌入字体与 5 个实操坑

举报
岚枫 发表于 2026/10/24 07:13:24 2026/10/24
【摘要】 实测 pdf-lib 1.17.1:标准字体画英文、嵌入 Noto Sans SC 画中文、给已有 PDF 盖章、合并两份 PDF,全部跑通。顺带踩到 5 个坑:Helvetica 画中文直接抛错、不写 subset 整个字体会塞进 PDF、坐标原点在左下角等。

pdf-lib 实测:不截图、不转 HTML,用代码一行行写出 PDF——中文嵌入字体与 5 个实操坑

前端要"把页面转成 PDF",大多数人第一反应是 html2canvas 截图加 jsPDF 拼接:所见即所得,但出来的是一张大图,文字选不了、搜索不了,放大还发虚。另一条路是程序化生成:每个字、每条线都由代码放置,pdf-lib(Hopding/pdf-lib,约 8.7 千星,MIT)就是这条路上最常用的纯 JavaScript 库。

我在 Node 22 上把它完整跑了一遍:英文、中文、改文件、合文件都试了,输出全部真实截取,坑位集中在第九节。

一、它是什么,分工是什么

pdf-lib 是一个读写 PDF 的纯 JS 库:创建文档、画文字和图形、嵌入字体、给已有 PDF 盖章追加页、把多份 PDF 合并成一份。它不渲染 HTML,也不做页面布局引擎。和截图路线的分工很清楚:要"网页长这样"用截图,要"数据生成的规范文档"用它。发票、合同、报表、证书这类版式固定的输出,是它的主战场。

二、安装

npm install pdf-lib @pdf-lib/fontkit

实测版本 pdf-lib 1.17.1,Node 22.22.2,无原生依赖,Windows 直接装。中文字体要额外嵌入,@pdf-lib/fontkit 是官方搭档,负责解析字体文件。

三、标准字体画英文

PDF 内置 14 种标准字体,不嵌任何东西就能用,出来的文件极小:

const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');

const doc = await PDFDocument.create();
const font = await doc.embedFont(StandardFonts.Helvetica);
const page = doc.addPage([595, 842]); // A4,单位是 pt
page.drawText('Hello from pdf-lib!', { x: 50, y: 780, size: 24, font });
page.drawText('Origin is bottom-left, y grows UP.', { x: 50, y: 740, size: 12, font });
fs.writeFileSync('demo1.pdf', await doc.save());

产物 938 字节(PDF 的 /ID 字段带生成时间戳,重跑字节数有几个字节的浮动,下文各字节数均为首次实跑值)。几行代码出文件,没有任何外部依赖。

四、Helvetica 画中文:响亮地失败

顺手拿标准字体写中文,直接抛错:

Error: WinAnsi cannot encode "中" (0x4e2d)

换成另一个字,报错连码位一起给:WinAnsi cannot encode "思" (0x601d)。原因是标准 14 字体只带西文编码(WinAnsi),中文不在其列。这个设计其实友好:它当场报错,不会像某些库那样静默画出一片空白。中文必须走嵌入字体这条路。

五、嵌入中文字体

Windows 自带的中文字体(微软雅黑、宋体)是 .ttc 集合格式,fontkit 不认;本机演示下载了 Noto Sans SC(SIL OFL 1.1 授权,16.4 MB)当嵌入字体:

const fontkit = require('@pdf-lib/fontkit');
const doc = await PDFDocument.create();
doc.registerFontkit(fontkit);
const noto = await doc.embedFont(fs.readFileSync('NotoSansSC.otf'), { subset: true });

const page = doc.addPage([595, 842]);
page.drawText('中文宋体字样:pdf-lib 嵌入字体', { x: 50, y: 780, size: 20, font: noto, color: rgb(0.1, 0.1, 0.4) });
page.drawRectangle({ x: 50, y: 740, width: 300, height: 30, color: rgb(0.9, 0.95, 1) });
page.drawText('带底纹的一行', { x: 60, y: 748, size: 14, font: noto });

产物 60,279 字节,字体名记录为 NotoSansCJKsc-Regular。注意 registerFontkit 这一步不能省,不注册就直接 embedFont 会报找不到字体解析器。

六、给已有的 PDF 盖章、加页

修改走 PDFDocument.load,读完想怎么画怎么画:

const doc = await PDFDocument.load(fs.readFileSync('demo3.pdf'));
const page = doc.getPages()[0];
page.drawText('STAMP 2026-10-17', { x: 380, y: 60, size: 10, font: stamp, color: rgb(0.8, 0.1, 0.1) });
const newPage = doc.addPage([300, 200]);
newPage.drawText('appended page', { x: 20, y: 160, size: 12, font: stamp });
fs.writeFileSync('demo4.pdf', await doc.save());

原文档 1 页,盖章并追加一页后 2 页,60,897 字节。已有内容原样保留,这是它和"重新生成一整份"的关键区别。

七、合并两份 PDF

合并的标准动作是 copyPages:从源文档复制页面,再 addPage 到目标文档。

const merged = await PDFDocument.create();
const pagesA = await merged.copyPages(docA, docA.getPageIndices());
const pagesB = await merged.copyPages(docB, docB.getPageIndices());
[...pagesA, ...pagesB].forEach(p => merged.addPage(p));
fs.writeFileSync('merged.pdf', await merged.save());

一份英文页加一份中文页合并后 2 页,60,586 字节,两边的文字和字体都正常带过去。

八、页码居中和坐标体系

页码要居中,得先量文字宽度。标准字体有 widthOfTextAtSize:

const label = `${i} / ${total}`;
const w = font.widthOfTextAtSize(label, 10);
page.drawText(label, { x: (595 - w) / 2, y: 30, size: 10, font });

三页文档加完页码 1,520 字节。坐标这块单独说,因为它和网页思维相反:pdf-lib 的原点在页面左下角,y 轴向上长。我用另一个渲染引擎(PyMuPDF)做了对拍验证,把 pdf-lib 写的 PDF 渲染出来提取文字位置:

pdf-lib 里写的 yPyMuPDF 实测距顶 y_top
80029.1
421408.1
50779.1

按 HTML/CSS 的"距顶距离"去写,内容会整片飞到页面外。页面高度 842 的 A4,中间线是 421,从底部往上数。

九、坑位记录(5 个,全部实测)

  1. 标准字体画中文直接抛错。WinAnsi cannot encode "中" (0x4e2d),报错带码位。中文只有嵌入字体一条路。
  2. subset: true 不写,整个字体会塞进 PDF。实测:嵌入 16.4 MB 的 Noto Sans SC,只画了 5 个汉字,产物 13.9 MB。加上 subset: true 后同一份内容只要 60 KB。子集化剪掉没用到的字形,这是默认最该记住的一个参数。
  3. .ttc 集合字体在 pdf-lib 里走不通。实测(wqy-microhei.ttc,5,177,387 字节):fontkit.create 不报错但返回空壳(familyName 是 undefined);embedFont 带 subset: true 抛 TypeError: _this.font.createSubset is not a function;不带子集虽然嵌得进去,一 drawText 就抛 TypeError: this.font.layout is not a function。Windows 自带的微软雅黑、宋体正是 .ttc 集合格式,直接用必踩坑。换单文件 .ttf/.otf(演示用的 Noto Sans SC 就是),或者先把 .ttc 拆开。
  4. 坐标原点在左下,y 向上。表格已对拍验证。画表格行、画页脚时最容易搞反。
  5. 合并用 copyPages,不是把字节拼起来。两份 PDF 的二进制直接拼接出来的文件阅读器不认;copyPages 会把页面连同资源(字体、图片)一起搬进新文档。

十、适合谁,边界在哪

适合:用数据批量生成发票、合同、报表、证书;给已有 PDF 盖章、加页、合并;Node 服务端或浏览器里产出可打印、可检索文字的规范文档。

不适合:把网页原样转 PDF,该用无头浏览器截图路线;读取和提取已有 PDF 的文字内容,它是写入方不是解析方;复杂版面(多栏混排、自动分页)没有现成排版引擎,得自己算坐标。

一句话选型:页面是数据拼出来的,用 pdf-lib;页面是设计出来的,用截图。

参考与署名:本文实测项目为 Hopding/pdf-lib(MIT License,实测版本 1.17.1);文中全部代码与输出均为 Node 22 本机实跑记录,未使用项目宣传图;演示用中文字体 Noto Sans SC(SIL OFL 1.1)仅用于本机演示、未随文分发。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。