HTMLからPDF生成Lambdaを作る
はじめに
昨今ペーパーレスペーパーレス言ってますね。と言いつつも紙の文化はまだまだ健在ですが。
今回はHTMLで作成された請求書をPDFにしてダウンロードする機能をAWS Lambdaに実装したいと思います。
Excelとかで請求書はよく使われるイメージですが、HTMLのほうが管理も楽でデザインも柔軟なので個人的には、というかエンジニア的には推していきたいところ。
対象読者
- AWS Lambdaを使ったことがある方
- Pythonを使ったことがある方
- Dockerを使ったことがある方
- AWS Lambdaで日本語HTMLのPDFを生成したい方
技術スタック
- Python 3.11
- AWS Lambda
- AWS SAM CLI
- Jinja2
- WeasyPrint
- Docker
概要
Flask
のHTMLテンプレートエンジンとしても採用されているJinja2
ライブラリを使用して、請求書HTMLを作成します。
作成したHTMLからWeasyPrint
ライブラリを使用してPDFを生成します。
AWS Lambdaの関数URLを使用したAPIとして実装しています。
全体的な流れとしては
- 請求書情報を取得
- 請求書HTMLに1の情報を埋め込み
- 2をPDFに変換
- 3をBase64変換してレスポンスを返却
今回のソースコードはGithubに置いてあるので動かしてみたい方はぜひ。
SAM CLI
ですぐデプロイできます。
コード解説
InvoiceService
InvoiceService
は、顧客と会社の情報を持つInvoice
オブジェクトを生成します。
今回はサンプルなのでハードコーディングしていますが、想定されるケースはアプリで使っているデータソースから請求書情報を取得するイメージです。
from dataclasses import dataclass
@dataclass(frozen=True)
class Item:
name: str
price: int
quantity: int
@dataclass(frozen=True)
class Customer:
name: str
postal_code: str
address: str
@dataclass(frozen=True)
class Company:
name: str
postal_code: str
address: str
invoice_number: int
registration_number: str
bank_name: str
bank_branch_name: str
bank_no: str
@dataclass(frozen=True)
class Invoice:
issued_date: str
customer: Customer
company: Company
items: list[Item]
total: int
class InvoiceService:
def get(self) -> Invoice:
customer = Customer(
name="株式会社サンプル",
postal_code="123-4567",
address="東京都新宿区1-2-3"
)
company = Company(
name="株式会社PDF",
HtmlService
HtmlService
は、Invoice
オブジェクトを受け取り、Jinja2テンプレートを使用してHTMLを生成します。やってることはasdictにして渡してるだけです。
HTMLファイルはsrc/template
配下に置いてあり、そこから今回の請求書のHTMLであるinvoive.html
を読み込んでいます。
number_format
は数値をカンマ区切りにする関数で、Jinja2のフィルターに登録して、金額を見やすくするために使います。
from dataclasses import asdict
from typing import Final
from jinja2 import Environment, FileSystemLoader
from invoice_service import Invoice
def number_format(price: int) -> str:
return "{:,}".format(price)
class HtmlService:
def __init__(self):
self._env: Final = Environment(loader=FileSystemLoader("./templates"))
self._env.filters["number_format"] = number_format
def render_invoice(self, invoice: Invoice) -> str:
template = self._env.get_template("invoice.html")
params = asdict(invoice)
html = template.render(params)
return html
PdfService
PdfService
はHTML文字列を受け取ってPDFのバイナリを作成して返却します。
WeasyPrint
のHTMLクラスを使って文字列を変換する際に、引数としてファイルパスを渡せばファイルにバイナリを書き込み、渡さなければバイナリを返却してくれます。
今回はファイルには書き込まないのでNoneチェックだけして返しています。
from weasyprint import HTML
class PdfService:
def create_from_html(self, content: str) -> bytes:
pdf = HTML(string=content,
encoding="utf-8").write_pdf()
if not pdf:
raise ValueError("PDF generation failed")
return pdf
lambda_handler
上記で説明した各種サービスを使ってPDFを生成してクライアントにレスポンスを返却します。
ヘッダー部分の'Content-Disposition': 'attachment; filename=invoice.pdf'
はハードコーディングしていますが、filename=invoice.pdf
の部分はダウンロードするPDFによって可変にするのがいいと思います。
import base64
from html_service import HtmlService
from invoice_service import InvoiceService
from pdf_service import PdfService
html_service = HtmlService()
invoice_service = InvoiceService()
pdf_service = PdfService()
def lambda_handler(event, context):
invoice = invoice_service.get()
html = html_service.render_invoice(invoice)
pdf = pdf_service.create_from_html(html)
pdf_base64 = base64.b64encode(pdf).decode("utf-8")
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/pdf",
"Content-Disposition": "attachment; filename=invoice.pdf"
},
"body": pdf_base64,
"isBase64Encoded": True
}
Dockerfile
Dockerfile
では、AWS LambdaのPython3.11イメージをベースに使っています。
以下の処理を行います。
- pangoのインストール
-
WeasyPrint
を使う上でpango
が必要です -
pango
は多言語を処理するためのライブラリみたいです。
-
- 日本語フォントのインストール
- 今回はBIZ UDGothicを使用します
-
/usr/share/fonts
配下に配置することでfontconfigが認知します
- ソースコードをLambdaのタスクルートにコピー
- 依存モジュールをpipでインストール
FROM public.ecr.aws/lambda/python:3.11
RUN yum -y update && yum install -y pango curl
RUN curl https://raw.githubusercontent.com/googlefonts/morisawa-biz-ud-gothic/main/fonts/ttf/BIZUDGothic-Bold.ttf -o /usr/share/fonts/BIZUDGothic-Bold.ttf \
& curl https://raw.githubusercontent.com/googlefonts/morisawa-biz-ud-gothic/main/fonts/ttf/BIZUDGothic-Regular.ttf -o /usr/share/fonts/BIZUDGothic-Regular.ttf \
& curl https://raw.githubusercontent.com/googlefonts/morisawa-biz-ud-gothic/main/fonts/ttf/BIZUDPGothic-Bold.ttf -o /usr/share/fonts/BIZUDPGothic-Bold.ttf \
& curl https://raw.githubusercontent.com/googlefonts/morisawa-biz-ud-gothic/main/fonts/ttf/BIZUDPGothic-Regular.ttf -o /usr/share/fonts/BIZUDPGothic-Regular.ttf
COPY src $LAMBDA_TASK_ROOT/
WORKDIR $LAMBDA_TASK_ROOT
RUN pip install -r requirements.txt
CMD ["app.lambda_handler"]
デプロイ
AWS SAM CLIを使用してデプロイします。
Docker Imageを使用したLambdaのデプロイで、特に解説するほどではないので今回は割愛します。
READMEを参照していただければと思います。
sam deploy
でデプロイ後、出力された関数URLをコピーしてブラウザでアクセスしてみましょう。
CloudFormation outputs from deployed stack
------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key FunctionUrl
Description -
Value https://xxxxxx.lambda-url.xxxxxx.on.aws/
------------------------------------------------------------------------------------------------------------------------------------------------------------------
関数URLにアクセス
以下のPDFがダウンロードされました。
HTMLはGithub Copilot
君に適当につくってもらった質素な請求書です。
最後に
今回は簡単なチュートリアル目的だったので関数URLを使用していますが、実際にアプリに組み込んだりと考えるとAPI Gateway
などをCognito
のオーソライザーなどで認証つけてあげれば結構Productionでも使える代物になるんじゃないかと思いました。
帳票出力系は意外とアプリででてくる部分だと思いますのでこういうミニマムな解決策もありかと。
みなさんも素敵なペーパーレスライフをお送りください。
Discussion