【node】JimpをTypescript+browserで使ってみる

# 前書き

javascriptで画像処理をしたいと思って検索していたら見つけたライブラリ。
よくみる犬のアイコンを想像したが、名前の由来が似ているだけで(おそらく)関係ない

今回はこのライブラリを使って画像処理をしてみた、という話より、
このライブラリをどうやってTypescript+Browser側で使用したかというお話。

# 動作環境

- node v18.12.1
- typescript v5.2.2
- react v18.2.0
- vite v4.4.9
- jimp v0.22.10
- use ESModules

# Jimpとは

[jimp - npm](https://www.npmjs.com/package/jimp)

全てjavascriptのコードで書かれており、ネイティブ依存なしのライブラリ。
`jpeg`、`png`、`bmp`、`tiff`、`gif`の形式に対応していて、リサイズや画像加工などができる。

# 使ってみる

```ts
import Jimp from 'jimp'

const main = async () => {
  const image = await Jimp.read('image.png')
  image.resize(256, 256)
  image.write('output.png')
}
```

`image.png`画像を読み込んで、256x256にリサイズして`output.png`として出力するコード。
サーバーサイドで動かす場合は、これだけで動く。とてもシンプル

# browserで使う

ではこれをブラウザで動かす。
ファイルを読み込み、またはファイルの書き出しはブラウザらしい挙動にするため、
`<input type="file">`と、canvasを用意する。

```html
<input type="file" id="file" />
<canvas id="canvas"></canvas>
```

```ts:index.ts
import Jimp from 'jimp'

const main = async () => {
  const file = document.getElementById('file') as HTMLInputElement
  const canvas = document.getElementById('canvas') as HTMLCanvasElement
  const ctx = canvas.getContext('2d')

  file.addEventListener('change', async () => {
    // inputからファイルを読み込む
    const arrayBuffer = await file.files[0].arrayBuffer()
    // Jimpで読み込み、リサイズ
    const image = await Jimp.read(Buffer.from(arrayBuffer))
    image.resize(256, 256)
    // canvasに描画
    const imageData = new ImageData(
      Uint8ClampedArray.from(image.bitmap.data),
      image.bitmap.width,
      image.bitmap.height
    )
    ctx.putImageData(imageData, 0, 0)
  })
}
```

これで、inputから画像を読み込んで、リサイズしてcanvasに描画することができる。
・・・ように見えて、実際には動かなかった。

```
Module "fs" has been externalized for browser compatibility. Cannot access "fs.existsSync" in client code.
```

`Jimp`の何処かに`fs`が使われているらしく、このままでは動かない。

# ブラウザのjimpを読み込む

[jimp/packages/jimp/browser/README.md at main · jimp-dev/jimp](https://github.com/jimp-dev/jimp/blob/main/packages/jimp/browser/README.md)

調べてみると、ブラウザ用も用意されていた。なのでこれをimportする。

```ts:index.ts
import 'jimp/browser/lib/jimp.js'

const main = async () => {
  // ...
}
```

ファイルから何か変数を読み込むのではなく、単純にファイルを読み込むだけ。
そうすると、Jimpがグローバルに定義されるようになる。
javascriptであればおそらくこれで終わり。

しかし、typescriptでは、Jimpが定義されていないというエラーが出る。
なので、型だけを定義する必要がある。

# 型定義を追加する

これが一番良くわからなかった。
シンプルに表現するのであれば、

```ts:index.ts
import 'jimp/browser/lib/jimp.js'

declare const Jimp: any

const main = async () => {
  // ...
}
```

と、anyで定義してしまえば良いのだが、もちろん補完は効かなくなる。

[Issuesで検索をかけてみて、](https://github.com/jimp-dev/jimp/issues?q=is%3Aissue+typescript+)それっぽいissueを探すも、うまくいかず。

色々試行錯誤した結果、以下の型定義ファイルを作成することで、それっぽい動きになった。

```ts:types/jimp.d.ts
import Jimp from 'jimp'

type JimpObject = typeof Jimp
type JimpType = Jimp
```

```ts:index.ts
import 'jimp/browser/lib/jimp.js'
import { JimpObject } from '../types/jimp'

declare const Jimp: JimpObject

const main = async () => {
  // ...
}
```

Jimpファイルの型定義ファイルをみにいくと、`Jimp`は、型(?)定義とオブジェクト(クラス?)定義の両方が1つになっている。
そのため、`typeof Jimp`と定義すると、`Jimp.read()`などのメソッドの戻り値の型がうまくいかず、
`Jimp`と定義すると、`Jimp.read()`などのメソッドが存在しないというエラーが出る。

これをうまく繋げて、1つの定義に収める方法がわからなかったため、2つに分けて定義した。

```ts:read.ts
import { JimpObject, JimpType } from '../types/jimp'
declare const Jimp: JimpObject

const read = (file: string): Promise<JimpType> => {
  const image = await Jimp.read(Buffer.from(arrayBuffer))
  return image
}
```

このように、グローバル定義されているJimpには`JimpObject`を、
`Jimp.read()`などのメソッドの戻り値の型には`JimpType`を使うことで、
補完が効くようになった。

# 終わりに

最初の方に型補完が効かないと気づけばおそらく別のライブラリを使っていたのだが、
なぜか一通りコードが書くまで気が付かなかったので、試行錯誤してしまった。
ブラウザでの画像処理むずかしい。。

# 参考サイト

[How to draw jimp image onto canvas · Issue #215 · jimp-dev/jimp](https://github.com/jimp-dev/jimp/issues/215)