gpdf で 1 つの段落に 2 つのフォントを混ぜる方法
gpdf で 1 段落に複数フォントを混ぜるには c.RichText を使い、各 Span に template.FontFamily を指定する。c.Text は文字列全体に 1 書体しか当てられない。
質問を言い換えると
1 つの段落 — 1 文、ラベル、表のセル — があって、その一部を別のフォントにしたい。Helvetica の行の中にコードスニペットを等幅で。ASCII の注文 ID の隣に日本語の名前を Noto Sans JP で。テキストを別々のブロックに分けずに、段落の途中でフォントを切り替えるにはどうするのか。
即答
c.Text はここでは間違った道具だ。文字列全体に 1 つの document.Style — フォントファミリーも 1 つ込み — を当てる。欲しいのは c.RichText で、各 span が独自のスタイルを持つ:
c.RichText(func(rt *template.RichTextBuilder) {
rt.Span("Run ")
rt.Span("gofmt ./...", template.FontFamily("Courier"))
rt.Span(" before you commit.")
})
span 3 つ、フォント 2 つ、段落 1 つ。レイアウトエンジンはワードプロセッサと同じ要領で span 境界をまたいで改行するので、等幅のフラグメントは周囲の Helvetica とインラインで流れる。
Courier が WithFont 呼び出しなしで動くのは、PDF Standard 14 フォントの 1 つだから — Helvetica や Times-Roman と同じく、どのビューアも最初から持っている。2 つ目のフォントが自分で用意する TrueType ファイル (ブランドフォント、CJK フォント) なら、1 回登録して名前で参照する。詳しくは後述。
動くコード (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 に切り替わり、1 つの span はデフォルトフォントの上に bold + italic を重ねる。WithFont なし、埋め込みフォントデータなし — PDF は Helvetica、Helvetica-BoldOblique、Courier を未埋め込みの Type 1 エントリとして参照する。どのリーダーも最初から持っているフォントだ。
RichText が span に対してやっていること
各 rt.Span は独自のスタイルのコピーを持つ document.RichTextFragment になる。オプションなしで呼んだ span はブロックスタイル — RichText ではカラムのデフォルト、つまりドキュメントのデフォルトフォントとサイズ — を継承する。template.FontFamily("Courier") 付きで呼んだ span はそのフィールドだけ上書きされ、他はそのまま。
レイアウト時、gpdf は各フラグメントを単語レベルの run に分割し、各 run を その run 自身のフォントメトリクスで計測して — だから同じ行の Courier の単語と Helvetica の単語が正しい幅になる — 貪欲法で run を行に詰める。1 行上の run は同じベースラインを共有するので、24 pt の span と 12 pt の span が並ぶと下端で揃い、行の高さは背の高い方に合わせて伸びる。
ここで 1 つ引っかかるのが、c.RichText の第 2 引数は段落レベルのスタイル、span ごとのオプションはフラグメントレベルだという区別:
| オプション | 置き場所 |
|---|---|
FontFamily / FontSize / Bold / Italic / TextColor / Underline / Strikethrough | span ごと — 各 rt.Span に渡す |
AlignLeft / AlignCenter / AlignRight / AlignJustify、行の高さ、TextIndent | 段落レベル — c.RichText の第 2 引数に渡す |
個々の rt.Span に AlignRight() を付けても何も起きない。アラインメントは行のプロパティであって、フラグメントのプロパティではない。
本命のケース: 欧文フォントの隣に CJK フォント
文中の等幅は簡単な方。実際にみんなが苦労するのは、1 行の中で欧文フォントと CJK フォントを混ぜることだ — 英語ラベルと日本語の値、製品コードと商品名。知っておくべきことが 2 つある。
1 つ目、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 に戻る
})
2 つ目 — そしてこれは口に出して言っておくべきところだが — 日本語の CJK フォントの多くは、まともな欧文グリフをすでに持っている。Noto Sans JP、IPAex、Source Han Sans、どれも ID 10293 をちゃんと描く。だから span ごとの混在に手を出す前に、本当に 2 つのフォントが要るのか、それとも惰性でそこに来ただけなのか、を考えてほしい。ドキュメント全体が「日本語+少しの ASCII」なら、いちばん簡単なのは gpdf.WithDefaultFont("NotoSansJP", 11) で混在ゼロ。RichText + FontFamily を使うのは、本当に見た目を変えたいとき — 数字には端正なジオメトリック欧文、本文にはヒューマニストな CJK — であって、単にスクリプトを表示させるためではない。
c.Text で十分なとき
文字列全体が 1 つのフォントなら、c.Text のままがいい — 軽いし読みやすい。c.Text("発行日: 2026-05-11", template.FontFamily("NotoSansJP")) は行全体が 1 フォントで、c.Text で処理できる。RichText が値を出すのは、スタイルが文字列の中で変わるときだけ。1 スタイルの行を、できるからといって RichText のコールバックで包まないこと。
関連レシピ
- gpdf で Bold と Italic を同時に指定する方法 — 同じ
RichTextの span メカニズムを、ファミリーではなくウェイトと斜体に適用したもの - gpdf にカスタム TrueType フォントを追加する方法 — 混ぜたい 2 つ目のフォントを登録する
- gpdf で日本語フォントを埋め込む方法 — 混在フォント行の CJK 側の
WithFontの使い方
gpdf を使ってみる
gpdf は Go の PDF 生成ライブラリ。MIT、ゼロ依存、CJK 対応。
go get github.com/gpdf-dev/gpdf