PhoneGapで手軽にiPhone/Androidアプリを作ろう

第4回Camera API/HTML5 Canvas/プラグインを利用したカメラアプリを作ろう

前回はPhoneGapを支える各種APIの紹介と、jQuery Mobileを用いたメモ帳アプリを実際に作成するところまでを紹介しました。本連載最終回となる今回は、実機でのテストをするまでの手順と、Camera APIとHTML5 Canvas、PhoneGapプラグインを使ったカメラアプリケーションの作成方法を紹介しましょう。

ユニバーサルアプリのビルド方法、実機テスト準備、外部プラグインの導入手順

今回はPhoneGapのCamera APIを使って、簡単なカメラアプリケーションを作成してみましょう。今回実装するカメラアプリの要件は次のとおりです。

  • iPhone/iPadのユニバーサルアプリケーションであること
  • iPhone/iPadともに横向き(Landscape)利用を前提とする
  • カメラで写真を撮るか、アルバム内の写真を選択する
  • 選択した写真に対して「グレースケール」⁠セピア」のフィルタ処理が行えるように
  • 画面数は「写真を取る/アルバムからの選択メニュー」⁠写真の確認/フィルタ処理/保存」の2画面
  • jQuery Mobileを利用して、index.html1ファイルで完結させる
  • 加工した写真はiOSのフォトアルバムに保存ができること

第1回で紹介した手順で新しいプロジェクトを作成します。プロジェクト名は「PhoneGapCamera」としました。今回は開発をはじめる前に、いくつかの準備を行います。

  • ①ユニバーサルアプリケーションをビルドするには
  • ②デバイスの向き(Device Orientations)とアプリのアイコン(App Icons⁠⁠、起動画面(Launch Images)のカスタマイズ
  • ③iPhone/iPad実機でテストする手順
  • ④外部プラグインの導入手順

①ユニバーサルアプリケーションをビルドするには

iPhone/iPad両デバイスに対応するユニバーサルアプリケーションをビルドするには、iOS Application Targetを変更する必要があります。Xcodeで作成したプロジェクトを選択し、Project Navigatorを表示します。画面右部に表示されるSummaryタブ内の「iOS Application Target」を、"iPhone"から"iPad"に変更します。

図1 Project Navigatorを開き、iOS Application Targetを"iPhone"から"iPad"に変更
図1 Project Navigatorを開き、iOS Application Target

これでユニバーサルアプリケーションをビルドする準備が整いました。アプリケーション内部ではユーザエージェントやDevice APIを用いて、処理や画面幅の調整をiPhone/iPadごとに処理を分岐させればOKです。

②デバイスの向き(Device Orientations)とアプリのアイコン(App Icons)、起動画面(Launch Images)のカスタマイズ

デバイスの向きとアプリのアイコン、起動画面をカスタマイズするときも①と同様、Project Nabigatorから行います。デバイスの向きは「Supported Device Orientations」から選択します。

  • Portrait: 縦向き
  • Upside Down: 縦向き(逆さま)
  • Landscape Left: 左横向き
  • Landscape Right: 右横向き
図2 Supported Device Orientations。4種類の中から対応させるデバイスの向きを選択
図2 Supported Device Orientations。4種類の中から対応させるデバイスの向きを選択

Supported Device Orientationsは複数選択することが可能です。今回はLandScape Rightのみを選択しました。

アプリのアイコンは「App Icons」から変更します。通常のアイコンサイズは57x57ピクセル、Retinaディスプレイ用のアイコンサイズは114×114ピクセルとなっています。デフォルトのファイルパスは次のとおりです。

  • 通常アイコン:
    ⁠PhoneGapプロジェクト名)/Resources/icons/icon.png
  • Retinaディスプレイアイコン:
    ⁠PhoneGapプロジェクト名)/Resources/icons/icon@2x.png

パスのアイコンファイルを直接編集するか、用意した画像ファイルをApp Iconsにドラッグ&ドロップして変更します。

図3 App Iconsを変更
図3 App Iconsを変更

起動画面は「Launch Images」から変更します。通常の画像サイズは320x480ピクセル、Retinaディスプレイ用の画像サイズは640x960ピクセルとなっています。デフォルトのファイルパスは次のとおりです。

  • 通常:
    ⁠PhoneGapプロジェクト名)/Resources/splash/Default.png
  • Retinaディスプレイ
    ⁠PhoneGapプロジェクト名)/Resources/splash/Default@2x.png

アイコン同様、パスのファイルを直接編集するか、用意した画像ファイルをLaunch Imagesにドラッグ&ドロップして変更します。

図4 Launch Imagesを変更
図4 Launch Imagesを変更

③iPhone/iPad実機でテストする手順

iPhoneシミュレータではカメラを利用することができないので、実機でテストを行う必要があります。実機に開発中のアプリケーションをインストールできるように、プロビジョニングファイルの設定などを行います。iOSデバイス上にアプリケーションをインストールするには、iOS Developer Programへの参加が必要です。ここでは、アクティベーションと証明書の登録作業が完了している前提で解説を行っていきます。

Xcodeを起動した状態でiOSデバイスを接続すると、Organizerが起動します。まだ1回も開発/デバッグ用途で使用したことのないデバイスの場合「Use for Development」ボタンが表示されます。

図5 Organizerにて、iOSデバイスに開発中のアプリケーションをインストールするための準備を行う
図5 Organizerにて、iOSデバイスに開発中のアプリケーションをインストールするための準備を行う

「Usr for Development」ボタンをクリックすると、iPhone Provisioning Portalへの認証が行われます。iPhone Dev Centerへログインするためのユーザ名とパスワードを入力して認証を行います。

図6 iPhone Provisioning Portalへの認証を行う
図6 iPhone Provisioning Portalへの認証を行う

認証を行ってしばらくすると、App IDの登録、プロビジョニングファイルの作成・登録が完了します。

図7 1ボタンでApp ID、プロビジョニングファイルの作成・登録が完了
図7 1ボタンでApp ID、プロビジョニングファイルの作成・登録が完了

これでこのデバイス上に開発中のアプリケーションをインストールすることが可能になりました。

④外部プラグインの導入手順

PhoneGap APIには「写真をフォトアルバムに保存する」機能が用意されていません。PhoneGapでサポートされていないネイティブの機能を使用する場合は、Objective-CでPhoneGapプラグインを作成し、JavaScriptからPhoneGap.exec()で呼び出す実装となります。

iOSで写真をフォトアルバムに保存するには、Objective-CのUIImageWriteToSavedPhotosAlbumメソッドを使用します。このUIImageWriteToSavedPhotosAlbumをPhoneGapから利用できるようにしたのが、SaveImageです。SaveImageはmyfreeweb(Grigory V.)が開発・公開しているPhoneGapプラグインの1つで、The MIT Licenseのもとで公開されています。SaveImageで画像を保存するJavaScriptコードのサンプルは次のとおりです。

window.plugins.SaveImage.saveImage('(base64エンコードされた文字列)');

SaveImageプラグインを利用するための手順は次のとおりです。

  • (1)www/以下にSaveImage.jsをデプロイ
  • (2)Plugins/以下にSaveImage.h, SaveImage.mをデプロイ後、修正
  • (3)Supporting Files/PhoneGap.plistを編集

④-(1)www/以下にSaveImage.jsをデプロイ

wwwディレクトリ以下にSaveImage.jsをデプロイします。index.htmlでは、このJavaScriptをロードする1文を追加します。

<script type="text/javascript" charset="utf-8" src="SaveImage.js"></script>

④-(2)Plugins/以下にSaveImage.h, SaveImage.mをデプロイ後、修正

Plugins以下にSaveImage.h, SaveImage.mをデプロイします。デプロイ後、2ファイルを修正します。2ファイルのdiffは次のとおりです。

リスト1 SaveImage.hの修正点(diff)
--- SaveImage.h.orig    2010-09-08 11:40:12.000000000 +0900
+++ SaveImage.h 2011-09-10 21:07:57.000000000 +0900
@@ -7,9 +7,9 @@
//

#import <foundation>
-#import "NSData+Base64.h"
-#import "PhoneGapCommand.h"
-@interface SaveImage : PhoneGapCommand {
+#import "PhoneGap/NSData+Base64.h"
+#import "PhoneGap/PGPlugin.h"
+@interface SaveImage : PGPlugin {
}

- (void)saveImage:(NSMutableArray*)sdata withDict:(NSMutableDictionary*)options;
リスト2 SaveImage.mの修正点(diff)
--- SaveImage.m.orig    2010-09-08 11:40:12.000000000 +0900
+++ SaveImage.m 2011-09-10 21:07:43.000000000 +0900
@@ -6,7 +6,7 @@
//  MIT licensed
//

-#import "NSData+Base64.h"
+#import "PhoneGap/NSData+Base64.h"
#import "SaveImage.h"
@implementation SaveImage
図8 Plugins/以下にSaveImage,h, SaveImage.mをデプロイ・修正
図8 Plugins/以下にSaveImage,h, SaveImage.mをデプロイ・修正

リリースされて時間のたっているPhoneGapプラグインの場合、旧称のファイルをインポートしようとしてビルドが通らないことがあります。たとえば"PhoneGapCommand.h"などです。"PhoneGapCommand.h"は現在、"PGPlugin.h"にファイル名が変更されています。PhoneGap iOS Plugins Problemsなどを参考にしながら、修正を行っていきましょう。

④-(3)Supporting Files/PhoneGap.plistを編集

最後にSupporting FilesのPhoneGap.plistを編集し、SaveImageプラグインを実行できるようにします。Pluginsディクショナリに以下の摘要でSaveImageを追加します。

  • Key: SaveImage
  • Type: String
  • Value: SaveImage
図9 PhoneGap.plistのPluginsディクショナリにSaveImageを追加
図9 PhoneGap.plistのPluginsディクショナリにSaveImageを追加

なお、導入するプラグインの種類によっては、各種フレームワークを追加する必要があります。プラグインのREADMEを参照しましょう。

Camera API/HTML5 Canvas/プラグインを利用したカメラアプリ開発

それではいよいよカメラアプリの開発を行ってみましょう。wwwディレクトリ以下にjquery Mobileの成果物を一式デプロイします。今回はCreative Commonsライセンスの画像ファイルを用いました。

Xcodeを開き、www/index.htmlを編集します。

リスト3 カメラアプリのwww/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Capture Photo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no;" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <link rel="stylesheet" href="jquery.mobile-1.0b2.min.css" />
	<script type="text/javascript" charset="utf-8" src="jquery-1.6.2.min.js"></script>
    <script type="text/javascript" charset="utf-8" src="jquery.mobile-1.0b2.min.js"></script>
    <script type="text/javascript" charset="utf-8" src="phonegap-1.0.0.js"></script>
    <script type="text/javascript" charset="utf-8" src="SaveImage.js"></script>
    <script type="text/javascript" charset="utf-8">
    <!--
    var pictureSource;
    var destinationType;
    
    document.addEventListener('deviceready', onDeviceReady, false);
    function onDeviceReady()
    {
        pictureSource = navigator.camera.PictureSourceType;
        destinationType = navigator.camera.DestinationType;

        // デバイス判定。画像の幅/高さの調節に使用
        if ( -1 < device.platform.indexOf('iPad') )
        {
            photoWidth = 1024;
            photoHeight = 768;
        }
        else
        {
            photoWidth = 450;
            photoHeight = 180;
        }
        $('.rawImage').css('width', photoWidth+'px').css('height', photoHeight+'px');
        $('.resultImage').css('width', photoWidth+'px').css('height', photoHeight+'px');

        // カメラ起動
        $('.capturePhoto').live
        (
            'click',
            function()
            {
                navigator.camera.getPicture
                (
                    onPhotoDataSuccess, onFail,
                    {
                        quality: 50, 
                        encodingType: Camera.EncodingType.PNG,
                        targetWidth: photoWidth,
                        targetHeight: photoHeight,
                        allowEdit: true 
                    }
                );
            }
        );
        
        // フォトアルバムから画像を選択
        $('.getPhoto').live
        (
            'click',
            function()
            {
                navigator.camera.getPicture
                (
                    onPhotoDataSuccess, onFail,
                    {
                        quality: 50, 
                        destinationType: destinationType.DATA_URL,
                        encodingType: Camera.EncodingType.PNG,
                        sourceType: pictureSource.PHOTOLIBRARY,
                        targetWidth: photoWidth,
                        targetHeight: photoHeight
                    }
                )
            }
        );

        // 成功時のコールバック関数
        function onPhotoDataSuccess(imageData)
        {
            $('.rawImage').attr('src', 'data:image/png;base64,' + imageData).show();
            $('.resultImage').attr('src', 'data:image/png;base64,' + imageData).hide();
            location.href='#edit';
            $('select#filterValue option:first-child').attr('selected', 'false');
            $('select#filterValue').selectmenu('refresh');
        }

        // 失敗時のコールバック関数
        function onFail(message)
        {
            alert('Failed because: ' + message);
        }

        // Canvasでの再描画
        function reDrawImage()
        {
            
            var canvas = document.createElement('canvas');
            var ctx = canvas.getContext('2d');
            var imgObj = new Image();
            imgObj.src = $('.rawImage').attr('src');
            canvas.width = imgObj.width;
            canvas.height = imgObj.height;
            ctx.drawImage(imgObj, 0, 0);

            // フィルタ処理
            switch($('#filterValue').val())
            {
                case 'grayscale':
                    var imgPixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
                    for(var y = 0; y < imgPixels.height; y++)
                    {
                        for(var x = 0; x < imgPixels.width; x++)
                        {
                            var i = (y * 4) * imgPixels.width + x * 4;
                            
                            // via Web Designer Wall - HTML5 Grayscale Image Hover
                            // http://webdesignerwall.com/tutorials/html5-grayscale-image-hover
                            var avg = (imgPixels.data[i] + imgPixels.data[i + 1] + imgPixels.data[i + 2]) / 3;
                            imgPixels.data[i] = avg;
                            imgPixels.data[i + 1] = avg;
                            imgPixels.data[i + 2] = avg;
                        }
                    }
                    ctx.putImageData(imgPixels, 0, 0, 0, 0, imgPixels.width, imgPixels.height);
                    break;

                case 'sepia':
                    var imgPixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
                    for(var y = 0; y < imgPixels.height; y++)
                    {
                        for(var x = 0; x < imgPixels.width; x++)
                        {
                            var i = (y * 4) * imgPixels.width + x * 4;
                            var r = imgPixels.data[i];
                            var g = imgPixels.data[i+1];
                            var b = imgPixels.data[i+2];

                            // via CamanJS - sepia
                            // https://github.com/meltingice/CamanJS
                            var adjust = 80 / 100;
                            r = Math.min(255, (r * (1 - (0.607 * adjust))) + (g * (0.769 * adjust)) + (b * (0.189 * adjust)));
                            g = Math.min(255, (r * (0.349 * adjust)) + (g * (1 - (0.314 * adjust))) + (b * (0.168 * adjust)));
                            b = Math.min(255, (r * (0.272 * adjust)) + (g * (0.534 * adjust)) + (b * (1- (0.869 * adjust))));
                            imgPixels.data[i] = r;
                            imgPixels.data[i + 1] = g;
                            imgPixels.data[i + 2] = b;
                        }
                    }
                    ctx.putImageData(imgPixels, 0, 0, 0, 0, imgPixels.width, imgPixels.height);
                    break;

                default:
                    break;
            }
            $('.rawImage').hide();
            $('.resultImage').attr('src', canvas.toDataURL()).show();
        }
            
        // フィルタのプルダウンが操作された場合に、画像の再描画を実施
        $('#filterValue').live
        (
           'change', reDrawImage
        );
         
        // 画像をフォトアルバムに保存
        $('#save').live
        (
            'click',
            function ()
            {
                var src = $('.resultImage').attr('src');
                window.plugins.SaveImage.saveImage(src.replace('data:image/png;base64,', '')); 
            }
        );
    
    }
    -->
    </script>
    <style>
    <!--
    .rawImage, .resultImage
    {
        display: none;
    }
    .menuColumn
    {
        width:49%;
        float:left;
    }
    .menuColumn p
    {
        text-align: center;
    }
    -->
    </style>
  </head>
  <body>
    <div id="home" data-role="page">
        <div data-role="header" data-theme="b"> 
            <h1>Camera</h1>
        </div> 
        <div data-role="content">
            <div style="width:100%">
                <div class="menuColumn">
                    <p>
                        <img src="Black-Camera-Symbol-128.png" class="capturePhoto" />
                    </p>
                    <input type="button" class="capturePhoto" value="Capture Photo">
                </div>
                <div class="menuColumn">
                    <p>
                        <img src="Album_128.png" class="getPhoto" />
                    </p>
                    <input type="button" class="getPhoto" value="From Photo Library">
                </div>
            </div>
        </div>
    </div>
    
    <div id="edit" data-role="page">
        <div data-role="header" data-theme="b">
            <a data-rel="back" href="#home" data-direction="reverse" data-role="button" data-icon="arrow-l">Back</a>
            <h1>Result</h1>             
            <a id="save" href="javascript:void(0)" data-direction="reverse" data-role="button" data-icon="check">Save</a>
        </div> 
        <div data-role="content">
            <img class="rawImage" src="" />
            <img class="resultImage" src="" />
        </div>
        <div data-role="footer" data-position="fixed" data-theme="b" class="ui-bar">
            <label for="filterValue">Filter:</label> 
            <select id="filterValue">
                <option value="No filter">No fileter</option>
                <option value="grayscale">Gray scale</option>
                <option value="sepia">Sepia</option>
            </select>
        </div> 
    </div>
        
  </body>
</html>

これをiPhoneで実行してみましょう。

図10 iPhoneにアプリケーションをインストール。アイコンがProject NavigatorのApp Iconsで変更したものになっている
図10 iPhoneにアプリケーションをインストール。アイコンがProject NavigatorのApp Iconsで変更したものになっている
図11 iPhoneでPhoneGapCameraを起動。スプラッシュがProject NavigatorのLaunch Imagesで変更したものになっている
図11 iPhoneでPhoneGapCameraを起動。スプラッシュがProject NavigatorのLaunch Imagesで変更したものになっている
図12 メニュー画面が表示される
図12 メニュー画面が表示される

アプリケーションを起動すると、メニューが表示されます。カメラで写真を撮る場合は左の「Capture Photo」から、フォトライブラリから写真を選択する場合は右の「From Photo Library」から行います。

アプリケーションの起動時にdevice.platformでiPhoneかiPadの判定を行っており、それぞれに最適化した幅・高さを変数に代入しています。

「Capture Photo」をタップすると、navigator.camera.getPictureでカメラを起動します。encodingTypeに"Camera.EncodingType.PNG"と指定することで、エンコードタイプをPNG形式にしています。また、allowEditをtrueにすることで写真を撮ったあとに簡単なリサイズ・拡大縮小を行えるようにしています。成功時のコールバック関数には、base64エンコードされた文字列が渡されます。

図13 ⁠Capture Photo」を選択し、カメラを起動
図13 「Capture Photo」を選択し、カメラを起動

「From Photo Library」をタップすると、navigator.camera.getPictureメソッドをもちい、sourceTypeに"pictureSource.PHOTOLIBRARY"を指定してフォトアルバムから写真を選択するUIを表示します。destinationTypeに"destinationType.DATA_URL"を指定することで、成功時のコールバック関数にbase64エンコードされた文字列が渡るようにしています。これは、次の画面でCanvasを用いて画像処理を行えるようにするためです。

図14 ⁠From Photo Library」を選択し、フォトライブラリを起動
図14 「From Photo Library」を選択し、フォトライブラリを起動

写真データがコールバック関数に渡されると、<img class="rawImage" />, <img class="resultImage" />に画像データを格納します。rawImageには画像加工前のデータを、resultImageにはCanvasでフィルタ処理を行った後の画像データを格納していきます。

図15 Result画面。撮影または選択した写真を閲覧・加工・保存する
図15 Result画面。撮影または選択した写真を閲覧・加工・保存する

撮影または選択した写真を閲覧・加工するResult画面では、フィルタ処理と画像の保存が行えます。フィルタでは「グレースケール」⁠セピア」の2種類が選択でき、プルダウンを変更すると写真にフィルタがかかります。

図16 フィルタでは「Gray scale」⁠Sepia」の2種類が選択可能
図16 フィルタでは「Gray scale」「Sepia」の2種類が選択可能

プルダウンが変更されると、reDrawImage()関数が呼び出されます。この関数では<img class="rawImage" />内の画像データを、Canvasで色調を変更し、<img class="resultImage" />に格納します。色調の計算式は、次のWebサイト、オープンソースソフトウェアの式を引用しました。

図17 フィルタで「Gray scale」を選択すると、写真がグレースケールに
図17 フィルタで「Gray scale」を選択すると、写真がグレースケールに
図18 フィルタで「Sepia」を選択すると、写真がセピア調に
図18 フィルタで「Sepia」を選択すると、写真がセピア調に

右上の「Save」ボタンをタップすると、フォトアルバムに保存が行われます。フォトアルバムへの保存は、PhoneGapプラグイン - SaveImageを使って実現しています。

図19 ⁠Save」ボタンをタップすると、フォトアルバムに保存が行われる
図19 「Save」ボタンをタップすると、フォトアルバムに保存が行われる
図20 iPadでPhoneGapCameraを起動・操作
図20 iPadでPhoneGapCameraを起動・操作

最後に

全4回の「PhoneGapで手軽にiPhone/Androidアプリを作ろう⁠⁠、いかがでしたでしょうか。いつものWebアプリを開発する要領で、実機でネイティブアプリケーションが動作するのを見ると、なかなか感動的です。

PhoneGapを使うことで、デザインはHTML5とCSSで行い、ネイティブ機能はPhoneGap APIを利用。PhoneGap APIで行えない操作はObjective-Cで外部プラグインとして実装といった、お互いの長所を活かし、短所を補える開発スタイルも可能です。スマートフォン向けアプリを開発してみたいけどObjective-C/Javaは敷居が高い…という方から現役のiOS/Androidアプリデベロッパまで、さまざまな開発現場で活用できるのではないでしょうか。

同様の開発環境としてTitanium Mobileがあります(gihyo.jpでの倉井龍太郎氏による連載Titanium Mobileで作る! iPhone/Androidアプリ⁠。興味がある方はこちらもぜひチェックしておきたいところです。

それでは、ご愛読ありがとうございました :)

おすすめ記事

記事・ニュース一覧