2009年6月アーカイブ

twitterとwassrに両対応したクライアントに、sabotterと言うのがあって、ずっと愛用している。
しかし、リリース版の公開は0.0.4で止まっていて、codereposのリポジトリではlang/javaからplatform/eclipseへ場所を移して開発が続いているらしい。(最初、lang/javaの方を見ていて、trunkを見ようとすると見れなくてはまった)
今回、twitterのidが32bit signed の限界を超えてしまって、いくつかのクライアントで問題が出たらしいけど、sabotterも新しいタイムラインが流れてこなくなってしまった。
幸いなことに、sabotterもsabotterが使っているtwitter4jもソースが公開されているので、手元でビルドして32bit問題に対応してしまおうじゃないかと言うコーナー。
ただし、場当たり的な対応なので、本家に送るようなパッチにはできていない。また、途中に感想や疑問を思ったままに書くけど、決して批判しているわけではなく、両ソフトの開発者の方々には日々感謝していることを明記しておく。

sabotterはeclipseのプラグインとして開発されたものなので、ビルドにはeclipseが必要。
eclipseにはいろいろなバージョンがあるんだけど、最初はたまたま自分のマシンに入っていた3.2.0で始めたら、どうも古過ぎたらしくいくつかプラグイン開発に必要なクラスが入っていなくてコンパイルできなかったため、PleiadesからEclipse 3.4.2 Ganymede SR2 32bit ベース / Pleiades All in One 3.4.2.20090426 のJava版を持ってきてインストールした。(実は、sabotterのビルドにはこれでは新しすぎたらしい)
これだと最初からsubversion関連が入っているので、ファイルメニューのインポートを選び、SVN - SVNからプロジェクト でソースが持ってこれる。
リポジトリーロケーションに、http://svn.coderepos.org/share/platform/eclipse/sabotter/trunk を入力し、「URLを正規化しますか?」の質問にはいいえと答え、「選択したリソースの子プロジェクトを検索」を選ぶと、sabotterのプラグインプロジェクトがたくさんチェックアウトされる。全体で一つのプロジェクトではなく、プラグイン/フィーチャー毎にプロジェクトが別れている。
Windowsだと、デフォルトの文字コードがMS932になっていてソースが文字化けするので、設定の一般 - ワークスペースで文字コードをUTF-8にしておく。
ソースを追っていくと、今回の32bit問題は、sabotter側ではなくtwitter4j側のようだった。
で、今度はtwitter4jのソースを持ってくる。
ファイルメニューからインポートを選び、SVNからプロジェクトを選ぶところまでは一緒。リポジトリロケーションに http://yusuke.homeip.net/svn/twitter4j/trunk を入力し、今度は「名前を指定してプロジェクトとしてチェックアウト」を選んだが、ウィザードの方が良かったかも知れない。
twitter4jは、MAVENを使っているらしいけど、build.xmlも含まれているのでantでビルドできる。
この場合、eclipseとは別にJDKがインストールしてあって、環境変数JAVA_HOMEとPATHを適切に設定する必要がある。
twitter4jのソースで修正したのは、twitter4j.AsyncTwitterとtwitter4j.Twitterの二つのクラス。
これらのファイルの中で、int statusIdとか、int sinceId とか int idと言う箇所を探して、片っ端から long に変更していく。idのところはフレンドIDとかページのところは変えずに、ステータスIDの所だけ修正した。
build.xmlを右クリックして、実行 - Ant ビルドを選ぶと、文字コード関連でエラーになったので、build.xmlの中のjavacタスクに encoding="utf-8" を追加した。
これで、32bit問題を解決した twitter4jができたので、これをsabotterに組み込む。
jp.xet.sabotter.coreプロジェクトのプロパティを開き、「Javaのビルドパス」を選択する。
そして、twitter4jを選び、「JARファイルのマイグレーション」を選ぶ。
参照ボタンを押し、先ほどビルドしたtwitter4jのライブラリを選択する。(アドホックな対応としては、このとき「既存のファイル名を保存する」にチェックをした方が良かったかも)
同じプロジェクトのbuild.propertiesを開き、bin.includesにあるtwitter4jのライブラリファイル名を変更する。
META-INF/MANIFEST.MFを開き、Bundle-ClassPath にあるtwitter4jのライブラリファイル名を変更する。
ここからいよいよsabotterのビルド。ちなみに、プラグイン版ではなくてスタンドアロン版。
jp.xet.sabotter.standaloneプロジェクトのallinone.productを開き、「構成」タブを開く。
「必須プラグインを計算する際に・・・」のチェックボックスをチェックし、「必須プラグインの追加」ボタンを押す。(これは、おそらく使用したeclipseが新しすぎたために必要な過程で、本来の開発環境と同じバージョンのプラグインSDKが使えていれば不要なはず)
これをやると、何故かplugin.xmlが壊れるので、リポジトリの内容に戻す。
後は、allinone.productの「概要」タブでエクスポートのところにある「Eclipse 製品エクスポート・ウィザード」のリンクを押すと、ウィザードが立ち上がるので、「宛先」に適当なディレクトリを指定してやればビルドしてくれる。
エラーが出たら、logファイルを固めたアーカイブができるので、それの中を見ればエラーがわかる。これ、すごく見づらいんだけど、なんでeclipseのコンソールに出ないのかな。
で、私の場合見事にエラーが出る。
jp.xet.sabotter.eclipseプロジェクトの、jp.xet.sabotter.eclipse.views.SabotterViewのcreatePartControl内のwindow.setStatus()を呼び出している3箇所の日本語が化けるらしい。
twitter4jと違って、build.xmlを使っているわけではなく、eclipseがこのファイルの文字コードはUTF-8だと知っているのに何故怒られるのかわからなかったので、とりあえず英語にしてしまった。(ついでに、remainTime != 0 の判定を、remainTime > 0に変えておいた)
で、無事にビルドが通ったので、出来上がったsabotterを起動してみると、最初にtwitterのIDとパスワードを設定したところで一度だけタイムラインが取れて、その後は応答しなくなってしまった。
ログを見て見ると、twitterのAPIのrate limit にひっかかって、無限リトライをしている模様。
試行錯誤の末、以下のような修正で落ち着いた。修正は、全てjp.xet.sabotter.eclipse.manager.MiniblogServiceManager。
  • ONE_MINUTEと言う定数があって、名前からすると1分をミリ秒であらわした数値が入るものだと思うんだけど、20秒になっていたので60秒に修正。
  • コンストラクタの中で、各種サービス用のフレンドタイムラインとか、Replyとかの取得のタイマをスタートさせているんだけど、私の様にはてな俳句を使ってないと0秒間隔ではてな俳句を取りに行こうとするので、設定が0だったら次のタイマを起動しないように修正。
  • startInterval()で、ONE_MINUTE待ってから最初の取得をするようになっていたので、これを3で割って20秒にした。
これで、とりあえず動いているように見える。
良くわからなかったのが、SabotterViewのremainTime。単純に起動してから300秒のカウントダウンを表示するんだけど、いったいなんのためのものなんだろう?
これで、自宅、会社ではWindows版のsabotter、出先ではiPhone版のsabotterで快適ライフを送れています。と言うお話でした。
# しっかし、文章ばっかりで書くと激しく読みづらいですね。もっと画面ハードコピーとか、ソース差分とかふんだんに入れないと誰も読んでくれないな(笑)

eclipseのプラグイン開発のことについて何も知らないので、正しい別のやり方があったら教えてください。
ずいぶん昔に、mail2entryと言うものを入れて、いわゆるmoblogができるようにしている。
これの出所がずっとわからなくなっていたのだけれど、今回ググってみたらすぐに見つかった。
mail2entry for posting images from mail to MT - Joi Ito's Web - JP

2002年の記事だ。

で、使えるようにしてはいるものの、ちっとも投稿しやしないんだけど、たまにはと思ってiPhoneから投稿してみた。
そうしたら、メールに添付した写真が1600x1200と言うばかでかいサイズで、しかも90度回転して表示されてしまった。
どうも、iPhoneを縦にして写真を撮ると、画像としては回転した状態になって、中にOrientationの情報が入るらしい。普段使っている画像転送ソフトは自動で回転してくれるので今までは気にしていなかったようだ。

ほとんど使わないとは言え、たまには使いたいので、なんとかすることにする。
やりたいことは、exif情報を読んで適切に回転することと、サムネイル画像を作ること。
pythonを触ったことがあるのは学生の頃なので、もう10年以上昔でさっぱり覚えていない。ソースを読んでも、(と{と[の意味もわからないし、mapって?lambdaって?って感じ。
軽く触った感じでは、(がリストで、[が配列で、{が辞書のようだ。mapと言うのはJavaとかのMapではなくて、コレクションに対して繰り返し関数を適用するもので、lambdaは式を関数にするもの(?)らしい。
画像をいじるならImageMagickだろう!と思って調べたが、pythonにはあまり有名なImageMagickバインディングが存在しないらしい。
その代わり、The Python Imaging Libraryと言うものを見つけたので、これを使うことにする。
FreeBSDの場合、portsからgraphics/py-imaging をインストールすると入る。
Imageオブジェクトに対して、_getexif()を呼ぶと、Exif情報が配列で取れて、0x0112にorientationが入ってるらしい。なんで0x0112かって言うと、PIL/ExifTags.py にTAGSって言うのがあって、そこを見るとわかる。
数値が取れるんだけど、その意味は画像の写真の向きを修正する@PHP - ブックマクロ開発にを参考にした。
自分が使うだけなので、反転には対応せずに、回転だけを相手にすることにする。
回転は、Imageオブジェクトに対してrotate(角度)を呼べば良い。
サムネイルの作成は、Imageオブジェクトのthumbnail(サイズ, フィルタ)を呼ぶ。
元画像のサイズは、Imageオブジェクトのsizeプロパティが配列になっていて、size[0]が幅、size[1]が高さ。
サムネイルのサイズ決定のアルゴリズムは、[メモ]Python Image Libraryで画像のサムネイルを作る - 好きなことしかできませんが、何か?を参考にさせていただいた。
後は、settings.py でimgtemplate に画像一枚毎のテンプレートがあるんだけど、そこにはimageurlしか変数がないので、どうやってサムネイルのurlを渡すか悩んだんだけど、saveimage.save()が返す配列の要素をまた配列にしてやって、lambda式の中で配列の要素を参照してやるようにしたらうまく行った。
元のソースがGPL2だったので一応差分を公開しておくけど、savelocalには面倒なので対応していない。あと、Exifなしのjpegを渡したらたぶん落ちる。そうそう、サムネイルは長辺が400pixelとハードコードしてる。
saveimage.py
*** saveimage.py.ORIG   2009-06-25 22:57:07.000000000 +0900
--- saveimage.py        2009-06-25 23:47:34.000000000 +0900
***************
*** 1,6 ****
--- 1,8 ----
  import os
  import stat
  import time
+ from PIL import Image
+ from StringIO import StringIO

  from settings import *
  import postimage
***************
*** 13,23 ****
      #      a very good way to do this w/o leakage ;-(
      imagetemplate = "".join(['photo-',
                               time.strftime("%Y%m%d-%H%M%S", time.gmtime()),
!                              '-%s.%s'])

      for index in range(len(images)) :
          (image,imgtype) = images[ index ]
          imagefilename = imagetemplate % (index,extension[imgtype])
          if savelocal :
              # XXX: what if the user doesn't want now.jpg and a timestamp?
              imagefilepath = imagesdirpath + '/' + imagefilename
--- 15,48 ----
      #      a very good way to do this w/o leakage ;-(
      imagetemplate = "".join(['photo-',
                               time.strftime("%Y%m%d-%H%M%S", time.gmtime()),
!                              '-%s'])
!     thumbtemplate = imagetemplate + "s.%s"
!     imagetemplate = imagetemplate + ".%s"

      for index in range(len(images)) :
          (image,imgtype) = images[ index ]
          imagefilename = imagetemplate % (index,extension[imgtype])
+         thumbfilename = thumbtemplate % (index,extension[imgtype])
+
+         # rotate
+         img = Image.open(StringIO(image))
+         orientation = img._getexif()[0x0112]
+         if orientation == 3:
+             img = img.rotate(180)
+         elif orientation == 6:
+             img = img.rotate(270)
+         elif orientation == 8:
+             img = img.rotate(90)
+         out = StringIO()
+         img.save(out, imgtype)
+         image = out.getvalue()
+         # make thumbnail
+         zoom = min(400.0 / max(img.size[0], img.size[1]), 1.0)
+         img.thumbnail((int(img.size[0] * zoom), int(img.size[1] * zoom)), Image.ANTIALIAS)
+         out = StringIO()
+         img.save(out, imgtype)
+         thumbnail = out.getvalue()
+
          if savelocal :
              # XXX: what if the user doesn't want now.jpg and a timestamp?
              imagefilepath = imagesdirpath + '/' + imagefilename
***************
*** 46,50 ****
              postdata = postimage.post(imageinfo)
              imageurl = postdata['url']

!         imageurls.append(imageurl)
      return imageurls
--- 71,85 ----
              postdata = postimage.post(imageinfo)
              imageurl = postdata['url']

!             imageremotepath = imagesdirpath + '/' + thumbfilename
!             import xmlrpclib
!             imagedata = xmlrpclib.Binary(thumbnail)
!             # XXX: what if the image isn't a jpg?
!             imageinfo = { 'name' : imageremotepath,
!                           'type' : "image/jpeg",
!                           'bits' : imagedata }
!             postdata = postimage.post(imageinfo)
!             thumburl = postdata['url']
!
!         imageurls.append([imageurl, thumburl])
      return imageurls
mail2entry.py
*** mail2entry.py.ORIG  2004-03-12 16:22:29.000000000 +0900
--- mail2entry.py       2009-06-25 23:39:59.000000000 +0900
***************
*** 1,4 ****
! #! /usr/bin/env python2.2

  """Post a new MT entry from a mail message"""

--- 1,4 ----
! #! /usr/bin/env python

  """Post a new MT entry from a mail message"""

***************
*** 24,30 ****
          if images :
              imageurls = saveimage.save ( images )
              imagecontent = map ( lambda imageurl : imgtemplate % \
!                                  { 'imageurl' : imageurl }, imageurls )
          else :
              imagecontent = u''

--- 24,30 ----
          if images :
              imageurls = saveimage.save ( images )
              imagecontent = map ( lambda imageurl : imgtemplate % \
!                                  { 'imageurl' : imageurl[0], 'thumburl' : imageurl[1] }, imageurls )
          else :
              imagecontent = u''

私が使っているsettings.pyのテンプレート
# template: Template for the resulting blog entry. You probably don't
#           need to change this.
template      = "%(caption)s\n" + \
                "%(imagecontent)s"

# imgtemplate: Template for a single image entry. This gets embedded
#           within _template_ when creating the full entry. If multiple
#           images are found within an email message, _imgtemplate_ will
#           be used for each image. The concatenation of all _imgtemplate_
#           are used within _template_.
imgtemplate = "<div class=\"photo\"><a href=\"%(imageurl)s\"><img src=\"%(thumburl)s\"></a></div>"
配列が使えることがわかったから、やろうと思えばimgタグにwidthとかheightとかを付けることも可能だけど、まあいっか。
2009年6月
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        

このアーカイブについて

このページには、2009年6月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2009年5月です。

次のアーカイブは2009年7月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Powered by Movable Type 6.1.1