Nuxt.js(vue.js)で画像を縮小してFirebaseStorageにアップロードする

# やりたいこと
- ユーザーが画像をアップロードできるページを作りたい
- 確認用サムネイルも表示したい
  - サムネイルは「どんな画像か」がわかるだけの小さなもの
  - アップロードするファイルとは別物 
- フロントで圧縮してからアップすることで通信量を節約したい
  - Firebase Storageの使用量も節約したい
- ~~サーバーサイドは書きたくない~~

# 手順
1. `input[type="file"]`でファイルを指定する
2. `new FileReader()`でファイル情報を取得する
3. `new Image()`でimg要素を作る
4. `img.src`に「2.」のファイル情報を放り込んで画像を作成する
5. `document.createElement('canvas')`で`canvas`要素を作る
6. 「5.」の`canvas`に「4.」の画像をリサイズしつつ貼り付ける
7. あらかじめ用意しておいたサムネイル用の`canvas`要素に「4.」の画像をリサイズしつつ貼り付ける
8. `toDataURL('image/jpeg')`で`data_url`形式の情報を取得する
9. FirebaseStorageにアップする

# コード

```vuejs:photoResize.vue
<template>
  <div>
    <input type="file" v-on:change="resize" accept=".jpg, .png" ref="input">
    <div>
      <canvas ref="thumbnail" :width="0" :height="0">
      <button v-on:click="reset">×</button>
    </div>
    <div>
      <button v-on:click="upload">upload</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newImage: '',
    }
  },
  methods: {
    resize(e) {
      const file = e.target.files[0]
      const image = new Image()
      const reader = new FileReader()
      const vm = this

      reader.readAsDataURL(file)
      reader.onload = (e) => {
        image.src = e.target.result
        image.onload = () => {
          vm.newImage = this.width < 1280 ? this.src : vm.makeImage(image)
          vm.makeTumbnail(image)
        }
      }
    },
    makeImage(image) {
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      const ratio = image.height / image.width
      const width = 1280
      const height = width * ratio
      canvas.width = width
      canvas.height = height
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
      return canvas.toDataURL('image/jpeg')
    },
    makeTumbnail(image) {
      const canvas = this.$refs.thumbnail
      const ctx = canvas.getContext('2d')
      const ratio = image.width / image.height
      const height = 120
      const width = height * ratio
      canvas.height = height
      canvas.width = width
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
    },
    reset() {
      const canvas = this.$refs.thumbnail
      this.newImage = ''
      canvas.height = 0
      canvas.width = 0
      this.$refs.input.value = ''
    },
    upload() {
      const photo = this.newImage
      const storage = firebase.storage()
      const ref = storage.ref().child('main.jpg')
      const vm = this
      ref.putString(photo, 'data_url').then(snapshot => {
        console.log('photo uploaded')
        vm.reset()
      })
    },
  }
}
</script>


```

# 詳細
## HTML部分
```vuejs
<template>
  <div>
    <input type="file" v-on:change="resize" accept=".jpg, .png" ref="input">
    <div>
      <!-- サムネイル用canvas -->
      <canvas ref="thumbnail" :width="0" :height="0">

      <!-- 選択した画像をリセットするためのボタン -->
      <button v-on:click="reset">×</button>
    </div>
    <div>
      <!-- アップロードボタン -->
      <button v-on:click="upload">upload</button>
  </div>
</template>
```
`input`では`v-model`を使いたいところですが`type="file"`では使えません。
`v-on="change"`でメソッドを呼び出します。
また、サムネイルを表示するための`canvas`要素を予め書いています。
リセットボタンはあると便利かなという程度です。

## 画像を作る

```javascript
  data() {
    return {
      newImage: '',
    }
  },
  methods: {
    resize(e) {
      const file = e.target.files[0]
      const image = new Image()
      const reader = new FileReader()
      const vm = this
      const maxWidth = 1280

      reader.readAsDataURL(file)

      reader.onload = (e) => {
        image.src = e.target.result

        image.onload = () => {
          vm.newImage = this.width < maxWidth ? this.src : vm.makeImage(image)
          vm.makeTumbnail(image)
        }
      }
    },
...
```
1. `new Reader()`でファイル情報を取得
2. `new Image()`で`img`要素を作成
3. 「2.」の`img`要素に「1.」のファイル情報を与える
4. `image`の準備が整い次第`makeImage()`と`makeThumbnail()`を呼び出す

この例では幅`1280`px以上の画像を`1280`pxに縮小するものです。
`maxWidth`の値は用途に応じて調整してください。
画像の`width`が`maxWidth`以上の場合は、`makeImage()`で縮小画像を生成、`maxWidth`以下の場合は入力した画像をそのまま`newImage`に格納します。

## 縮小画像を作る
```javascript
    makeImage(image) {
      // canvas要素を作成
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')

      // 縦横比を算出
      const ratio = image.height / image.width

      // 生成する画像の横幅
      const width = 1280

      const height = width * ratio
      canvas.width = width
      canvas.height = height

      // canvas描画作成
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)

      // data_url形式に変換したものを返す
      return canvas.toDataURL('image/jpeg')
    },
```
前述の通り、この例では`1280`pxを基準としています。
用途に応じて`width`の値を調整してください。

**ここまででアップロードする画像の作成は完了です。**

## サムネイルを作成する
```javascript
    makeTumbnail(image) {
      // 予めHTMLに記述したcanvasを指定
      const canvas = this.$refs.thumbnail
      const ctx = canvas.getContext('2d')

      // 縦横比を算出
      const ratio = image.width / image.height

      // サムネイルのサイズを指定
      const height = 120
      const width = height * ratio

      // canvasの大きさを指定
      canvas.height = height
      canvas.width = width

      // サムネイルに画像を描画
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
    },
```
この例では`height="120px"`の小さいサムネイルを作成しています。
用途に応じて調整してください。

## 入力をリセット
```javascript
    reset() {
      const canvas = this.$refs.thumbnail
      this.newImage = ''

      // サムネイル用canvasのサイズを0に
      canvas.height = 0
      canvas.width = 0

      // inputの入力をリセット
      this.$refs.input.value = ''
    },
```
画像リセット用メソッドです。
用途に応じて。

## 画像をアップロードする
```javascript
    upload() {
      const photo = this.newImage
      const storage = firebase.storage()

      // アップロード先のフォルダ、ファイル名を指定
      const ref = storage.ref().child('main.jpg')

      const vm = this

      // ファイルをアップロード
      ref.putString(photo, 'data_url').then(snapshot => {
        console.log('photo uploaded')

        // 入力をリセット
        vm.reset()
      })
    },
```
firebaseに画像をアップします。
`storage.ref().child('main.jpg')`がアップロード先です。
`storage.ref().child('hozon/shitai/basho/main.jpg')`とすると保存フォルダを指定できます。

# まとめ
コンポーネントとして色んなシチュエーションで使いまわしたいということもあると思います。
現場では`prop`と`$emit`を使って色んな場面で使えるリサイズ用コンポーネントとして使っています。
その方法もそのうち…

## 参考
[JavaScript で画像をリサイズする方法](https://qiita.com/komakomako/items/8efd4184f6d7cf1363f2)
[ブラウザでローカル画像をリサイズしてアップロード](https://www.bokukoko.info/entry/2016/03/28/JavaScript_%E3%81%A7%E7%94%BB%E5%83%8F%E3%82%92%E3%83%AA%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)