はじめに
PythonでPDFを生成するライブラリReportLabで、
標準選択できるフォント以外のフォントを使う際に一筋縄では出来なかったので、
解決方法を残します。
LambdaのデプロイはAWS SAMを使っています。他の方法でもやり方は同じです。
LambdaやLambda Layerのデプロイ方法については本記事では解説しませんので、他の記事を参照ください。
解決コード
任意のフォントを登録→最終的にS3にプットするまでのコードです。
コードだけ知りたい、という方はこちらを参考にしてください。
import io
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import mm
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
import boto3
FONT_NAME = "GenJyuuGothic"
FONT_PATH = "/opt/python/fonts/GenJyuuGothic-Medium.ttf"
# フォントの登録 エラー回避のために同じフォント名で登録されていないかチェック
if FONT_NAME not in pdfmetrics.getRegisteredFontNames():
pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
def export_pdf():
# キャンバスを用意 メモリ上に書き込むようにする
buffer = io.BytesIO()
pdf_canvas = canvas.Canvas(buffer, pagesize=A4)
# フォントをセット 第1引数に登録したフォント名を指定
pdf_canvas.setFont(FONT_NAME, 10)
# 文字を描画
pdf_canvas.drawString(10 * mm, 10 * mm, "サンプル")
# 保存
pdf_canvas.save()
# 読み込み位置をリセット
buffer.seek(0)
# バイトデータを取得
pdf_data = buffer.getvalue()
# S3に格納
s3 = boto3.client("s3")
s3.put_object(Bucket=bucket_name, Key=key, Body=pdf_data)
詳しい解説をしていきます。
ReportLabとは
PythonでPDFを生成するライブラリです。
他にもPDF生成のライブラリはいろいろありますが、ネット上の情報が多く扱いやすいです。「reportlab 使い方」とかでググるといっぱい記事が出てきます。
白紙の状態からPDFを生成できるので、自由にレイアウトを作れるのがポイントです。
環境
- Lambdaランタイム: Python3.12
- ReportLab: 4.2.0
解決方法
流れとしては以下になります。
- 使用したいフォントのttfファイル(拡張子
.ttf
)を用意する - ttfファイルをデプロイするLambdaレイヤーに含める
- レイヤー内のttfファイルを関数から読み込む
3につまづいて解決に2時間くらい格闘しました。順番に解説します
使用したいフォントのttfファイルを用意する
わたしは「源柔ゴシック」というフォントを使いたかったので、
以下のサイトからzipファイルをダウンロード
→展開
→太さや微妙なスタイルの違いで複数ファイルがあります。わたしはGenJyuuGothic-Medium.ttf
を使いました。
ttfファイルをデプロイするLambdaレイヤーに含める
レイヤーのフォルダ配下にfonts
フォルダを作り、その中にttf
ファイルを配置します。
common_layer
がレイヤーフォルダの名前です(なんでもいいです)。
レイヤーフォルダ直下でなくても問題ないです。
この状態でデプロイするとレイヤーにフォントファイルが含まれます。
レイヤー内のttfファイルを関数から読み込む
FONT_NAME = "GenJyuuGothic"
FONT_PATH = "/opt/python/fonts/GenJyuuGothic-Medium.ttf"
# エラー回避のために同じフォント名で登録されていないかチェック
if FONT_NAME not in pdfmetrics.getRegisteredFontNames():
pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
(importは解決コードに書いているので省略)
レイヤーが展開されるパスは公式docに書いてます。Pythonランタイムの場合は/opt/python
配下に展開されます。これを見てなくてパスを特定するのに30分くらい悩みました涙。
ちなみにreportlab標準の「HeiseiKakuGo-W5」を使う場合は以下のコード
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
# HeiseiKakuGo-W5を使う場合
FONT_NAME = "HeiseiKakuGo-W5"
FONT_SIZE = 11
# フォントの登録
pdfmetrics.registerFont(UnicodeCIDFont(FONT_NAME))
フォントの登録はこれでOKです。
登録したフォントを使う時は次のようにします。
FONT_NAME = "GenJyuuGothic"
FONT_PATH = "/opt/python/fonts/GenJyuuGothic-Medium.ttf"
pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
# キャンバスを用意
pdf_canvas = canvas.Canvas("/tmp/sample.pdf", pagesize=A4)
# フォントをセット 第1引数に登録したフォント名を指定
pdf_canvas.setFont(FONT_NAME, 10)
# 文字を描画
pdf_canvas.drawString(10 * mm, 10 * mm, "サンプル")
さて、多くの場合Lambdaで作成したPDFなりExcelなりの帳票はS3に保管すると思います。
reportlabにはCanvasにgetpdfdata
という作成したPDFをバイト型で取得できる関数が用意されています。
# 保存
pdf_canvas.save()
pdf_data = pdf_canvas.getpdfdata()
s3 = boto3.client("s3")
s3.put_object(Bucket=bucket_name, Key=key, Body=pdf_data)
上記コードは
HeiseiKakuGo-W5
のようにUnicodeCIDFont
で読み込んだ標準フォントの場合はうまくいくのですが、TTFont
で読み込んだ場合は、getpdfdata
実行時に以下のエラーが出ます。
[ERROR] ValueError: redefining named object: 'toUnicodeCMap:AAAAAA+GenJyuuGothic-Medium'
同じフォント名で複数回登録しようとした際に発生するエラーのようで、getpdfdata
の内部処理で発生します。
これだとgetpdfdata
が使えないので、次のコードで解決しました。
# キャンバスを用意 メモリ上に書き込むようにする
buffer = io.BytesIO()
pdf_canvas = canvas.Canvas(buffer, pagesize=A4)
# ~~省略~~
pdf_canvas.save()
# 読み込み位置をリセット
buffer.seek(0)
# バイトデータを取得
pdf_data = buffer.getvalue()
s3 = boto3.client("s3")
s3.put_object(Bucket=bucket_name, Key=key, Body=pdf_data)
これで無事任意のフォントでPDF作成→S3に格納までできました。
おわりに
「Lambda上でフォント登録できんやん、詰み」となっている人の参考になれば幸いです。