Python3 + Bottle + Pillow でレスポンスとして画像を返す

とりあえず結論

Python3 で画像を取り扱うには、Pillow(PILの派生版, Python3対応)を使うと良いようです。

python-pillow.github.io

サーバー側で作成・処理した Pillow のオブジェクトを、HTTPレスポンスで画像としてクライアントに表示させるには、次のようにします。 ポイントは、Pillow オブジェクトの保存先に BytesIO オブジェクトを指定している点です。

from bottle import default_app, route, HTTPResponse
from io     import BytesIO
from PIL    import Image
import requests

application = default_app()

@route('/route/to/your/image/shown')
def show_image():

    # URLから画像を開く
    image_url     = "path/to/target/image"
    response      = requests.get(image_url)
    pillow_object = Image.Open(BytesIO(response.content))
    
    # -- ここで pillow_object に対して何らかの処理 --
    
    # HTTPレスポンスを作成する
    
    output = BytesIO()
    pillow_object.save(output, format='png')
    
    res = HTTPResponse(status=200, body=output.getvalue())
    res.set_header('Content-Type', 'image/png')
    return res

サーバーでは wsgi 対応フレームワークとしてBottle を使用しています。

bottlepy.org

クライアント側では、以下のようにして呼び出すことができます。

<img src="/route/to/your/image/shown"/>

Python3 と StringIO, BytesIO

ところで、ネットで上記の方法を調べようとして適当なキーワードで検索すると、以下のような回答にぶつかります。

See Image.save(). It can take a file object in which case you can write it to a StringIO instance. Thus something like:

output = StringIO.StringIO()
base.save(output, format='PNG')
return [output.getvalue()]

python imaging library - How to return in-memory PIL image from WSGI application - Stack Overflow

画像イメージを StringIO に保存すると良い、ということですね。ところが、これは Python 3.x ではうまくいきません。なぜかというと、

The StringIO and cStringIO modules are gone. Instead, import the io module and use io.StringIO or io.BytesIO for text and data respectively.

python - How to use StringIO in Python3? - Stack Overflow

What’s New In Python 3.0 — Python v3.0.1 documentation

つまり、Python 2 で有効であった StingIOcStyingIO は、Python 3 では撤廃されて、代わりに io.StringIOio.BytesIO になったことが原因です。 そこで 上記のStackOverflow の回答のように、

try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO

StringIO.StringIO の代わりに io.StringIO で処理しようとしても、今回のケースではやはりうまく行きません。

公式サイトのドキュメントを読むとわかるのですが、画像などバイナリファイルを扱うには、bytes オブジェクトに対応した io.BytesIO でなければいけないのですね。 StringIO.StringIOio.StringIO の名前が似ているのでややこしいのですが、Python3 ではより適切に役割が分離されたということでしょう。

テキスト I/O は、 str オブジェクトを受け取り、生成します。すなわち、背後にあるストレージがバイト列 (例えばファイルなど) を格納するときは常に、透過的にデータのエンコード・デコードを行ない、オプションでプラットフォーム依存の改行文字変換を行います。

バイナリー I/O (buffered I/O とも呼ばれます) は bytes オブジェクトを受け取り、生成します。エンコード、デコード、改行文字変換は一切行いません。このカテゴリのストリームは全ての非テキストデータや、テキストデータの扱いを手動で管理したい場合に利用することができます。

io --- ストリームを扱うコアツール — Python 3.10.0b2 ドキュメント

MongoDB に画像を保存するときに BytesIO を使用するのも同様の理由と思われます。