Python
Flask
GAE
GCS
pillow
BrainPadDay 21

PIL を使って GCS にある画像を動的にリサイズして送信する

画像のサムネイルを生成して GCS に置くのが面倒くさいので、動的に画像サイズを変更してレスポンスを返すようにしたいと思って調べたら、PIL というモジュールを使うのが良さそう。

pillow のインストール

PIL は開発が停滞していて、3 系に対応していないので、PIL のフォークの pillow を使うと良いらしい。
https://librabuch.jp/blog/2013/05/python_pillow_pil/

pip install pillow

pillow でリサイズする

基本的な使い方はとても簡単で、Image オブジェクトを作ったら、メソッド経由で色々できる。
画像をリサイズする場合は以下のような感じ。

from PIL import Image

# 画像オブジェクトを生成
# 引数には filename か file like object が入る
img = Image.open('hoge.jpg')

# リサイズ処理 引数は (width, height)
resized = img.resize(('400', '300'))

# 画像の保存
resized.save('hoge_resized.jpg', format='jpeg')

拡張子から画像 format を自動で判断してくれるようだが、jpg という format が存在しないので、拡張子が.jpg の場合は jpeg を手動で設定する必要がある。これはちょっとイケてない。

GCS から画像をダウンロード

GCS にアクセスするには google-cloud を入れる。

pip install google-cloud

GCS から画像をダウンロードするには以下のようにする。
今回はファイルに保存することが目的ではないので、bytesIO を使ってオンメモリで処理している。

import io
from google.cloud.storage.client import Client

client = Client(project='your-project')
bucket = client.get_bucket('your-bucket')
blob = app.bucket.get_blob('imagename')

# オンメモリで処理したいので bytesIO を使う
img_file = io.BytesIO()
blob.download_to_file(img_file)

# ファイルに保存したい場合
blob.download_to_filename('img_file.jpg')

Flask を使った例

GET /image/test.jpg にアクセスすると gs://your-bucket/test.jpg を取得する。
クエリ GET /image/test.jpg?h=100&w=100 を渡すと、100x100px に画像をリサイズする。

from flask import Flask, request, send_file

app = Flask(__name__)
client = Client(project='your-project')
app.bucket = client.get_bucket('your-bucket')


@app.route('/image/<image_name>')
def image(image_name):
    h, w = request.args.get('h', type=int), request.args.get('w', type=int)
    blob = app.bucket.get_blob(image_name)
    img_file = io.BytesIO()
    blob.download_to_file(img_file)
    if h and w:
        img = Image.open(img_file)
        resized = img.resize((w, h))
        img_file = io.BytesIO()
        resized.save(img_file, format='jpeg')
    img_file.seek(0)  # 0byte に seek しておかないと送信する画像が空になるので注意
    return send_file(img_file, attachment_filename=image_name)

画像が存在しない場合

bucket.get_blob は画像が存在しなかった場合に None を返すので、これを使って分岐できる。
今回は、単色グレーの画像を作成して返してみる。

blob = app.bucket.get_blob(image_name)
if not blob:
    if not (h and w):
        h, w = 1, 1  # サイズの指定がない場合は 1x1px の画像を作成
    # グレーの画像を生成する
    img = Image.new('RGB', (w, h), (0xdd, 0xdd, 0xdd))
    img.save(img_file, format='jpeg')
    img_file.seek(0)
    response = make_response(send_file(img_file, attachment_filename=image_name))
    response.headers['Cache-Control'] = 'no-cache'  # 生成画像はキャッシュされないようにする
    return response

一通り実装した例

import io
from PIL import Image
from flask import Flask, make_response, request, send_file
from google.cloud.storage.client import Client

app = Flask(__name__)
client = Client(project='your-project')
app.bucket = client.get_bucket('your-bucket')


@app.route('/image/<image_name>')
def image(image_name):
    h, w = request.args.get('h', type=int), request.args.get('w', type=int)
    blob = app.bucket.get_blob(image_name)
    img_file = io.BytesIO()

    if not blob:
        if not (h and w):
            h, w = 1, 1
        img = Image.new('RGB', (w, h), (0xdd, 0xdd, 0xdd))
        img.save(img_file, format='jpeg')
        img_file.seek(0)
        response = make_response(send_file(img_file, attachment_filename=image_name))
        response.headers['Cache-Control'] = 'no-cache'
        return response

    blob.download_to_file(img_file)
    if h and w:
        img = Image.open(img_file)
        resized = img.resize((w, h))
        img_file = io.BytesIO()
        resized.save(img_file, format='jpeg')
    img_file.seek(0)
    return send_file(img_file, attachment_filename=image_name)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=True)

GAE に載せると GCS ←→ インスタンス間の帯域が大きいので 400x300px 程度のサムネイルなら大体 200ms 台で返ってきているので、十分使えると思う。GAE ではデフォルトでエッジキャッシュされるので、2回目以降の取得では Flask を経由しない。GAE は便利!