記事一覧

テーブルを複数ページにまたがって出力するには?

何もしなくていい。1 ページに収まらない行数のテーブルを渡せば、gpdf がボディを自動で改ページし、各ページの先頭にヘッダーを繰り返す。

質問を言い換えると

請求明細、取引ログ、300 行のエクスポート — どう見ても A4 一枚には収まらないテーブルがある。Go の PDF ライブラリで、これを 2 ページ目・3 ページ目に流し、各ページの先頭にヘッダーを再表示させるには何をすればいい? gpdf なら答えは短い。

結論

何もしなくていい。Table を 1 回呼んで全行を渡せば、gpdf が改ページする:

c.Table(header, rows) // rows が 300 行 — gpdf がページに分割する

ボディは 1 行ずつ必要なページ数に分割される。header スライスは継続ページの先頭に自動で再描画される — 列幅もスタイルも同じ。PageBreak() メソッドも MaxRowsPerPage オプションも行数カウントのループも無い。オーバーフロー処理はレイアウトエンジンの仕事であって、あなたの仕事ではない。

動くコード

複数ページのテーブルを出力する完全なプログラム。main.go として保存し go run .report.pdf が出る。

package main

import (
    "fmt"
    "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))),
    )

    brand := pdf.RGBHex(0x1A237E)

    header := []string{"日付", "請求書番号", "顧客名", "金額"}
    rows := make([][]string, 0, 200)
    for i := 1; i <= 200; i++ {
        rows = append(rows, []string{
            fmt.Sprintf("2026-%02d-%02d", (i%6)+1, (i%28)+1),
            fmt.Sprintf("INV-%05d", 10000+i),
            fmt.Sprintf("顧客 #%d", i),
            fmt.Sprintf("%d", (100+i*7)*100),
        })
    }

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("2026 年 請求台帳", template.FontSize(18), template.Bold())
            c.Spacer(document.Mm(4))

            c.Table(header, rows,
                template.ColumnWidths(20, 20, 40, 20),
                template.TableHeaderStyle(
                    template.TextColor(pdf.White),
                    template.BgColor(brand),
                ),
            )
        })
    })

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

200 行を A4 に流すと約 8 ページになる。そのどのページにも紺色のヘッダーが先頭に乗り、ボディは前ページが止まった場所からちょうど続く。このコードで「複数ページ」を匂わせているのはループ上限の 200 だけだ。

(日本語の 日付 ヘッダー・顧客 # などを描くには日本語 TTF の登録が必要。gpdf.WithFont("NotoSansJP", ttf)gpdf.WithDefaultFont("NotoSansJP")NewDocument に渡す。詳しくは日本語 TrueType フォントを埋め込む。)

仕組み

信頼するために知っておく価値がある。レイアウトエンジンはテーブルをレイアウトするとき、ボディ行を順に測り、次の行が利用可能な高さを超える直前まで現在のページに足していく。収まらなかった行はオーバーフロー用テーブルになる — 同じ Header同じ Footer、残りのボディ行を持つ新しい *document.Table だ。gpdf はレイアウト済みの部分をページに流し、次ページを開き、新しいページの高さでオーバーフロー用テーブルを再びレイアウトエンジンに渡す。残りが無くなるまで繰り返す。

ここから 2 つのことが導かれる:

  • ヘッダーが繰り返されるのは、それがあなたのループではなく tbl.Header にあるから。 オーバーフロー用テーブルは同じスライスを再利用するので、各ページで同一に再描画される。これはタダで手に入る。
  • 「ヘッダーが収まらない」エッジケースが存在しない。 エンジンは何行のボディが入るかを測る前にヘッダー分の高さを確保する。ヘッダー + 最低 1 行のボディがページに収まらないなら、中途半端に分割せずテーブル全体を次ページに送る。

フッターも繰り返す

合計行(あるいは「ページ集計」)をページ下部にも出したいなら、それは document.Table.Footer — ビルダー経由ではなく document レイヤーでテーブルを組んだときに使える:

import "github.com/gpdf-dev/gpdf/document"

tbl := &document.Table{
    Columns: []document.TableColumn{
        {Width: document.Pct(20)}, {Width: document.Pct(20)},
        {Width: document.Auto},    {Width: document.Pct(20)},
    },
    Header: headerRows, // []document.TableRow
    Body:   bodyRows,
    Footer: []document.TableRow{footerRow},
}

Footer スライスは継続ページごとに繰り返される。ヘッダーと同じ仕組みだ。ビルダーの c.Table(...) がフッターを露出していないのは、短いテーブルの大半に不要だから — 必要になった時点で「よくあるケース」の外に出ている。テーブルの詳説が document レイヤーを解説している。

テーブルを新しいページから始める

テーブル単位の「新ページで開始」オプションは無い。ページレベルでやる — テーブルを持つ行の前にページを足す:

doc.AddPage() // 下のテーブルはこのページの先頭から始まる
page2 := doc.AddPage()
page2.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Table(header, rows /* , opts... */)
    })
})

テーブルに必要な「改ページ」制御はこれだけだ。テーブルの内側の改ページは自動で処理され、外側の改ページは単に「このブロックがどこから始まるか」でしかない。

できないこと

  • 「この行はまとめておく」。 ボディ行はすべて分割対象。「4〜7 行目のグループは同じページに残す」というアノテーションは無い。既知の欠落だ。請求明細とその小計を絶対にページ間で裂きたくないなら、そのグループの前で新ページを始めるか、document レイヤーでテーブルを組んで自前の改ページヒントを入れる。
  • 最終ページだけのフッター。 document.Table.Footer は設計上すべてのページで繰り返す(ページごとの列合計がよくあるケースだから)。文書末尾に一度だけの総計が欲しいなら、テーブルのではなくに別ブロックとして足す。
  • テーブル内のページ番号。 「3 / 8 ページ」はテーブルではなく文書のフッターに属する。それがどこに置かれるかはページ番号・ヘッダー・フッターを参照。

10 分溶かすミス

  • PageBreak オプションを探す。 無いし、欲しくもない — それを手で呼んでいる時点でもう負けている。全行を渡せばいい。
  • データを自分でページ単位に切る。 1 ページ目に rows[0:40]、2 ページ目に rows[40:80]… やめたほうがいい。行の計算をどこかで間違え、最終ページが短くなり、ヘッダーのスタイルがズレる。スライス全体を gpdf に渡す。
  • 1 ページ目にだけヘッダーが出ると思う。 そういうライブラリもある。gpdf はすべてのページで繰り返す。印刷してめくる人にとってはこれが正しい。
  • 150 ページのテーブルに 6 MB の CJK フォント。 フォントは実際に使うグリフだけにサブセットされるので問題ない — 出力は小さいまま。ただしサブセット化を何らかの理由で無効にしていたら、長いテーブルで効いてくる。サブセット化はオンのまま(デフォルト)にしておく。

関連レシピ

gpdf を使ってみる

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

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs