ブラウザ側で画像をリサイズしてFirebase Storageにアップロードする(Vue.js)


browser-image-compressionを使って、簡単に画像のリサイズをすることができます。
あまりの手軽さに感動したのでまとめます。

browser-image-compressionの導入と利用については既に素晴らしい記事がありますので、ぜひご参考ください。
当記事でもbrowser-image-compressionの使い方はさらっと紹介しつつ、Firebase Storageへのアップロードまで一気通貫でまとめます。

なお、以下がプロジェクトに導入、設定済みであることを前提としています

  • Vuetify
  • Firebase

browser-image-compressionの導入

npmもしくはyarnで導入します。

yarn add browser-image-compression

リサイズ・アップロード

(準備)リサイズ用モジュールの作成

imageCompression.js
import imageCompression from 'browser-image-compression'

export default {
  async getCompressImageFile(file) {
    const options = {
      maxSizeMB: 0.8, //最大ファイルサイズ
      maxWidthOrHeight: 600, //最大縦横値
    }
    return await imageCompression(file, options)
  },
}

リサイズしたい目的によって、単にファイルサイズを小さくしたいこともあれば、画像のサイズを一定以下にしないといけないこともあると思います。
今回私が利用したケースにおいては画像サイズが重要だったため、maxWidthOrHeightを指定しています。maxSizeMBは念の為に指定していましたが、なくても良かったかもです。
もっと言うとこれぐらいの記述量なら外出ししなくてもよかったかも……

(本題)アップロード処理

先ほどのリサイズ処理をアップロード前に呼んでリサイズを行い、リサイズ後画像をアップロードします。
流れを大きく分けると以下の3段階です。

  1. フォーム上で選択された画像をscriptで受け取る
  2. 受け取った画像をリサイズする
  3. リサイズ後の画像をFirebase Storageにアップロードする

先にコード全体を掲載し、そのうえで順に触れていきます。

template部分
<v-file-input
  accept="image/*"
  label="写真"
  prepend-icon="mdi-camera"
  @change="uploadImage"
></v-file-input>
script部分
import { storage } from '@/plugins/firebase'
import imageCompression from '@/imageCompression.js'

//中略

async uploadImage(fileInfo) {
  //選択された画像の情報を取得
  this.imageInfo = fileInfo

  //画像をリサイズする
  console.log('start compress')
  this.compressedImage = await imageCompression.getCompressImageFile(
    this.imageInfo
  )
  console.log('finished compress')

  //storageへの参照
  const storageRef = storage.ref()
  const xxxImagesRef = storageRef.child('xxx/' + uniqueImageName)

  //アップロードしてURLを取得
  //コールバック関数内からthisへ参照できないため、selfに退避する
  let self = this
  xxxImagesRef.put(this.compressedImage).then(async function (snapshot) {
    self.imageUrl = await snapshot.ref.getDownloadURL()
  })
},
(参考)firebase.js
import Vue from 'vue'
import { firestorePlugin } from 'vuefire'
import firebase from 'firebase/app'
import 'firebase/firestore'
import 'firebase/storage'

Vue.use(firestorePlugin)

const firebaseApp = firebase.initializeApp({
  //xxx部分は各々の内容で置換
  apiKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
  authDomain: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
  databaseURL: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
  projectId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
  storageBucket: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
  messagingSenderId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
  appId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
  measurementId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
})

export const db = firebaseApp.firestore()
export const storage = firebaseApp.storage()

※私はfirebase.jsを切り出していますが、main.jsに同内容を記述している方はよしなに読み替えてください。

1.フォーム上で選択された画像をscriptで受け取る

まずは画像アップロードフォームを作ります。Vuetifyでさくさく作ります。

template部分
<v-file-input
  accept="image/*"
  label="写真"
  prepend-icon="mdi-camera"
  @change="uploadImage"
></v-file-input>

v-file-inputは、特に複雑なことをしなくても、このような記述だけで任意のメソッドにファイルの情報を渡すことができます。今回の例であれば、ファイルが選択された(あるいは変更された)ときにそのファイルをuploadImageに渡し、リサイズとアップロードを行います。
@changeだとファイルを変更するたびにアップロードが発生するので、Storage上にゴミが溜まります。ユースケース次第で、@changeではリサイズのみに留めてもよいでしょう。

続いて、v-file-inputで選択された画像ファイルを受け取るための記述は以下のみです。引数としてファイルを受け取ります。

uploadImage_ファイル受け取り
async uploadImage(fileInfo) {
  //選択された画像の情報を取得
  this.imageInfo = fileInfo

ここの概念がまだ理解できていません。単にファイルの属性情報などだけを受け取るのではなく、まさにファイルとして振る舞うモノを変数として受け取っているということに違和感があります……

2.受け取った画像をリサイズする

uploadImage_リサイズ処理
  //画像をリサイズする
  this.compressedImage = await imageCompression.getCompressImageFile(
    this.imageInfo
  )

script上部でimportしたimageCompressionの中のgetCompressImageFileを動かします。
1.で受け取った画像ファイル(this.imageInfo)を引数にし、戻り値(=リサイズ後の画像)を新しい変数に格納しています。

3.リサイズ後の画像をFirebase Storageにアップロードする

最後は2.で得られたリサイズ後の画像をアップロードします。

uploadImage_アップロード処理
  //storageへの参照
  const storageRef = storage.ref()
  const xxxImagesRef = storageRef.child('xxx/' + uniqueImageName)

  //アップロードしてURLを取得
  //コールバック関数内からthisへ参照できないため、selfに退避する
  let self = this
  xxxImagesRef.put(this.compressedImage).then(async function (snapshot) {
    self.imageUrl = await snapshot.ref.getDownloadURL()
  })

まずはともあれStorageへの参照を作ります。Storage内部の任意の子ディレクトリに保存したい場合、既存の参照からさらにchild()メソッドでもう一段階深い参照を作ることができます。(xxxの部分は任意のディレクトリ名に読み替えてください)

適当な参照が作れたら、その参照のput()メソッドでファイルをアップロードします。
単にアップロードするだけでなく、アップロードしたファイルのダウンロード用URLも欲しかったので、コールバック関数内でgetDownloadURL()によってURLを取得しています。

一連の処理が完了すると、Storageは以下のようになっています。

もとの画像が17MBほどだったので、かなりの容量を節約することができました!

参考