WebサイトURLからカラーパレット生成する

WebサイトのURLを入力するとカラーパレット生成してくれるツールを作成しました。
![スクリーンショット 2022-06-08 1.37.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/983cdd72-8926-c8c4-1633-b6b530f3fd00.png)

↓ 動いてるところ

https://twitter.com/tenugui_taro/status/1534225039641702400

### 何をやっているか
- ヘッドレスブラウザを用いて該当URLのフルスクリーンショット取得
- スクショ画像からカラーパレット生成
- Firebaseを用いたバックエンド実装

![スクリーンショット 2022-06-08 2.19.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/977690b5-3f11-9453-60d2-573d714cdd6e.png)


それぞれ簡単にやり方を記載していきます。

:::note warn
Firebase Local Emulator Suite を使用したローカル処理です。
クラウドへのデプロイ時の挙動は未確認であることをご了承ください。
:::

## Backend

### Firebase初期設定
[ドキュメント](https://firebase.google.com/docs)が詳しいですが簡単に手順を記載します。
任意のフォルダでFirebaseの設定コマンドを実行し、機能の選択とエミュレーターのインストールを行います。

```
$ firebase init

? Which Firebase features do you want to set up for this directory?
 ● Firestore
 ● Functions
 ● Storage
 ● Emulators

=== Emulators Setup
? Which Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices.
　Functions Emulator, Firestore Emulator, Storage Emulator
? Which port do you want to use for the functions emulator? 5001
? Which port do you want to use for the firestore emulator? 8080
? Which port do you want to use for the storage emulator? 9199
? Would you like to enable the Emulator UI? Yes
? Which port do you want to use for the Emulator UI (leave empty to use any available port)? 
? Would you like to download the emulators now? Yes
```

:::note info
エミュレーターのインストールがうまくいかない場合は、[Local Emulator Suite のインストール、構成、統合](https://firebase.google.com/docs/emulator-suite/install_and_configure?hl=ja)を参照し、Node.jsやJava JDKのバージョンなどご確認下さい。
:::


エミュレーター起動コマンドを実行し、ブラウザでデバッグ用画面が表示されれば準備完了です。
```
$ firebase emulators:start

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘
```

![スクリーンショット 2022-06-06 4.49.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/ecf30f6a-ae45-2c9e-dc35-b562ae758416.png)


### 画像からカラーパレット生成
[extract-colors](https://www.npmjs.com/package/extract-colors)を使用して画像からカラーパレットを生成する処理を作成します。

https://qiita.com/tenugui_taro/items/8ce28a7c5a276261861e

使い方はシンプルで、ファイルパスとオプション設定を指定して`extractColors`を実行すると色情報が得られます。

```js
// functions/index.js
const functions = require("firebase-functions");

const path = require("path");
const {extractColorsFromSrc} = require("extract-colors");
const src = path.join(__dirname, "../assets/test.jpg");

exports.helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!");
});

// 画像からカラーパレット生成
exports.colorTest = functions.https.onRequest((request, response) => {
  functions.logger.info("colorTest logs!", {structuredData: true});
  extractColorsFromSrc(src)
      .then(functions.logger.info)
      .then(() => response.send("colorTest from Firebase!"))
      .catch((err) => {
        response.send(`[Error] ${err.message}`);
        functions.logger.warn(err);
      });
});
```

↓ 実行時に得られる色情報
![スクリーンショット 2022-06-06 3.01.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/4de33742-dd15-a7c8-15fb-f3c5a2a274d3.png)

### URLからWebサイトのスクリーンショットを取得する
ヘッドレスブラウザ操作ができる[Puppeteer](https://pptr.dev)を用いて、Webサイトのフルスクーリンショットを取得します。
コードはこちらの記事を参考にさせていただきました。

https://ics.media/entry/220331/

```js
const puppeteer = require("puppeteer");
const {existsSync, mkdirSync} = require("fs");

const SCREENSHOT_DIR = "./screenshot";
const SCREENSHOT_FILE_PATH = `${SCREENSHOT_DIR}/screenshot.png`;
const VIEW_PORT = {width: 1280, height: 720};

exports.getScreenshot = async (url) => {
  if (!existsSync(SCREENSHOT_DIR)) {
    mkdirSync(SCREENSHOT_DIR);
  }

  const browser = await puppeteer.launch();

  const page = await browser.newPage();
  await page.setViewport(VIEW_PORT);
  await page.goto(`${url}`);

  await page.screenshot({
    path: SCREENSHOT_FILE_PATH,
    fullPage: true,
  });

  await browser.close();
};
```

```js
// functions/index.js
const functions = require("firebase-functions");

exports.screenshotTest = functions.https.onRequest(
    async (request, response) => {
      functions.logger.info("screenshotTest logs!", {structuredData: true});

      const url = request.query.url;
      if (typeof url !== "string" || url.length === 0) {
        response.send("invalid url!");
      }
      getScreenshot(url)
          .then(functions.logger.info)
          .then(() => response.send("screenshotTest from Firebase!"))
          .catch((err) => {
            response.send(`[Error] ${err.message}`);
            functions.logger.warn(err);
          });
    },
); 
```


適当にURLを渡して呼び出すと`screenshot`フォルダにフルスクリーンショットが格納されます。
![スクリーンショット 2022-06-06 3.48.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/01820757-7ec9-6ae1-82a5-4359b4139a97.png)

### Storageへの画像保存
[Firebase Admin SDK](https://firebase.google.com/docs/admin/setup?hl=ja)を用いてバケットを利用します。
`bucket.upload`で指定した格納先に画像があることを確認します。

```js
// functions/index.js
const functions = require("firebase-functions");

// バケット初期化
const admin = require("firebase-admin");
admin.initializeApp();
const bucket = admin.storage().bucket();

// ストレージへの画像保存
exports.storageTest = functions.https.onRequest(
    (request, response) => {
      functions.logger.info("storageTest logs!", {structuredData: true});

      bucket.upload(
          "./screenshot/screenshot.png",
          {destination: "uploads/screenshot.png"},
      );

      response.send("storageTest from Firebase!");
    },
);
```

![スクリーンショット 2022-06-06 5.34.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/ba65c831-c916-062e-dd7c-565aa0f126fe.png)

### FireStoreへの保存
Cloud Storageへの画像保存と同様にFirebase Admin SDKを用いて保存します。
```js
// functions/index.js
const functions = require("firebase-functions");

const admin = require("firebase-admin");
admin.initializeApp();
const collection = admin.firestore().collection("uploads");

exports.firestoreTest = functions.https.onRequest(
    (request, response) => {
      functions.logger.info("firestoreTest logs!", {structuredData: true});

      collection.add({text: "hello"});

      response.send("firestoreTest from Firebase!");
    },
);
```
![スクリーンショット 2022-06-06 5.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/cc0f9202-b36f-bc8c-3bf8-f2c79a4d8cb4.png)

## Frontend
### Firebase Local Emulator Suiteとの接続
エミュレーターと接続するには、`initializeApp`で初期化したのちにポート番号を指定してローカルへ向き先を設定します。

```js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore";
import { getStorage, connectStorageEmulator } from "firebase/storage";

const firebaseConfig = {
  // ...
};

initializeApp(firebaseConfig);

const db = getFirestore();
connectFirestoreEmulator(db, "localhost", 8080);

const storage = getStorage();
connectStorageEmulator(storage, "localhost", 9199);
```

### 終わりに
Firebase Local Emulator Suiteはバックエンド実装の手間を省けるので個人ツール程度であればとても使いやすく感じました。
また、スクリーンショット取得の部分ですがスクロールアニメーションなどに対応するのであれPuppeteerでの設定が必要になりますのでご注意ください。