JQuery ブラウザで画像ファイルを読み込んでサイズや形式を変換する

#画像ファイルのアップロードで今風にブラウザで変換させたい！
昔はPHPやCGIで変換していましたが、いまは何でもブラウザでできちゃう時代！
レッツトライ！
##結論：実はめんどくさかった
まずは結果から。
以下は実際にプログラムからPNG画像を256x256以内に縮小してJPEG形式に変更してみたものです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/102207/a417e5d6-6446-1b94-3f8d-8ab7d6c7a0c2.png)
[サンプル](https://embed.plnkr.co/NVwczG/)

**ドロップ**と表示されたボックスにファイルを放り込むとファイルの形式やサイズ等を調べて表示。
**変換後**のボックスに縮小したり形式を変更した画像を表示しています。
一見すると簡単そうですが、実は何段階ものコールバックイベントをネストしていて非常にややこしいのです・・・

- まずファイルを``FileReader``オブジェクトで読み込み
- ロードが完了したら以下を実行(``onload``)
	- ファイルサイズを取得
	- バイナリ配列に変換してヘッダーを解析してMIME-Typeを判別
	- バイナリではない場合はXML形式で読み込みなおして解析(SVG用)
	- BLOB-URLを作成してHTMLで表示させる
	- 表示が完了したら以下を実行(``onload``)
 		- イメージサイズを取得して表示(この時点にならないと取得できない)
		- ``Canvas``オブジェクトを作成してイメージを縮小して描画する
		- できた画像をHTMLで表示させる
		- 変換後イメージを``FileReader``で読み直す
		- ロードが完了したら以下を実行(``onload``)
			- 変換後のファイルサイズを取得して表示する

#ファイルの読み込み
ドラッグドロップから読み込みます。
``FileReader``のプロパティに``this``を入れてイベントハンドラから参照できるようにしています。
SVGの判別に必要な「元の``file``オブジェクト」がイベントハンドラ内では取得できないので、それも渡しておきます。

```javascript
    $(".dropable_box").on("dragover", function(e){
        e.stopPropagation()
        e.preventDefault()
    })
    //ドロップイベント
    $(".dropable_box").on("drop", function(e){
        e.stopPropagation()//親DOMへのイベントのバブリング禁止
        e.preventDefault()
        let self = this;
        $.each(e.originalEvent.dataTransfer.files, function(idx, file){
            var fr = new FileReader()
            fr.self = self //thisエレメント
            fr.file = file //SVG用に元ファイルも渡す
            fr.onload = checkFileType //イベント定義
            fr.readAsArrayBuffer(file) //読み込み開始
        })
    })
```

#ヘッダーを解析してMIME-Typeを判別
読み込んだ画像ファイルをHTMLに表示させる為、まず``BLOB-URL``を作成しますが、その際にMIME-Type(ファイル形式)が必要になります。
ファイル形式を厳密に調べるためにバイナリヘッダーを調べます。
``checkFile_callback``は判別完了時のコールバックです。

```javascript
    //ファイルタイプのチェック
    //Refference from https://stackoverflow.com/questions/18299806/how-to-check-file-mime-type-with-javascript-before-upload
    function checkFileType(e){
        let arr = (new Uint8Array(e.target.result)).subarray(0, 4)
        let header = "",
        type = ""
        for (let i = 0; i < arr.length; i++) {
            header += arr[i].toString(16)
        }
        switch (header) {
        case "89504e47":
            type = "image/png"
            break;
        case "47494638":
            type = "image/gif"
            break;
        case "ffd8ffe0":
        case "ffd8ffe1":
        case "ffd8ffe2":
            type = "image/jpeg"
            break;
        default:
            type = ""
            break;
        }
    	if(type!=="") {
           	//正しく読み込めた
           	checkFile_callback.call(this.self, type, e.target.result)
           	return;
    	}
        //SVG形式で読み込みチェック
        let fr = new FileReader();
        fr.self = this.self; //元エレメント
        fr.bin = e.target.result;
    	fr.onloadend = function(e2) {
            //XMLパーサーを使う
            let PARSER = new DOMParser()
            let doc = null,
            type = "image/svg+xml"
            try {
            	  doc = PARSER.parseFromString(e2.target.result, type)
            } catch(er){
                //失敗(異常系)
                checkFile_callback.call(this.self, "unknown", null)
                return;
            }
            //失敗(正常系)
            if(doc.getElementsByTagName("parsererror").length > 0){
                checkFile_callback.call(this.self, "unknown", null)
                return;
            }
            //正しく読み込めた
            checkFile_callback.call(this.self, type, this.bin)
            return;
    	}
    	fr.readAsText(this.file)
    }
```

#変換処理
実際に形式を変換するのはこの一文だけです。

```javascript
this.src = canvas.toDataURL("image/jpeg")
```

しかしこの例ではすべて256x256に収まるようにCanvasオブジェクトを使って描画しなおしし、さらにファイルサイズの取得も行うため、長くなっています。

```javascript
    //ファイルチェック後のコールバック
    function checkFile_callback(type, data){
		    if(type !== "unknown"){
            //バイナリをBLOB-URL化
            let bin = new Uint8Array(data)
            let blob_url = URL.createObjectURL(
                new Blob([bin], { 'type': type })
            )
            //読み込んだファイルを表示
            $(this).css({"background": "url("+blob_url+") center center/contain no-repeat"})

            //ファイルサイズの変更
            let image = new Image() //イメージを作成
            image.src = blob_url //画像ファイルを読み込む
            //読み込み完了イベント
            image.onload = function(){
                $("#info_before").html("File-Size: "+getSizeStr(bin.length)+"<br>Type: "+type+"</br>Image-Size: "+this.naturalWidth + " x " + this.naturalHeight)
                let canvas = document.createElement("canvas") //キャンバスを作成
                let ctx = canvas.getContext('2d')
                //256x256より大きければサイズを縮小
                if(this.naturalWidth > 256 || this.naturalHeight > 256){
                    //縮小時のアスペクト値を維持するための計算
                    let resize = 256 / [this.naturalWidth, this.naturalHeight].sort()[1] 
                    canvas.width = this.naturalWidth * resize
                    canvas.height = this.naturalHeight * resize
                    //あらかじめ白で塗りつぶす(透過色対策)
                    ctx.fillStyle="white"
                    ctx.fillRect(0,0,canvas.width,canvas.height)
                    //キャンバスへ縮小描画
                    ctx.drawImage(this,0,0,canvas.width,canvas.height)
                } else {
                    canvas.width = this.naturalWidth
                    canvas.height = this.naturalHeight
                    //あらかじめ白で塗りつぶす(透過色対策)
                    ctx.fillStyle="white"
                    ctx.fillRect(0,0,canvas.width,canvas.height)
                    //そのまま描画
                    ctx.drawImage(this,0,0)
                }
                //イメージ形式をJpegへ変換
                this.src = canvas.toDataURL("image/jpeg")
                //再読込完了イベント
                this.onload = function(){
                    //変換後のURLをセット
                    $(".result").attr("src", this.src).css({"width": this.naturalWidth, "height": this.naturalHeight})
                    //URLからファイルサイズを取得する
                    var xhr = new XMLHttpRequest()
                    xhr.open("GET", this.src)
                    xhr.self = this
                    xhr.responseType = "arraybuffer"
                    xhr.onload = function() {
                        let bin = new Uint8Array(xhr.response)
                        //詳細表示
                        $("#info_after").html("File-Size: "+getSizeStr(bin.length)+"<br>Type: image/jpeg</br>Image-Size: "+this.self.naturalWidth + " x " + this.self.naturalHeight)
                    };
                    xhr.send()
                }
            }
        }
    }
    //ファイルサイズを単位で表示
    function getSizeStr(e){
        var t = ["Bytes", "KB", "MB", "GB", "TB"]
        if (0 === e) return "n/a"
        var n = parseInt(Math.floor(Math.log(e) / Math.log(1024)))
        return Math.round(e / Math.pow(1024, n)) + " " + t[n]
    }
```

#参考サイト:
[How to check file MIME type with javascript before upload?](https://stackoverflow.com/questions/18299806/how-to-check-file-mime-type-with-javascript-before-upload)
[JavaScript Canvas Image Conversion](https://davidwalsh.name/convert-canvas-image)