【Lambda】ReportLabで任意のフォントを使用してS3に格納する

## はじめに

PythonでPDFを生成するライブラリ[ReportLab]で、
標準選択できるフォント以外のフォントを使う際に一筋縄では出来なかったので、
解決方法を残します。

Lambdaのデプロイは[AWS SAM][aws-sam]を使っています。他の方法でもやり方は同じです。
LambdaやLambda Layerのデプロイ方法については本記事では解説しませんので、他の記事を参照ください。

## 解決コード

任意のフォントを登録→最終的にS3にプットするまでのコードです。
コードだけ知りたい、という方はこちらを参考にしてください。

```python
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

## 解決方法

流れとしては以下になります。

1. 使用したいフォントのttfファイル（拡張子`.ttf`）を用意する
2. ttfファイルをデプロイするLambdaレイヤーに含める
3. レイヤー内のttfファイルを関数から読み込む

3につまづいて解決に2時間くらい格闘しました。順番に解説します

### 使用したいフォントのttfファイルを用意する

わたしは「源柔ゴシック」というフォントを使いたかったので、
以下のサイトからzipファイルをダウンロード
→展開
→太さや微妙なスタイルの違いで複数ファイルがあります。わたしは`GenJyuuGothic-Medium.ttf`を使いました。

http://jikasei.me/font/genjyuu/

### ttfファイルをデプロイするLambdaレイヤーに含める

レイヤーのフォルダ配下に`fonts`フォルダを作り、その中に`ttf`ファイルを配置します。
`common_layer`がレイヤーフォルダの名前です（なんでもいいです）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649149/edc029b7-191b-7906-4ebb-1daf7c471277.png)

レイヤーフォルダ直下でなくても問題ないです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649149/7ba9f63c-e653-6dda-a7f8-48e540840ee6.png)

この状態でデプロイするとレイヤーにフォントファイルが含まれます。

### レイヤー内のttfファイルを関数から読み込む

```python
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分くらい悩みました涙。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-layers.html

ちなみにreportlab標準の「HeiseiKakuGo-W5」を使う場合は以下のコード

```python
from reportlab.pdfbase.cidfonts import UnicodeCIDFont

# HeiseiKakuGo-W5を使う場合
FONT_NAME = "HeiseiKakuGo-W5"
FONT_SIZE = 11

# フォントの登録
pdfmetrics.registerFont(UnicodeCIDFont(FONT_NAME))
```

フォントの登録はこれでOKです。
登録したフォントを使う時は次のようにします。

```python
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をバイト型で取得できる関数が用意されています。

```python
# 保存
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`実行時に以下のエラーが出ます。

```bash
 [ERROR] ValueError: redefining named object: 'toUnicodeCMap:AAAAAA+GenJyuuGothic-Medium'
```

同じフォント名で複数回登録しようとした際に発生するエラーのようで、`getpdfdata`の内部処理で発生します。
これだと`getpdfdata`が使えないので、次のコードで解決しました。

```python
# キャンバスを用意 メモリ上に書き込むようにする
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上でフォント登録できんやん、詰み」となっている人の参考になれば幸いです。

[ReportLab]: https://pypi.org/project/reportlab/
[aws-sam]: https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html