在使用 Flying Saucer(ITextRenderer) 将 HTML 模板转换为 PDF 的过程中,遇到了一个非常典型、也非常折磨人的问题:
字体文件明明加载成功,PDF 却始终不显示中文,或者字体完全不对。
🔍 一、问题现象
- 使用 HTML + CSS + Freemarker 模板生成 PDF:
- HTML 在浏览器中显示完全正常
- 背景图、图片、二维码都正常
但生成的 PDF 中:
- 中文不显示
- 或显示为方块
- 或全部回退成默认字体
CSS 看起来也没问题:
.title {
font-family: 仿宋;
}
.content {
font-family: 黑体;
}Java 里也注册了字体:
renderer.getFontResolver().addFont("simfang.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);但就是不生效。
🧠 二、第一次误区:以为「字体文件名 = 字体名」
很多人(包括我自己)一开始都会认为:
simfang.ttf → font-family: simfang或者:
font-family: 仿宋;
font-family: 仿宋_GB2312;这是完全错误的。
在 Flying Saucer / iText 中:
CSS 中的 font-family 并不是文件名,也不是系统显示名,而是字体内部的 PostScript Name。
🧪 三、关键转折:如何发现字体真正的名称?
1️⃣ 打印字体信息
iText 在加载字体时,其实已经解析了字体内部信息。
在调试过程中,可以发现类似输出(或通过代码打印),就在你 addFont 之后,加上👇👇👇:
BaseFont bf1 = BaseFont.createFont(
basePath + "simfang.ttf",
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED
);
System.out.println("FontName = " + bf1.getPostscriptFontName());
System.out.println("FamilyNames = " + Arrays.toString(bf1.getFamilyFontName()));
System.out.println("FullNames = " + Arrays.toString(bf1.getFullFontName()));FontName = FangSong
FamilyNames = [...]
FullNames = [...]这一步非常关键:
FontName 才是 Flying Saucer / iText 真正识别的名字
而不是你看到的中文名,也不是文件名。
2️⃣ 常见中文字体的真实名称
通过实际加载与验证,总结出几个最常见的映射关系:
字体文件 CSS 中应使用
| 字体文件 | CSS 中应使用 |
|---|---|
| simfang.ttf | FangSong |
| simkai.ttf | KaiTi |
| simhei.ttf | SimHei |
⚠️ 注意大小写,建议保持一致。
3️⃣ 验证方式(非常重要)
把 CSS 改成:
.title {
font-family: FangSong;
}
.award {
font-family: KaiTi;
}
.content {
font-family: SimHei;
}然后重新生成 PDF,如果字体生效,说明:
- 字体文件加载成功
- FontName 使用正确
- 编码和嵌入方式没问题
🛠️ 四、Java 端的正确字体注册方式
字体注册必须满足三个条件:
renderer.getFontResolver().addFont(
"simfang.ttf",
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED
);为什么必须这样?
- IDENTITY_H:支持 Unicode(中文)
- EMBEDDED:字体必须嵌入 PDF,否则中文丢失
- Flying Saucer 不支持系统字体 fallback
任何一个条件不满足,中文都可能直接消失。
⚠️ 五、CSS 层面必须遵守的规则
❌ 不要使用的方式
@font-face { ... } /* 不支持 */
font-family: 仿宋; /* 无效 */
font-family: 仿宋_GB2312; /* 无效 */✅ 正确写法
font-family: FangSong;
font-family: KaiTi;
font-family: SimHei;只写 PostScript 名称。
🔔 六、顺带踩到的一个布局大坑
在调整证书版式时,还发现 Flying Saucer 对 CSS 的支持非常有限:
❌ 不支持 / 不稳定的属性
- flex / justify-content
- margin-right: -xxx
- transform: translateX()
✅ 稳定可用的替代方案
- 整体位移:padding-left 或 margin-left
- 精准定位:position: relative + absolute
- 行内偏移:padding-left
例如:
.footer {
padding-left: 200px;
}比负 margin 稳定得多。
🙆 七、最终可复用的经验总结
- 只要记住下面这 7 点,Flying Saucer 的中文字体基本不会再踩坑:
- 字体必须是 .ttf
- 字体必须手动注册
- 使用 BaseFont.IDENTITY_H
- 使用 BaseFont.EMBEDDED
- 不要依赖系统字体
- CSS 中只写 PostScript FontName
- 不要相信浏览器里的显示结果