JavaScript
promise
canvas

【Javascript】Promiseを使ってローカルファイルからcanvasへ画像を読み込む

概要

この記事では、Promiseオブジェクトを用いて、<input type="file">からローカルファイルを読み込み、<canvas>へその画像を書き出すコードを紹介します(IEではPromise オブジェクトは使えないため注意!)。
Promiseオブジェクトは、チェーンメソッドを用いた、非同期処理を簡単に書く方法です。ここではPromiseが何かについては説明しません。予め知っているものとして、話を進めます(私はこの記事を読んだり、ググったりして勉強しました)。
後半には、ローカルファイルをcanvasに書き出すクラスも記載しています。

画像読み込みのコード

まずは、画像読み込みの大枠を説明します。画像ファイルを選択した後の処理はおおまかに3つの部分に分かれています。

  1. FileReader オブジェクトを用いて、ファイルのURLを取得する
  2. Image オブジェクトに画像のソースを読み込む
  3. canvas に Imageオブジェクトを描画する

この処理をコードで書くと、このようになります。

index.html
<input type="file" id="file">
<canvas id="canvas"></canvas>
onchange.js
document.getElementById("file").onchange = (event) => {

    const fileObject = event.target.files[0];

    // 画像ファイル以外の場合は処理を終了する
    if(!fileObject.type.match(/^image\/(png|jpeg|gif)$/)) return ;

    // ローカルにある画像ファイルのURLを取得する
    const reader = new FileReader();
    reader.readAsDataURL(fileObject);

    // 画像のソースを読み込む
    const img = new Image();
    img.src = result;

    // canvasにImageオブジェクトを描画する
    const canvas = document.getElementById("canvas");
    canvas.width  = img.width;
    canvas.height = img.height;
    canvas.getContext("2d").drawImage(img, 0, 0);
};

しかし、このままでは画像は描画されません。なぜなら、1. FileReader オブジェクトを用いて、ファイルのURLを取得するの工程で。画像ファイルのDataURLの読み込みに時間がかかるからです。それが読み込み終わってなければ、Image オブジェクトの中身は空(初期状態)のままとなり、canvasへ画像が表示されることもありません。
そこで、読み込み終わってから処理をするためにPromise オブジェクトを使います。

Promise を使ったコード

読み込みが終了したかどうかは、onload イベントで分かります。Promise オブジェクトの引数 resolve を用いてこのようなコードにすることでcanvasに画像を読み込むことができます。

onchange.js
document.getElementById("file").onchange = (event) => {

    const fileObject = event.target.files[0];

    // 画像ファイル以外の場合は処理を終了する
    if(!fileObject.type.match(/^image\/(png|jpeg|gif)$/)) return ;

    new Promise((resolve, reject) => {

        // ローカルにある画像ファイルのURLを取得する
        const reader = new FileReader();
        reader.onload = (event) => {
            resolve(event.target.result);
        }
        reader.readAsDataURL(fileObject);

    }).then((result) => {

        // 画像のソースを読み込む
        const img = new Image();
        img.src = result;

        // canvasにImageオブジェクトを描画する
        const canvas = document.getElementById("canvas");
        canvas.width  = img.width;
        canvas.height = img.height;
        canvas.getContext('2d').drawImage(img, 0, 0);
    });
};

Promise を使わないコード

Promise を使うことのできなかったころ(IEは今も)は、Promiseを自分自身で実装しない限り、このような入れ子のコードで書くしかありませんでした。

onchange.js
document.getElementById("file").onchange = (event) => {

    const fileObject = event.target.files[0];

    // 画像ファイル以外の場合は処理を終了する
    if(!fileObject.type.match(/^image\/(png|jpeg|gif)$/)) return ;

    // ローカルにある画像ファイルのURLを読み込む
    const reader = new FileReader();
    reader.onload = (event) => {
        // 画像のソースを設定する
        const img = new Image();
        img.src = event.target.result;

        // canvasにImageオブジェクトを描画する
        const canvas = document.getElementById("canvas");
        canvas.width  = img.width;
        canvas.height = img.height;
        canvas.getContext('2d').drawImage(img, 0, 0);
    }
    reader.readAsDataURL(fileObject);
}

この入れ子の書き方は、処理の順番が分かりにくいという以外の欠点も持っています。それは、onload イベントに入ると、classオブジェクトとしてのthisが使用不能になるという点です。

ローカルファイルをcanvasに書き出すクラス

FileReader オブジェクトは onload 以外にもいろいろなイベントを持っています[2]。このクラスでは、画像を読み込み始めや、読み込み終わりのイベントをキャッチして処理を実行できるようにしました。もちろん使わなくても構いません。

LoadLocalFileToCanvas.js
class LoadLocalFileToCanvas{
    constructor(canvas){
        this._canvas = document.getElementById(canvas);
    }

    load(fileObject){

        if(!fileObject.type.match(/^image\/(png|jpeg|gif)$/)) return ;

        new Promise((resolve, reject) => {
            const reader = new FileReader();

            reader.onloadstart = this.start;
            reader.onloadend = this.end;
            reader.onerror = this.catch;
            reader.onload = (event) => {
                resolve(event.target.result);
            }

            reader.readAsDataURL(fileObject);

        }).then((result) => {
            const img = new Image();
            img.src = result;

            this._canvas.width  = img.width;
            this._canvas.height = img.height;
            this._canvas.getContext('2d').drawImage(img, 0, 0);
            this.then();
        });
    }
    then(){
        // Please implement it.
    }
    start(){
        // Please implement it.
    }
    end(){
        // Please implement it.
    }
    catch(event){
        console.log(event);
    }
}

このクラスの使い方

このクラスを継承して、処理を実装してもよいのですが、今回は汎用性を考えて無名関数を使ってみました。こちらのページのImageDataをより簡単に使えるクラスを用いて、画像をグレースケールにして出力しています。

onchange.js
document.getElementById("file").onchange = (event) => {
    const loading = document.getElementById("nowloading");

    const local = new LoadLocalFileToCanvas("canvas");
    local.then = () => {
        const data = new SimpleImageData(canvas);
        while(data.hasNext){
            const y = 0.298912 * data.r + 0.586611 * data.g + 0.114478 * data.b;
            data.setRGB(y, y, y);
        }
        data.load();
    }
    local.start = () => {
        loading.style.display = "block";
    }
    local.end = () => {
        loading.style.display = "none";
    }
    local.load(event.target.files[0]);
};
index.js
<input type="file" id="file">
<div id="nowloading" style="display: none">
読み込み中です。ファイルサイズが大きい場合時間がかかります。
</div>
<canvas id="canvas">

おわりに

canvasに画像ファイルを読み込むソースは、私がまだJavascriptに触り始めてまもないころに作ったものです。当時はよい記事がなく、理解するのに苦労しました。しかし、先ほど調べてみたところ、答えがそのままズバリ書いてある記事が、検索の上位に出るようになっていました。加えて、Imageオブジェクトのソースを読み込み待ちしなくてもよくなっていました。そして私は、数年ごしでそれらをイマドキ風の書き方に変えてみるわけです。
この記事をご覧の皆さまのお役に立てば光栄です。ソースコードに不備がありましたら、コメントいただけましたら幸いです。

参考文献

[1][https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise]
[2][https://developer.mozilla.org/ja/docs/Web/API/FileReader]
[3][https://qiita.com/sy_sft_/items/1166e3b9e07247ebd24e]