Google Cloud Functions(Beta)のImageMagick Tutorialに入門

# 概要

前回の[Google Cloud Functions(Beta)の基本的な仕組み、使い方を学ぶ] (https://qiita.com/rubytomato@github/items/cc36086c58a662e14521)に引き続き、ImageMagick Tutorialを通してGoogle Cloud Functionsを学習したまとめの記事です。

公式のImageMagick Tutorialは、アップロードされた画像をCloud Vision APIを利用して有害画像か判定し、有害であれば画像をImageMagickのBlur処理(ぼかし)で加工するという内容です。

この記事ではだいぶ省略して、アップロードされた画像をImageMagickでresizeし、resize後の画像を別のバケットへアップロードするという内容になります。

**環境**

* Node.js 6.11.5
* Cloud SDK 196.0.0
* [@google-cloud/storage] (https://www.npmjs.com/package/@google-cloud/storage) 1.6.0
* [imagemagick] (https://www.npmjs.com/package/imagemagick) 0.1.3

**参考**

* [Cloud Functions > Documentation > ImageMagick Tutorial] (https://cloud.google.com/functions/docs/tutorials/imagemagick?hl=ja)
* [Google Cloud Functionsに関するドキュメント] (https://cloud.google.com/functions/docs/?hl=ja)


# 実行環境のセットアップ

## ローカルPC

必要があればCloud SDKをアップデートします。

```text
> gcloud components update
```

依存するモジュールをインストールします。

```text
> npm install --save @google-cloud/storage
```

```text
> npm install --save imagemagick
```

## GCP

このチュートリアルではCloud Storageのバケットを2つ利用します。
1つ目はオリジナル画像のアップロード先でバックグラウンド関数のトリガーとなります。2つ目はリサイズした画像のアップロード先です。

GCP Consoleかgsutilツールでバケットを作成します。
この例ではgsutilで作成しています。バケット名は全バケットで一意である必要があるためランダムな文字列を付加しています。

オリジナル画像のアップロード先バケット

```text
> gsutil mb -c regional -l us-west1 gs://image_upload_ti5mc9dif
```

リサイズした画像のアップロード先バケット

```text
> gsutil mb -c regional -l us-west1 gs://image_resize_4uayd0auq
```

# デモプロジェクト

## プロジェクトの構造

```text
demo-gcf-resize
  |
  +--- /node_modules
  |
  +--- .gcloudignore
  |
  +--- config.json
  |
  +--- package.json
  |
  +--- index.js
```

**.gcloudignore**について

デプロイ時にGCPにアップロードしたくないファイルの条件を記述します。

```text:.gcloudignore
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
#   $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore

node_modules
```

**package.json**

```json:package.json
{
  "name": "demo-gcf-resize",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/storage": "^1.6.0",
    "imagemagick": "^0.1.3"
  }
}
```

## デモコード

リサイズ画像をアップロードするバケット名をconfig.jsonに定義します。

```json:config.json
{
  "RESIZE_BUCKET_NAME": "image_resize_4uayd0auq"
}
```

Google Cloud Functionsへデプロイする関数の定義です。

* resizeImages
    * アップロードされた画像をリサイズするバックグラウンド関数
    * アップロード用バケットから画像をダウンロードし、リサイズ後にリサイズ後バケットへアップロードする
* imageFeatures
    * リサイズされた画像のメタデータを出力するバックグラウンド関数
    * リサイズ後バケットにアップロードされた画像をダウンロードし、画像のメタデータをログに出力する

```js:index.js
const config = require('./config.json');

const fs = require('fs');
const path = require('path');
const imagemagick = require('imagemagick');
const storage = require('@google-cloud/storage')();

exports.resizeImages = (event) => {
    const object = event.data;
    console.log(object);

    if (object.resourceState === 'not_exists') {
        console.log('This is a deletion event.');
        return;
    } else if (!object.name) {
        console.log('This is a deploy event.');
        return;
    }

    const file = storage.bucket(object.bucket).file(object.name);
    console.log(`Analyzing ${file.name}.`);

    return resize(file);
};

function resize (file) {
    const tempLocalFilename = `/tmp/${path.parse(file.name).base}`;

    return file.download({destination: tempLocalFilename})
        .catch((err) => {
            return Promise.reject(err);
        })
        .then(() => {
            console.log(`Image ${file.name} has been downloaded to ${tempLocalFilename}.`);
            return new Promise((resolve, reject) => {
                imagemagick.resize({srcPath: tempLocalFilename, dstPath: tempLocalFilename, width: 256}, (err, stdout, stderr) => {
                    if (err) {
                        console.error('Failed to resize image.', err);
                        reject(err);
                    } else if (stderr) {
                        console.error('Failed to resize image.', err);
                        reject(stderr);
                    }
                    resolve();
                });
            });
        })
        .then(() => {
            console.log(`Image ${file.name} has been resize.`);
            return storage.bucket(config.RESIZE_BUCKET_NAME).upload(tempLocalFilename, {destination: file.name})
                .catch((err) => {
                    console.error('Failed to upload resize image.', err);
                    return Promise.reject(err);
                });
        })
        .then(() => {
            return new Promise((resolve, reject) => {
                fs.unlink(tempLocalFilename, (err) => {
                   if (err) {
                       reject(err);
                   }
                   resolve();
                });
            });
        });
}

exports.imageFeatures = (event) => {
    const object = event.data;
    console.log(object);

    const file = storage.bucket(object.bucket).file(object.name);
    console.log(`Analyzing ${file.name}.`);

    return feature(file);
};

function feature(file) {
    const tempLocalFilename = `/tmp/${path.parse(file.name).base}`;

    return file.download({destination: tempLocalFilename})
        .catch((err) => {
            return Promise.reject(err);
        })
        .then(() => {
            return new Promise((resolve, reject) => {
                imagemagick.identify(tempLocalFilename, (err, features) => {
                    if (err) {
                        reject(err);
                    }
                    console.log(features);
                    resolve();
                });
            });
        })
        .then(() => {
            return new Promise((resolve, reject) => {
                fs.unlink(tempLocalFilename, (err) => {
                    if (err) {
                        reject(err);
                    }
                    resolve();
                });
            });
        });
}
```

## デプロイ

プロジェクトのルートディレクトリでdeployコマンドを実行します。

### resizeImages

バケットにアップロードされた画像ファイルをリサイズするバックグラウンド関数です。

```text
> gcloud beta functions deploy resizeImages --trigger-bucket image_upload_ti5mc9dif --entry-point resizeImages --memory=128MB
```

**--trigger-bucket**

バックグラウンド関数のトリガーとなるバケット名を指定します。

**--entry-point**

index.jsの中にexportする関数が複数ある場合、どの関数をGoogle Cloud Functionsのエントリポイントとするか指定します。

**--memory**

関数に割り当てるメモリを指定します。省略すると256MBが割り当てられます。

### imageFeatures

リサイズした画像のメタデータを表示するだけのバックグラウンド関数です。

```
> gcloud beta functions deploy imageFeatures --trigger-bucket image_resize_4uayd0auq --entry-point imageFeatures --memory=128MB
```

### プロジェクトのルートディレクトリ以外でデプロイする

プロジェクトのルートディレクトリでデプロイする場合は省略できますが、ルートディレクトリ以外の場所からデプロイする場合は`--source`オプションにルートディレクトリを指定します。

```text
> gcloud beta functions deploy [FUNCTION_NAME] --source=/path/to/[ROOT_DIRECTORY] ...
```

## 動作確認

```text
> gsutil cp test.jpg gs://image_upload_ti5mc9dif
```

resizeImages関数のログ

![gcf_resize.png](https://qiita-image-store.s3.amazonaws.com/0/22772/f8a4263e-9267-faba-0ce7-e13c8fc8b3d9.png)

imageFeatures関数のログ
ピンクの下線がリサイズ画像のメタデータのログ出力です。

![gcf_feature.png](https://qiita-image-store.s3.amazonaws.com/0/22772/56e6bda3-6066-6f03-4fce-3d3d1cebd91b.png)