記事一覧

gpdf で画像を等比でカラム幅に収めるには?

c.Image にバイト列を渡すだけ。gpdf はデフォルトでカラム幅に等比縮小する。明示したいときだけ FitWidth / FitHeight を使う。

質問を言い換えると

ロゴ・グラフ・スクショなど、たとえば 1200×800 の PNG を gpdf のカラムに入れたい。アスペクト比の計算は 手でやりたくない。横長に潰したくない。隣のカラムにはみ出させたくない。等比で縮小して綺麗に収める、それだけがしたい。

即答

c.Image(imgBytes)

ほとんどのケースはこれで終わり。c.Image のデフォルトは FitContain で、アスペクト比を保ったままカラム幅に縮小される。元画像がカラムより小さければそのままの寸法で描画される。

カラム幅より小さい上限を付けたいなら template.FitWidthtemplate.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 で換算して描画印刷解像度で作られた図版で、リサンプリングを避けたいとき

FitWidthFitHeight はどちらも片方の寸法を固定して、もう片方は 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 を試す

gpdf は Go の PDF 生成ライブラリ。MIT、外部依存ゼロ、画像とフォントを純 Go で扱う。

go get github.com/gpdf-dev/gpdf

⭐ GitHub で Star する · ドキュメントを読む