画像をリサイズしてblobでプレビュー表示する方法【Vue/Canvas】

ユーザーが画像をアップロードした際に、サーバー側へPOSTする前に
クライアント側で（容量が大きい場合）リサイズし、プレビュー表示したかったので実装しました


`2019.6.4 追記`
ライブラリの使用が可能なら下記記事のほうが簡単に出来ます
* [【簡単】画像選択時のプレビュー表示とExif対応【JavaScript-Load-Image/Vue】 - Qiita](https://qiita.com/kawa64372358/items/f8f729a29f0b980f7e32)


## やっていること

- 画像をupload
- 内部でリサイズ
- base64からblobへトランスコード
- プレビュー表示

実際の挙動はこんな感じです！

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239038/459495fe-c76f-2b58-1b2a-22f52815c846.gif)

# 実際のコード
## template部分
```index.vue
<template>
  <div class="resize-img">
    <!-- 画像選択 -->
    <div v-show="!resizedImg" class="resize-img__post">
      <label for="file" class="resize-img__post__label">画像
        <input
          id="file"
          ref="fileInput"
          type="file"
          accept=".jpeg, .png"
          @change="uploadImg">
      </label>
    </div>
    <!-- プレビュー -->
    <div v-show="resizedImg" class="resize-img__preview">
      <div class="resize-img__preview__circle" @click="clearUploadImg">
        <span class="resize-img__preview__circle__close-icon">×</span>
      </div>
      <canvas ref="canvas" class="resize-img__preview__canvas"/>
    </div>
  </div>
</template>
```

## script部分
```index.vue

<script>
export default {
  data() {
    return {
      resizedImg: null
    };
  },
  destroyed() {
    this.clearUploadImg();
  },
  methods: {
    uploadImg(e) {
      const file = e.target.files[0];
      const reader = new FileReader();
      reader.onload = (e) => {
        this.generateImgUrl(e.target.result);
      };
      reader.readAsDataURL(file);
    },
    generateImgUrl(file) {
      const image = new Image();
      image.crossOrigin = 'Anonymous';

      image.onload = (e) => {
        const resizedBase64 = this.makeResizeImg(image);
        // リサイズ済みのBase64をblobに変換
        const resizedBlob = this.base64ToBlob(resizedBase64);
        // urlを生成してプレビュー表示できるようにする
        const resizedImg = window.URL.createObjectURL(resizedBlob);
        this.resizedImg = resizedImg;
      };
      image.src = file;
    },
    makeResizeImg(image) {
      const canvas = this.$refs.canvas;
      const ctx = canvas.getContext('2d'); // 2Dコンテキスト
      // 縦横で長い方の最大値を1000とする
      const MAX_SIZE = 1000;

      // MAX_SIZEよりも小さかったらそのまま
      if (image.width < MAX_SIZE && image.height < MAX_SIZE) {
        [canvas.width, canvas.height] = [image.width, image.height];
        ctx.drawImage(image, 0, 0);
        return canvas.toDataURL('image/jpeg');
      }

      let dstWidth;
      let dstHeight;
      // 縦横比の計算
      if (image.width > image.height) {
        dstWidth = MAX_SIZE;
        dstHeight = (image.height * MAX_SIZE) / image.width;
      } else {
        dstHeight = MAX_SIZE;
        dstWidth = (image.width * MAX_SIZE) / image.height;
      }
      canvas.width = dstWidth;
      canvas.height = dstHeight;
      // リサイズ
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, dstWidth, dstHeight);

      // data_url形式に変換したものを返す
      return canvas.toDataURL('image/jpeg');
    },
    clearUploadImg() {
      this.resizedImg = null;
      if (this.$refs.fileInput && this.$refs.fileInput.value !== undefined) {
        this.$refs.fileInput.value = '';
      }
    },
    base64ToBlob(base64) {
      const bin = atob(base64.replace(/^.*,/, ''));
      const buffer = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i++) {
        buffer[i] = bin.charCodeAt(i);
      }
      return new Blob([buffer.buffer], {
        type: 'image/png'
      });
    }
  }
};
</script>
```

## css部分
```style.scss
.resize-img {
  width: 300px;
  height: 300px;
  margin: 0 auto;
  margin-top: 20px;

  &__post {
    border: 1px solid rgba(#000, 0.16);
    line-height: 30rem;

    &__label {
      display: inline-block;
      width: 100%;
      color: rgba(0, 0, 0, 0.4);
      text-align: center;

      & > input {
        display: none;
      }
    }
  }

  &__preview {
    width: 300px;
    height: 300px;

    &__circle {
      position: absolute;
      right: 37px;
      width: 27px;
      height: 27px;
      margin: 5px;
      padding: 2px 9px;
      border-radius: 50%;
      background-color: rgba(0, 0, 0, 0.3);

      &__close-icon {
        color: #fff;
      }
    }

    &__canvas {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  }
}
```

## 試しに`MAX_SIZE`を変更して検証
元画像のサイズ　`960 × 641`
![スクリーンショット 2019-05-22 17.36.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239038/90f59ff5-48cd-89ee-0863-cf6edc031b00.png)

### MAX_SIZE = 1000 の場合

![スクリーンショット 2019-05-22 18.52.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239038/4a0922de-eb44-74e5-82b7-c63cb2acec11.png)

画像はMAX_SIZEよりも小さいので原寸表示されます

### MAX_SIZE = 500 の場合

![スクリーンショット 2019-05-22 18.52.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239038/1ff32ac6-e021-838e-b018-11e6479abecf.png)

画像はMAX_SIZEよりも大きいので500pxにリサイズされます。いい感じ！

# 参考
弊社フロントエンドエンジニアの記事！まさしくな記事で参考にさせていただきました
* [Vue.jsでファイルアップロードの実装--ついでにプレビュー機能も実装した話-- - Qiita](https://qiita.com/sakiyama0101/items/655729bc1893392dfd42)

実際にリサイズできるサービスのリンクと中のコードが書いてあって参考になりました
* [HTMLとJavaScriptで画像をリサイズ | blog.PanicBlanket.com](http://blog.panicblanket.com/archives/4504)

Nuxtで書かれていて参考になりました
* [Nuxt.js(vue.js)で画像を縮小してFirebaseStorageにアップロードする - Qiita](https://qiita.com/5ou/items/e82abf8d3b24fbf98514)

サーバーへPOSTする際のファイル形式の参考になりました
（base64とかblobとか分からなかったので・・）
* [Canvasで描画した画像を送信してサーバに保存する - Qiita](https://qiita.com/0829/items/a8c98c8f53b2e821ac94#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E5%81%B4)

canvasのdrawImageがよく分からなかったので、参考になりました
* [drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)－Canvasリファレンス](http://www.htmq.com/canvas/drawImage_s.shtml)