gpdf で画像を等比でカラム幅に収めるには?
c.Image にバイト列を渡すだけ。gpdf はデフォルトでカラム幅に等比縮小する。明示したいときだけ FitWidth / FitHeight を使う。
質問を言い換えると
ロゴ・グラフ・スクショなど、たとえば 1200×800 の PNG を gpdf のカラムに入れたい。アスペクト比の計算は 手でやりたくない。横長に潰したくない。隣のカラムにはみ出させたくない。等比で縮小して綺麗に収める、それだけがしたい。
即答
c.Image(imgBytes)
ほとんどのケースはこれで終わり。c.Image のデフォルトは FitContain で、アスペクト比を保ったままカラム幅に縮小される。元画像がカラムより小さければそのままの寸法で描画される。
カラム幅より小さい上限を付けたいなら template.FitWidth か template.FitHeight を追加する:
c.Image(imgBytes, template.FitWidth(document.Mm(40)))
c.Image(imgBytes, template.FitHeight(document.Mm(20)))
どちらもアスペクト比を維持する。指定するのは片方だけで、もう片方は gpdf が計算する。
動くサンプル
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
logo, err := os.ReadFile("logo.png")
if err != nil {
log.Fatal(err)
}
chart, err := os.ReadFile("chart.png")
if err != nil {
log.Fatal(err)
}
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
// ロゴ用の狭いカラム。30mm に固定。
r.Col(3, func(c *template.ColBuilder) {
c.Image(logo, template.FitWidth(document.Mm(30)))
})
// グラフ用の広いカラム。デフォルトのまま使えるだけ使う。
r.Col(9, func(c *template.ColBuilder) {
c.Image(chart)
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("report.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
3 カラム幅のロゴセルは FitWidth(30mm) で固定している。カラム幅がレイアウトでどう変わってもロゴは常に 30mm 幅。9 カラム幅のグラフは c.Image(chart) だけで、与えられた幅を全部使う。両方とも等比。元画像のピクセル数をコードで知っている必要はない。
gpdf の「等比」が指すもの
fit モードは 4 つある。デフォルト 1 つで実用の 9 割は片付く:
| モード | 挙動 | 用途 |
|---|---|---|
FitContain (デフォルト) | アスペクト比を保ってボックス内に収める。片方の寸法に余白が残ることがある | ロゴ・グラフ・スクショ — ほぼ全部 |
FitCover | アスペクト比を保ったままボックス全体を覆う。はみ出した部分はクリップ | ヒーローバナー、プロフィール写真の正方クロップ |
FitStretch | ボックスにぴったり合うように引き伸ばす。アスペクト比が崩れる | ほぼ使わない。使うときは大抵バグ |
FitOriginal | 元ピクセル寸法を 72 DPI で換算して描画 | 印刷解像度で作られた図版で、リサンプリングを避けたいとき |
FitWidth と FitHeight はどちらも片方の寸法を固定して、もう片方は FitContain で算出する。「幅を気にしている」「高さを気にしている」を素直に書けるショートカットなので、WithFitMode を直接呼ぶ機会はほぼない。
ハマりどころ
一番よく見る失敗は、アスペクト比が合わない width と height を両方指定して「画像が潰れる」と言うパターン:
// 本気で意図していなければ書かないほうがいい
c.Image(img,
template.FitWidth(document.Mm(40)),
template.FitHeight(document.Mm(40)),
)
PNG が 1200×800 なのに 40×40 のボックスに無理やり入れるなら、アスペクト比 (FitStretch 挙動) と画像の一部 (FitCover 挙動) のどちらかを犠牲にするしかない。デフォルトは FitContain なので、gpdf はアスペクト比を保ったまま片方を埋め切らない。結果は 40mm 幅・約 26mm 高で、40mm の枠の下半分が空く。
直し方は片方の寸法だけ指定して計算は gpdf に任せる。本当に正方にクロップしたければ、矛盾した 2 寸法ではなく FitCover を使う:
c.Image(img,
template.FitWidth(document.Mm(40)),
template.FitHeight(document.Mm(40)),
template.WithFitMode(document.FitCover),
)
ピクセル数は嘘をつかない
gpdf はスケール判断の前に PNG / JPEG ヘッダから元ピクセル寸法を読む。だから 4000×3000 の写真を 60mm のカラムに入れても「元画像側で縮小される」わけではない。元バイト列をそのまま PDF に埋め込み、リサンプリングは PDF リーダー側でやる。表示寸法をどう変えても出力 PDF のサイズは同じ。
ファイルサイズが印刷品質より大事なら、image/draw あたりで先にダウンサンプリングしてから gpdf に渡す。ライブラリが勝手にピクセルを捨てることはない。これは呼び出し側の判断。
レイアウト崩れに備えるなら
ページ分割や狭いテーブルセルでカラムが想定より細くなったとき、デフォルトの FitContain はロゴを切手サイズまで縮めてくれる。それが嫌なら下限を設定する:
c.Image(logo,
template.FitWidth(document.Mm(30)),
template.MinDisplayWidth(document.Mm(20)),
)
MinDisplayWidth はレイアウトエンジンに「20mm を下回らないと収まらないなら、このページに描かずに次のページへ送れ」と伝える。読める寸法で出るか、出ないか。中途半端に潰れた状態にはならない。
関連レシピ
- gpdf で透過 PNG を埋め込む方法 — 同じ
c.Image入口。アルファチャンネル側の話 - gpdf の 12 カラムグリッドの仕組み — 「カラム幅」が実際に何 mm になるかの話
- テーブルのカラム幅を指定する方法 — 画像の入る箱がカラムでなくテーブルセルの場合
gpdf を試す
gpdf は Go の PDF 生成ライブラリ。MIT、外部依存ゼロ、画像とフォントを純 Go で扱う。
go get github.com/gpdf-dev/gpdf