フリーランスのためのネットビジネス専門学校 ネットで独立開業を目指す人を応援
フリーランスのためのネットビジネス専門学校 ネットで独立開業を目指す人を応援

JQuery ブラウザ上で読み込んだローカル画像のサイズや形式を変換しようしたら、わりと大変だった

画像ファイルのアップロードで今風にブラウザで変換させたい!

昔はPHPやCGIで変換していましたが、いまは何でもブラウザでできちゃう時代!
レッツトライ!

結論:実はめんどくさかった

まずは結果から。
以下は実際にプログラムからPNG画像を256×256以内に縮小してJPEG形式に変更してみたものです。
image.png
サンプル

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

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

ファイルの読み込み

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

    $(".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は判別完了時のコールバックです。

    //ファイルタイプのチェック
    //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)
    }

変換処理

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

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

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

    //ファイルチェック後のコールバック
    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]
    }

おまけ:できた画像をPOSTできるように<input type=file>に変換する

今回は使っていませんが、変換された画像をPOSTできるように以下のような関数を使いました。
※結構前にどこかのスニペットを拾ってきたのですが、参考サイトがわからなくなってしまいました。。

addFileList(エレメント, blob_urlまたはimg.src, "適当なファイル名")のように使います。

    //加工後のイメージを<input type=file>に変換する
    function addFileList(input, url, filename) {
        if (typeof url === 'string')
        url = [url]
        else if (!Array.isArray(url)) {
            throw new Error('url needs to be a file path string or an Array of file path strings')
        }
        const file_list = url.map(fp => filename)
        file_list.__proto__ = Object.create(FileList.prototype)
        Object.defineProperty(input, 'files', {
            value: file_list
        })
        return input
    }

参考サイト:

How to check file MIME type with javascript before upload?
JavaScript Canvas Image Conversion

コメント

記事に戻る

コメントを残す