🖨️

Go言語でHTMLテンプレートをPDF/PNGに変換する方法

に公開

ゴール

  • Go で HTML テンプレを html/template から生成
  • chromedp(headless Chrome)で PDF と PNG を作る
  • 一発で試せるサンプル(CLI フラグ付き)

0. 前提(macOS, Apple Silicon)

  • Go 1.21+(推奨 1.22 以上)
  • Google Chrome (または Chromium) がインストール済み
    (未導入なら brew install --cask google-chrome)

1. プロジェクト初期化

mkdir html2pdf-go && cd html2pdf-go
go mod init example.com/html2pdf
go get github.com/chromedp/chromedp

2. HTML テンプレートを用意

template.html(例:簡易レシート)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>{{.Title}}</title>
  <style>
    :root{ --brand:#0ea5e9; --muted:#64748b; }
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; margin: 32px; }
    h1 { color: var(--brand); margin: 0 0 8px; }
    .meta { color: var(--muted); font-size: 14px; margin-bottom: 24px; }
    table { border-collapse: collapse; width: 100%; }
    th, td { padding: 8px 12px; border-bottom: 1px solid #e2e8f0; text-align: left; }
    tfoot td { font-weight: bold; }
    .footer { margin-top: 32px; color: var(--muted); font-size: 12px; }
  </style>
</head>
<body>
  <h1>{{.Title}}</h1>
  <div class="meta">発行日: {{.IssuedAt}} / 請求先: {{.Customer}}</div>

  <table>
    <thead><tr><th>品目</th><th>数量</th><th>単価</th><th>小計</th></tr></thead>
    <tbody>
      {{range .Items}}
      <tr>
        <td>{{.Name}}</td>
        <td>{{.Qty}}</td>
        <td>¥{{.Price}}</td>
        <td>¥{{.Subtotal}}</td>
      </tr>
      {{end}}
    </tbody>
    <tfoot>
      <tr><td colspan="3">合計</td><td>¥{{.Total}}</td></tr>
    </tfoot>
  </table>

  <div class="footer">
    これはサンプル出力です。PDF 印刷や PNG スクショで見栄えを確認できます。
  </div>
</body>
</html>

3. Go コード(chromedp で PDF / PNG)

main.go

package main

import (
	"bytes"
	"context"
	"encoding/base64"
	"flag"
	"html/template"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/chromedp/cdproto/emulation"
	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/chromedp"
)

// テンプレに流し込むデータ構造
type Item struct {
	Name     string
	Qty      int
	Price    int
	Subtotal int
}

type ViewData struct {
	Title    string
	IssuedAt string
	Customer string
	Items    []Item
	Total    int
}

func main() {
	var tplPath, outPDF, outPNG, chromeExec string
	var width, height int
	flag.StringVar(&tplPath, "tpl", "template.html", "HTML template path")
	flag.StringVar(&outPDF, "pdf", "out.pdf", "PDF output path")
	flag.StringVar(&outPNG, "png", "out.png", "PNG output path")
	flag.StringVar(&chromeExec, "chrome-path", "", "Custom Chrome/Chromium executable (optional)")
	flag.IntVar(&width, "w", 1280, "viewport width for PNG")
	flag.IntVar(&height, "h", 800, "viewport height for PNG")
	flag.Parse()

	// 1. テンプレ読み込み + データ適用
	tpl, err := template.ParseFiles(tplPath)
	must(err)

	// サンプルデータ
	data := ViewData{
		Title:    "ご請求書",
		IssuedAt: time.Now().Format("2006-01-02"),
		Customer: "exMedia 株式会社",
		Items: []Item{
			{"クラウド利用料", 3, 12000, 3 * 12000},
			{"サポート費", 1, 2000, 1 * 20000},
		},
	}
	for _, it := range data.Items {
		data.Total += it.Subtotal
	}

	var buf bytes.Buffer
	must(tpl.Execute(&buf, data))
	html := buf.String()

	// data URL で読み込ませる (ファイル保存不要)
	dataURL := "data:text/html;base64," + base64.StdEncoding.EncodeToString([]byte(html))

	// 2. chromedp セッション作成
	allocOpts := append(chromedp.DefaultExecAllocatorOptions[:])
	if chromeExec != "" {
		allocOpts = append(allocOpts, chromedp.ExecPath(chromeExec))
	}

	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), allocOpts...)
	defer cancel()

	// タイムアウト (ページが重い場合は延長)
	ctx, cancel := context.WithTimeout(allocCtx, 30*time.Second)
	defer cancel()

	ctx, cancel = chromedp.NewContext(ctx)
	defer cancel()

	// 3. ページを開いて PDF 生成 & PNG 取得
	var pdfBytes, pngBytes []byte
	err = chromedp.Run(
		ctx,
		// Viewport は PNG の見え方に効く (PDF には影響しない)
		emulation.SetDeviceMetricsOverride(int64(width), int64(height), 1.0, false),
		chromedp.Navigate(dataURL),
		chromedp.WaitReady("body"),

		// PDF (A4/背景印刷あり)
		chromedp.ActionFunc(func(ctx context.Context) error {
			pdf, _, err := page.PrintToPDF().WithPrintBackground(true).
				// 用紙サイズを変える例: A4 = 8.27 x 11.69 inch
				WithPaperWidth(8.27).
				WithPaperHeight(11.69).
				Do(ctx)
			if err != nil {
				return err
			}
			pdfBytes = pdf
			return nil
		}),

		// フルページ PNG(品質 90)
		chromedp.FullScreenshot(&pngBytes, 90),
	)
	must(err)

	// 4. 保存
	must(os.WriteFile(filepath.Clean(outPDF), pdfBytes, 0o644))
	must(os.WriteFile(filepath.Clean(outPNG), pngBytes, 0o644))

	log.Printf("✅ Done. PDF=%s, PNG=%s\n", outPDF, outPNG)
}

func must(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

4. 実行

go run . \
  -tpl template.html \
  -pdf invoice.pdf \
  -png invoice.png
  • invoice.pdf(A4, 背景印刷あり)
  • invoice.png(1280×800 のフルページスクリーンショット)
    • 下記が生成された画像

7. まとめ

  • chromedp を使えば、Go から Chrome を操作して HTML → PDF/PNG を安定生成できる
  • テンプレは html/template、実体は data URL で渡すと楽
  • 画像/フォントは data URL or file:// にして解決

Discussion