画像アップロードにおいて、ブラウザ側でリサイズしてからアップロードするとサーバー側の負担を減らすことができます。
ということで今回は、Vue.jsでcanvasを使った画像リサイズとアップロード機能の実装を紹介していきます。
またコンポーネントの設計にはAtomicDesignを採用します。
AtomicDesignについて知りたい方は「Vue.jsでAtomic Designを実践する - Qiita」を参考にしてください。
コンポーネント全体の流れ
まずは全体の流れを説明します。
- inputコンポーネントを作る
- リサイズに関する値をinputコンポーネントに渡す
- resizedイベントから画像のBlobを取得する
- そのBlobを使ってアップロードする
inputコンポーネントを作る
ResizableImageInput
という<input>
要素を持ったコンポーネントを作成します。
<template>
<input
ref="input"
type="file"
accept="image/gif,image/jpeg,image/jpg,image/png"
:disabled="disabled"
@change="resize()"
>
</template>
<script>
import base64ToBlob from 'b64-to-blob' // https://www.npmjs.com/package/b64-to-blob
export default {
props: {
drawImageArgs: {
type: Function,
required: true
},
disabled: {
type: Boolean,
default: false
}
},
methods: {
resize () {
const file = this.$refs.input.files[0]
if (!file) {
return
}
const reader = new FileReader()
reader.onload = (event) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const image = new Image()
image.crossOrigin = 'Anonymous'
image.onload = () => {
const drawImageArgs = this.drawImageArgs(image)
if (drawImageArgs.length === 9) {
canvas.width = drawImageArgs[7]
canvas.height = drawImageArgs[8]
}
ctx.drawImage(...drawImageArgs)
const base64 = canvas.toDataURL()
this.$emit('resized', { base64, blob: base64ToBlob(base64.replace(/^.*,/, '')) })
}
image.src = event.target.result
}
reader.readAsDataURL(file)
}
}
}
</script>
1つずつ解説していきます。
template
template自体は、単純なinput要素です。
ユーザーがinput要素からファイルを選択すればchangeイベントが発生し、resize()メソッドが呼ばれます。
<template>
<input
ref="input"
type="file"
accept="image/gif,image/jpeg,image/jpg,image/png"
:disabled="disabled"
@change="resize()"
>
</template>
resize()メソッド
const file = this.$refs.input.files[0]
refsからinputのDOMオブジェクトを取得します。
reader.onload = (event) => {
//
}
reader.readAsDataURL(file)
onloadでファイルを読み込んだ際のイベント処理を登録しておきます。読み込みはreadAsDataURLで行います。
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const image = new Image()
image.crossOrigin = 'Anonymous'
image.onload = () => {
//
}
image.src = event.target.result
reader.onloadの中身です。
createElement('canvas')
でcanvas要素を作成し、new Image()
でimg要素を作成しています。
const drawImageArgs = this.drawImageArgs(image)
if (drawImageArgs.length === 9) {
canvas.width = drawImageArgs[7]
canvas.height = drawImageArgs[8]
}
ctx.drawImage(...drawImageArgs)
image.onloadの中身です。
this.drawImageArgs
はpropsで親から渡ってきた関数で、drawImage関数に渡す引数になっています。drawImageについては次節で説明します。
const base64 = canvas.toDataURL()
this.$emit('resized', { base64, blob: base64ToBlob(base64.replace(/^.*,/, '')) })
canvas.toDataURL()
で画像のbase64を取得できます。
そしてそのbase64とbase64をblobにしたものを、$emitで親コンポーネントへ伝えます。
サーバーが受け付ける画像フォーマットは基本的にBlobであるためBlobを親コンポーネントへ伝えます。
そしてbase64も伝えることにより、例えば親コンポーネントでプレビュー用として簡単に利用することができます。(base64はimg要素のhrefに入れればそのまま画像が表示できるため)
input要素にリサイズに関する値を渡す
先ほど出てきたdrawImageArgs
というpropsですが、それを親コンポーネントから渡す必要があります。
まずは親コンポーネントの全体像を見てみましょう。
<template>
<ResizableImageInput
:disabled="sendingForm"
:draw-image-args="drawImageArgs"
@resized="uploadProfileImage"
/>
</template>
<script>
import ResizableImageInput from '~/components/molecules/ResizableImageInput'
export default {
components: { ResizableImageInput },
data () {
return {
sendingForm: true
}
},
methods: {
async uploadProfileImage ({ blob }) {
this.sendingForm = true
const formData = new FormData()
formData.append('profile_image', blob)
await uploadFunction(formData)
this.sendingForm = false
},
drawImageArgs (image) {
const maxSize = 500
let sx = 0
let sy = 0
let imageWidth = image.width
let imageHeight = image.height
if (imageWidth > imageHeight) {
sx = (imageWidth - imageHeight) / 2
imageWidth = imageHeight
}
if (imageHeight > imageWidth) {
sy = (imageHeight - imageWidth) / 6
imageHeight = imageWidth
}
const dstWidth = imageWidth > maxSize ? imageWidth * maxSize / imageWidth : imageWidth
const dstHeight = imageHeight > maxSize ? imageHeight * maxSize / imageHeight: imageHeight
return [image, sx, sy, imageWidth, imageHeight, 0, 0, dstWidth, dstHeight]
}
}
}
</script>
drawImageArgs
drawImageArgsをメインに説明していきます。
今回の例では、「アップロードされた画像を最大500pxで正方形トリミングする」という引数を作成します。
drawImageArgsの引数については以下を参考にしてください。
- drawImage() メソッド - Canvasリファレンス - HTML5.JP
- drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)-Canvasリファレンス
またinputで選択された画像が正方形ではない場合、以下のようにして対応します。
if (imageWidth > imageHeight) {
sx = (imageWidth - imageHeight) / 2
imageWidth = imageHeight
}
これはwidthがheightよりも長い場合、両端を切り落として中央を中心に正方形トリミングするという処理です。
heightの方が長い場合は、widthとheightの差分の1/6の高さを中心としてトリミングしています。これは一般的に画像の焦点(人物の顔など)が画像上の上部に位置することが多い、という曖昧な基準で決めています。(なのでご自由に変更していただいて構いません。)
if (imageHeight > imageWidth) {
sy = (imageHeight - imageWidth) / 6
imageHeight = imageWidth
}
そして最後にdrawImageの引数を返しています。
return [image, sx, sy, imageWidth, imageHeight, 0, 0, dstWidth, dstHeight]
アップロード処理
あとはアップロードの処理のみです。
子コンポーネント(ResizableImageInput)からresizedイベントが発行されるので、 @resized="uploadProfileImage"
で購読し、アップロード処理を行います。
uploadProfileImage ({ blob }) {
this.sendingForm = true
const formData = new FormData()
formData.append('profile_image', blob)
await uploadFunction(formData)
this.sendingForm = false
},
まとめ
以上が、Vue.jsによるcanvasを使った画像リサイズとアップロード機能の実装です。
drawImageの引数を親コンポーネントから受け取ることで、非常に再利用がしやすいコンポーネントとなりました。
何か改善点や質問点などがありましたら、コメントよりお気軽にご連絡ください。