如何在 gpdf 中嵌入日文字体?
将 TTF 字节传给 gpdf.WithFont 即可。自动子集化嵌入、无需 CGO,用 Go 生成日文 PDF 的最短路径。
这个问题的另一种表达
如何用 gpdf 生成带日文(或任意 CJK)文本的 PDF —— 不用重复 AddUTF8Font 那一套,不引入 CGO,也不在每个 PDF 里塞 5 MB 的字体文件?
速答
读取 TTF 字节;把 gpdf.WithFont("NotoSansJP", fontBytes) 传给 NewDocument;可选地设为默认字体。三行设置,gpdf 会自动把实际用到的字形子集化,最终 PDF 里只携带你用到的那部分字形。
完整示例
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
font, err := os.ReadFile("NotoSansJP-Regular.ttf")
if err != nil {
log.Fatal(err)
}
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
gpdf.WithFont("NotoSansJP", font),
gpdf.WithDefaultFont("NotoSansJP", 12),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("こんにちは、世界。", template.FontSize(24), template.Bold())
c.Text("日本語 PDF、これだけ。")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("hello.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
从 Google Fonts 下载 NotoSansJP-Regular.ttf,放到 main.go 旁边,运行 go run main.go,就能得到一页带日文的 PDF。
这三行背后发生了什么
两件事在后台默默完成,都不需要你插手。
子集化嵌入。 Noto Sans JP 含约 17,000 个字形,Regular 单体约 5 MB。如果整字体直接嵌入,一张只印了四行日文的收据也会是 5 MB 起步。gpdf 会扫描已渲染的文本,算出用到的字形 ID,仅把这个子集写进 PDF。一张短小的发票通常只携带 20–40 KB 的字体数据,而不是 5 MB。
gofpdf 也能做子集化,但它要求 AddUTF8Font 接收文件路径和 UTF-8 标志,并在游标移动过程中完成加载 —— 文档中途切换字体就显得别扭。gpdf 在构造阶段一次性注册,之后每次 c.Text 只按字体族名引用,不再需要逐次准备。
不使用 CGO。 这一点比听起来重要。很多语言生态里的字体处理要走 FreeType 或 HarfBuzz,结果就是一个 C 依赖、构建缓存失效方式改变、Docker 镜像多出层次、从 macOS 交叉编译到 linux/arm64 也开始需要额外配置。gpdf 用纯 Go 解析 TrueType 表,go build 仍然产出静态二进制。distroless 镜像里放一个 Go 二进制加一个 TTF 文件就够了。
粗体与斜体
日文 Noto 家族每个字重都是单独的文件。要用粗体,把 Bold 的 TTF 以 -Bold 后缀单独注册:
reg, _ := os.ReadFile("NotoSansJP-Regular.ttf")
bold, _ := os.ReadFile("NotoSansJP-Bold.ttf")
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", reg),
gpdf.WithFont("NotoSansJP-Bold", bold),
gpdf.WithDefaultFont("NotoSansJP", 12),
)
然后 template.Bold() 就会选中 -Bold 变体。-Italic 与 -BoldItalic 同理。没注册变体时会回退到合成字重 —— 屏幕上能读,但字形并不地道。正式发票请注册真实字重。
在同一文档中混用中日韩
注册多少字体族都行,gpdf 彼此独立管理。用 template.FontFamily(...) 按文本切换:
jp, _ := os.ReadFile("NotoSansJP-Regular.ttf")
sc, _ := os.ReadFile("NotoSansSC-Regular.ttf")
kr, _ := os.ReadFile("NotoSansKR-Regular.ttf")
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", jp),
gpdf.WithFont("NotoSansSC", sc),
gpdf.WithFont("NotoSansKR", kr),
gpdf.WithDefaultFont("NotoSansJP", 12),
)
page.AutoRow(func(r *template.RowBuilder) {
r.Col(4, func(c *template.ColBuilder) {
c.Text("日本語")
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("中文", template.FontFamily("NotoSansSC"))
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("한국어", template.FontFamily("NotoSansKR"))
})
})
受 Han Unification 影响,日文与简体中文共享 Unicode 码位,但字形并不相同。同一码位在不同字体下会渲染成不同字形 —— 字体选择不是美学问题,而是正确性问题。给跨境业务做发票、运单时,需要同时注册两个字体族。
常见陷阱:豆腐字
如果写了日文却没注册 WithFont,gpdf 会回退到 Base-14 标准 PDF 字体 —— 它们不包含 CJK 字形,结果字符会显示成一串空矩形,俗称「豆腐字」:
□□□□□、□□。
看到这种输出,原因只有一个:没有注册 CJK 字体,或者用的字体族不含这些字形。修复方式也只有一个:加上 WithFont,再用 WithDefaultFont 设为默认,或在 c.Text 上显式传入 template.FontFamily。
延伸阅读
- gofpdf 已归档。如何迁移到 gpdf。 —— 从
pdf.AddUTF8Font迁移的完整映射 - Go PDF 库横评 2026 —— gpdf 与 gofpdf、gopdf、Maroto、unipdf 在 CJK 上的对比
- 字体指南 ——
WithFont的完整参考与变体命名规则
试试 gpdf
gpdf 是一个 Go PDF 生成库。MIT 协议、零外部依赖、原生支持 CJK。
go get github.com/gpdf-dev/gpdf