PlaywrightでPDFを生成する方法|コードサンプルとAPIへの移行ガイド
Playwrightはブラウザの自動化とE2Eテストで広く使われていますが、page.pdf()を使えばHTMLページをそのままPDFに変換できます。この記事では、Playwrightを使ったPDF生成の基礎から本番運用の課題まで、実践的なコード例とともに解説します。
既存のPuppeteerからの移行を検討している方はPuppeteerからPDF APIへの移行ガイドも参考にしてください。
Playwrightでのページ設定とインストール
インストール
npm install playwright
# Chromiumブラウザをインストール(PDF生成はChromiumのみ対応)
npx playwright install chromium
基本的なPDF生成
const { chromium } = require('playwright');
async function generatePdf(htmlContent, outputPath) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// HTMLを直接セット
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
// PDF生成
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '20mm',
bottom: '20mm',
left: '15mm',
right: '15mm',
},
});
await browser.close();
return pdfBuffer;
}
// 使用例
const html = `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
body { font-family: 'Noto Sans JP', sans-serif; font-size: 14px; }
h1 { color: #1a56db; }
</style>
</head>
<body>
<h1>請求書</h1>
<p>Playwrightで生成したPDFです。</p>
</body>
</html>`;
generatePdf(html, 'output.pdf').then(buf => {
require('fs').writeFileSync('output.pdf', buf);
console.log('PDF生成完了');
});
URLからPDFを生成する
const { chromium } = require('playwright');
async function generatePdfFromUrl(url, outputPath) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// URLにナビゲート(フォント・画像の読み込みを待つ)
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
const pdfBuffer = await page.pdf({
path: outputPath, // ファイルに直接書き込む
format: 'A4',
printBackground: true,
});
await browser.close();
return pdfBuffer;
}
generatePdfFromUrl('https://example.com', 'page.pdf');
page.pdf() オプション全解説
Playwrightのpage.pdf()が受け付けるオプションを体系的に整理します。
用紙サイズと向き
const pdf = await page.pdf({
// 用紙フォーマット(letterがデフォルト)
format: 'A4', // A4, A3, Letter, Legal, Tabloid など
// または幅・高さで指定(formatより優先度が低い)
width: '210mm',
height: '297mm',
// 横向き
landscape: true,
// 印刷倍率(0.1〜2の範囲)
scale: 1.0,
});
余白の設定
const pdf = await page.pdf({
margin: {
top: '20mm',
bottom: '20mm',
left: '15mm',
right: '15mm',
},
});
背景色・背景画像の出力
// デフォルトはfalse(背景なし)
const pdf = await page.pdf({
printBackground: true, // CSSの背景色・背景画像をPDFに出力
});
背景色が出力されない場合はこのオプションを確認してください。
ヘッダーとフッター
const pdf = await page.pdf({
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center; color: #999;">
<span class="title"></span>
</div>
`,
footerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center; color: #999;">
ページ <span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
`,
// ヘッダー・フッターを使う場合、余白を大きめに
margin: { top: '40px', bottom: '40px', left: '20px', right: '20px' },
});
ヘッダー・フッターで使えるクラス:
| クラス | 出力内容 |
|---|---|
pageNumber |
現在のページ番号 |
totalPages |
総ページ数 |
date |
現在の日付 |
title |
ページタイトル(<title>タグの値) |
url |
ページのURL |
ページ範囲の指定とタグ付きPDF
const pdf = await page.pdf({
// 特定のページのみ出力(例: 1〜3ページと5ページ)
pageRanges: '1-3, 5',
// アクセシブルなタグ付きPDF(Playwright v1.42+)
tagged: true,
// PDFのアウトライン(目次のブックマーク)を埋め込む(v1.42+)
outline: true,
});
CSS printメディアクエリの活用
Playwrightは@media printスタイルをデフォルトで適用します。印刷専用のCSSを記述することでPDF出力を細かく制御できます。
/* 画面での表示 */
.sidebar {
display: block;
width: 250px;
}
/* PDF出力時はサイドバーを非表示 */
@media print {
.sidebar {
display: none;
}
/* 改ページ制御 */
.page-break {
break-before: page;
page-break-before: always; /* IE対応 */
}
/* セクション内で改ページさせない */
.invoice-section {
break-inside: avoid;
page-break-inside: avoid;
}
/* 見出しが単独でページ末尾に残らないように */
h1, h2, h3 {
break-after: avoid;
page-break-after: avoid;
}
/* 用紙サイズと余白 */
@page {
size: A4;
margin: 20mm 15mm;
}
@page :first {
margin-top: 30mm; /* 1ページ目だけ余白を広げる */
}
}
screenメディアで出力する場合
@media printではなく@media screenスタイルでPDFを出力したい場合はemulateMediaを使います:
// printメディアではなくscreenメディアを使う
await page.emulateMedia({ media: 'screen' });
const pdf = await page.pdf({ printBackground: true });
実践的なサンプル:日本語請求書PDF
const { chromium } = require('playwright');
const invoiceData = {
invoiceNumber: 'INV-2026-0042',
issueDate: '2026年4月19日',
dueDate: '2026年4月30日',
clientName: '株式会社サンプル',
items: [
{ name: 'Webシステム開発', qty: 1, unitPrice: 100000 },
{ name: 'デザイン作成', qty: 1, unitPrice: 30000 },
],
};
function buildInvoiceHtml(data) {
const subtotal = data.items.reduce((s, i) => s + i.qty * i.unitPrice, 0);
const tax = Math.round(subtotal * 0.1);
const total = subtotal + tax;
const rows = data.items.map(item => `
<tr>
<td>${item.name}</td>
<td style="text-align:right;">${item.qty}式</td>
<td style="text-align:right;">¥${item.unitPrice.toLocaleString()}</td>
<td style="text-align:right;">¥${(item.qty * item.unitPrice).toLocaleString()}</td>
</tr>
`).join('');
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Noto Sans JP', sans-serif; font-size: 13px; color: #1a1a1a; }
@page { size: A4; margin: 15mm 20mm; }
.page { max-width: 210mm; padding: 20px; }
.header { display: flex; justify-content: space-between; margin-bottom: 32px; border-bottom: 3px solid #1a56db; padding-bottom: 16px; }
.title { font-size: 32px; font-weight: 700; color: #1a56db; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th { background: #1a56db; color: #fff; padding: 10px 12px; text-align: left; }
td { padding: 10px 12px; border-bottom: 1px solid #e5e7eb; }
.total { font-size: 20px; font-weight: 700; color: #1a56db; }
</style>
</head>
<body>
<div class="page">
<div class="header">
<div class="title">請 求 書</div>
<div style="text-align:right; color:#6b7280; font-size:12px;">
<strong>${data.invoiceNumber}</strong><br>
発行日: ${data.issueDate}<br>
期限: ${data.dueDate}
</div>
</div>
<p><strong>${data.clientName} 御中</strong></p>
<table>
<thead><tr><th>品目</th><th>数量</th><th>単価</th><th>小計</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<p style="text-align:right; margin-top:16px;">小計: ¥${subtotal.toLocaleString()}</p>
<p style="text-align:right;">消費税(10%): ¥${tax.toLocaleString()}</p>
<p class="total" style="text-align:right; margin-top:8px;">合計: ¥${total.toLocaleString()}</p>
</div>
</body>
</html>`;
}
async function main() {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(buildInvoiceHtml(invoiceData), { waitUntil: 'networkidle' });
const buf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '15mm', bottom: '15mm', left: '20mm', right: '20mm' },
displayHeaderFooter: true,
footerTemplate: `<div style="font-size:9px;width:100%;text-align:center;color:#9ca3af;">
Generated by Playwright — ページ <span class="pageNumber"></span> / <span class="totalPages"></span>
</div>`,
});
require('fs').writeFileSync('invoice.pdf', buf);
await browser.close();
console.log('invoice.pdf を生成しました');
}
main();
Playwrightを本番で使う際の課題
PlaywrightによるPDF生成は手軽ですが、本番環境では以下の課題に直面します。
1. メモリ消費
Chromiumプロセスは1インスタンスあたり150〜300MBのメモリを消費します。同時リクエストが増えると線形にメモリが増加し、OOMクラッシュの原因になります。
// ブラウザのプール管理(並列数を制限)
const { chromium } = require('playwright');
class BrowserPool {
constructor(size = 3) {
this.size = size;
this.pool = [];
this.queue = [];
}
async acquire() {
if (this.pool.length < this.size) {
const browser = await chromium.launch({ headless: true });
return browser;
}
return new Promise((resolve) => this.queue.push(resolve));
}
release(browser) {
if (this.queue.length > 0) {
const next = this.queue.shift();
next(browser);
} else {
browser.close();
}
}
}
2. コールドスタート
Chromiumの起動には1〜3秒かかります。リクエストごとに起動・終了するとレイテンシが大きくなります。ブラウザを再利用(Keep-Alive)することでコールドスタートを回避できますが、メモリリークのリスクが生じます。
3. サーバーレス環境での制限
Lambda・Cloud Run・Vercel Functionsなど、サーバーレス環境ではChromiumバイナリの配置やメモリ制限に悩まされます。
| 環境 | 最大メモリ | コールドスタート | Chromium対応 |
|---|---|---|---|
| AWS Lambda | 10GB | 1〜3秒 | 要 chrome-aws-lambda |
| Cloud Run | 32GB | 数百ms | 標準Dockerイメージで可 |
| Vercel Functions | 3GB | 1〜2秒 | 制限あり(@sparticuz/chromium) |
4. フォントの問題
Linuxサーバーには日本語フォントが標準でインストールされていません。DockerfileでNoto Sans JPをインストールする例:
FROM node:20-slim
# Chromiumの依存パッケージ + 日本語フォント
RUN apt-get update && apt-get install -y \
chromium \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
# Playwright用環境変数
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]
FUNBREW PDF APIへの移行パス
PlaywrightでのPDF生成が本番規模に達したとき、FUNBREW PDF APIへの移行が現実的な選択肢になります。
なぜAPIへの移行を検討するか
| 課題 | Playwright自前 | FUNBREW PDF API |
|---|---|---|
| メモリ管理 | 自前でプール管理必要 | API側で自動スケール |
| コールドスタート | 起動に1〜3秒 | 数百ms以内(マネージド) |
| 日本語フォント | Dockerfileで手動設定 | Noto Sans JPプリインストール |
| 監視・エラー検知 | 自前で実装 | APIメトリクス・Webhookあり |
| インフラコスト | サーバー常時稼働 | 使用分だけ課金 |
| メンテナンス | Playwright/Chromiumバージョン管理 | 不要 |
移行の手順
既存のPlaywrightコードからFUNBREW PDF APIへの移行は、HTMLを生成する部分はそのままにAPI呼び出し部分だけを差し替えるだけです:
// Before: Playwright
const { chromium } = require('playwright');
async function generatePdf(html) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const buf = await page.pdf({ format: 'A4', printBackground: true });
await browser.close();
return buf;
}
// After: FUNBREW PDF API(HTMLの生成ロジックはそのまま)
async function generatePdf(html) {
const response = await fetch('https://pdf.funbrew.cloud/api/pdf/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
engine: 'quality', // Chromiumベース(Playwrightと同等品質)
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
},
}),
});
const result = await response.json();
return result.data.download_url; // PDF URL
}
移行後もHTMLテンプレートはそのまま再利用できます。Playwright固有の@media printスタイルはFUNBREW PDFでも同様に動作します。
詳細な移行手順はPuppeteerからPDF APIへの移行ガイドを参照してください(Playwrightにも同様に適用できます)。
よくある質問(FAQ)
Q: Playwrightで生成したPDFとFUNBREW PDFの出力品質は同じですか?
はい。FUNBREW PDFのqualityエンジンはChromiumベースで動作するため、Playwrightと同じレンダリングエンジンを使用しています。CSSの解釈・フォントレンダリング・改ページの挙動は同等です。
Q: Playwrightで複数ページのPDFを生成するには?
単一の長いHTMLを渡すだけで自動的に複数ページに分割されます。@pageルールで用紙サイズと余白を指定し、break-before: pageやbreak-inside: avoidで改ページを制御します。
Q: Playwright PDF生成をTypeScriptで書くには?
import { chromium } from 'playwright';
async function generatePdf(html: string): Promise<Buffer> {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const buf = await page.pdf({
format: 'A4',
printBackground: true,
});
await browser.close();
return buf;
}
まとめ
- Playwright
page.pdf()はChromiumベースで、printBackground・displayHeaderFooter・taggedなど豊富なオプションを持つ - CSS
@media printと@pageを活用して改ページ・余白・ヘッダーを細かく制御できる - 本番の課題: メモリ消費・コールドスタート・フォント設定・サーバーレス環境の制限
- スケール時の選択肢: FUNBREW PDF APIへ移行するとHTML生成ロジックを変えずにインフラ管理を省略できる
PlaygroundでブラウザからPDF出力を確認したり、APIドキュメントでFUNBREW PDFのオプションを確認してみてください。
関連記事
- PuppeteerからPDF APIへの移行ガイド — Playwright/Puppeteerからの移行手順
- HTML to PDF CSS設計テクニック集 — 改ページ・余白・テーブルのCSS
- PDF 日本語フォント完全ガイド — Noto Sans JPの設定とBase64埋め込み
- PDF API本番運用チェックリスト — 監視・スケーリングのベストプラクティス
- PDF APIエラーハンドリングガイド — リトライ戦略とエラー対応
- Playground — ブラウザでPDF出力をリアルタイム確認
- APIドキュメント — FUNBREW PDF APIの全仕様