記事一覧

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-ItalicHelvetica-Oblique にエイリアス
ありありHelvetica-BoldItalicHelvetica-BoldOblique にエイリアス

エイリアスのステップが Helvetica の特殊なところ。buildFontVariantID はファミリーに関係なく汎用の -Italic / -BoldItalic サフィックスを常に出す。その後、Standard 14 の init フックが Helvetica-ItalicHelvetica-Oblique に、Helvetica-BoldItalicHelvetica-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 の別々のフィールド (FontWeightFontStyle) をセットしているだけなので、順序は関係ない。

関連レシピ

gpdf を使ってみる

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

go get github.com/gpdf-dev/gpdf

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