記事一覧

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 とインラインで流れる。

CourierWithFont 呼び出しなしで動くのは、PDF Standard 14 フォントの 1 つだから — HelveticaTimes-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 は HelveticaHelvetica-BoldObliqueCourier を未埋め込みの 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 / Strikethroughspan ごと — 各 rt.Span に渡す
AlignLeft / AlignCenter / AlignRight / AlignJustify、行の高さ、TextIndent段落レベル — c.RichText の第 2 引数に渡す

個々の rt.SpanAlignRight() を付けても何も起きない。アラインメントは行のプロパティであって、フラグメントのプロパティではない。

本命のケース: 欧文フォントの隣に 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 を使ってみる

gpdf は Go の PDF 生成ライブラリ。MIT、ゼロ依存、CJK 対応。

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · ドキュメントを読む