記事一覧

Col を行の右端に寄せるには?

空の Col で押し出して列を右端に置き、中のテキストは template.AlignRight() で右揃えにする。この 2 つは別物で、大抵は両方必要です。

著者: Taiki Noda

質問を別の言葉で

r.Col(4, ...) を 1 つだけ書いた行を作って、その列がページの右側に来ることを期待した。実際には左に描画され、幅は 3 分の 1、右側には大きな余白が空いた。あるいは列は右に寄せられたのに、中のテキストは左端に張り付いたまま。gpdf での正しい書き方は?

結論

gpdf に Offset も Push も、行レベルの Justify もありません。列を右に押し出すのは空の埋め草 Col、中のテキストを右に寄せるのは template.AlignRight()。 この 2 つは別物で、ほぼ常に両方要ります。

page.AutoRow(func(r *template.RowBuilder) {
	r.Col(8, func(c *template.ColBuilder) {}) // 埋め草
	r.Col(4, func(c *template.ColBuilder) {
		c.Text("合計: ¥25,575", template.AlignRight())
	})
})

コード

完全なプログラム。多くの人がこの疑問に行き着く、請求書ヘッダーの形です。

package main

import (
	"log"
	"os"

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

func main() {
	doc := template.New(
		template.WithPageSize(document.A4),
		template.WithMargins(document.UniformEdges(document.Mm(20))),
	)

	page := doc.AddPage()

	// 左ブロックと右ブロックを 1 行に: 8 + 4 = 12。
	page.AutoRow(func(r *template.RowBuilder) {
		r.Col(8, func(c *template.ColBuilder) {
			c.Text("株式会社アクメ", template.Bold(), template.FontSize(16))
			c.Text("東京都渋谷区渋谷 1-2-3", template.TextColor(pdf.Gray(0.4)))
		})
		r.Col(4, func(c *template.ColBuilder) {
			c.Text("請求書 No.1042", template.AlignRight(), template.Bold())
			c.Text("発行日 2026-09-02", template.AlignRight(),
				template.TextColor(pdf.Gray(0.4)))
		})
	})

	page.AutoRow(func(r *template.RowBuilder) {
		r.Col(12, func(c *template.ColBuilder) {
			c.Spacer(document.Mm(10))
		})
	})

	// 合計ブロック: 8 スパンの空白のあと、4 スパンをマージンに密着させる。
	page.AutoRow(func(r *template.RowBuilder) {
		r.Col(8, func(c *template.ColBuilder) {})
		r.Col(4, func(c *template.ColBuilder) {
			c.Text("小計:      ¥23,250", template.AlignRight())
			c.Text("消費税 10%:  ¥2,325", template.AlignRight())
			c.Spacer(document.Mm(2))
			c.Line(template.LineThickness(document.Pt(1)))
			c.Spacer(document.Mm(2))
			c.Text("合計:      ¥25,575", template.AlignRight(),
				template.Bold(), template.FontSize(14))
		})
	})

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

なぜ埋め草の列が要るのか

ここは丸暗記より理解しておく価値があります。gpdf のレイアウトで引っかかる他のいくつかの挙動も、同時に説明が付くからです。

行は水平方向のボックスです。RowBuilder.build は r.cols を順に走査して列ごとに子ボックスを 1 つ生成しますが、その子に設定するスタイルは幅だけです。

colBox := &document.Box{
	Content: cb.buildNodes(),
	BoxStyle: document.BoxStyle{
		Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),
	},
}

gridColumns は 12。つまり Col(4) は「行の幅の 33.33% のボックス」以上でも以下でもありません。行は子を左から右へ並べ、子が尽きたところで止まります。余白という概念を持っていないのは、document.BoxStyle に JustifyContent フィールドが存在しないからです。リポジトリ全体を grep しても出てきません。あるのは Width / Height / MinWidth / MaxWidth / MinHeight / MaxHeight / Margin / Padding / Border / Background / Direction / Position だけ。これが語彙の全部です。

だから Col(4) を単独で置くと、幅 33.33% のボックスが 1 つだけ入った行になり、それが水平フローの先頭に置かれます。残りの 66.67% は「列の右にある空きスペース」ではなく、そもそも何も無い。押し返す相手がいないのです。

埋め草の Col(8) は、その空間を占める何かをレイアウトエンジンに与えます。木構造に空のボックスが 1 つ増えるだけで、描画結果には何も出ません。

ついでに言えば Col(8) に特別な意味はありません。Col(6) + Col(3) + Col(3) でも最後のブロックは同じように右端に来るし、後から埋め草側にコンテンツを足しても構造を組み直す必要はありません。

罠: 列は右、テキストは左

一番時間を溶かすミスがこれです。

r.Col(8, func(c *template.ColBuilder) {})
r.Col(4, func(c *template.ColBuilder) {
	c.Text("合計: ¥25,575") // ← AlignRight が無い
})

列は狙った場所にあります。テキストはそうではない。列の左境界、つまりページの 66.67% の位置から始まるので、なんとなく 右寄せに見えてしまう。金額の桁が全部同じうちは気付かず、3 桁の数字と 5 桁の数字が並んだところで初めて小数点が揃っていないことに気付く、という順番になりがちです。

template.AlignRight() は TextOption (TextAlign を設定する func(*document.Style)) なので、個々の c.Text 呼び出しに付けます。列単位でまとめて設定する手段はありません。ブロック内の全行に書いてください。

Table・Image・ページ番号

テキストオプションは他の要素の内部には届きません。要素ごとに専用の入口があります。

// Table: ヘッダーのスライスと位置対応する列ごとの配置。
c.Table(header, rows, template.ColumnAlign(
	document.AlignLeft, document.AlignRight, document.AlignRight,
))

// Image: 属する列の中での配置。
c.Image(logoBytes, template.WithAlign(document.AlignRight))

// ページ番号は TextOption を取るので AlignRight がそのまま効く。
c.PageNumber(template.AlignRight())

ColumnAlign が取るのは TextOption ではなく document.TextAlign の値です。template.AlignRight() ではなく document.AlignRight。混同しやすいですが、コンパイラがすぐ教えてくれます。

行では解決できないとき

ページの他の要素と無関係に座標へ固定したいもの — 領収印、斜めの透かし、隅のマーク — はグリッドから出ます。

page.Absolute(document.Mm(140), document.Mm(60),
	func(c *template.ColBuilder) {
		c.Text("領収済", template.AlignRight(), template.Bold(),
			template.FontSize(28), template.TextColor(pdf.RGBHex(0xC62828)))
	},
	template.AbsoluteWidth(document.Mm(50)),
)

これは非常口であって定石ではありません。普通のコンテンツのために x 座標を計算し始めていたら、答えは埋め草の列のほうです。

最後にもう 1 つ。Col は個々のスパンを 1〜12 にクランプしますが、合計 は誰もチェックしません。1 行に Col(5) を 3 つ書けば幅の合計は 125% になり、3 列目は折り返さずに右マージンをはみ出します。gpdf は警告を出しません。スパンは自分で数えてください。

関連レシピ

gpdf を使ってみる

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

go get github.com/gpdf-dev/gpdf

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