gpdf で用紙サイズを自由に指定するには?
document.Size はポイント単位。Mm(100).Resolve(0, 0) で変換して WithPageSize に渡す。ラベルや JIS B5 の罠も解説。
質問を言い換えると
必要なのは A4 でも Letter でもない。4×6 インチの配送ラベル、80mm 幅のサーマルレシート、100×148mm のはがき、あるいは JIS B5 —— 厄介なことに、これは document.B5 定数とは別物です。WithPageSize は document.Size を受け取る。任意の寸法でそれをどう作るのか、そして作ったときに何がハマるのか。
TL;DR
document.Size は PDF ポイント(1/72 インチ)の float64 が 2 つあるだけ。Resolve で変換して渡します:
label := document.Size{
Width: document.In(4).Resolve(0, 0), // 288 pt
Height: document.In(6).Resolve(0, 0), // 432 pt
}
doc := template.New(template.WithPageSize(label))
正常系はこれで終わり。残りはハマり方 3 パターンの話です。
コード
4×6 インチ配送ラベルの完全なプログラム。gpdf v1.0.17 で検証済みで、出力の /MediaBox は [0 0 288 432] になります。
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
// mm converts millimetres to PDF points, the unit document.Size uses.
func mm(v float64) float64 { return document.Mm(v).Resolve(0, 0) }
// in converts inches to PDF points.
func in(v float64) float64 { return document.In(v).Resolve(0, 0) }
func main() {
// A 4×6 inch shipping label: 288 × 432 pt.
label := document.Size{Width: in(4), Height: in(6)}
doc := template.New(
template.WithPageSize(label),
template.WithMargins(document.UniformEdges(document.Mm(6))),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("SHIP TO", template.Bold(), template.FontSize(10))
c.Text("Jane Doe", template.FontSize(18), template.Bold())
c.Text("742 Evergreen Terrace")
c.Text("Springfield, OR 97477")
c.Spacer(document.Mm(6))
c.QRCode("TRACK-1Z999AA10123456784", template.QRSize(document.Mm(40)))
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("label.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
mm / in の小さなヘルパーはプロジェクトにコピーしておく価値があります。どうせ自分で書くことになるので。
日本語の宛名を入れるなら、日本語フォントの埋め込みと組み合わせてください。サイズ指定の部分は何も変わりません。
なぜ Size は Value じゃないのか
gpdf の他の場所では document.Mm(20) を渡せば、ポイントへの変換はレイアウトエンジンが後でやってくれる。余白も Spacer も QR のサイズも、全部 document.Value です。だから自然とこう書きたくなる:
document.Size{Width: document.Mm(100), Height: document.Mm(150)} // コンパイルが通らない
通りません。Size の定義はこうだからです:
type Size struct {
Width, Height float64
}
理由は、Size が座標系の一番底だから。Value は Pct や Em にもなれて、それを解決するには親のサイズかフォントサイズが要る。ページこそがその「親」で、その上には何パーセントの基準になるものが存在しない。だからページは具体的な数値でなければならず、PDF の /MediaBox がポイントで書かれる以上、その数値はポイントになる。
Resolve(parentSize, fontSize) はレイアウトエンジンが内部で呼んでいるメソッドです。絶対単位なら引数は両方無視されるので、Resolve(0, 0) は単に「ポイントをくれ」の意味。係数は固定で、Mm は ×2.83465、Cm は ×28.3465、In は ×72。
で、これとセットで来る罠。こっちはコンパイルが通ります:
document.Size{Width: 100, Height: 150} // mm のつもり
結果は 35 × 53mm のページ。エラーも警告もない —— 100pt は幅として何の問題もない値だから。PDF を開いてラベルが切手サイズになっているのを見て初めて気づく。この記事から 1 つだけ持ち帰るなら、ポイントで考えているとき以外、Size に裸の数値を書かないこと。
向きと JIS B5 問題
幅が先、高さが後。横向きにするなら自分で入れ替えるか、Landscape() を呼ぶ:
template.WithPageSize(document.A4.Landscape())
Landscape() は高さが幅より大きいときだけ入れ替えるので、すでに横向きのサイズに呼んでも何も起きません。設定ファイルからサイズが来て、どっち向きか分からないときに便利。
日本で印刷する人向けの余談。gpdf の document.B5 は ISO B5 で 176 × 250mm。オフィスの複合機で「B5」と言ったら、それは JIS B5 の 182 × 257mm です。定数の GoDoc にもそう書いてあるけど、GoDoc は印刷結果に 3mm の隙間ができるまで誰も読まない。JIS サイズはカスタムサイズとして作ります:
jisB5 := document.Size{Width: mm(182), Height: mm(257)}
jisB4 := document.Size{Width: mm(257), Height: mm(364)}
hagaki := document.Size{Width: mm(100), Height: mm(148)} // 官製はがき
nagagata3 := document.Size{Width: mm(120), Height: mm(235)} // 長形3号封筒
帳票系で B5 横の納品書を出している現場は、ここで一度は ISO と JIS を取り違えます。
1 ドキュメントに 1 サイズ
AddPage(size) はありません。WithPageSize はドキュメントのオプションで、全ページ —— ページネーターが自動で作るはみ出しページも含めて —— がそれを使います。
A4 の送付状のあとに 4×6 のラベルを 1 ファイルにまとめたいなら、別々に生成してマージします:
merged, err := gpdf.Merge([]gpdf.Source{
{Data: coverA4}, // document.A4 で生成
{Data: labelPDF}, // 4×6 サイズで生成
})
各ページはマージ後もそれぞれの /MediaBox を保持します。確認済みで、595.28 × 841.89 のあとに 288 × 432。印刷ダイアログは混在サイズを普通に扱えます。ラベルプリンタのドライバは扱えないこともあるけど、それはあなたとドライバの問題。
JSON スキーマは名前指定のみ
JSON スキーマの page.size が受け付けるのは名前付きサイズだけ —— "A0"〜"A10"、"B0"〜"B10"、"C0"〜"C10"、"DL"、"Letter"、"Legal" —— と、"orientation": "landscape"。"100mm x 150mm" のような文字列は unknown page size: "100mm x 150mm" で失敗します。
ただし Go から渡したオプションはスキーマより優先される。なのでテンプレートは JSON のまま、サイズだけコードで指定すればいい:
doc, err := gpdf.NewDocumentFromJSON(schema, data,
gpdf.WithPageSize(document.Size{Width: mm(80), Height: mm(200)}),
)
NewDocumentFromTemplate でも同じです。
小さい用紙とデフォルト余白 20mm
これは記事を書いている最中に実際に踏んだので、独立したセクションにします。
template.New のデフォルト余白は四辺 20mm。A4 なら妥当です。でも 50 × 30mm の商品ラベルだと、上下で 40mm 取られて本文の高さは −10mm。60 × 40 ポイントのページ(また裸の数値の罠)に 5mm 余白だと、本文の高さは約 12pt —— 12pt の文字 1 行すら入らない。
どちらのケースも、v1.0.16 までは Generate() がエラーを返しませんでした。そもそも返ってこない。ページネーターが絶対に入らない行を置こうとし続ける。共通点に気づくまでテストプログラム 3 本がハングして、正直あの 30 分は本当にアホらしかった。しかも修正のスクショを撮っている最中に 4 本目がまだハングした。列より幅の広い単語が語中で折り返され、残りのテキストにページごとにスペースが 1 個ずつ増えていたので、「止まっている」ようには見えなかった。これは完全に想定外だった。
v1.0.17 からは、マイクロ秒でエラーが返ります:
import "github.com/gpdf-dev/gpdf/document/layout"
data, err := doc.Generate()
if errors.Is(err, layout.ErrContentDoesNotFit) {
// layout: content does not fit on an empty page: body area is
// 28.35 x 0.00 pt on a 141.73 x 85.04 pt page; ...
log.Fatal(err)
}
本文領域の数値を見れば、たいてい原因は分かります。高さが 0 以下なら、余白・ヘッダー・フッターがページを食い尽くしています。
なので、A6 より小さい用紙では引き続き余白を明示的に指定してください。エラーは「何かがおかしい」と教えてくれるだけで、余白を決めてはくれません。
レシートはもう少し素直です。80mm 幅で高さ固定のページなら普通に改ページされる —— 80 × 120mm のページに 40 行の明細を流したら 3 ページになりました。1 本の連続した紙にしたいなら、高さを先に決め打ちする必要があります。gpdf は内容に合わせてページを伸ばしてはくれません。
関連レシピ
- gpdf の 12 カラムグリッドはどう動く? —— 列幅は設定したページに対するパーセンテージ
- ページ番号・ヘッダー・フッターを確実に入れる —— 小さい用紙ではヘッダーも本文の高さを削る
- Go で請求書 PDF を 50 行以下で生成する —— 比較用の A4 フルドキュメント
- 基本的な使い方ガイド ——
template.Newの全オプション
gpdf を使ってみる
gpdf は Go の PDF 生成ライブラリ。MIT、ゼロ依存、CJK 対応。
go get github.com/gpdf-dev/gpdf