PDFにQRコードを埋め込む方法|Node.js・Python実装ガイド
QRコードをPDFに追加するケースは、証明書の照合URL、配送伝票のトラッキング、イベントチケット、請求書確認など多岐にわたります。実装のポイントはサーバー側でQRコードをBase64データURIとして生成し、HTMLテンプレートに埋め込んでPDF APIで変換することです。
このガイドではNode.jsとPythonの実装例、複数ページPDF・一括生成・フッターへの配置パターンをコピペで使えるコードと共に解説します。
なぜBase64データURIを使うのか
PDF APIはヘッドレスブラウザ環境でHTMLをレンダリングします。ローカルファイルパス(/tmp/qr.png)は参照できないため、画像が表示されません。信頼性の高い方法は、QR画像をBase64データURIにエンコードしてHTMLに直接埋め込むことです:
<img src="data:image/png;base64,iVBORw0KGgo..." alt="QRコード" />
この方法はサーバー上・Lambda関数・Dockerコンテナのいずれの環境でも動作します。
Node.js実装
依存パッケージのインストール
npm install qrcode axios
基本的な実装例
const QRCode = require('qrcode');
const axios = require('axios');
async function generatePdfWithQr(targetUrl, outputPath) {
// Step 1: Base64データURIとしてQRコードを生成
const qrDataUri = await QRCode.toDataURL(targetUrl, {
errorCorrectionLevel: 'M',
width: 200,
margin: 2,
});
// Step 2: QRコードを埋め込んだHTMLテンプレートを作成
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: "Noto Sans JP", sans-serif; padding: 40px; }
.qr-container {
display: flex;
align-items: center;
gap: 20px;
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
}
.qr-container img { width: 120px; height: 120px; }
.qr-label { font-size: 12px; color: #555; margin-top: 8px; }
h1 { font-size: 24px; }
</style>
</head>
<body>
<h1>QRコード付きドキュメント</h1>
<p>以下のQRコードをスキャンして文書の正当性を確認できます。</p>
<div class="qr-container">
<div>
<img src="${qrDataUri}" alt="照合用QRコード" />
<p class="qr-label">スキャンして確認</p>
</div>
<div>
<strong>文書ID:</strong> DOC-2026-001<br>
<strong>発行日:</strong> 2026-04-28<br>
<strong>照合URL:</strong><br>
<small>${targetUrl}</small>
</div>
</div>
</body>
</html>
`;
// Step 3: FUNBREW PDF APIでPDFに変換
const response = await axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{ html },
{
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}`,
'Content-Type': 'application/json',
},
responseType: 'arraybuffer',
}
);
// Step 4: PDFを保存
require('fs').writeFileSync(outputPath, response.data);
console.log(`PDFを保存しました: ${outputPath}`);
}
generatePdfWithQr(
'https://verify.example.com/doc/DOC-2026-001',
'./output.pdf'
);
一括生成(バッチ処理)
複数のPDF(チケット・証明書・請求書)を生成する場合、QRコードの生成を先にまとめて並列実行しておくと効率的です:
const QRCode = require('qrcode');
const axios = require('axios');
const fs = require('fs');
const QR_OPTIONS = { errorCorrectionLevel: 'M', width: 180, margin: 2 };
async function batchGeneratePdfs(records) {
// QRコードを並列生成
const qrDataUris = await Promise.all(
records.map(r => QRCode.toDataURL(r.verifyUrl, QR_OPTIONS))
);
// 5並列でPDF生成
const results = [];
for (let i = 0; i < records.length; i += 5) {
const batch = records.slice(i, i + 5);
const batchQrs = qrDataUris.slice(i, i + 5);
const batchResults = await Promise.all(
batch.map((record, idx) => generateOnePdf(record, batchQrs[idx]))
);
results.push(...batchResults);
}
return results;
}
async function generateOnePdf(record, qrDataUri) {
const html = buildTemplate(record, qrDataUri);
const response = await axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{ html },
{
headers: { 'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}` },
responseType: 'arraybuffer',
}
);
const filename = `./output/${record.id}.pdf`;
fs.writeFileSync(filename, response.data);
return filename;
}
function buildTemplate(record, qrDataUri) {
return `
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><style>
@page { size: A4; margin: 20mm; }
body { font-family: "Noto Sans JP", sans-serif; }
.qr { width: 100px; height: 100px; float: right; }
</style></head>
<body>
<img class="qr" src="${qrDataUri}" alt="QR" />
<h2>${record.title}</h2>
<p>ID: ${record.id}</p>
<p>宛先: ${record.recipientName}</p>
</body></html>
`;
}
Python実装
依存パッケージのインストール
pip install qrcode[pil] requests
基本的な実装例
import qrcode
import base64
import io
import requests
import os
def generate_pdf_with_qr(target_url: str, output_path: str) -> None:
"""QRコード付きPDFを生成する"""
# Step 1: QRコードを生成
qr = qrcode.QRCode(
version=None,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=8,
border=2,
)
qr.add_data(target_url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# Step 2: Base64データURIに変換
buffer = io.BytesIO()
img.save(buffer, format="PNG")
qr_b64 = base64.b64encode(buffer.getvalue()).decode()
qr_data_uri = f"data:image/png;base64,{qr_b64}"
# Step 3: HTMLテンプレートを構築
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: "Noto Sans JP", sans-serif; padding: 40px; }}
.qr-section {{
display: flex;
align-items: center;
gap: 24px;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}}
.qr-section img {{ width: 120px; height: 120px; }}
p {{ margin: 4px 0; }}
</style>
</head>
<body>
<h1>QRコード付きドキュメント</h1>
<div class="qr-section">
<img src="{qr_data_uri}" alt="QRコード" />
<div>
<p><strong>スキャンして文書を確認</strong></p>
<p>URL: {target_url}</p>
<p>発行日: 2026-04-28</p>
</div>
</div>
</body>
</html>
"""
# Step 4: FUNBREW PDF APIでPDFに変換
response = requests.post(
"https://pdf.funbrew.cloud/api/v1/generate",
json={"html": html},
headers={
"Authorization": f"Bearer {os.environ['FUNBREW_API_KEY']}",
"Content-Type": "application/json",
},
)
response.raise_for_status()
with open(output_path, "wb") as f:
f.write(response.content)
print(f"PDFを保存しました: {output_path}")
if __name__ == "__main__":
generate_pdf_with_qr(
"https://verify.example.com/doc/DOC-2026-001",
"output.pdf",
)
非同期バッチ処理(Python)
import asyncio
import aiohttp
import qrcode
import base64
import io
import os
QR_CONFIG = {
"error_correction": qrcode.constants.ERROR_CORRECT_M,
"box_size": 7,
"border": 2,
}
def make_qr_data_uri(url: str) -> str:
qr = qrcode.QRCode(**QR_CONFIG)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO()
img.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode()
return f"data:image/png;base64,{b64}"
async def generate_one(session: aiohttp.ClientSession, record: dict) -> str:
qr_uri = make_qr_data_uri(record["verify_url"])
html = f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head>
<body>
<img src="{qr_uri}" width="100" height="100" />
<h2>{record['title']}</h2>
<p>宛先: {record['name']}</p>
</body></html>"""
async with session.post(
"https://pdf.funbrew.cloud/api/v1/generate",
json={"html": html},
headers={"Authorization": f"Bearer {os.environ['FUNBREW_API_KEY']}"},
) as resp:
resp.raise_for_status()
pdf_bytes = await resp.read()
path = f"./output/{record['id']}.pdf"
with open(path, "wb") as f:
f.write(pdf_bytes)
return path
async def batch_generate(records: list) -> list:
semaphore = asyncio.Semaphore(5) # 最大5並列
async def limited(session, record):
async with semaphore:
return await generate_one(session, record)
async with aiohttp.ClientSession() as session:
tasks = [limited(session, r) for r in records]
return await asyncio.gather(*tasks)
PDFのヘッダー・フッターにQRコードを配置する
複数ページPDFで全ページにQRコードを表示するには、headerTemplateまたはfooterTemplateにQRコードのデータURIを埋め込みます。コンプライアンス文書で各ページを個別に照合できる必要がある場合に便利です:
const QRCode = require('qrcode');
const axios = require('axios');
async function generateWithQrFooter(html, verifyUrl) {
const qrDataUri = await QRCode.toDataURL(verifyUrl, {
errorCorrectionLevel: 'M',
width: 80,
margin: 1,
});
const footerTemplate = `
<div style="
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20mm;
font-size: 9pt;
font-family: Arial, sans-serif;
color: #555;
">
<span>ページ <span class="pageNumber"></span> / <span class="totalPages"></span></span>
<img src="${qrDataUri}" style="width: 25mm; height: 25mm;" alt="照合QR" />
</div>
`;
return axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{
html,
displayHeaderFooter: true,
footerTemplate,
marginTop: '15mm',
marginBottom: '35mm', // QRフッターのスペースを確保
},
{
headers: { 'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}` },
responseType: 'arraybuffer',
}
);
}
QRコードサイズの目安
| 用途 | 推奨サイズ | エラー訂正レベル |
|---|---|---|
| 文書照合 | 25〜30mm | M(15%) |
| イベントチケット | 30〜40mm | Q(25%) |
| 証明書(A4) | 30〜35mm | M(15%) |
| 請求書コーナー | 20〜25mm | M(15%) |
| ページフッター | 20〜25mm | L(7%) |
小さいサイズや低品質プリントが想定される場合はエラー訂正レベルQ(25%)を使用してください。L(7%)は大判・高品質印刷専用です。
よくある問題と対処法
PDFでQRコードが表示されない
原因: QRコードをファイルパス(/tmp/qr.png)で参照している
対処: QRCode.toDataURL()(Node.js)またはbase64.b64encode()(Python)でBase64に変換し、<img src>属性に直接埋め込む
QRコードが読み取れない
原因: レンダリングサイズが小さすぎる、またはクワイエットゾーン(マージン)が不足している
対処: ソース画像のwidthを150px以上に設定し、marginを2以上に保つ。印刷サイズは最低でも20mm以上を確保する
QRコードがぼやけている
原因: ソースPNGの解像度が低い
対処: width: 300以上で生成し、CSS(width: 30mm)で表示サイズを制限する。PDF レンダラーがダウンサンプリングしてくれる
証明書での応用例
QRコード照合と一括証明書生成の組み合わせは最も一般的な本番パターンです。受講者CSVのインポート・HTMLテンプレート変数置換・S3アーカイブを含む完全な実装はPDF証明書自動生成ガイドを参照してください。
各証明書のQRコードは、証明書IDを受け取って有効性を返す照合エンドポイントを指すようにします:
https://verify.example.com/cert/{certificateId}
certificateIdはメールアドレスと発行日のハッシュから決定論的に生成するとDB参照なしで再現できます。
次のステップ
- PDF証明書自動生成ガイド — QR照合付き一括証明書生成
- HTML→PDF CSSが効かない?@page・改ページ・背景色の即効スニペット20+ — 正確なレイアウト制御のためのprint CSS
- PDF API バッチ処理ガイド — 大量PDF生成の並列処理パターン
- FUNBREW PDF Playground — コードを書く前にQR埋め込みHTMLをテスト