Cloud Functionsで動的に画像変換

# 目標
![cat.jpg](https://qiita-image-store.s3.amazonaws.com/0/278434/9e1b9d4b-8a3a-beb4-5511-6c4cfb4da088.jpeg)
↑こういう画像を`cat.jpg?size=1000`とか`cat.jpg?encode=webp`とかでサイズ変換したり、webpにエンコードしたりして配信できるようにする。

ちなみにこの猫画像は[pexels](https://www.pexels.com/search/cat/)でcatと検索したら一番上に出てきたものです。

# 準備
当たり前ですがGCPを使うので[gcloud](https://cloud.google.com/sdk/gcloud/)をインストールしておきます。

あとGCPのプロジェクトをセットアップして、適当にバケットを作成しておきます。
<img width="798" alt="スクリーンショット 2018-10-20 0.08.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/278434/c36e76dc-def1-eb2a-6835-36f04a47a87b.png">

# 関数をデプロイしてみる
まず公式チュートリアルにもあるHello World!だけを返す関数をデプロイしてみます。

```javascript:index.js
exports.helloGET = (req, res) => {
  res.send('Hello World!');
};
```

適当なフォルダに`index.js`を作成し、上のように書きます。そのままシェルで

```bash
gcloud functions deploy helloGET --runtime nodejs6 --trigger-http
```
と入力するとCloudFunctionsにデプロイできます。
デプロイした関数のURLはこんな感じ↓で、アクセスしてみると「Hello World!」と表示されます。
[https://us-central1-qiita-219915.cloudfunctions.net/helloGET](https://us-central1-qiita-219915.cloudfunctions.net/helloGET)
<img width="564" alt="スクリーンショット 2018-10-21 4.01.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/278434/4c4bcb3b-9034-ae5b-2bd8-6bc5d2c25e11.png">

# 必要なライブラリを追加

```bash
yarn init -y
```

```bash
yarn add @google-cloud/storage fs-extra sharp imagemin imagemin-mozjpeg imagemin-pngquant
```

## @google-cloud/storage
GCPのStorageにアクセスするのに使います。

## fs-extra
普段使っているので。ensureDirが便利。

## sharp
[imagemagickより早いらしい。](http://sharp.pixelplumbing.com/en/stable/performance/)

## imagemin
画像を圧縮してくれます。圧縮しないなら不要。
それぞれの形式にあった[プラグイン](https://www.npmjs.com/search?q=keywords%3Aimageminplugin)と共に使用します。今回はjpgとpng用のものを用意。

# GCS
とりあえずGCSからファイルを読み込んでそのまま送り返してみます。

```javascript:index.js
const {Storage} = require('@google-cloud/storage');
const os = require('os');
const path = require('path');
const fs = require('fs-extra');

const gcs = new Storage();
const bucket = gcs.bucket('qiita-auto-resizer');

exports.resizeImg = async (req, res) => {
  /**
   * 例えば
   * cloudfunctions.net/resizeImg/a/b/cat.jpg
   * にアクセスした場合は
   * path: '/a/b/cat.jpg'
   * name: 'cat.jpg'
   * となる。
   */
  const requestData = {
    path: req.path,
    name: req.path.split('/').pop()
  }

  /**
   * tmp/workspaceを使う。
   */
  const workDir = path.join(os.tmpdir(), 'workspace');

  /**
   * tmp/workspaceがない場合は作成
   */
  await fs.ensureDir(workDir);

  /**
   * Storageから
   * tmp/workspace/ファイル名
   * にファイルをダウンロードする。
   */
  const tmpfile = path.join(workDir, requestData.name);
  const file = bucket.file(requestData.path);
  await file.download({
    destination: tmpfile
  });

  /**
   * ここで加工
   */

  /**
   * 加工したファイルを読み込んで送信
   */
  const responseImg = await fs.readFile(tmpfile);
  res.send(responseImg);
};
```

```bash
gcloud functions deploy img --entry-point resizeImg --runtime nodejs8 --trigger-http
```
`--entry-point`を指定することでアクセスする際のurlの見た目を整えられます。
今回は`/img/`になるようにしてみました。アクセスする際は↓のようになります。
`~~~~.cloudfunctions.net/img/cat.jpg`

あとasync awaitを使うために`--runtime nodejs8`を指定しました。

<img width="507" alt="スクリーンショット 2018-10-21 4.39.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/278434/fbc82fb3-5615-a762-6b57-58008f5932e7.png">
↑アクセスするとファイルが降ってきます💧

```javascript:diff
@@ -42,6 +42,9 @@ exports.resizeImg = async (req, res) => {
     destination: tmpfile
   });

+  const metadata = await file.getMetadata();
+  const contentType = metadata[0].contentType;
+
   /**
    * ここで加工
    */
@@ -50,5 +53,7 @@ exports.resizeImg = async (req, res) => {
    * 加工したファイルを読み込んで送信
    */
   const responseImg = await fs.readFile(tmpfile);
+
+  res.set('Content-Type', contentType);
   res.send(responseImg);
 };
```
↑Content-Typeを指定してあげます。
<img width="563" alt="スクリーンショット 2018-10-21 4.51.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/278434/b68e89b2-989d-7fc6-d230-6663e20c4829.png">

# 画像加工
今回使用するライブラリの「[sharp](http://sharp.pixelplumbing.com/en/stable/)」は色々機能が付いていますが、とりあえずリサイズとwebp変換を実装してみます。

```javascript:diff
@@ -2,6 +2,7 @@ const {Storage} = require('@google-cloud/storage');
 const os = require('os');
 const path = require('path');
 const fs = require('fs-extra');
+const sharp = require('sharp');

 const gcs = new Storage();
 const bucket = gcs.bucket('qiita-auto-resizer');
@@ -18,7 +19,11 @@ exports.resizeImg = async (req, res) => {
    */
   const requestData = {
     path: req.path,
-    name: req.path.split('/').pop()
+    name: req.path.split('/').pop(),
+    query: {
+      size: req.query.size,
+      webp: req.query.webp
+    }
   }

@@ -43,17 +48,24 @@ exports.resizeImg = async (req, res) => {
   });

   const metadata = await file.getMetadata();
-  const contentType = metadata[0].contentType;
+  let contentType = metadata[0].contentType;

-  /**
-   * ここで加工
-   */
+  let buffer = await sharp(tmpfile);
+
+  if(requestData.query.size){
+    const size = parseInt(requestData.query.size);
+    buffer = await buffer.resize(size, size).max().withoutEnlargement();
+  }
+  if(requestData.query.webp){
+    buffer = await buffer.webp();
+    contentType = 'image/webp';
+  }
+  buffer = await buffer.toBuffer();

   /**
    * 加工したファイルを読み込んで送信
    */
-  const responseImg = await fs.readFile(tmpfile);

   res.set('Content-Type', contentType);
-  res.send(responseImg);
+  res.send(buffer);
 };
```

🎉
<img width="702" alt="スクリーンショット 2018-10-21 5.19.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/278434/5d7dcffe-cce7-ab11-5a51-43bbbc3413b3.png">

ちなみにサイズはjpgが16.6KBでwebpが11.1KBでした。
オリジナルの5360x3560サイズの場合だとjpgが1MBなのに対してwebpは442KBです。

大きいサイズの画像を変換しているとcloudfunctionsのメモリが足りなくて怒られるので適当にサイズを増やしました。

```bash
gcloud functions deploy img --entry-point resizeImg --runtime nodejs8 --trigger-http --memory 512MB
```

# キャッシュを効かせる
毎回アクセスがあるたびに変換を行うのは時間もお金もかかります。ナンセンスです。

```javascript:diff
@@ -67,5 +67,6 @@ exports.resizeImg = async (req, res) => {
    */

   res.set('Content-Type', contentType);
+  res.set('cache-control', 'public, max-age=3600');
   res.send(buffer);
 };
```

レスポンスヘッダーにcache-controlを追加するだけでGCPのエッジキャッシュを利用できます。同じURLへのアクセスが同じものを返す時は基本的にオンにしておくべきです。

# 圧縮
imageminを使って圧縮してみます。

```javascript:diff
@@ -3,6 +3,9 @@ const os = require('os');
 const path = require('path');
 const fs = require('fs-extra');
 const sharp = require('sharp');
+const imagemin = require('imagemin');
+const imageminMozjpeg = require('imagemin-mozjpeg');
+const imageminPngquant = require('imagemin-pngquant');

 const gcs = new Storage();
 const bucket = gcs.bucket('qiita-auto-resizer');
@@ -63,8 +66,15 @@ exports.resizeImg = async (req, res) => {
   buffer = await buffer.toBuffer();

   /**
-   * 加工したファイルを読み込んで送信
+   * 圧縮
    */
+  buffer = await imagemin.buffer(buffer,
+    {
+      plugins: [
+        imageminMozjpeg({quality:80}),
+        imageminPngquant({quality: '75-90'})
+      ]
+    });

   res.set('Content-Type', contentType);
   res.set('cache-control', 'public, max-age=3600');
```

オリジナルの5360x3560サイズで1MB→882KBになりました。微妙...。

#終わりに
今回作ったfunctionsは↓のURLで確かめられます。

```
https://us-central1-qiita-219915.cloudfunctions.net/img/cat.jpg
```

#参考文献
- [sharp](http://sharp.pixelplumbing.com/en/stable/)
- [Cloud Functions for Firebaseが最高だった話](https://qiita.com/HALU5071/items/e43729ac5b06b0506fbe)