Picup を利用して Rails アプリに iPhone / iPad から画像をアップロードする

ご存じ iPhone / iPad のブラウザである Mobile Safari は、HTML5 の機能を多数実装してるわりに、<input type="file"> を使ったファイルのアップロードができない。

もちろん専用のネイティブアプリを作成すれば可能だけど、まあ、ほとんどの場合めんどくさいよね。

そこで、多少ユーザを選ぶけど、便利そうなのが William Lindmeier 氏制作の iOS アプリ Picup である。

ここでは、Picup の仕組みと、Rails アプリでの実装例を紹介する。

Picup の仕組み

iOS や MacOS (X?) では、URL スキームにアプリケーションを関連づけられる。Picup の場合 fileupload:// に続けて、アプリに渡したいパラメータをつけて Safari 等で開けば、Picup にパラメータが渡され起動する。

サーバ側では、Picup の Web サイトで配布しているスクリプト (picup.js) を利用して、 を、fileupload://... を開くボタンに書き換える (別にこのスクリプトを使わなくても fileupload://... へのリンクを自前で作ってもいい)。

このボタンがクリックされると、POST 先の URL (postURL)、フィールド名 (postImageParam) などのパラメータを渡して Picup が起動される。そして、Picup 上で、ユーザーが、カメラで撮影するか既存の写真を選択し、Done をタップすると、さきほど渡されたパラメータをもとに POST してくれる。

さらに、callbackURL を指定すると、POST 後に任意の URL を Safari で開くこともできる。その際に # 以降にいくつかパラメータが渡されるので、それを利用して、ユーザーを誘導することもできる。

Rails での作例

以下では、Ruby 1.9.2 + Rails 3.0.7 + carrierwave で Picup による写真のアップロードに対応した Rails アプリを作る。

とりあえず初期設定。

$ rails new picup_demo -d mysql -J
# Gemfile
- gem 'mysql2'
+ gem 'mysql2', '0.2.7'
+ gem 'carrierwave'
+ gem 'jquery-rails'
$ bundle$ rails g jquery:install

jquery や後述の picup.js をロードするようにする。

# app/views/layouts/application.html.erb
- <%= javascript_include_tag :defaults %>
+ <%= javascript_include_tag :all %>

写真のモデル・コントローラ・ビューを scaffold でつくる。

$ rails g scaffold photo file:string
$ rake db:create
$ rake db:migrate

アップローダをつくり、photos.file に設定する。

$ rails g uploader photo
# app/models/photo.rb
  class Photo < ActiveRecord::Base
+   mount_uploader :file, PhotoUploader
  end

フォームのフィールドを、type="file" にする。show で写真を表示する。

# app/views/photos/_form.html.erb
- <%= form_for(@photo) do |f| %>
+ <%= form_for(@photo, html: {multipart: true}) do |f| %>
- <%= f.text_field :file %>
+ <%= f.file_field :file %>
# app/views/photos/show.html/erb
- <%= @photo.file %>
+ <%= image_tag(@photo.file.url) %>

この時点で PC の Web ブラウザからアップロードできるか確認しておくとよい。

$ rails s
# http://localhost:3000/photos/new へ Web ブラウザでアクセス

Picup のヘルパースクリプトを導入。http://picupapp.com/picup.js.zip をダウンロード、展開し、picup.js を public/javascripts 下に移動する。

type="file" のフィールドを Picup を開くボタンに置き換える。

# app/views/photos/_form.html.erb
<script type="text/javascript" charset="utf-8">
$(function(){
  window.name = "Picup_demo_new_photo";
   // アップロード後表示されるページから遷移するためにウィンドウ名をつけておく
  Picup.convertFileInput(
    "photo_file", // <input type="file" /> の ID
    { 'referrername': escape('Picup Demo'), // Picup に表示されるアプリケーション名
      'purpose': escape('Upload A Photo'), //Picup に表示されるメッセージ
      'postImageParam': 'photo_file', // フィールド名、この場合、rails では params[:photo_file] でアクセスできる。
      'postURL': 'http://192.168.1.10:3000/photos.json', // post先
      'callbackURL': 'http://192.168.1.10:3000/uploaded' // アップロード後に開く URL
    }
  );
});
</script>
# app/controllers/photos_controller.rb
  def create
-   @photo = Photo.new(params[:photo])
+   @photo = Photo.new(file: params[:photo_file])

IPアドレスは適当に書き換えて。 ここまででとりあえずアップロードはできる。 (アップロード後表示されるページは RoutingError になっちゃうけど、/photos を見ると、正しくアップロードできているのが確認できる)

アップロード後、アップロードされた写真のページへ遷移させたいので、create のレスポンスに、そのページの URL を含ませる。

# app/controllers/photos_controller.rb
  format.html { redirect_to(@photo, :notice => 'Photo was successfully created.') }
  format.xml { render :xml => @photo, :status => :created, :location => @photo }
+ format.json {
+   imgur_like_response = {
+     rsp: {
+       stat: "ok",
+       image: {
+         original_image: photo_url(@photo)
+       }
+     }
+   }
+   render :json => imgur_like_response
+ }

{rsp: ...} となっているハッシュは、imgur に画像をアップロードした際のレスポンスを模したものである。いままで触れなかったが、Picup は postURL の指定がない場合、imgur.com に画像をアップロードする。その際、レスポンスに含まれる original_image の値は、アップロード後開かれる URL の # 以降に remoteImageURL=... の形で渡される (imgur.com からのレスポンスでは他にもいろいろな情報が付与されているが、無視されている)。

# public/uploaded.html
<!DOCTYPE html>
<html>
  <head>
  <title>Picup Demo</title>
    <script>
      <!--
      var redirect = function() {
        var keys_values = window.location.hash.substring(1).split("&amp;");
        var params = {};
        for(var i = 0; i < keys_values.length; i += 1){
          var key_value = keys_values[i].split("=");
          params[key_value[0]] = key_value[1];
        }
        if (params.remoteImageURL) {
          window.open(unescape(params.remoteImageURL), 'Picup_demo_new_photo');
          window.close();
        }
      };
      redirect();
      //-->
    </script>
  </head>
  <body>
    <h1>アップロード完了</h1>
    <a href="javascript:redirect();">自動的に移動しない場合はここをタップしてください。</a>
  </body>
</html>

これが、アップロード後開かれるページである。URL の # 以降の remoteImageURL=... の値を読み出し、もともとフォームを表示していたタブ (Picup_demo_new_photo) で開き、このページ自身は閉じるようにしている。いちおうこれで、PC のブラウザからアップロードした際と似た挙動を実現できる。

なお、 の置き換えを、UA が Mobile Safari であることを確認した上で (Apple 公式の DetectingWebKit を使えば簡単) 行えば、PC のブラウザでも Mobile Safari でもほぼ同じように使える。

気になるところ

先述のとおり Picup.convertFileInput には postImageParam オプションで、送信されるファイルのフィールド名を指定できる。ここは、Rails の規約に従って photo[file] としたいところだが、実際に試してみると、Rails にはフィールド名が photo%5bfile%5d として渡されてしまい、params[:photo][:file] のようなかたちではアクセスできない。無用なエスケープがされちゃってるんですね。ちょっと残念。

あと、POST のレスポンスをもうすこし利用できるとうれしい。imgur のレスポンスに特化してるのがもったいない。