如何在 gpdf 中同时使用粗体和斜体
在同一个 span 上传入 template.Bold() 和 template.Italic() 即可。但是 TrueType 字体必须注册全部 4 个变体,否则 BoldItalic 查找会静默回退到基础字体。
把问题换种说法
我想让 PDF 中的一个单词、或者整行文字,同时是粗体和斜体。怎么一次指定两个样式?为什么有时候输出看起来既不粗也不斜?
快速回答
在同一个 c.Text 调用上传入两个选项:
c.Text("WARNING", template.Bold(), template.Italic())
gpdf 会组装变体 ID Family-BoldItalic 并在已注册字体中查找。对于 Adobe Standard 14 字体族(Helvetica、Courier、Times)这能直接工作——gpdf 在内部把 -BoldItalic 别名到正式名 -BoldOblique,并使用内置的 AFM 度量。自己注册的 TrueType 字体需要注册全部 4 个变体,否则查找会静默回退到基础字体。
大多数 bug 都出在第二点。
可运行代码(Helvetica,无需注册字体)
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("Regular Helvetica.")
c.Text("Bold only.", template.Bold())
c.Text("Italic only.", template.Italic())
c.Text("Bold and italic.", template.Bold(), template.Italic())
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("emphasis.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
4 行代码,4 种样式。完全不用 WithFont。生成的 PDF 以非嵌入的 Type 1 条目引用 Helvetica、Helvetica-Bold、Helvetica-Oblique、Helvetica-BoldOblique——每个 PDF 阅读器都已经有这些字体。
gpdf 实际做了什么
解析器从样式标志拼出变体 ID:
Bold() | Italic() | 查找的变体 ID |
|---|---|---|
| 否 | 否 | Helvetica |
| 是 | 否 | Helvetica-Bold |
| 否 | 是 | Helvetica-Italic → 别名到 Helvetica-Oblique |
| 是 | 是 | Helvetica-BoldItalic → 别名到 Helvetica-BoldOblique |
别名步骤是 Helvetica 唯一特殊的地方。buildFontVariantID 不管字体族总是输出通用的 -Italic / -BoldItalic 后缀;随后 Standard 14 的 init 钩子把 Helvetica-Italic 指向 Helvetica-Oblique,把 Helvetica-BoldItalic 指向 Helvetica-BoldOblique,让度量和阅读器绘制一致。Courier 同理。Times 不需要别名,因为它的正名本来就是 Times-Italic / Times-BoldItalic。
陷阱:TrueType 字体必须注册全部 4 个
CJK 文档静默出问题就在这里。即使注册了 Noto Sans JP,只要缺了任一变体,缺失的槽不会经过 Bold 或 Italic 过渡——它直接落到基础字体。
// 看起来没问题。其实不是。
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", regular),
gpdf.WithFont("NotoSansJP-Bold", bold),
gpdf.WithDefaultFont("NotoSansJP", 12),
)
// 这里会用普通 NotoSansJP 渲染——既不粗也不斜。
c.Text("强调文字", template.Bold(), template.Italic())
原因在解析器实现里。先查 NotoSansJP-BoldItalic,未命中时只回退一个东西:基础字体 NotoSansJP。没有「退而求其次用粗体版」的中间步骤。你要 bold-italic,得到的是普通。
修复方法是把要用的变体全部注册:
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
regular := mustRead("NotoSansJP-Regular.ttf")
bold := mustRead("NotoSansJP-Bold.ttf")
italic := mustRead("NotoSansJP-Italic.ttf")
boldItalic := mustRead("NotoSansJP-BoldItalic.ttf")
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithFont("NotoSansJP", regular),
gpdf.WithFont("NotoSansJP-Bold", bold),
gpdf.WithFont("NotoSansJP-Italic", italic),
gpdf.WithFont("NotoSansJP-BoldItalic", boldItalic),
gpdf.WithDefaultFont("NotoSansJP", 12),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("普通文本")
c.Text("强调", template.Bold(), template.Italic())
})
})
data, _ := doc.Generate()
os.WriteFile("jp-emphasis.pdf", data, 0o644)
}
func mustRead(path string) []byte {
b, err := os.ReadFile(path)
if err != nil { log.Fatal(err) }
return b
}
顺便说一句,Noto Sans JP 官方发行版其实没有斜体(slanted)字重——日文排版本来就很少用斜体——所以实际的日文文档大多数只注册 regular 和 bold,日文 span 上干脆不用 template.Italic()。这样没问题。规则是:某个字体族你从不调用 Italic(),就不需要它的斜体变体。只有调用了 Italic() 却没有注册对应文件时才会踩坑。
在同一段中混合粗体和斜体
c.Text 给整个字符串应用一种样式。想在句中局部强调用 c.RichText:
c.RichText(func(rt *template.RichTextBuilder) {
rt.Span("The ")
rt.Span("quick brown fox", template.Bold(), template.Italic())
rt.Span(" jumps over the lazy dog.")
})
每个 rt.Span 有自己的样式标志,布局引擎会像字处理软件那样在 span 之间换行。在单个 Span 上同时给 Bold() + Italic() 和在 c.Text 上一样,走同一个 -BoldItalic 变体查找——是同一套代码。
还有一点值得指出:Bold() 和 Italic() 是可交换的。template.Italic(), template.Bold() 和 template.Bold(), template.Italic() 输出完全相同。它们只是在同一个 document.Style 上设置两个不同字段(FontWeight 和 FontStyle),顺序无关紧要。
相关食谱
- 如何在 gpdf 中嵌入日文字体 ——
WithFont完整用法,包含 4 变体模式 - 为什么 PDF 中日文显示为空心方块(豆腐字) —— 基础字体也未注册时「静默回退」是什么样子
- 如何在 gpdf 中使用 Noto Sans JP —— 挑选哪个 Noto 文件,以及
go:embed如何简化分发
试试 gpdf
gpdf 是 Go 的 PDF 生成库。MIT 协议,零外部依赖,原生支持 CJK。
go get github.com/gpdf-dev/gpdf