Python
GAE
exif
PIL
CloudStorage

GAE - Pythonで、EXIFの回転情報を元に画像回転させてCloud Storageにアップする。

More than 1 year has passed since last update.

はじめに

以前にPILを使ってサーバ側で画像を回転させようとして失敗しました。原因は次のエラー

".../PIL-1.1.7/PIL/ImageFile.py", line 476, in _save
    fh = fp.fileno()
UnsupportedOperation: fileno

fileno()に何か問題があるらしい。ネットで検索してみると他にもこのエラーに苦戦している人がちらほら見られる。このエラーについてはよくわからないです。

なので今回はこれを打開する案として、GAEのImages Python APIを使ってサーバ側で回転させてみます。

Images Python APIとは
AppEngineが提供している、画像データを操作する機能。画像のサイズ変更、回転、反転、トリミングを行うことがでる。APIは、フォーマット、幅、高さ、カラー値のヒストグラムなど、イメージに関する情報も抽出できる。アプリからデータを受け取ることもGoogle Cloud Storageの 値を使用することもできる。
また、Images APIに加えて、Python 2.7のPython Imaging Library(PIL)で提供される変換を使用することもできる。
(詳しくはこちら)

EXIFの取得

APIのインポート

from google.appengine.api import images

base64をImage APIで開く

binary = base64.b64decode(base64_string)
gae_img = images.Image(binary)

情報を取得

ここからEXIF情報を取得していくが、Image Python APIを使って取得する場合、次の手順に従わないとエラーが出てしまう。

# 1. 画像に対してなんらかの処理(変換)
gae_img.rotate(0)
# 2. 画像に対しての変換を実行
gae_img.execute_transforms(parse_source_metadata=True)
# 3. EXIF情報を取得
exif = gae_img.get_original_metadata()

順を追って説明していくと、gae_img.get_original_metadata()でいきなりEXIF情報を取得したいところだが、これをするにはgae_img.execute_transforms(parse_source_metadata=True)でソースイメージのメタデータ(EXIF)を解析する必要がある。
このgae_img.execute_transforms()は上でも書いてある通り、"画像に対しての変換を実行する"というメソッドであるから、これをやる前に"画像が変換されていることが前提"である。だからこの手前にわざわざgae_img.rotate(0)という一見無意味な処理を書いている。このgae_img.rotate()についてはあとで説明する。

ここまででprint exifすると

{u'MimeType': 0, u'ColorProfile': False, u'Orientation': 6, u'ColorSpace': 1, u'ImageWidth': 3264, u'ImageLength': 2448}

ディクショナリ型で入っていることがわかる。
画像の向きについてはOrientationという項目で判断できる。

print exif['Orientation']
# "1"から"8"までの数字のどれかが出力される。

これでEXIF情報の取得ができた。

EXIF情報を元に画像を回転させる

上記まででやったことをまとめます。

from google.appengine.api import images

binary = base64.b64decode(base64_string)
gae_img = images.Image(binary)

gae_img.rotate(0)
gae_img.execute_transforms(parse_source_metadata=True)
exif = gae_img.get_original_metadata()

ここからexifの中のOrientationを元に画像を回転させる。

if exif['Orientation'] == 3:
    gae_img.rotate(180)
elif exif['Orientation'] == 6:
    gae_img.rotate(90)
elif exif['Orientation'] == 8:
    gae_img.rotate(270)
else : 
    gae_img.rotate(0)

converted_img = gae_img.execute_transforms(parse_source_metadata=True)

gae_img.rotate()で画像を90度ずつ回転させることができる。
前述でも述べたようにgae_img.execute_transforms()の性質により、画像に対して一度は変換を行わないとエラーが出てしまうため、elseで一度は変換が行われるようにしている。

今回は上下反転や左右反転された画像は送られて来ないものと考えているため、2,4,5,7の場合の処理は書いていない。
そして回転させたあと、画像に加えた変更を実行させるために、gae_img.execute_transforms(parse_source_metadata=True)を実行する。

Cloud Storageにアップロードする

ここからCloud Storageにアップロードして行きます。
手順としては
1. 環境変数を判別
2. 環境ごとに適した場所にファイルを開く
3. 開いた先に書き込む(アップロード)
このようになります。
では実際にコードを見て行きましょう。

import cloudstorage as gcs

def upload_gcs(self, name, type, binary):
    # 環境変数を抽出
    env = os.getenv('SERVER_SOFTWARE')
    # 任意の環境であった場合、Google Cloud Storageにアップロード
    if (env and (env.startswith('Google App Engine/') or env.startswith('Development/'))):
        # FileLogic.GCS_IDには保存する場所を示すurlがあらかじめ入っている。
        file_url = FileLogic.GCS_ID + name
        # Google Cloud StorageのAPIでファイルを開く
        with gcs.open(file_url, 'w', content_type=type, options={'x-goog-acl': 'public-read'}) as gcs_file:
            # 画像の書き込み(アップロード)
            gcs_file.write(binary)
            # FileLogic.GCS_HOST_NAMEにはホスト名があらかじめ入っている
            file_url = FileLogic.GCS_HOST_NAME + file_url
    else:
        # 任意の環境でなかった場合ローカルに書き込む
        file_url = 'local_file/' + name
        with open(file_url, 'w') as file:
            file.write(binary)
    return file_url

Google Cloud StorageのAPIでopenし、Cloud Storageバケット内の既存のオブジェクトを開く。この際、'w'を渡すことで書き込みモードでファイルを開くことができる。
書き込みモードで任意の場所を開いたら、writeで画像をアップロードする。