gpdf で Bold と Italic を同時に指定する方法
template.Bold() と template.Italic() を同じ span に渡すだけ。ただし TrueType フォントは 4 バリアント全部を登録しないと BoldItalic のルックアップが base family に静かにフォールバックする。
質問を言い換えると
1 単語、あるいは 1 行だけを PDF の中で太字かつ斜体にしたい。両方を同時に指定するにはどう書くのか。そして、なぜ時々どちらにも見えない出力になるのか。
即答
同じ c.Text 呼び出しに両方のオプションを渡すだけ:
c.Text("WARNING", template.Bold(), template.Italic())
gpdf は Family-BoldItalic というバリアント ID を組み立てて登録済みフォントを引く。Adobe Standard 14 ファミリー (Helvetica, Courier, Times) では何もせずに動く — gpdf が -BoldItalic を正式名の -BoldOblique に内部的にエイリアスし、ビルトインの AFM メトリクスを使うからだ。自分で登録する TrueType フォントの場合、4 つのバリアントを全部登録しないと、ルックアップは静かに base family にフォールバックする。
バグのほとんどはこの 2 点目で生まれる。
動くコード (Helvetica、フォント登録なし)
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"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.Text("Regular Helvetica.")
c.Text("Bold only.", template.Bold())
c.Text("Italic only.", template.Italic())
c.Text("Bold and italic.", template.Bold(), template.Italic())
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("emphasis.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
4 行で 4 つのスタイル。WithFont 呼び出しは一切なし。生成された PDF は Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique を未埋め込みの Type 1 エントリとして参照する。どの PDF ビューアも最初から持っているフォントだ。
gpdf が実際にやっていること
リゾルバはスタイルフラグからバリアント ID を組み立てる:
Bold() | Italic() | ルックアップされるバリアント ID |
|---|---|---|
| なし | なし | Helvetica |
| あり | なし | Helvetica-Bold |
| なし | あり | Helvetica-Italic → Helvetica-Oblique にエイリアス |
| あり | あり | Helvetica-BoldItalic → Helvetica-BoldOblique にエイリアス |
エイリアスのステップが Helvetica の特殊なところ。buildFontVariantID はファミリーに関係なく汎用の -Italic / -BoldItalic サフィックスを常に出す。その後、Standard 14 の init フックが Helvetica-Italic を Helvetica-Oblique に、Helvetica-BoldItalic を Helvetica-BoldOblique にマップし直す。こうしてメトリクスがビューアの描画と一致する。Courier も同じ扱い。Times はエイリアスが不要で、正式名がそのまま Times-Italic / Times-BoldItalic になっている。
罠: TrueType フォントは 4 つ全部の登録が必要
CJK 文書が静かに壊れるのはここ。Noto Sans JP を登録していても、どれかのバリアントを忘れると、欠けたスロットは Bold や Italic 経由ではなく、直接 base family に落ちる。
// 一見正しく見える。そうじゃない。
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", regular),
gpdf.WithFont("NotoSansJP-Bold", bold),
gpdf.WithDefaultFont("NotoSansJP", 12),
)
// ここは NotoSansJP (通常) で描画される — 太字でも斜体でもない。
c.Text("強調したい", template.Bold(), template.Italic())
理由はリゾルバの実装にある。まず NotoSansJP-BoldItalic を引き、ヒットしないと、ぴたり 1 つだけにフォールバックする: base family の NotoSansJP。「せめて bold 版」という中間ステップは存在しない。bold-italic を頼んだのに通常が返ってきた、という出力になる。
対策は、使うバリアントを全部登録すること:
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 := mustRead("NotoSansJP-Regular.ttf")
bold := mustRead("NotoSansJP-Bold.ttf")
italic := mustRead("NotoSansJP-Italic.ttf")
boldItalic := mustRead("NotoSansJP-BoldItalic.ttf")
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithFont("NotoSansJP", regular),
gpdf.WithFont("NotoSansJP-Bold", bold),
gpdf.WithFont("NotoSansJP-Italic", italic),
gpdf.WithFont("NotoSansJP-BoldItalic", boldItalic),
gpdf.WithDefaultFont("NotoSansJP", 12),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("通常のテキスト")
c.Text("強調", template.Bold(), template.Italic())
})
})
data, _ := doc.Generate()
os.WriteFile("jp-emphasis.pdf", data, 0o644)
}
func mustRead(path string) []byte {
b, err := os.ReadFile(path)
if err != nil { log.Fatal(err) }
return b
}
ちなみに Noto Sans JP の公式配布には実は斜体 (slanted) のカットが存在しない — 和文の斜体組版はそもそも珍しい — ので、実際の日本語文書では regular と bold だけ登録して template.Italic() は日本語 span には使わない、という運用がほとんど。それで問題ない。ルールはこうだ: そのファミリーで Italic() を一度も呼ばないなら、italic バリアントは不要。Italic() を呼ぶのにファイルを登録していない時だけ、罠になる。
一段落の中で Bold と Italic を混ぜる
c.Text は文字列全体に 1 つのスタイルを当てる。文中で一部だけ強調するには c.RichText:
c.RichText(func(rt *template.RichTextBuilder) {
rt.Span("The ")
rt.Span("quick brown fox", template.Bold(), template.Italic())
rt.Span(" jumps over the lazy dog.")
})
各 rt.Span が独自のスタイルフラグを持ち、レイアウトエンジンはワードプロセッサと同じ要領で span 間の改行を処理する。Bold() + Italic() を 1 つの Span に付けた場合、c.Text と同じ -BoldItalic バリアントのルックアップに行く — コードパスは共通だ。
もう 1 つ書いておく: Bold() と Italic() は可換 (commutative)。template.Italic(), template.Bold() と template.Bold(), template.Italic() は同じ出力になる。同じ document.Style の別々のフィールド (FontWeight と FontStyle) をセットしているだけなので、順序は関係ない。
関連レシピ
- gpdf で日本語フォントを埋め込む方法 —
WithFontの完全な使い方、4 バリアントパターン込み - PDF に豆腐 (□) が出るのはなぜか — base family も未登録だった場合の「静かなフォールバック」がどう見えるか
- gpdf で Noto Sans JP を使う方法 — どの Noto ファイルを選ぶか、
go:embedでの配布
gpdf を使ってみる
gpdf は Go の PDF 生成ライブラリ。MIT、ゼロ依存、CJK 対応。
go get github.com/gpdf-dev/gpdf