Skip to content

PDF生成(ReportLab)


ReportLab

ReportLab は OS に依存せず、 PyPI パッケージをインストールするだけで使える。

ここでは、名前を渡すと領収書の PDF をダウンロードさせるエンドポイントを作る。

main.py

import uuid

from fastapi import FastAPI, Response
from utils import create_receipt_pdf
from config import settings

app = FastAPI()

@app.get("/generate-receipt")
async def generate_receipt(name: str):
    receipt_id = str(uuid.uuid4())

    pdf_bytes = create_receipt_pdf(
        name=name,
        amount=settings.RECEIPT_AMOUNT,
        company=settings.RECEIPT_ISSUER,
        receipt_id=receipt_id,
    )

    filename = f"receipt_{receipt_id[:8]}.pdf"

    return Response(
        content=pdf_bytes,
        media_type="application/pdf",
        headers={
            "Content-Disposition": f'attachment; filename="{filename}"'
        }
    )

リクエストごとに uuid で領収書 ID を発行して、それを PDF の中身にもファイル名(先頭 8 桁)にも使ってる。
Content-Dispositionattachment を指定すると、ブラウザで開かずにダウンロードになる。 filename*=UTF-8'' の書き方をしておけば日本語ファイル名でも文字化けしにくいけど、そもそもファイル名には日本語を使わないように指定しておくのが無難か。

config.py

設定値は pydantic-settings で管理する。
cf. 設定管理

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    RECEIPT_AMOUNT: int = 5000
    RECEIPT_ISSUER: str = "デフォルトカンパニー"

    model_config = SettingsConfigDict(env_file=".env")

settings = Settings()

.env

RECEIPT_AMOUNT=10000
RECEIPT_ISSUER="実在しない株式会社"

utils.py

PDF を組み立てる本体。 canvas.Canvas に座標を指定しながら文字や線を置いていく。

import io
import os
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.pdfbase.ttfonts import TTFont


def create_receipt_pdf(name: str, amount: int, company: str, receipt_id: str) -> bytes:
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    FONT_PATH = os.path.join(BASE_DIR, "fonts", "NotoSansJP-Regular.ttf")

    font_name = "NotoSans"
    pdfmetrics.registerFont(TTFont(font_name, FONT_PATH))

    buffer = io.BytesIO()
    p = canvas.Canvas(buffer, pagesize=A4)
    width, height = A4
    p.setTitle("Receipt")

    p.setFillColorRGB(0.6, 0.6, 0.6)
    p.setFont(font_name, 9)
    p.drawRightString(208*mm, height - 5*mm, f"領収書ID: {receipt_id}")
    p.setFillColorRGB(0, 0, 0)

    p.setFont(font_name, 24)
    p.drawCentredString(width / 2.0, height - 30*mm, "領 収 書")

    p.setFont(font_name, 12)
    p.drawString(20*mm, height - 45*mm, "受付番号: 1111")

    p.setFont(font_name, 16)
    p.drawString(20*mm, height - 55*mm, f"{name} 様")

    p.setFont(font_name, 18)
    p.drawString(20*mm, height - 70*mm, f"金額: ¥{amount:,} -")
    p.line(20*mm, height - 72*mm, 100*mm, height - 72*mm)

    p.setFont(font_name, 12)
    p.drawString(20*mm, height - 85*mm, "但: 参加費として上記正に領収いたしました。")

    p.setFont(font_name, 9)
    p.setFillColorRGB(0.3, 0.3, 0.3)
    p.drawString(20*mm, height - 100*mm, "※発行後にキャンセルされた場合、本領収書は無効になります。")
    p.setFillColorRGB(0, 0, 0)

    p.setFont(font_name, 10)
    p.drawRightString(190*mm, height - 120*mm, "2026年4月24日")
    p.drawRightString(190*mm, height - 125*mm, company)

    p.showPage()
    p.save()

    pdf_bytes = buffer.getvalue()
    buffer.close()

    return pdf_bytes

mm を使うと 20*mm のようにミリ単位で座標を指定できる。 reportlab の原点は左下なので、上からの位置は height - 30*mm のように高さから引いて指定する。

フォント

日本語を描画するには日本語フォントの登録が必要。自分で使いたいフォントがあるなら、 Noto Sans JP あたりからダウンロードして、上のコードなら、 fonts/ ディレクトリに保存しておけば使える。


生成された PDF ファイルはこんな感じ。

reportlab で生成した領収書 PDF

Response / StreamingResponse / FileResponse の使い分け

  • Response — 成果物が全部メモリにある小さいデータ( QR コードや 1 枚の PDF など)。 content=bytes で素直に返せて、ヘッダーも付けやすい。
  • StreamingResponse — ジェネレータやファイルをチャンクで逐次返すとき。全体をメモリに載せずに流せるので、大きいデータほど効く。
  • FileResponse — すでにディスクにあるファイルをそのまま返すとき。