Hatena::ブログ(Diary)

しばそんノート

2010-01-11

W3C File APIを使ってJavaScriptでファイル加工

現在W3Cで仕様策定が進められているFile APIを使うと、JavaScriptからローカルファイルの情報や内容にアクセス出来るようになります。

Firefoxでは3.0時代から似たような機能が実装されていたようですが*1、今回やや仕様を変更した上で標準化されます。

まだワーキングドラフトの段階ですが、Firefox 3.6 RC1*2に既にほとんどのAPIが実装されていますので、今すぐに使ってみることができます。

そこで、試しにこんなサンプルを作ってみました。Firefox 3.6で以下のページにアクセスしてみてください。

ファイル選択欄でビットマップファイル(.bmp)を選択、あるいはブラウザにドラッグアンドドロップすると、その画像をネガポジ反転(階調の反転)して表示します。複数選択も可です。*3 *4

↓実行例

f:id:shibason:20100111143129j:image

ファイル加工の際にサーバと一切通信せず、JavaScriptだけで処理を行っていることがポイントです。JavaScript/CSSを全てHTMLインラインで記述していますので、上のページをローカルに保存しておけば、LANケーブルを引っこ抜いても動作させられます。

今回はごく単純なサンプルですが、オープンソースの画像処理ライブラリ等をJavaScriptに移植すれば、もっと本格的な画像編集アプリケーションも作れそうです。

…もっとも、正直なところ、この類の処理をJavaScriptで実装するメリットはあまり無いのも事実なのですがw

強いて言えば、

  • プラグイン無しのブラウザだけで動作可能なアプリケーションで、
  • かつWeb上にデータを送信する必要が無い

あたりがメリットに成り得るでしょうか。

実際はCSVXMLなどのテキストデータを読み込ませてちょっとした処理を行うとか、ファイルをアップロードする前になんらかのバリデーションやフィルタをかけるとか、そういう使い方がメインになる…のかな?

サンプル解説

先程のサンプルのコード説明を少しだけ。抜粋になりますので、全コードは上のページでソース表示して見てください。File APIだけでなく、HTML5の各種機能も色々ごっちゃ混ぜになった説明になっていますが、あしからず。また、各仕様のアップデートにより、将来的にこのページの説明が間違ったものとなる可能性もありますので、ご注意ください。

最初に、ヘッダやフッタを除いたHTML本体部分*5のコードを載せておきます。

fileタイプのinput要素にmultiple属性をつけてやると、ファイルが複数選択できるようになります。

  // 203〜208行目
  <p> 
    ビットマップファイルを選択、またはここへドロップしてください。
    (複数選択可)
  </p>
  <input id="file_select" type="file" multiple="multiple" />
  <div id="result_container"></div>
選択されたファイルを取得する方法

fileタイプのinput要素で選択されたファイルはfiles属性から取得することができます。

  // 30〜33行目
  var file_select = document.getElementById('file_select');
  file_select.addEventListener('change', function() {
    update(file_select.files);
  }, false);
ドロップされたファイルを取得する方法

本題ではありませんが、ドラッグアンドドロップされたファイルの情報の取り方も。

ドラッグアンドドロップAPIの詳細は省きますが、DataTransferオブジェクトのfiles属性から取得することができます。

  // 36〜42行目
  document.documentElement.addEventListener('dragover', function(event) {
    event.preventDefault();
  }, false);
  document.documentElement.addEventListener('drop', function(event) {
    event.preventDefault();
    update(event.dataTransfer.files);
  }, false);
取得したファイルを処理する

いずれの方法で取得したファイルもFileList型になっていますので、添字を使って各ファイルにアクセスします。ファイルを一つしか選択しなかった場合でも必ずFileList型になります。

  // 53〜55行目
  for (var i = 0; i < files.length; i++) {
    result_container.appendChild(convert(files[i]));
  }
ファイルの情報を参照する

各ファイルの情報はFile型として格納されています。

name属性でパスを含まないファイル名、type属性でMIMEタイプ、size属性でバイト単位のサイズを取得できます。

  // 63〜69行目
  var info = document.createElement('div');
  var name = document.createElement('a');
  name.appendChild(document.createTextNode(file.name));
  info.appendChild(name);
  var text = ' (' + file.size + 'バイト) [' + file.type + ']';
  info.appendChild(document.createTextNode(text));
  result.appendChild(info);
ファイルの内容をバイナリで取得する

ファイルの内容を読み込むにはFileReader型を使用します。

readAsBinaryStringメソッドで、ファイルの内容をそのままバイナリで取得できます。

非同期で読み込まれますので、onloadイベントハンドラをセットしておき、その中でresult属性からデータを参照する形になります。*6

  // 80〜108行目
  var byte_reader = new FileReader();
  byte_reader.onload = function() {
    try {
      var bitmap = new BitMapImage(byte_reader.result);
      /* 中略 */    
    }
  };
  byte_reader.readAsBinaryString(file);
ファイルの内容をDataスキームURLで取得する

サンプルでは処理が重くなるのでコメントアウトしてありますが、readAsDataURLメソッドを使うと、ファイルの内容をDataスキームURLで取得できます。

これをそのままimgタグのsrc属性に設定すれば画像を表示できますし、aタグのhref属性に設定すればクリックでダウンロードさせることもできます。*7

  // 73〜77行目
  var url_reader = new FileReader();
  url_reader.onload = function() {
    name.setAttribute('href', url_reader.result);
  };
  url_reader.readAsDataURL(file);
ファイルの内容をテキストで取得する

サンプルでは使っていませんが、readAsTextメソッドを使うと、エンコードの変換を行った上でテキストを読み込むことができます。テキストファイル類はこちらを使用するのが良いでしょう。

バイナリデータを加工する

File APIには直接関係ありませんが、JavaScriptでバイナリデータを扱うには少し工夫が要ります。

中身はバイナリですが、あくまでもString型のデータとして返ってきますので、バイナリコードを取り出すには都度String#charCodeAtをかける必要があります。

サンプルでは、加工したデータを一旦配列にためています。

  // 177行目
  reversed_data[reversed_data.length] = 255 - this.data.charCodeAt(i);

この段階ではただの整数の配列ですので、再度これをバイナリデータに変換してやる必要があります。これはString.fromCharCodeで行えます。

  // 187〜188行目
  this.data = this.data.substr(0, this.off_bits) +
              String.fromCharCode.apply(null, reversed_data);

これでデータの加工は完了です。

その他

今回は加工したデータをimgタグで表示しただけですが、例えば取得・加工したデータをXMLHttpRequestで送信したり、aタグのhref属性にくっつけてダウンロード出来るようにしたり*8 *9、あるいはWeb Storageに保存してみたり、色々使い方はあるかと思います。

加工処理に時間がかかる場合はWeb Workersを使い、別スレッドで処理するようにしても良いかもしれません。

今までも非標準のブラウザ独自実装を使えば色々凝ったこともできましたが、これからは標準でこういう機能が使えるようになるということで、ちょっとワクワクしますね。

現在のところFile APIが使えるのはFirefox 3.6だけのようですが、他のHTML5関連の仕様も含め、早く多くのブラウザで使えるようになって欲しいなぁ、と思います。…そう遠いことではなさそうな気もしますけど。

参考サイト

*1:参考:Taken SPC : Firefox 3 における <input type="file"> で指定されたファイルへのアクセス

*2:2010年1月11日現在

*3:Windows形式、24bitカラー、圧縮なしのビットマップファイル以外には対応していません。

*4:あまり大きな、あるいは多数のファイルを選択すると、処理が非常に重くなります。

*5:articleタグ内

*6:同期読み込み用のインターフェースもあります。

*7:MIMEタイプによってはそのままブラウザのウィンドウに表示されます。

*8:MIMEタイプをapplication/octet-streamにすれば、常にダウンロードダイアログを出させることができます。

*9:ただし常にファイル名の入力が必要になります。本当はもっとベターなダウンロードの方法があれば良いのですが…。

2009-06-13

TwitterFoxのパフォーマンス改善

(2009/06/26追記)TwitterFoxのバージョンアップによってこの問題は解消しました。1.8.2以降であれば下記の修正は必要ありません。

※以下の内容は、私の中途半端な知識に基づいた適当な改造となっております。この内容をそのまま適用した場合、何らかの問題が発生するかもしれません。そのあたりは自己責任ということでお願い致します。

※間違っている部分へのツッコミは大歓迎です。というか、是非よろしくお願い致します。

前置き

TwitterFox、便利ですよね。私みたいな、それほどヘビーでもないけど、完全にライトなわけでもない…という中途半端なついたったー(ついったらー?)には、非常にお手軽でありがたいツールです。

そのTwitterFoxが2009/06/11に1.8にバージョンアップし、見た目的にも内部的にも大幅に変更が加えられました。*1

基本的には歓迎できる改良点ばかりなのですが、一つだけ、どうしても気になる点が…。

タイムラインの取得時に、今まで感じていなかった「引っかかり」のようなものを感じるようになってしまったんですね。私はよくニコニコ動画を見ているのですが、TwitterFoxが新しいステータスを受信するたびに動画再生が一瞬停止してしまったりして、正直ちょっとストレスを感じるようになってしまいました。

この機会に他のツールを探してみる、という選択肢もあったのですが、それなりにTwitterFoxを気に入っていますし、ボトルネックを探して自分で解決する方法をとってみることにしました。

原因

ここまでで既に前置きが長くなってしまいましたので…途中経過はばっさりと省略して。

結論から言うと、DB(SQLiteファイル)への書き込み処理がボトルネックとなっていた模様です。

components/nsTwitterFox.js の683行目から始まる TwitterNotifier#retrieveTimeline 、及び同ファイルの前半部分に定義されている User , Status, DirectMessage 各クラスの insertIntoDB メソッドを追ってみるとわかるのですが、どうやら

  • ステータスが1つ更新される度に3つのINSERT文が実行される
  • それらはトランザクションに纏められておらず、バラバラに実行される

という状態になっているらしいんですね。

トランザクションの外にあるINSERT文は、それ自体が一つの小さなトランザクションとして処理されますので、例えば1度の更新で20のステータスを受信した場合、20*3=60回ものトランザクションが実行されることになります。

さすがにそれでは重くもなりますよね…。

対応

ということで、やっつけ対応ですが、 TwitterNotifier#retrieveTimeline の中で msg.insertIntoDB を呼び出しているforループ全体を db.exec("BEGIN TRANSACTION");db.exec("COMMIT TRANSACTION"); で囲ってやります。

こんな感じ。

  retrieveTimeline: function(obj, req, method) {
    if (obj) {
      // Added 'unread' flag if the message is new in any of stored messages
      this.log("Get " + obj.length + " " + method);
      db.exec("BEGIN TRANSACTION"); // ← 追加
      for (var i in obj) {

        var msg;
        var result;

        if (method == "messages") {
          msg = new DirectMessage(obj[i]);
          result = msg.insertIntoDB(this._accounts[this._user].id);
        }
        else {
          msg = new Status(obj[i], method);
          result = msg.insertIntoDB(this._accounts[this._user].id);
        }
        msg.type = method;
        if (result) {
          this._unread[method].push(msg.id);
          this._newMessages.push(msg);
        }
      }
      db.exec("COMMIT TRANSACTION"); // ← 追加
    }
  },

これで何個ステータスを受信しようが1つのトランザクションで纏めて処理されますので、処理にかかる時間は最小限に抑えられるはずです。

実際に私の環境では、この変更を加えて以降、今のところ「引っかかり」は感じていません。やったねたえちゃん!

その他の変更

私版TwitterFoxでは、他にも個人的な好みにより、以下の修正が加えてあります。

components/nsTwitterFox.js の14行目

var MAX_STORED_MESSAGES = 40;

var MAX_STORED_MESSAGES = 100;

に変更。

更に、同じく components/nsTwitterFox.js の555〜556行目

      var count = this._unread[type].length;
      if (count < 20) count = 20;

      var count = this._unread[type].length + 20;
      if (count < MAX_STORED_MESSAGES) count = MAX_STORED_MESSAGES;

に変更。

見ていただければわかるかと思いますが、通常は最大100件、未読数が80件以上ある場合は未読数+20件まで一度に表示できるように変更してあります。

このあたりはタイムラインの速さによって色々調整してみるのもいいかもしれません。

パッチ

ここまでの変更をパッチに纏めると、こんな感じになります。

diff -u -r twitterfox-1.8.1-fx/components/nsTwitterFox.js twitternotifier@naan.net/components/nsTwitterFox.js
--- twitterfox-1.8.1-fx/components/nsTwitterFox.js      2009-06-10 09:40:18.000000000 +0900
+++ twitternotifier@naan.net/components/nsTwitterFox.js 2009-06-13 15:42:14.000000000 +0900
@@ -11,7 +11,7 @@
 var CLASS_NAME = "Twitter Notifier";
 var CONTRACT_ID = "@naan.net/twitternotifier;1";
 
-var MAX_STORED_MESSAGES = 40;
+var MAX_STORED_MESSAGES = 100;
 
 var TWEET_FRIENDS   = 0;
 var TWEET_MENTIONS  = 1;
@@ -552,8 +552,8 @@
     var type = obj.type;
     if (this._accounts[this._user]) {
       var ret = {};
-      var count = this._unread[type].length;
-      if (count < 20) count = 20;
+      var count = this._unread[type].length + 20;
+      if (count < MAX_STORED_MESSAGES) count = MAX_STORED_MESSAGES;
       if (type == "messages") {
         ret.msgs = this.restoreMessagesFromDB(this._accounts[this._user].id, count);
       }
@@ -684,6 +684,7 @@
     if (obj) {
       // Added 'unread' flag if the message is new in any of stored messages
       this.log("Get " + obj.length + " " + method);
+      db.exec("BEGIN TRANSACTION");
       for (var i in obj) {
 
         var msg;
@@ -703,6 +704,7 @@
           this._newMessages.push(msg);
         }
       }
+      db.exec("COMMIT TRANSACTION");
     }
   },
 

このパッチを %APPDATA%\Mozilla\Firefox\Profiles\(プロファイル名)\extensions\twitternotifier@naan.net\components\nsTwitterFox.js に対して適用すればOKです。

快適なTwitterFoxライフをお楽しみ下さい!

ついでに

1.8.1になってデフォルトフォントサイズが小さくなったようですので、見た目が大きく変わってちょっとギョっとするかもしれませんが、普通に設定画面*2からフォントサイズの変更ができますので、そこで大きめに設定してやればOKです。1.8以前はフォントサイズ12相当だった模様なので、私は12に設定しておきました。

*1:翌日06/12に早速1.8.1へのマイナーバージョンアップがありましたが、この記事で述べている範囲のことは1.8.1でもそのまま適用できます。

*2:TwitterFoxのアイコンを右クリック