HTMLフォームをPDF化する:申請書・契約書・アンケートをAPIで変換
申請書、入力フォーム、行政手続き、保険の引受書類、クライアントとの契約書——HTMLフォームはあらゆる業務に登場します。ユーザーがフォームを送信した後の定番処理は「署名・保存可能なPDFを出力する」ことです。ブラウザの印刷ダイアログを使う方法はブラウザごとに出力が変わり、Puppeteerをサーバーで動かす方法はデプロイに300MBのバイナリが増えます。PDF生成APIはこの二つの問題を解消します——HTMLを送るだけで、ピクセルパーフェクトなPDFが返ってきます。
このガイドではFUNBREW PDF APIを使ってHTMLフォームをPDFに変換する方法を解説します。申請書・契約書・アンケートという典型的なフォーム3種類に対して、Node.js・Python・PHPの動くコードを提供します。
まずはアカウント不要で試せるプレイグラウンドでサンプルの変換を体験してみてください。
なぜブラウザ印刷でなくAPIを使うか
| 方法 | レイアウト再現性 | サーバーサイド | CJKフォント | 本番運用 |
|---|---|---|---|---|
| ブラウザ印刷ダイアログ | ブラウザによって異なる | No | 環境依存 | No |
| Puppeteer(自己ホスト) | 優秀 | Yes | 手動設定 | 複雑 |
| wkhtmltopdf | 中程度 | Yes | 限定的 | 開発停滞 |
| FUNBREW PDF API | 優秀 | Yes | 組み込み | Yes |
フォームPDF生成APIの最大の利点は、フォントや色、改ページ、マージンをサーバーが完全にコントロールできることです。エンドユーザーがどのブラウザ・OSを使っていても生成結果が同一になります。
各手法の詳細な比較はHTMLをPDFに変換する完全ガイドをご覧ください。
基本フロー:入力値をHTMLに埋め込んでから変換
フォームの種類を問わず、ワークフローは常に同じです。
- ユーザーがフォームに入力して送信
- バックエンドが送信値を埋め込んだHTMLを組み立てる
- FUNBREW PDF APIにHTMLをPOSTする
- 返却されたPDFバイト列をストリームまたは保存する
ユーザー → フォーム送信 → バックエンド → FUNBREW PDF API → PDFバイト列 → 保存またはレスポンス
送るHTMLはキーバリューの <table> でも、カスタムフォントやヘッダー付きの複数ページドキュメントでも構いません。ChromiumブラウザがレンダリングするのとまったくHtml同じ結果が得られます。
クイックスタート:最初のフォームをPDF化
# HTMLフォームをPDFに変換するワンライナー
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>申請書</h1><p><strong>氏名:</strong>山田 太郎</p><p><strong>メール:</strong>yamada@example.com</p>",
"engine": "quality",
"format": "A4",
"margin": { "top": "20mm", "bottom": "20mm", "left": "20mm", "right": "20mm" }
}' \
--output application.pdf
APIキーはダッシュボードから発行できます。まず試すだけならプレイグラウンドが便利です。
フォームタイプ1:申請書・登録フォーム
入学届、求職申込書、許可申請書など——申請書には一貫したフォーマットとアーカイブ性が求められます。
Node.js 実装例
// generate-application-pdf.js
import { htmlToPdf } from './pdf-client.js'; // pdf-generation-nodejs-guideを参照
function buildApplicationHtml(data) {
const {
applicantName,
email,
phone,
dateOfBirth,
address,
notes = '',
submittedAt = new Date().toLocaleString('ja-JP'),
} = data;
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 20mm 15mm; }
* { box-sizing: border-box; }
body {
font-family: 'Noto Sans JP', 'Hiragino Sans', sans-serif;
font-size: 12px;
color: #1e293b;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.header {
border-bottom: 3px solid #2563eb;
padding-bottom: 12px;
margin-bottom: 24px;
}
h1 { font-size: 22px; color: #2563eb; margin: 0 0 4px; }
.meta { color: #64748b; font-size: 11px; }
.section { margin-bottom: 20px; }
.section-title {
font-size: 13px;
font-weight: 700;
color: #475569;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 4px;
margin-bottom: 10px;
}
.field-row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 4px;
padding: 6px 0;
border-bottom: 1px solid #f1f5f9;
}
.field-label { font-weight: 600; color: #64748b; }
.notes-box {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 4px;
padding: 10px;
min-height: 60px;
white-space: pre-wrap;
}
.footer {
margin-top: 32px;
font-size: 10px;
color: #94a3b8;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<h1>申請書</h1>
<div class="meta">受付番号: APP-${Date.now()} | 送信日時: ${submittedAt}</div>
</div>
<div class="section">
<div class="section-title">申請者情報</div>
<div class="field-row"><span class="field-label">氏名</span><span>${applicantName}</span></div>
<div class="field-row"><span class="field-label">メール</span><span>${email}</span></div>
<div class="field-row"><span class="field-label">電話番号</span><span>${phone}</span></div>
<div class="field-row"><span class="field-label">生年月日</span><span>${dateOfBirth}</span></div>
<div class="field-row"><span class="field-label">住所</span><span>${address}</span></div>
</div>
<div class="section">
<div class="section-title">備考</div>
<div class="notes-box">${notes || '—'}</div>
</div>
<div class="footer">
このドキュメントは自動生成されました。お問い合わせは support@example.com まで
</div>
</body>
</html>`;
}
// Expressルートの例
app.post('/api/applications/submit', async (req, res) => {
const formData = req.body; // バリデーション済みの前提
try {
const html = buildApplicationHtml(formData);
const pdf = await htmlToPdf(html, {
engine: 'quality',
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
// オプションA: クライアントにPDFを直接返す
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="application-${Date.now()}.pdf"`,
'Content-Length': pdf.length,
});
res.send(pdf);
// オプションB: S3に保存してURLを返す(pdf-generation-nodejs-guide参照)
} catch (err) {
res.status(500).json({ error: 'PDF生成失敗', detail: err.message });
}
});
Python 実装例
# generate_application_pdf.py
import os
import requests
from datetime import datetime
FUNBREW_API_KEY = os.environ["FUNBREW_API_KEY"]
FUNBREW_API_URL = os.environ.get("FUNBREW_API_URL", "https://api.pdf.funbrew.cloud/v1")
def build_application_html(data: dict) -> str:
submitted_at = datetime.now().strftime("%Y年%m月%d日 %H:%M")
return f"""<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: 'Noto Sans JP', sans-serif; font-size: 12px;
color: #1e293b; padding: 30px; }}
h1 {{ color: #2563eb; border-bottom: 2px solid #2563eb; padding-bottom: 8px; }}
.row {{ display: grid; grid-template-columns: 120px 1fr; padding: 6px 0;
border-bottom: 1px solid #f1f5f9; }}
.lbl {{ font-weight: 600; color: #64748b; }}
</style>
</head>
<body>
<h1>申請書</h1>
<p style="color:#64748b;font-size:11px">送信日時: {submitted_at}</p>
<div class="row"><span class="lbl">氏名</span><span>{data['applicantName']}</span></div>
<div class="row"><span class="lbl">メール</span><span>{data['email']}</span></div>
<div class="row"><span class="lbl">電話番号</span><span>{data['phone']}</span></div>
<div class="row"><span class="lbl">住所</span><span>{data['address']}</span></div>
<div style="margin-top:16px">
<strong>備考:</strong>
<div style="background:#f8fafc;border:1px solid #e2e8f0;padding:10px;margin-top:6px">
{data.get('notes', '—')}
</div>
</div>
</body>
</html>"""
def html_form_to_pdf(form_data: dict) -> bytes:
html = build_application_html(form_data)
resp = requests.post(
f"{FUNBREW_API_URL}/pdf/from-html",
headers={"Authorization": f"Bearer {FUNBREW_API_KEY}"},
json={
"html": html,
"engine": "quality",
"format": "A4",
"margin": {"top": "20mm", "bottom": "20mm", "left": "15mm", "right": "15mm"},
},
timeout=30,
)
resp.raise_for_status()
return resp.content
# 使い方
if __name__ == "__main__":
form = {
"applicantName": "山田 太郎",
"email": "yamada@example.com",
"phone": "090-1234-5678",
"address": "東京都渋谷区渋谷1-1-1",
"notes": "地域の紹介プログラム経由で応募しました。",
}
pdf_bytes = html_form_to_pdf(form)
with open("application.pdf", "wb") as f:
f.write(pdf_bytes)
print(f"PDF保存完了: {len(pdf_bytes):,} バイト")
フォームタイプ2:契約書・同意書
契約書には署名欄と、下書き状態を示すウォーターマークが必要なことがあります。どちらもHTMLで簡単に実現できます。
PHP実装例(署名欄付き)
<?php
// generate_contract_pdf.php
function buildContractHtml(array $data): string
{
$clientName = htmlspecialchars($data['clientName']);
$providerName = htmlspecialchars($data['providerName']);
$serviceDesc = htmlspecialchars($data['serviceDescription']);
$startDate = htmlspecialchars($data['startDate']);
$value = number_format($data['contractValue']);
$isDraft = $data['isDraft'] ?? false;
$generatedAt = date('Y年m月d日 H:i');
$watermark = $isDraft
? '<div style="position:fixed;top:40%;left:50%;transform:translate(-50%,-50%) rotate(-30deg);
font-size:100px;color:rgba(239,68,68,0.12);font-weight:900;
white-space:nowrap;pointer-events:none;z-index:9999;">下書き</div>'
: '';
return <<<HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 25mm 20mm; }
body {
font-family: 'Noto Serif JP', 'Times New Roman', serif;
font-size: 11px;
color: #1e293b;
line-height: 1.8;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
h1 { font-size: 18px; text-align: center; margin-bottom: 4px; }
.subtitle { text-align: center; font-size: 11px; color: #64748b; margin-bottom: 32px; }
.clause { margin-bottom: 16px; }
.clause-title { font-weight: 700; margin-bottom: 4px; }
.sig-section {
margin-top: 48px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
.sig-block { border-top: 1px solid #1e293b; padding-top: 8px; }
.sig-label { font-size: 10px; color: #64748b; }
</style>
</head>
<body>
{$watermark}
<h1>業務委託契約書</h1>
<div class="subtitle">生成日時: {$generatedAt}</div>
<div class="clause">
<div class="clause-title">第1条(当事者)</div>
<p>本契約は、<strong>{$providerName}</strong>(以下「受託者」)と
<strong>{$clientName}</strong>(以下「委託者」)の間で締結されます。</p>
</div>
<div class="clause">
<div class="clause-title">第2条(業務内容)</div>
<p>{$serviceDesc}</p>
</div>
<div class="clause">
<div class="clause-title">第3条(期間・報酬)</div>
<p>業務開始日:<strong>{$startDate}</strong>。
契約金額:<strong>¥{$value}</strong>(税別)。</p>
</div>
<div class="clause">
<div class="clause-title">第4条(準拠法)</div>
<p>本契約は日本法に準拠し、解釈されるものとします。</p>
</div>
<div class="sig-section">
<div class="sig-block">
<p> </p>
<div class="sig-label">受託者署名:{$providerName}</div>
<div class="sig-label">日付:___________________</div>
</div>
<div class="sig-block">
<p> </p>
<div class="sig-label">委託者署名:{$clientName}</div>
<div class="sig-label">日付:___________________</div>
</div>
</div>
</body>
</html>
HTML;
}
function htmlFormToPdf(array $formData): string
{
$html = buildContractHtml($formData);
$ch = curl_init('https://api.pdf.funbrew.cloud/v1/pdf/from-html');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $_ENV['FUNBREW_API_KEY'],
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'html' => $html,
'engine' => 'quality',
'format' => 'A4',
'margin' => ['top' => '25mm', 'bottom' => '25mm', 'left' => '20mm', 'right' => '20mm'],
]),
]);
$pdf = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("PDF API returned HTTP $status");
}
return $pdf;
}
// 使い方
$formData = [
'clientName' => '株式会社サンプル',
'providerName' => '合同会社マイエージェンシー',
'serviceDescription' => 'Webサイトのリニューアルおよび継続的な保守。',
'startDate' => '2026年6月1日',
'contractValue' => 1250000,
'isDraft' => true,
];
$pdfBytes = htmlFormToPdf($formData);
file_put_contents('contract-draft.pdf', $pdfBytes);
echo "保存完了: " . strlen($pdfBytes) . " バイト\n";
フォームタイプ3:アンケート・調査回答
アンケートのPDF化はHR・調査・コンプライアンス業務でよく登場します。動的な質問・回答ペアをきれいにレンダリングするのがポイントです。
// generate-survey-pdf.js
function buildSurveyHtml(survey) {
const { title, respondentName, submittedAt, answers = [] } = survey;
const answerRows = answers.map((qa, i) => `
<div class="qa-block">
<div class="question">Q${i + 1}. ${qa.question}</div>
<div class="answer">${Array.isArray(qa.answer)
? '<ul>' + qa.answer.map(a => `<li>${a}</li>`).join('') + '</ul>'
: qa.answer || '<em>(未回答)</em>'
}</div>
</div>
`).join('');
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 20mm 15mm; }
body { font-family: 'Noto Sans JP', sans-serif; font-size: 12px;
color: #1e293b; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
h1 { color: #2563eb; font-size: 20px; margin-bottom: 4px; }
.meta { font-size: 11px; color: #64748b; margin-bottom: 24px; }
.qa-block { margin-bottom: 16px; break-inside: avoid; }
.question { font-weight: 700; background: #f1f5f9; padding: 6px 10px;
border-left: 3px solid #2563eb; margin-bottom: 4px; }
.answer { padding: 6px 10px 6px 14px; }
ul { margin: 0; padding-left: 20px; }
</style>
</head>
<body>
<h1>${title}</h1>
<div class="meta">
回答者: <strong>${respondentName}</strong>
| 送信日時: ${submittedAt}
</div>
${answerRows}
</body>
</html>`;
}
// 使い方
const survey = {
title: '従業員満足度調査 2026年Q2',
respondentName: '佐藤 花子',
submittedAt: new Date().toLocaleString('ja-JP'),
answers: [
{ question: '職場環境への満足度を教えてください', answer: '4 / 5' },
{ question: 'よく利用している福利厚生を選んでください', answer: ['健康保険', 'リモートワーク', '学習手当'] },
{ question: '改善してほしいことは何ですか?', answer: '非同期コミュニケーションのガイドラインをもっと整備してほしいです。' },
],
};
const html = buildSurveyHtml(survey);
const pdf = await htmlToPdf(html, { engine: 'quality', format: 'A4' });
多数の回答を一括でPDF化する場合はバッチ処理ガイドを参照してください。
フォームPDFのスタイリングのコツ
改ページの制御
フォームPDFで最もよくあるレイアウトバグは、質問と回答が別ページに分かれてしまうことです。CSSで防止できます。
.qa-block, .field-row, .clause {
break-inside: avoid; /* ブロック内での分割を防ぐ */
page-break-inside: avoid; /* 旧エンジン用 */
}
@page ルール、ヘッダー/フッター、ページ番号の完全なリファレンスはHTML to PDF CSSのコツを参照してください。
色の正確な再現
ブラウザは印刷時に色補正を適用します。背景色や画像をPDFで正しくレンダリングするには以下が必要です。
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
2カラムレイアウト
氏名・日付・電話番号など短いフィールドが多い場合は2カラムレイアウトでスペースを節約できます。
.field-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
CSS GridはFUNBREW PDFのChromiumエンジン(engine: "quality")で動作します。fastエンジン(engine: "fast")を使う場合はテーブルベースのレイアウトを推奨します。fastエンジン固有のCSSのコツはwkhtmltopdf CSSガイドを参照してください。
エラーハンドリング
async function safeFormToPdf(html, options = {}) {
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await htmlToPdf(html, options);
} catch (err) {
if (attempt === maxRetries || err.response?.status < 500) throw err;
const delay = 1000 * 2 ** (attempt - 1); // 1秒、2秒、4秒
console.warn(`[PDF] リトライ ${attempt}/${maxRetries}、${delay}ms後 — ${err.message}`);
await new Promise(r => setTimeout(r, delay));
}
}
}
包括的なエラーハンドリングとリトライパターンはPDF APIエラーハンドリングガイドを参照してください。
生成したフォームPDFの保存と提供
PDF生成後は通常ストレージに保存します。
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'ap-northeast-1' });
async function storeFormPdf(pdfBuffer, formId) {
const key = `forms/${new Date().toISOString().slice(0, 10)}/${formId}.pdf`;
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: pdfBuffer,
ContentType: 'application/pdf',
ServerSideEncryption: 'AES256',
Metadata: { formId },
}));
return `https://${process.env.S3_BUCKET}.s3.ap-northeast-1.amazonaws.com/${key}`;
}
署名付きURLを含むストレージ・取得パターンの詳細はPDF生成 Node.jsガイドを参照してください。
よくある質問
サーバーなしでHTMLフォームをPDFに変換できますか?
一貫した結果を得るにはサーバーサイドのステップが必要です。クライアントサイドの方法(ブラウザ印刷、jsPDF)はブラウザごとに出力が異なりCSSを正確に再現できません。どのバックエンド言語からでもFUNBREW PDF APIへの1行のHTTPリクエストが最速の方法です。
HTMLを送る代わりにライブのWebフォームのURLを指定できますか?
はい。FUNBREW PDF APIはHTMLの文字列の代わりにURLも受け付けます。Chromiumと同じようにページを読み込むため、JavaScript で動的にレンダリングされるフォームにも対応しています。リクエスト本文の html フィールドの代わりに url フィールドを使用してください。
フィルイン可能なPDFフォーム(AcroForm)を生成できますか?
APIは入力値を埋め込んだHTMLから静的なPDFを生成します。AcroForm形式のフィールドが必要な場合は、フォームフィールドに入力値を埋め込んだ状態でHTMLをレンダリングしてください。生成されるPDFはユーザーの送信内容の記録になり、アーカイブ用途には十分です。
送れるHTMLの最大サイズは?
APIは数MB規模のHTMLを受け付けます。画像が多い大規模なフォームの場合、画像をbase64のデータURIとして埋め込むか、公開URLで参照するのが確実です(<img src="https://...">)。
契約書に署名画像を追加するには?
フロントエンドのcanvas要素で署名を取得してbase64エンコードされたPNG形式で送り、HTMLに <img src="data:image/png;base64,<...>"> として埋め込みます。APIは元の解像度でレンダリングします。
まとめ
| フォームタイプ | 主なHTMLパターン | 重要なCSS |
|---|---|---|
| 申請書・登録フォーム | display:grid フィールド行 |
break-inside:avoid |
| 契約書・同意書 | 条項divと署名グリッド | position:fixed ウォーターマーク |
| アンケート・調査 | 配列からQ&Aブロックを動的生成 | ブロックごとの break-inside:avoid |
フォームをPDFに変換するパターンはAPIコール1つに集約されます——入力値を埋め込んだHTMLを送って、PDFを受け取る。FUNBREW PDF APIはフォント・日本語文字・レイアウトの正確さを、ローカルのブラウザやバイナリなしで処理します。
関連記事
- HTMLをPDFに変換する完全ガイド — 全変換手法の比較
- HTML to PDF CSSのコツ — 改ページ、ヘッダー/フッター、印刷CSS
- PDF生成 Node.jsガイド — Express・Lambda・Edgeのパターン
- PDF請求書テンプレートガイド — 請求書ドキュメントのベストプラクティス
- PDF APIバッチ処理 — 大量フォームPDFの効率的な生成
- PDF APIエラーハンドリング — リトライ・タイムアウト・フォールバックパターン
- ユースケース — PDF生成の実際の活用シナリオ