AWS Lambdaで画像をWebPに変換する

こんにちはクアッド株式会社のsegawaです。
Quad inc Advent Calendar 2017の1日目の記事です。

# WebPとは

https://developers.google.com/speed/webp/

WebP（ウェッピー）はGoogleの開発した画像フォーマットで、PNGやJPGよりも25%ほど画像サイズを小さくできるとうたわれています。
実際には `quality=80` だとJPGの元画像と比較して半分以下までサイズを減らすことができました。

PCでの対応ブラウザはChromeとOperaのみですが、macのChrome/Safari/FireFox、WindowsもIE11/Firefoxで表示は可能でした。
プロジェクトではiPhoneアプリとAdobe AIRアプリに表示するために利用しました。


# PythonでWebPに変換する

公式で提供している [cwebp](https://developers.google.com/speed/webp/docs/cwebp) をOSコマンド実行します。
PillowやImageMagickでの変換を試してみましたが、私の環境では上手くいかず時間の関係でこちらを採用しました。

## cewbpのダウンロード

https://developers.google.com/speed/webp/docs/precompiled

上記ページ内の「downloads repository」のリンクからダウンロードページに飛ぶので
`libwebp-0.6.0-linux-x86-64.tar.gz` をダウンロードして解凍します。
※[Lambdaの実行環境](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/current-supported-versions.html)はAmazon Linuxです


## cwebpの使い方

解凍してできたbinディレクトリのなかにエンコーダがあります。
またlibフォルダの中にある `libwebp.a` も使います。

cwebpはJPG/PNG/TIFFに対応しています。

```:cwebp
cwebp [options] input_file -o output_file.webp
```

なお、gif画像の場合はgif2webpを利用します。

```:gif2webp
gif2webp [options] input_file.gif -o output_file.webp
```

## サンプル

quality を 80 にしています。
これ以下だと、画像が荒れて見えました。

```python:変換の例
def convert_to_webp(infile):
    filename, ext = os.path.splitext(infile)
    quality = 80
    out = filename + ".webp"
    cmd = "cwebp -quiet -q {} {} -o {}".format(quality, infile, out)
    try:
        result = subprocess.check_call(cmd, shell=True)
        print('convert result => ' + str(result))
    except Exception as e:
        print(e)
    return out
```

# AWS Lambdaに実装

## libにパスを通しておく

Lambdaでcwebpを実行するにはパスを通す必要があるため、以下のようにパスを設定しておきます。

```python:libにパスを通す
import os

os.environ["PATH"] = os.environ.get("PATH") + ':' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib')
os.environ["LD_LIBRARY_PATH"] = os.environ.get("LD_LIBRARY_PATH") + ':' + os.path.join(
    os.path.abspath(os.path.dirname(__file__)), 'lib')
```

## zipファイルの作成

Lambdaで cwebp を実行するには、Pythonプログラムと共にエンコーダをzipでまとめてアップロードする必要があります。

```:zipの構成
/webp_converter.zip
  └webp_converter.py
　	　├/lib
　　　　　├cwebp
　　　　　└libwebp.a
```

あわせて使用するPythonライブラリも含める必要があるので、zip圧縮するディレクトリ内にインストールしておきます。
今回は `requests` を利用しています。

```:Pythonライブラリのインストール例
pip install requests -q -t .
```

## Lambdaにzipをアップロード

Lambdaへの反映方法はいくつかありますが、ここでは作成したzipファイルをAWSCLIでアップロードします。

```console:aws-cliでLambdaファンクションを作成
# aws lambda create-function \
	--function-name webp_converter \
	--runtime python3.6 \
	--role arn:aws:iam::************:role/lambda_basic_execution \
	--handler webp_converter.lambda_handler \
	--timeout 60 \
	--memory-size 128 \
	--publish \
	--environment Variables={ENV=staging} \
	--zip-file ＜zipファイルのパス＞
```

# コードサンプル

以下のコードはパラメータで受け取ったURLの画像をダウンロードし、Lambda上で変換、S3に保存するサンプルです。
画像フォーマットの判定等が抜けているので非対応の画像を渡すとエラーになります。

```python:Lambdaファンクション
# -*- coding: utf_8 -*-

import os
import requests
from boto3.session import Session

os.environ["PATH"] = os.environ.get("PATH") + ':' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib')
os.environ["LD_LIBRARY_PATH"] = os.environ.get("LD_LIBRARY_PATH") + ':' + os.path.join(
    os.path.abspath(os.path.dirname(__file__)), 'lib')

s3_client = None


def lambda_handler(event, context):

    global s3_client
    if not s3_client:
        session = Session(
            aws_access_key_id="*********", # S3のアクセスキー
            aws_secret_access_key="**************", # S3のシークレット
            region_name="********" #S3のリージョン
        )
        s3_client = session.resource('s3')

    try:
        url = event['url']
        s3_path = event['s3_path']

        # 画像ダウンロード
        save_path = download_img(url)

        # ダウンロードした画像WebPに変換
        webp_path = convert_to_webp(save_path)

        # S3に保存
        save_to_s3(webp_path, s3_path)
    except Exception as e:
        print(e)
        raise e
    else:
        print('SUCCESS')
        return {"result": "OK"}


def download_img(url):
    save_path = "/tmp/" + url.split("/")[-1]
    r = requests.get(url, stream=True)
    if r.status_code == 200:
        with open(save_path, 'wb') as file:
            for chunk in r.iter_content(chunk_size=1024):
                file.write(chunk)
    return save_path


def convert_to_webp(infile):
    filename, ext = os.path.splitext(infile)
    quality = 80
    out = filename + ".webp"
    cmd = "cwebp -quiet -q {} {} -o {}".format(quality, infile, out)
    try:
        result = subprocess.check_call(cmd, shell=True)
        print('convert result => ' + str(result))
    except Exception as e:
        print(e)
    return out


def save_to_s3(webp_path, s3_path):
    if not os.path.exists(webp_path):
        print('file is NOT exists : ' + webp_path)
    save_file_name = s3_path + '/' + webp_path.split("/")[-1]
    data = open(webp_path, 'rb')
    s3_client.meta.client.put_object(
        ACL='public-read',
        Body=data,
        Bucket="**********", # S3のバケット
        Key=save_file_name,
        ContentType='image/webp'
    )


def remove_file(original_path, webp_path):
    os.remove(original_path)
    os.remove(webp_path)


if __name__ == '__main__':
    lambda_handler({}, {})
```

1処理あたり数秒で終わるので、大量に画像を変換するのにLambdaは便利ですね😃