[Cloud Functions] FirebaseStorage に保存された画像をサムネイル用に整形と軽量化する

この記事は、【 [可茂IT塾 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/kamo-it "可茂IT塾 Advent Calendar 2024") 】の6日目の記事です。

Flutter / Firebase のアプリ開発で実装したことを記事にしました。
誰かのお役に立てれば、幸いです。

FirebaseStorage に画像を保存したときに、
オリジナルとは別にサムネイル用の画像を用意することがあると思います。

CloudFunctions で、Storage に画像が登録されたことをトリガーにして、
サムネイル用画像を作成し、保存するファンクションを Python で実装しました。

## 実装した機能
- 投稿された画像が長方形の場合は、短い方の辺の長さの正方形に整形する
- サムネイル画像は、128×128px にする
- オリジナルとは別ディレクトリに保存する

## 実装したコード
```python
from google.cloud import storage
from PIL import Image
from io import BytesIO
import os
import pathlib

def gen_thumbnail(event):

    bucket_name = event.data.bucket
    # ファイルパス
    file_name = pathlib.PurePath(event.data.name)
    content_type = event.data.content_type

    # 無限ループ対策
    if "_thumb" in file_name.stem:
        print(f"Skipping thumbnail file: {file_name}")
        return

    # コンテンツタイプの確認
    if not content_type or not content_type.startswith("image/"):
        print(f"This is not an image. ({content_type})")
        return

    print(f"Processing file: {file_name} in bucket: {bucket_name}")

    # 画像をメモリ上に読み込む
    storage_client = storage.Client()
    bucket = storage_client.get_bucket(bucket_name)
    blob = bucket.blob(str(file_name))
    image_data = blob.download_as_bytes()
    image = Image.open(BytesIO(image_data))

    # 画像の縦横のサイズを取得、最短の辺の長さを求める
    width, height = image.size
    min_size = min(width, height)
    
    # 画像の最短の辺の正方形に整形する
    left = (width - min_size) // 2
    top = (height - min_size) // 2
    right = (width + min_size) // 2
    bottom = (height + min_size) // 2
    cropped_image = image.crop((left, top, right, bottom))

    thumbnail_size = (128, 128)
    cropped_image.thumbnail(thumbnail_size)

    file_path, ext = os.path.splitext(str(file_name))
    # 保存ディレクトリをサムネイル用に変更
    thumbnail_path = file_path.replace("image/", "thumbnail/")
    thumbnail_name = f"{thumbnail_path}_thumb{ext}"
    print(f"Thumbnail Name: {thumbnail_name}")

    thumb_io = BytesIO()
    image_format = image.format if image.format else "jpeg"
    cropped_image.save(thumb_io, format=image_format)
    thumb_io.seek(0)

    thumb_blob = bucket.blob(thumbnail_name)

    thumbnail_content_type = f"image/{image_format.lower()}"
    thumb_blob.upload_from_file(thumb_io, content_type=thumbnail_content_type)
    
```

## Pillow を使用した画像整形

Python にて画像を処理するパッケージとして、Pillow を使用しました。

正方形のサムネイル画像を並べて表示する仕様があったので、画像整形をしました。

下はそのイメージです。

![スクリーンショット 2024-12-15 16.07.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1435645/814e2cfe-d1b5-1798-5949-8740e40e8c18.png)

整形のコード
```python
   # 画像の縦横のサイズを取得、最短の辺の長さを求める
    width, height = image.size
    min_size = min(width, height)

    # 画像の最短の辺の正方形に整形する
    left = (width - min_size) // 2
    top = (height - min_size) // 2
    right = (width + min_size) // 2
    bottom = (height + min_size) // 2
    cropped_image = image.crop((left, top, right, bottom))
```

1. 画像のwidth, heightの長さを取得
1. 短い方を選択
1. 整形画像の角の座標を指定する

イメージの場合、左上を基準（緑の点）にして、サムネイル画像の座標（赤い点）は、
左上（100, 0）、右上（700, 0）、左下（100, 600）、右下（700, 600）
の座標になります。
上記の座標を当てはめると下のようなコードになる。

```python
    cropped_image = image.crop((100, 0, 700, 600))
```

これらを指定して、画像整形をしました。

## まとめ
今後は、トリガーの条件をもう少し細かく指定したいのでGCPの方にデプロイする予定ですが、一旦、機能的には問題ないものができたので、記事にしました。
よりよい書き方などがあれば、ご指摘いただけると助かります。
Pythonは初心者です。

## ドキュメントにはサムネイル作成用のコード例がある

https://firebase.google.com/docs/storage/extend-with-functions?hl=ja&gen=2nd#download-transform

上記のコードを使用すれば、簡単にサムネイル作成用のファンクションを実装できると思ったのですが、正方形に整形するなど、仕様を満たしてくれないところがあったので、そのままは使えませんでした。
ただ、とても参考にはなったので、こちらで紹介させてもらいます。