アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Ajaxによるmultipart/postでの画像ファイルアップロード その2

今回の課題


前回の記事
http://blog.asial.co.jp/1260
で、Formタグで画像を選択していましたが、この方法は、Android 4.4では出来なくなっています。(Android 4.4.2で確認。今後のバージョンアップで改善される可能性はあります)

そこで、ここではFormタグからではなく、File APIから画像を取得してファイルをアップロードする実装を考えてみます。今回は、サンプルとして、getPictureメソッドを使って、デバイス内の画像ファイルを選択してサーバーにアップするものを考えます。

注意事項:
BlobがWebViewで使用出来る必要があります。Nexus 5では確認していますが、4.4より以前のAndroid端末などでBlobが使えないと、この記事の方法は出来ません。

【2014/07/3捕捉】
Android 4.4.4では上記の問題が改善され、Formタグでも画像が選択出来るようになりました。

サーバー側の実装


サーバー側の実装は、前回の記事とまったく同じなので、説明は割愛します。
サーバー側は、通常のフォーム処理と同じで良いところがこの方法の優れた点です。(レスポンスをjsonにするところだけは違います)

クライアント側の実装


最初に、コードの全体を示します。



        function getPhoto () {
            navigator.camera.getPicture(movePic, onFail, 
                { quality: 50,destinationType: Camera.DestinationType.FILE_URI,
                    sourceType: navigator.camera.PictureSourceType.SAVEDPHOTOALBUM });
        }

        function movePic(imageuri){ 
            window.resolveLocalFileSystemURI(imageuri, resolveOnSuccess, resOnError); 
        }

        function onFail (message) {
            alert('ERROR' + message);
        }
        
        function resolveOnSuccess(entry) {
            entry.file( function(file) { 
                var reader = new FileReader();
                reader.onloadend = function(evt) {
                    var formData = new FormData();
                    formData.append("userfile", new Blob([evt.target.result],{"type":file.type}), "uesrfile.png");
                    upload(formData);
                };
                reader.readAsArrayBuffer( file );
            }, resOnError );
        }

        function resOnError(error) {
            alert(error.code);
        }
    
        function upload(fd) {
            $.ajax(
            'http://[server url]/uploader.php',
                {
                type: 'post',
                processData: false,
                contentType: false,
                data: fd,
                dataType: "json",
                success: function(data) {
                    alert( data.message );
                    console.log(data);
                },
                error: function(XMLHttpRequest, textStatus, errorThrown) {
                    alert( "ERROR" );
                    alert( textStatus );
                    alert( errorThrown );
                }
            });
            return false;
        }


また、body部に記述するhtmlは次のようになります。



    <h1>Select File</h1>
    <input type="button" onclick="getPhoto()"; value="Select File"><br/>


プログラムの中身ですが、まず前回と違って、body部にformタグがありません。inputタグはありますが、単にgetPhoto()メソッドを読んでいるだけになります。

次に、javascript部分について。uploadメソッドを呼ぶところは、基本的に前回と同様です。引数のfdをそのまま送っているところだけ、少し違います。

そして、前回と大きく違う今回のポイントは、最初に呼ばれたgetPhoto()メソッドから、実際にファイルをアップロードするupload()メソッドまでの間の処理になります。
まず最初に、movePic()メソッドが呼ばれて、選択した画像ファイルをfileEntryオブジェクトに変換します。
次に、resolveOnSuccess()メソッドが呼ばれて、fileEntryオブジェクトをfileオブジェクトに変換します。さらに、そのfileオブジェクトをArrayBufferとして読み込んで、blobオブジェクトを作り、FormDataに結合して、upload()メソッドを呼び出します。

上記の仕組みにより、navigator.camera.getPicture()メソッドで取得した画像ファイルを、Formデータとしてサーバーに送信することが出来ます。

捕捉



上記の処理は、ちょっと冗長に感じると思います。そもそも、fileEntryオブジェクトのfileメソッドでfileオブジェクトを取得出来ているので(ややこしくてすみません)、これをそのままformdataにappendすれば簡単です。本来、FileクラスはBlobクラスの子クラスなので、そのままappendできて良いと思いますが、、、。しかしながら、実際に試してみると、そのやり方では現行のMonaca (Cordova 2.9相当)では、formdataはmultipartとして認識されず、通常のpostデータとして送られてしまいます。
(PHP側では、$_FILESではなく$_POST変数に値が入ります) このため、一度、ArrayBufferクラスを経由して、再度blobオブジェクトを作り直しています。

この問題は、おそらくですが、FileEntryやFileクラスがCordovaで定義されているクラスであるのに対し、Blobクラスはブラウザがもともともっているクラスであるため、FileクラスがBlobクラスの子クラスとして認識されないからだと思います。

Cordova 3系以後では、File Apiプラグイン扱いになり、外すことも出来るので、その場合は、上記のように一度ArrayBufferで読み直すことなく動作させることが出来るようになると思います。


まとめ



HTML5の持つFile APIは非常に強力で、ハイブリッドアプリを作る上でのデータ処理にはとても重要なのですが、その使い方はやや複雑だと思います。今回の記事では、FormとしてFile APIで取得したファイルの送信を例としてあげさせていただきました。Blobを持つ機種については、応用することで、写真をCampasに表示して加工を行い、それをファイルにしてアップロードしたりなども出来ますので、参考にして下さい。