全部文章

如何在 gpdf 中在同一段落里混用两种字体

在 gpdf 中要在一个段落里混用多种字体,用 c.RichText 并给每个 span 设置 template.FontFamily。c.Text 只能给整个字符串套一种字体。

换个说法

我有一个段落 —— 一句话、一个标签、一个表格单元 —— 想让其中一部分用一种字体,另一部分用另一种。Helvetica 的一行里嵌一段等宽的代码片段。ASCII 订单号旁边用 Noto Sans JP 写一个日文名字。怎样在段落中途换字体,又不把文本拆成不同的块?

简短答案

c.Text 在这里是错的工具。它给整个字符串套一个 document.Style —— 字体族也只有一个。你要的是 c.RichText,每个 span 带自己的样式:

c.RichText(func(rt *template.RichTextBuilder) {
    rt.Span("Run ")
    rt.Span("gofmt ./...", template.FontFamily("Courier"))
    rt.Span(" before you commit.")
})

3 个 span,2 种字体,1 个段落。布局引擎会像文字处理器那样跨 span 边界换行,所以等宽片段会和周围的 Helvetica 行内一起流动。

Courier 不用 WithFont 也能用,因为它是 PDF Standard 14 字体之一 —— 和 HelveticaTimes-Roman 一样,每个阅读器都自带。如果第二个字体是你自己提供的 TrueType 文件(品牌字体、CJK 字体),就注册一次再按名字引用。下面细说。

可运行代码(Helvetica + Courier,无字体文件)

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/pdf"
    "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.RichText(func(rt *template.RichTextBuilder) {
                rt.Span("Run ")
                rt.Span("gofmt ./...", template.FontFamily("Courier"))
                rt.Span(" before every commit. ")
                rt.Span("It is not optional", template.Bold(), template.Italic())
                rt.Span(".")
            })
            c.RichText(func(rt *template.RichTextBuilder) {
                rt.Span("The field is ")
                rt.Span("created_at", template.FontFamily("Courier"), template.TextColor(pdf.RGBHex(0xB00020)))
                rt.Span(" — not ")
                rt.Span("createdAt", template.FontFamily("Courier"))
                rt.Span(".")
            })
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("mixed-fonts.pdf", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

正文保持 Helvetica(默认),行内标识符切到 Courier,有一个 span 在默认字体上叠了 bold + italic。没有 WithFont,没有内嵌字体数据 —— PDF 把 HelveticaHelvetica-BoldObliqueCourier 作为未内嵌的 Type 1 条目引用,每个阅读器都已经有这些字体。

RichText 对这些 span 做了什么

每个 rt.Span 都会变成一个带自己样式副本的 document.RichTextFragment。不带选项调用的 span 继承块样式 —— 在 RichText 里就是列的默认值,也就是文档的默认字体和大小。带 template.FontFamily("Courier") 调用的 span 只覆盖那个字段,其它保持不变。

布局时 gpdf 把每个 fragment 拆成单词级的 run,用 该 run 自己的字体度量去测量 —— 所以同一行里 Courier 的单词和 Helvetica 的单词宽度才对 —— 然后用贪心法把 run 装进行。同一行上的 run 共享一条基线,所以 24 pt 的 span 挨着 12 pt 的 span 时在底部对齐,行高会增长以容纳高的那个。

有一点容易让人栽跟头:c.RichText 的第二个参数是段落级样式,每个 span 的选项是片段级的:

选项该放在哪
FontFamily / FontSize / Bold / Italic / TextColor / Underline / Strikethrough每个 span —— 传给每个 rt.Span
AlignLeft / AlignCenter / AlignRight / AlignJustify、行高、TextIndent段落级 —— 作为 c.RichText 的第二个参数传

在单个 rt.Span 上加 AlignRight() 什么也不会发生。对齐是行的属性,不是片段的属性。

真正的场景:拉丁字体挨着 CJK 字体

句中等宽是简单版。大家真正头疼的是在一行里混用西文字体和 CJK 字体 —— 英文标签和日文值、产品代码和商品名。要知道两点。

第一,gpdf 不按文字系统挑字体。如果某个 span 的字体族是 Helvetica 而文本是 日本語,你会得到豆腐字(□)—— Helvetica 没有 CJK 字形,gpdf 也不会悄悄拉来别的已注册字体来补。你要自己给 CJK 的 span 设上 CJK 字体族:

ttf, _ := os.ReadFile("NotoSansJP-Regular.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", ttf),
)
// ...
c.RichText(func(rt *template.RichTextBuilder) {
    rt.Span("Customer: ")                                 // 默认 → Helvetica
    rt.Span("山田 太郎", template.FontFamily("NotoSansJP")) // CJK → Noto Sans JP
    rt.Span("  (ID 10293)")                               // 回到 Helvetica
})

第二 —— 这一点值得说出来 —— 多数日文 CJK 字体本身就带不错的拉丁字形。Noto Sans JP、IPAex、Source Han Sans,它们都能把 ID 10293 画得很好。所以在动手做逐 span 混排之前,先问问你是真的要两种字体,还是只是习惯使然走到了这一步。如果整份文档是「日文 + 一点 ASCII」,最简单的就是 gpdf.WithDefaultFont("NotoSansJP", 11),根本不混。要用 RichText + FontFamily,是在你真的想要不同的观感时 —— 数字用干净的几何拉丁体,正文用人文主义 CJK 体 —— 而不是为了让文字能显示出来。

什么时候 c.Text 就够了

如果整个字符串是一种字体,继续用 c.Text —— 更轻,也更好读。c.Text("発行日: 2026-05-11", template.FontFamily("NotoSansJP")) 是整行一种字体,c.Text 能处理。RichText 体现价值,只在样式在字符串内部变化时。别因为能这么做,就把单一样式的一行包进 RichText 回调里。

相关菜谱

试试 gpdf

gpdf 是一个用于生成 PDF 的 Go 库。MIT 许可,零外部依赖,原生支持 CJK。

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · 阅读文档