【JavaScript】フォーム上などの画像ファイルをブラウザだけでリサイズ

JavaScriptで画像ファイルをリサイズしたい時があると思います。僕はありました。
単に[File](https://developer.mozilla.org/ja/docs/Web/API/File)を投入して変換するコードが意外となかったのでここに共有します。
この例では、リサイズ後の画像をブラウザからダウンロードの形で取得できるようにしました。

# DEMO
<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="html,result" data-user="jukaism" data-slug-hash="GRJRrPW" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="GRJRrPW">
  <span>See the Pen <a href="https://codepen.io/jukaism/pen/GRJRrPW">
  GRJRrPW</a> by hiroism (<a href="https://codepen.io/jukaism">@jukaism</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# Canvasを利用します
Canvasは図を表示するための仕様ですが、機能が豊富なので画像加工にも利用できます。

```html
  <body>
    <canvas style="width: 484px; height: 253px;" id="canvas"></canvas>
  </body>
```
画像をリサイズしたい幅と高さを指定しておきます。邪魔なら`display: none;`も可。

# コード全体
htmlファイルにしてブラウザで開けば動くコードです。
スクリプト内の**resize(file)**にFileを投入するほか、
添付フィールドを用意したので、ここから画像を選んでも動作します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272677/bf72f6e5-d2f9-376d-6dc2-0a8cc857e717.png)

```html
<html lang="ja">
  <body>
    <input type="file" id="image_zone">
    <br>
    <canvas style="width: 484px; height: 253px;" id="canvas"></canvas>
    <script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script> <!-- IEで必要。不要なら削除を -->
    <script>
      // image_zoneに新しいファイルがセットされた時、resizeを呼ぶ
      const imageZone = document.getElementById('image_zone')
      imageZone.addEventListener('change', resizePinnedImage, false)
      function resizePinnedImage(e) {
          const file = e.target.files[0]
          if (!file.type.match('image.*')) { return }
          resize(file)
      }
      // Canvasに画像をリサイズして貼り、終わったらブラウザからダウンロードっぽく取得
      function resize(file) {
        imageToCanvas(file).then(function (canvas) {
          // [5] キャンバスから画像を取得
          if (canvas.msToBlob) {
            window.navigator.msSaveBlob(canvas.msToBlob(), 'resized.png')
          } else {
            const a = document.createElement('a')
            a.href = canvas.toDataURL(file.type)
            a.download = 'resized.' + file.type.split('/')[1]
            a.click()
          }
        }).catch(function (error) {
          console.error(error)
        })
      }

      function imageToCanvas (imageFile) {
        // [1] Fileから画像データを得る
        return new Promise(function (resolve, reject) {
          readImage(imageFile).then(function (src) {
            loadImage(src).then(function (image) {
              // [2] Canvasの呼び出し
              const canvas = document.getElementById("canvas")
              const ctx = canvas.getContext('2d')
              // [3] 縮尺を計算
              const scale = Math.max((canvas.height / image.height), (canvas.width / image.width))
              // [4] 画像を切り取り、Canvasに展開して返す
              ctx.drawImage(
                // Canvasに貼り付ける画像
                image, 
                // 画像の切り取り開始位置（左上からの横距離、縦距離）
                (image.width - (canvas.width / scale)) / 2, (image.height - (canvas.height / scale)) / 2,
                // 画像の切り取り範囲（幅、高さ）
                canvas.width / scale, canvas.height / scale,
                // キャンバスの貼り付け開始位置（左上からの横距離、縦距離）
                0, 0,
                // 画像への貼り付け範囲（幅、高さ）
                canvas.width, canvas.height
              )
              resolve(canvas)
            }).catch(function (error) {
              reject(error)
            })
          }).catch(function (error) {
            reject(error)
          })
        })
      }

      // [1-A] Fileの中身を得る
      function readImage(image) {
        return new Promise(function (resolve, reject) {
          const reader = new FileReader()
          reader.onload = function () { resolve(reader.result) }
          reader.onerror = function (e) { reject(e) }
          reader.readAsDataURL(image)
        })
      }
      // [1-B] 空の画像データを作った後に中身を投入
      function loadImage(src) {
        return new Promise(function (resolve, reject) {
          const img = new Image()
          img.onload = function () { resolve(img) }
          img.onerror = function (e) { reject(e) }
          img.src = src
        })
      }
    </script>
  </body>
</html>

```

 **[1] FileをImageにする** 
FileReaderでFileの中身を取り出し、空のImageに投入します。
このあたりでPromiseを多用しているためIE用のPolyfillを入れています。

```javascript
    <script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
      // ..略
      function imageToCanvas (imageFile) {
        // [1] Fileから画像データを得る
        return new Promise(function (resolve, reject) {
          readImage(imageFile).then(function (src) {
            loadImage(src).then(function (image) {
            // ここに画像データ(image)が渡ってくるので処理
            }).catch(function (error) {
              reject(error)
            })
          }).catch(function (error) {
            reject(error)
          })
        })
      }
      // [1-A] Fileの中身を得る
      function readImage(image) {
        return new Promise(function (resolve, reject) {
          const reader = new FileReader()
          reader.onload = function () { resolve(reader.result) }
          reader.onerror = function (e) { reject(e) }
          reader.readAsDataURL(image)
        })
      }
      // [1-B] 空の画像データを作った後に中身を投入
      function loadImage(src) {
        return new Promise(function (resolve, reject) {
          const img = new Image()
          img.onload = function () { resolve(img) }
          img.onerror = function (e) { reject(e) }
          img.src = src
        })
      }
```

 **[2] HTML上のCanvasの呼び出し** 
キャンバスと、その描画用の[コンテキスト](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D)を呼び出します。

```javascript
              // [2] Canvasの呼び出し
              const canvas = document.getElementById("canvas")
              const ctx = canvas.getContext('2d')
```

**[3] 縮尺の計算**
アスペクト比を維持したいので、縦横どちらか大きい方の縮小率を採用します。

```javascript
              // [3] 縮尺を計算
              const scale = Math.max((canvas.height / image.height), (canvas.width / image.width))
```
**[4] 画像を切り取り、Canvasに展開**
Canvasのメソッド`drawImage`を使って画像を貼り付け。
この時画像の切り取り範囲とCanvasへの貼付け範囲を一気に指定する。混乱しがち。
本当はここで画像にしたかったが、ブラウザによってこの後のデータの取り出し方が違うのでここではCanvasを返す。

```javascript
              // [4] 画像を切り取り、Canvasに展開して返す
              ctx.drawImage(
                // Canvasに貼り付ける画像
                image, 
                // 画像の切り取り開始位置（左上からの横距離、縦距離）
                (image.width - (canvas.width / scale)) / 2, (image.height - (canvas.height / scale)) / 2,
                // 画像の切り取り範囲（幅、高さ）
                canvas.width / scale, canvas.height / scale,
                // キャンバスの貼り付け開始位置（左上からの横距離、縦距離）
                0, 0,
                // 画像への貼り付け範囲（幅、高さ）
                canvas.width, canvas.height
              )
              resolve(canvas)
```

**[5] Canvasから画像を取得**
Canvasにリサイズ後の画像が表示され、[Canvasの各種メソッド](https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement)でDataURLやBlobが取得出来る。
サンプルではブラウザからダウンロードできるようにした。IEでもいけますがpng固定になるので注意。

```javascript
          // [5] キャンバスから画像を取得
          if (canvas.msToBlob) { // IEの場合
            window.navigator.msSaveBlob(canvas.msToBlob(), 'resized.png')
          } else { // その他ブラウザ
            const a = document.createElement('a')
            a.href = canvas.toDataURL(file.type)
            a.download = 'resized.' + file.type.split('/')[1]
            a.click()
          }
```

以上です。