画像のサムネイルを生成して 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 は便利!