最近做了一个在线生成PDF,适时预览并可在线打印的系统。使用了pdf-lib.js(https://pdf-lib.js.org),作为前端创建PDF的框架,虽然很方便,但遇到一个棘手的问题:如何解决中文显示。
PDF文件不同于web文件(HTML+CSS),它的字体是嵌入文件的,而由于安全性和PDF文件本身机制的问题,并不能读取本地字体文件。而国际PDF阅读器规范中的 “Standard 14 Fonts”(14 种标准字体)中并没有中文字体,这就导致了在不嵌入中文字体的情况下,出现中文就会无法渲染。
Standard 14 Fonts
内置的 14 种标准字体 这 14 种字体全部属于 拉丁字母(英文) 系列,分为四大类:
1.无衬线 (Sans Serif)字体: Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique
2.衬线 (Serif/Roman) 字体:Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic
3.等宽 (Monospace)字体: Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique
4.符号 (Symbol) :Symbol, ZapfDingbats
PDF 的设计初衷是 “所见即所得” (WYSIWYG)。它的目标是:不管你把这个文件发给谁,在手机上、打印机上还是 20 年后的电脑上,看到的排版必须一模一样。
封闭性:PDF 不信任阅读者的系统。它认为:“万一对方电脑里没有微软雅黑,我的排版不就乱了吗?”
机制:为了保证绝对一致,
pdf-lib必须执行 “字体嵌入 (Embedding)”。它需要把字体文件的每一个二进制数据“缝进” PDF 文件里。
面临的难点:
这个系统需要兼容正体中文(繁体)和简体中文,而这种字体体量大得惊人。兼容性最好的Adobe全字符体——Super OTC 体积高达100M+,师出同门的Google Noto也差不多这个体量,如放到Web项目,简直是灾难,浏览器瞬间会被卡死。
于是只能放弃使用这种大字符集的字体,选择性地支持正体或简体。
但是单独的简体字体,体积也很大,思源Source Han Serif,单独一种字重的字体,也高达20M+,Noto也要10M-20M。如果要加载这么大的字体,配置一般的电脑尤其是内存小的电脑,会频繁出现假死的情况。
于是,只能想办法减少字体的体积。
通过万能的Gemini,终于有了解决办法:字体子集化。
说白了,就是从原字体中提取一部分字符,只要能覆盖自己常用的字符就行了。
方案:
1. 用集成工具做子集化。这是最常用的方式,工具例如,Fontmin(https://ecomfe.github.io/fontmin/)。
这种方式就是要准备“字体白名单”,也就是你要把那些你需要的字符一一列出来,让它把你需要的字符从庞大字库中“抠”出来。
可以到GitHub上下载常用中文字符3500字等整理好的字符集,导入Fontmin中。也可以自己整理用到的文字再加进去。
2.用 Python 做字体子集化。
#安装fonttools库
pip install fonttools brotli
#命令执行子集化
pyftsubset original.ttf --text-file=chars.txt --output-file=sub.woff2 --flavor=woff2
'''
其中
original.ttf:原始字体,如思源宋体
--text-file:字符白名单文件
--output-file输出字体
'''
#不指定字符白名单,只按照某个标准来子集化(如变成GB2312标准字符集)
pyftsubset NotoSerifSC-Medium.ttf --unicodes="U+0020-007E,U+00A0-00FF,U+2000-206F,U+3000-303F,U+4E00-9FA5" --output-file=label_gb2312.ttf --layout-features='*'3.在线动态字体子集化
① Node.js 生态方案:Fontmin,基于 fonteditor-core,支持 TTF、EOT、WOFF、WOFF2 之间的转换。它支持插件机制,可以方便地动态剪裁。示例代码:
const Fontmin = require('fontmin');
const fontmin = new Fontmin()
.src('fonts/SourceHanSansCN-Regular.ttf') // 原始 15MB 字体
.use(Fontmin.glyph({
text: '张三的PDF发票金额: ¥100.00' // 前端传过来的动态文本
}))
.use(Fontmin.ttf2woff2()) // 转换为更小的 woff2 格式
.dest('dist/fonts');
fontmin.run((err, files) => {
// 这里的 files[0].contents 就是剪裁后只有几 KB 的字体二进制流
});font-compress-service框架:
下载地址:https://github.com/xituimao/font-compress-service
可以通过URL参数直接传入远程字体地址和需保留文字及字符集。
② Python / 命令行生态(性能与压缩率天花板)
FontTools (pyftsubset),Google、Adobe 等大厂都在使用的世界级行业标准开源字体处理库。
其自带的命令行工具 pyftsubset 性能极其强悍,对 OpenType 特性(如连字、变体)保留最完整,且生成的 woff2 压缩率最高。
动态调用方式:在后端(如 Go 或 Java)通过进程调用(Exec)其命令行:
pyftsubset original.ttf --text="需要保留的文字" --flavor=woff2 --output-file=subset.woff2③ WebAssembly 方案(全前端免后端黑科技)
如果绝对不想部署后端,但又想实现动态子集化,目前可以通过 WebAssembly(Wasm)将底层的 C/C++ 字体剪裁库搬到前端浏览器运行。
harfbuzzjs / hb-subset
简介:将大名鼎鼎的开源字形排版引擎
HarfBuzz编译为 Wasm 的版本。特点:可以让浏览器直接具备在本地“秒级”剪裁 20MB 字体的能力。前端收集到文本后,直接在浏览器内存里通过 Wasm 剪裁原始字体,再喂给
pdf-lib。缺点:首次加载页面时,用户需要下载 Wasm 模块和完整的原始大字体文件(缓存在浏览器中),初次加载较慢,适合离线工况或管理后台。
④云厂商现成方案
Google Fonts:Google 率先实现了自动子集化。当网页请求字体时,它的智能网络会自动分析你的 HTML 文本,或通过 &text=… 参数动态返回切片字体。
<!-- 必须对传递的字符进行 URL 编码(UrlEncode) -->
<link href="https://fonts.googleapis.com/css2?display=swap&family=Noto+Sans+SC:wght@400&text=%E6%9C%A0%E6%96%B0%E5%8F%91%E7%A5%A8" rel="stylesheet">
function applyDynamicGoogleFont(elementId, fontName) {
const element = document.getElementById(elementId);
if (!element) return;
// 1. 获取元素内的文本,去重并排序
const textContent = element.innerText;
const uniqueChars = Array.from(new Set(textContent)).sort().join('');
// 2. 对字符串进行 URL 编码
const encodedText = encodeURIComponent(uniqueChars);
// 3. 拼接 Google Fonts API 链接
// 注意:建议加上 &display=swap 防止字体未加载出来时页面白屏
const fontUrl = `https://fonts.googleapis.com/css2?display=swap&family=${fontName.replace(/ /g, '+')}&text=${encodedText}&display=swap`;
// 4. 检查是否已有该字体的 link 标签,没有则创建,有则更新
let link = document.getElementById(`font-link-${elementId}`);
if (!link) {
link = document.createElement('link');
link.id = `font-link-${elementId}`;
link.rel = 'stylesheet';
document.head.appendChild(link);
}
link.href = fontUrl;
// 5. 在 CSS 中应用该字体
element.style.fontFamily = `"${fontName}", sans-serif`;
}
// 【使用场景】:当你的业务组件渲染完成后调用
// 假设 <div id="my-title">欢迎来到 AI 生产力工具库</div>
applyDynamicGoogleFont('my-title', 'Noto Sans SC');
PDF相关开发小知识:
Chrome的PDF渲染核心核心是PDFium(Google 和福昕合作开发的C / C++开源项目),Firefox使用的则是自家的PDF.js(纯 JavaScript 实现的 PDF 渲染器),开发环境下它们有一些使用上的差异:
在 PDF 官方规范中,
FitH是针对“宽度自适应”的,但在 Firefox 的pdf.js源代码里,他们对参数的映射是这样的:
view=FitH: 对应的内部指令是 FitH(垂直滚动模式下对齐宽度)。
zoom=page-fit: 对应的内部指令是 page-fit(强制将整个页面缩放到当前容器可视窗口内)。Firefox中可以通过“about:config”——“pdfjs.defaultZoomValue”,将值设置为page-fit/page-width,可实现自适应窗口展示,不过这种方式只适用于用浏览器直接打开pdf文件的情况下适用(这种情况是用 Firefox 原生 PDF.js 打开的)。