gracetory’s blog

東池袋にある合同会社グレストリのエンジニアブログです

Monaca(Cordova)にて、カメラで撮影して画像をアップロードする方法

f:id:grnishi:20191129182904j:plain

はじめに

こんにちわ。エンジニアのgrnishiです。中曽根元総理が亡くなったそうです。日本専売公社、日本国有鉄道、日本電信電話公社の民営化、半官半民だった日本航空の民営化などを行ったらしいですが、私はその頃子供だったためいまいちピンと来ません。

私にとっての民営化といえば郵政民営化や道路公団民営化ですかね。東京に住んでいなかったので、東京メトロや成田空港は印象が薄いです。

本題

Monacaで作ったアプリに「カメラで撮影し、撮影した画像をサーバに保存する」という機能を入れる必要があり、四苦八苦した思い出についてまとめたいと思います。

※掲載しているソースコードは説明用に省略して書いています。

環境

クライアント側環境

Cordovaバージョン: 9.0.0

iOSプラットフォーム: 5.0.1

Androidプラットフォーム: 8.0.0

Gradleバージョン: 4.10.3

Xcodeバージョン: 10.2.1

サーバ側環境

CentOS: 6.10

Apache: 2.2.34

PHP: 7.2.10

何はともあれカメラで撮影できるように。

まずはcordova-plugin-cameraを有効可します。

f:id:grnishi:20191129133438p:plain

こいつです。

下記ソースコードです。

function takePicture(){
  var option = {
    destinationType: Camera.DestinationType.DATA_URL, 
    encodingType: Camera.EncodingType.JPEG,
    saveToPhotoAlbum: true, //撮影後端末に保存
    correctOrientation: true // 撮影時と同じ向きに写真を回転
  };

  //カメラを起動
  navigator.camera.getPicture(onSuccess, onError, option);

  //成功時に呼び出されるコールバック関数
  function onSuccess(imageData){
    if (typeof(imageData) != 'undefined' && imageData != '') {
      $("#pic").attr("src","data:image/jpeg;base64," + imageData);
 }
  }

  //失敗時に呼び出されるコールバック関数
  function onError(message){
    alert("Error:" + message);
  }
}
  <ons-icon icon="fa-camera" size="32px" fixed-width="false" style="color:dodgerblue" onClick="takePicture()"></ons-icon>
  <img id="pic" src="" width="80%">

これで撮影はできますし、撮影した画像を表示されるようになりました。 さて、ここからサーバにアップロードする処理を追記していきます。

base64を使う。

さて、サーバにアップロードするために下記のように修正しました。

function takePicture(){
  var option = {
    destinationType: Camera.DestinationType.DATA_URL, 
    encodingType: Camera.EncodingType.JPEG,
    saveToPhotoAlbum: true, //撮影後端末に保存
    correctOrientation: true // 撮影時と同じ向きに写真を回転
  };

  //カメラを起動
  navigator.camera.getPicture(onSuccess, onError, option);

  //成功時に呼び出されるコールバック関数
  function onSuccess(imageData){
    if (typeof(imageData) != 'undefined' && imageData != '') {
      $("#pic").attr("src","data:image/jpeg;base64," + imageData);

      $.ajax({
        type: 'post',
        url: "http://example.com/upload.php",
        data: {image_data: image,},
        dataType: 'json',
        cache: false,
        async: false,
        success: function(e, textStatus) {
          // アラート
          alert('アップロードに成功しました。');
        },
        error: function(XMLHttpRequest, textStatus, errorThrown) {
          alert("error");
        },
      });
    }
  }

  //失敗時に呼び出されるコールバック関数
  function onError(message){
    alert("Error:" + message);
  }
}
<?php 
  // ファイル名と保存場所の設定
  $filename = date('YmdHis') . '.jpg';
  $filepath = 'upload/' . $filename;

  // 画像をサーバーに保存
  file_put_contents($filepath, base64_decode($_POST['image_data']));

  // jsonで出力
  header("Content-Type: text/javascript; charset=utf-8");
  echo "ok";
  die;

base64エンコードされた画像がimage_dataに入ってくるはずなので、何度やってもPHPでデコードして保存しているのだが、画像ファイルが壊れてしまう。

ログを確認すると、クライアントから送られて来ているbase64文字列に不正があるのか、明らかに長い文字列生成され、PHPでの復元に失敗しました。

どの道base64は安定せず、メモリ不足に陥りやすいっていう話ですしね。

FileTransferを使う

Base64エンコードがうまくいかないので次はファイルをそのままアップロードする事としまうす。

cordova-plugin-file-transfer

プログラムを以下のように変更。

function takePicture(){
  var option = {
    destinationType: Camera.DestinationType.FILE_URL, 
    encodingType: Camera.EncodingType.JPEG,
    saveToPhotoAlbum: true, //撮影後端末に保存
    correctOrientation: true // 撮影時と同じ向きに写真を回転
  };

  //カメラを起動
  navigator.camera.getPicture(onSuccess, onError, option);

  //成功時に呼び出されるコールバック関数
  function onSuccess(FileURL){
    if (typeof(FileURL) != 'undefined' && FileURL!= '') {
      $("#pic").attr("src",FileURL);

      var options = new FileUploadOptions();
      options.fileKey = "file";
      options.fileName = "test.jpg";
      options.mimeType = "image/jpeg";

      var fileTransfer = new FileTransfer();
      fileTransfer.upload(FileURL, encodeURI("http://example.com/upload.php"), onUploadSuccess, onUploadFail, options);
      function onUploadSuccess(result) {
        alert('アップロードに成功しました。');
      }
      function onUploadFail(error) {
          alert("error");
      }
    }
  }

  //失敗時に呼び出されるコールバック関数
  function onError(message){
    alert("Error:" + message);
  }
}
<?php 
  // ファイル名と保存場所の設定
  $filename = date('YmdHis') . '.jpg';
  $filepath = 'upload/' . $filename;

  // 画像をサーバーに保存
  $ret = move_uploaded_file($_FILES['file']['tmp_name'], $filepath);

  // jsonで出力
  header("Content-Type: text/javascript; charset=utf-8");
  if ($ret) {
    echo "ok";
  } else {
    echo "ng";
  }
  die;

実行してみる。

ReferenceError: FileTransfer is not defined

ん?なぜだ。ちゃんとcordova-plugin-file-transferを有効化してるのに。。。

なぜだかさっぱりわからないので下記を入れてみた。

<script>
document.addEventListener("deviceready", onDeviceReady, false);
function onDeviceReady() {
  console.log(FileTransfer);
}
</script>
undefined

むむ。。ここでやっと公式マニュアルを見てみる。

docs.monaca.io

非推奨プラグイン って書いてある。

これが原因かどうかはわかりませんが非推奨なら使っちゃ駄目だろうという事でリンクにあった

Transition off of cordova-plugin-file-transfer - Apache Cordova

ここを読んで実装へ。

XHRを使う

function takePicture(){
  var option = {
    destinationType: Camera.DestinationType.FILE_URL, 
    encodingType: Camera.EncodingType.JPEG,
    saveToPhotoAlbum: true, //撮影後端末に保存
    correctOrientation: true // 撮影時と同じ向きに写真を回転
  };

  //カメラを起動
  navigator.camera.getPicture(onSuccess, onError, option);

  //成功時に呼び出されるコールバック関数
  function onSuccess(imageURI) {
    window.resolveLocalFileSystemURL(imageURI, fileEntryToArrayBuffer, onError);
  }

  //失敗時に呼び出されるコールバック関数
  function onError(message){
    alert("Error:" + message);
  }

  function fileEntryToArrayBuffer(fileEntry) {
    // Fileオブジェクトを取得
    fileEntry.file((file) => { 
    // FileReaderを生成
    let reader = new FileReader();
    // ArrayBuffer形式への変換完了時の処理
    reader.onloadend = ArrayBufferToBlob;
    // ArrayBuffer形式に変換
    reader.readAsArrayBuffer(file);
    }, onError);
  }

  function ArrayBufferToBlob(event) {
    var formData = new FormData();
    formData.append("image", new Blob([event.target.result],{"type":"jpeg"}), "test.jpg");

    $.ajax({
      type: 'post',
      url: "http://example.com/upload.php",
      data: form_data,
      dataType: 'json',
      cache: false,
      async: false,
      success: function(e, textStatus) {
        // アラート
        alert('アップロードに成功しました。');
      },
      error: function(XMLHttpRequest, textStatus, errorThrown) {
        alert("error");
      },
    });
  }
}

phpは変わっていないです。

成功しました。

あれこれググったりしながら作ったのですが、環境の違い、情報が古いなど色々苦しみました。

さいごに

この業界、変化のスピードがとても早いです。ほんの1年前の有用な情報が今はゴミになっている事もあります。

だからといってアウトプットする意味が無いというわけではありません。

読み手の事を考えると、いつ、どういう環境で動かしたものなのかというものはきちんと書いておく事が重要なのかなと思いました。