全部文章

如何在 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 条目引用 HelveticaHelvetica-BoldHelvetica-ObliqueHelvetica-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 上设置两个不同字段(FontWeightFontStyle),顺序无关紧要。

相关食谱

试试 gpdf

gpdf 是 Go 的 PDF 生成库。MIT 协议,零外部依赖,原生支持 CJK。

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · 阅读文档