ダミー画像生成api（placeなんちゃら）をnode.js + expressで作るためのメモ

## はじめに
[placekitten](https://placekitten.com/)とか[placehold](http://placehold.jp/)みたいなダミー画像生成サービスを作りたいと思ったのでちょっと試してみた記録です。
正直秒で作れたので特に言うことはないです。

## 完成形イメージ
![スクリーンショット 2023-03-20 20.54.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/be1d84d1-208d-6970-d27d-6ed5d34133ec.png)
![スクリーンショット 2023-03-20 20.53.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/fb340393-e6d8-acea-6d87-bfcd32f6f4a2.png)
![スクリーンショット 2023-03-20 20.54.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/c199a826-1fb1-c986-8fbb-8bff33529107.png)

それじゃ実装を説明します。

## 実装

### 事前準備

まずは適当なフォルダ作ってその中で、下記のような形で必要なライブラリをインストールしましょう。
```
npm init -y
npm install express cors sharp
```
※`sharp`は画像リサイズ用のライブラリです（https://sharp.pixelplumbing.com/）

ファイル構成は下記のような形にします。`index.html`は中身適当でいいですが、下記に一応自分のを置いときます。
私の画像はwaifu diffusionで生成したものを利用していますが、何でもOKです。（jsのサイズ指定部分を合わせて変更して下さい。）

```
.
├── images
│   ├── 1.png
│   ├── 2.png
│   ├── 3.png
│   ├── 4.png
│   ├── 5.png
│   ├── 6.png
│   ├── 7.png
│   ├── 8.png
│   ├── 9.png
│   └── 10.png
├── index.html
├── index.js
├── node_modules
├── package-lock.json
├── package.json
└── readme.md
```

### index.html
```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>placeImage</title>
  </head>
  <body>
    <h1>placeImage</h1>
    <img src="http://localhost:3000/400/300" />
    <h2>使い方</h2>
    <p>下記のように幅と高さを指定すれば画像を取得できます。</p>
    <p class="caution">
      注意：幅512px、高さ512px以上の指定は、512pxにそれぞれリサイズされますのでご注意下さい。<br />
      また、3桁以上の数字を入力した場合や数値以外の値を入力した場合は404エラーとなります。
    </p>
    <p class="link">
      例：<a href="http://localhost:3000/400/300" target="_blank"
        >http://localhost:3000/400/300</a
      >
    </p>
  </body>
</html>

```

あと、`package.json`にindex.jsを起動するための記述を追加して下さい。

```json
"scripts": {
    "start": "node index.js"
}
```

## apiの実装

コードの全体像が下記です。先程作成した`index.js`内に記述して下さい。

```javascript
const express = require("express");
const path = require("path");
const cors = require("cors");
const sharp = require("sharp");

const app = express();

// 画像の数を入れる定数
const imagesLength = 10;

// 他のサービスで利用する場合にCORSエラーが起きないようにするやつ（ローカルだけならいらない）
app.use(cors());

// indexページの生成
app.get("/", (req, res) => {
  res.sendFile(__dirname + "/index.html");
});

// widthとheightを1~4桁で取ってランダムな画像をトリミングして表示
app.get("/:width([0-9]{1,3})/:height([0-9]{1,3})", async (req, res) => {
  const width = Math.min(Number(req.params.width), 512);
  const height = Math.min(Number(req.params.height), 512);

  let fileName = Math.floor(Math.random() * imagesLength) + 1 + ".png";
  let filepath = path.join(__dirname, `images/${fileName}`);

  try {
    const resizedBuffer = await sharp(filepath)
      .resize(width, height, { fit: "cover" })
      .toBuffer();

    const encodedBuffer = resizedBuffer.toString("base64");

    var img = Buffer.from(encodedBuffer, "base64");

    res.writeHead(200, {
      "Content-Type": "image/png",
      "Content-Length": img.length,
    });
    res.end(img);
  } catch (e) {
    res.status(404).send("<h1>404 page not found</h1>");
  }
});

// 404ページ
app.all("*", (req, res) => {
  res.status(404).send("<h1>404 page not found</h1>");
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log("listen on port:", port));
```

重要な画像の取得部分のみ説明します。

```javascript
// widthとheightを1~4桁で取ってランダムな画像をトリミングして表示
app.get("/:width([0-9]{1,3})/:height([0-9]{1,3})", async (req, res) => {
  const width = Math.min(Number(req.params.width), 512);
  const height = Math.min(Number(req.params.height), 512);

  let fileName = Math.floor(Math.random() * imagesLength) + 1 + ".png";
  let filepath = path.join(__dirname, `images/${fileName}`);

  try {
    const resizedBuffer = await sharp(filepath)
      .resize(width, height, { fit: "cover" })
      .toBuffer();

    const encodedBuffer = resizedBuffer.toString("base64");

    var img = Buffer.from(encodedBuffer, "base64");

    res.writeHead(200, {
      "Content-Type": "image/png",
      "Content-Length": img.length,
    });
    res.end(img);
  } catch (e) {
    res.status(404).send("<h1>404 page not found</h1>");
  }
});
```

expressではパスに正規表現を利用できるため、下記のような形で横幅と高さのパラメータを制限しています。
今回は数値かつ、1~3桁の範囲で指定しています。
`"/:width([0-9]{1,3})/:height([0-9]{1,3})"`

下記で横幅、高さを指定します。今回は最大値を512にしていますが、好きに変更して下さい。
```javascript
const width = Math.min(Number(req.params.width), 512);
const height = Math.min(Number(req.params.height), 512);
```

ここで画像ファイルをランダムに取得しています。
```javascript
let fileName = Math.floor(Math.random() * imagesLength) + 1 + ".png";
let filepath = path.join(__dirname, `images/${fileName}`);
```

最後に画像をバッファにしてレスポンスに渡しています。
何らかの理由で画像が表示できなければ404ページを表示します。
```javascript
try {
    const resizedBuffer = await sharp(filepath)
      .resize(width, height, { fit: "cover" })
      .toBuffer();

    const encodedBuffer = resizedBuffer.toString("base64");

    var img = Buffer.from(encodedBuffer, "base64");

    res.writeHead(200, {
      "Content-Type": "image/png",
      "Content-Length": img.length,
    });
    res.end(img);
  } catch (e) {
    res.status(404).send("<h1>404 page not found</h1>");
  }
```

以上です。


それでは〜〜〜


p.s.
1年前に転職してから一個も記事あげられてなかったのですが、ようやく記事を書けました...