記事一覧

gpdf にカスタム TrueType フォントを追加するには?

TTF をバイト列で読み込んで gpdf.WithFont でファミリ名を登録する。Inter からアイコンフォントまで、どんな TrueType でも同じ手順で動く。

言い換えると

.ttf がある。ブランドの Inter、コードブロック用の JetBrains Mono、アイコン用のグリフフォント。これを gpdf のドキュメントに取り込んで、c.Text(...) から名前で呼びたい。どうやる?

即答

TTF のバイト列を読む。gpdf.WithFont("好きなファミリ名", bytes)NewDocument に渡す。あとは template.FontFamily(...) でその名前を指定するか、gpdf.WithDefaultFont で既定にしてしまえばいい。

ファミリ名は 任意の文字列。フォント内部の name テーブルとは無関係で、gpdf が FontFamily を解決するときに使うルックアップキーに過ぎない。短めの名前にしておく。

動くコード

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.ttfmain.go の隣に置く (rsms.me/inter からダウンロード)。go run main.go。これだけ。

gpdf がバイト列に対してやっていること

Generate() が呼ばれると、gpdf は TrueType のテーブル (cmap, glyf, loca, hmtx …) を 純 Go でパースする。FreeType も CGO もない。レンダリング対象のテキストを走査し、実際に使われたコードポイントを集め、そのコードポイント分だけにグリフテーブルをサブセット化 する。PDF には Type0 / CIDFontType2 フォントとして、必要なグリフだけが埋め込まれる。

実際的な効果: 600 KB の Inter-Regular.ttf を 1〜2 段落で使っただけなら、PDF 内のフォントサブセットは 12 KB 程度になる。ブランドフォントを使ってもファイルが膨れない。

Bold / Italic は別ファイルが必要

ここが踏み抜きやすい。gpdf は bold や italic を合成しない。「線を太くする」ようなアルゴリズム処理はない。代わりに、スタイルフラグからバリアント ID を組み立ててルックアップする:

Bold()Italic()ルックアップキー
なしなしInter
ありなしInter-Bold
なしありInter-Italic
ありありInter-BoldItalic

Inter-Bold を登録していなければ、ルックアップは黙って素の Inter にフォールバックする。PDF は出力されるが、太字のはずの箇所がレギュラーのまま。警告は出ない。

4 種類とも登録する:

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),
)

ウェイトが 1 種類しかないフォント (アイコンフォントやディスプレイフォントに多い) なら、そのフォントに対しては template.Bold()template.Italic() を使わない。バリアントを持たないこと自体は問題ない。間違ったバリアントにフォールバックするのが「太字が太字にならない」バグ報告の正体。

バイナリにフォントを同梱する

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 はそれがアイコンかどうか気にしない — 単なるコードポイントとして文字と同じようにサブセット化する。

商用日本語フォント (モリサワ・フォントワークス) を使う場合

商用の日本語フォントをサーバーサイドで PDF に埋め込む用途は、ライセンスで明確に許諾されているか必ず確認する。サブセット埋め込みでも「PDF への埋め込み許可」と「サーバーでの動的生成許可」は別条項なことが多い。ライセンス確認なしに業務 PDF に埋め込むと、後から億単位の請求につながり得る

無償で商用利用も可能な日本語 TTF が必要なら Noto Sans JPIPAex Gothic を選ぶのが安全。

よくあるミス

  • 呼び出し側でファミリ名のタイポ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 でスター · ドキュメントを読む