全部文章

如何在 gpdf 中添加自定义 TrueType 字体?

读取 TTF 字节,用 gpdf.WithFont 注册字族,再按名称引用即可。Inter、Roboto、图标字体、品牌字体都同样适用。

换个说法

我有一个 .ttf 文件 — 品牌用的 Inter、代码块用的 JetBrains Mono、图标用的字体。怎么把它放进 gpdf 文档,并在 c.Text(...) 里按名字引用?

速答

读取 TTF 字节。把 gpdf.WithFont("你的字族名", bytes) 传给 NewDocument。然后用 template.FontFamily(...) 引用 "你的字族名",或用 gpdf.WithDefaultFont 设为默认。

字族名是 任意字符串。它和字体内部的 name 表毫无关系 — 只是 gpdf 解析 FontFamily 选项时使用的查找 key。取个短的。

可运行代码

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, err := os.ReadFile("Inter-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("Inter", regular),
        gpdf.WithDefaultFont("Inter", 11),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("Quarterly Report", template.FontSize(28))
            c.Text("Generated with gpdf and Inter.")
        })
    })

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

Inter-Regular.ttf 放在 main.go 旁边 (从 rsms.me/inter 下载)。go run main.go。完事。

gpdf 对字节做了什么

Generate() 触发后,gpdf 用纯 Go 解析 TrueType 表 (cmap, glyf, loca, hmtx …) — 不依赖 FreeType,不需要 CGO。它扫描渲染文本,收集实际用到的码点,然后对字形表做子集化,只保留这些码点所需的字形。PDF 内嵌入一个 Type0 / CIDFontType2 字体,仅承载真正需要的字形。

实际效果: 600 KB 的 Inter-Regular.ttf,如果文档只用了几段,PDF 内的字体子集大约只有 12 KB。能用上品牌字体而不让文件变大。

Bold 和 Italic 需要各自的文件

这一点最容易踩坑。gpdf 不会合成 bold 或 italic — 没有「让笔画变粗」这种算法。它根据样式标志拼接出变体 ID 来查找:

Bold()Italic()查找 key
Inter
Inter-Bold
Inter-Italic
Inter-BoldItalic

如果没注册 Inter-Bold,查找会静默回退到普通的 Inter。PDF 仍然会生成,但所有「应该是粗体」的部分都是常规字重。没有任何警告。

四个变体一起注册:

regular, _    := os.ReadFile("Inter-Regular.ttf")
bold, _       := os.ReadFile("Inter-Bold.ttf")
italic, _     := os.ReadFile("Inter-Italic.ttf")
boldItalic, _ := os.ReadFile("Inter-BoldItalic.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("Inter", regular),
    gpdf.WithFont("Inter-Bold", bold),
    gpdf.WithFont("Inter-Italic", italic),
    gpdf.WithFont("Inter-BoldItalic", boldItalic),
    gpdf.WithDefaultFont("Inter", 11),
)

如果某个字体只有一种字重 (很多图标字体和展示字体都是这样),就不要对它调用 template.Bold()template.Italic()。少一个变体没关系。错误地回退到另一个变体才是 bug 报告的根源。

把字体打进二进制

启动时 os.ReadFile 在开发阶段没问题。生产环境里,字体是程序的一部分,应该跟着二进制走:

import _ "embed"

//go:embed fonts/Inter-Regular.ttf
var interRegular []byte

doc := gpdf.NewDocument(
    gpdf.WithFont("Inter", interRegular),
)

go build 把字节直接编译进二进制。再也不会在周五傍晚追查「部署镜像里的 .ttf 跑哪去了」。

图标字体一样的玩法

Font Awesome、Material Symbols 的 TTF 版、IcoMoon、企业自定义字形集 — 都只是 TrueType 文件。注册方式相同:

icons, _ := os.ReadFile("MaterialSymbols-Regular.ttf")
doc := gpdf.NewDocument(
    gpdf.WithFont("Icons", icons),
    gpdf.WithDefaultFont("Inter", 11), // 正文默认
)

// 在某列里:
c.Text("", template.FontFamily("Icons"), template.FontSize(20)) // "home" 图标

Unicode 转义码是字体文档里写的值。gpdf 不在乎那个字形是不是图标 — 在它眼里就是个码点,按字母同样的方式做子集化。

常见错误

  • 调用处字族名打错。 template.FontFamily("Intr") 静默回退到默认字族。无报错无警告。文字突然变成 Helvetica?先查这个。
  • 容器化时不用 //go:embed 精简过的 Docker 上下文丢掉了 .ttf,运行时静默回退,最后从用户邮件里得知问题。打进二进制。
  • 把字体的 PostScript 名当字族名用。 "Inter-Regular" 是 PostScript 名。用它注册 WithFont,bold 查找会去找 "Inter-Regular-Bold" — 根本不存在。取干净的根字族名 ("Inter"),让变体后缀去处理样式。

相关菜谱

试试 gpdf

gpdf 是一个 Go PDF 生成库。MIT 协议、零外部依赖、TrueType 处理纯 Go 实现。

go get github.com/gpdf-dev/gpdf

⭐ 在 GitHub 上 Star · 阅读文档