在浏览器里把 HTML 转成 PDF,最影响成品观感的就是字体:默认字体再好,也替代不了品牌字体和正式文档的排版需求。dompdf.js 的纯前端渲染方案让自定义字体变得简单直接——把字体文件通过 @font-face 声明进 HTML 模板,配合 font-family 指定字体族,生成的 PDF 就能完整还原网页上的字形效果,支持 TTF 与 OTF 格式,中文场景还有内置思源黑体兜底。不过字体加载涉及网络请求、加载时序、字体族回退等多个环节,任何一个环节处理不当,都会导致生成的 PDF 字体不对或字形缺失。本文从字体格式选择、@font-face 声明写法、加载时序控制、中文字体子集化到常见问题排查,完整梳理 dompdf.js 自定义字体的最佳实践,让开发者一次接入、长期省心,交付的文档与设计稿高度一致。
默认字体能覆盖大多数通用场景,但企业文档、品牌物料和对外交付的文件往往有严格的视觉规范:VI 手册指定的标题字体、合同里的专用字体、设计稿要求的衬线字体,用默认字体替代会明显降低专业感。dompdf.js 内置思源黑体可以保证中文不乱码,但英文和数字的字形、特殊符号的样式,仍然需要自定义字体才能与品牌保持一致,输出文件和网页端视觉完全统一。
自定义字体还能解决排版细节问题:不同字体的字符宽度、字距和行高度量不同,同样的文案用不同字体排出来,换行位置和页面长度都会变化。在生成 PDF 前把字体固定下来,意味着每次导出的文档版式可复现、可预期,合同、报表这类对版式稳定性要求高的场景尤其受益,避免同一份文档在不同机器上导出效果不一致。
对多语言场景,自定义字体更是刚需:阿拉伯语、希伯来语等文字需要对应的字体才能正确显示,生僻汉字、方言用字也需要扩展字库覆盖。通过字体声明把需要的字体族一次性配好,生成的 PDF 才能在不同语言环境下都保持正确,交付给客户或存档后不会出现缺字方块,多语言产品的导出质量才有保障。
dompdf.js 支持 TTF 与 OTF 两种常见字体格式,两者都是轮廓字体,字形质量没有本质差别,选择时主要看字体厂商的交付形式:大多数免费与商业字体同时提供 TTF 和 OTF,直接使用即可,不需要额外转换。TrueType 与 OpenType 的主要区别在于曲线描述方式,对最终渲染结果的影响很小,日常开发按字体源文件选用最省事。
需要注意字体文件的授权:嵌入 PDF 属于字体再分发,要确认字体许可允许嵌入,商用字体尤其要检查 EULA。另外,同一个字体族的常规、粗体、斜体通常是多个独立文件,需要分别声明,而不是只加载常规字重,否则 CSS 里的 font-weight: bold 会找不到对应文件,浏览器只能模拟加粗,PDF 里的效果也会打折扣。
单文件体积也是考虑因素:一个完整的中文字体动辄十几兆,而拉丁字体只有几十到几百 KB。如果字体只用于少量标题或英文字符,选择子集版本能显著加快加载;反过来,如果文档要覆盖大量生僻汉字,就应选择字库更全的版本,宁可多花一点加载时间,也要保证 PDF 里不缺字,交付质量优先。
把 @font-face 声明放进传给 addPage 的 HTML 里,dompdf.js 解析样式后会自动加载字体文件,font-family 按声明顺序回退。声明时务必让 font-family 名称与 CSS 使用处完全一致,并把 font-weight、font-style 与字体文件一一对应,一个文件对应一个字重,不要试图让一个文件承担多个字重,回退逻辑才会符合预期。
src 里的 url 支持相对路径、绝对 URL 和 data URL 三种形式。相对路径以调用方环境为基准解析,开发时要确认路径正确;字体文件较大时建议先用 fetch 预取再转 data URL 传入,或者直接把字体作为静态资源与页面同源部署,能避免跨域问题,也方便离线生成,模板自包含、可随时复现。
字体族声明顺序就是回退顺序:把自定义字体放最前,内置思源黑体放中间,通用字体族放最后。这样自定义字体覆盖不到的字符会自动落到思源黑体,保证中文永远有字形可渲染,英文和特殊符号用自定义字体,层级清晰、效果稳定,生成结果也不会出现意外缺字,多语言混排同样可靠。
import { DomPDF } from 'dompdf.js';
const html = `
<style>
@font-face {
font-family: 'BrandSans';
src: url('fonts/BrandSans-Regular.ttf') format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'BrandSans';
src: url('fonts/BrandSans-Bold.ttf') format('truetype');
font-weight: 700;
}
body { font-family: 'BrandSans', 'Source Han Sans SC', sans-serif; }
.title { font-size: 18pt; font-weight: 700; }
</style>
<h1 class="title">品牌字体标题示例</h1>
<p>正文优先使用 BrandSans,中文自动回退到内置思源黑体。</p>`;
await document.fonts.ready;
const pdf = new DomPDF();
pdf.addPage(html, { format: 'A4' });
pdf.save('custom-font-demo.pdf');
Web Font 是异步加载的,addPage 执行时字体可能还没下载完,此时生成的 PDF 就会使用回退字体,等字体加载完成后再次生成才能得到正确结果。因此把 addPage 放到字体就绪之后再执行是标准做法,document.fonts.ready 返回的 Promise 在所有字体加载完成后 resolve,用它做门控最可靠,可以彻底避免字体竞态问题。
如果字体是通过 CSS 的 @font-face 声明的,document.fonts.ready 会等待它们;如果使用 JavaScript 的 FontFace API 动态加载,则要先 await face.load() 并把字体加入 document.fonts,再等待 document.fonts.ready。两种方式都建议在生成 PDF 前统一 await,确保模板里用到的每个字体族都已可用,生成一次成功,不依赖运气。
加载失败也要有预案:字体文件 404、跨域被拦截或格式不支持时,字体不会加载,页面会静默使用回退字体。可以在控制台监听 loadingdone 与 loadingerror 事件定位问题,也可以在字体就绪后校验 document.fonts.check('12px BrandSans') 是否返回 true,返回 false 说明字体不可用,此时应提示用户或切换备用方案,避免交付缺字文档。
中文字体动辄数兆到十几兆,直接作为网页资源加载会拖慢页面,也会拖慢 PDF 生成的等待时间。如果文档只用少量中文,可以考虑子集化:用字体工具把用到的字符提取成小体积子集字体,文件可以从十几兆降到几百 KB,加载时间从秒级降到毫秒级,体验差异非常明显,适合批量导出、高频生成的场景。
子集化工具很多,日常开发常用 fonttools、pyftsubset 等命令行工具,按文本内容提取字符集,再生成子集 TTF/OTF 交给模板使用。注意子集只包含用到的字符,后续文案新增生僻字时必须重新生成子集,否则新字符会缺字;对内容不可预知的文档,建议保留完整字库或采用动态子集方案,安全优先。
另一个思路是合理利用内置思源黑体:大多数中文文档正文用思源黑体即可满足需求,只有标题、品牌文字等少量元素需要自定义字体。按需混用可以大幅减少字体文件体积,加载更快、生成更稳,文档整体观感依然专业,这是中文 PDF 生成里性价比最高的字体策略,也最适合团队长期维护。
Q: 生成的 PDF 里自定义字体没有生效,显示成了默认字体?A: 先检查 @font-face 的 font-family 名称与使用处是否完全一致(含大小写),再确认字体文件路径与格式参数正确,最后用 document.fonts.check 验证字体是否真正加载完成;四步排查可以覆盖绝大多数失效场景,多数问题出在前两步。
Q: 粗体斜体看起来是浏览器模拟的?A: 为每个字重分别声明 @font-face 文件,并保证 font-weight、font-style 属性与文件一致;模拟粗体在矢量 PDF 里会表现为笔画变形的效果,观感与真粗体有明显差距,正式文档建议全部使用真实字重文件,观感与印刷效果都更专业。
最佳实践小结:字体声明统一放在模板 style 顶部、名称与字重一一对应、生成前 await document.fonts.ready、中文正文优先用内置思源黑体、自定义字体按需子集化。按这五条执行,自定义字体基本不会再出问题,交付的 PDF 与设计稿高度一致,客户和同事都能放心使用,团队迭代也顺畅。
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。