Hatena::Grouprubyist

takuma104のRuby/Rails日記

ツッコミ大歓迎!間違等ありましたらご指摘ください! / はてダはこちらで書いてます。

2008-05-19

Mac OSX の binary plist 形式から、XML plist 形式に一括変換

| 02:18 | Mac OSX の binary plist 形式から、XML plist 形式に一括変換 - takuma104のRuby/Rails日記 を含むブックマーク はてなブックマーク - Mac OSX の binary plist 形式から、XML plist 形式に一括変換 - takuma104のRuby/Rails日記 Mac OSX の binary plist 形式から、XML plist 形式に一括変換 - takuma104のRuby/Rails日記 のブックマークコメント

Mac OSX のリソース形式や、ちょっとした値などの保存によく使われる plist という形式のファイルがあります. たとえば OSX のリソース等の保存にもよく使われます. この plist には、形式として2種類あって、binary と xml があり、どちらも OS 的には同様に処理されます(OSから見ると等価になります).

たとえばリソースエディタの Interface Builder で編集した際には、ほぼ、binaryのplist形式になってしまいます. これだと、たとえば Subversion などでソース管理をしている際に、diff が見れず、変更点が些細な物であっても差分が見れない、という問題があります.

解決するには簡単で、OSXのコマンドの plist を使って binary から xml に変換すれば良いのですが、このコマンドは再起的に処理してくれないというのがあり、あと一括して、あるディレクトリ以下の バイナリ plist を見つけて処理してくれる物が欲しかったので、ruby でくるんでみました。

#!/usr/bin/env ruby -wKU

dir = ARGV.shift || '.'
Dir::glob(File.expand_path(dir) + "/**/*") do |fn|
  if File.ftype(fn) == 'file'
    open(fn, 'rb') do |f|
      magic = f.read(8)
      if magic == 'bplist00'
        r = system "plutil -convert xml1 '#{fn}'"
        if r
          puts "Success: #{fn}"
        else
          puts "Failed: #{fn}"
        end
      end
    end
  end
end

使い方は、適当な名前たとえば conv.rb とかで保存して

$ ruby conv.rb ./

とかすると、カレントディレクトリ以下の全バイナリ plist を検出して、すべてxml形式に変換してくれます. 変換した物は Success: とか表示されます. Subversion などに commit する際に毎回実行すると幸せになれると思います.

(おまけのoneliner)

Subversion で binary mode になっているときに、そのプロパティを一括削除

$ svn st | awk '{print $2}' | xargs svn pd svn:mime-type 
トラックバック - http://rubyist.g.hatena.ne.jp/takuma104/20080519

2008-05-07

Ruby/EventMachine での非同期サーバを Generator を使って同期サーバっぽく書く [その2]

| 03:28 | Ruby/EventMachine での非同期サーバを Generator を使って同期サーバっぽく書く [その2] - takuma104のRuby/Rails日記 を含むブックマーク はてなブックマーク - Ruby/EventMachine での非同期サーバを Generator を使って同期サーバっぽく書く [その2] - takuma104のRuby/Rails日記 Ruby/EventMachine での非同期サーバを Generator を使って同期サーバっぽく書く [その2] - takuma104のRuby/Rails日記 のブックマークコメント

昨日の続きで、さらに TCPServer での Thread-loop 内と全く同じメソッドが EventMachine でも使えるように改良してみました。

使い方は、

ruby hoge.rb -e

とか -e オプションをつけると、EventMachine を使用したサーバになり、-eをつけないと TCPServer を使用したサーバになります。

ポイントは、on_accept メソッドを両者で使い回しているところです。EventMachine のほうを socket に合わせる形にしている FilberIO クラスにほとんど秘密があります(この名前が微妙だな。なにかいい名前無いかな??)

ソースは以下です。


#!/usr/bin/env ruby -KU

require 'rubygems'
require 'eventmachine'
require 'socket'
require 'generator'
require 'optparse'

class FiberIO
  def initialize(connection)
    @recv_data = ''
    @connection = connection
    Generator.new do |g|
      @generator = g
      yield(self)
    end
  end
  
  def read(size)
    loop do
      d = pop(size)
      if d
        return d
      else
        remain = size - @recv_data.size
        @generator.yield(remain)
      end
    end
  end
  
  def write(buf)
    @connection.send_data buf
  end
  
  def push(data)
    @recv_data << data
    if @generator.next?
      @generator.next
    end
  end
  
  def close
    @connection.close_connection_after_writing
  end
  
  def peeraddr
    addr = Socket.unpack_sockaddr_in(@connection.get_peername)
    ["AF_INET", addr[0], addr[1], addr[1]]
  end
  
private
  def pop(size)
    if @recv_data.size >= size
      r = @recv_data[0..size-1]
      @recv_data = @recv_data[size..-1]
      r
    else
      nil
    end
  end
end

class GeneratorSampleConnection < EventMachine::Connection
  def initialize(arg)
    @proc = arg
  end
  
  def post_init
    @io = FiberIO.new(self) {|io| @proc.call(io)}
  end

  def receive_data(data)
    @io.push(data)
  end
end

def on_accept(sock)
  puts "Connected from %s:%d" % [sock.peeraddr[3],sock.peeraddr[1]]
  puts sock.read(10)
  puts "---"
  puts sock.read(20)
  puts "---END"
  sock.write ">> OK\r\n"
  sock.close
end

def loop_eventmachine(host, port)
  EventMachine::run do
    EventMachine.start_server(host, port, GeneratorSampleConnection, Proc.new do |io|
      on_accept(io)
    end)
    puts "Now accepting connections on address port #{port} (EventMachine)"
  end
end

def loop_tcpserver(port)
  gs = TCPServer.open(port)
  puts "Now accepting connections on address port #{port} (TCPServer)"
  loop do
    Thread.start(gs.accept) do |sock|
      on_accept(sock)
    end
  end
end

if $0 == __FILE__
  host = "127.0.0.1"
  options = {:p=>3793,:e=>false}
  
  opt = OptionParser.new
  opt.on('-p VAL') {|v| options[:p] = v.to_i }
  opt.on('-e') {|v| options[:e] = true }
  opt.parse!(ARGV)

  if ARGV.size != 0
    puts "-e      : use EventMachine (default=TCPServer)"
    puts "-p port : listening port (default=3793)"
  end
  
  if options[:e]
    loop_eventmachine(host, options[:p])
  else
    loop_tcpserver(options[:p])
  end
end
トラックバック - http://rubyist.g.hatena.ne.jp/takuma104/20080507

2008-05-06

Ruby/EventMachine での非同期サーバを Generator を使って同期サーバっぽく書く

| 04:24 | Ruby/EventMachine での非同期サーバを Generator を使って同期サーバっぽく書く - takuma104のRuby/Rails日記 を含むブックマーク はてなブックマーク - Ruby/EventMachine での非同期サーバを Generator を使って同期サーバっぽく書く - takuma104のRuby/Rails日記 Ruby/EventMachine での非同期サーバを Generator を使って同期サーバっぽく書く - takuma104のRuby/Rails日記 のブックマークコメント

Ruby/EventMachine という、非同期サーバがお手軽に書けるライブラリを教えてもらいました。ほかにも、Rev というのもあるようですが、今回試したのは、EventMachine のほう。Ruby/EventMachine は、現在の実装だと select(2) に落ちているようです。Rev はすでに epoll(2) 実装になっているようです。


まず、EventMachine のインストールは、

$ sudo gem install eventmachine

とかでいけます。ネイティブエクステンションとかも勝手にコンパイルとかしてくれます。gems 便利!

簡単な使い方は、Young risk taker.: [Ruby] Ruby/EventMachineでネットワークプログラミング などがあります。このサンプルがちょっと間違っててそのまま動かないので、修正して必要最小限にしたのは以下のようになります。

require 'rubygems'
require 'eventmachine'

class CharacterCount < EventMachine::Connection
  def post_init
    puts "Received a new connection"
  end

  def receive_data(data)
    puts "Received data: #{data}"
    send_data "#{data.length} (characters)\r\n"
    close_connection_after_writing
  end
end

EventMachine::run do
  host, port = "127.0.0.1", 3793
  EventMachine.start_server(host, port, CharacterCount)
  puts "Now accepting connections on address #{host}, port #{port}"
end

これだけで 127.0.0.1 の Port 3793 で、listen を開始して、Ctrl-C とかするまで動き続けるサーバーが出来ます。telnet とかで、3793 に接続して文字入力して改行を押すと、文字数とかが返ってくると思います。

CharacterCount クラスが、EventMachine::Connection クラスを継承していて、EventMachine.start_server にて登録されています。この CharacterCount のインスタンスが、TCPコネクションが張られるたびに自動生成されるようです。EventMachine::Connection のメソッドは、

  • post_init が、コネクション貼られたときに呼び出される
  • receive_data がデータを受信した時に呼び出される
  • データ送信は、send_data です。

のようになります。で、例えば同期サーバ (TCPServer) で以下のような処理を書きたい場合に、

require "socket"

port = 3793
gs = TCPServer.open(port)

loop do
  Thread.start(gs.accept) do |s|
    puts "Now accepting connections on address port #{port}"
    puts s.read(10)
    puts "---"
    puts s.read(20)
    puts "---END"
    s.write(">> OK\r\n")
    s.close
  end
end

これをそのままEventMachineのような非同期サーバにそのまま変更しようと思うと、結構大変です。非同期サーバの作り方として、よくありがちなのが、状態遷移マシンを作って、例えばある状態の時は、10バイトまで読み続けて、ある状態になったら、応答を返してとかですかね。でもそれだと、このまま同期サーバーを非同期サーバに移植しようと思うと大変で気が滅入ります。

ということで、Ruby1.8におけるFiber (Co-routine, microthreadなどの呼び名がありますが、基本思想的には同じものです) 機構の代替えになるような Generator を使って、同期サーバーっぽく書けるようにしてみました。まだまだ改善余地があると思いますが、とりあえず、これで動作しています。

reader_setup の引数ブロックが、Generator 内部になっているので、reader メソッドで、上記の同期サーバーとほぼ同じ書き方が出来ています。*1

ちなみに、Generator は、遅いそうなので、これではあまり高速という話にはならないかもしれません。(ちゃんと計測したりとかはまだしてません)Ruby 1.9 の Fiber でも同様なことは書けると思いますので、速さを求めるなら、Ruby 1.9 の Fiber でも良いかもしれません。

#!/usr/bin/env ruby -KU

require 'rubygems'
require 'eventmachine'
require 'socket'
require 'generator'

class GeneratorSampleConnection < EventMachine::Connection
  def initialize
    @recv_data = ''
    @reader_generator = nil
  end
  
  def post_init
    addr = Socket.unpack_sockaddr_in(get_peername)
    puts "Connected from %s:%d" % [addr[1],addr[0]]
    reader_setup { reader }
  end

  def receive_data(data)
    push(data)
  end

private
  def reader
    puts read_bytes(10)
    puts "---"
    puts read_bytes(20)
    puts "---END"
    send_data ">> OK\r\n"
    close_connection_after_writing
  end

private
  def reader_setup
    Generator.new do |g|
      @reader_generator = g
      yield
    end
  end

  def push(data)
    @recv_data << data
    if @reader_generator.next?
      @reader_generator.next
    end
  end
  
  def pop(size)
    if @recv_data.size >= size
      r = @recv_data[0..size-1]
      @recv_data = @recv_data[size..-1]
      r
    else
      nil
    end
  end  

  def read_bytes(size)
    loop do
      d = pop(size)
      if d
        return d
      else
        remain = size - @recv_data.size
        @reader_generator.yield(remain)
      end
    end
  end
end

EventMachine::run do
  host, port = "127.0.0.1", 3793
  EventMachine.start_server(host, port, GeneratorSampleConnection)
  puts "Now accepting connections on address #{host}, port #{port}"
end

*1:ちなみにEventMachineはすべてシングルスレッドで動作するので、あまりRubyのグリーンスレッドも使いたくなかった、というのもあります

takuma104takuma1042008/05/08 03:31EventMachineとはなにか、というところから解説を追記しました。

トラックバック - http://rubyist.g.hatena.ne.jp/takuma104/20080506

2008-05-05

RubyでDP1のRAW画像からサムネイルとかのJPEGを抜き出すコードを書いた

| 01:54 | RubyでDP1のRAW画像からサムネイルとかのJPEGを抜き出すコードを書いた - takuma104のRuby/Rails日記 を含むブックマーク はてなブックマーク - RubyでDP1のRAW画像からサムネイルとかのJPEGを抜き出すコードを書いた - takuma104のRuby/Rails日記 RubyでDP1のRAW画像からサムネイルとかのJPEGを抜き出すコードを書いた - takuma104のRuby/Rails日記 のブックマークコメント

これまたニッチwなのを書きました。SIGMA DP1 というすばらしい絵が撮れるデジカメがあるのですが、この RAW 画像が、なかなか普通の RAW 現像ソフトとかで対応してもらえなくて結構ファイルの取り扱いに困ります。DP1 付属の RAW 現像ソフトはよく出来ているのですが、やっぱり OS のプレビュー使いたいなとかで、JPEG にしたいとかがあると思います。そんな時 RAW の中に JPEG がそのまま格納(オリジナルサイズのも!)されているのを知ったので、それを抜き出せないかと。ちょっと書いてみたらまあまあシンプルだったので貼ってみます。

DP1 の RAW フォーマットである X3F の解説は、こちらから。作者さんthanks!

http://anotherm8.exblog.jp/7838817/

一番下にコードを貼りました。使い方は x3f.rb とかで保存して

 $ ruby x3f.rb SDIM0000.X3F

とかで、同じディレクトリに SDIM0000.X3F.thumb.jpg と SDIM0000.X3F.jpg の2ファイルができます。

.thumb.jpg の方がサムネイル用 (221x147) で、 .jpg のほうが、元サイズとおなじ 2640 x 1780 のサイズの JPEG になります。.jpg のほうは、Flickrとかにまず上げる際とかに、そのまま使えそうですね。もちろんちゃんと現像した方が全然良いですが(たぶんAuto現像でもこれよりいい絵が出来るんじゃないかと思いますが)、とりあえず見れると言えば見れる絵です。

ちなみに、引数にディレクトリを指定すると、それ以下の全フォルダ内のX3Fをスキャンして全部このJPEGを作成します。

module X3F
  class Parser
    def initialize(fn)
      @x3f = open(fn, 'rb')
      @dic_entries = []
    end
    
    def close
      @x3f.close
    end
    
    def parse_dictionary
      len = File.size(@x3f)
      doff = read32u(len - 4)
      raise "Premature end of file" if doff > len
      raise 'SECd not found.' if read4B(doff) != 'SECd'
      raise 'SECd version is not 0x00020000.' if read32u(doff+4) != 0x00020000
      entries = read32u(doff+8)
      entries.times do |i|
        pt = doff + 12 + i * 4 * 3
        @dic_entries <<  {:offset=>read32u(pt), :size=>read32u(pt+4), :tag=>read4B(pt+8)}
      end
    end

    def save_jpeg(fn, thumbnail=false)
      e = nil
      if thumbnail
        e = @dic_entries[4]
      else
        e = @dic_entries[0]
      end

      raise 'JPEG not found' if e[:tag] != 'IMA2'
      open(fn, 'wb') do |f|
        o = e[:offset]; s = e[:size]
        o += 28; s -= 28 # skip head 28 bytes
        @x3f.pos = o
        f.write(@x3f.read(s))
      end
    end

private
    def read4B(offset)
      @x3f.pos = offset
      @x3f.read(4)
    end
    
    def read32u(offset)
      read4B(offset).unpack('V1')[0]
    end
  end
end

if $0 == __FILE__
  def pickjpegs(path)
    x3f = X3F::Parser.new(path)
    x3f.parse_dictionary
    x3f.save_jpeg(path + '.jpg', false) # for full size JPEG
    x3f.save_jpeg(path + '.thumb.jpg', true) # for thumbnail JPEG
    x3f.close
  end
  
  path = ARGV.shift
  case File.ftype(path)
  when 'directory'
    Dir::glob(File.expand_path(path) + "/**/*.X3F") do |fn|
      puts fn
      pickjpegs(fn)
    end
  when 'file'
    pickjpegs(path)
  else
    puts "usage: ruby x3f.rb x3f_filename.x3f"
  end
end

きたしきたし2008/05/11 12:41すばらしい!
実は同じことをやろうとして昨日半日X3FファイルをHexエディタで読んでいました。。。
既にやっておられる方がいらっしゃったとは。

jpeg以外の部分のデータ形式が早く解読(or 公開)されるといいですね。

takuma104takuma1042008/05/11 15:34ありがとうございます。Foveonデータの方ですが、dcraw.cではまだ対応されてないみたいですね...
http://cybercom.net/~dcoffin/dcraw/

SIGMA社のSD9/SD10/SD14には対応しているので、そのうち対応するのかな?作者の人に聞いてみようかと思います.

takuma104takuma1042008/05/11 22:37さきほど作者のdcoffinさんに、サンプルのRAWファイルとともに対応を聞いておきました。何かレスがあったらまたここに書き込みます。

トラックバック - http://rubyist.g.hatena.ne.jp/takuma104/20080505

2008-03-31

ほかのOSで動いてるのに、なぜかWindowsだけで動かないときのありがちなミス

| 02:16 | ほかのOSで動いてるのに、なぜかWindowsだけで動かないときのありがちなミス - takuma104のRuby/Rails日記 を含むブックマーク はてなブックマーク - ほかのOSで動いてるのに、なぜかWindowsだけで動かないときのありがちなミス - takuma104のRuby/Rails日記 ほかのOSで動いてるのに、なぜかWindowsだけで動かないときのありがちなミス - takuma104のRuby/Rails日記 のブックマークコメント

つうかこんなの常識とか突っ込まれそうですが。

open('hoge') do |f|
  f.read
end

とかして、readした内容が化ける。これは、Windowsだと、fopen(3)がデフォルトで、テキストモードなため。ほかのOSは普通バイナリモード。

解決策は、

open('hoge','rb') do |f|
  f.read
end

とかすればOK。

あとは、

open('hoge') do |f|
  f.binmode
  f.read
end

とかでもOKみたい。こっちのが汎用的??

なかだなかだ2008/04/02 11:29openしたままでunlinkしようとしたりとか。

takuma104takuma1042008/04/02 12:24あー、あんまりお行儀良くなかったですね。
ブロック構文にしてみました。

moromoro2008/04/02 13:14Symbol#to_procがあると File.open('hoge','rb',&:read)と書けてカッコいいということを某所で教えてもらいました。

takuma104takuma1042008/04/02 13:49おお、これみたいな感じですね。確かにスマート。
http://wota.jp/ac/?date=20060309#p04

takuma104takuma1042008/04/02 13:52こっちのがええかな?
http://nov.tdiary.net/20060317.html

トラックバック - http://rubyist.g.hatena.ne.jp/takuma104/20080331