記事一覧

gpdf で日本語フォントを埋め込むには?

gpdf.WithFont にTTFバイトを渡すだけ。サブセット埋め込みも自動、CGOも不要。Go で日本語 PDF を作る最短手順。

著者: gpdf team

質問を言い換えると

gpdf で日本語(あるいは CJK 全般)の PDF を作りたい。AddUTF8Font のお作法も、CGO も、一枚ごとに 5 MB のフォントを埋め込むのも避けたい。最短でどう書くか。

TL;DR

TTF を os.ReadFile で読み込み、gpdf.WithFont("NotoSansJP", fontBytes)NewDocument に渡し、必要ならデフォルトフォントに指定するだけ。セットアップ 3 行で、gpdf は使った文字のグリフだけを抽出して埋め込む — 5 MB のフォント丸ごとは入らない。

動くコード

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/template"
)

func main() {
    font, err := os.ReadFile("NotoSansJP-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    doc := gpdf.NewDocument(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("NotoSansJP", font),
        gpdf.WithDefaultFont("NotoSansJP", 12),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("こんにちは、世界。", template.FontSize(24), template.Bold())
            c.Text("日本語 PDF、これだけ。")
        })
    })

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

NotoSansJP-Regular.ttfGoogle Fonts からダウンロードして main.go の隣に置き、go run main.go を叩けば 1 ページの日本語 PDF が出る。

この 3 行で何が起きているか

背後では 2 つの処理が走っていて、どちらもアプリ側で面倒を見る必要はない。

サブセット埋め込み。 Noto Sans JP は 17,000 字前後のグリフを含み、Regular だけでもディスク上で約 5 MB。これをそのまま埋め込むと、4 行の日本語が載った領収書でも PDF が 5 MB を超えてしまう。gpdf は描画したテキストを走査して使われたグリフ ID を洗い出し、そのサブセットだけを PDF に書き込む。短い請求書 1 枚ならフォントデータは 20〜40 KB 程度に収まる。

gofpdf もサブセット化自体は可能だったが、AddUTF8Font にファイルパスと UTF-8 フラグを渡して都度読み込ませる API 設計のため、ドキュメント途中でのフォント切り替えが扱いづらかった。gpdf はドキュメント生成時に一度だけ登録し、以降は c.Text がファミリ名で参照する。呼び出しごとのお膳立ては不要。

CGO 不使用。 これは地味に大きい。他言語のエコシステムではフォント処理が FreeType や HarfBuzz を経由することが多く、C 依存が入るとビルドキャッシュの挙動が変わり、Docker イメージにレイヤが増え、macOS から linux/arm64 へのクロスコンパイルで一手間かかるようになる。gpdf は TrueType テーブルを純 Go で読んでいる。go build は static のまま。distroless コンテナに Go バイナリと TTF だけ入れて出荷できる。

Bold / Italic を使う

Noto Sans JP は Weight ごとに別ファイル。太字を使いたい場合は Bold 用 TTF を -Bold サフィックス付きで別登録する:

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

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", reg),
    gpdf.WithFont("NotoSansJP-Bold", bold),
    gpdf.WithDefaultFont("NotoSansJP", 12),
)

これで template.Bold()-Bold バリアントを拾う。同じ規約で -Italic-BoldItalic も使える。バリアントを登録していない場合は合成ウェイトにフォールバックされる — 画面では読めるが、タイポグラフィ的には正直でない。本番の請求書には実バリアントを登録しておく。

日中韓を同じドキュメントに混在させる

登録は何ファミリでも OK。文字ごとに template.FontFamily(...) で切り替える:

jp, _ := os.ReadFile("NotoSansJP-Regular.ttf")
sc, _ := os.ReadFile("NotoSansSC-Regular.ttf")
kr, _ := os.ReadFile("NotoSansKR-Regular.ttf")

doc := gpdf.NewDocument(
    gpdf.WithFont("NotoSansJP", jp),
    gpdf.WithFont("NotoSansSC", sc),
    gpdf.WithFont("NotoSansKR", kr),
    gpdf.WithDefaultFont("NotoSansJP", 12),
)

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("日本語")
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("中文", template.FontFamily("NotoSansSC"))
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("한국어", template.FontFamily("NotoSansKR"))
    })
})

漢字統合 (Han unification) の都合で、日本語と中国語簡体字は Unicode コードポイントが重なるが、実際に描画される字形は別物。同じコードポイントでもフォント次第で字の形が変わる ので、越境 EC 向けに和文・中文両対応の帳票を作るなら両方のフォントを登録する必要がある。国によって「骨」「直」などの形が違うことがあるのはここが原因。

豆腐文字 (□□□□) が出たら

日本語を書いているのに WithFont を忘れると、gpdf は標準 14 フォントにフォールバックし、そこには CJK のグリフが入っていないため文字が矩形のまま表示される。これが俗に言う「豆腐文字」。

□□□□□、□□。

この出力を見たら原因は一択: CJK フォントが未登録か、書いた文字を含まない別ファミリで描画している。直し方も一択: WithFont を追加して WithDefaultFont で既定にするか、c.Texttemplate.FontFamily を付ける。

関連記事

gpdf を使ってみる

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

go get github.com/gpdf-dev/gpdf

⭐ GitHub でスター · ドキュメントを読む