大きな画像をJavaScriptでリサイズしてからAjax送信する方法

さてさて、この間 wordpressみたいなドラッグ・アンド・ドロップをVueで実装という記事を公開しました。この記事の目的はユーザビリティだったのですが、ドラッグ&ドロップ機能の先では画像のアップロードを想定していました。

最近のウェブサイトでは画像をサーバーへ保存するというのは珍しいことではないでしょうが、実はアップロードする側の状況が変化してきたことである問題が発生するようになってきました。

それが・・・

スマホ・カメラの高解像度化のせいで画像アップロードに時間がかかってしまう

というものです。

つまり、サイズが大きなファイルをアップロードするのに時間がかかってしまうということですね。

特にまだ日本のウェブサイトは、以下のように確認ページを挟む場合も多く存在していますので、その場合(バリデーションを厳密にするなら)画像サイズが大きい場合は1番と2番で画像をアップロードする必要がでてきます。

  1. 名前などを入力するページ (←ここで画像アップロード)
  2. 入力したものを確認するページ (←ここでも画像アップロード)
  3. 完了

これでは待ち時間が長くなってしまいますし、データ送信するごとにスマホの通信速度制限を気にしないといけなくなってしまいます。

そこで!

今回は選択された画像ファイルをブラウザでサイズ変更してからアップロードする方法をLaravel + Vueで実装してみます。

皆さんのお役にたてると嬉しいです♪
(最後に今回実際に開発したコードをダウンロードできます)

開発環境: Laravel 5.8、Vue 2.6(JavaScriptの記述法はES6)

HTML部分をつくる

まずはベースとなるHTML部分を作っていきます。
といっても、Vueaxioscdnで読み込み、それぞれイベントがついた「ファイル選択ボックス」と「送信ボタン」を配置したシンプルなものです。

※ なお、複数選択に対応しています。

また、Vueの変数の中身は以下のとおりです。

  1. maxWidth ・・・ リサイズする画像の最大幅
  2. smallImages ・・・ リサイズされた画像の配列

実際のコードはこうなります。

<html>
<body>
<div id="app">
    <input type="file" accept="image/*" multiple @change="onImageChange">
    <br>
    <button type="button" @click="onSubmit">送信</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
<script>

    new Vue({
        el: '#app',
        data: {
            maxWidth: 300,
            smallImages: []
        },
        methods: {
            onSubmit() {

                // ここでAjax送信

            },
            onImageChange() {

                // ここで画像をリサイズ

            }
        }
    });

</script>
</body>
</html>

JavaScript部分をつくる

ではJavaScript部分を見ていきましょう。

実装方法の手順

ブラウザ上で画像をリサイズするには以下の手順が必要になります。

  1. HTML5のcanvas要素をつくる(表示はしません)
  2. canvasのサイズを変更
  3. 選択画像をcanvasに描画
  4. 描画した画像データを取得

画像をリサイズする部分

では、今回記事のメインになるJavaScriptで選択画像をリサイズする部分onImageChange()をつくっていきます。

まずは実際のコードから。

onImageChange(e) {

    this.smallImages = [];
    const files = e.target.files;

    for(let file of files) {

        let reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = (e) => {

            let img = new Image();
            img.onload = () => {

                let width = img.width;
                let height = img.height;

                if(width > this.maxWidth) {

                    height = Math.round(height * this.maxWidth / width);
                    width = this.maxWidth;

                }

                let canvas = document.createElement('canvas');
                canvas.width = width;
                canvas.height = height;
                let ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0, width, height);
                ctx.canvas.toBlob((blob) => {

                    const imageFile = new File([blob], file.name, {
                        type: file.type,
                        lastModified: Date.now()
                    });
                    this.smallImages.push(imageFile);

                }, file.type, 1);

            };
            img.src = e.target.result;

        };

    }

}

少しコードが複雑ですがひとつずつ説明していきます。

まず、画像が選択されたらループの中で全ファイルをFileReaderを通して読み出します。

そしてその読み出しが完了したらImageオブジェクトを作成しonloadの中で、その画像と同じサイズのcanvasに描画していくのですが、この時、もし画像の幅をチェックして最大値(上の例では300px)よりも大きければcanvasのサイズを変更してから画像を描画することでリサイズを実現しています。

また、画像の描画が完了したらtoBlob()を使って、Fileオブジェクトを作成しsmallImages配列にひとつずつ格納していきます。

※ ちなみにIE 11ではtoBlob()はサポートされていないので、JavaScript Canvas to Blobとうライブラリを使うといいでしょう。

【追記:2019.4.24】IE 11の場合 File APIでもエラーが発生してしまうことをご指摘いただきました。もしIE 11で利用したい場合は以下のように直接blogにデータを追加してみてください。(ただし、ES6で紹介していますのでコード自体をbabelなどで変換する必要があります)

ctx.canvas.toBlob((blob) => {

    blob.lastModifiedDate = new Date();
    blob.name = file.name;
    this.smallImages.push(blob);

    if(this.smallImages.length == files.length) {

        this.isResizing = false;    // リサイズ完了!

    }

}, file.type, 1);

なお、コードの中にでてくるfile.nameはファイル名、file.typeはそのファイルのmime-typeimage/pngなど)が入っています。

データ送信する部分

リサイズされた画像データを取得できれば、後はAjaxで送信するだけです。
画像データはFormDataを使うといいでしょう。

実際のコードです。

methods: {
    onSubmit() {

        let formData = new FormData;

        this.smallImages.forEach((smallImage) => {

            formData.append('images[]', smallImage);

        });

        axios.post('/resize_ajax', formData)
            .then((response) => {

                // Ajax通信が成功した時

            });

    },

    // 省略

注意すべき点

実は上のコードには入っていませんが、気をつけておくべき点があります。
それは、処理のタイミングです。

つまり、FileReader()Image()は、非同期で実行されるので「まだ画像のリサイズが終わってないのに送信ボタンが押せてしまう」という状況ができてしまうわけです。ブラウザを実行するデバイスがロースペックの場合は特に時間がかかるので、気をつける必要があるでしょう。

そのため、true / false の変数をひとつ作り、リサイズが完了するまでは送信ボタンをクリックできなくするという方法を私はよく使います。

では実際のコードを見てみましょう。

(HTML部分)

<input type="file" accept="image/*" :disabled="isResizing" multiple @change="onImageChange">

(JavaScript部分)

data: {

    // 省略

    isResizing: false
},
onImageChange(e) {

    this.isResizing = true;
    
    // 省略

    for(let file of files) {

        let reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = (e) => {

            let img = new Image();
            img.onload = () => {

                // 省略

                ctx.canvas.toBlob((blob) => {

                    // 省略

                    if(this.smallImages.length == files.length) {

                        this.isResizing = false;    // リサイズ完了!

                    }

                }, file.type, 1);

            };
            img.src = e.target.result;

        };

    }

}

こうすることで、画像のリサイズを実行している間はinputdisabledが有効になってボタンがクリックできなくなります。(お好みでv-ifを使って「リサイズ中…」などのテキストと表示を切り替えてもいいでしょう。)

Laravel部分

では最後にデータ送信する先のLaravel部分のコードを見てみましょう。

public function resize_ajax(Request $request) {

    if($request->has('images')) {

        foreach ($request->images as $photo) {

            $photo->store('images'); // storage/app/imagesフォルダに保存

        }

    }

}

中身はシンプルで、imagesというデータ(中身は画像のデータですが、配列なので、hasFile()は使っていません)が存在しているかをチェックし、もし見つかった場合はループを使ってひとつずつデータを保存しているだけです。

実際のコードをダウンロード

今回実際に開発したソースコードを以下からダウンロードすることができます。

※ ただし、Laravel部分はご自身で実装してください。

大きな画像をJavaScriptでリサイズしてからAjax送信

おわりに

ということで、今回はスマホやPCなどのクライアント側で選択した画像をリサイズしてから送信するテクニックをご紹介しました。

この方法を使うと、以下3つのメリットがゲットできることになります。

  • 画像のアップロードが高速になる
  • スマホなどの通信制限に優しくなる
  • サーバー側の負荷が減る

逆にデメリットとなると、画像を選択してから送信可能になるまで少しだけ時間がかかるってことだけですが、最近のデバイスは高機能なのでこちらもあまり気にする必要はないかもしれません。

ぜひ皆さんもこのテクニックで省力化したウェブ開発をしてみてくださいね。

※ なお、VueでAjax送信するためのJSライブラリ「v-api-form」にも今回のリサイズ機能を追加しておきましたので、よろしければ使ってみてくださいね。

ではでは〜!

この記事が役立ちましたらシェアお願いします😊✨