Vueで画像をリサイズして表示する、そしてそれはバックエンドに送られる(canvasAPI, Base64利用)

あらすじ

fujiten3.hatenablog.com

の続き。

この記事でやること

以前↑の記事にて、簡易的な画像のプレビュー機能 + 送信機能を実装しました。

その内容を一言でいうと、クライアントサイドで画像をBase64形式で符号化し、文字列として取り扱う送信フォームを実装するといったものでした。

が、このプレビュー機能では、受け取った画像をそのままブラウザで表示するため、たびたびブラウザの画面いっぱいを占有し、「デカすぎんだろ…」状態になってしまいます。最近のスマートフォンの高性能カメラで撮った画像のサイズは4032x3024といったものになることがあるので、特にその傾向が顕著です。

これを避ける方法はいくつかあります。たとえば、Vueを使用している場合は、Vuetifyというライブラリが提供するv-avatarのsizeプロパティを利用するなどすれば、特に深く考えずともリサイズが可能です。

しかし、この記事ではcanvasAPIを利用して、つまり「HTMLElementである<canvas>タグをJavaScriptで操作すること」によって、フロント側の画像をリサイズする処理を書いていきたいと思います。

というのも、WebAPIsとJavaScriptを利用すれば、特定のライブラリに依存しない処理を書けるので、様々なプロジェクトで使え、汎用性が高いからです。

つまり、タイトルでは「Vueでリサイズ」となっておりますが、実際には「JavaScriptでリサイズする」、といった内容になります。まあ、Vue上で表現しているからタイトル詐欺ではないでしょう。

また、この記事は前記事を読んでいることがある程度前提条件となっておりますので、ご容赦下さい。

動くもの

挙動をGIFで置いておきます。もともとの画像サイズがどんなに大きくても、一定以下のサイズに、縦横の比率を保ちながら縮尺されます。

f:id:fujiten3:20200324214611g:plain

jsfiddle.net

コードの全体像は上のJSFiddleをご参考下さい。

大雑把な処理の流れ

Base64形式で符号化された画像用の文字列を、HTMLImageエレメントのsrc属性に代入する

そのImageエレメントをもとに、canvasAPIでリサイズした画像を描画する。

リサイズ後の画像を、canvasAPIがもともと持っている関数によって、再びBase64形式の文字列にする。

プレビューのためのImageに再代入

Vue側

new Vue({
  el: "#app",
  name: 'ImageUploder',
  data () {
    return {

      // avatarはimageのsrc属性にHTML側のコートでバインドしている。imageのsrc属性には、Base64形式でエンコードされたimage、または直接URLを指定できる。
      avatar: '',
      message: '',
      error: '',

      // 本筋とは関係ないが、初期画像がないと寂しかったのでプロパティの追加
      initialImageUrl: ''
    }
  },
  created () {

    // 本筋とは関係ない初期画像(サンプル画像)
    this.initialImageUrl = 'https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-600w-1389188336.jpg'
    this.avatar = this.initialImageUrl
  },
  methods: {
    setError (error, text) {
      this.error = (error.response && error.response.data && error.response.data.error) || text
    },

    // この関数の挙動は前記事参照
    getBase64 (file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        reader.readAsDataURL(file)
        reader.onload = () => resolve(reader.result)
        reader.onerror = error => reject(error)
      })
    },
    onImageChange (e) {
      const images = e.target.files || e.dataTransfer.files
      this.getBase64(images[0])
        .then(image => {

          // canvasに渡すためのImageElementを作成
          const originalImg = new Image()

          // src属性に、もともとの画像のBase64形式符号化済み文字列を代入
          originalImg.src = image

          // Imgの画像読み込みが完了したあとに、処理を実行
          originalImg.onload = () => {

            // creatReseizedCanvasElement関数については下記参照
            const resizedCanvas = this.createResizedCanvasElement(originalImg)
            const resizedBase64 = resizedCanvas.toDataURL(images[0].type)
            this.avatar = resizedBase64
          }
        })
        .catch(error => this.setError(error, '画像のアップロードに失敗しました。'))
    },

    // ImageElementを受け取り、それを元に新たな画像をcanvasに描画し直す関数
    createResizedCanvasElement (originalImg) {
      const originalImgWidth = originalImg.width
      const orifinalImgHeight = originalImg.height

      // resizeWidthAndHeight関数については下記参照
      const [resizedWidth, resizedHeight] = this.resizeWidthAndHeight(originalImgWidth, orifinalImgHeight)
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      canvas.width = resizedWidth
      canvas.height = resizedHeight

      // drawImage関数の仕様はcanvasAPIのドキュメントを参照下さい
      ctx.drawImage(originalImg, 0, 0, resizedWidth, resizedHeight)
      return canvas
    },

    // 縦横の比率を変えず、定めた大きさを超えないWidthとHeightの値を割り出す関数
    resizeWidthAndHeight (width, height) {

      // 今回は400x400のサイズにしましたが、ここはプロジェクトによって柔軟に変更してよいと思います
      const MAX_WIDTH = 400
      const MAX_HEIGHT = 400

      // 縦と横の比率を保つ
      if (width > height) {
        if (width > MAX_WIDTH) {
          height *= MAX_WIDTH / width
          width = MAX_WIDTH
        }
      } else {
        if (height > MAX_HEIGHT) {
          width *= MAX_HEIGHT / height
          height = MAX_HEIGHT
        }
      }
      return [width, height]
    },

    // 前の記事参照。APIを叩く処理を記述することを意図して作られた関数。
    upload () {
        if (this.avatar && this.avatar !== this.initialImageUrl) {
        /* postで画像を送る処理をここに書く */
        this.message = 'アップロードしました' 
        this.avatar = ''
        this.error = ''
      } else if (!this.avatar) {
        this.error = '画像がありません'
      } else {
        this.error = '変更前の画像はアップロードできません'
      }
    }
  }
})

以上です。

見づらいですね、ただ複雑な処理はしていないので、一つ一つ処理を追っていけば、難しくはないと思います。

「大雑把な処理の流れ」の項目でも記述しましたが、canvasAPIのdrawImage関数(リサイズのための関数)に対して渡す「ImageElement」を作るための処理を書いている、と思って頂ければ問題ありません。