NodeJSの画像処理ライブラリ「sharp」を使って画像を連結する

# はじめに
NodeJSでImageMagickのappendのようにサイズがバラバラの画像を単純に横並びで連結させたい機会があったのですが、~~単純すぎて~~中々記事が見つからなかったので備忘録的な意味合いも込めて書き残します。

# やること
- sharpで画像情報取得、連結

# バージョン
NodeJS: v12.14.1
sharp: v0.23.4

# 成果物

## 出来上がる画像
![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469399/ced66353-9122-c832-7e74-4b908dc9f1a2.png)


## ソースコード

```bash:terminal
yarn init -y
yarn add sharp
```
ソースはこちら
https://github.com/engabesi/appendImages

まず全文を貼ります。

```javascript:index.js
const sharp = require("sharp");

(async () => {
  const imagePaths = ["images/1.jpg", "images/2.jpg", "images/3.jpg"];
  const imageAttrs = [];

  // 連結する画像の情報取得
  const promises = [];
  const imagePromise = path =>
    new Promise(async resolve => {
      const image = await sharp(path);
      let width = 0,
        height = 0;
      await image
        .metadata()
        .then(meta => ([width, height] = [meta.width, meta.height]));
      const buf = await image.toBuffer();
      resolve({ width, height, buf });
    });
  imagePaths.forEach(path => promises.push(imagePromise(path)));
  await Promise.all(promises).then(values => {
    values.forEach(value => imageAttrs.push(value));
  });

  // outputする画像の設定
  const outputImgWidth = imageAttrs.reduce((acc, cur) => acc + cur.width, 0);
  const outputImgHeight = Math.max(...imageAttrs.map(v => v.height));
  let totalLeft = 0;
  const compositeParams = imageAttrs.map(image => {
    const left = totalLeft;
    totalLeft += image.width;
    return {
      input: image.buf,
      gravity: "northwest",
      left: left,
      top: 0
    };
  });

  // 連結処理
  sharp({
    create: {
      width: outputImgWidth,
      height: outputImgHeight,
      channels: 4,
      background: { r: 255, g: 255, b: 255, alpha: 0 }
    }
  })
    .composite(compositeParams)
    .toFile("output.png");
})();
```

これで`node index.js`で実行すると上記のように左上詰めで画像が連結されます。

# 解説
まず`sharp`をimportします。
sharpについて詳しくは公式Documentを見てください。
https://github.com/lovell/sharp
https://sharp.pixelplumbing.com/en/stable/

```javascript
const sharp = require("sharp");
```

## 画像情報取得
連結する画像パスの配列と画像情報を格納する配列を宣言します。

```javascript
const imagePaths = ["images/1.jpg", "images/2.jpg", "images/3.jpg"];
const imageAttrs = [];
```

画像のwidth, height, bufferを取得する`Promise`を作成し、`Promise.all`で全画像分並列実行させます。
`Promise.all`の`then`で先程宣言した画像情報を格納する配列に取得した情報をpushします

```javascript
const promises = [];
const imagePromise = path =>
  new Promise(async resolve => {
    const image = await sharp(path);
    let width = 0, height = 0;
    await image
      .metadata()
      .then(meta => ([width, height] = [meta.width, meta.height]));
    const buf = await image.toBuffer();
    resolve({ width, height, buf });
  });
imagePaths.forEach(path => promises.push(imagePromise(path)));
await Promise.all(promises).then(values => {
  values.forEach(value => imageAttrs.push(value));
});
```
なぜPromise実行時(`imagePromise(path)`)の`then`で処理しないのかですが、こちらの`then`は実行が完了した順に走ることになります。
今回は連結される並びを固定にしたいので`Promise.all`の`then`で処理を行っています。

## 出力画像設定
`width`には全画像のwidth合計値、`height`には全画像のheightの最大値を取得します

```javascript
const outputImgWidth = imageAttrs.reduce((acc, cur) => acc + cur.width, 0);
const outputImgHeight = Math.max(...imageAttrs.map(v => v.height));
```

`sharp`は`composite`というメソッドで画像の結合処理等を行います。
その`composite`メソッドのparam設定を行います

```javascript
let totalLeft = 0;
const compositeParams = imageAttrs.map(image => {
  const left = totalLeft;
  totalLeft += image.width;
  return {
    input: image.buf,
    gravity: "northwest",
    left: left,
    top: 0
  };
});
```
`input`に画像のbufferを入れます。
`gravity`は方角で指定します。今回左上詰めで連結させるので`northwest`を入れます。
`left`と`top`はoffsetをpx単位で設定します。
詳しくは`sharp`の公式Docを読んでください。
https://sharp.pixelplumbing.com/en/stable/api-composite/

## 画像出力

最後に`sharp`で画像を出力します。

```javascript
sharp({
  create: {
    width: outputImgWidth,
    height: outputImgHeight,
    channels: 4,
    background: { r: 255, g: 255, b: 255, alpha: 0 }
  }
})
  .composite(compositeParams)
  .toFile("output.png");
```

これで`output.png`がrootに作成されます。
連結の余白部分が透明でなくてもよかったり、サイズを削減したい場合は、
`channels`を4から3に変更して`background`のalphaを消したり、
出力画像拡張子をjpgに変更等好きなように変更してください。

# まとめ
これで`reject`を使ってエラーハンドリングをしていない等、色々ゴリ押し気味ですが画像連結処理を実装できました。
一応車輪の再発明をしていないかとドキュメントを眺めましたがそれらしい処理が見当たりませんでした。
もしあれば教えていただけると助かります。