Help us understand the problem. What is going on with this article?

Cloud Functionsで動的に画像変換

More than 1 year has passed since last update.

目標

cat.jpg
↑こういう画像をcat.jpg?size=1000とかcat.jpg?encode=webpとかでサイズ変換したり、webpにエンコードしたりして配信できるようにする。

ちなみにこの猫画像はpexelsでcatと検索したら一番上に出てきたものです。

準備

当たり前ですがGCPを使うのでgcloudをインストールしておきます。

あとGCPのプロジェクトをセットアップして、適当にバケットを作成しておきます。
スクリーンショット 2018-10-20 0.08.56.png

関数をデプロイしてみる

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

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

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

gcloud functions deploy helloGET --runtime nodejs6 --trigger-http

と入力するとCloudFunctionsにデプロイできます。
デプロイした関数のURLはこんな感じ↓で、アクセスしてみると「Hello World!」と表示されます。
https://us-central1-qiita-219915.cloudfunctions.net/helloGET
スクリーンショット 2018-10-21 4.01.52.png

必要なライブラリを追加

yarn init -y
yarn add @google-cloud/storage fs-extra sharp imagemin imagemin-mozjpeg imagemin-pngquant

@google-cloud/storage

GCPのStorageにアクセスするのに使います。

fs-extra

普段使っているので。ensureDirが便利。

sharp

imagemagickより早いらしい。

imagemin

画像を圧縮してくれます。圧縮しないなら不要。
それぞれの形式にあったプラグインと共に使用します。今回はjpgとpng用のものを用意。

GCS

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

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);
};
gcloud functions deploy img --entry-point resizeImg --runtime nodejs8 --trigger-http

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

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

スクリーンショット 2018-10-21 4.39.31.png
↑アクセスするとファイルが降ってきます💧

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を指定してあげます。
スクリーンショット 2018-10-21 4.51.32.png

画像加工

今回使用するライブラリの「sharp」は色々機能が付いていますが、とりあえずリサイズとwebp変換を実装してみます。

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);
 };

🎉
スクリーンショット 2018-10-21 5.19.56.png

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

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

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

キャッシュを効かせる

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

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を使って圧縮してみます。

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

参考文献

tsin1rou
普段はReact周りを弄っています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした