Help us understand the problem. What is going on with this article?

Vue.jsで画像ファイルを選択する方法、拡大縮小する方法

概要

Vue.jsで、<input type="file">タグを用いてローカルの画像ファイル(※1)を選択して読み込む方法を記載する。合わせて、拡大縮小する方法も記載する。

※1:スマホならカメラでの撮影を選択できることも多い(スマホのブラウザ実装次第)。
※2:Vue CLIを前提とするので、CDN版で了する際は、html部分とjs部分を適宜読み替えること。

前提

Vue CLIで生成されるApp.vueを次のように変更し、componentsフォルダ配下にMyClient.vueファイルを配置するものとする。以降では、この構成を前提としてMyClient.vueファイルの実装方法をについて述べる。

App.vue
<template>
  <div id="app">
    <MyClient></MyClient>
  </div>
</template>

<script>
import MyClient from './components/MyClient.vue'

export default {
  name: 'App',
  components: {
    MyClient
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

ローカルの画像ファイルを読み込んで表示する方法

ファイル選択ダイアログの入力受けて画像ファイルを表示するには次のようにする。

  1. <input type="file">タグに v-on:change="メソッド名"を追加して、ファイル選択ダイアログの実行結果を受け取れるようにする
  2. v-on:change="メソッド名"で指定したメソッドにイベントオブジェクトが渡されるので、そこからファイルパスを取得する
  3. 続いて、FileReaderオブジェクトを利用して、Base64エンコードしたデータとして受け取る

※file属性では、セキュリティの都合で「外部(ダイアログ以外)からローカルファイルパスを指定できない」ので、v-modelによる双方向バインドは指定不可。

※読み込んだファイルを、REST APIでどこかのバックエンドにアップロードすることが多いと思われるんので、その際によく使われる形式であるBase64で読み込んでおく。imgタグはBase64形式を画像として表示できるので問題ない。他の形式での読み込み方は、こちらを参照のこと。→ https://developer.mozilla.org/ja/docs/Web/API/FileReader

ファイル選択ダイアログで読み込んだ画像ファイル(壁紙)を表示してみた例

MyClient.vueファイルの実装例は次のようになる。

MyClient.vue
<template>
<div>
    <div id="id_title">
        ファイル選択ダイアログからの画像ファイルのアップロード
    </div>
    <br>
    <div id="id_face_imaeg">
        <img :src="targetImage" alt="選択された画像" class="image">
    </div>
    <br>
    <br>
    <div id="id_register_image">
        <input v-on:change="selectedFile" type="file" name="file" accept="image/jpeg, image/png"><br>
        <br>
        <br>
    </div>
</div>
</template>


<script>
// javascriptファイルをココへ。

export default {
    name : "MyClient",
    components : { 
    },
    data : function () {
        return {
            targetImage : null
        }
    },
    methods : {
        getFileAsBase64 : function(filePath) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = e => resolve(e.target.result);
                reader.onerror = error => reject(error);
                reader.readAsDataURL(filePath);
                // ここまでで「resolve(e.target.result)」でbase64化された画像ファイルデータが返却される。
                // https://fujiten3.hatenablog.com/entry/2019/07/10/133132
            })
        },  
        selectedFile : function (e) {
            let files = e.target.files;
            e.preventDefault(); // 標準のInputタグの動作をキャンセル
            // http://tech.aainc.co.jp/archives/10714
            // https://developer.mozilla.org/ja/docs/Web/API/File/Using_files_from_web_applications

            if(files && files.length > 0){ // 有効なFileオブジェクトが渡された時は、画像ファイルとして読み込みを実施
                this.getFileAsBase64(files[0])
                .then((imgDataBase64)=>{
                    this.targetImage = imgDataBase64;
                });
                // ToDo: エラー処理
            }
        }  
    }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* Cssファイルはここへ配置する。 */

</style>

読み込んだ画像をリサイズ(拡大縮小)する方法

読み込んだ画像を拡大縮小してからアップロードを行いたい場合があります。
その際に、JavaScriptのみでリサイズ後の画像データを生成するには、次のようにします。

※表示時にリサイズしたいだけ(データは変更しない)なら、imgタグの縦横設定でOKです。

  1. 読み込んだ画像データをImageオブジェクトへ変換
  2. HTML5の canvas タグへ描画する際の「縮尺指定」を利用してリサイズを実行
  3. canvasタグの描画データを「その縦横サイズの画像」として新規に取得する

MyClient.vueファイルの実装例は次のようになる。なお、このサンプルではcanvasエリアを非表示の設定としている。

MyClient.vue
<template>
<div>
    <div id="id_title">
        ファイル選択ダイアログからの画像ファイルのアップロード
    </div>
    <br>
    <div id="id_face_imaeg">
        <img :src="targetImage" alt="選択された画像" class="image">
    </div>
    <br>
    <br>
    <div id="id_register_image">
        <input v-on:change="selectedFile" type="file" name="file" accept="image/jpeg, image/png"><br>
        <br>
        <br>
    </div>
    <div id="id_canvas_for_resize" v-show="false">
        拡大縮小用のCanvasエリア<br>
        <canvas ref="canvas"/>
    </div>
</div>
</template>


<script>
// javascriptファイルをココへ。

export default {
    name : "MyClient",
    components : { 
    },
    data : function () {
        return {
            resizeUpperLimitPixel : 320,
            targetImage : null
        }
    },
    methods : {
        getFileAsBase64 : function(filePath) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = e => resolve(e.target.result);
                reader.onerror = error => reject(error);
                reader.readAsDataURL(filePath);
                // ここまでで「resolve(e.target.result)」でbase64化された画像ファイルデータが返却される。
                // https://fujiten3.hatenablog.com/entry/2019/07/10/133132
            })
        },  
        resizeImage64withCanvase : function (loadedFile, canvas) {
            // 以下、縮小を掛けるにはimage経由でのcanvasへの貼り付けを利用する。
            // https://qiita.com/busroutemap/items/b563dfe8b08bb3338eb5
            // https://qiita.com/su_mi1228/items/492c89db7f96823a26c0
            // https://www.mahirokazuko.com/entry/2019/08/20/133713

            return new Promise((resolve)=>{
                const image = new Image();
                image.onload = () => resolve(image); // (e)は利用されないので省略。
                image.src = loadedFile;
            }).then((image)=>{
                const ctx = canvas.getContext('2d'); // 2Dコンテキスト
                const MAX_SIZE = this.resizeUpperLimitPixel; // 縦横で長い方の最大値を指定する(例:800)とする

                if (image.width < MAX_SIZE && image.height < MAX_SIZE) {
                    // MAX_SIZEよりも小さかったらそのまま貼り付ける

                    [canvas.width, canvas.height] = [image.width, image.height];
                    ctx.drawImage(image, 0, 0);
                }else{
                    let dstWidth;
                    let dstHeight;
                    // 縦横比の計算
                    if (image.width > image.height) {
                        dstWidth = MAX_SIZE;
                        dstHeight = (image.height * MAX_SIZE) / image.width;
                    } else {
                        dstHeight = MAX_SIZE;
                        dstWidth = (image.width * MAX_SIZE) / image.height;
                    }
                    canvas.width = dstWidth;
                    canvas.height = dstHeight;
                    // canvasに既に描画されている画像があればそれを消す
                    ctx.clearRect(0,0,dstWidth,dstHeight);
                    // リサイズして貼り付ける
                    ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, dstWidth, dstHeight);
                }

                // canvasから画像をbase64として取得する
                let base64 = canvas.toDataURL('image/jpeg');

                return Promise.resolve(base64);
            });
        },
        selectedFile : function (e) {
            let files = e.target.files;
            e.preventDefault(); // 標準のInputタグの動作をキャンセル
            // http://tech.aainc.co.jp/archives/10714
            // https://developer.mozilla.org/ja/docs/Web/API/File/Using_files_from_web_applications

            if(files && files.length > 0){ // 有効なFileオブジェクトが渡された時は、画像ファイルとして読み込みを実施
                this.getFileAsBase64(files[0])
                .then((imgDataBase64)=>{
                    return this.resizeImage64withCanvase(imgDataBase64, this.$refs.canvas);
                }).then((imgDataBase64)=>{
                    this.targetImage = imgDataBase64;
                });
                // ToDo: エラー処理
            }
        }  
    }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* Cssファイルはここへ配置する。 */

</style>

画像選択とリサイズまでをComponent化する

画像ファイル選択とリサイズの部分をComponent化(部品可)して、「<SelectImage v-on:selectedImage="finishSelectingImage">と記載したら、読み込みとリサイズ完了後にfinishSelectingImage()が呼ばれる(※呼ばれる関数名は任意)」ようにすることを考える。

この場合、呼び出し側の MyClient.veu は次のようにする。
なお、リサイズ値とCanvasの表示非表示を設定できるようにしてある。

MyClient.vue
<template>
<div>
    <div id="id_title">
        ファイル選択ダイアログからの画像ファイルのアップロード
    </div>

    <div id="id_face_imaeg">
        <div id="id_face_imaeg_uploaded" v-show="targetImage">
            <img :src="targetImage" alt="サムネイル" class="image">
        </div>
        <div id="id_face_imaeg_notfound" v-show="!targetImage">
            <img alt="画像は未選択" src="../assets/no_image.png">
        </div>
    </div>
    <br>
    <br>

    <SelectImage v-on:selectedImage="finishSelectingImage" v-bind:isCanvasShow="false" v-bind:resizeUpperLimitPixel="360">
        <!-- 画像の読み込み -->
    </SelectImage>
</div>
</template>


<script>
// javascriptファイルをココへ。

import SelectImage from './SelectImage.vue';


export default {
    name : "MyClient",
    components : { 
        SelectImage,
    },
    data : function () {
        return {
            targetImage : null
        }
    },
    methods : {
        clickReset : function () {
            this.targetImage = null;
        },
        finishSelectingImage : function (selectedImage) {
            if(selectedImage){
                this.targetImage = selectedImage;
            }
        }
    }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* Cssファイルはここへ配置する。 */

</style>

呼び出し先のcomponentは、この例では「SelectImage」として次のように実装する。

SelectImage.vue
<template>
<div>
    <div id="id_register_image">
        <input v-on:change="selectedFile" type="file" name="file" accept="image/jpeg, image/png"><br>
        ※画像は{{resizeUpperLimitPixel}}x{{resizeUpperLimitPixel}}以下へリサイズされます。<br>
        <br>
        <br>
        <div class="selecting-image-footer">
            <slot name="footer"><!-- default footer -->
                <!-- 戻るボタンなどが必要な場合は、呼び出しもとで設定すること -->
            </slot>
        </div>
    </div>
    <div id="id_canvas_for_resize" v-show="isCanvasShow">
        <canvas ref="canvas"/>
    </div>
</div>
</template>

<script>
// javascriptファイルをココへ。


export default {
    name : "SelectImage",
    props : {
        isCanvasShow : {
            type: Boolean,
            default: true,
            required: false
        },
        resizeUpperLimitPixel : {
            type: Number,
            default: 800,
            required: false
        }
    },
    data : function () {
        return {
            uploadedImage : null
        }
    },
    created : function () {
    },
    methods : {
        getFileAsBase64 : function(filePath) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = e => resolve(e.target.result);
                reader.onerror = error => reject(error);
                reader.readAsDataURL(filePath);
                // ここまでで「resolve(e.target.result)」でbase64化された画像ファイルデータが返却される。
                // https://fujiten3.hatenablog.com/entry/2019/07/10/133132
            })
        },  
        resizeImage64withCanvase : function (loadedFile, canvas) {
            // 以下、縮小を掛けるにはimage経由でのcanvasへの貼り付けを利用する。
            // https://qiita.com/busroutemap/items/b563dfe8b08bb3338eb5
            // https://qiita.com/su_mi1228/items/492c89db7f96823a26c0
            // https://www.mahirokazuko.com/entry/2019/08/20/133713

            return new Promise((resolve)=>{
                const image = new Image();
                image.onload = () => resolve(image); // (e)は利用されないので省略。
                image.src = loadedFile;
            }).then((image)=>{
                const ctx = canvas.getContext('2d'); // 2Dコンテキスト
                const MAX_SIZE = this.resizeUpperLimitPixel; // 縦横で長い方の最大値を指定する(例:800)とする

                if (image.width < MAX_SIZE && image.height < MAX_SIZE) {
                    // MAX_SIZEよりも小さかったらそのまま貼り付ける

                    [canvas.width, canvas.height] = [image.width, image.height];
                    ctx.drawImage(image, 0, 0);
                }else{
                    let dstWidth;
                    let dstHeight;
                    // 縦横比の計算
                    if (image.width > image.height) {
                        dstWidth = MAX_SIZE;
                        dstHeight = (image.height * MAX_SIZE) / image.width;
                    } else {
                        dstHeight = MAX_SIZE;
                        dstWidth = (image.width * MAX_SIZE) / image.height;
                    }
                    canvas.width = dstWidth;
                    canvas.height = dstHeight;
                    // canvasに既に描画されている画像があればそれを消す
                    ctx.clearRect(0,0,dstWidth,dstHeight);
                    // リサイズして貼り付ける
                    ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, dstWidth, dstHeight);
                }

                // canvasから画像をbase64として取得する
                let base64 = canvas.toDataURL('image/jpeg');

                return Promise.resolve(base64);
            });
        },
        selectedFile : function (e) {
            let files = e.target.files;
            e.preventDefault(); // 標準のInputタグの動作をキャンセル
            // http://tech.aainc.co.jp/archives/10714
            // https://developer.mozilla.org/ja/docs/Web/API/File/Using_files_from_web_applications

            if(files && files.length > 0){ // 有効なFileオブジェクトが渡された時は、画像ファイルとして読み込みを実施
                this.getFileAsBase64(files[0])
                .then((imgDataBase64)=>{
                    return this.resizeImage64withCanvase(imgDataBase64, this.$refs.canvas);
                }).then((imgDataBase64)=>{
                    this.uploadedImage = imgDataBase64;
                    this.$emit('selectedImage', this.uploadedImage);
                });
                // ToDo: エラー処理
            }
        }  
    }
}
</script>

<style scoped>
</style>

以上ー。

参考サイト

hoshimado
趣味で日曜プログラミング。仕事はたぶんIT関連? この2016年春から、ふとJavaScript周りに興味が沸いたので、Webアプリベースで「自分が便利」ツール作成しつつ、その裏で「コピペで使えるコード」の共有と勉強を目的にQiitaに参戦。https://twitter.com/hoshimado7
http://fluorite.halfmoon.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away