如何在 gpdf 中使用 Noto Sans JP?
用 gpdf.WithFont 注册 static 版 NotoSansJP-Regular.ttf,不要用 variable font。gpdf 会把 17,000 个字形子集化到每份 PDF 不到 40 KB。
把问题换个说法
你想在 gpdf 文档里渲染日语,字体选了 Noto Sans JP — Google 发布的 SIL OFL 免费无衬线字体,完整覆盖 JIS 字符集。你已经下好了 Google Fonts 的 zip 包。从这里开始,你想知道的三件事是:选哪个文件、注册哪几个字重、zip 里藏着的那一个坑是什么。
结论 (TL;DR)
解压 zip 之后,用 static/NotoSansJP-Regular.ttf — 不是根目录下的 variable font。把它传给 gpdf.WithFont("NotoSansJP", bytes) 并设为默认字体。gpdf 会从大约 17,000 个字形里只把你实际渲染过的那些子集化后嵌入 PDF — 一份普通发票最终携带的字体数据通常在 20–40 KB。
完整示例
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", 11),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("請求書", template.FontSize(28), template.Bold())
c.Text("Noto Sans JP、これで十分。")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("invoice.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
从 Google Fonts 下载 zip,解压后把 static/NotoSansJP-Regular.ttf 放到 main.go 旁边,运行 go run main.go — 就能得到一页的 PDF。
选 static TTF,不要选 variable font
在 Google Fonts 点 Get font → Download all,解压 zip,里面有两组看起来差不多、实际上完全不一样的文件:
- 根目录的
NotoSansJP-VariableFont_wght.ttf— variable 字体,把 100–900 所有字重合并在一个文件里,约 7 MB static/目录 — 9 个独立的 TTF,从NotoSansJP-Thin.ttf到NotoSansJP-Black.ttf,每个约 5 MB
选 static/ 里的那一个。
gpdf 的 TrueType 解析器是刻意收窄过功能的。它处理字形轮廓、复合字形、cmap、hmtx — 渲染固定字重文本所需的表。但它不处理让 variable font 真正可变的 fvar / gvar / HVAR 表。如果你把 VariableFont_wght.ttf 喂给它,要么解析器直接报错,要么更糟糕 — 它拿到默认实例的字形,然后默默无视你以为自己设好的字重轴。
文件大小也支持 static 方案。variable 字体把整个字重轴上的所有 outline 都塞在一个文件里,这是设计初衷 — 但如果你只用 Regular,就等于白付了另外 8 个字重的数据。static Regular 是 5 MB,variable 是 7 MB。子集化会把两者都削掉,但 static 是更干净的输入。
关键就是这 4 行
真正有意思的只有构造函数的选项:
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", font),
gpdf.WithDefaultFont("NotoSansJP", 11),
)
字体族名 ("NotoSansJP") 是任意的。gpdf 把它当查找键用 — 不是文件路径,也不是读自字体元数据的名字。如果你的项目里 "body"、"jp"、"Noto" 读起来更顺手就用那个。只要后面 template.FontFamily(...) 保持一致即可。
WithDefaultFont 能让你不用在每个 c.Text 里写 template.FontFamily("NotoSansJP")。省掉它,gpdf 会回落到 Helvetica — 而 Helvetica 不覆盖任何 CJK 码位,你会得到一篇只有标题正常、正文全是豆腐 (□□□) 的 PDF。
到底需要哪几个字重?
发票、收据、业务报告只需要 Regular 和 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", 11),
)
用 -Bold 这个后缀注册之后,template.Bold() 会自动挑到它。-Italic 和 -BoldItalic 规则相同。不过 Noto Sans JP 没有斜体 — CJK 字体通常不发布斜体,因为字形本身没有自然的倾斜形式。需要在日语里做强调时,用颜色、字号或粗体代替。
宣传册需要 Medium 或 SemiBold 时,用任意后缀注册,再按字体族名直接引用即可:
gpdf.WithFont("NotoSansJP-Medium", medium)
// ...
c.Text("見出し", template.FontFamily("NotoSansJP-Medium"))
基于后缀的 Bold/Italic 快捷方式只在 -Bold / -Italic / -BoldItalic 这三个字面名上生效,其他都按族名显式引用。
子集化之后的真实体积
Noto Sans JP Regular 磁盘上约 5 MB。这个数字让一些团队去另建字体 CDN、或者给 PDF 做后处理剥离字体。用 gpdf 不需要。
下面是实际落到 PDF 里的数据量:
| 文档 | 使用字形数 | PDF 中的字体数据 |
|---|---|---|
| 一行收据 (约 15 字) | 约 14 | 约 11 KB |
| 普通发票 (约 200 字) | 约 80 | 约 28 KB |
| 10 页报告 (约 8,000 字) | 约 900 | 约 180 KB |
| 字典级满字 (JIS Level 1 全) | 约 6,800 | 约 2.1 MB |
(gpdf v1.0,静态子集化开启。数字会因字形 ID 落在 CFF 和 hmtx 哪一块而上下浮动几 KB)
一份最终 50 KB 的发票 PDF,一半以上是字体数据。但比起不做子集化直接嵌入 5 MB 来说,这点开销几乎可以忽略,查看器会瞬间打开。
Noto Sans JP 与 Noto Sans CJK JP — 不要搞混
Noto 家族里有两个都声称能处理日语的子族,名字相似得让人以为可以互换。实际上不是。
Noto Sans JP 是你要用的。TTF 格式,单一语言,每个字重一个文件。就是 Google Fonts 上下载的那个。
Noto Sans CJK JP 是覆盖整个 CJK 的超级族。以 OpenType Collection (.ttc) 形式发布,把日语、简体中文、繁体中文、韩语的字形经过汉字统合 (Han unification) 后塞在一个文件里。早期 Noto 发行版和 notofonts.github.io/noto-cjk 上的都是这个。
gpdf 直接支持 TTF。TTC 是容器格式 — 你需要在传给 WithFont 之前挑选正确的 face index,而且每个 face 里的 cmap 是针对特定 CJK 地区调过的,等于你在默默地替汉字统合做选择。直接选 JP 专用的 TTF 会让这些选择显式化。
新项目用 Noto Sans JP。如果遗留项目里已经有 NotoSansCJK-Regular.ttc,用 pyftsubset 或 fonttools 抽出 JP face,把结果 TTF 作为项目里的标准产物 check in。
把字体编译进二进制
PDF 生成服务大多跑在容器里,发字体最干净的方式是编译进去:
package main
import (
_ "embed"
"github.com/gpdf-dev/gpdf"
)
//go:embed NotoSansJP-Regular.ttf
var notoJP []byte
func main() {
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", notoJP),
gpdf.WithDefaultFont("NotoSansJP", 11),
)
// ...
}
二进制从约 8 MB 涨到约 13 MB。换来的是:Docker 镜像只有一个产物而不是两个,COPY --from=builder /app /app 就够用,也不会有人因为忘记复制字体文件而上线一个坏掉的容器。每天生成几千份 PDF 的批处理任务,这是合理的默认方案。
相关阅读
- 如何在 gpdf 中嵌入日语字体? — 适用于任何 CJK TTF 的通用方法
- gofpdf 已归档。gpdf 迁移指南 — 从
AddUTF8Font过来的迁移表 - Go PDF 库横评 2026 — 主要库在 CJK 上的对比
- 字体指南 —
WithFont完整参考
试用 gpdf
gpdf 是 Go 的 PDF 生成库。MIT 许可,零外部依赖,原生 CJK 支持。
go get github.com/gpdf-dev/gpdf