2026/04/28

PDFにQRコードを埋め込む方法|Node.js・Python実装ガイド

QRコードPDF生成Node.jsPython

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参照なしで再現できます。

次のステップ

Powered by FUNBREW PDF